c11n 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|