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 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: