From 10414e4360b69301d7b066a2b194b39bee1ce5ad Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Fri, 20 Feb 2015 14:31:51 +0100 Subject: [PATCH] Rewrite/refactor of the full code base --- .gitignore | 14 ++ .rspec | 1 + .rubocop.yml | 33 +++ .travis.yml | 27 +++ CHANGELOG.md | 7 + Gemfile | 40 +++- Gemfile.lock | 26 --- Guardfile | 34 ++++ LICENSE | 22 ++ MIT-LICENSE | 20 -- README.md | 85 +++++--- Rakefile | 41 ++++ lib/po_to_json.rb | 394 +++++++++++++++++++++++++----------- lib/po_to_json/version.rb | 39 ++++ po_to_json.gemspec | 71 ++++++- spec/{ => fixtures}/test.po | 6 +- spec/po_to_json_spec.rb | 224 ++++++++++++++++---- spec/spec_helper.rb | 56 ++++- 18 files changed, 893 insertions(+), 247 deletions(-) create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 .rubocop.yml create mode 100644 .travis.yml create mode 100644 CHANGELOG.md delete mode 100644 Gemfile.lock create mode 100644 Guardfile create mode 100644 LICENSE delete mode 100644 MIT-LICENSE create mode 100644 Rakefile create mode 100644 lib/po_to_json/version.rb rename spec/{ => fixtures}/test.po (91%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c852aae --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.DS_Store + +/.idea +/.yardoc +/.bundle +/.vagrant + +/coverage +/pkg +/doc +/vendor + +/Gemfile.lock +/Gemfile.local diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..660778b --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--colour --format documentation diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..0ecb051 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,33 @@ +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/AlignParameters: + EnforcedStyle: with_fixed_indentation + +Style/Documentation: + Enabled: false + +Style/ModuleFunction: + Enabled: false + +Style/WordArray: + Enabled: false + +Style/PerlBackrefs: + Enabled: false + +Style/ElseAlignment: + Enabled: false + +Style/Next: + Enabled: false + +Metrics/MethodLength: + CountComments: false + Max: 30 + +Metrics/ClassLength: + Max: 300 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0d615fe --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: ruby + +rvm: + - 2.1.0 + - 2.0.0 + - 1.9.3 + +env: + - JSON_VERSION=1.6.8 + - JSON_VERSION=1.7.7 + - JSON_VERSION=1.8.2 + +notifications: + email: + recipients: + - thomas@webhippie.de + on_success: change + on_failure: always + + irc: + channels: + - "irc.freenode.net#webhippie" + template: + - "%{repository_slug} (%{commit}) : %{message} by %{author}" + - "You can see details at %{build_url}" + on_success: change + on_failure: always diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5f90cfd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [0.1.0](https://github.com/webhippie/po_to_json/releases/tag/v0.1.0) - 2015-02-19 + +* Transfer from github.com/nubis/po_to_json +* Added TravisCI, Rubocop and Coveralls +* Updated structure to my opinionated gem style diff --git a/Gemfile b/Gemfile index be4e8db..919856d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,43 @@ -source "http://rubygems.org" +# +# Copyright (c) 2012-2015 Dropmysite.com +# Copyright (c) 2015 Webhippie +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +source "http://rubygems.org" gemspec +gem "json", "~> #{ENV["JSON_VERSION"]}" if ENV["JSON_VERSION"] + group :development do - gem 'rspec' + gem "guard", require: false + gem "guard-rubocop", require: false + gem "guard-rspec", require: false end + +group :test do + gem "simplecov", require: false + gem "coveralls", require: false + gem "codeclimate-test-reporter", require: false + gem "rubocop", require: false +end + +instance_eval(File.read("Gemfile.local")) if File.exist? "Gemfile.local" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 0e146e0..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,26 +0,0 @@ -PATH - remote: . - specs: - po_to_json (0.0.4) - json - -GEM - remote: http://rubygems.org/ - specs: - diff-lcs (1.1.3) - json (1.7.6) - rspec (2.12.0) - rspec-core (~> 2.12.0) - rspec-expectations (~> 2.12.0) - rspec-mocks (~> 2.12.0) - rspec-core (2.12.2) - rspec-expectations (2.12.1) - diff-lcs (~> 1.1.3) - rspec-mocks (2.12.1) - -PLATFORMS - ruby - -DEPENDENCIES - po_to_json! - rspec diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..68cb1dd --- /dev/null +++ b/Guardfile @@ -0,0 +1,34 @@ +# +# Copyright (c) 2012-2015 Dropmysite.com +# Copyright (c) 2015 Webhippie +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +guard :rspec, cmd: "bundle exec rspec", all_on_start: true do + watch(/^spec\/.+_spec\.rb$/) + watch(/^lib\/(.+)\.rb$/) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch("spec/spec_helper.rb") { "spec" } +end + +guard :rubocop, all_on_start: true do + watch(/.+\.rb$/) + watch(/(?:.+\/)?\.rubocop\.yml$/) { |m| File.dirname(m[0]) } +end diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d326e24 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2012-2015 Dropmysite.com +Copyright (c) 2015 Webhippie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MIT-LICENSE b/MIT-LICENSE deleted file mode 100644 index 67bf5bb..0000000 --- a/MIT-LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright 2012 Dropmysite.com. https://dropmyemail.com - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 827b900..4a4eea0 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,87 @@ -## po_to_json +# PoToJson -Convert gettext PO files to json to use in your javascript app, based po2json.pl (by DuckDuckGo, Inc. http://duckduckgo.com/, Torsten Raudssus . +[![Gem Version](http://img.shields.io/gem/v/po_to_json.svg)](https://rubygems.org/gems/po_to_json) +[![Build Status](https://secure.travis-ci.org/webhippie/po_to_json.svg)](https://travis-ci.org/webhippie/po_to_json) +[![Code Climate](https://codeclimate.com/github/webhippie/po_to_json.svg)](https://codeclimate.com/github/webhippie/po_to_json) +[![Test Coverage](https://codeclimate.com/github/webhippie/po_to_json/badges/coverage.svg)](https://codeclimate.com/github/webhippie/po_to_json) +[![Dependency Status](https://gemnasium.com/webhippie/po_to_json.svg)](https://gemnasium.com/webhippie/po_to_json) -Ideally you'll use this on a rake task that creates json versions of your po files, which can later be used from javascript -with Jed ( http://slexaxton.github.com/Jed/ ) +Convert gettext PO files to JSON to use in your javascript app, based on +po2json.pl by [DuckDuckGo, Inc.](http://duckduckgo.com/). Ideally you'll use +this on a Rake task that creates JSON versions of your PO files, which can +later be used from javascript with [Jed](http://slexaxton.github.io/Jed/) -## Installing -Via rubygems: -```ruby -gem install po_to_json -``` +## Versions + +This gem is tested on the following versions automated through TravisCI: + +* Ruby + * 1.9.3 + * 2.0.0 + * 2.1.0 +* Json + * 1.6.8 + * 1.7.7 + * 1.8.2 + + +## Installation -In your gemfile: ```ruby -gem 'po_to_json' +gem "po_to_json", "~> 0.1.0" ``` + ## Usage -Most common use would be to generate a Jed ready javascript file. For example, in a Rails 3 project: +Most common use would be to generate a Jed ready javascript file. For example, +in a Rails project: ```ruby -require 'po_to_json' -json_string = PoToJson.new("#{Rails.root}/locale/es/app.po").generate_for_jed('es') -File.open("#{Rails.root}/app/assets/javascripts/locale/es/app.js",'w').write(json_string) +require "po_to_json" + +json = PoToJson.new(Rails.root.join("locale", "de", "app.po").generate_for_jed("de") +Rails.root.join("app", "assets", "javascripts", "locale", "de", "app.js").write(json) ``` -If you need a pretty json, add `:pretty => true` to `generate_for_jed`, like +If you need a pretty json, add `pretty: true` to `generate_for_jed`, like the +following example: ```ruby -json_string = PoToJson.new("#{Rails.root}/locale/es/app.po").generate_for_jed('es', :pretty => true) +require "po_to_json" + +json = PoToJson.new(Rails.root.join("locale", "de", "app.po").generate_for_jed("de", pretty: true) +Rails.root.join("app", "assets", "javascripts", "locale", "de", "app.js").write(json) ``` -The javascript file generated has a global 'locales' object with an attribute corresponding to the generated language: +The javascript file generated has a global "locales" object with an attribute +corresponding to the generated language: ```javascript -i18n = new Jed(locales['es']) -i18n.gettext('Hello World') // Should evaluate to 'Hola Mundo' +i18n = new Jed(locales["de"]) +i18n.gettext("Hello World") // Should evaluate to "Hallo Welt" ``` -## Maintainers -* eromirou (https://github.com/eromirou) -* Nubis (https://github.com/nubis) +## Contributing + +Fork -> Patch -> Spec -> Push -> Pull Request + + +## Authors + +* [Thomas Boerger](https://github.com/tboerger) +* [Nubis](https://github.com/nubis) +* [Other contributors](https://github.com/webhippie/po_to_json/graphs/contributors) + ## License -MIT License. Copyright 2012 Dropmysite.com. https://dropmyemail.com +MIT + + +## Copyright + +Copyright (c) 2012-2015 Dropmysite.com +Copyright (c) 2015 Webhippie diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..3b71b03 --- /dev/null +++ b/Rakefile @@ -0,0 +1,41 @@ +# +# Copyright (c) 2012-2015 Dropmysite.com +# Copyright (c) 2015 Webhippie +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +begin + require "bundler" + Bundler::GemHelper.install_tasks +rescue LoadError + warn "Failed to load bundler tasks" +end + +require "rubocop/rake_task" +RuboCop::RakeTask.new + +require "yard" +YARD::Rake::YardocTask.new + +require "rspec/core/rake_task" +RSpec::Core::RakeTask.new(:spec) + +task default: [:spec, :rubocop] diff --git a/lib/po_to_json.rb b/lib/po_to_json.rb index ef856c4..2eb82ad 100644 --- a/lib/po_to_json.rb +++ b/lib/po_to_json.rb @@ -1,172 +1,328 @@ -require 'json' +# +# Copyright (c) 2012-2015 Dropmysite.com +# Copyright (c) 2015 Webhippie +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) + +if File.exist? ENV["BUNDLE_GEMFILE"] + require "bundler" + Bundler.setup(:default) +else + gem "json", version: ">= 1.6.0" +end + +require "json" class PoToJson + autoload :Version, File.expand_path("../po_to_json/version", __FILE__) + + attr_accessor :files + attr_accessor :glue + attr_accessor :options + attr_accessor :errors + attr_accessor :values + attr_accessor :buffer + attr_accessor :lastkey + attr_accessor :trans + + def initialize(files, glue = "|") + @files = files + @glue = glue + end + def generate_for_jed(language, overwrite = {}) + @options = parse_options(overwrite.merge(language: language)) + @parsed ||= inject_meta(parse_document) - # Gettext translations may be contextualized like so User|name - # The default 'GLUE' in rails gettext is '|' so we use that here too. - def initialize(path_to_po, context_glue = '|') - # Gettext translations may be contextualized like so User|name - # The default 'GLUE' in rails gettext is '|' so we use that here too. - @context_glue = context_glue - @path_to_po = path_to_po + generated = build_json_for(build_jed_for(@parsed)) + + [ + "var #{@options[:variable]} = #{@options[:variable]} || {};", + "#{@options[:variable]}['#{@options[:language]}'] = #{generated};" + ].join(" ") end + def generate_for_json(language, overwrite = {}) + @options = parse_options(overwrite.merge(language: language)) + @parsed ||= inject_meta(parse_document) - # Generates a jed-compatible js file from the current PO. - # This include adding some wrapping structure to the translations and - # making sure the minimum headers required by Jed are provided. - # Jed is a js gettext library ( http://slexaxton.github.com/Jed/ ) - # The generated file leaves the generated json inside a global locales - # object which you can use to instatiate Jed: - # >>> i18n = new Jed(locales['es']) - # >>> i18n.gettext('Hello World') - # => 'Hola Mundo' - def generate_for_jed(language_code, opts={}) - @parsed ||= self.parse + generated = build_json_for(build_json_for(@parsed)) - @parsed['']['lang'] = language_code - @parsed['']['domain'] = 'app' - @parsed['']['plural_forms'] ||= @parsed['']['Plural-Forms'] + fail "Not implemented yet, current value is #{generated}!" + end - jed_json = { - :domain => 'app', - :locale_data => { :app => @parsed } - } + def parse_document + reset_buffer + reset_result - if opts[:pretty] - "var locales = locales || {}; locales['#{language_code}'] = #{JSON.pretty_generate(jed_json)};" - else - "var locales = locales || {}; locales['#{language_code}'] = #{JSON.generate(jed_json)};" + File.foreach(files) do |line| + matches_values_for(line.chomp) end + + flush_buffer + parse_header + + values end + def flush_buffer + return unless buffer[:msgid] - # Messages in a PO file are defined as a series of 'key value' pairs, - # values may span over more than one line. Each key defines an attribute - # of the message, like message id, context, pluralization options, etc. - # Each message is separated by a blank line. - # The parser reads attributes until it finds an empty line, at that point - # it saves the attributes read so far into a message and stores it in a hash - # to be later turned into a json object. - def parse - @parsed_values = {} - @buffer = {} - @last_key_type = "" - @errors = [] - File.foreach(@path_to_po) do |line| - line = line.chomp - case line - # Empty lines means we have parsed one full message - # so far and need to flush the buffer - when /^$/ then flush_buffer + build_trans + assign_trans - # These are the po file comments - # The second '#' in the following regex is in square brackets - # b/c it messed up my syntax highlighter, no other reason. - when /^(#[^~]|[#]$)/ then next + reset_buffer + end - when /^(?:#~ )?msgctxt\s+(.*)/ then add_to_buffer($1, :msgctxt) + def parse_header + return if reject_header - when /^(?:#~ )?msgid\s+(.*)/ then add_to_buffer($1, :msgid) + values[""][1].split("\\n").each do |line| + next if line.empty? + build_header_for(line) + end - when /^(?:#~ )?msgid_plural\s+(.*)/ then add_to_buffer($1, :msgid_plural) + values[""] = headers + end - when /^(?:#~ )?msgstr\s+(.*)/ then add_to_buffer($1, :msgstr_0) + def reject_header + if values[""].nil? || values[""][1].nil? + values[""] = {} + true + else + false + end + end - when /^(?:#~ )?msgstr\[0\]\s+(.*)/ then add_to_buffer($1, :msgstr_0) + protected - when /^(?:#~ )?msgstr\[(\d+)\]\s+(.*)/ then add_to_buffer($2, "msgstr_#{$1}".to_sym) + def trans + @trans ||= [] + end - when /^(?:#~ )?"/ then add_to_buffer(line) + def errors + @errors ||= [] + end - else - @errors << ["Strange line #{line}"] - end - end + def values + @values ||= {} + end - # In case the file did not end with a newline, we want to flush the buffer - # one more time to write the last message too. - flush_buffer + def buffer + @buffer ||= {} + end - # This will turn the header values into a friendlier json structure too. - parse_header_lines + def headers + @headers ||= {} + end - return @parsed_values + def lastkey + @lastkey ||= "" end - def flush_buffer - return unless @buffer[:msgid] + def reset_result + @values = {} + @errors = [] + end + + def reset_buffer + @buffer = {} + @trans = [] + @lastkey = "" + end + + def detect_ctxt + msgctxt = buffer[:msgctxt] + msgid = buffer[:msgid] - msg_ctxt_id = if @buffer[:msgctxt] && @buffer[:msgctxt].size > 0 - @buffer[:msgctxt] + @context_glue + @buffer[:msgid] + if msgctxt && msgctxt.size > 0 + [msgctxt, glue, msgid].join("") else - @buffer[:msgid] + msgid end + end - msgid_plural = if @buffer[:msgid_plural] && @buffer[:msgid_plural].size > 0 - @buffer[:msgid_plural] - end + def detect_plural + plural = buffer[:msgid_plural] + plural if plural && plural.size > 0 + end - # find msgstr_* translations and push them on - trans = [] - @buffer.each do |key, string| - trans[$1.to_i] = string if key.to_s =~ /^msgstr_(\d+)/ + def build_trans + buffer.each do |key, string| + trans[$1.to_i] = string if key.to_s.match(/^msgstr_(\d+)/) end - trans.unshift(msgid_plural) - @parsed_values[msg_ctxt_id] = trans if trans.size > 1 + trans.unshift(detect_plural) + end - @buffer = {} - @last_key_type = "" + def assign_trans + values[detect_ctxt] = trans if trans.size > 1 end - # The buffer keeps key/value pairs for all the config options - # defined on an entry, including the message id and value. - # For continued lines, the key_type can be empty, so the last - # used key type will be used. Also, the content will be appended - # to the last key rather than assigned. - def add_to_buffer(value, key_type = nil) + def push_buffer(value, key = nil) value = $1 if value =~ /^"(.*)"/ - value.gsub(/\\"/, '"') + value.gsub(/\\"/, "\"") + + if key.nil? + buffer[lastkey] = [ + buffer[lastkey], + value + ].join("") + else + buffer[key] = value + @lastkey = key + end + end + + def parse_options(options) + defaults = { + pretty: false, + domain: "app", + variable: "locales" + } + + defaults.merge(options) + end + + def inject_meta(hash) + hash[""]["lang"] ||= options[:language] + hash[""]["domain"] ||= options[:domain] + hash[""]["plural_forms"] ||= hash[""]["Plural-Forms"] - if key_type.nil? - @buffer[@last_key_type] += value + hash + end + + def build_header_for(line) + if line =~ /(.*?):(.*)/ + key, value = $1, $2 + + if headers.key? key + errors.push "Duplicate header: #{line}" + elsif key =~ /#-#-#-#-#/ + errors.push "Marked header: #{line}" + else + headers[key] = value.strip + end else - @buffer[key_type] = value - @last_key_type = key_type + errors.push "Malformed header: #{line}" end end - # The parsed values are expected to have an empty string key ("") - # which corresponds to the po file metadata defined in it's header. - # the header has information like the translator, the pluralization, etc. - # Each header line is subseqently parsed into a more usable hash. - def parse_header_lines - if @parsed_values[""].nil? || @parsed_values[""][1].nil? - @parsed_values[""] = {} - return + def build_json_for(hash) + if options[:pretty] + JSON.pretty_generate(hash) + else + JSON.generate(hash) end + end + + def build_jed_for(hash) + { + domain: options[:domain], + locale_data: { + options[:domain] => hash + } + } + end - headers = {} - # Heading lines may have escaped newlines in them - @parsed_values[""][1].split(/\\n/).each do |line| - next if line.size == 0 - - if line =~ /(.*?):(.*)/ - key, value = $1, $2 - if headers[key] && headers[key].size > 0 - @errors << ["Duplicate header line: #{line}"] - elsif key =~ /#-#-#-#-#/ - @errors << ["Marker in header line: #{line}"] + def matches_values_for(line) + return if generic_rejects? line + return if generic_detects? line + + return if iterate_detects_for(line) + + errors.push "Strange line #{line}" + end + + def iterate_detects_for(line) + specific_detects.each do |detect| + match = line.match(detect[:regex]) + + if match + if detect[:index] + push_buffer(match[detect[:index]], detect[:key].call(match)) else - headers[key] = value + push_buffer(line) end - else - @errors << ["Malformed header #{line}"] + + return true end end - @parsed_values[""] = headers + false + end + + def generic_rejects?(line) + if line.match(/^$/) || line.match(/^(#[^~]|[#]$)/) + flush_buffer && true + else + false + end + end + + def generic_detects?(line) + match = line.match(/^(?:#~ )?msgctxt\s+(.*)/) + + if match + push_buffer( + match[1], + :msgctxt + ) + + return true + end + + false + end + + def specific_detects + [{ + regex: /^(?:#~ )?msgctxt\s+(.*)/, + index: 1, + key: proc { :msgctxt } + }, { + regex: /^(?:#~ )?msgid\s+(.*)/, + index: 1, + key: proc { :msgid } + }, { + regex: /^(?:#~ )?msgid_plural\s+(.*)/, + index: 1, + key: proc { :msgid_plural } + }, { + regex: /^(?:#~ )?msgstr\s+(.*)/, + index: 1, + key: proc { :msgstr_0 } + }, { + regex: /^(?:#~ )?msgstr\[0\]\s+(.*)/, + index: 1, + key: proc { :msgstr_0 } + }, { + regex: /^(?:#~ )?msgstr\[(\d+)\]\s+(.*)/, + index: 2, + key: proc { |m| "msgstr_#{m[1]}".to_sym } + }, { + regex: /^(?:#~ )?"/, + index: nil + }] end end diff --git a/lib/po_to_json/version.rb b/lib/po_to_json/version.rb new file mode 100644 index 0000000..1d1e23c --- /dev/null +++ b/lib/po_to_json/version.rb @@ -0,0 +1,39 @@ +# +# Copyright (c) 2012-2015 Dropmysite.com +# Copyright (c) 2015 Webhippie +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +class PoToJson + class Version + MAJOR = 0 + MINOR = 1 + PATCH = 0 + + PRE = nil + + class << self + def to_s + [MAJOR, MINOR, PATCH, PRE].compact.join(".") + end + end + end +end diff --git a/po_to_json.gemspec b/po_to_json.gemspec index a5cbfc0..5499b61 100644 --- a/po_to_json.gemspec +++ b/po_to_json.gemspec @@ -1,12 +1,63 @@ +# +# Copyright (c) 2012-2015 Dropmysite.com +# Copyright (c) 2015 Webhippie +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +$LOAD_PATH.push File.expand_path("../lib", __FILE__) +require "po_to_json/version" + Gem::Specification.new do |s| - s.name = 'po_to_json' - s.version = '0.0.7' - s.date = '2013-05-02' - s.summary = 'Convert gettext PO files to json' - s.description = 'Convert gettext PO files to json to use in your javascript app, based po2json.pl (by DuckDuckGo, Inc. http://duckduckgo.com/, Torsten Raudssus .)' - s.authors = ["Nubis", "eromirou"] - s.email = 'nubis@woobiz.com.ar' - s.files = Dir["lib/**/*"] + ["README.md", "MIT-LICENSE"] - s.homepage = "https://github.com/nubis/po_to_json" - s.add_dependency 'json' + s.name = "po_to_json" + s.version = PoToJson::Version + s.date = Time.now.strftime("%F") + + s.authors = ["Thomas Boerger", "Nubis"] + s.email = ["thomas@webhippie.de", "nubis@woobiz.com.ar"] + + s.summary = <<-EOF + Convert gettext PO files to JSON + EOF + + s.description = <<-EOF + Convert gettext PO files to JSON objects so that you can use it in your + application. + EOF + + s.homepage = "https://github.com/webhippie/po_to_json" + s.license = "MIT" + + s.files = ["CHANGELOG.md", "README.md", "LICENSE"] + s.files += Dir.glob("lib/**/*") + s.files += Dir.glob("bin/**/*") + + s.test_files = Dir.glob("spec/**/*") + + s.executables = [] + s.require_paths = ["lib"] + + s.add_development_dependency "bundler" + s.add_development_dependency "rake" + s.add_development_dependency "yard" + s.add_development_dependency "rspec" + + s.add_dependency "json", ">= 1.6.0" end diff --git a/spec/test.po b/spec/fixtures/test.po similarity index 91% rename from spec/test.po rename to spec/fixtures/test.po index 51968d8..f995331 100644 --- a/spec/test.po +++ b/spec/fixtures/test.po @@ -2,8 +2,6 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# -#, fuzzy msgid "" msgstr "" "Project-Id-Version: version 0.0.1\n" @@ -58,9 +56,9 @@ msgid "car" msgstr "Auto" #: locale/testlog_phrases.rb:2 -msgid "this is a dynamic translation which was found thorugh gettext_test_log!" +msgid "this is a dynamic translation which was found!" msgstr "" -"Dies ist eine dynamische Übersetzung, die durch gettext_test_log " +"Dies ist eine dynamische Übersetzung, die " "gefunden wurde!" #: locale/test_escape.rb:2 diff --git a/spec/po_to_json_spec.rb b/spec/po_to_json_spec.rb index a9f1568..bd7348b 100644 --- a/spec/po_to_json_spec.rb +++ b/spec/po_to_json_spec.rb @@ -1,49 +1,195 @@ -# encoding: utf-8 -require 'spec_helper' +# +# Copyright (c) 2012-2015 Dropmysite.com +# Copyright (c) 2015 Webhippie +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +require "spec_helper" describe PoToJson do - before :all do - @subject = PoToJson.new(File.join(File.dirname(__FILE__), '..', 'spec', 'test.po')) + let(:po_to_json) do + @subject = PoToJson.new( + File.expand_path("../fixtures/test.po", __FILE__) + ) end - describe 'when parsing' do - before(:all){ @parsed = @subject.parse } - - it { @parsed['']['Last-Translator'].should == ' FULL NAME ' } - it { @parsed['%{relative_time} ago'].should == [nil, 'vor %{relative_time}'] } - it { @parsed['Axis'].should == ['Axis', 'Achse', 'Achsen'] } - it { @parsed['Car was successfully created.'].should == [nil, 'Auto wurde erfolgreich gespeichert'] } - it { @parsed['Car was successfully updated.'].should == [nil, 'Auto wurde erfolgreich aktualisiert'] } - it { @parsed['Car|Model'].should == [nil, 'Modell'] } - it { @parsed['Untranslated'].should == [nil, ''] } - it { @parsed['Car|Wheels count'].should == [nil, 'Räderzahl'] } - it { @parsed['Created'].should == [nil, 'Erstellt'] } - it { @parsed['Month'].should == [nil, 'Monat'] } - it { @parsed['car'].should == [nil, 'Auto'] } - it { @parsed['Umläüte'].should == [nil, 'Umlaute'] } - it do - @parsed["You should escape '\\\\' as '\\\\\\\\'."].should == - [nil, "Du solltest '\\\\' als '\\\\\\\\' escapen."] - end - it do - @parsed['this is a dynamic translation which was found thorugh gettext_test_log!'].should == - [nil, 'Dies ist eine dynamische Übersetzung, die durch gettext_test_log gefunden wurde!'] + + describe "parsing" do + subject do + po_to_json.parse_document + end + + it "should find the last author" do + expect(subject[""]["Last-Translator"]).to( + eq("FULL NAME ") + ) + end + + it "should parse embedded variables" do + expect(subject["%{relative_time} ago"]).to( + eq([nil, "vor %{relative_time}"]) + ) + end + + it "should match pluralizations" do + expect(subject["Axis"]).to( + eq(["Axis", "Achse", "Achsen"]) + ) + end + + it "should match glued values" do + expect(subject["Car|Model"]).to( + eq([nil, "Modell"]) + ) + end + + it "should match embedded glue" do + expect(subject["Car|Wheels count"]).to( + eq([nil, "Räderzahl"]) + ) + end + + it "should return empty strings as well" do + expect(subject["Untranslated"]).to( + eq([nil, ""]) + ) + end + + it "should match german umlauts" do + expect(subject["Umläüte"]).to( + eq([nil, "Umlaute"]) + ) + end + + it "should match escaped values" do + expect(subject["You should escape '\\\\' as '\\\\\\\\'."]).to( + eq([nil, "Du solltest '\\\\' als '\\\\\\\\' escapen."]) + ) + end + + it "should match multiline translations" do + expect(subject["this is a dynamic translation which was found!"]).to( + eq([nil, "Dies ist eine dynamische Übersetzung, die gefunden wurde!"]) + ) + end + + it "should match simple strings" do + expect(subject["Car was successfully created."]).to( + eq([nil, "Auto wurde erfolgreich gespeichert"]) + ) + + expect(subject["Car was successfully updated."]).to( + eq([nil, "Auto wurde erfolgreich aktualisiert"]) + ) + + expect(subject["Created"]).to( + eq([nil, "Erstellt"]) + ) + + expect(subject["Month"]).to( + eq([nil, "Monat"]) + ) + + expect(subject["car"]).to( + eq([nil, "Auto"]) + ) end end - describe 'when generating a jed compatible file' do - before(:all){ @jed_json = @subject.generate_for_jed('de') } - it { @jed_json.include?("var locales = locales || {}; locales['de'] = ").should be_true } - it { @jed_json.include?('"domain":"app"').should be_true } - it { @jed_json.include?('"lang":"de"').should be_true } - it { @jed_json.include?('"plural_forms":" nplurals=INTEGER; plural=EXPRESSION;"').should be_true } + describe "generate jed compatible" do + describe "with minified output" do + subject do + po_to_json.generate_for_jed("de") + end + + it "should output the var definition" do + expect( + subject.include?("var locales = locales || {}; locales['de'] = ") + ).to be_truthy + end + + it "should include domain string" do + expect( + subject.include?('"domain":"app"') + ).to be_truthy + end + + it "should include lang string" do + expect( + subject.include?('"lang":"de"') + ).to be_truthy + end + + it "should include pluralization" do + val = '"plural_forms":"nplurals=INTEGER; plural=EXPRESSION;"' + expect( + subject.include? val + ).to be_truthy + end - context 'with :pretty => true' do - before(:all){@jed_json = @subject.generate_for_jed('de', :pretty => true)} - it { @jed_json.include?("var locales = locales || {}; locales['de'] = ").should be_true } - it { @jed_json.include?('"domain": "app"').should be_true } - it { @jed_json.include?('"lang": "de"').should be_true } - it { @jed_json.include?('"plural_forms": " nplurals=INTEGER; plural=EXPRESSION;"').should be_true } + it "should include a single line break" do + expect( + subject.count("\n") + ).to be < 1 + end + end + + context "with pretty output" do + subject do + po_to_json.generate_for_jed("de", pretty: true) + end + + it "should output the var definition" do + expect( + subject.include?("var locales = locales || {}; locales['de'] = ") + ).to be_truthy + end + + it "should include domain string" do + expect( + subject.include?('"domain": "app"') + ).to be_truthy + end + + it "should include lang string" do + expect( + subject.include?('"lang": "de"') + ).to be_truthy + end + + it "should include pluralization" do + val = '"plural_forms": "nplurals=INTEGER; plural=EXPRESSION;"' + expect( + subject.include? val + ).to be_truthy + end + + it "should include multiple line breaks" do + expect( + subject.count("\n") + ).to be > 0 + end end end -end + # describe "generate simple hashes" do + # pending "have to be implemented" + # end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 056d5c4..12c401a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,53 @@ -$LOAD_PATH.unshift File.expand_path("../lib", File.dirname(__FILE__)) -require 'json' -require 'po_to_json' +# +# Copyright (c) 2012-2015 Dropmysite.com +# Copyright (c) 2015 Webhippie +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +require "simplecov" + +if ENV["CODECLIMATE_REPO_TOKEN"] + require "coveralls" + require "codeclimate-test-reporter" + + Coveralls.wear! + CodeClimate::TestReporter.start + + SimpleCov.start do + add_filter "/spec" + + formatter SimpleCov::Formatter::MultiFormatter[ + SimpleCov::Formatter::HTMLFormatter, + CodeClimate::TestReporter::Formatter + ] + end +else + SimpleCov.start do + add_filter "/spec" + end +end + +require "po_to_json" +require "rspec" + +RSpec.configure do |config| + config.mock_with :rspec +end