c11n 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: