rails_translation_manager 0.0.1 → 1.1.0
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 +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
|