rails_translation_manager 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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