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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/bin/c11n +22 -0
- data/c11n.gemspec +25 -0
- data/lib/c11n.rb +8 -0
- data/lib/c11n/configuration.rb +72 -0
- data/lib/c11n/conversion/composed_key_deserializer.rb +37 -0
- data/lib/c11n/conversion/plain_deserializer.rb +21 -0
- data/lib/c11n/conversion/serializer.rb +51 -0
- data/lib/c11n/executable/runner.rb +43 -0
- data/lib/c11n/exporter/android.rb +41 -0
- data/lib/c11n/exporter/google_drive.rb +40 -0
- data/lib/c11n/exporter/ios.rb +21 -0
- data/lib/c11n/exporter/yaml.rb +26 -0
- data/lib/c11n/external/google_drive_driver.rb +64 -0
- data/lib/c11n/importer/android.rb +41 -0
- data/lib/c11n/importer/google_drive.rb +47 -0
- data/lib/c11n/importer/ios.rb +36 -0
- data/lib/c11n/importer/yaml.rb +42 -0
- data/lib/c11n/synchronizer.rb +58 -0
- data/lib/c11n/synchronizer/android.rb +50 -0
- data/lib/c11n/synchronizer/ios.rb +44 -0
- data/lib/c11n/synchronizer/rails.rb +41 -0
- data/lib/c11n/table.rb +131 -0
- data/lib/c11n/translations.rb +49 -0
- data/lib/c11n/types.rb +6 -0
- data/lib/c11n/version.rb +3 -0
- data/spec/c11n/configuration_spec.rb +61 -0
- data/spec/c11n/conversion/composed_key_deserializer_spec.rb +31 -0
- data/spec/c11n/conversion/serializer_spec.rb +32 -0
- data/spec/c11n/table_spec.rb +18 -0
- data/spec/c11n/translations_spec.rb +41 -0
- data/spec/spec_helper.rb +5 -0
- metadata +145 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'c11n/external/google_drive_driver'
|
2
|
+
require 'c11n/types'
|
3
|
+
|
4
|
+
module C11n
|
5
|
+
module Exporter
|
6
|
+
class GoogleDrive
|
7
|
+
def initialize(options = {})
|
8
|
+
@driver = C11n::External::GoogleDriveDriver.new(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def table
|
12
|
+
@table ||= C11n::Table.new(@driver.table)
|
13
|
+
end
|
14
|
+
|
15
|
+
def export(translations, locale)
|
16
|
+
translations.translations_for(locale).each do |key, translation|
|
17
|
+
serialized_translation = serialize(translation, translations.types[key])
|
18
|
+
table.set(locale, key, serialized_translation, type: translations.types[key], category: translations.categories[key])
|
19
|
+
end
|
20
|
+
|
21
|
+
export_table(table)
|
22
|
+
end
|
23
|
+
|
24
|
+
def export_table(table)
|
25
|
+
@driver.write_table(table.rows)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def serialize(value, type = nil)
|
31
|
+
case type
|
32
|
+
when C11n::Types::ARRAY
|
33
|
+
value.join("\n\n")
|
34
|
+
else
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module C11n
|
2
|
+
module Exporter
|
3
|
+
class Ios
|
4
|
+
def initialize(options = {})
|
5
|
+
@path = options[:path]
|
6
|
+
end
|
7
|
+
|
8
|
+
def export(translations, locale)
|
9
|
+
file.print(translations.translations_for(locale).map do |key, translation|
|
10
|
+
"\"#{key}\" = \"#{translation}\";"
|
11
|
+
end.join("\n"))
|
12
|
+
|
13
|
+
file.close
|
14
|
+
end
|
15
|
+
|
16
|
+
def file
|
17
|
+
@file ||= File.new(@path, 'w')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'c11n/conversion/composed_key_deserializer'
|
2
|
+
|
3
|
+
module C11n
|
4
|
+
module Exporter
|
5
|
+
class Yaml
|
6
|
+
def initialize(options = {})
|
7
|
+
@base_path = options[:base_path]
|
8
|
+
@path = options[:path] unless @base_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def path_for(locale)
|
12
|
+
@path || "#{@base_path}/#{locale}.yml"
|
13
|
+
end
|
14
|
+
|
15
|
+
def file
|
16
|
+
@file ||= File.new(@path, 'w')
|
17
|
+
end
|
18
|
+
|
19
|
+
def export(translations, locale)
|
20
|
+
deserialized_translations = C11n::Conversion::ComposedKeyDeserializer.new(translations.translations_for(locale)).deserialize
|
21
|
+
file.print(YAML.dump(locale => deserialized_translations))
|
22
|
+
file.close
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'google_drive'
|
2
|
+
|
3
|
+
module C11n
|
4
|
+
module External
|
5
|
+
class GoogleDriveDriver
|
6
|
+
attr_accessor :email, :password, :spreadsheet_key, :worksheet_number
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
load_from_options(options)
|
10
|
+
load_configuration
|
11
|
+
end
|
12
|
+
|
13
|
+
def load_from_options(options)
|
14
|
+
@email = options[:email]
|
15
|
+
@password = options[:password]
|
16
|
+
@spreadsheet_key = options[:spreadsheet_key]
|
17
|
+
@worksheet_number = options[:worksheet_number]
|
18
|
+
end
|
19
|
+
|
20
|
+
def load_configuration
|
21
|
+
@email ||= C11n::Configuration.instance.external(:google_drive)[:email]
|
22
|
+
@password ||= C11n::Configuration.instance.external(:google_drive)[:password]
|
23
|
+
@spreadsheet_key ||= C11n::Configuration.instance.external(:google_drive)[:spreadsheet_key]
|
24
|
+
@worksheet_number ||= C11n::Configuration.instance.external(:google_drive)[:worksheet_number]
|
25
|
+
end
|
26
|
+
|
27
|
+
def connection
|
28
|
+
@connection ||= ::GoogleDrive.login(@email, @password)
|
29
|
+
end
|
30
|
+
|
31
|
+
def worksheet
|
32
|
+
@worksheet ||= spreadsheet.worksheets[@worksheet_number]
|
33
|
+
end
|
34
|
+
|
35
|
+
def spreadsheet
|
36
|
+
@spreadsheet ||= connection.spreadsheet_by_key(@spreadsheet_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def table
|
40
|
+
raw_table = []
|
41
|
+
|
42
|
+
for row in 1..worksheet.num_rows
|
43
|
+
raw_table[row - 1] ||= []
|
44
|
+
|
45
|
+
for col in 1..worksheet.num_cols
|
46
|
+
raw_table[row - 1][col - 1] ||= worksheet[row, col]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
raw_table
|
51
|
+
end
|
52
|
+
|
53
|
+
def write_table(table)
|
54
|
+
table.each_with_index do |row, row_index|
|
55
|
+
row.each_with_index do |column, column_index|
|
56
|
+
worksheet[row_index + 1, column_index + 1] = column if column
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
worksheet.save
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'c11n/types'
|
3
|
+
|
4
|
+
module C11n
|
5
|
+
module Importer
|
6
|
+
class Android
|
7
|
+
attr_reader :types, :categories
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@path = options[:path]
|
11
|
+
@locale = options[:locale]
|
12
|
+
@types = {}
|
13
|
+
@categories = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def file
|
17
|
+
File.open(@path, 'r')
|
18
|
+
end
|
19
|
+
|
20
|
+
def document
|
21
|
+
Nokogiri::XML(file)
|
22
|
+
end
|
23
|
+
|
24
|
+
def import
|
25
|
+
{ @locale.to_sym => to_hash }
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_hash
|
29
|
+
document.at_xpath('resources').elements.inject({}) do |result, element|
|
30
|
+
if element.name == 'string'
|
31
|
+
@types[element['name']] = C11n::Types::CDATA if element.children.any? { |child| child.cdata? }
|
32
|
+
result.merge(element['name'] => element.text.strip)
|
33
|
+
elsif element.name == 'string-array'
|
34
|
+
@types[element['name']] = C11n::Types::ARRAY
|
35
|
+
result.merge(element['name'] => element.elements.map(&:text))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'c11n/external/google_drive_driver'
|
2
|
+
require 'c11n/table'
|
3
|
+
|
4
|
+
module C11n
|
5
|
+
module Importer
|
6
|
+
class GoogleDrive
|
7
|
+
def initialize(options = {})
|
8
|
+
@driver = C11n::External::GoogleDriveDriver.new(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def table
|
12
|
+
@table ||= C11n::Table.new(@driver.table)
|
13
|
+
end
|
14
|
+
|
15
|
+
def import
|
16
|
+
deserialized = {}
|
17
|
+
|
18
|
+
table.to_hash.each do |locale, translations|
|
19
|
+
deserialized[locale] = translations.inject({}) do |result, key_and_translation|
|
20
|
+
result.merge key_and_translation.first => deserialize(key_and_translation.last, types[key_and_translation.first])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
deserialized
|
25
|
+
end
|
26
|
+
|
27
|
+
def categories
|
28
|
+
{}
|
29
|
+
end
|
30
|
+
|
31
|
+
def types
|
32
|
+
table.types
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def deserialize(value, type = nil)
|
38
|
+
case type
|
39
|
+
when C11n::Types::ARRAY
|
40
|
+
value.split("\n\n")
|
41
|
+
else
|
42
|
+
value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module C11n
|
2
|
+
module Importer
|
3
|
+
class Ios
|
4
|
+
attr_reader :types, :categories
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@path = options[:path]
|
8
|
+
@locale = options[:locale]
|
9
|
+
@types = {}
|
10
|
+
@categories = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def import
|
14
|
+
result = {}
|
15
|
+
|
16
|
+
file.each_line do |line|
|
17
|
+
result.merge parse(line)
|
18
|
+
end
|
19
|
+
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def file
|
24
|
+
File.open(@path, 'r')
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def parse(line)
|
30
|
+
matched_data = line.scan(/"([^"]+)"\s*=\s*"([^"]*)"/).first
|
31
|
+
|
32
|
+
matched_data && { matched_data.first => matched_data.last }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'c11n/conversion/serializer'
|
3
|
+
require 'c11n/types'
|
4
|
+
|
5
|
+
module C11n
|
6
|
+
module Importer
|
7
|
+
class Yaml
|
8
|
+
def initialize(options = {})
|
9
|
+
@base_path = options[:base_path]
|
10
|
+
@path = options[:path]
|
11
|
+
@types = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def path_for(locale)
|
15
|
+
@path || "#{@base_path}/#{locale}.yml"
|
16
|
+
end
|
17
|
+
|
18
|
+
def file
|
19
|
+
File.open(@path, 'r')
|
20
|
+
end
|
21
|
+
|
22
|
+
def import
|
23
|
+
raw_yaml = YAML.load(file.read)
|
24
|
+
translations = C11n::Conversion::Serializer.new(raw_yaml.values.first).serialize
|
25
|
+
|
26
|
+
translations.each do |key, translation|
|
27
|
+
@types[key] = C11n::Types::ARRAY if translation.is_a?(Array)
|
28
|
+
end
|
29
|
+
|
30
|
+
{ raw_yaml.keys.first => translations }
|
31
|
+
end
|
32
|
+
|
33
|
+
def categories
|
34
|
+
{}
|
35
|
+
end
|
36
|
+
|
37
|
+
def types
|
38
|
+
@types
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'c11n/configuration'
|
2
|
+
require 'c11n/translations'
|
3
|
+
require 'c11n/importer/google_drive'
|
4
|
+
require 'c11n/exporter/google_drive'
|
5
|
+
require 'c11n/synchronizer/ios'
|
6
|
+
require 'c11n/synchronizer/android'
|
7
|
+
require 'c11n/synchronizer/rails'
|
8
|
+
|
9
|
+
module C11n
|
10
|
+
class Synchronizer
|
11
|
+
def project
|
12
|
+
@project ||= case @configuration.type
|
13
|
+
when 'ios'
|
14
|
+
C11n::Synchronizer::Ios.new(@configuration.to_hash)
|
15
|
+
when 'android'
|
16
|
+
C11n::Synchronizer::Android.new(@configuration.to_hash)
|
17
|
+
when 'rails'
|
18
|
+
C11n::Synchronizer::Rails.new(@configuration.to_hash)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(options = {})
|
23
|
+
@configuration = options[:configuration]
|
24
|
+
@translations = C11n::Translations.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def google_drive_importer
|
28
|
+
@google_drive_importer ||= C11n::Importer::GoogleDrive.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def google_drive_exporter
|
32
|
+
@google_drive_exporter ||= C11n::Exporter::GoogleDrive.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def upload
|
36
|
+
project.importers.each do |locale, importer|
|
37
|
+
@translations.import_with(importer)
|
38
|
+
@translations.export_with(google_drive_exporter, locale)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def download
|
43
|
+
@translations.import_with(google_drive_importer)
|
44
|
+
|
45
|
+
project.exporters.each do |locale, exporter|
|
46
|
+
@translations.export_with(exporter, locale)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def setup
|
51
|
+
schema = [C11n::Table::CATEGORY_COLUMN, C11n::Table::KEY_COLUMN, C11n::Table::TYPE_COLUMN] + project.importers.keys.map(&:to_s)
|
52
|
+
|
53
|
+
table_with_schema = C11n::Table.new([schema])
|
54
|
+
|
55
|
+
google_drive_exporter.export_table(table_with_schema)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'c11n/importer/android'
|
2
|
+
require 'c11n/exporter/android'
|
3
|
+
|
4
|
+
module C11n
|
5
|
+
class Synchronizer
|
6
|
+
class Android
|
7
|
+
RESOURCES = 'res'
|
8
|
+
BASE_PATTERN = /^values-?/
|
9
|
+
FILE_NAME = 'strings.xml'
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@project_root = options[:root]
|
13
|
+
@default_locale = options[:default_locale]
|
14
|
+
end
|
15
|
+
|
16
|
+
def importers
|
17
|
+
@importers ||= Hash[localization_files.map do |locale, file_path|
|
18
|
+
[locale, C11n::Importer::Android.new(path: file_path, locale: locale)]
|
19
|
+
end]
|
20
|
+
end
|
21
|
+
|
22
|
+
def exporters
|
23
|
+
@exporters ||= Hash[localization_files.map do |locale, file_path|
|
24
|
+
[locale, C11n::Exporter::Android.new(path: file_path)]
|
25
|
+
end]
|
26
|
+
end
|
27
|
+
|
28
|
+
def localization_files
|
29
|
+
@localization_files ||= localization_directories.inject({}) do |files, localization_directory|
|
30
|
+
locale = localization_directory.gsub(BASE_PATTERN, '')
|
31
|
+
path = File.join(@project_root, RESOURCES, localization_directory, FILE_NAME)
|
32
|
+
|
33
|
+
files.merge (locale.empty? ? @default_locale : locale.to_sym) => path
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def localization_directories
|
38
|
+
@localization_directories ||= Dir.new(File.join(@project_root, RESOURCES)).entries.select { |entry| entry =~ BASE_PATTERN }.select do |entry|
|
39
|
+
Dir.new(File.join(@project_root, RESOURCES, entry)).entries.include?(FILE_NAME)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def locale_for(directory_name)
|
46
|
+
directory_name == BASE_NAME ? @default_locale.to_sym : directory_name.gsub("#{BASE_NAME}-", '').to_sym
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'c11n/importer/ios'
|
2
|
+
require 'c11n/exporter/ios'
|
3
|
+
|
4
|
+
module C11n
|
5
|
+
class Synchronizer
|
6
|
+
class Ios
|
7
|
+
SUPPORT_FILES = 'support files'
|
8
|
+
LOCALIZABLE = 'Localizable.strings'
|
9
|
+
|
10
|
+
def initiailze(options = {})
|
11
|
+
@project_root = options[:root]
|
12
|
+
end
|
13
|
+
|
14
|
+
def importers
|
15
|
+
@importers ||= Hash[localization_files.map do |locale, file_path|
|
16
|
+
[locale, C11n::Importer::Ios.new(path: file_path, locale: locale)]
|
17
|
+
end]
|
18
|
+
end
|
19
|
+
|
20
|
+
def exporters
|
21
|
+
@exporters ||= Hash[localization_files.map do |locale|
|
22
|
+
[locale, C11n::Exporter::Ios.new(path: file_path)]
|
23
|
+
end]
|
24
|
+
end
|
25
|
+
|
26
|
+
def localization_files
|
27
|
+
@localization_files ||= localization_directories.inject({}) do |files, localization_directory|
|
28
|
+
locale = localization_directory.gsub(/\.lproj$/, '').to_sym
|
29
|
+
path = File.join(@project_root, SUPPORT_FILES, localization_directory, LOCALIZABLE)
|
30
|
+
|
31
|
+
files.merge locale => path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def locales
|
36
|
+
localization_directories.map { |directory| directory.gsub(/\.lproj$/, '') }.map(&:to_sym)
|
37
|
+
end
|
38
|
+
|
39
|
+
def localization_directories
|
40
|
+
@localization_directories ||= Dir.new(File.join(@project_root, SUPPORT_FILES)).entries.select { |entry| entry =~ /lproj$/ }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|