├── 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 | --------------------------------------------------------------------------------