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