i18n-migrations 0.1.1 → 0.1.2
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 +4 -4
- data/.i18n-migrations.default.yml +6 -1
- data/README.md +31 -1
- data/bin/i18n-migrate +11 -2
- data/i18n-migrations.gemspec +1 -0
- data/lib/i18n/migrations/config.rb +4 -0
- data/lib/i18n/migrations/google_spreadsheet.rb +3 -4
- data/lib/i18n/migrations/google_translate_dictionary.rb +57 -5
- data/lib/i18n/migrations/migration.rb +10 -4
- data/lib/i18n/migrations/migrator.rb +69 -12
- data/lib/i18n/migrations/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2789bfb627b7453b272680ee6eede9f6bedbfacf
|
4
|
+
data.tar.gz: fc625101156e0217ece5a1fe2379686e7f4401a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e74ac7d4a4e593cca6422d7be921ec73b3e3a188190bed95a097a54adafd1f9942858724a3cfc548329f6083941575f6bc3b8a598929d548908e766eb5cfa96
|
7
|
+
data.tar.gz: 85815a347cb2fc71bdba8ef5d0ffb711336b90c9a6d15d26d4b539487acb3c23ff73a313a408f47e2e2f0e97ee6b4374424cbe196cdf679f870eeb0cc051c8a4
|
@@ -16,7 +16,12 @@ google_service_account_key_path: i18n/google_drive_key.json
|
|
16
16
|
|
17
17
|
# each locale will have a spreadsheet that translators will use to do their work, these are the links to them
|
18
18
|
google_spreadsheets:
|
19
|
-
es: https://docs.google.com/spreadsheets/d/
|
19
|
+
es: https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit
|
20
20
|
|
21
21
|
# this is your api key to use google translate
|
22
22
|
google_translate_api_key: [INSERT_GOOGLE_TRANSLATE_API_KEY]
|
23
|
+
|
24
|
+
# put things like your product name here along with any possible mistranslations
|
25
|
+
do_not_translate:
|
26
|
+
"I18n Migrations":
|
27
|
+
- I18n Migraciones
|
data/README.md
CHANGED
@@ -27,10 +27,40 @@ And then execute:
|
|
27
27
|
Or install it yourself as:
|
28
28
|
|
29
29
|
$ gem install i18n-migrations
|
30
|
+
|
31
|
+
From your project file, you'll want to run
|
32
|
+
|
33
|
+
$ i18n-migrate setup
|
34
|
+
|
35
|
+
This will create a config file you can edit.
|
30
36
|
|
31
37
|
## Usage
|
32
38
|
|
33
|
-
|
39
|
+
Let's imagine that your config file look like this:
|
40
|
+
|
41
|
+
migration_dir: i18n/migrate
|
42
|
+
locales_dir: config/locales
|
43
|
+
main_locale: en
|
44
|
+
other_locales:
|
45
|
+
- es
|
46
|
+
..
|
47
|
+
|
48
|
+
In your project file, you should then have all your english terms in ```config/locales/en.yml```
|
49
|
+
|
50
|
+
To create a new locale (like es.yml):
|
51
|
+
|
52
|
+
1. Translate all the terms w/ google translate
|
53
|
+
|
54
|
+
> i18n-migrate new_locale es
|
55
|
+
|
56
|
+
2. Create a spreadsheet that is world editable (for now). You'll want to add the link to it to your config file. It should look like:
|
57
|
+
|
58
|
+
| key | en | es | notes |
|
59
|
+
|
60
|
+
2. Push this to your google spreadsheet (the -f means it won't try to pull first)
|
61
|
+
|
62
|
+
> i18n-migrate push -f es
|
63
|
+
|
34
64
|
|
35
65
|
## Development
|
36
66
|
|
data/bin/i18n-migrate
CHANGED
@@ -5,6 +5,10 @@ require_relative '../lib/i18n/migrations/config'
|
|
5
5
|
|
6
6
|
migrator = I18n::Migrations::Migrator.new
|
7
7
|
|
8
|
+
def extract_option(name)
|
9
|
+
!!ARGV.delete(name)
|
10
|
+
end
|
11
|
+
|
8
12
|
case ARGV.shift
|
9
13
|
when 'setup'
|
10
14
|
puts 'Where should we create a default config file? [.]'
|
@@ -38,7 +42,11 @@ case ARGV.shift
|
|
38
42
|
migrator.pull ARGV[0] || 'all'
|
39
43
|
|
40
44
|
when 'push'
|
41
|
-
|
45
|
+
force = extract_option('-f')
|
46
|
+
migrator.push(ARGV[0] || 'all', force)
|
47
|
+
|
48
|
+
when 'validate'
|
49
|
+
migrator.validate(ARGV[0] || 'all')
|
42
50
|
|
43
51
|
when 'new_locale'
|
44
52
|
locale = ARGV.shift
|
@@ -63,7 +71,8 @@ Commands:
|
|
63
71
|
rollback - Rollback to previous version.
|
64
72
|
redo - Rollback and then migrate again.
|
65
73
|
pull - Pull latest translation spreadsheet.
|
66
|
-
push - Push to translation spreadsheet.
|
74
|
+
push - Push to translation spreadsheet. (-f to force, without doing a pull first)
|
75
|
+
validate - check all translations according to our rules and fix what we can
|
67
76
|
new_locale - Copy your current main locale file to a new language, translating all keys.
|
68
77
|
version - Print version of locales.
|
69
78
|
|
data/i18n-migrations.gemspec
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
require 'google_drive'
|
2
|
-
require 'config'
|
3
2
|
|
4
3
|
module I18n
|
5
4
|
module Migrations
|
6
5
|
class GoogleSpreadsheet
|
7
6
|
attr_reader :sheet
|
8
7
|
|
9
|
-
def initialize(locale)
|
10
|
-
@session = GoogleDrive::Session.from_service_account_key(
|
8
|
+
def initialize(locale, spreadsheet_url, key_path)
|
9
|
+
@session = GoogleDrive::Session.from_service_account_key(key_path)
|
11
10
|
|
12
|
-
url =
|
11
|
+
url = spreadsheet_url || raise("Can't find google spreadsheet for #{locale}")
|
13
12
|
@spreadsheet = @session.spreadsheet_by_url(url)
|
14
13
|
@sheet = sheet_for("Sheet1")
|
15
14
|
end
|
@@ -3,21 +3,73 @@ require 'rest-client'
|
|
3
3
|
module I18n
|
4
4
|
module Migrations
|
5
5
|
class GoogleTranslateDictionary
|
6
|
-
def initialize(
|
7
|
-
@
|
6
|
+
def initialize(from_locale, to_locale, key, do_not_translate)
|
7
|
+
@from_locale, @to_locale, @key, @do_not_translate = from_locale, to_locale, key, do_not_translate
|
8
8
|
end
|
9
9
|
|
10
|
+
# returns [translated term, notes]
|
10
11
|
def lookup(term)
|
11
|
-
return term if @from_locale == @to_locale
|
12
|
+
return [term, ''] if @from_locale == @to_locale
|
12
13
|
|
13
14
|
response = RestClient.get 'https://www.googleapis.com/language/translate/v2', {
|
14
15
|
accept: :json,
|
15
16
|
params: { key: @key, source: @from_locale, target: @to_locale, q: term }
|
16
17
|
}
|
17
|
-
JSON.parse(response.body)['data']['translations'].first['translatedText']
|
18
|
+
translated_term = JSON.parse(response.body)['data']['translations'].first['translatedText']
|
19
|
+
translated_term, errors = fix(term, translated_term)
|
20
|
+
unless errors.empty?
|
21
|
+
STDERR.puts "'#{term}' => '#{translated_term}'\n#{errors.join(', ').red}"
|
22
|
+
end
|
23
|
+
[translated_term, (errors.map{|e| "[error: #{e}]"} + ['[autotranslated]']).join("\n")]
|
18
24
|
end
|
19
25
|
|
20
|
-
|
26
|
+
VARIABLE_STRING = /%\{[^\}]+\}/
|
27
|
+
# returns updated after term, errors
|
28
|
+
def fix(before, after)
|
29
|
+
errors = []
|
30
|
+
|
31
|
+
# do not translate
|
32
|
+
@do_not_translate.each do |term, bad_translations|
|
33
|
+
if before.include?(term) && !after.include?(term)
|
34
|
+
if (translation = find_included_translation(after, bad_translations))
|
35
|
+
after = after.gsub(translation, term)
|
36
|
+
else
|
37
|
+
errors << "missing #{term}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# common mistakes
|
43
|
+
after = after.gsub('% {', '%{')
|
44
|
+
|
45
|
+
# match up variables, should have same variable in before and after
|
46
|
+
before_variables = before.scan(VARIABLE_STRING)
|
47
|
+
after_variables = after.scan(VARIABLE_STRING)
|
48
|
+
|
49
|
+
if before_variables.sort != after_variables.sort
|
50
|
+
missing = before_variables - after_variables
|
51
|
+
extra = after_variables - before_variables
|
52
|
+
|
53
|
+
# we'll try to fix if it looks easy
|
54
|
+
if missing.length == 1 && extra.length == 1
|
55
|
+
after = after.sub(extra.first, missing.first)
|
56
|
+
else
|
57
|
+
errors << "missing #{missing.join(', ')}" if missing.length > 0
|
58
|
+
errors << "extra #{extra.join(', ')}" if extra.length > 0
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
[after, errors]
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def find_included_translation(after, bad_translations)
|
68
|
+
bad_translations.each do |translation|
|
69
|
+
return translation if after.include?(translation)
|
70
|
+
end
|
71
|
+
nil
|
72
|
+
end
|
21
73
|
end
|
22
74
|
end
|
23
75
|
end
|
@@ -57,8 +57,7 @@ module I18n
|
|
57
57
|
|
58
58
|
def _add(key, term, overrides)
|
59
59
|
assert_does_not_exist! key
|
60
|
-
|
61
|
-
@notes[key] = "[autotranslated]"
|
60
|
+
assign_translation(key, term, overrides)
|
62
61
|
end
|
63
62
|
|
64
63
|
def _mv(from, to)
|
@@ -69,8 +68,7 @@ module I18n
|
|
69
68
|
|
70
69
|
def _update(key, term, overrides)
|
71
70
|
assert_exists! key
|
72
|
-
|
73
|
-
@notes[key] = "[autotranslated]"
|
71
|
+
assign_translation(key, term, overrides)
|
74
72
|
end
|
75
73
|
|
76
74
|
def _rm(key)
|
@@ -90,6 +88,14 @@ module I18n
|
|
90
88
|
def assert_does_not_exist!(key)
|
91
89
|
raise "#{key} already exists in #{@locale}" if @data.has_key?(key)
|
92
90
|
end
|
91
|
+
|
92
|
+
def assign_translation(key, term, overrides)
|
93
|
+
if overrides[@locale.to_sym]
|
94
|
+
@data[key] = overrides[@locale.to_sym]
|
95
|
+
else
|
96
|
+
@data[key], @notes[key] = @dictionary.lookup(term)
|
97
|
+
end
|
98
|
+
end
|
93
99
|
end
|
94
100
|
end
|
95
101
|
end
|
@@ -2,6 +2,7 @@ require 'fileutils'
|
|
2
2
|
require 'yaml'
|
3
3
|
require 'active_support/inflector'
|
4
4
|
require 'active_support/core_ext/object'
|
5
|
+
require 'colorize'
|
5
6
|
|
6
7
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
8
|
require 'google_translate_dictionary'
|
@@ -55,30 +56,36 @@ end
|
|
55
56
|
def pull(locale_or_all)
|
56
57
|
each_locale(locale_or_all) do |locale|
|
57
58
|
next if locale == config.main_locale
|
58
|
-
sheet =
|
59
|
+
sheet = get_google_spreadsheet(locale)
|
59
60
|
pull_locale(locale, sheet)
|
60
61
|
migrate(locale)
|
61
62
|
end
|
62
63
|
end
|
63
64
|
|
64
|
-
def push(locale_or_all)
|
65
|
+
def push(locale_or_all, force = false)
|
65
66
|
each_locale(locale_or_all) do |locale|
|
66
67
|
next if locale == config.main_locale
|
67
|
-
sheet =
|
68
|
-
|
69
|
-
|
68
|
+
sheet = get_google_spreadsheet(locale)
|
69
|
+
unless force
|
70
|
+
pull_locale(locale, sheet)
|
71
|
+
migrate(locale)
|
72
|
+
end
|
70
73
|
push_locale(locale, sheet)
|
71
74
|
end
|
72
75
|
end
|
73
76
|
|
74
|
-
def new_locale(new_locale)
|
75
|
-
dictionary =
|
77
|
+
def new_locale(new_locale, limit = nil)
|
78
|
+
dictionary = new_dictionary(new_locale)
|
76
79
|
new_data, new_notes = {}, {}
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
+
count = 0
|
81
|
+
main_data = read_locale_data(config.main_locale)
|
82
|
+
main_data.each do |key, term|
|
83
|
+
new_data[key], new_notes[key] = dictionary.lookup(term)
|
80
84
|
print '.'.green
|
85
|
+
break if limit && limit < count += 1
|
81
86
|
end
|
87
|
+
new_data['VERSION'] = main_data['VERSION']
|
88
|
+
puts
|
82
89
|
write_locale_data_and_notes(new_locale, new_data, new_notes)
|
83
90
|
end
|
84
91
|
|
@@ -88,8 +95,48 @@ end
|
|
88
95
|
end
|
89
96
|
end
|
90
97
|
|
98
|
+
def validate(locale_or_all)
|
99
|
+
each_locale(locale_or_all) do |locale|
|
100
|
+
next if locale == config.main_locale
|
101
|
+
update_locale_info(locale) do |data, notes|
|
102
|
+
validate_locale(locale, data, notes)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
91
107
|
private
|
92
108
|
|
109
|
+
def validate_locale(locale, data, notes)
|
110
|
+
main_data = read_locale_data(config.main_locale)
|
111
|
+
dict = new_dictionary(locale)
|
112
|
+
main_data.each do |key, main_term|
|
113
|
+
old_term = data[key]
|
114
|
+
new_term, errors = dict.fix(main_term, old_term)
|
115
|
+
if new_term != old_term
|
116
|
+
data[key] = new_term
|
117
|
+
puts "#{"Fix".green} #{key.green}:"
|
118
|
+
puts "#{config.main_locale}: #{main_term}"
|
119
|
+
puts "#{locale} (old): #{old_term}"
|
120
|
+
puts "#{locale} (new): #{new_term}"
|
121
|
+
end
|
122
|
+
replace_errors_in_notes(notes, key, errors)
|
123
|
+
if errors.length > 0
|
124
|
+
puts "Error #{errors.join(', ')} #{key}"
|
125
|
+
puts "#{config.main_locale}: #{main_term}"
|
126
|
+
puts "#{locale}: #{old_term}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def replace_errors_in_notes(all_notes, key, errors)
|
132
|
+
return if all_notes[key].blank? && errors.empty?
|
133
|
+
|
134
|
+
notes = all_notes[key]
|
135
|
+
notes = notes.present? ? notes.split("\n") : []
|
136
|
+
notes = notes.reject { |n| n.start_with?("[error:") }
|
137
|
+
all_notes[key] = (errors + notes).join("\n")
|
138
|
+
end
|
139
|
+
|
93
140
|
def update_locale_info(locale)
|
94
141
|
data, notes = read_locale_data_and_notes(locale)
|
95
142
|
yield data, notes
|
@@ -164,7 +211,7 @@ end
|
|
164
211
|
end
|
165
212
|
|
166
213
|
def migrate_locale(locale, data, notes)
|
167
|
-
missing_versions = all_versions - locale_versions(data)
|
214
|
+
missing_versions = (all_versions - locale_versions(data)).sort
|
168
215
|
if missing_versions.empty?
|
169
216
|
puts "#{locale}: up-to-date"
|
170
217
|
return
|
@@ -191,7 +238,7 @@ end
|
|
191
238
|
filename = File.join(config.migration_dir, "#{version}.rb")
|
192
239
|
require filename
|
193
240
|
migration_class_name = version.gsub(/^\d{12}_/, '').camelcase
|
194
|
-
dictionary =
|
241
|
+
dictionary = new_dictionary(locale)
|
195
242
|
|
196
243
|
migration = begin
|
197
244
|
migration_class_name.constantize.new(locale, data, notes, dictionary, direction)
|
@@ -273,6 +320,16 @@ end
|
|
273
320
|
def all_versions
|
274
321
|
Dir[config.migration_dir + '/*.rb'].map { |name| File.basename(name).gsub('.rb', '') }
|
275
322
|
end
|
323
|
+
|
324
|
+
def new_dictionary(locale)
|
325
|
+
GoogleTranslateDictionary.new(config.main_locale, locale, config.google_translate_api_key, config.do_not_translate)
|
326
|
+
end
|
327
|
+
|
328
|
+
def get_google_spreadsheet(locale)
|
329
|
+
GoogleSpreadsheet.new(locale,
|
330
|
+
config.google_spreadsheets[locale],
|
331
|
+
config.google_service_account_key_path).sheet
|
332
|
+
end
|
276
333
|
end
|
277
334
|
end
|
278
335
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: i18n-migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Lightsmith
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-11-
|
11
|
+
date: 2017-11-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: colorize
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
description: We help you manage your locale translations with migrations, just the
|
84
98
|
way Active Record helps you manage your db with migrations.
|
85
99
|
email:
|