├── Rakefile
├── lib
├── locales_export_import
│ ├── version.rb
│ ├── yaml2csv.rb
│ └── csv2yaml.rb
└── locales_export_import.rb
├── spec
├── support
│ └── files
│ │ ├── de-DE.yml
│ │ ├── sample_locale.yml
│ │ └── sample_locale.csv
└── locales_export_import
│ ├── yaml2csv_spec.rb
│ └── csv2yaml_spec.rb
├── Gemfile
├── .gitignore
├── LICENSE.txt
├── locales_export_import.gemspec
└── README.md
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
--------------------------------------------------------------------------------
/lib/locales_export_import/version.rb:
--------------------------------------------------------------------------------
1 | module LocalesExportImport
2 | VERSION = "0.5.0"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/support/files/de-DE.yml:
--------------------------------------------------------------------------------
1 | de-DE:
2 | views:
3 | generic:
4 | but: aber
5 | yes_please: Ja-ja!
6 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in locales_export_import.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/lib/locales_export_import.rb:
--------------------------------------------------------------------------------
1 | require 'locales_export_import/yaml2csv'
2 | require 'locales_export_import/csv2yaml'
3 | require 'locales_export_import/version'
4 |
5 | module LocalesExportImport
6 | end
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 buru
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/locales_export_import.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'locales_export_import/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = 'locales_export_import'
8 | spec.version = LocalesExportImport::VERSION
9 | spec.authors = ['buru', 'renatocn']
10 | spec.email = ['pavlozahozhenko@gmail.com']
11 | spec.description = %q{Used for exporting locale yaml files to CSV format. CSV files are then being imported into Excel, edited by translators, then imported back to yaml.}
12 | spec.summary = 'Used for exporting and importing locale yaml files to CSV'
13 | spec.homepage = 'https://github.com/buru/locales_export_import'
14 | spec.license = 'MIT'
15 |
16 | spec.files = `git ls-files`.split($/)
17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19 | spec.require_paths = ['lib']
20 |
21 | spec.add_development_dependency 'bundler', '~> 1.3'
22 | spec.add_development_dependency 'rake', '~> 0'
23 | spec.add_development_dependency 'rspec', '~> 0'
24 | end
25 |
--------------------------------------------------------------------------------
/spec/locales_export_import/yaml2csv_spec.rb:
--------------------------------------------------------------------------------
1 | require 'locales_export_import'
2 |
3 | describe ::LocalesExportImport::Yaml2Csv do
4 |
5 | let(:test_input_file_name) { ::File.join('spec', 'support', 'files', 'sample_locale.yml') }
6 | let(:test_output_file_name) { ::File.join('spec', 'support', 'files', 'test.csv') }
7 |
8 | after(:each) do
9 | ::File.delete(test_output_file_name) if ::File.exist?(test_output_file_name)
10 | end
11 |
12 | it 'should convert yaml file to csv' do
13 | subject.convert([test_input_file_name], test_output_file_name)
14 | ::CSV.foreach(test_output_file_name, :headers => true, encoding: 'UTF-8') do |row|
15 | if $. == 2
16 | expect(row['key']).to eq('de-DE.views.generic.back')
17 | expect(row['de-DE_value']).to eq('Zurück')
18 | end
19 | if $. == 61
20 | expect(row['key']).to eq('de-DE.emails.email_verification.from')
21 | expect(row['de-DE_value']).to eq('kundenservice@blacorp.com')
22 | end
23 | end
24 | end
25 |
26 | it 'should export only values matched by regex pattern' do
27 | pattern = /Passwor.{1}/
28 | subject.convert([test_input_file_name], test_output_file_name, pattern)
29 | ::CSV.foreach(test_output_file_name, :headers => true, encoding: 'UTF-8') do |row|
30 | expect(row['de-DE_value']).to match(pattern)
31 | end
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/lib/locales_export_import/yaml2csv.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 | require 'csv'
3 |
4 | module LocalesExportImport
5 | module Yaml2Csv
6 | extend self
7 |
8 | def convert(input_files, output_file, pattern = nil)
9 | @arr = ::Array.new
10 | @locales = ::Array.new
11 | input_files.each do |input_file|
12 | input_data = ::YAML.load_file(::File.join(input_file))
13 | input_data.keys.each do |key|
14 | # 1st level should contain only one key -- locale code
15 | @locales << key
16 | construct_csv_row(key, input_data[key], pattern)
17 | end
18 | end
19 | ::CSV.open(::File.join(output_file), 'wb') do |csv|
20 | # headers
21 | csv << ['key', *@locales.map {|l| "#{l}_value"}]
22 | @arr.each { |row| csv << row }
23 | end
24 | end
25 |
26 | def construct_csv_row(key, value, pattern)
27 | case value
28 | when ::String
29 | if !pattern || value =~ pattern
30 | if @locales.length > 1 && (existing_key_index = @arr.find_index {|el| el.first.partition('.').last == key.partition('.').last})
31 | @arr[existing_key_index] << value
32 | else
33 | @arr << [key, value]
34 | end
35 | end
36 | when ::Array
37 | # ignoring arrays to avoid having duplicate keys in CSV
38 | # value.each { |v| construct_csv_row(key, v) }
39 | when ::Hash
40 | value.keys.each { |k| construct_csv_row("#{key}.#{k}", value[k], pattern) }
41 | end
42 | end
43 |
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/locales_export_import/csv2yaml.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 | require 'csv'
3 |
4 | module LocalesExportImport
5 | module Csv2Yaml
6 | extend self
7 |
8 | def convert(input_file, output_path = nil, file_prefix = nil)
9 | @yaml = ::Hash.new
10 | ::CSV.foreach(::File.join(input_file), :headers => true) do |row|
11 | puts "inspect: #{row.inspect}"
12 | key = row['key'].strip
13 | row.headers.each do |header|
14 | if header && header.end_with?('_value')
15 | locale = header.partition('_').first
16 | unless @yaml.has_key?(locale)
17 | locale_file = get_output_file_name(locale, output_path, file_prefix)
18 | @yaml[locale] = ::File.exists?(locale_file) ? ::YAML.load_file(locale_file) : ::Hash.new
19 | end
20 | value = row[header]
21 | key_for_locale = [locale, key.partition('.').last].join('.')
22 | puts "adding key: #{key_for_locale}"
23 | add_value_to_tree(@yaml[locale], key_for_locale, value) unless value.nil? || value.empty?
24 | end
25 | end
26 | end
27 | puts "Resulting structure: #{@yaml.inspect}"
28 | output_files = ::Array.new
29 | @yaml.keys.each do |locale|
30 | output_file = get_output_file_name(locale, output_path, file_prefix)
31 | ::File.write(output_file, @yaml[locale].to_yaml)
32 | output_files << output_file
33 | end
34 | return output_files, @yaml
35 | end
36 |
37 | def add_value_to_tree(hash, key, value)
38 | if !key.include?('.')
39 | hash[key] = value if hash.is_a?(::Hash)
40 | else
41 | head, _, tail = key.partition('.')
42 | hash[head] = ::Hash.new unless hash.has_key?(head)
43 | add_value_to_tree(hash[head], tail, value)
44 | end
45 | end
46 |
47 | def get_output_file_name(locale, output_path, file_prefix)
48 | ::File.join(*[output_path, "#{file_prefix}#{locale}.yml"].compact)
49 | end
50 |
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/spec/locales_export_import/csv2yaml_spec.rb:
--------------------------------------------------------------------------------
1 | require 'locales_export_import'
2 |
3 | describe ::LocalesExportImport::Csv2Yaml do
4 |
5 | let(:test_input_file_name) { ::File.join('spec', 'support', 'files', 'sample_locale.csv') }
6 | let(:test_output_file_name) { 'de-DE.yml' }
7 | let(:custom_output_file_name) { ::File.join('spec', 'support', 'files', 'custom_de-DE.yml') }
8 |
9 | context '#convert' do
10 |
11 | after(:each) do
12 | ::File.delete(test_output_file_name) if ::File.exist?(test_output_file_name)
13 | ::File.delete(custom_output_file_name) if ::File.exist?(custom_output_file_name)
14 | end
15 |
16 | it 'should convert csv file to yaml named after appropriate locale' do
17 | output_file_names, _ = subject.convert(test_input_file_name)
18 | expect(output_file_names.first).to eq(test_output_file_name)
19 | end
20 |
21 | it 'should convert csv line into a hash' do
22 | _, yaml_hash = subject.convert(test_input_file_name)
23 | expect(yaml_hash['de-DE']['de-DE']['views']['generic']['cheer']).to eq('Gut')
24 | end
25 |
26 | it 'should output to custom directory with custom prefix if output options are given' do
27 | output_file_names, _ = subject.convert(test_input_file_name, 'spec/support/files/', 'custom_')
28 | expect(output_file_names.first).to eq(custom_output_file_name)
29 | end
30 |
31 | it 'should output to custom directory with if output path lacks trailing space' do
32 | output_file_names, _ = subject.convert(test_input_file_name, 'spec/support/files', 'custom_')
33 | expect(output_file_names.first).to eq(custom_output_file_name)
34 | end
35 |
36 | context 'edit existing locale file' do
37 |
38 | before(:each) do
39 | ::FileUtils.cp(::File.join('spec', 'support', 'files', 'de-DE.yml'), custom_output_file_name)
40 | end
41 |
42 | it 'should merge already existing output file contents with new output file with the same name' do
43 | _, yaml_hash = subject.convert(test_input_file_name, 'spec/support/files', 'custom_')
44 | expect(yaml_hash['de-DE']['de-DE']['views']['generic']['but']).to eq('aber')
45 | end
46 |
47 | it 'should override value in existing locale file' do
48 | _, yaml_hash = subject.convert(test_input_file_name, 'spec/support/files', 'custom_')
49 | expect(yaml_hash['de-DE']['de-DE']['views']['generic']['yes_please']).to eq('Ja, bitte!')
50 | end
51 |
52 | end
53 |
54 | end
55 |
56 | end
57 |
--------------------------------------------------------------------------------
/spec/support/files/sample_locale.yml:
--------------------------------------------------------------------------------
1 | ---
2 | de-DE:
3 | views:
4 | generic:
5 | back: Zurück
6 | cancel: Abbrechen
7 | cheer: Gut
8 | confirm: Bestätigen
9 | send: Senden
10 | edit: Bearbeiten
11 | empty: Feld ausfüllen
12 | euro_per_month: € / Monat
13 | no_thanks: Nein, danke
14 | whats_this: Was ist das?
15 | "yes": Ja
16 | "no": Nein
17 | yes_please: Ja, bitte!
18 | months: Monaten
19 | continue: Weiter
20 | or: oder
21 | good_luck: Viel Glück!
22 | chosen:
23 | no_results: Kein Ergebnis für...gefunden
24 | keep_typing: Weiter schreiben...
25 | looking_for: Suchen nach
26 | home:
27 | answer_and_win: Antworten und gewinnen
28 | best_deals: Großartige Preise.
29 | keep_your_number_html: Behalten Sie Ihre bisherige Nummer
30 | latest_devices: Aktuellste Geräte.
31 | low_price_guarantee_html: Garantiert niedriger Preis
32 | participate: Nehmen Sie an einer Umfrage teil
33 | take_1min_survey: An einer 1-Minuten-Umfrage teilnehmen und 125.000 € gewinnen
34 | win_up_to_html: Gewinnen Sie bis zu 200.000 €
35 | pagination:
36 | last: Vorige »
37 | next: Nächste ›
38 | profile:
39 | account_information: Bankverbindung
40 | address: Straße
41 | apply_changes: Speichern
42 | change_password: Passwort ändern
43 | code_send_to: 'Identifikationscode wurde an folgende Adresse gesendet:'
44 | code_sent_to_number: 'Identifikationscode wurde an folgende Nummer gesandt:'
45 | confirm_password: Passwort wiederholen
46 | confirmed: Bestätigt
47 | contact_details: Kontaktinformationen
48 | current_password: Altes Passwort
49 | elisa_viihde: 'Elisa Viihde:'
50 | email_address: 'E-Mail Addresse:'
51 | first_name: 'Vorname:'
52 | gender: 'Geschlecht:'
53 | last_name: 'Nachname:'
54 | last_participation_date: 'Letzte Teilnahme:'
55 | new_password: Neues Passwort
56 | number_of_participations: 'Anzahl der Teilnahmen:'
57 | operator: 'Netzbetreiber:'
58 | password: 'Passwort:'
59 | phone_number: 'Mobilfunknummer:'
60 | residence: 'Wohnort:'
61 | saunalahti_broadband: Telekom Breitband
62 | signup_date: 'Registrierung:'
63 | ssn: 'Persönliche ID:'
64 | storey_and_apartment: 'Gebäudeteil:'
65 | verification_code: Identifikationscode
66 | year_of_birth: 'Geburtsjahr:'
67 | zip_code: 'Postleitzahl:'
68 | emails:
69 | email_verification:
70 | from: kundenservice@blacorp.com
71 | subject: .PROMO Identifizierungscode
72 | text: 'Der .PROMO Identifizierungscode für Ihre E-Mail Addresse lautet: %s'
73 |
--------------------------------------------------------------------------------
/spec/support/files/sample_locale.csv:
--------------------------------------------------------------------------------
1 | key,de-DE_value
2 | de-DE.views.generic.back,Zurück
3 | de-DE.views.generic.cancel,Abbrechen
4 | de-DE.views.generic.cheer,Gut
5 | de-DE.views.generic.confirm,Bestätigen
6 | de-DE.views.generic.send,Senden
7 | de-DE.views.generic.edit,Bearbeiten
8 | de-DE.views.generic.empty,Feld ausfüllen
9 | de-DE.views.generic.euro_per_month,€ / Monat
10 | de-DE.views.generic.no_thanks,"Nein, danke"
11 | de-DE.views.generic.whats_this,Was ist das?
12 | de-DE.views.generic.yes,Ja
13 | de-DE.views.generic.no,Nein
14 | de-DE.views.generic.yes_please,"Ja, bitte!"
15 | de-DE.views.generic.months,Monaten
16 | de-DE.views.generic.continue,Weiter
17 | de-DE.views.generic.or,oder
18 | de-DE.views.generic.good_luck,Viel Glück!
19 | de-DE.views.generic.chosen.no_results,Kein Ergebnis für...gefunden
20 | de-DE.views.generic.chosen.keep_typing,Weiter schreiben...
21 | de-DE.views.generic.chosen.looking_for,Suchen nach
22 | de-DE.views.home.answer_and_win,Antworten und gewinnen
23 | de-DE.views.home.best_deals,Großartige Preise.
24 | de-DE.views.home.keep_your_number_html,Behalten Sie Ihre bisherige Nummer
25 | de-DE.views.home.latest_devices,Aktuellste Geräte.
26 | de-DE.views.home.low_price_guarantee_html,Garantiert niedriger Preis
27 | de-DE.views.home.participate,Nehmen Sie an einer Umfrage teil
28 | de-DE.views.home.take_1min_survey,An einer 1-Minuten-Umfrage teilnehmen und 125.000 € gewinnen
29 | de-DE.views.home.win_up_to_html,Gewinnen Sie bis zu 200.000 €
30 | de-DE.views.pagination.last,Vorige »
31 | de-DE.views.pagination.next,Nächste ›
32 | de-DE.views.profile.account_information,Bankverbindung
33 | de-DE.views.profile.address,Straße
34 | de-DE.views.profile.apply_changes,Speichern
35 | de-DE.views.profile.change_password,Passwort ändern
36 | de-DE.views.profile.code_send_to,Identifikationscode wurde an folgende Adresse gesendet:
37 | de-DE.views.profile.code_sent_to_number,Identifikationscode wurde an folgende Nummer gesandt:
38 | de-DE.views.profile.confirm_password,Passwort wiederholen
39 | de-DE.views.profile.confirmed,Bestätigt
40 | de-DE.views.profile.contact_details,Kontaktinformationen
41 | de-DE.views.profile.current_password,Altes Passwort
42 | de-DE.views.profile.elisa_viihde,Elisa Viihde:
43 | de-DE.views.profile.email_address,E-Mail Addresse:
44 | de-DE.views.profile.first_name,Vorname:
45 | de-DE.views.profile.gender,Geschlecht:
46 | de-DE.views.profile.last_name,Nachname:
47 | de-DE.views.profile.last_participation_date,Letzte Teilnahme:
48 | de-DE.views.profile.new_password,Neues Passwort
49 | de-DE.views.profile.number_of_participations,Anzahl der Teilnahmen:
50 | de-DE.views.profile.operator,Netzbetreiber:
51 | de-DE.views.profile.password,Passwort:
52 | de-DE.views.profile.phone_number,Mobilfunknummer:
53 | de-DE.views.profile.residence,Wohnort:
54 | de-DE.views.profile.saunalahti_broadband,Telekom Breitband
55 | de-DE.views.profile.signup_date,Registrierung:
56 | de-DE.views.profile.ssn,Persönliche ID:
57 | de-DE.views.profile.storey_and_apartment,Gebäudeteil:
58 | de-DE.views.profile.verification_code,Identifikationscode
59 | de-DE.views.profile.year_of_birth,Geburtsjahr:
60 | de-DE.views.profile.zip_code,Postleitzahl:
61 | de-DE.emails.email_verification.from,kundenservice@blacorp.com
62 | de-DE.emails.email_verification.subject,.PROMO Identifizierungscode
63 | de-DE.emails.email_verification.text,Der .PROMO Identifizierungscode für Ihre E-Mail Addresse lautet: %s
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Locales Export & Import
2 |
3 | This gem is designed to work with Rails I18n locale files, but it's not dependent on Rails, so can be also used elsewhere. Translation agencies prefer working with the tools they know, typically Excel, while ruby developers usually store localized strings in yaml files. locale_export_import helps with easy conversion between the two formats to make developer-translator interaction less painful.
4 |
5 | The typical workflow of adding new locale(s) with this gem is as follows:
6 |
7 | 1. Developer exports his base locale file to CSV (currently only CSV is supported, XLSX support is planned).
8 | 2. Translator opens the file in Excel, adds one or several columns with translated texts (one column per locale).
9 | 3. Developer converts the resulting file to YAML file(s) and commits the changes.
10 |
11 | ## Installation
12 |
13 | Add this line to your application's Gemfile:
14 |
15 | gem 'locales_export_import'
16 |
17 | And then execute:
18 |
19 | $ bundle
20 |
21 | Or install it yourself as:
22 |
23 | $ gem install locales_export_import
24 |
25 | ## Usage
26 |
27 | #### Converting locale YAML file to CSV:
28 | ```
29 | LocalesExportImport::Yaml2Csv.convert(locale_file_names_array, output_file_name, pattern = nil)
30 | ```
31 |
32 | For example:
33 | ```
34 | LocalesExportImport::Yaml2Csv.convert(['config/locales/en.yml'], 'en_keys.csv')
35 | ```
36 | Resulting CSV is in the following format:
37 | ```
38 | key,en_value
39 | en.views.login.remember_me,Remember me
40 | ...
41 | ```
42 |
43 | For multiple locales at once:
44 | ```
45 | LocalesExportImport::Yaml2Csv.convert(
46 | %w[config/locales/en-UK.yml config/locales/de-DE.yml],
47 | 'en_de_keys.csv'
48 | )
49 | ```
50 | And the result will be something like this:
51 | ```
52 | key,en-UK_value,de-DE_value
53 | en.views.login.remember_me,Remember me,Mich eingeloggt lassen
54 | en.views.login.log_in,Log in,Einloggen
55 | ...
56 | ```
57 | Note that each column header for translation texts should be in the format #{locale}_value
58 |
59 | Exporting only the texts that match a certain pattern:
60 | ```
61 | LocalesExportImport::Yaml2Csv.convert(['config/locales/en.yml'], 'en_login_keys.csv', /login/i)
62 | ```
63 |
64 | #### Converting CSV back to YAML:
65 | ```
66 | LocalesExportImport::Csv2Yaml.convert(csv_file_name)
67 | ````
68 |
69 | The result will be the locale file(s) in the current working directory, one file for each locale column found in the headers. E.g. if CSV file header row was "key,en-UK,de-DE,fi-FI", then the resulting files will be en-UK.yml, de-DE.yml, and fi-FI.yml populated with corresponding translated strings.
70 |
71 | Note that if you already have one or several locale files in the same folder (e.g. en-UK.yml and de-DE.yml), these files will be loaded and updated with new values. That way you can import new portion of translations to already exsisting locale file, adding only the new ones while keeping the old keys/values intact.
72 |
73 |
74 | ##### Output options
75 |
76 | If you have your own way to organize files with directories and names, you can pass an output_path and a file_prefix.
77 |
78 | ```
79 | LocalesExportImport::Csv2Yaml.convert(csv_file_name, 'config/locales/my_directory/sub_directory/', 'some_prefix_')
80 | ````
81 |
82 | And the result will be something like this:
83 | ```
84 | config/locales/my_directory/sub_directory/some_prefix_en.yml
85 | ````
86 |
87 | These arguments are optional.
88 |
89 | ## Contributing
90 |
91 | 1. Fork it
92 | 2. Create your feature branch (`git checkout -b my-new-feature`)
93 | 3. Commit your changes (`git commit -am 'Add some feature'`)
94 | 4. Push to the branch (`git push origin my-new-feature`)
95 | 5. Create new Pull Request
96 |
--------------------------------------------------------------------------------