i18n-migrations 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1d5b7ed6e5c2a76bacd9a1d12916d0f7fbba5c0d
4
- data.tar.gz: ba7a796930572af0d6fa2f6f8ff9433c1f0b9032
3
+ metadata.gz: 2789bfb627b7453b272680ee6eede9f6bedbfacf
4
+ data.tar.gz: fc625101156e0217ece5a1fe2379686e7f4401a6
5
5
  SHA512:
6
- metadata.gz: 1ce65173fa1d15de262b216f589eeec8885780a329a65e03866b1066e677546e56f88c2e52b84c2e3dac9c4ccf93aa4db0ca09bf2bab8798b929500a676f63aa
7
- data.tar.gz: 7dd74b2511050c59a71e679009a07997d0a11183ca17c0d1c76e40f15ea7c5afbb49406fc3cdc71272d3fcb2485a93855e52f352227087b213f7ac8758154938
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/1QGCzMbTyxF2gSUpWwRKAq-mMFIgJwsOLp1lfXsSF5X0/edit
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
- TODO: Write usage instructions here
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
- migrator.push ARGV[0] || 'all'
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
 
@@ -28,4 +28,5 @@ Gem::Specification.new do |spec|
28
28
  spec.add_dependency 'google_drive'
29
29
  spec.add_dependency 'activesupport'
30
30
  spec.add_dependency 'rest-client'
31
+ spec.add_dependency 'colorize'
31
32
  end
@@ -33,6 +33,10 @@ module I18n
33
33
  get_value(:google_translate_api_key)
34
34
  end
35
35
 
36
+ def do_not_translate
37
+ get_value(:do_not_translate)
38
+ end
39
+
36
40
  def read!
37
41
  yaml_file = find_config_file(CONFIG_FILE_NAME)
38
42
  unless yaml_file
@@ -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(Config.google_service_account_key_path)
8
+ def initialize(locale, spreadsheet_url, key_path)
9
+ @session = GoogleDrive::Session.from_service_account_key(key_path)
11
10
 
12
- url = Config.google_spreadsheets[locale] || raise("Can't find google spreadsheet for #{locale}")
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(key, from_locale, to_locale)
7
- @key, @from_locale, @to_locale = key, from_locale, to_locale
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
- # we should validate translations based on reasonable rules
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
- @data[key] = overrides[@locale.to_sym] || @dictionary.lookup(term)
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
- @data[key] = overrides[@locale.to_sym] || @dictionary.lookup(term)
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 = GoogleSpreadsheet.new(locale).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 = GoogleSpreadsheet.new(locale).sheet
68
- pull_locale(locale, sheet)
69
- migrate(locale)
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 = GoogleTranslateDictionary.new(config.google_translate_api_key, config.main_locale, new_locale)
77
+ def new_locale(new_locale, limit = nil)
78
+ dictionary = new_dictionary(new_locale)
76
79
  new_data, new_notes = {}, {}
77
- read_locale_data(config.main_locale).each do |key, term|
78
- new_data[key] = dictionary.lookup(term)
79
- new_notes[key] = '[autotranslated]'
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 = GoogleTranslateDictionary.new(config.google_translate_api_key, config.main_locale, locale)
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
@@ -1,5 +1,5 @@
1
1
  module I18n
2
2
  module Migrations
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  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.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-16 00:00:00.000000000 Z
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: