rails_translation_manager 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bcf654f074b115a7c80d173e821333d2b8b35b01
4
+ data.tar.gz: 84cea8e22ca03313c47e7432be605cee30905391
5
+ SHA512:
6
+ metadata.gz: 6823237f243a4127c4c14ece436ec69212c1e481a1d11d8329649cfbcdc61177bf73f0097c1978fed353c863c83949f9eee62f616c16086f28757ce6645b5f0b
7
+ data.tar.gz: 2d8bc1c02272590a00a8bcbec3d002cec989f27c69e5025e65729ebf56c198089d0f29c8f7b87aaab5434642b28f2f11037a9596babdb1e096dd9ff8bda1a850
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.4
data/.travis.yml ADDED
@@ -0,0 +1,2 @@
1
+ rvm:
2
+ - 2.1.4
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,7 @@
1
+ ## Contributing
2
+
3
+ 1. Fork it
4
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
5
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
6
+ 4. Push to the branch (`git push origin my-new-feature`)
7
+ 5. Create a new Pull Request
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rails_translation_manager.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+ Copyright (C) 2015 HM Government (Government Digital Service)
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # Rails Translation Manager
2
+
3
+ Support for translation workflow in rails applications.
4
+
5
+ ## Nomenclature
6
+
7
+ - **CSV**: comma separated values, a tabular data format which can be loaded into a
8
+ spreadsheet package
9
+ - **I18n**: an abbreviation of 'internationalisation', which is the process of adding
10
+ support for multiple locales and languages to an application.
11
+ `I18n` is also the name of a ruby gem which supports
12
+ internationalisation in ruby applications.
13
+ - **interpolation**: a technique used in I18n whereby data is inserted ("interpolated")
14
+ into a translated string of text, for example `Hello %{name}`
15
+ would become `Hello Sarah` if the variable `name` had the
16
+ value `Sarah`.
17
+ - **YAML**: yet another markup language, a textual data format used for storing
18
+ translation strings in rails applications
19
+
20
+ ## Technical documentation
21
+
22
+ This gem provides a rails engine which adds rake tasks to manage translation
23
+ files.
24
+
25
+ It is intended to be included within your rails application by referencing it
26
+ as a dependency in your `Gemfile`. You will then be able to use the rake tasks
27
+ to manage your translation files and import/export translation strings.
28
+
29
+ ### Dependencies
30
+
31
+ To date it has only been tested with a rails 3.2.18 app, but it should work with later (and older) rails apps as well.
32
+
33
+ ### Installation
34
+
35
+ Add this line to your application's Gemfile:
36
+
37
+ ```ruby
38
+ gem 'rails_translation_manager'
39
+ ```
40
+
41
+ And then execute:
42
+
43
+ $ bundle
44
+
45
+ The gem depends on your rails application environment so it would not make
46
+ sense to install this gem stand-alone.
47
+
48
+ ### Running the application
49
+
50
+ The primary usage of this gem is to support translation workflow.
51
+
52
+ Once you have installed the gem into your application as described above, the
53
+ expected usage is:
54
+
55
+ 1. export translations to a CSV file using:
56
+
57
+ ```
58
+ rake translation:export:all[target_directory]
59
+ ```
60
+
61
+ or
62
+
63
+ ```
64
+ rake translation:export[target_directory,base_locale,target_locale]
65
+ ```
66
+
67
+ 2. send the appropriate CSV file to a translator
68
+
69
+ 3. wait for translation to happen
70
+
71
+ 4. receive translation file back, check it for [character encoding issues](https://github.com/alphagov/character_encoding_cleaner)
72
+
73
+ 5. import the translation file using either:
74
+
75
+ ```
76
+ rake translation:import:all[source_directory]
77
+ ```
78
+
79
+ or
80
+
81
+ ```
82
+ rake translation:import[locale,path]
83
+ ```
84
+
85
+ this will generate `.yml` files for each translation
86
+
87
+ 6. commit any changed `.yml` files
88
+
89
+ ```
90
+ git add config/locale
91
+ git commit -m 'added new translations'
92
+ ```
93
+
94
+ ### Validation of interpolation keys
95
+
96
+ A second feature supported by this library is the validation of interpolation
97
+ keys.
98
+
99
+ The I18n library supports 'interpolation' using the following syntax:
100
+
101
+ ```yaml
102
+ en:
103
+ some_view:
104
+ greeting: Hello, %{name}
105
+ ```
106
+
107
+ in this case the application can pass in the value of the `name` variable.
108
+
109
+ If a translation includes an interpolation placeholder which has not been
110
+ given a value by the application backend, then a runtime error will be raised.
111
+
112
+ Unfortunately the placeholder variables sometimes get changed by mistake, or
113
+ by translators who are not aware that they should not modify text within the
114
+ special curly braces of the interpolation placeholder.
115
+
116
+ This is obviously not great, and the validation task is intended to guard
117
+ against it.
118
+
119
+ It will check all translation files and report any which contain placeholders
120
+ which do not exist in the english file.
121
+
122
+ ```
123
+ $ rake translation:validate
124
+ Success! No unexpected interpolation keys found.
125
+ ```
126
+
127
+ ### Rake command reference
128
+
129
+ #### Export a specific locale to CSV
130
+
131
+ ```
132
+ rake translation:export[directory,base_locale,target_locale]
133
+ ```
134
+
135
+ #### Export all locales to CSV files
136
+
137
+ ```
138
+ rake translation:export:all[directory]
139
+ ```
140
+
141
+ #### Import a specific locale CSV to YAML within the app
142
+
143
+ ```
144
+ rake translation:import[locale,path]
145
+ ```
146
+
147
+ #### Import all locale CSV files to YAML within the app
148
+
149
+ ```
150
+ rake translation:import:all[directory]
151
+ ```
152
+
153
+ #### Regenerate all locales from the EN locale - run this after adding keys
154
+
155
+ ```
156
+ rake translation:regenerate[directory]
157
+ ```
158
+
159
+ #### Check translation files for errors
160
+
161
+ ```
162
+ rake translation:validate
163
+ ```
164
+
165
+ ### Running the test suite
166
+
167
+ To run the test suite just run `bundle exec rake` from within the
168
+ `rails_translation_manager` directory.
169
+
170
+ You will need to clone the repository locally and run `bundle install` the
171
+ first time you do this, eg.:
172
+
173
+ ```sh
174
+ $ git clone git@github.com:alphagov/rails_translation_manager.git
175
+ $ cd rails_translation_manager
176
+ $ bundle install
177
+ $ bundle exec rake
178
+ ...
179
+ ```
180
+
181
+ ## Licence
182
+
183
+ [MIT License](LICENSE.txt)
184
+
185
+ ## Versioning policy
186
+
187
+ We use [semantic versioning](http://semver.org/), and bump the version
188
+ on master only. Please don't submit your own proposed version numbers.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new("test") do |t|
5
+ t.description = "Run tests"
6
+ t.ruby_opts << "-rubygems"
7
+ t.libs << "test"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ t.verbose = true
10
+ end
11
+
12
+ task :default => :test
@@ -0,0 +1,111 @@
1
+ # encoding: utf-8
2
+ require "yaml"
3
+ require "csv"
4
+ require 'i18n'
5
+ require "active_support/core_ext/object/blank"
6
+ require "active_support/core_ext/hash/keys.rb"
7
+
8
+ class RailsTranslationManager::Exporter
9
+ def initialize(directory, source_locale_path, target_locale_path)
10
+ @source_locale_path = source_locale_path
11
+ @target_locale_path = target_locale_path
12
+ @source_locale = File.basename(target_locale_path).split(".")[0]
13
+ @target_locale = File.basename(target_locale_path).split(".")[0]
14
+ @output_path = File.join(directory, @target_locale + ".csv")
15
+
16
+ @keyed_source_data = translation_file_to_keyed_data(source_locale_path, @source_locale)
17
+ @keyed_target_data = translation_file_to_keyed_data(target_locale_path, @target_locale)
18
+ end
19
+
20
+ def export
21
+ csv = CSV.generate do |csv|
22
+ csv << CSV::Row.new(["key", "source", "translation"], ["key", "source", "translation"], true)
23
+ @keyed_source_data.keys.sort.each do |key|
24
+ if key =~ /^language_names\./
25
+ next unless key =~ /#{@target_locale}$/
26
+ end
27
+ if is_pluralized_key?(key)
28
+ export_pluralization_rows(key, csv)
29
+ else
30
+ csv << export_row(key, @keyed_source_data[key], @keyed_target_data[key])
31
+ end
32
+ end
33
+ end
34
+ File.open(@output_path, "w") { |f| f.write csv.to_s }
35
+ end
36
+
37
+ private
38
+
39
+ def export_pluralization_rows(key, csv)
40
+ I18n.t('i18n.plural.keys', locale: @target_locale).map(&:to_s).each do |plural_key|
41
+ csv << export_row(depluralized_key_for(key, plural_key), @keyed_source_data.fetch(key, {})[plural_key], @keyed_target_data.fetch(key, {})[plural_key])
42
+ end
43
+ end
44
+
45
+ def export_row(key, source_value, target_value)
46
+ CSV::Row.new(['key', 'source', 'translation'], [key, source_value, target_value])
47
+ end
48
+
49
+ def translation_file_to_keyed_data(path, locale)
50
+ if File.exist?(path)
51
+ hash = YAML.load_file(path).values[0]
52
+ hash_to_keyed_data("", hash, locale)
53
+ else
54
+ {}
55
+ end
56
+ end
57
+
58
+ def hash_to_keyed_data(prefix, hash, locale)
59
+ if hash_is_for_pluralization?(hash, locale)
60
+ {pluralized_prefix(prefix) => hash.stringify_keys}
61
+ else
62
+ results = {}
63
+ hash.each do |key, value|
64
+ if value.is_a?(Hash)
65
+ results.merge!(hash_to_keyed_data(key_for(prefix, key), value, locale))
66
+ else
67
+ results[key_for(prefix, key)] = value
68
+ end
69
+ end
70
+ results
71
+ end
72
+ end
73
+
74
+ # if the hash is only made up of the plural keys for the locale, we
75
+ # assume it's a plualization set. Note that zero is *always* an option
76
+ # regardless of the keys fetched
77
+ # (see https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/pluralization.rb#L34)
78
+ def hash_is_for_pluralization?(hash, locale)
79
+ plural_keys = I18n.t('i18n.plural.keys', locale: locale)
80
+ raise missing_pluralisations_message(locale) unless plural_keys.is_a?(Array)
81
+ ((hash.keys.map(&:to_s) - plural_keys.map(&:to_s)) - ['zero']).empty?
82
+ end
83
+
84
+ def missing_pluralisations_message(locale)
85
+ "No pluralization forms defined for locale '#{locale}'. " +
86
+ "This probably means that the rails-18n gem does not provide a " +
87
+ "definition of the plural forms for this locale, you may need to " +
88
+ "define them yourself."
89
+ end
90
+
91
+ def key_for(prefix, key)
92
+ prefix.blank? ? key.to_s : "#{prefix}.#{key}"
93
+ end
94
+
95
+ def is_pluralized_key?(key)
96
+ key =~ /\APLURALIZATION\-KEY\:/
97
+ end
98
+
99
+ def pluralized_prefix(prefix)
100
+ if is_pluralized_key?(prefix)
101
+ prefix
102
+ else
103
+ "PLURALIZATION-KEY:#{prefix}"
104
+ end
105
+ end
106
+
107
+ def depluralized_key_for(prefix, key)
108
+ depluralized_prefix = prefix.gsub(/\APLURALIZATION\-KEY\:/, '')
109
+ key_for(depluralized_prefix, key)
110
+ end
111
+ end
@@ -0,0 +1,60 @@
1
+ require "yaml"
2
+ require "csv"
3
+
4
+ class RailsTranslationManager::Importer
5
+ def initialize(locale, csv_path, import_directory)
6
+ @csv_path = csv_path
7
+ @locale = locale
8
+ @import_directory = import_directory
9
+ end
10
+
11
+ def import
12
+ csv = CSV.read(@csv_path, headers: true, header_converters: :downcase)
13
+ data = {}
14
+ csv.each do |row|
15
+ key = row["key"]
16
+ key_parts = key.split(".")
17
+ if key_parts.length > 1
18
+ leaf_node = (data[key_parts.first] ||= {})
19
+ key_parts[1..-2].each do |part|
20
+ leaf_node = (leaf_node[part] ||= {})
21
+ end
22
+ leaf_node[key_parts.last] = parse_translation(row["translation"])
23
+ else
24
+ data[key_parts.first] = parse_translation(row["translation"])
25
+ end
26
+ end
27
+
28
+ File.open(import_yml_path, "w") do |f|
29
+ yaml = {@locale.to_s => data}.to_yaml(separator: "")
30
+ yaml_without_header = yaml.split("\n").map { |l| l.gsub(/\s+$/, '') }[1..-1].join("\n")
31
+ f.write(yaml_without_header)
32
+ f.puts
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def import_yml_path
39
+ File.join(@import_directory, "#{@locale}.yml")
40
+ end
41
+
42
+ def parse_translation(translation)
43
+ if translation =~ /^\[/
44
+ values = translation.gsub(/^\[/, '').gsub(/\]$/, '').gsub("\"", '').split(/\s*,\s*/)
45
+ values.map { |v| parse_translation(v) }
46
+ elsif translation =~ /^:/
47
+ translation.gsub(/^:/, '').to_sym
48
+ elsif translation =~ /^true$/
49
+ true
50
+ elsif translation =~ /^false$/
51
+ false
52
+ elsif translation =~ /^\d+$/
53
+ translation.to_i
54
+ elsif translation == "nil"
55
+ nil
56
+ else
57
+ translation
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,9 @@
1
+ require 'rails_translation_manager'
2
+
3
+ module RailsTranslationManager
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ Dir[File.expand_path('../tasks/*.rake', File.dirname(__FILE__))].each { |f| load f }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,92 @@
1
+ require 'yaml'
2
+ require 'logger'
3
+
4
+ class RailsTranslationManager::Validator
5
+ def initialize(translation_file_path, logger = Logger.new(nil))
6
+ @translation_file_path = translation_file_path
7
+ @logger = logger
8
+ end
9
+
10
+ def check!
11
+ @logger.info "Checking translation files in '#{@translation_file_path}' for unexpected interpolation keys"
12
+ @logger.info "Loading reference file (#{reference_file_name})"
13
+ @logger.info "Checking..."
14
+ reference = load_translation_file("#{@translation_file_path}/#{reference_file_name}")
15
+ Dir["#{@translation_file_path}/*.yml"].reject do |entry|
16
+ File.basename(entry) == reference_file_name
17
+ end.inject([]) do |errors, entry|
18
+ translation_file = load_translation_file(entry)
19
+ errors + unexpected_substitution_keys(reference, translation_file)
20
+ end
21
+ end
22
+
23
+ def unexpected_substitution_keys(reference, translation_file)
24
+ reference_substitutions = substitutions_in(reference)
25
+ target_substitutions = substitutions_in(translation_file)
26
+
27
+ targets_by_path = target_substitutions.each_with_object({}) do |target, hash|
28
+ hash[exclude_locale_from_path(target.path)] = target
29
+ end
30
+
31
+ reference_substitutions.each_with_object([]) do |reference, unexpected_substitutions|
32
+ target = targets_by_path[exclude_locale_from_path(reference.path)]
33
+ next if target.nil? || reference.has_all_substitutions?(target)
34
+ unexpected_substitutions << UnexpectedSubstition.new(target, reference)
35
+ end
36
+ end
37
+
38
+ def substitutions_in(translation_file)
39
+ flatten(translation_file).reject do |translation|
40
+ translation.substitutions.empty?
41
+ end
42
+ end
43
+
44
+ class TranslationEntry < Struct.new(:path, :value)
45
+ def substitutions
46
+ @substitutions ||= self.value.scan(/%{([^}]*)}/)
47
+ end
48
+
49
+ def has_all_substitutions?(other)
50
+ (other.substitutions - self.substitutions).empty?
51
+ end
52
+ end
53
+
54
+ class UnexpectedSubstition < Struct.new(:target, :reference)
55
+ def to_s
56
+ missing = (self.reference.substitutions - self.target.substitutions)
57
+ extras = (self.target.substitutions - self.reference.substitutions)
58
+ message = %Q{Key "#{target.path.join('.')}":}
59
+ if extras.any?
60
+ message << %Q{ Extra substitutions: ["#{extras.join('", "')}"].}
61
+ end
62
+ if missing.any?
63
+ message << %Q{ Missing substitutions: ["#{missing.join('", "')}"].}
64
+ end
65
+ message
66
+ end
67
+ end
68
+
69
+ def flatten(translation_file, path = [])
70
+ translation_file.map do |key, value|
71
+ case value
72
+ when Hash
73
+ flatten(value, path + [key])
74
+ else
75
+ TranslationEntry.new(path + [key], value || "")
76
+ end
77
+ end.flatten
78
+ end
79
+
80
+ def load_translation_file(filename)
81
+ YAML.load_file(filename)
82
+ end
83
+
84
+ def reference_file_name
85
+ "en.yml"
86
+ end
87
+
88
+ private
89
+ def exclude_locale_from_path(path)
90
+ path[1..-1]
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module RailsTranslationManager
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,8 @@
1
+ require "rails_translation_manager/version"
2
+ require "rails_translation_manager/railtie" if defined?(Rails)
3
+ require "rails-i18n"
4
+
5
+ module RailsTranslationManager
6
+ autoload :Exporter, "rails_translation_manager/exporter"
7
+ autoload :Importer, "rails_translation_manager/importer"
8
+ end
@@ -0,0 +1,70 @@
1
+ $LOAD_PATH.unshift(File.expand_path("../.."), __FILE__)
2
+ require "rails_translation_manager"
3
+
4
+ namespace :translation do
5
+
6
+ desc "Regenerate all locales from the EN locale - run this after adding keys"
7
+ task(:regenerate, [:directory] => [:environment]) do |t, args|
8
+ directory = args[:directory] || "tmp/locale_csv"
9
+
10
+ Rake::Task["translation:export:all"].invoke(directory)
11
+ Rake::Task["translation:import:all"].invoke(directory)
12
+ end
13
+
14
+ desc "Export a specific locale to CSV."
15
+ task :export, [:directory, :base_locale, :target_locale] => [:environment] do |t, args|
16
+ FileUtils.mkdir_p(args[:directory]) unless File.exist?(args[:directory])
17
+ base_locale = Rails.root.join("config", "locales", args[:base_locale] + ".yml")
18
+ target_locale_path = Rails.root.join("config", "locales", args[:target_locale] + ".yml")
19
+ exporter = RailsTranslationManager::Exporter.new(args[:directory], base_locale, target_locale_path)
20
+ exporter.export
21
+ end
22
+
23
+ namespace :export do
24
+ desc "Export all locales to CSV files."
25
+ task :all, [:directory] => [:environment] do |t, args|
26
+ directory = args[:directory] || "tmp/locale_csv"
27
+ FileUtils.mkdir_p(directory) unless File.exist?(directory)
28
+ locales = Dir[Rails.root.join("config", "locales", "*.yml")]
29
+ base_locale = Rails.root.join("config", "locales", "en.yml")
30
+ target_locales = locales - [base_locale.to_s]
31
+ target_locales.each do |target_locale_path|
32
+ exporter = RailsTranslationManager::Exporter.new(directory, base_locale, target_locale_path)
33
+ exporter.export
34
+ end
35
+ puts "Exported locale CSV to #{directory}"
36
+ end
37
+ end
38
+
39
+ desc "Import a specific locale CSV to YAML within the app."
40
+ task :import, [:locale, :path] => [:environment] do |t, args|
41
+ importer = RailsTranslationManager::Importer.new(args[:locale], args[:path], Rails.root.join("config", "locales"))
42
+ importer.import
43
+ end
44
+
45
+ namespace :import do
46
+ desc "Import all locale CSV files to YAML within the app."
47
+ task :all, [:directory] => [:environment] do |t, args|
48
+ directory = args[:directory] || "tmp/locale_csv"
49
+ Dir[File.join(directory, "*.csv")].each do |csv_path|
50
+ locale = File.basename(csv_path, ".csv")
51
+ importer = RailsTranslationManager::Importer.new(locale, csv_path, Rails.root.join("config", "locales"))
52
+ importer.import
53
+ end
54
+ end
55
+ end
56
+
57
+ desc "Check translation files for errors"
58
+ task :validate do
59
+ require 'rails_translation_manager/validator'
60
+ logger = Logger.new(STDOUT)
61
+ validator = RailsTranslationManager::Validator.new(Rails.root.join('config', 'locales'), logger)
62
+ errors = validator.check!
63
+ if errors.any?
64
+ puts "Found #{errors.size} errors:"
65
+ puts errors.map(&:to_s).join("\n")
66
+ else
67
+ puts "Success! No unexpected interpolation keys found."
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rails_translation_manager/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rails_translation_manager"
8
+ spec.version = RailsTranslationManager::VERSION
9
+ spec.authors = ["Edd Sowden"]
10
+ spec.email = ["edd.sowden@digital.cabinet-office.gov.uk"]
11
+ spec.summary = %q{Tasks to manage translation files}
12
+ spec.description = ""
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "rails-i18n"
22
+ spec.add_dependency "activesupport"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.7"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "minitest"
27
+ end
@@ -0,0 +1,239 @@
1
+ require "test_helper"
2
+
3
+ require "rails_translation_manager/exporter"
4
+ require "tmpdir"
5
+ require "csv"
6
+ require "i18n"
7
+
8
+ module RailsTranslationManager
9
+ class ExporterTest < Minitest::Test
10
+
11
+ def setup
12
+ # fast test helper means we have to setup the pluralizations we're
13
+ # going to use manually as rails-i18n does it in the rails app
14
+ I18n.backend.class.send(:include, I18n::Backend::Pluralization)
15
+ I18n.available_locales = [:fr, :es, :ar, :sk, :uk]
16
+ with_pluralization_forms(
17
+ 'fr' => [:one, :other],
18
+ 'es' => [:one, :other],
19
+ 'ar' => [:zero, :one, :two, :few, :many, :other],
20
+ 'sk' => [:one, :few, :other],
21
+ 'uk' => [:one, :few, :many, :other]
22
+ )
23
+ end
24
+
25
+ test 'should export CSV file for filling in a given translation' do
26
+ given_locale(:en, {
27
+ world_location: {
28
+ type: {
29
+ country: "Country"
30
+ },
31
+ country: "Spain",
32
+ headings: {
33
+ mission: "Our mission",
34
+ offices: "Offices"
35
+ }
36
+ }
37
+ })
38
+
39
+ Exporter.new(export_directory, locale_path(:en), locale_path(:fr)).export
40
+
41
+ assert File.file?(exported_file("fr.csv")), "should write a file"
42
+
43
+ data = read_csv_data(exported_file("fr.csv"))
44
+ assert_equal ["Country", nil], data["world_location.type.country"]
45
+ assert_equal ["Spain", nil], data["world_location.country"]
46
+ assert_equal ["Our mission", nil], data["world_location.headings.mission"]
47
+ assert_equal ["Offices", nil], data["world_location.headings.offices"]
48
+ end
49
+
50
+ test 'should include any existing translations in the output file' do
51
+ given_locale(:en, {
52
+ world_location: {
53
+ type: {
54
+ country: "Country"
55
+ },
56
+ country: "Spain",
57
+ headings: {
58
+ mission: "Our mission",
59
+ offices: "Offices"
60
+ }
61
+ }
62
+ })
63
+ given_locale(:fr, {
64
+ world_location: {
65
+ type: {
66
+ country: "Pays"
67
+ },
68
+ country: "Espange"
69
+ }
70
+ })
71
+
72
+ Exporter.new(export_directory, locale_path(:en), locale_path(:fr)).export
73
+
74
+ assert File.file?(exported_file("fr.csv")), "should write a file"
75
+
76
+ data = read_csv_data(exported_file("fr.csv"))
77
+ assert_equal ["Country", "Pays"], data["world_location.type.country"]
78
+ assert_equal ["Spain", "Espange"], data["world_location.country"]
79
+ assert_equal ["Our mission", nil], data["world_location.headings.mission"]
80
+ assert_equal ["Offices", nil], data["world_location.headings.offices"]
81
+ end
82
+
83
+ test 'should not include any language names that are not English or the native in the output file' do
84
+ given_locale(:en, {
85
+ language_names: {
86
+ en: "English",
87
+ es: "Spanish",
88
+ fr: "French"
89
+ }
90
+ })
91
+
92
+ Exporter.new(export_directory, locale_path(:en), locale_path(:fr)).export
93
+
94
+ assert File.file?(exported_file("fr.csv")), "should write a file"
95
+
96
+ data = read_csv_data(exported_file("fr.csv"))
97
+ assert_equal ["French", nil], data["language_names.fr"]
98
+ assert_equal nil, data["language_names.es"], "language key for spanish should not be present"
99
+ end
100
+
101
+ test 'should export correct pluralization forms for target' do
102
+ given_locale(:en, {
103
+ ministers: {
104
+ one: 'minister',
105
+ other: 'ministers'
106
+ }
107
+ })
108
+
109
+ Exporter.new(export_directory, locale_path(:en), locale_path(:ar)).export
110
+
111
+ assert File.file?(exported_file("ar.csv")), "should write a file"
112
+
113
+ data = read_csv_data(exported_file("ar.csv"))
114
+ ['zero', 'one', 'two', 'few', 'many', 'other'].each do |arabic_plural_form|
115
+ assert data.has_key?("ministers.#{arabic_plural_form}"), "expected plural form #{arabic_plural_form} to be present, but it's not"
116
+ end
117
+ end
118
+
119
+ test 'should export source pluralization forms values to target when the forms match' do
120
+ given_locale(:en, {
121
+ ministers: {
122
+ one: 'minister',
123
+ other: 'ministers'
124
+ }
125
+ })
126
+
127
+ Exporter.new(export_directory, locale_path(:en), locale_path(:sk)).export
128
+
129
+ assert File.file?(exported_file("sk.csv")), "should write a file"
130
+
131
+ data = read_csv_data(exported_file("sk.csv"))
132
+ assert_equal ['minister', nil], data['ministers.one']
133
+ assert_equal ['ministers', nil], data['ministers.other']
134
+ assert_equal [nil, nil], data['ministers.few']
135
+ end
136
+
137
+ test 'should keep existing target pluralization form values' do
138
+ given_locale(:en, {
139
+ ministers: {
140
+ one: 'minister',
141
+ other: 'ministers'
142
+ }
143
+ })
144
+ given_locale(:sk, {
145
+ ministers: {
146
+ one: 'min',
147
+ few: 'mini'
148
+ }
149
+ })
150
+
151
+ Exporter.new(export_directory, locale_path(:en), locale_path(:sk)).export
152
+
153
+ assert File.file?(exported_file("sk.csv")), "should write a file"
154
+
155
+ data = read_csv_data(exported_file("sk.csv"))
156
+ assert_equal ['minister', 'min'], data['ministers.one']
157
+ assert_equal ['ministers', nil], data['ministers.other']
158
+ assert_equal [nil, 'mini'], data['ministers.few']
159
+ end
160
+
161
+ test 'should allow for zero keys when detecting pluralization forms' do
162
+ given_locale(:en, {
163
+ ministers: {
164
+ zero: 'no ministers',
165
+ one: 'minister',
166
+ other: 'ministers'
167
+ }
168
+ })
169
+
170
+ Exporter.new(export_directory, locale_path(:en), locale_path(:uk)).export
171
+
172
+ assert File.file?(exported_file("uk.csv")), "should write a file"
173
+
174
+ data = read_csv_data(exported_file("uk.csv"))
175
+ ['one', 'few', 'many', 'other'].each do |ukranian_plural_form|
176
+ assert data.has_key?("ministers.#{ukranian_plural_form}"), "expected plural form #{ukranian_plural_form} to be present, but it's not"
177
+ end
178
+ end
179
+
180
+ test 'should leave keys alone if the hash doesn\'t look like it only contains pluralization forms' do
181
+ given_locale(:en, {
182
+ ministers: {
183
+ one: 'minister',
184
+ other: 'ministers',
185
+ monkey: 'monkey'
186
+ }
187
+ })
188
+
189
+ Exporter.new(export_directory, locale_path(:en), locale_path(:uk)).export
190
+
191
+ assert File.file?(exported_file("uk.csv")), "should write a file"
192
+
193
+ data = read_csv_data(exported_file("uk.csv"))
194
+ ['few', 'many'].each do |ukranian_plural_form|
195
+ refute data.has_key?("ministers.#{ukranian_plural_form}"), "expected plural form #{ukranian_plural_form} to be missing, but it's present"
196
+ end
197
+ ['one', 'other', 'monkey'].each do |non_plural_forms|
198
+ assert data.has_key?("ministers.#{non_plural_forms}"), "expected non-plural form #{non_plural_forms} to be present, but it's not"
199
+ end
200
+ end
201
+
202
+ private
203
+
204
+ def read_csv_data(file)
205
+ csv = CSV.read(file, headers: true)
206
+ csv.inject({}) { |h, row| h[row["key"]] = [row["source"], row["translation"]]; h }
207
+ end
208
+
209
+ def given_locale(locale, keys)
210
+ File.open(locale_path(locale), "w") do |f|
211
+ f.puts({locale => keys}.to_yaml)
212
+ end
213
+ end
214
+
215
+ def locale_path(locale)
216
+ File.join(export_directory, "#{locale}.yml")
217
+ end
218
+
219
+ def exported_file(name)
220
+ File.new(File.join(export_directory, name))
221
+ end
222
+
223
+ def export_directory
224
+ @tmpdir ||= Dir.mktmpdir
225
+ end
226
+
227
+ def with_pluralization_forms(pluralizations)
228
+ pluralizations.each do |locale, pluralization_forms|
229
+ I18n.backend.store_translations(locale, {
230
+ 'i18n' => {
231
+ 'plural' => {
232
+ 'keys' => pluralization_forms
233
+ }
234
+ }
235
+ })
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,166 @@
1
+ require "test_helper"
2
+ require "rails_translation_manager/importer"
3
+ require "tmpdir"
4
+ require "csv"
5
+
6
+ module RailsTranslationManager
7
+ class ImporterTest < Minitest::Test
8
+ test 'should create a new locale file for a filled in translation csv file' do
9
+ given_csv(:fr,
10
+ [:key, :source, :translation],
11
+ ["world_location.type.country", "Country", "Pays"],
12
+ ["world_location.country", "Germany", "Allemange"],
13
+ ["other.nested.key", "original", "translated"]
14
+ )
15
+
16
+ Importer.new(:fr, csv_path(:fr), import_directory).import
17
+
18
+ yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
19
+ expected = {"fr" => {
20
+ "world_location" => {
21
+ "country" => "Allemange",
22
+ "type" => {
23
+ "country" => "Pays"
24
+ }
25
+ },
26
+ "other" => {
27
+ "nested" => {
28
+ "key" => "translated"
29
+ }
30
+ }
31
+ }}
32
+ assert_equal expected, yaml_translation_data
33
+ end
34
+
35
+ test 'outputs YAML without the header --- line for consistency with convention' do
36
+ given_csv(:fr,
37
+ [:key, :source, :translation],
38
+ ["key", "value", "le value"],
39
+ )
40
+
41
+ Importer.new(:fr, csv_path(:fr), import_directory).import
42
+
43
+ assert_equal "fr:", File.readlines(File.join(import_directory, "fr.yml")).first.strip
44
+ end
45
+
46
+ test 'outputs a newline at the end of the YAML for consistency with code editors' do
47
+ given_csv(:fr,
48
+ [:key, :source, :translation],
49
+ ["key", "value", "le value"],
50
+ )
51
+
52
+ Importer.new(:fr, csv_path(:fr), import_directory).import
53
+
54
+ assert_match /\n$/, File.readlines(File.join(import_directory, "fr.yml")).last
55
+ end
56
+
57
+ test 'strips whitespace from the end of lines for consistency with code editors' do
58
+ given_csv(:fr,
59
+ [:key, :source, :translation],
60
+ ["key", "value", nil],
61
+ )
62
+
63
+ Importer.new(:fr, csv_path(:fr), import_directory).import
64
+
65
+ lines = File.readlines(File.join(import_directory, "fr.yml"))
66
+ refute lines.any? { |line| line =~ /\s\n$/ }
67
+ end
68
+
69
+ test 'imports arrays from CSV as arrays' do
70
+ given_csv(:fr,
71
+ [:key, :source, :translation],
72
+ ["fruit", ["Apples", "Bananas", "Pears"], ["Pommes", "Bananes", "Poires"]]
73
+ )
74
+
75
+ Importer.new(:fr, csv_path(:fr), import_directory).import
76
+
77
+ yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
78
+ expected = {"fr" => {
79
+ "fruit" => ["Pommes", "Bananes", "Poires"]
80
+ }}
81
+ assert_equal expected, yaml_translation_data
82
+ end
83
+
84
+ test 'interprets string "nil" as nil' do
85
+ given_csv(:fr,
86
+ [:key, :source, :translation],
87
+ ["things", ["one", nil, "two"], ["une", nil, "deux"]]
88
+ )
89
+
90
+ Importer.new(:fr, csv_path(:fr), import_directory).import
91
+
92
+ yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
93
+ expected = {"fr" => {
94
+ "things" => ["une", nil, "deux"]
95
+ }}
96
+ assert_equal expected, yaml_translation_data
97
+ end
98
+
99
+ test 'interprets string ":thing" as symbol' do
100
+ given_csv(:fr,
101
+ [:key, :source, :translation],
102
+ ["sentiment", ":whatever", ":bof"]
103
+ )
104
+
105
+ Importer.new(:fr, csv_path(:fr), import_directory).import
106
+
107
+ yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
108
+ expected = {"fr" => {
109
+ "sentiment" => :bof
110
+ }}
111
+ assert_equal expected, yaml_translation_data
112
+ end
113
+
114
+ test 'interprets integer strings as integers' do
115
+ given_csv(:fr,
116
+ [:key, :source, :translation],
117
+ ["price", "123", "123"]
118
+ )
119
+
120
+ Importer.new(:fr, csv_path(:fr), import_directory).import
121
+
122
+ yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
123
+ expected = {"fr" => {
124
+ "price" => 123
125
+ }}
126
+ assert_equal expected, yaml_translation_data
127
+ end
128
+
129
+ test 'interprets boolean values as booleans, not strings' do
130
+ given_csv(:fr,
131
+ [:key, :source, :translation],
132
+ ["key1", "is true", "true"],
133
+ ["key2", "is false", "false"]
134
+ )
135
+
136
+ Importer.new(:fr, csv_path(:fr), import_directory).import
137
+
138
+ yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
139
+ expected = {"fr" => {
140
+ "key1" => true,
141
+ "key2" => false
142
+ }}
143
+ assert_equal expected, yaml_translation_data
144
+ end
145
+
146
+ private
147
+
148
+ def csv_path(locale)
149
+ File.join(import_directory, "#{locale}.csv")
150
+ end
151
+
152
+ def given_csv(locale, header_row, *rows)
153
+ csv = CSV.generate do |csv|
154
+ csv << CSV::Row.new(["key", "source", "translation"], ["key", "source", "translation"], true)
155
+ rows.each do |row|
156
+ csv << CSV::Row.new(["key", "source", "translation"], row)
157
+ end
158
+ end
159
+ File.open(csv_path(locale), "w") { |f| f.write csv.to_s }
160
+ end
161
+
162
+ def import_directory
163
+ @import_directory ||= Dir.mktmpdir
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+ require 'rails_translation_manager/validator'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+
7
+ module RailsTranslationManager
8
+ class ValidatorTest < Minitest::Test
9
+ def setup
10
+ @translation_path = Dir.mktmpdir
11
+ @translation_validator = Validator.new(@translation_path)
12
+ end
13
+
14
+ def teardown
15
+ FileUtils.remove_entry_secure(@translation_path)
16
+ end
17
+
18
+ def create_translation_file(locale, content)
19
+ File.open(File.join(@translation_path, "#{locale}.yml"), "w") do |f|
20
+ f.write(content.lstrip)
21
+ end
22
+ end
23
+
24
+ test "can create a flattened list of substitutions" do
25
+ translation_file = YAML.load(%q{
26
+ en:
27
+ view: View '%{title}'
28
+ test: foo
29
+ })
30
+ expected = [Validator::TranslationEntry.new(%w{en view}, "View '%{title}'")]
31
+ assert_equal expected, @translation_validator.substitutions_in(translation_file)
32
+ end
33
+
34
+ test "detects extra substitution keys" do
35
+ create_translation_file("en", %q{
36
+ en:
37
+ document:
38
+ view: View '%{title}'
39
+ })
40
+ create_translation_file("sr", %q{
41
+ sr:
42
+ document:
43
+ view: Pročitajte '%{naslov}'
44
+ })
45
+ errors = Validator.new(@translation_path).check!
46
+
47
+ expected = %q{Key "sr.document.view": Extra substitutions: ["naslov"]. Missing substitutions: ["title"].}
48
+ assert_equal [expected], errors.map(&:to_s)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ require 'minitest/autorun'
2
+
3
+ module SimpleTestDsl
4
+ def test(name, &block)
5
+ define_method "test_#{name}", &block
6
+ end
7
+ end
8
+
9
+ Minitest::Test.extend(SimpleTestDsl)
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_translation_manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Edd Sowden
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails-i18n
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: ''
84
+ email:
85
+ - edd.sowden@digital.cabinet-office.gov.uk
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".ruby-version"
92
+ - ".travis.yml"
93
+ - CONTRIBUTING.md
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - lib/rails_translation_manager.rb
99
+ - lib/rails_translation_manager/exporter.rb
100
+ - lib/rails_translation_manager/importer.rb
101
+ - lib/rails_translation_manager/railtie.rb
102
+ - lib/rails_translation_manager/validator.rb
103
+ - lib/rails_translation_manager/version.rb
104
+ - lib/tasks/translation.rake
105
+ - rails_translation_manager.gemspec
106
+ - test/rails_translation_manager/exporter_test.rb
107
+ - test/rails_translation_manager/importer_test.rb
108
+ - test/rails_translation_manager/validator_test.rb
109
+ - test/test_helper.rb
110
+ homepage: ''
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.2.2
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: Tasks to manage translation files
134
+ test_files:
135
+ - test/rails_translation_manager/exporter_test.rb
136
+ - test/rails_translation_manager/importer_test.rb
137
+ - test/rails_translation_manager/validator_test.rb
138
+ - test/test_helper.rb