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 +7 -0
- data/.gitignore +14 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/CONTRIBUTING.md +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +188 -0
- data/Rakefile +12 -0
- data/lib/rails_translation_manager/exporter.rb +111 -0
- data/lib/rails_translation_manager/importer.rb +60 -0
- data/lib/rails_translation_manager/railtie.rb +9 -0
- data/lib/rails_translation_manager/validator.rb +92 -0
- data/lib/rails_translation_manager/version.rb +3 -0
- data/lib/rails_translation_manager.rb +8 -0
- data/lib/tasks/translation.rake +70 -0
- data/rails_translation_manager.gemspec +27 -0
- data/test/rails_translation_manager/exporter_test.rb +239 -0
- data/test/rails_translation_manager/importer_test.rb +166 -0
- data/test/rails_translation_manager/validator_test.rb +51 -0
- data/test/test_helper.rb +9 -0
- metadata +138 -0
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
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.4
|
data/.travis.yml
ADDED
data/CONTRIBUTING.md
ADDED
data/Gemfile
ADDED
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,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,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
|
data/test/test_helper.rb
ADDED
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
|