rails_translation_manager 0.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +23 -0
- data/Jenkinsfile +9 -0
- data/LICENSE.txt +1 -1
- data/README.md +57 -1
- data/Rakefile +14 -9
- data/config/locales/plurals.rb +49 -0
- data/lib/rails_translation_manager/cleaner.rb +14 -0
- data/lib/rails_translation_manager/importer.rb +35 -19
- data/lib/rails_translation_manager/locale_checker/all_locales.rb +61 -0
- data/lib/rails_translation_manager/locale_checker/base_checker.rb +13 -0
- data/lib/rails_translation_manager/locale_checker/incompatible_plurals.rb +79 -0
- data/lib/rails_translation_manager/locale_checker/locale_checker_helper.rb +23 -0
- data/lib/rails_translation_manager/locale_checker/missing_english_locales.rb +31 -0
- data/lib/rails_translation_manager/locale_checker/missing_foreign_locales.rb +31 -0
- data/lib/rails_translation_manager/locale_checker/plural_forms.rb +16 -0
- data/lib/rails_translation_manager/locale_checker.rb +50 -0
- data/lib/rails_translation_manager/version.rb +1 -1
- data/lib/rails_translation_manager/yaml_writer.rb +17 -0
- data/lib/rails_translation_manager.rb +21 -2
- data/lib/tasks/translation.rake +37 -28
- data/lib/tasks/translation_helper.rb +11 -0
- data/rails_translation_manager.gemspec +4 -1
- data/spec/locales/cleaner/clean.yml +5 -0
- data/spec/locales/cleaner/with_whitespace.yml +5 -0
- data/spec/locales/importer/fr.csv +8 -0
- data/spec/locales/in_sync/cy/browse.yml +12 -0
- data/spec/locales/in_sync/en/browse.yml +8 -0
- data/spec/locales/out_of_sync/cy.yml +3 -0
- data/spec/locales/out_of_sync/en.yml +6 -0
- data/spec/rails_translation_manager/cleaner_spec.rb +13 -0
- data/spec/rails_translation_manager/importer_spec.rb +81 -0
- data/spec/rails_translation_manager/locale_checker/all_locales_spec.rb +24 -0
- data/spec/rails_translation_manager/locale_checker/incompatible_plurals_spec.rb +94 -0
- data/spec/rails_translation_manager/locale_checker/locale_checker_helper_spec.rb +61 -0
- data/spec/rails_translation_manager/locale_checker/missing_english_locales_spec.rb +72 -0
- data/spec/rails_translation_manager/locale_checker/missing_foreign_locales_spec.rb +80 -0
- data/spec/rails_translation_manager/locale_checker/plural_forms_spec.rb +27 -0
- data/spec/rails_translation_manager/locale_checker_spec.rb +68 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/tasks.rb +7 -0
- data/spec/tasks/translation_spec.rb +123 -0
- data/test/rails_translation_manager/yaml_writer_test.rb +60 -0
- data/tmp/.gitkeep +0 -0
- metadata +104 -13
- data/lib/rails_translation_manager/validator.rb +0 -92
- data/test/rails_translation_manager/importer_test.rb +0 -166
- data/test/rails_translation_manager/validator_test.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0b6a096ee29746cead2529ea0cb83660c0e0217b710e085790969f203f246871
|
4
|
+
data.tar.gz: 3c13cb54190c17f1382246827c120cf928e7763a9de4b4724ec36ffe6005fc62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37abc2c32de6245dc3d1cd57da56f14ab3c68f88f9b3802a65735d3c36c02984f823d45ff177729daad6b2234929d03535ca28239a4188889a14be60f35875df
|
7
|
+
data.tar.gz: 73f08794d826203ba2be01695751d742bd7a546dee1bf9f8946c98cf57350a48651addd5eef53cfcd9b23303900d6efe0dcd41fb0ae81d67ba060ed851b5061a
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.6.6
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
## 1.1.0
|
2
|
+
|
3
|
+
Allow multiple files per language to be imported. https://github.com/alphagov/rails_translation_manager/pull/20
|
4
|
+
|
5
|
+
## 1.0.0
|
6
|
+
|
7
|
+
Adds logic to verify locale files are in sync with each other and have the
|
8
|
+
correct plural forms. Also introduces RSpec into the Gem which will replace
|
9
|
+
Minitest in the coming iterations.
|
10
|
+
|
11
|
+
Integrates RTM with I18n-tasks. Adds wrappers around `add-missing` & `normalize` tasks, adds Cleaner class to remove the whitespace added by i18n-tasks, adds tests and byebug gem as a debugging tool.
|
12
|
+
|
13
|
+
## 0.1.0
|
14
|
+
|
15
|
+
Don't change the $LOAD_PATH in translation.rake.
|
16
|
+
|
17
|
+
## 0.0.2
|
18
|
+
|
19
|
+
Added `steal` rake tasks to copy translations from another app.
|
20
|
+
|
21
|
+
## 0.0.1
|
22
|
+
|
23
|
+
Initial release.
|
data/Jenkinsfile
ADDED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -14,7 +14,7 @@ Support for translation workflow in rails applications.
|
|
14
14
|
into a translated string of text, for example `Hello %{name}`
|
15
15
|
would become `Hello Sarah` if the variable `name` had the
|
16
16
|
value `Sarah`.
|
17
|
-
- **YAML**:
|
17
|
+
- **YAML**: YAML Ain't Markup Language, a textual data format used (in this case) for storing
|
18
18
|
translation strings in rails applications
|
19
19
|
|
20
20
|
## Technical documentation
|
@@ -124,6 +124,48 @@ $ rake translation:validate
|
|
124
124
|
Success! No unexpected interpolation keys found.
|
125
125
|
```
|
126
126
|
|
127
|
+
### Stealing translations from another app
|
128
|
+
|
129
|
+
A third feature is the ability to "steal" one or more locales from an existing
|
130
|
+
application. This functionality works by providing a mapping file, which defines
|
131
|
+
how the translation keys in the the source app's files map to those in the app
|
132
|
+
the gem is installed in.
|
133
|
+
|
134
|
+
For example, given a locale file like this in the app to "steal" from:
|
135
|
+
|
136
|
+
```yaml
|
137
|
+
es:
|
138
|
+
document:
|
139
|
+
type:
|
140
|
+
case_study: Caso de estudio
|
141
|
+
consultation: Consulta
|
142
|
+
```
|
143
|
+
|
144
|
+
and a mapping file like this:
|
145
|
+
|
146
|
+
```yaml
|
147
|
+
document.type: content_item.format
|
148
|
+
```
|
149
|
+
|
150
|
+
running `rake translation:steal[es,../other_app,mapping_file_path.yml]` will
|
151
|
+
result in the following locale file being created:
|
152
|
+
|
153
|
+
```yaml
|
154
|
+
es:
|
155
|
+
content_item:
|
156
|
+
format:
|
157
|
+
case_study: Caso de estudio
|
158
|
+
consultation: Consulta
|
159
|
+
```
|
160
|
+
|
161
|
+
The mapping file can live anywhere, as long as the full path (including filename)
|
162
|
+
is given in the rake task invocation.
|
163
|
+
|
164
|
+
The process will preserve data already in the output file if it is not
|
165
|
+
referenced in the mapping, but will always override data belonging to keys
|
166
|
+
that are in the mapping.
|
167
|
+
|
168
|
+
|
127
169
|
### Rake command reference
|
128
170
|
|
129
171
|
#### Export a specific locale to CSV
|
@@ -150,6 +192,8 @@ rake translation:import[locale,path]
|
|
150
192
|
rake translation:import:all[directory]
|
151
193
|
```
|
152
194
|
|
195
|
+
####
|
196
|
+
|
153
197
|
#### Regenerate all locales from the EN locale - run this after adding keys
|
154
198
|
|
155
199
|
```
|
@@ -162,6 +206,18 @@ rake translation:regenerate[directory]
|
|
162
206
|
rake translation:validate
|
163
207
|
```
|
164
208
|
|
209
|
+
#### Steal a specific locale file from another app
|
210
|
+
|
211
|
+
```
|
212
|
+
rake translation:steal[locale,source_app_path,mapping_file_path]
|
213
|
+
```
|
214
|
+
|
215
|
+
#### Steal all locale files from another app
|
216
|
+
|
217
|
+
```
|
218
|
+
rake translation:steal:all[source_app_path,mapping_file_path]
|
219
|
+
```
|
220
|
+
|
165
221
|
### Running the test suite
|
166
222
|
|
167
223
|
To run the test suite just run `bundle exec rake` from within the
|
data/Rakefile
CHANGED
@@ -1,12 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'rails_i18n'
|
7
|
+
|
8
|
+
Rake::TestTask.new('test') do |t|
|
9
|
+
t.description = 'Run tests'
|
10
|
+
t.libs << 'test'
|
11
|
+
t.test_files = FileList['test/**/*_test.rb']
|
9
12
|
t.verbose = true
|
10
13
|
end
|
11
14
|
|
12
|
-
|
15
|
+
RSpec::Core::RakeTask.new(:spec)
|
16
|
+
|
17
|
+
task default: %i[spec test]
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
{
|
4
|
+
# Dari - this isn't an iso code. Probably should be 'prs' as per ISO 639-3.
|
5
|
+
dr: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
6
|
+
# Latin America and Caribbean Spanish
|
7
|
+
"es-419": { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
8
|
+
# Scottish Gaelic
|
9
|
+
gd: { i18n: { plural: { keys: %i[one two few other],
|
10
|
+
rule: lambda do |n|
|
11
|
+
if [1, 11].include?(n)
|
12
|
+
:one
|
13
|
+
elsif [2, 12].include?(n)
|
14
|
+
:two
|
15
|
+
elsif [3, 4, 5, 6, 7, 8, 9, 10, 13, 14, 15, 16, 17, 18, 19].include?(n)
|
16
|
+
:few
|
17
|
+
else
|
18
|
+
:other
|
19
|
+
end
|
20
|
+
end } } },
|
21
|
+
# Gujarati
|
22
|
+
gu: { i18n: { plural: { keys: %i[one other], rule: ->(n) { [0, 1].include?(n) ? :one : :other } } } },
|
23
|
+
# Armenian
|
24
|
+
hy: { i18n: { plural: { keys: %i[one other], rule: ->(n) { [0, 1].include?(n) ? :one : :other } } } },
|
25
|
+
# Kazakh
|
26
|
+
kk: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
27
|
+
# Pashto
|
28
|
+
ps: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
29
|
+
# Punjabi Shahmukhi
|
30
|
+
"pa-pk": { i18n: { plural: { keys: %i[one other], rule: ->(n) { [0, 1].include?(n) ? :one : :other } } } },
|
31
|
+
# Sinhalese
|
32
|
+
si: { i18n: { plural: { keys: %i[one other], rule: ->(n) { [0, 1].include?(n) ? :one : :other } } } },
|
33
|
+
# Somali
|
34
|
+
so: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
35
|
+
# Albanian
|
36
|
+
sq: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
37
|
+
# Norwegian
|
38
|
+
no: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
39
|
+
# Tamil
|
40
|
+
ta: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
41
|
+
# Turkmen
|
42
|
+
tk: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
43
|
+
# Uzbek
|
44
|
+
uz: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
|
45
|
+
# Chinese Hong Kong
|
46
|
+
'zh-hk' => { i18n: { plural: { keys: %i[other], rule: -> { :other } } } },
|
47
|
+
# Chinese Taiwan
|
48
|
+
'zh-tw' => { i18n: { plural: { keys: %i[other], rule: -> { :other } } } }
|
49
|
+
}
|
@@ -1,42 +1,43 @@
|
|
1
1
|
require "yaml"
|
2
2
|
require "csv"
|
3
|
+
require_relative "yaml_writer"
|
3
4
|
|
4
5
|
class RailsTranslationManager::Importer
|
5
|
-
|
6
|
-
|
6
|
+
include YAMLWriter
|
7
|
+
|
8
|
+
attr_reader :locale, :csv_path, :import_directory, :multiple_files_per_language
|
9
|
+
|
10
|
+
def initialize(locale:, csv_path:, import_directory:, multiple_files_per_language:)
|
7
11
|
@locale = locale
|
12
|
+
@csv_path = csv_path
|
8
13
|
@import_directory = import_directory
|
14
|
+
@multiple_files_per_language = multiple_files_per_language
|
9
15
|
end
|
10
16
|
|
11
17
|
def import
|
12
|
-
csv = CSV.read(
|
13
|
-
|
14
|
-
csv
|
18
|
+
csv = CSV.read(csv_path, headers: true, header_converters: :downcase)
|
19
|
+
|
20
|
+
multiple_files_per_language ? import_csv_into_multiple_files(csv) : import_csv(csv)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def import_csv(csv, import_yml_path = File.join(import_directory, "#{locale}.yml"))
|
26
|
+
data = csv.each_with_object({}) do |row, hash|
|
15
27
|
key = row["key"]
|
16
28
|
key_parts = key.split(".")
|
17
29
|
if key_parts.length > 1
|
18
|
-
leaf_node = (
|
30
|
+
leaf_node = (hash[key_parts.first] ||= {})
|
19
31
|
key_parts[1..-2].each do |part|
|
20
32
|
leaf_node = (leaf_node[part] ||= {})
|
21
33
|
end
|
22
34
|
leaf_node[key_parts.last] = parse_translation(row["translation"])
|
23
35
|
else
|
24
|
-
|
36
|
+
hash[key_parts.first] = parse_translation(row["translation"])
|
25
37
|
end
|
26
38
|
end
|
27
39
|
|
28
|
-
|
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
|
+
write_yaml(import_yml_path, { locale.to_s => data })
|
40
41
|
end
|
41
42
|
|
42
43
|
def parse_translation(translation)
|
@@ -57,4 +58,19 @@ class RailsTranslationManager::Importer
|
|
57
58
|
translation
|
58
59
|
end
|
59
60
|
end
|
61
|
+
|
62
|
+
def import_csv_into_multiple_files(csv)
|
63
|
+
group_csv_by_file(csv).each do |group|
|
64
|
+
language_dir = File.join(import_directory, locale)
|
65
|
+
|
66
|
+
Dir.mkdir(language_dir) unless Dir.exists?(language_dir)
|
67
|
+
|
68
|
+
import_yml_path = File.join(import_directory, locale, "#{group[0]}.yml")
|
69
|
+
import_csv(group[1], import_yml_path)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def group_csv_by_file(csv)
|
74
|
+
csv.group_by { |row| row["key"].split(".").first }
|
75
|
+
end
|
60
76
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AllLocales
|
4
|
+
attr_reader :locale_path
|
5
|
+
|
6
|
+
def initialize(locale_path)
|
7
|
+
@locale_path = locale_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def generate
|
11
|
+
paths = locale_file_paths.compact
|
12
|
+
|
13
|
+
raise NoLocaleFilesFound, "No locale files found for the supplied path" if paths.blank?
|
14
|
+
|
15
|
+
paths.flat_map do |locale_group|
|
16
|
+
{
|
17
|
+
locale: locale_group[:locale],
|
18
|
+
keys: all_keys_for_locale(locale_group)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def locale_file_paths
|
26
|
+
I18n.available_locales.map do |locale|
|
27
|
+
grouped_paths = Dir[locale_path].select do |path|
|
28
|
+
locale = locale.downcase
|
29
|
+
|
30
|
+
path =~ %r{/#{locale}/} || path =~ %r{/#{locale}\.yml$}
|
31
|
+
end
|
32
|
+
|
33
|
+
{ locale: locale.downcase, paths: grouped_paths } if grouped_paths.present?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_keys_for_locale(locale_group)
|
38
|
+
locale_group[:paths].flat_map do |path|
|
39
|
+
keys_from_file(file_path: path)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def keys_from_file(locale_hash: nil, key_chain: nil, locale_keys: [], file_path: nil)
|
44
|
+
locale_hash ||= YAML.load_file(file_path)
|
45
|
+
keys = locale_hash.keys
|
46
|
+
keys.each do |key|
|
47
|
+
if locale_hash.fetch(key).is_a?(Hash)
|
48
|
+
keys_from_file(locale_hash: locale_hash.fetch(key), key_chain: "#{key_chain}.#{key}", locale_keys: locale_keys)
|
49
|
+
else
|
50
|
+
keys.each do |final_key|
|
51
|
+
locale_keys << "#{key_chain}.#{final_key}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# remove locale prefix from keys, e.g: ".en.browse.page" -> "browse.page"
|
57
|
+
locale_keys.uniq.map { |key| key.split(".")[2..].join(".") }
|
58
|
+
end
|
59
|
+
|
60
|
+
class NoLocaleFilesFound < StandardError; end
|
61
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class IncompatiblePlurals < BaseChecker
|
4
|
+
include LocaleCheckerHelper
|
5
|
+
|
6
|
+
def report
|
7
|
+
format_plural_errors if plural_errors.present?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def format_plural_errors
|
13
|
+
<<~OUTPUT.chomp
|
14
|
+
\e[31m[ERROR]\e[0m Incompatible plural forms, for:
|
15
|
+
|
16
|
+
#{plural_errors.join("\n\n")}
|
17
|
+
|
18
|
+
\e[1mIf the keys reported above are not plurals, rename them avoiding plural keywords: #{PLURAL_KEYS}\e[22m
|
19
|
+
OUTPUT
|
20
|
+
end
|
21
|
+
|
22
|
+
def plural_errors
|
23
|
+
@plural_errors ||= grouped_plural_keys_for_locale.flat_map do |plurals|
|
24
|
+
plural_form = all_plural_forms[plurals[:locale]]
|
25
|
+
|
26
|
+
if plural_form.blank?
|
27
|
+
"- \e[31m[ERROR]\e[0m Please add plural form for '#{plurals[:locale]}' <link to future documentation>"
|
28
|
+
else
|
29
|
+
incompatible_plurals(plurals, plural_form)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def incompatible_plurals(plurals, plural_form)
|
35
|
+
error_messages = plurals[:groups].map do |plural_group|
|
36
|
+
actual_plural_form = plural_group[:keys].sort
|
37
|
+
|
38
|
+
next if actual_plural_form == plural_form.sort
|
39
|
+
|
40
|
+
"- '#{plurals[:locale]}', with parent '#{plural_group[:parent]}'. Expected: #{plural_form}, actual: #{actual_plural_form}"
|
41
|
+
end
|
42
|
+
|
43
|
+
error_messages.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
def grouped_plural_keys_for_locale
|
47
|
+
all_locales.map do |locale|
|
48
|
+
{
|
49
|
+
locale: locale[:locale],
|
50
|
+
groups: grouped_plural_keys(only_plurals(locale[:keys]))
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def grouped_plural_keys(keys)
|
56
|
+
# %w[parent.key parent.other_key] -> [{ parent: "parent", keys: %w[key other_key] }]
|
57
|
+
group_by_parent_keys(keys).map do |plural_key_group|
|
58
|
+
{
|
59
|
+
parent: plural_key_group.first,
|
60
|
+
keys: plural_key_group.last.map { |key_chain| key_chain.split(".").last.to_sym }
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def group_by_parent_keys(keys)
|
66
|
+
# %w[parent.key parent.other_key] -> { "parent" => %w[parent.key parent.other_key] }
|
67
|
+
keys.group_by do |key|
|
68
|
+
key.split('.')[0..-2].join('.')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def only_plurals(keys)
|
73
|
+
keys.select { |key| key_is_plural?(key) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def all_plural_forms
|
77
|
+
@all_plural_forms ||= PluralForms.all
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LocaleCheckerHelper
|
4
|
+
PLURAL_KEYS = %w[zero one two few many other].freeze
|
5
|
+
|
6
|
+
def exclude_plurals(keys)
|
7
|
+
keys.reject { |key| key_is_plural?(key) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def group_keys(locales)
|
11
|
+
locales.keys.group_by do |key|
|
12
|
+
locales[key]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def english_keys_excluding_plurals(all_locales)
|
17
|
+
exclude_plurals(all_locales.find { |locale| locale[:locale] == :en }[:keys])
|
18
|
+
end
|
19
|
+
|
20
|
+
def key_is_plural?(key)
|
21
|
+
PLURAL_KEYS.include?(key.split(".").last)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MissingEnglishLocales < BaseChecker
|
4
|
+
include LocaleCheckerHelper
|
5
|
+
|
6
|
+
def report
|
7
|
+
grouped_missing_keys = group_keys(missing_english_locales)
|
8
|
+
|
9
|
+
format_missing_english_locales(grouped_missing_keys) if grouped_missing_keys.present?
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def format_missing_english_locales(keys)
|
15
|
+
formatted_keys = keys.to_a.map do |group|
|
16
|
+
"\e[1mMissing English keys:\e[22m #{group[0]}\n\e[1mFound in:\e[22m #{group[1]}"
|
17
|
+
end
|
18
|
+
|
19
|
+
"\e[31m[ERROR]\e[0m Missing English locales, either remove these keys from the foreign locales or add them to the English locales\n\n#{formatted_keys.join("\n\n")}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def missing_english_locales
|
23
|
+
all_locales.each_with_object({}) do |locale, hsh|
|
24
|
+
missing_keys = exclude_plurals(locale[:keys]) - english_keys_excluding_plurals(all_locales)
|
25
|
+
|
26
|
+
next if missing_keys.blank?
|
27
|
+
|
28
|
+
hsh[locale[:locale]] = missing_keys
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MissingForeignLocales < BaseChecker
|
4
|
+
include LocaleCheckerHelper
|
5
|
+
|
6
|
+
def report
|
7
|
+
grouped_missing_keys = group_keys(missing_foreign_locales)
|
8
|
+
|
9
|
+
format_missing_foreign_locales(grouped_missing_keys) if grouped_missing_keys.present?
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def format_missing_foreign_locales(keys)
|
15
|
+
formatted_keys = keys.to_a.map do |group|
|
16
|
+
"\e[1mMissing foreign keys:\e[22m #{group[0]}\n\e[1mAbsent from:\e[22m #{group[1]}"
|
17
|
+
end
|
18
|
+
|
19
|
+
"\e[31m[ERROR]\e[0m Missing foreign locales, either add these keys to the foreign locales or remove them from the English locales\e[0m\n\n#{formatted_keys.join("\n\n")}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def missing_foreign_locales
|
23
|
+
all_locales.each_with_object({}) do |locale, hsh|
|
24
|
+
missing_keys = english_keys_excluding_plurals(all_locales) - exclude_plurals(locale[:keys])
|
25
|
+
|
26
|
+
next if missing_keys.blank?
|
27
|
+
|
28
|
+
hsh[locale[:locale]] = missing_keys
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class PluralForms
|
4
|
+
def self.all
|
5
|
+
I18n.available_locales.each_with_object({}) do |locale, hsh|
|
6
|
+
begin
|
7
|
+
# fetches plural form (rule) from rails-i18n for locale
|
8
|
+
plural_form = I18n.with_locale(locale) { I18n.t!("i18n.plural.keys") }.sort
|
9
|
+
rescue I18n::MissingTranslationData
|
10
|
+
plural_form = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
hsh[locale.downcase] = plural_form
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsTranslationManager
|
4
|
+
class LocaleChecker
|
5
|
+
attr_reader :locale_path
|
6
|
+
|
7
|
+
CHECKER_CLASSES = [MissingEnglishLocales,
|
8
|
+
MissingForeignLocales,
|
9
|
+
IncompatiblePlurals].freeze
|
10
|
+
|
11
|
+
def initialize(locale_path)
|
12
|
+
@locale_path = locale_path
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate_locales
|
16
|
+
output_result
|
17
|
+
rescue AllLocales::NoLocaleFilesFound => e
|
18
|
+
puts e
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def output_result
|
25
|
+
errors = checker_errors.compact
|
26
|
+
|
27
|
+
if errors.blank?
|
28
|
+
puts "Locale files are in sync, nice job!"
|
29
|
+
true
|
30
|
+
else
|
31
|
+
errors.each do |error_message|
|
32
|
+
puts "\n"
|
33
|
+
puts error_message
|
34
|
+
puts "\n"
|
35
|
+
end
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def checker_errors
|
41
|
+
CHECKER_CLASSES.flat_map do |checker|
|
42
|
+
checker.new(all_locales).report
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def all_locales
|
47
|
+
@all_locales ||= AllLocales.new(locale_path).generate
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module YAMLWriter
|
4
|
+
|
5
|
+
# `to_yaml` outputs an initial '---\n', which is supposed to be a document
|
6
|
+
# separator. We don't want this in the written locale files, so we serialize
|
7
|
+
# to a string, remove the header and any trailing whitehspace, and write to
|
8
|
+
# the file.
|
9
|
+
def write_yaml(filepath, data)
|
10
|
+
File.open(filepath, "w") do |f|
|
11
|
+
yaml = data.to_yaml(separator: "")
|
12
|
+
yaml_without_header = yaml.split("\n").map { |l| l.gsub(/\s+$/, '') }[1..-1].join("\n")
|
13
|
+
f.write(yaml_without_header)
|
14
|
+
f.puts
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|