c11n 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ require 'c11n/translations'
2
+ require 'c11n/importer/yaml'
3
+ require 'c11n/exporter/yaml'
4
+
5
+ module C11n
6
+ class Synchronizer
7
+ class Rails
8
+ def initialize(options = {})
9
+ @project_root = options[:root]
10
+ end
11
+
12
+ def importers
13
+ @importers ||= Hash[localization_files.map do |locale, file_path|
14
+ [locale, C11n::Importer::Yaml.new(path: file_path)]
15
+ end]
16
+ end
17
+
18
+ def exporters
19
+ @exporters ||= Hash[localization_files.map do |locale, file_path|
20
+ [locale, C11n::Exporter::Yaml.new(path: file_path)]
21
+ end]
22
+ end
23
+
24
+ def localization_files
25
+ Dir.new(locales_parent_directory_path).entries.select { |entry| entry =~ /\.yml$/ }.inject({}) do |files, entry|
26
+ files.merge locale_for(entry) => File.join(locales_parent_directory_path, entry)
27
+ end
28
+ end
29
+
30
+ def locales_parent_directory_path
31
+ File.join(@project_root, 'config/locales')
32
+ end
33
+
34
+ private
35
+
36
+ def locale_for(localization_file)
37
+ localization_file.gsub(/\.yml$/, '').to_sym
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,131 @@
1
+ require 'c11n/conversion/plain_deserializer'
2
+
3
+ module C11n
4
+ # Translations in tabular form. The schema of translations table must match
5
+ # to the schema specified below:
6
+ #
7
+ # 1. If category information exists
8
+ #
9
+ # [Category] [Key] [Locale 1] [Locale 2] ... [Locale N]
10
+ #
11
+ # 2. If category information does not exist
12
+ #
13
+ # [Key] [Locale 1] [Locale 2] ... [Locale N]
14
+ #
15
+ # Columns composing the schema must appear on the first row of the sheet.
16
+ class Table
17
+ CATEGORY_COLUMN = 'Category'.freeze
18
+ TYPE_COLUMN = 'Type'.freeze
19
+ KEY_COLUMN = 'Key'.freeze
20
+
21
+ def initialize(raw_table, options = {})
22
+ @raw_table = raw_table
23
+ @deserializer_class = options[:deserializer_class] || C11n::Conversion::PlainDeserializer
24
+ @categories = {}
25
+ @types = {}
26
+ end
27
+
28
+ # Returns key-value mappings for specified locale as an array
29
+ def table_for(locale)
30
+ @raw_table[1..-1].map do |columns|
31
+ if columns
32
+ key = key_column_from(columns)
33
+ translation = translation_column_from(columns, locale) || ''
34
+
35
+ key ? [key, translation] : nil
36
+ else
37
+ nil
38
+ end
39
+ end.compact
40
+ end
41
+
42
+ def schema
43
+ @raw_table[0]
44
+ end
45
+
46
+ def locales
47
+ has_categories? ? schema[2..-1] : schema[1..-1]
48
+ end
49
+
50
+ def categories
51
+ {}
52
+ end
53
+
54
+ def types
55
+ if has_types?
56
+ @types = @raw_table[1..-1].inject({}) do |types, columns|
57
+ key = columns[key_column_index]
58
+ type = columns[type_index]
59
+
60
+ types.merge(key => type) if type
61
+ end
62
+ else
63
+ {}
64
+ end
65
+ end
66
+
67
+ def to_hash
68
+ Hash[locales.map { |locale| [locale.to_sym, @deserializer_class.new(table_for(locale)).deserialize] }]
69
+ end
70
+
71
+ def rows
72
+ @raw_table
73
+ end
74
+
75
+ def set(locale, key, translation, options = {})
76
+ unless locales.map(&:to_s).map(&:downcase).include? locale.to_s.downcase
77
+ schema[schema.length] = locale
78
+ end
79
+
80
+ row_with_key(key)[locale_index_of(locale)] = translation
81
+ row_with_key(key)[category_index] = options[:category] if has_categories? && options[:category]
82
+ row_with_key(key)[type_index] = options[:type] if has_types? && options[:type]
83
+ end
84
+
85
+ private
86
+
87
+ def key_column_from(columns)
88
+ columns[key_column_index]
89
+ end
90
+
91
+ def key_column_index
92
+ schema.map(&:downcase).index(KEY_COLUMN.downcase)
93
+ end
94
+
95
+ def translation_column_from(columns, locale)
96
+ columns[locale_index_of(locale)]
97
+ end
98
+
99
+ def locale_index_of(locale)
100
+ schema.map(&:downcase).index(locale.to_s.downcase)
101
+ end
102
+
103
+ def category_index
104
+ schema.map(&:downcase).index(CATEGORY_COLUMN.downcase)
105
+ end
106
+
107
+ def type_index
108
+ schema.map(&:downcase).index(TYPE_COLUMN.downcase)
109
+ end
110
+
111
+ def has_categories?
112
+ schema.map(&:downcase).include?(CATEGORY_COLUMN.downcase)
113
+ end
114
+
115
+ def has_types?
116
+ schema.map(&:downcase).include?(TYPE_COLUMN.downcase)
117
+ end
118
+
119
+ def row_with_key(key)
120
+ @raw_table[1..-1].detect do |row|
121
+ row[key_column_index].to_s == key.to_s
122
+ end || @raw_table[@raw_table.length] = new_row_with_key(key)
123
+ end
124
+
125
+ def new_row_with_key(key)
126
+ [].tap do |row|
127
+ row[key_column_index] = key
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,49 @@
1
+ require 'c11n/conversion/composed_key_deserializer'
2
+
3
+ module C11n
4
+ class Translations
5
+ attr_reader :categories, :types
6
+
7
+ def initialize(options = {})
8
+ @translations = {}
9
+ @deserializer_class = options[:deserializer_class] || C11n::Conversion::ComposedKeyDeserializer
10
+ end
11
+
12
+ def add_translation(locale, key, value)
13
+ translations_for(locale.to_sym)[key] = value
14
+ end
15
+
16
+ def translation(locale, key)
17
+ translations_for(locale.to_sym)[key]
18
+ end
19
+
20
+ def add_translations(locale, translations)
21
+ @translations[locale.to_sym] = translations
22
+ end
23
+
24
+ def import_with(importer, given_locale = nil)
25
+ importer.import.each do |locale, imported|
26
+ next if given_locale && given_locale.to_sym != locale.to_sym
27
+
28
+ add_translations(locale, imported)
29
+ end
30
+
31
+ @categories = importer.categories
32
+ @types = importer.types
33
+
34
+ self
35
+ end
36
+
37
+ def export_with(exporter, locale)
38
+ exporter.export(self, locale)
39
+ end
40
+
41
+ def translations_for(locale)
42
+ @translations[locale.to_sym] ||= {}
43
+ end
44
+
45
+ def to_hash
46
+ @translations
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,6 @@
1
+ module C11n
2
+ module Types
3
+ ARRAY = 'Array'.freeze
4
+ CDATA = 'CDATA'.freeze
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module C11n
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe C11n::Configuration do
4
+ let(:configuration) { C11n::Configuration.new }
5
+
6
+ describe '#method_missing' do
7
+ it { expect { configuration.some_key = 'value' }.not_to raise_error }
8
+
9
+ context 'when a setter method is invoked before' do
10
+ let(:value) { 'value' }
11
+ before { configuration.some_key = value }
12
+
13
+ it { expect(configuration.configurations[:some_key]).to eq(value) }
14
+ end
15
+
16
+ context 'when a getter method is invoked' do
17
+ context 'when configurations has an exact match' do
18
+ let(:value) { 'configuration value' }
19
+ before { configuration.configurations[:some_key] = value }
20
+
21
+ it { expect(configuration.some_key).to eq(value) }
22
+ end
23
+
24
+ context 'when configurations does not have an exact match' do
25
+ it { expect { configuration.some_key }.to raise_error(NoMethodError) }
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#external' do
31
+ context 'when it is invoked with a block' do
32
+ it { expect(configuration.external(:google_drive) { |config| config.some = 'thing' }).to be_kind_of(C11n::Configuration) }
33
+ end
34
+
35
+ context 'when it is invoked without a block' do
36
+ context 'when an external service is configured before' do
37
+ before { configuration.external(:google_drive) { |config| config.some = 'thing' } }
38
+
39
+ it { expect(configuration.externals[:google_drive]).not_to be_nil }
40
+ end
41
+
42
+ context 'when no external service with matching name is configured before' do
43
+ it { expect { configuration.external(:google_drive) }.to raise_error(C11n::Configuration::NotConfigured) }
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#load_from_hash' do
49
+ let(:google_config) { { other: 'things' } }
50
+ let(:config_hash) { { some: 'thing', external: { google: google_config } } }
51
+
52
+ it { expect { configuration.load_from_hash(config_hash) }.not_to raise_error }
53
+
54
+ context 'when it is loaded' do
55
+ before { configuration.load_from_hash(config_hash) }
56
+
57
+ it { expect(configuration.some).to eq('thing') }
58
+ it { expect(configuration.external(:google).to_hash).to eq(google_config) }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'spec_helper'
4
+
5
+ describe C11n::Conversion::ComposedKeyDeserializer do
6
+ describe '#deserialize' do
7
+ let(:source) { [['general.title', '너말고 니친구'], ['views.confirmations.title', '너말고 니친구 메일 인증'], ['views.confirmations.send', '확인 메일 다시 받기'], ['views.passwords.title', '패스워드 리셋하기']] }
8
+ let(:result) { { general: { title: '너말고 니친구' }, views: { confirmations: { title: '너말고 니친구 메일 인증', send: '확인 메일 다시 받기' }, passwords: { title: '패스워드 리셋하기' } } } }
9
+ let(:deserializer) { C11n::Conversion::ComposedKeyDeserializer.new(source) }
10
+
11
+ it { expect { deserializer.deserialize }.not_to raise_error }
12
+ it { expect(deserializer.deserialize).to eq(result) }
13
+ end
14
+
15
+ describe '#path_for' do
16
+ let(:path) { 'views.confirmations.title' }
17
+ let(:deserializer) { C11n::Conversion::ComposedKeyDeserializer.new([]) }
18
+
19
+ it { expect(deserializer.send(:path_for, path)).to eq([:views, :confirmations, :title]) }
20
+ end
21
+
22
+ describe '#create_or_get_leaf_node_for' do
23
+ let(:path) { [:views, :confirmations, :title] }
24
+ let(:deserializer) { C11n::Conversion::ComposedKeyDeserializer.new([]) }
25
+
26
+ before { deserializer.create_or_get_leaf_node_for(path) }
27
+
28
+ it { expect(deserializer.translations[:views]).not_to be_nil }
29
+ it { expect(deserializer.translations[:views][:confirmations]).not_to be_nil }
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe C11n::Conversion::Serializer do
6
+ describe '#serialize' do
7
+ let(:source) { { general: { title: '너말고 니친구' }, views: { confirmations: { title: '너말고 니친구 메일 인증', send: '확인 메일 다시 받기' }, passwords: { title: '패스워드 리셋하기' } } } }
8
+ let(:result) { { 'general.title' => '너말고 니친구', 'views.confirmations.title' => '너말고 니친구 메일 인증', 'views.confirmations.send' => '확인 메일 다시 받기', 'views.passwords.title' => '패스워드 리셋하기' } }
9
+ let(:serializer) { C11n::Conversion::Serializer.new(source) }
10
+
11
+ it { expect(serializer.serialize).to eq(result) }
12
+ end
13
+
14
+ describe '#serialized_translations_for' do
15
+ let(:prefix) { 'prefix' }
16
+ let(:serializer) { C11n::Conversion::Serializer.new([]) }
17
+
18
+ context 'when depth of recursion is 0' do
19
+ let(:translations) { { hello: 'Hello', world: 'World' } }
20
+ let(:result) { [C11n::Conversion::Serializer::Pair.new('prefix.hello', 'Hello'), C11n::Conversion::Serializer::Pair.new('prefix.world', 'World')] }
21
+
22
+ it { expect(serializer.send(:serialized_translations_for, prefix, translations)).to eq(result) }
23
+ end
24
+
25
+ context 'when depth of recursion is larger than 0' do
26
+ let(:translations) { { recursive: { hello: 'Hello', world: 'World' } } }
27
+ let(:result) { [C11n::Conversion::Serializer::Pair.new('prefix.recursive.hello', 'Hello'), C11n::Conversion::Serializer::Pair.new('prefix.recursive.world', 'World')] }
28
+
29
+ it { expect(serializer.send(:serialized_translations_for, prefix, translations)).to eq(result) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe C11n::Table do
4
+ let(:raw_table) do
5
+ [
6
+ ['Key', 'ko', 'en'],
7
+ ['attribute_a', 'korean_a', 'english_a'],
8
+ ['attribute_b', 'korean_b', 'english_b']
9
+ ]
10
+ end
11
+
12
+ let(:table) { C11n::Table.new(raw_table) }
13
+
14
+ describe '#table_for' do
15
+ it { expect(table.table_for(:ko)).to eq([['attribute_a', 'korean_a'], ['attribute_b', 'korean_b']]) }
16
+ it { expect(table.table_for(:en)).to eq([['attribute_a', 'english_a'], ['attribute_b', 'english_b']]) }
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe C11n::Translations do
4
+ let(:translations) { C11n::Translations.new }
5
+ let(:translations_hash) { translations.to_hash }
6
+
7
+ describe '#translations_for' do
8
+ it { expect(translations.translations_for(:en)).not_to be_nil }
9
+ it { expect(translations.translations_for(:en)).to be_empty }
10
+ it { expect(translations.translations_for(:en)).to eq({}) }
11
+ end
12
+
13
+ describe '#add_translation' do
14
+ let(:key) { 'attribute_a' }
15
+ let(:value) { 'a' }
16
+ before { translations.add_translation(:en, key, value) }
17
+
18
+ it { expect(translations_hash[:en][key]).to eq(value) }
19
+ end
20
+
21
+ describe '#add_translations' do
22
+ let(:raw_translations) { { 'attribute_a' => 'a', 'attribute_b' => 'b' } }
23
+
24
+ before { translations.add_translations(:en, raw_translations) }
25
+
26
+ it { expect(translations_hash[:en]).to eq(raw_translations) }
27
+ end
28
+
29
+ describe '#import_with' do
30
+ let(:raw_translations) { { 'attribute_a' => 'a', 'attribute_b' => 'b' } }
31
+ let(:mock_importer) { double(import: { en: raw_translations }, categories: {}, types: {}) }
32
+
33
+ it { expect { translations.import_with(mock_importer) }.not_to raise_error }
34
+
35
+ context 'when it is done before' do
36
+ before { translations.import_with(mock_importer) }
37
+
38
+ it { expect(translations_hash[:en]).to eq(raw_translations) }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'c11n'
4
+
5
+ Dir['./lib/**/*.rb'].each { |f| require f }
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: c11n
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Inbeom Hwang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: google_drive
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
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: nokogiri
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Imports/exports i18n translations from/to Google Drive Spreadsheet for
70
+ easier collaboration.
71
+ email:
72
+ - hwanginbeom@gmail.com
73
+ executables:
74
+ - c11n
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - .gitignore
79
+ - .rspec
80
+ - Gemfile
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - bin/c11n
85
+ - c11n.gemspec
86
+ - lib/c11n.rb
87
+ - lib/c11n/configuration.rb
88
+ - lib/c11n/conversion/composed_key_deserializer.rb
89
+ - lib/c11n/conversion/plain_deserializer.rb
90
+ - lib/c11n/conversion/serializer.rb
91
+ - lib/c11n/executable/runner.rb
92
+ - lib/c11n/exporter/android.rb
93
+ - lib/c11n/exporter/google_drive.rb
94
+ - lib/c11n/exporter/ios.rb
95
+ - lib/c11n/exporter/yaml.rb
96
+ - lib/c11n/external/google_drive_driver.rb
97
+ - lib/c11n/importer/android.rb
98
+ - lib/c11n/importer/google_drive.rb
99
+ - lib/c11n/importer/ios.rb
100
+ - lib/c11n/importer/yaml.rb
101
+ - lib/c11n/synchronizer.rb
102
+ - lib/c11n/synchronizer/android.rb
103
+ - lib/c11n/synchronizer/ios.rb
104
+ - lib/c11n/synchronizer/rails.rb
105
+ - lib/c11n/table.rb
106
+ - lib/c11n/translations.rb
107
+ - lib/c11n/types.rb
108
+ - lib/c11n/version.rb
109
+ - spec/c11n/configuration_spec.rb
110
+ - spec/c11n/conversion/composed_key_deserializer_spec.rb
111
+ - spec/c11n/conversion/serializer_spec.rb
112
+ - spec/c11n/table_spec.rb
113
+ - spec/c11n/translations_spec.rb
114
+ - spec/spec_helper.rb
115
+ homepage: ''
116
+ licenses: []
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: '0'
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.0.6
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Manage i18n for your projects using Google Drive.
138
+ test_files:
139
+ - spec/c11n/configuration_spec.rb
140
+ - spec/c11n/conversion/composed_key_deserializer_spec.rb
141
+ - spec/c11n/conversion/serializer_spec.rb
142
+ - spec/c11n/table_spec.rb
143
+ - spec/c11n/translations_spec.rb
144
+ - spec/spec_helper.rb
145
+ has_rdoc: