ad_localize 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/ad_localize +3 -0
- data/lib/ad_localize.rb +36 -0
- data/lib/ad_localize/ad_logger.rb +33 -0
- data/lib/ad_localize/constant.rb +22 -0
- data/lib/ad_localize/csv_file_manager.rb +46 -0
- data/lib/ad_localize/csv_parser.rb +127 -0
- data/lib/ad_localize/option_handler.rb +57 -0
- data/lib/ad_localize/platform/android_formatter.rb +68 -0
- data/lib/ad_localize/platform/ios_formatter.rb +69 -0
- data/lib/ad_localize/platform/json_formatter.rb +18 -0
- data/lib/ad_localize/platform/platform_formatter.rb +76 -0
- data/lib/ad_localize/platform/yml_formatter.rb +18 -0
- data/lib/ad_localize/runner.rb +45 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4ae0a65677bc70e3175aae752514334aac36642bfb2a2dc2ce8e750f70bbf1d2
|
4
|
+
data.tar.gz: 30fb7aa05e9b7b6d27f608ec8d9c712f71be21be91560f082cb13f935167e258
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f9e675d6438e1883632aa685e74166021fbe71e75ae1de4b4fda6d93b5c3705b177e0b19dc7399737261d97499c6521c5f746228e021df5401f97ef3fa57ccd6
|
7
|
+
data.tar.gz: 86b8d0efe4d91fe6eebb6849a76e1bb090c797de13ce3d430fb8a49efa7c93986626a1bb31bfccc639b19006ab7f6edae8c60e339f36b3aeb5d8734fc3e5b024
|
data/bin/ad_localize
ADDED
data/lib/ad_localize.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
require 'byebug'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'pathname'
|
8
|
+
require 'yaml'
|
9
|
+
require 'logger'
|
10
|
+
require 'colorize'
|
11
|
+
require 'csv'
|
12
|
+
require 'active_support'
|
13
|
+
require 'open-uri'
|
14
|
+
require 'optparse'
|
15
|
+
require 'json'
|
16
|
+
require 'nokogiri'
|
17
|
+
|
18
|
+
require 'ad_localize/ad_logger'
|
19
|
+
require 'ad_localize/constant'
|
20
|
+
require 'ad_localize/csv_file_manager'
|
21
|
+
require 'ad_localize/csv_parser'
|
22
|
+
require 'ad_localize/option_handler'
|
23
|
+
require 'ad_localize/runner'
|
24
|
+
require 'ad_localize/platform/platform_formatter'
|
25
|
+
require 'ad_localize/platform/android_formatter'
|
26
|
+
require 'ad_localize/platform/ios_formatter'
|
27
|
+
require 'ad_localize/platform/json_formatter'
|
28
|
+
require 'ad_localize/platform/yml_formatter'
|
29
|
+
|
30
|
+
module AdLocalize
|
31
|
+
LOGGER = AdLogger.new
|
32
|
+
|
33
|
+
def self.run
|
34
|
+
Runner.new.run
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AdLocalize
|
2
|
+
class AdLogger
|
3
|
+
SEVERITY = { debug: Logger::DEBUG , info: Logger::INFO, warn: Logger::WARN, error: Logger::ERROR, fatal: Logger::FATAL, unknown: Logger::UNKNOWN }
|
4
|
+
AUTHORIZED_COLORS = [:black, :yellow, :red, :blue, :green]
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@logger = Logger.new(STDOUT)
|
8
|
+
@logger.level = Logger::INFO
|
9
|
+
end
|
10
|
+
|
11
|
+
def log(level, color, text)
|
12
|
+
exit unless text and level and color
|
13
|
+
|
14
|
+
level, color = [level, color].map(&:to_sym)
|
15
|
+
raise "Color not supported" unless AUTHORIZED_COLORS.include? color
|
16
|
+
raise "Invalid severity" unless SEVERITY.dig(level)
|
17
|
+
|
18
|
+
@logger.add(SEVERITY.dig(level), text.send(color))
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug!
|
22
|
+
@logger.level = Logger::DEBUG
|
23
|
+
end
|
24
|
+
|
25
|
+
def debug?
|
26
|
+
@logger.debug?
|
27
|
+
end
|
28
|
+
|
29
|
+
def close
|
30
|
+
@logger.close
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module AdLocalize
|
2
|
+
module Constant
|
3
|
+
SUPPORTED_PLATFORMS = %w(ios android yml json)
|
4
|
+
PLURAL_KEY_SYMBOL = :plural
|
5
|
+
SINGULAR_KEY_SYMBOL = :singular
|
6
|
+
COMMENT_KEY_SYMBOL = :comment
|
7
|
+
COMMENT_KEY_COLUMN_IDENTIFIER = "comment"
|
8
|
+
DEFAULT_CONFIG = {
|
9
|
+
platforms: {
|
10
|
+
export_directory_names: {
|
11
|
+
ios: "%{locale}.lproj",
|
12
|
+
android: "values%{locale}"
|
13
|
+
}
|
14
|
+
}
|
15
|
+
}
|
16
|
+
CONFIG = YAML.load_file(Pathname::pwd + 'config.yml').deep_symbolize_keys rescue DEFAULT_CONFIG
|
17
|
+
EXPORT_FOLDER = 'exports'
|
18
|
+
IOS_SINGULAR_EXPORT_FILENAME = "Localizable.strings"
|
19
|
+
IOS_PLURAL_EXPORT_FILENAME = "Localizable.stringsdict"
|
20
|
+
ANDROID_EXPORT_FILENAME = "strings.xml"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module AdLocalize
|
2
|
+
class CsvFileManager
|
3
|
+
CSV_CONTENT_TYPES = %w(text/csv text/plain)
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def csv?(file)
|
7
|
+
!file.nil? && CSV_CONTENT_TYPES.include?(`file --brief --mime-type '#{file}'`.strip)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns the downloaded file name (it is located in the current directory)
|
11
|
+
def download_from_drive(key)
|
12
|
+
LOGGER.log(:info, :black, "Downloading file from google drive...")
|
13
|
+
download_string_path = "./#{key}.csv"
|
14
|
+
begin
|
15
|
+
File.open(download_string_path, "wb") do |saved_file|
|
16
|
+
# the following "open" is provided by open-uri
|
17
|
+
open(drive_download_url(key), "rb") do |read_file|
|
18
|
+
saved_file.write(read_file.read)
|
19
|
+
end
|
20
|
+
File.basename saved_file
|
21
|
+
end
|
22
|
+
rescue => e
|
23
|
+
delete_drive_file(download_string_path)
|
24
|
+
LOGGER.log(:error, :red, e.message)
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def select_csvs(files)
|
30
|
+
files.select do |file|
|
31
|
+
LOGGER.log(:error, :red, "#{file} is not a csv. It will be ignored") unless self.csv?(file)
|
32
|
+
self.csv?(file)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete_drive_file(file)
|
37
|
+
Pathname.new(file).delete unless file.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def drive_download_url(key)
|
42
|
+
"https://docs.google.com/spreadsheets/d/#{key}/export?format=csv&id=#{key}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module AdLocalize
|
2
|
+
class CsvParser
|
3
|
+
CSV_WORDING_KEYS_COLUMN = "key"
|
4
|
+
PLURAL_KEY_REGEXP = /\#\#\{(\w+)\}/
|
5
|
+
|
6
|
+
attr_accessor :locales
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@locales = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Raise a StandardError if no locale is detected in the csv
|
13
|
+
def extract_data(file_name)
|
14
|
+
data = {}
|
15
|
+
CSV.foreach(file_name, headers: true, skip_blanks: true) do |row|
|
16
|
+
validity_status = check_row(row)
|
17
|
+
if validity_status.zero?
|
18
|
+
next
|
19
|
+
elsif validity_status == -1
|
20
|
+
exit
|
21
|
+
else
|
22
|
+
find_locales(row) if locales.empty?
|
23
|
+
row_data = parse_row(row)
|
24
|
+
data.deep_merge!(row_data) unless row_data.nil?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
data
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
# Returns 1 if row is ok, 0 if row there are missing information and -1 if row is not a csv row
|
32
|
+
def check_row(row)
|
33
|
+
valid_row = 1
|
34
|
+
# Check non empty row
|
35
|
+
if row.field(CSV_WORDING_KEYS_COLUMN).nil?
|
36
|
+
LOGGER.log(:error, :red, "Missing key in line #{$.}") unless row.fields.all?(&:nil?)
|
37
|
+
valid_row = 0
|
38
|
+
elsif not row.headers.include?(CSV_WORDING_KEYS_COLUMN)
|
39
|
+
LOGGER.log(:error, :red, "[CSV FORMAT] #{file_name} is not a valid file")
|
40
|
+
valid_row = -1
|
41
|
+
end
|
42
|
+
return valid_row
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_locales(row)
|
46
|
+
wording_key_column_index = row.index(CSV_WORDING_KEYS_COLUMN)
|
47
|
+
if row.length > wording_key_column_index
|
48
|
+
self.locales = row.headers.slice((wording_key_column_index+1)..-1).compact.reject do |header|
|
49
|
+
header.to_s.include? Constant::COMMENT_KEY_COLUMN_IDENTIFIER
|
50
|
+
end
|
51
|
+
end
|
52
|
+
if locales.empty?
|
53
|
+
raise 'NO DETECTED LOCALES'
|
54
|
+
else
|
55
|
+
LOGGER.log(:debug, :black, "DETECTED LOCALES : #{locales.join(', ')}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_row(row)
|
60
|
+
key_infos = parse_key(row)
|
61
|
+
return nil if key_infos.nil?
|
62
|
+
|
63
|
+
locales.each_with_object({ key_infos.dig(:key) => {} }) do |locale, output|
|
64
|
+
current_key = key_infos.dig(:key)
|
65
|
+
current_key_already_has_wording_for_locale = output[current_key].key? locale.to_sym
|
66
|
+
output[current_key][locale.to_sym] = { key_infos.dig(:numeral_key) => {} } unless current_key_already_has_wording_for_locale
|
67
|
+
|
68
|
+
if key_infos.dig(:numeral_key) == Constant::SINGULAR_KEY_SYMBOL
|
69
|
+
trace_wording(row[locale], "[#{locale.upcase}] Singular key ---> #{current_key}", "[#{locale.upcase}] Missing translation for #{current_key}")
|
70
|
+
value = row[locale] || default_wording(locale, current_key)
|
71
|
+
elsif key_infos.dig(:numeral_key) == Constant::PLURAL_KEY_SYMBOL
|
72
|
+
trace_wording(row[locale],
|
73
|
+
"[#{locale.upcase}] Plural key ---> plural_identifier : #{key_infos.dig(:plural_identifier)}, key : #{current_key}",
|
74
|
+
"[#{locale.upcase}] Missing translation for #{current_key} (#{key_infos.dig(:plural_identifier)})")
|
75
|
+
value = { key_infos.dig(:plural_identifier) => row[locale] || default_wording(locale, "#{current_key} (#{key_infos.dig(:plural_identifier)})") }
|
76
|
+
else
|
77
|
+
raise "Unknown numeral key #{key_infos.dig(:numeral_key)}"
|
78
|
+
end
|
79
|
+
|
80
|
+
check_wording_parameters(row[locale], locale, current_key)
|
81
|
+
output[current_key][locale.to_sym][key_infos.dig(:numeral_key)] = value
|
82
|
+
locale_comment_column = "#{Constant::COMMENT_KEY_COLUMN_IDENTIFIER} #{locale}"
|
83
|
+
output[current_key][locale.to_sym][Constant::COMMENT_KEY_SYMBOL] = row[locale_comment_column] unless locale_comment_column.blank? and row[locale_comment_column].blank?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_key(row)
|
88
|
+
key = row.field(CSV_WORDING_KEYS_COLUMN)
|
89
|
+
plural_prefix = key.match(PLURAL_KEY_REGEXP)
|
90
|
+
plural_identifier = nil
|
91
|
+
invalid_plural = false
|
92
|
+
|
93
|
+
if plural_prefix.nil?
|
94
|
+
numeral_key = Constant::SINGULAR_KEY_SYMBOL
|
95
|
+
else
|
96
|
+
numeral_key = Constant::PLURAL_KEY_SYMBOL
|
97
|
+
key = plural_prefix.pre_match
|
98
|
+
plural_identifier = plural_prefix.captures&.first
|
99
|
+
LOGGER.log(:debug, :red, "Invalid key #{key}") if key.nil?
|
100
|
+
LOGGER.log(:debug, :red, "Empty plural prefix!") if plural_identifier.nil?
|
101
|
+
invalid_plural = plural_identifier.nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
(key.nil? or invalid_plural) ? nil : { key: key.to_sym, numeral_key: numeral_key, plural_identifier: plural_identifier&.to_sym }
|
105
|
+
end
|
106
|
+
|
107
|
+
def default_wording(locale, key)
|
108
|
+
LOGGER.debug? ? "<[#{locale.upcase}] Missing translation for #{key}>" : ""
|
109
|
+
end
|
110
|
+
|
111
|
+
def trace_wording(wording, present_message, missing_message)
|
112
|
+
if wording
|
113
|
+
LOGGER.log(:debug, :black, present_message)
|
114
|
+
else
|
115
|
+
LOGGER.log(:debug, :yellow, missing_message)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def check_wording_parameters(value, locale, key)
|
120
|
+
formatted_arguments = value&.scan(/%(\d$)?@/) || []
|
121
|
+
if formatted_arguments.size >= 2
|
122
|
+
is_all_ordered = formatted_arguments.inject(true){ |is_ordered, match| is_ordered &&= !match.join.empty? }
|
123
|
+
LOGGER.log(:warn, :red, "[#{locale.upcase}] Multiple arguments for #{key} but no order") unless is_all_ordered
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module AdLocalize
|
2
|
+
class OptionHandler
|
3
|
+
GOOGLE_DRIVE_DOCUMENT_ID = { length: 32, regexp: /\A[\w-]+\Z/ }
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def parse
|
7
|
+
args = { debug: false, only: nil}
|
8
|
+
|
9
|
+
opt_parser = OptionParser.new do |opts|
|
10
|
+
opts.banner = "Usage: ruby bin/export [options] file(s)"
|
11
|
+
|
12
|
+
|
13
|
+
opts.on("-h", "--help", "Prints help") do
|
14
|
+
puts opts
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
opts.on("-d", "--debug", "Run in debug mode") do
|
18
|
+
args[:debug] = true
|
19
|
+
LOGGER.debug!
|
20
|
+
end
|
21
|
+
opts.on("-k", "--drive-key #{GOOGLE_DRIVE_DOCUMENT_ID.dig(:length)}_characters", String, "Use google drive spreadsheets") do |key|
|
22
|
+
is_valid_drive_key = !!(key =~ GOOGLE_DRIVE_DOCUMENT_ID.dig(:regexp)) && (key.size >= GOOGLE_DRIVE_DOCUMENT_ID.dig(:length))
|
23
|
+
args[:drive_key] = is_valid_drive_key ? key : nil
|
24
|
+
end
|
25
|
+
opts.on("-o", "--only platform1,platform2", Array, "Only generate localisation files for the specified platforms. Supported platforms : #{Constant::SUPPORTED_PLATFORMS.join(', ')}") do |platforms|
|
26
|
+
args[:only] = filter_option_args("-o", platforms) { |platform| !!Constant::SUPPORTED_PLATFORMS.index(platform) }
|
27
|
+
end
|
28
|
+
opts.on("-t", "--target-dir PATH", String, "Path to the target directory") do |output_path|
|
29
|
+
pn = Pathname.new(output_path)
|
30
|
+
if pn.directory? and pn.readable? and pn.writable?
|
31
|
+
args[:output_path] = output_path
|
32
|
+
else
|
33
|
+
raise ArgumentError.new("Invalid target directory. Check the permissions")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
begin
|
39
|
+
opt_parser.parse!
|
40
|
+
rescue OptionParser::MissingArgument => e
|
41
|
+
LOGGER.log(:error, :red, "Missing argument for option #{e.args.join(',')}")
|
42
|
+
rescue ArgumentError => e
|
43
|
+
LOGGER.log(:error, :red, e.message)
|
44
|
+
end
|
45
|
+
args
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def filter_option_args(option, args)
|
50
|
+
result = args.select { |arg| yield(arg) }
|
51
|
+
invalid_args = args - result
|
52
|
+
LOGGER.log(:debug, :red, "Invalid arg(s) for option #{option}: #{invalid_args.join(', ')}") unless invalid_args.empty?
|
53
|
+
result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module AdLocalize::Platform
|
2
|
+
class AndroidFormatter < PlatformFormatter
|
3
|
+
def platform
|
4
|
+
:android
|
5
|
+
end
|
6
|
+
|
7
|
+
def export(locale, data, export_extension = nil, substitution_format = nil)
|
8
|
+
locale = locale.to_sym
|
9
|
+
export_dir_suffix = (locale == default_locale) ? "" : "-#{locale.downcase}"
|
10
|
+
create_locale_dir(export_dir_suffix)
|
11
|
+
|
12
|
+
xml_doc = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
13
|
+
xml.resources {
|
14
|
+
data.each do |key, wording|
|
15
|
+
singular_wording = wording.dig(locale, AdLocalize::Constant::SINGULAR_KEY_SYMBOL)
|
16
|
+
add_singular_wording_to_xml(key, singular_wording, xml) unless singular_wording.blank?
|
17
|
+
|
18
|
+
plural_wording = wording.dig(locale, AdLocalize::Constant::PLURAL_KEY_SYMBOL)
|
19
|
+
add_plural_wording_to_xml(key, plural_wording, xml) unless plural_wording.blank?
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
export_dir(export_dir_suffix).join(AdLocalize::Constant::ANDROID_EXPORT_FILENAME).open("w") do |file|
|
25
|
+
file.puts xml_doc.to_xml(indent: 4)
|
26
|
+
end
|
27
|
+
AdLocalize::LOGGER.log(:debug, :black, "Android [#{locale}] ---> DONE!")
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def ios_converter(value)
|
32
|
+
processedValue = value.gsub(/</, "<")
|
33
|
+
processedValue = processedValue.gsub(/>/, ">")
|
34
|
+
processedValue = processedValue.gsub(/(?<!\\)'/, "\\\\'")
|
35
|
+
processedValue = processedValue.gsub(/(?<!\\)\"/, "\\\"")
|
36
|
+
processedValue = processedValue.gsub(/&(?!(?:amp|lt|gt|quot|apos);)/, '&')
|
37
|
+
processedValue = processedValue.gsub(/(%(\d+\$)?@)/, '%\2s') # should match values like %1$s and %s
|
38
|
+
processedValue = processedValue.gsub(/(%((\d+\$)?(\d+)?)i)/, '%\2d') # should match values like %i, %3$i, %01i, %1$02i
|
39
|
+
processedValue = processedValue.gsub(/%(?!((\d+\$)?(s|(\d+)?d)))/, '%%') # negative lookahead: identifies when user really wants to display a %
|
40
|
+
processedValue = processedValue.gsub("\\U", "\\u")
|
41
|
+
"\"#{processedValue}\""
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_singular_wording_to_xml(key, text, xml)
|
45
|
+
xml.string(name: key) {
|
46
|
+
add_wording_text_to_xml(text, xml)
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_plural_wording_to_xml(key, plural_hash, xml)
|
51
|
+
xml.plurals(name: key) {
|
52
|
+
plural_hash.each do |plural_type, plural_text|
|
53
|
+
next if plural_text.blank?
|
54
|
+
xml.item(quantity: plural_type) {
|
55
|
+
add_wording_text_to_xml(plural_text, xml)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_wording_text_to_xml(wording_text, xml)
|
62
|
+
raise 'Wording text should not be empty' if wording_text.blank?
|
63
|
+
xml.text(
|
64
|
+
ios_converter(wording_text)
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module AdLocalize::Platform
|
2
|
+
class IosFormatter < PlatformFormatter
|
3
|
+
def platform
|
4
|
+
:ios
|
5
|
+
end
|
6
|
+
|
7
|
+
def export(locale, data, export_extension = nil, substitution_format = nil)
|
8
|
+
create_locale_dir(locale)
|
9
|
+
[AdLocalize::Constant::PLURAL_KEY_SYMBOL, AdLocalize::Constant::SINGULAR_KEY_SYMBOL].each do |numeral_key|
|
10
|
+
numeral_data = data.select {|key, wording| wording.dig(locale.to_sym)&.key? numeral_key}
|
11
|
+
if numeral_data.empty?
|
12
|
+
AdLocalize::LOGGER.log(:info, :black, "[#{locale.upcase}] no #{numeral_key.to_s} keys were found to generate the file")
|
13
|
+
else
|
14
|
+
send("write_#{numeral_key}", locale, numeral_data)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
def write_singular(locale, singulars)
|
21
|
+
locale = locale.to_sym
|
22
|
+
|
23
|
+
singulars.each do |key, locales_wording|
|
24
|
+
value = locales_wording.dig(locale, AdLocalize::Constant::SINGULAR_KEY_SYMBOL)
|
25
|
+
comment = locales_wording.dig(locale, AdLocalize::Constant::COMMENT_KEY_SYMBOL)
|
26
|
+
export_dir(locale).join(AdLocalize::Constant::IOS_SINGULAR_EXPORT_FILENAME).open("a") do |file|
|
27
|
+
line = "\"#{key}\" = \"#{value}\";"
|
28
|
+
line << " // #{comment}" unless comment.nil?
|
29
|
+
line << "\n"
|
30
|
+
file.puts line
|
31
|
+
end
|
32
|
+
end
|
33
|
+
AdLocalize::LOGGER.log(:debug, :black, "iOS singular [#{locale}] ---> DONE!")
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_plural(locale, plurals)
|
37
|
+
locale = locale.to_sym
|
38
|
+
|
39
|
+
xml_doc = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
40
|
+
xml.plist {
|
41
|
+
xml.dict {
|
42
|
+
plurals.each do |wording_key, translations|
|
43
|
+
xml.key wording_key
|
44
|
+
xml.dict {
|
45
|
+
xml.key "NSStringLocalizedFormatKey"
|
46
|
+
xml.string "%\#@key@"
|
47
|
+
xml.key "key"
|
48
|
+
xml.dict {
|
49
|
+
xml.key "NSStringFormatSpecTypeKey"
|
50
|
+
xml.string "NSStringPluralRuleType"
|
51
|
+
xml.key "NSStringFormatValueTypeKey"
|
52
|
+
xml.string "d"
|
53
|
+
translations[locale][AdLocalize::Constant::PLURAL_KEY_SYMBOL].each do |wording_type, wording_value|
|
54
|
+
xml.key wording_type
|
55
|
+
xml.string wording_value
|
56
|
+
end
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
export_dir(locale).join(AdLocalize::Constant::IOS_PLURAL_EXPORT_FILENAME).open("w") do |file|
|
64
|
+
file.puts xml_doc.to_xml(indent: 4)
|
65
|
+
end
|
66
|
+
AdLocalize::LOGGER.log(:debug, :black, "iOS plural [#{locale}] ---> DONE!")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module AdLocalize::Platform
|
2
|
+
class JsonFormatter < PlatformFormatter
|
3
|
+
def platform
|
4
|
+
:json
|
5
|
+
end
|
6
|
+
|
7
|
+
def export(locale, data, export_extension = "json", substitution_format = "angular")
|
8
|
+
super(locale, data, export_extension, substitution_format) do |json_data, file|
|
9
|
+
file.puts json_data.to_json
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
def ios_converter(value)
|
15
|
+
value.gsub(/(%(?!)?(\d+\$)?[@isd])/, "VARIABLE_TO_SET")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module AdLocalize::Platform
|
2
|
+
class PlatformFormatter
|
3
|
+
PLURAL_KEY_SYMBOL = :plural
|
4
|
+
SINGULAR_KEY_SYMBOL = :singular
|
5
|
+
OPTIONS = {output_path: ".."}
|
6
|
+
|
7
|
+
attr_accessor :platform_dir
|
8
|
+
attr_reader :default_locale, :output_path
|
9
|
+
|
10
|
+
def initialize(default_locale, output_path)
|
11
|
+
@default_locale = default_locale.to_sym
|
12
|
+
@output_path = output_path # Must be set before platform_dir
|
13
|
+
@platform_dir = export_base_directory.join(platform.to_s)
|
14
|
+
create_platform_dir
|
15
|
+
end
|
16
|
+
|
17
|
+
def platform
|
18
|
+
raise 'Please override me!'
|
19
|
+
end
|
20
|
+
|
21
|
+
def export(locale, data, export_extension, substitution_format)
|
22
|
+
locale = locale.to_sym
|
23
|
+
formatted_data = data.each_with_object({}) do |(key, wording), hash_acc|
|
24
|
+
hash_acc[locale.to_s] = {} unless hash_acc.key? locale.to_s
|
25
|
+
if wording.dig(locale)&.key? :singular
|
26
|
+
value = ios_converter(wording.dig(locale, :singular))
|
27
|
+
hash_acc[locale.to_s][key.to_s] = value
|
28
|
+
end
|
29
|
+
if wording.dig(locale)&.key? :plural
|
30
|
+
hash_acc[locale.to_s][key.to_s] = {} unless hash_acc[locale.to_s].key? key.to_s
|
31
|
+
wording.dig(locale, :plural).each do |plural_type, plural_text|
|
32
|
+
value = ios_converter(plural_text)
|
33
|
+
hash_acc[locale.to_s][key.to_s][plural_type.to_s] = value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
platform_dir.join("#{locale.to_s}.#{export_extension}").open("w") do |file|
|
39
|
+
yield(formatted_data, file)
|
40
|
+
end
|
41
|
+
|
42
|
+
AdLocalize::LOGGER.log(:debug, :black, "#{platform.to_s.upcase} [#{locale}] ---> DONE!")
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
def export_base_directory
|
47
|
+
if output_path
|
48
|
+
Pathname.new(output_path)
|
49
|
+
else
|
50
|
+
Pathname::pwd.join(AdLocalize::Constant::EXPORT_FOLDER)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_platform_dir
|
55
|
+
if platform_dir.directory?
|
56
|
+
FileUtils.rm_rf("#{platform_dir}/.", secure: true)
|
57
|
+
else
|
58
|
+
platform_dir.mkpath
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def export_dir(locale)
|
63
|
+
platform_dir + AdLocalize::Constant::CONFIG.dig(:platforms, :export_directory_names, platform.to_sym) % { locale: locale.downcase }
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_locale_dir(locale)
|
67
|
+
raise 'Locale needed' unless locale
|
68
|
+
|
69
|
+
if export_dir(locale).directory?
|
70
|
+
FileUtils.rm_rf("#{export_dir(locale)}/.", secure: true)
|
71
|
+
else
|
72
|
+
export_dir(locale).mkdir
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module AdLocalize::Platform
|
2
|
+
class YmlFormatter < PlatformFormatter
|
3
|
+
def platform
|
4
|
+
:yml
|
5
|
+
end
|
6
|
+
|
7
|
+
def export(locale, data, export_extension = "yml", substitution_format = "yml")
|
8
|
+
super(locale, data, export_extension, substitution_format) do |yml_data, file|
|
9
|
+
file.puts yml_data.to_yaml
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
def ios_converter(value)
|
15
|
+
value.gsub(/(%(?!)?(\d+\$)?[@isd])/, "VARIABLE_TO_SET")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module AdLocalize
|
2
|
+
class Runner
|
3
|
+
attr_accessor :options
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@options = OptionHandler.parse
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(args = ARGV)
|
10
|
+
LOGGER.log(:info, :black, "OPTIONS : #{options}")
|
11
|
+
input_files = (args + [options.dig(:drive_key)]).compact # drive_key can be nil
|
12
|
+
if input_files.length.zero?
|
13
|
+
LOGGER.log(:error, :red, "No CSV to parse. Use option -h to see how to use this script")
|
14
|
+
else
|
15
|
+
file_to_parse = args.first
|
16
|
+
LOGGER.log(:warn, :yellow, "Only one CSV can be treated - the priority goes to #{file_to_parse}") if input_files.length > 1
|
17
|
+
if args.empty?
|
18
|
+
options[:drive_file] = CsvFileManager.download_from_drive(options.dig(:drive_key))
|
19
|
+
file_to_parse = options.dig(:drive_file)
|
20
|
+
end
|
21
|
+
CsvFileManager.csv?(file_to_parse) ? export(file_to_parse) : LOGGER.log(:error, :red, "#{file_to_parse} is not a csv")
|
22
|
+
CsvFileManager.delete_drive_file(options[:drive_file]) if options[:drive_file]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def export(file)
|
28
|
+
LOGGER.log(:info, :black, "********* PARSING #{file} *********")
|
29
|
+
LOGGER.log(:info, :black, "Extracting data from file...")
|
30
|
+
parser = CsvParser.new
|
31
|
+
data = parser.extract_data(file)
|
32
|
+
if data.empty?
|
33
|
+
LOGGER.log(:error, :red, "No data were found in the file - cannot start the file generation process")
|
34
|
+
else
|
35
|
+
export_platforms = options.dig(:only) || Constant::SUPPORTED_PLATFORMS
|
36
|
+
export_platforms.each do |platform|
|
37
|
+
platform_formatter = "AdLocalize::Platform::#{platform.to_s.camelize}Formatter".constantize.new(parser.locales.first, options.dig(:output_path))
|
38
|
+
parser.locales.each do |locale|
|
39
|
+
platform_formatter.export(locale, data)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ad_localize
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Edouard Siegel
|
8
|
+
- Patrick Nollet
|
9
|
+
- Adrien Couque
|
10
|
+
- Tarek Belkahia
|
11
|
+
- Hugo Hache
|
12
|
+
- Vincent Brison
|
13
|
+
- Joanna Vigné
|
14
|
+
- Nicolas Braun
|
15
|
+
- Corentin Huard
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
date: 2018-04-18 00:00:00.000000000 Z
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: bundler
|
23
|
+
requirement: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.16'
|
28
|
+
type: :development
|
29
|
+
prerelease: false
|
30
|
+
version_requirements: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1.16'
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rake
|
37
|
+
requirement: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '10.0'
|
42
|
+
type: :development
|
43
|
+
prerelease: false
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '10.0'
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: minitest
|
51
|
+
requirement: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '5.0'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '5.0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: byebug
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '10.0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '10.0'
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: json
|
79
|
+
requirement: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '1.8'
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 1.8.3
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - "~>"
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '1.8'
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.8.3
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: nokogiri
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.8'
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 1.8.2
|
107
|
+
type: :runtime
|
108
|
+
prerelease: false
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - "~>"
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '1.8'
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 1.8.2
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: activesupport
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '5.1'
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: 5.1.5
|
127
|
+
type: :runtime
|
128
|
+
prerelease: false
|
129
|
+
version_requirements: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - "~>"
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '5.1'
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: 5.1.5
|
137
|
+
- !ruby/object:Gem::Dependency
|
138
|
+
name: colorize
|
139
|
+
requirement: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - "~>"
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: 0.8.1
|
144
|
+
type: :runtime
|
145
|
+
prerelease: false
|
146
|
+
version_requirements: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - "~>"
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: 0.8.1
|
151
|
+
description: " Convert a wording file in localization files. Supported export formats
|
152
|
+
are : iOS, Android,\n JSON and YAML "
|
153
|
+
email:
|
154
|
+
- joanna.vigne@fabernovel.com
|
155
|
+
- hugo.hache@fabernovel.com
|
156
|
+
- edouard.siegel@fabernovel.com
|
157
|
+
executables:
|
158
|
+
- ad_localize
|
159
|
+
extensions: []
|
160
|
+
extra_rdoc_files: []
|
161
|
+
files:
|
162
|
+
- bin/ad_localize
|
163
|
+
- lib/ad_localize.rb
|
164
|
+
- lib/ad_localize/ad_logger.rb
|
165
|
+
- lib/ad_localize/constant.rb
|
166
|
+
- lib/ad_localize/csv_file_manager.rb
|
167
|
+
- lib/ad_localize/csv_parser.rb
|
168
|
+
- lib/ad_localize/option_handler.rb
|
169
|
+
- lib/ad_localize/platform/android_formatter.rb
|
170
|
+
- lib/ad_localize/platform/ios_formatter.rb
|
171
|
+
- lib/ad_localize/platform/json_formatter.rb
|
172
|
+
- lib/ad_localize/platform/platform_formatter.rb
|
173
|
+
- lib/ad_localize/platform/yml_formatter.rb
|
174
|
+
- lib/ad_localize/runner.rb
|
175
|
+
homepage: https://technologies.fabernovel.com
|
176
|
+
licenses:
|
177
|
+
- MIT
|
178
|
+
metadata: {}
|
179
|
+
post_install_message:
|
180
|
+
rdoc_options: []
|
181
|
+
require_paths:
|
182
|
+
- lib
|
183
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
189
|
+
requirements:
|
190
|
+
- - ">="
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '0'
|
193
|
+
requirements: []
|
194
|
+
rubyforge_project:
|
195
|
+
rubygems_version: 2.7.3
|
196
|
+
signing_key:
|
197
|
+
specification_version: 4
|
198
|
+
summary: AdLocalize
|
199
|
+
test_files: []
|