i18n-migrations 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.i18n-migrations.default.yml +22 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +48 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/i18n-migrate +74 -0
- data/bin/setup +7 -0
- data/i18n-migrations.gemspec +31 -0
- data/lib/i18n/migrations/config.rb +94 -0
- data/lib/i18n/migrations/google_spreadsheet.rb +22 -0
- data/lib/i18n/migrations/google_translate_dictionary.rb +23 -0
- data/lib/i18n/migrations/migration.rb +95 -0
- data/lib/i18n/migrations/migrator.rb +278 -0
- data/lib/i18n/migrations/version.rb +5 -0
- data/lib/i18n-migrations.rb +7 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1d5b7ed6e5c2a76bacd9a1d12916d0f7fbba5c0d
|
4
|
+
data.tar.gz: ba7a796930572af0d6fa2f6f8ff9433c1f0b9032
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1ce65173fa1d15de262b216f589eeec8885780a329a65e03866b1066e677546e56f88c2e52b84c2e3dac9c4ccf93aa4db0ca09bf2bab8798b929500a676f63aa
|
7
|
+
data.tar.gz: 7dd74b2511050c59a71e679009a07997d0a11183ca17c0d1c76e40f15ea7c5afbb49406fc3cdc71272d3fcb2485a93855e52f352227087b213f7ac8758154938
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# this is where your migration files will live. it will be relative to your config file
|
2
|
+
migration_dir: i18n/migrate
|
3
|
+
|
4
|
+
# this is where your locale files will live (en.yml, es.yml, etc). it will be relative to your config file
|
5
|
+
locales_dir: config/locales
|
6
|
+
|
7
|
+
# this is the locale you will be translating from
|
8
|
+
main_locale: en
|
9
|
+
|
10
|
+
# put all other locales you want to use here, if they don't already exist, add them with i18n-migrations new-locale es
|
11
|
+
other_locales:
|
12
|
+
- es
|
13
|
+
|
14
|
+
# you need a service account key in order to access google spreadsheets. This is the path to it, relative to your config file
|
15
|
+
google_service_account_key_path: i18n/google_drive_key.json
|
16
|
+
|
17
|
+
# each locale will have a spreadsheet that translators will use to do their work, these are the links to them
|
18
|
+
google_spreadsheets:
|
19
|
+
es: https://docs.google.com/spreadsheets/d/1QGCzMbTyxF2gSUpWwRKAq-mMFIgJwsOLp1lfXsSF5X0/edit
|
20
|
+
|
21
|
+
# this is your api key to use google translate
|
22
|
+
google_translate_api_key: [INSERT_GOOGLE_TRANSLATE_API_KEY]
|
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
transparentclassroom
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.4.1
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Jeremy Lightsmith
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# I18n Migrations
|
2
|
+
|
3
|
+
We help you manage your locale translations with migrations, just the way Active Record helps you manage your db with migrations.
|
4
|
+
|
5
|
+
There are several tools out there that allow you to dynamically store / load / translate strings in your app. We prefer to deploy our app with static files using the excellent i18n gem. But how to translate?
|
6
|
+
|
7
|
+
Our flow is:
|
8
|
+
|
9
|
+
1. Use a migration to make a change (add/remove/update/move) to your translations locally. In this state, we will use google translate to quickly guess at translations.
|
10
|
+
1. When ready to deploy, push these changes to google spreadsheets, one for each translation, there, your translators can fix google translate's mistakes.
|
11
|
+
1. Pull to replace your translations with what's in those google spreadsheets.
|
12
|
+
1. Migrate to play back any changes after pulling / merging / etc.
|
13
|
+
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'i18n-migrations'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install i18n-migrations
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
TODO: Write usage instructions here
|
34
|
+
|
35
|
+
## Development
|
36
|
+
|
37
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
38
|
+
|
39
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
40
|
+
|
41
|
+
## Contributing
|
42
|
+
|
43
|
+
1. Fork it ( https://github.com/[my-github-username]/i18n-migrations/fork )
|
44
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
45
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
46
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
47
|
+
5. Create a new Pull Request
|
48
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "i18n/migrations"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/i18n-migrate
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/i18n/migrations/migrator'
|
4
|
+
require_relative '../lib/i18n/migrations/config'
|
5
|
+
|
6
|
+
migrator = I18n::Migrations::Migrator.new
|
7
|
+
|
8
|
+
case ARGV.shift
|
9
|
+
when 'setup'
|
10
|
+
puts 'Where should we create a default config file? [.]'
|
11
|
+
dir = gets.chomp
|
12
|
+
dir = dir == '' ? '.' : dir
|
13
|
+
file = I18n::Migrations::Config.copy_default_config_file(dir)
|
14
|
+
|
15
|
+
puts 'You will need to configure this file before you can get going.'
|
16
|
+
puts File.expand_path(file)
|
17
|
+
|
18
|
+
when 'new'
|
19
|
+
name = ARGV.shift
|
20
|
+
if name
|
21
|
+
migrator.new_migration name
|
22
|
+
else
|
23
|
+
STDERR.puts 'Usage: im new [name]'
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
|
27
|
+
when 'migrate'
|
28
|
+
migrator.migrate
|
29
|
+
|
30
|
+
when 'rollback'
|
31
|
+
migrator.rollback(ARGV[0] || 'all')
|
32
|
+
|
33
|
+
when 'redo'
|
34
|
+
migrator.rollback(ARGV[0] || 'all')
|
35
|
+
migrator.migrate
|
36
|
+
|
37
|
+
when 'pull'
|
38
|
+
migrator.pull ARGV[0] || 'all'
|
39
|
+
|
40
|
+
when 'push'
|
41
|
+
migrator.push ARGV[0] || 'all'
|
42
|
+
|
43
|
+
when 'new_locale'
|
44
|
+
locale = ARGV.shift
|
45
|
+
if locale
|
46
|
+
migrator.new_locale(locale)
|
47
|
+
else
|
48
|
+
STDERR.puts 'Usage: im new_locale [name]'
|
49
|
+
exit 1
|
50
|
+
end
|
51
|
+
|
52
|
+
when 'version'
|
53
|
+
migrator.version
|
54
|
+
|
55
|
+
else
|
56
|
+
puts <<-USAGE
|
57
|
+
Usage: im [command]
|
58
|
+
|
59
|
+
Commands:
|
60
|
+
setup - Setup a new project w/ i18n-migrations.
|
61
|
+
new - Create a new migration.
|
62
|
+
migrate - Migrate to current version.
|
63
|
+
rollback - Rollback to previous version.
|
64
|
+
redo - Rollback and then migrate again.
|
65
|
+
pull - Pull latest translation spreadsheet.
|
66
|
+
push - Push to translation spreadsheet.
|
67
|
+
new_locale - Copy your current main locale file to a new language, translating all keys.
|
68
|
+
version - Print version of locales.
|
69
|
+
|
70
|
+
USAGE
|
71
|
+
puts
|
72
|
+
puts "Commands:"
|
73
|
+
puts
|
74
|
+
end
|
data/bin/setup
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'i18n/migrations/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "i18n-migrations"
|
8
|
+
spec.version = I18n::Migrations::VERSION
|
9
|
+
spec.authors = ["Jeremy Lightsmith"]
|
10
|
+
spec.email = ["jeremy.lightsmith@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Migrations for doing i18n.}
|
13
|
+
spec.description = %q{We help you manage your locale translations with migrations, just the way Active Record helps you manage your db with migrations.}
|
14
|
+
spec.homepage = "https://github.com/transparentclassroom/i18n-migrations"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "bin"
|
19
|
+
# spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.executables = ["i18n-migrate"]
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.required_ruby_version = '~> 2.4'
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
|
28
|
+
spec.add_dependency 'google_drive'
|
29
|
+
spec.add_dependency 'activesupport'
|
30
|
+
spec.add_dependency 'rest-client'
|
31
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Migrations
|
5
|
+
class Config
|
6
|
+
CONFIG_FILE_NAME = '.i18n-migrations.yml'
|
7
|
+
|
8
|
+
def migration_dir
|
9
|
+
get_file(:migration_dir)
|
10
|
+
end
|
11
|
+
|
12
|
+
def locales_dir
|
13
|
+
get_file(:locales_dir)
|
14
|
+
end
|
15
|
+
|
16
|
+
def main_locale
|
17
|
+
get_value(:main_locale)
|
18
|
+
end
|
19
|
+
|
20
|
+
def other_locales
|
21
|
+
get_value(:other_locales)
|
22
|
+
end
|
23
|
+
|
24
|
+
def google_service_account_key_path
|
25
|
+
get_file(:google_service_account_key_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def google_spreadsheets
|
29
|
+
get_value(:google_spreadsheets)
|
30
|
+
end
|
31
|
+
|
32
|
+
def google_translate_api_key
|
33
|
+
get_value(:google_translate_api_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def read!
|
37
|
+
yaml_file = find_config_file(CONFIG_FILE_NAME)
|
38
|
+
unless yaml_file
|
39
|
+
STDERR.puts "Can't find a #{CONFIG_FILE_NAME} file. Try running 'i18n-migrations setup'"
|
40
|
+
exit(1)
|
41
|
+
end
|
42
|
+
|
43
|
+
@root_dir = File.dirname(yaml_file)
|
44
|
+
|
45
|
+
@config = begin
|
46
|
+
YAML::load(File.read(yaml_file))
|
47
|
+
rescue Psych::SyntaxError
|
48
|
+
STDERR.puts("YAML configuration file contains invalid syntax.")
|
49
|
+
STDERR.puts($!.message)
|
50
|
+
exit(1)
|
51
|
+
end
|
52
|
+
|
53
|
+
# todo check for required keys
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.copy_default_config_file(dir)
|
58
|
+
File.open(File.join(dir, CONFIG_FILE_NAME), 'w') do |f|
|
59
|
+
f << File.read(File.join(File.dirname(__FILE__), '../../../.i18n-migrations.default.yml'))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def get_value(key)
|
66
|
+
if @config.has_key?(key.to_s)
|
67
|
+
@config[key.to_s]
|
68
|
+
else
|
69
|
+
STDERR.puts "You must have defined #{key} in #{@root_dir}/#{CONFIG_FILE_NAME}"
|
70
|
+
exit(1)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_file(key)
|
75
|
+
file = File.join(@root_dir, get_value(key))
|
76
|
+
unless File.exist?(file)
|
77
|
+
STDERR.puts "#{File.expand_path(file)} does not exist"
|
78
|
+
exit(1)
|
79
|
+
end
|
80
|
+
file
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_config_file(name)
|
84
|
+
file = File.expand_path(name)
|
85
|
+
loop do
|
86
|
+
return file if File.exist?(file)
|
87
|
+
next_file = File.join(File.dirname(File.dirname(file)), name)
|
88
|
+
return nil if file == next_file
|
89
|
+
file = next_file
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'google_drive'
|
2
|
+
require 'config'
|
3
|
+
|
4
|
+
module I18n
|
5
|
+
module Migrations
|
6
|
+
class GoogleSpreadsheet
|
7
|
+
attr_reader :sheet
|
8
|
+
|
9
|
+
def initialize(locale)
|
10
|
+
@session = GoogleDrive::Session.from_service_account_key(Config.google_service_account_key_path)
|
11
|
+
|
12
|
+
url = Config.google_spreadsheets[locale] || raise("Can't find google spreadsheet for #{locale}")
|
13
|
+
@spreadsheet = @session.spreadsheet_by_url(url)
|
14
|
+
@sheet = sheet_for("Sheet1")
|
15
|
+
end
|
16
|
+
|
17
|
+
def sheet_for(name)
|
18
|
+
@spreadsheet.worksheet_by_title(name) || raise("couldn't find worksheet for #{name}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Migrations
|
5
|
+
class GoogleTranslateDictionary
|
6
|
+
def initialize(key, from_locale, to_locale)
|
7
|
+
@key, @from_locale, @to_locale = key, from_locale, to_locale
|
8
|
+
end
|
9
|
+
|
10
|
+
def lookup(term)
|
11
|
+
return term if @from_locale == @to_locale
|
12
|
+
|
13
|
+
response = RestClient.get 'https://www.googleapis.com/language/translate/v2', {
|
14
|
+
accept: :json,
|
15
|
+
params: { key: @key, source: @from_locale, target: @to_locale, q: term }
|
16
|
+
}
|
17
|
+
JSON.parse(response.body)['data']['translations'].first['translatedText']
|
18
|
+
end
|
19
|
+
|
20
|
+
# we should validate translations based on reasonable rules
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module I18n
|
2
|
+
module Migrations
|
3
|
+
class Migration
|
4
|
+
# locale = en | es | ...
|
5
|
+
# data = all keys -> all translations in this locale
|
6
|
+
# notes = some keys -> notes about the translation in this locale
|
7
|
+
# dictionary = call dictionary.lookup(term) to get localized version of a term
|
8
|
+
# direction = :up | :down (up when migrating, down when rolling back)
|
9
|
+
def initialize(locale, data, notes, dictionary, direction = :up, verbose = false)
|
10
|
+
@locale, @data, @notes, @dictionary, @direction, @verbose = locale, data, notes, dictionary, direction, verbose
|
11
|
+
end
|
12
|
+
|
13
|
+
# Overrides can be provided, e.g. { es: 'El foo de la barro' }
|
14
|
+
def add(key, term, overrides = {})
|
15
|
+
if @direction == :up
|
16
|
+
info "adding #{key}"
|
17
|
+
_add key, term, overrides
|
18
|
+
else
|
19
|
+
info "unadding #{key}"
|
20
|
+
_rm key
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def mv(old_key, new_key)
|
25
|
+
if @direction == :up
|
26
|
+
info "moving #{old_key} => #{new_key}"
|
27
|
+
_mv old_key, new_key
|
28
|
+
else
|
29
|
+
info "moving back #{new_key} => #{old_key}"
|
30
|
+
_mv new_key, old_key
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Overrides can be provided, e.g. { es: 'El foo de la barro' }
|
35
|
+
def rm(key, old_term, overrides = {})
|
36
|
+
if @direction == :up
|
37
|
+
info "removing #{key}"
|
38
|
+
_rm key
|
39
|
+
else
|
40
|
+
info "unremoving #{key}"
|
41
|
+
_add key, old_term, overrides
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Overrides can be provided, e.g. { es: 'El foo de la barro' }
|
46
|
+
def update(key, new_term, old_term, overrides = {})
|
47
|
+
if @direction == :up
|
48
|
+
info "updating #{key}"
|
49
|
+
_update key, new_term, overrides
|
50
|
+
else
|
51
|
+
info "unupdating #{key}"
|
52
|
+
_update key, old_term, {}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def _add(key, term, overrides)
|
59
|
+
assert_does_not_exist! key
|
60
|
+
@data[key] = overrides[@locale.to_sym] || @dictionary.lookup(term)
|
61
|
+
@notes[key] = "[autotranslated]"
|
62
|
+
end
|
63
|
+
|
64
|
+
def _mv(from, to)
|
65
|
+
assert_exists! from
|
66
|
+
assert_does_not_exist! to
|
67
|
+
@data[to] = @data.delete(from)
|
68
|
+
end
|
69
|
+
|
70
|
+
def _update(key, term, overrides)
|
71
|
+
assert_exists! key
|
72
|
+
@data[key] = overrides[@locale.to_sym] || @dictionary.lookup(term)
|
73
|
+
@notes[key] = "[autotranslated]"
|
74
|
+
end
|
75
|
+
|
76
|
+
def _rm(key)
|
77
|
+
assert_exists! key
|
78
|
+
@data.delete(key)
|
79
|
+
@notes.delete(key)
|
80
|
+
end
|
81
|
+
|
82
|
+
def info(message)
|
83
|
+
puts message if @verbose
|
84
|
+
end
|
85
|
+
|
86
|
+
def assert_exists!(key)
|
87
|
+
raise "#{key} doesn't exist in #{@locale}" unless @data.has_key?(key)
|
88
|
+
end
|
89
|
+
|
90
|
+
def assert_does_not_exist!(key)
|
91
|
+
raise "#{key} already exists in #{@locale}" if @data.has_key?(key)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
require 'active_support/inflector'
|
4
|
+
require 'active_support/core_ext/object'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
|
+
require 'google_translate_dictionary'
|
8
|
+
require 'google_spreadsheet'
|
9
|
+
require 'config'
|
10
|
+
|
11
|
+
module I18n
|
12
|
+
module Migrations
|
13
|
+
class Migrator
|
14
|
+
def config
|
15
|
+
@config ||= Config.new.read!
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_migration(name)
|
19
|
+
name = name.parameterize(separator: '_')
|
20
|
+
file_name = "#{Time.now.strftime('%Y%m%d%H%M')}_#{name.downcase.gsub(' ', '_')}.rb"
|
21
|
+
unless Dir.exist?(config.migration_dir)
|
22
|
+
puts "Creating migration directory #{config.migration_dir} because it didn't exist."
|
23
|
+
FileUtils.mkdir_p(config.migration_dir)
|
24
|
+
end
|
25
|
+
File.open(File.join(config.migration_dir, file_name), 'w') do |f|
|
26
|
+
f << <<-CONTENTS
|
27
|
+
require 'i18n-migrations'
|
28
|
+
|
29
|
+
class #{name.camelcase} < I18n::Migrations::Migration
|
30
|
+
def change
|
31
|
+
# add('foo.bar', 'The foo of the bar')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
CONTENTS
|
35
|
+
end
|
36
|
+
puts "Wrote new migration to #{file_name}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def migrate(locale_or_all = 'all')
|
40
|
+
each_locale(locale_or_all) do |locale|
|
41
|
+
update_locale_info(locale) do |data, notes|
|
42
|
+
migrate_locale(locale, data, notes)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def rollback(locale_or_all)
|
48
|
+
each_locale(locale_or_all) do |locale|
|
49
|
+
update_locale_info(locale) do |data, notes|
|
50
|
+
rollback_locale(locale, data, notes)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def pull(locale_or_all)
|
56
|
+
each_locale(locale_or_all) do |locale|
|
57
|
+
next if locale == config.main_locale
|
58
|
+
sheet = GoogleSpreadsheet.new(locale).sheet
|
59
|
+
pull_locale(locale, sheet)
|
60
|
+
migrate(locale)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def push(locale_or_all)
|
65
|
+
each_locale(locale_or_all) do |locale|
|
66
|
+
next if locale == config.main_locale
|
67
|
+
sheet = GoogleSpreadsheet.new(locale).sheet
|
68
|
+
pull_locale(locale, sheet)
|
69
|
+
migrate(locale)
|
70
|
+
push_locale(locale, sheet)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def new_locale(new_locale)
|
75
|
+
dictionary = GoogleTranslateDictionary.new(config.google_translate_api_key, config.main_locale, new_locale)
|
76
|
+
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
|
+
print '.'.green
|
81
|
+
end
|
82
|
+
write_locale_data_and_notes(new_locale, new_data, new_notes)
|
83
|
+
end
|
84
|
+
|
85
|
+
def version
|
86
|
+
each_locale do |locale|
|
87
|
+
puts "#{locale}: #{locale_versions(read_locale_data(locale)).last}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def update_locale_info(locale)
|
94
|
+
data, notes = read_locale_data_and_notes(locale)
|
95
|
+
yield data, notes
|
96
|
+
write_locale_data_and_notes(locale, data, notes)
|
97
|
+
end
|
98
|
+
|
99
|
+
def read_locale_data_and_notes(locale)
|
100
|
+
data = read_locale_data(locale)
|
101
|
+
notes = locale == config.main_locale ? {} : read_locale_from_file(locale, "../#{locale}_notes.yml")
|
102
|
+
[data, notes]
|
103
|
+
end
|
104
|
+
|
105
|
+
def read_locale_data(locale)
|
106
|
+
read_locale_from_file(locale, "#{locale}.yml")
|
107
|
+
end
|
108
|
+
|
109
|
+
def write_locale_data_and_notes(locale, data, notes)
|
110
|
+
write_locale_to_file(locale, "#{locale}.yml", data)
|
111
|
+
write_locale_to_file(locale, "../#{locale}_notes.yml", notes) unless locale == config.main_locale
|
112
|
+
end
|
113
|
+
|
114
|
+
def pull_locale(locale, sheet)
|
115
|
+
puts "Pulling #{locale}"
|
116
|
+
data = {}
|
117
|
+
notes = {}
|
118
|
+
count = 0
|
119
|
+
|
120
|
+
(2..sheet.num_rows).each do |row|
|
121
|
+
key, value, note = sheet[row, 1], sheet[row, 3], sheet[row, 4]
|
122
|
+
if key.present?
|
123
|
+
assign_complex_key(data, key.split('.'), value.present? ? value : '')
|
124
|
+
if note.present?
|
125
|
+
assign_complex_key(notes, key.split('.'), note)
|
126
|
+
end
|
127
|
+
count += 1
|
128
|
+
print '.'
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
write_locale_data_and_notes(locale, data, notes)
|
133
|
+
write_locale_remote_version(locale, data)
|
134
|
+
|
135
|
+
puts "\n#{count} keys"
|
136
|
+
end
|
137
|
+
|
138
|
+
def write_locale_remote_version(locale, data)
|
139
|
+
write_locale_to_file(locale,
|
140
|
+
"../#{locale}_remote_version.yml",
|
141
|
+
{ 'VERSION' => locale_versions(data) })
|
142
|
+
end
|
143
|
+
|
144
|
+
def push_locale(locale, sheet)
|
145
|
+
main_data = read_locale_data(config.main_locale)
|
146
|
+
data, notes = read_locale_data_and_notes(locale)
|
147
|
+
row = 2
|
148
|
+
|
149
|
+
puts "Pushing #{locale}"
|
150
|
+
|
151
|
+
main_data.each do |key, value|
|
152
|
+
sheet[row, 1] = key
|
153
|
+
sheet[row, 2] = value
|
154
|
+
sheet[row, 3] = data[key]
|
155
|
+
sheet[row, 4] = notes[key]
|
156
|
+
row += 1
|
157
|
+
print '.'
|
158
|
+
end
|
159
|
+
|
160
|
+
sheet.synchronize
|
161
|
+
write_locale_remote_version(locale, data)
|
162
|
+
|
163
|
+
puts "\n#{main_data.keys.length} keys"
|
164
|
+
end
|
165
|
+
|
166
|
+
def migrate_locale(locale, data, notes)
|
167
|
+
missing_versions = all_versions - locale_versions(data)
|
168
|
+
if missing_versions.empty?
|
169
|
+
puts "#{locale}: up-to-date"
|
170
|
+
return
|
171
|
+
end
|
172
|
+
puts "#{locale}: Migrating #{missing_versions.join(', ')}"
|
173
|
+
missing_versions.each do |version|
|
174
|
+
migrate_locale_to_version(locale, data, notes, version, :up)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def rollback_locale(locale, data, notes)
|
179
|
+
last_version = locale_versions(data).last
|
180
|
+
if last_version == nil
|
181
|
+
puts "#{locale}: no more migrations to roll back"
|
182
|
+
return
|
183
|
+
end
|
184
|
+
puts "#{locale}: Rolling back #{last_version}"
|
185
|
+
raise "Can't find #{last_version}.rb to rollback" unless all_versions.include?(last_version)
|
186
|
+
|
187
|
+
migrate_locale_to_version(locale, data, notes, last_version, :down)
|
188
|
+
end
|
189
|
+
|
190
|
+
def migrate_locale_to_version(locale, data, notes, version, direction)
|
191
|
+
filename = File.join(config.migration_dir, "#{version}.rb")
|
192
|
+
require filename
|
193
|
+
migration_class_name = version.gsub(/^\d{12}_/, '').camelcase
|
194
|
+
dictionary = GoogleTranslateDictionary.new(config.google_translate_api_key, config.main_locale, locale)
|
195
|
+
|
196
|
+
migration = begin
|
197
|
+
migration_class_name.constantize.new(locale, data, notes, dictionary, direction)
|
198
|
+
rescue
|
199
|
+
raise "Couldn't load migration #{migration_class_name} in #{filename}"
|
200
|
+
end
|
201
|
+
|
202
|
+
migration.change
|
203
|
+
|
204
|
+
if direction == :up
|
205
|
+
data['VERSION'] = (locale_versions(data) + [version]).join("\n")
|
206
|
+
else
|
207
|
+
data['VERSION'] = (locale_versions(data) - [version]).join("\n")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def locale_versions(data)
|
212
|
+
(data['VERSION'] && data['VERSION'].split("\n")) || []
|
213
|
+
end
|
214
|
+
|
215
|
+
def read_locale_from_file(locale, filename)
|
216
|
+
filename = File.join(config.locales_dir, filename)
|
217
|
+
begin
|
218
|
+
hash = {}
|
219
|
+
add_to_hash(hash, YAML.load(File.read(filename))[locale.to_s])
|
220
|
+
hash
|
221
|
+
rescue
|
222
|
+
puts "Error loading #{filename}"
|
223
|
+
raise
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def write_locale_to_file(locale, filename, hash)
|
228
|
+
# we have to go from flat keys -> values to a hash that contains other hashes
|
229
|
+
complex_hash = {}
|
230
|
+
hash.keys.sort.each do |key|
|
231
|
+
value = hash[key]
|
232
|
+
assign_complex_key(complex_hash, key.split('.'), value.present? ? value : '')
|
233
|
+
end
|
234
|
+
File.open(File.join(config.locales_dir, filename), 'w') do |file|
|
235
|
+
file << { locale => complex_hash }.to_yaml
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def assign_complex_key(hash, key, value)
|
240
|
+
if key.length == 0
|
241
|
+
# should never get here
|
242
|
+
elsif key.length == 1
|
243
|
+
hash[key[0]] = value
|
244
|
+
else
|
245
|
+
hash[key[0]] ||= {}
|
246
|
+
assign_complex_key(hash[key[0]], key[1..-1], value)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# flattens new_hash and adds it to hash
|
251
|
+
def add_to_hash(hash, new_hash, prefix = [])
|
252
|
+
return unless new_hash
|
253
|
+
|
254
|
+
new_hash.each do |key, value|
|
255
|
+
if value.is_a?(Hash)
|
256
|
+
add_to_hash(hash, value, prefix + [key])
|
257
|
+
else
|
258
|
+
hash[(prefix + [key]).join('.')] = value
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def each_locale(locale = 'all')
|
264
|
+
(locale == 'all' ? all_locales : [locale]).each do |l|
|
265
|
+
yield l
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def all_locales
|
270
|
+
[config.main_locale] + config.other_locales
|
271
|
+
end
|
272
|
+
|
273
|
+
def all_versions
|
274
|
+
Dir[config.migration_dir + '/*.rb'].map { |name| File.basename(name).gsub('.rb', '') }
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: i18n-migrations
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Lightsmith
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: google_drive
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rest-client
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: We help you manage your locale translations with migrations, just the
|
84
|
+
way Active Record helps you manage your db with migrations.
|
85
|
+
email:
|
86
|
+
- jeremy.lightsmith@gmail.com
|
87
|
+
executables:
|
88
|
+
- i18n-migrate
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- ".gitignore"
|
93
|
+
- ".i18n-migrations.default.yml"
|
94
|
+
- ".rspec"
|
95
|
+
- ".ruby-gemset"
|
96
|
+
- ".ruby-version"
|
97
|
+
- ".travis.yml"
|
98
|
+
- CODE_OF_CONDUCT.md
|
99
|
+
- Gemfile
|
100
|
+
- LICENSE.txt
|
101
|
+
- README.md
|
102
|
+
- Rakefile
|
103
|
+
- bin/console
|
104
|
+
- bin/i18n-migrate
|
105
|
+
- bin/setup
|
106
|
+
- i18n-migrations.gemspec
|
107
|
+
- lib/i18n-migrations.rb
|
108
|
+
- lib/i18n/migrations/config.rb
|
109
|
+
- lib/i18n/migrations/google_spreadsheet.rb
|
110
|
+
- lib/i18n/migrations/google_translate_dictionary.rb
|
111
|
+
- lib/i18n/migrations/migration.rb
|
112
|
+
- lib/i18n/migrations/migrator.rb
|
113
|
+
- lib/i18n/migrations/version.rb
|
114
|
+
homepage: https://github.com/transparentclassroom/i18n-migrations
|
115
|
+
licenses:
|
116
|
+
- MIT
|
117
|
+
metadata: {}
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - "~>"
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '2.4'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 2.6.11
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: Migrations for doing i18n.
|
138
|
+
test_files: []
|