ad_localize 3.6.0 → 4.0.0

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -1
  3. data/Gemfile.lock +34 -11
  4. data/README.md +148 -145
  5. data/ad_localize.gemspec +2 -0
  6. data/bin/console +1 -0
  7. data/exe/ad_localize +1 -1
  8. data/lib/ad_localize.rb +52 -11
  9. data/lib/ad_localize/ad_logger.rb +22 -9
  10. data/lib/ad_localize/cli.rb +10 -0
  11. data/lib/ad_localize/constant.rb +2 -21
  12. data/lib/ad_localize/entities/key.rb +74 -0
  13. data/lib/ad_localize/entities/locale_wording.rb +60 -0
  14. data/lib/ad_localize/entities/translation.rb +20 -0
  15. data/lib/ad_localize/entities/wording.rb +24 -0
  16. data/lib/ad_localize/interactors/execute_export_request.rb +43 -0
  17. data/lib/ad_localize/interactors/export_csv_files.rb +17 -0
  18. data/lib/ad_localize/interactors/export_g_spreadsheet.rb +55 -0
  19. data/lib/ad_localize/interactors/export_wording.rb +27 -0
  20. data/lib/ad_localize/interactors/merge_wordings.rb +43 -0
  21. data/lib/ad_localize/interactors/platforms/export_android_locale_wording.rb +39 -0
  22. data/lib/ad_localize/interactors/platforms/export_ios_locale_wording.rb +62 -0
  23. data/lib/ad_localize/interactors/platforms/export_json_locale_wording.rb +23 -0
  24. data/lib/ad_localize/interactors/platforms/export_platform_factory.rb +44 -0
  25. data/lib/ad_localize/interactors/platforms/export_properties_locale_wording.rb +29 -0
  26. data/lib/ad_localize/interactors/platforms/export_yaml_locale_wording.rb +23 -0
  27. data/lib/ad_localize/mappers/android_translation_mapper.rb +18 -0
  28. data/lib/ad_localize/mappers/csv_path_to_wording.rb +76 -0
  29. data/lib/ad_localize/mappers/ios_translation_mapper.rb +12 -0
  30. data/lib/ad_localize/mappers/locale_wording_to_hash.rb +25 -0
  31. data/lib/ad_localize/mappers/options_to_export_request.rb +28 -0
  32. data/lib/ad_localize/mappers/translation_group_mapper.rb +14 -0
  33. data/lib/ad_localize/mappers/translation_mapper.rb +30 -0
  34. data/lib/ad_localize/mappers/value_range_to_wording.rb +69 -0
  35. data/lib/ad_localize/option_handler.rb +30 -57
  36. data/lib/ad_localize/repositories/file_system_repository.rb +13 -0
  37. data/lib/ad_localize/repositories/g_sheets_repository.rb +44 -0
  38. data/lib/ad_localize/requests/export_request.rb +77 -0
  39. data/lib/ad_localize/requests/g_spreadsheet_options.rb +47 -0
  40. data/lib/ad_localize/requests/merge_policy.rb +28 -0
  41. data/lib/ad_localize/serializers/info_plist_serializer.rb +27 -0
  42. data/lib/ad_localize/serializers/json_serializer.rb +13 -0
  43. data/lib/ad_localize/serializers/localizable_strings_serializer.rb +27 -0
  44. data/lib/ad_localize/serializers/localizable_stringsdict_serializer.rb +35 -0
  45. data/lib/ad_localize/serializers/properties_serializer.rb +25 -0
  46. data/lib/ad_localize/serializers/strings_serializer.rb +33 -0
  47. data/lib/ad_localize/serializers/with_template.rb +19 -0
  48. data/lib/ad_localize/serializers/yaml_serializer.rb +13 -0
  49. data/lib/ad_localize/templates/android/strings.xml.erb +19 -0
  50. data/lib/ad_localize/templates/ios/InfoPlist.strings.erb +3 -0
  51. data/lib/ad_localize/templates/ios/Localizable.strings.erb +3 -0
  52. data/lib/ad_localize/templates/ios/Localizable.stringsdict.erb +41 -0
  53. data/lib/ad_localize/templates/properties/template.properties.erb +3 -0
  54. data/lib/ad_localize/version.rb +1 -1
  55. data/lib/ad_localize/view_models/translation_group_view_model.rb +15 -0
  56. data/lib/ad_localize/view_models/translation_view_model.rb +19 -0
  57. metadata +74 -11
  58. data/Makefile +0 -11
  59. data/lib/ad_localize/csv_file_manager.rb +0 -64
  60. data/lib/ad_localize/csv_parser.rb +0 -165
  61. data/lib/ad_localize/platform/android_formatter.rb +0 -70
  62. data/lib/ad_localize/platform/ios_formatter.rb +0 -143
  63. data/lib/ad_localize/platform/json_formatter.rb +0 -17
  64. data/lib/ad_localize/platform/platform_formatter.rb +0 -109
  65. data/lib/ad_localize/platform/yml_formatter.rb +0 -19
  66. data/lib/ad_localize/runner.rb +0 -55
@@ -0,0 +1,27 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ class ExportWording
4
+ def initialize
5
+ @export_platform_factory = Platforms::ExportPlatformFactory.new
6
+ end
7
+
8
+ def call(export_request:, wording:)
9
+ LOGGER.debug("Starting export wording")
10
+ locales = export_request.locales.size.zero? ? wording.locales : wording.locales & export_request.locales
11
+ export_request.platforms.each do |platform|
12
+ locales.each do |locale|
13
+ platform_dir = compute_platform_dir(export_request: export_request, platform: platform)
14
+ export_platform = @export_platform_factory.build(platform: platform)
15
+ export_platform.call(wording: wording, locale: locale, platform_dir: platform_dir)
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def compute_platform_dir(export_request:, platform:)
23
+ export_request.multiple_platforms? ? export_request.output_path.join(platform.to_s) : export_request.output_path
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ class MergeWordings
4
+ REPLACE_MERGE_POLICY = 'replace'.freeze
5
+ KEEP_MERGE_POLICY = 'keep'.freeze
6
+ MERGE_POLICIES = [KEEP_MERGE_POLICY, REPLACE_MERGE_POLICY]
7
+
8
+ def self.valid_policy?(policy:)
9
+ MERGE_POLICIES.include?(policy)
10
+ end
11
+
12
+ def call(wordings:, merge_policy:)
13
+ if wordings.size == 1
14
+ wordings.first
15
+ elsif wordings.size > 1
16
+ LOGGER.debug("Merge wordings before processing")
17
+ merge_many(wordings: wordings, merge_policy: merge_policy)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def merge_many(wordings:, merge_policy:)
24
+ final_wording = wordings.first
25
+ wordings[1..-1].each do |wording|
26
+ wording.locale_wordings.each do |locale_wording|
27
+ locale_wording_reference = final_wording.translations_for(locale: locale_wording.locale)
28
+ locale_wording.translations.each do |translation|
29
+ translation_reference = locale_wording_reference.translation_for(key: translation.key)
30
+ if translation_reference.present?
31
+ next unless merge_policy.replace?
32
+ translation_reference.value = translation.value
33
+ else
34
+ locale_wording_reference.add_translation(translation: translation)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ final_wording
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ module Platforms
4
+ class ExportAndroidLocaleWording
5
+ STRINGS_FILENAME = 'strings.xml'.freeze
6
+ LOCALE_DIRECTORY_CONVENTION = "values%{locale_suffix}".freeze
7
+
8
+ def initialize
9
+ @strings_serializer = Serializers::StringsSerializer.new
10
+ @file_system_repository = Repositories::FileSystemRepository.new
11
+ end
12
+
13
+ def call(wording:, locale:, platform_dir:)
14
+ LOGGER.debug("Starting export Android wording for locale #{locale}")
15
+ locale_wording = wording.translations_for(locale: locale)
16
+ return unless has_android_wording?(locale_wording: locale_wording)
17
+
18
+ output_dir = compute_output_dir(wording: wording, locale: locale, platform_dir: platform_dir)
19
+ @file_system_repository.create_directory(path: output_dir)
20
+
21
+ content = @strings_serializer.render(locale_wording: locale_wording)
22
+ @file_system_repository.write(content: content, path: output_dir.join(STRINGS_FILENAME))
23
+ LOGGER.debug("#{STRINGS_FILENAME} done !")
24
+ end
25
+
26
+ private
27
+
28
+ def has_android_wording?(locale_wording:)
29
+ locale_wording.has_singular_keys? || locale_wording.has_plural_keys?
30
+ end
31
+
32
+ def compute_output_dir(wording:, locale:, platform_dir:)
33
+ locale_suffix = wording.default_locale?(locale: locale) ? '' : "-#{locale}"
34
+ platform_dir.join(LOCALE_DIRECTORY_CONVENTION % { locale_suffix: locale_suffix })
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,62 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ module Platforms
4
+ class ExportIOSLocaleWording
5
+ INFO_PLIST_FILENAME = "InfoPlist.strings".freeze
6
+ LOCALIZABLE_STRINGS_FILENAME = "Localizable.strings".freeze
7
+ LOCALIZABLE_STRINGSDICT_FILENAME = "Localizable.stringsdict".freeze
8
+ LOCALE_DIRECTORY_CONVENTION = "%{locale}.lproj".freeze
9
+
10
+ def initialize
11
+ @info_plist_serializer = Serializers::InfoPlistSerializer.new
12
+ @localizable_strings_serializer = Serializers::LocalizableStringsSerializer.new
13
+ @localizable_stringsdict_serializer = Serializers::LocalizableStringsdictSerializer.new
14
+ @file_system_repository = Repositories::FileSystemRepository.new
15
+ end
16
+
17
+ def call(wording:, locale:, platform_dir:)
18
+ LOGGER.debug("Starting export iOS wording for locale #{locale}")
19
+ locale_wording = wording.translations_for(locale: locale)
20
+ return unless has_ios_wording?(locale_wording: locale_wording)
21
+
22
+ output_dir = platform_dir.join(LOCALE_DIRECTORY_CONVENTION % { locale: locale })
23
+ @file_system_repository.create_directory(path: output_dir)
24
+
25
+ export_info_plist(locale_wording: locale_wording, output_dir: output_dir)
26
+ export_localizable_strings(locale_wording: locale_wording, output_dir: output_dir)
27
+ export_localizable_stringsdict(locale_wording: locale_wording, output_dir: output_dir)
28
+ end
29
+
30
+ private
31
+
32
+ def has_ios_wording?(locale_wording:)
33
+ locale_wording.has_info_plist_keys? ||
34
+ locale_wording.has_singular_keys? ||
35
+ locale_wording.has_plural_keys? ||
36
+ locale_wording.has_adaptive_keys?
37
+ end
38
+
39
+ def export_info_plist(locale_wording:, output_dir:)
40
+ return unless locale_wording.has_info_plist_keys?
41
+ content = @info_plist_serializer.render(locale_wording: locale_wording)
42
+ @file_system_repository.write(content: content, path: output_dir.join(INFO_PLIST_FILENAME))
43
+ LOGGER.debug("#{INFO_PLIST_FILENAME} done !")
44
+ end
45
+
46
+ def export_localizable_strings(locale_wording:, output_dir:)
47
+ return unless locale_wording.has_singular_keys?
48
+ content = @localizable_strings_serializer.render(locale_wording: locale_wording)
49
+ @file_system_repository.write(content: content, path: output_dir.join(LOCALIZABLE_STRINGS_FILENAME))
50
+ LOGGER.debug("#{LOCALIZABLE_STRINGS_FILENAME} done !")
51
+ end
52
+
53
+ def export_localizable_stringsdict(locale_wording:, output_dir:)
54
+ return unless locale_wording.has_plural_keys? || locale_wording.has_adaptive_keys?
55
+ content = @localizable_stringsdict_serializer.render(locale_wording: locale_wording)
56
+ @file_system_repository.write(content: content, path: output_dir.join(LOCALIZABLE_STRINGSDICT_FILENAME))
57
+ LOGGER.debug("#{LOCALIZABLE_STRINGSDICT_FILENAME} done !")
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ module Platforms
4
+ class ExportJSONLocaleWording
5
+ def initialize
6
+ @json_serializer = Serializers::JSONSerializer.new
7
+ @file_system_repository = Repositories::FileSystemRepository.new
8
+ end
9
+
10
+ def call(wording:, locale:, platform_dir:)
11
+ LOGGER.debug("Starting export JSON wording for locale #{locale}")
12
+ locale_wording = wording.translations_for(locale: locale)
13
+ content = @json_serializer.render(locale_wording: locale_wording)
14
+ return if content[locale].blank?
15
+
16
+ @file_system_repository.create_directory(path: platform_dir)
17
+ @file_system_repository.write(content: content, path: platform_dir.join("#{locale}.json"))
18
+ LOGGER.debug("#{locale}.json done !")
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ module Platforms
4
+ class ExportPlatformFactory
5
+ def build(platform:)
6
+ case platform
7
+ when 'json'
8
+ json_builder
9
+ when 'yml'
10
+ yaml_builder
11
+ when 'android'
12
+ android_builder
13
+ when 'ios'
14
+ ios_builder
15
+ when 'properties'
16
+ properties_builder
17
+ else
18
+ raise ArgumentError.new('Unknown platform for builder factory')
19
+ end
20
+ end
21
+
22
+ def json_builder
23
+ @json_builder ||= ExportJSONLocaleWording.new
24
+ end
25
+
26
+ def yaml_builder
27
+ @yaml_builder ||= ExportYAMLLocaleWording.new
28
+ end
29
+
30
+ def android_builder
31
+ @android_builder ||= ExportAndroidLocaleWording.new
32
+ end
33
+
34
+ def ios_builder
35
+ @ios_builder ||= ExportIOSLocaleWording.new
36
+ end
37
+
38
+ def properties_builder
39
+ @properties_builder ||= ExportPropertiesLocaleWording.new
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ module Platforms
4
+ class ExportPropertiesLocaleWording
5
+ def initialize
6
+ @properties_serializer = Serializers::PropertiesSerializer.new
7
+ @file_system_repository = Repositories::FileSystemRepository.new
8
+ end
9
+
10
+ def call(wording:, locale:, platform_dir:)
11
+ LOGGER.debug("Starting export Properties wording for locale #{locale}")
12
+ locale_wording = wording.translations_for(locale: locale)
13
+ return unless has_properties_wording?(locale_wording: locale_wording)
14
+
15
+ content = @properties_serializer.render(locale_wording: locale_wording)
16
+ @file_system_repository.create_directory(path: platform_dir)
17
+ @file_system_repository.write(content: content, path: platform_dir.join("#{locale}.properties"))
18
+ LOGGER.debug("#{locale}.properties done !")
19
+ end
20
+
21
+ private
22
+
23
+ def has_properties_wording?(locale_wording:)
24
+ locale_wording.has_singular_keys?
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ module Platforms
4
+ class ExportYAMLLocaleWording
5
+ def initialize
6
+ @yaml_serializer = Serializers::YAMLSerializer.new
7
+ @file_system_repository = Repositories::FileSystemRepository.new
8
+ end
9
+
10
+ def call(wording:, locale:, platform_dir:)
11
+ LOGGER.debug("Starting export YAML wording for locale #{locale}")
12
+ locale_wording = wording.translations_for(locale: locale)
13
+ content = @yaml_serializer.render(locale_wording:locale_wording)
14
+ return if content[locale].blank?
15
+
16
+ @file_system_repository.create_directory(path: platform_dir)
17
+ @file_system_repository.write(content: content, path: platform_dir.join("#{locale}.yml"))
18
+ LOGGER.debug("#{locale}.yml done !")
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ module AdLocalize
2
+ module Mappers
3
+ class AndroidTranslationMapper < TranslationMapper
4
+ private
5
+
6
+ def sanitize_value(value:)
7
+ return if value.blank?
8
+ processedValue = value.gsub(/(?<!\\)'/, "\\\\'") # match ' unless there is a \ before
9
+ processedValue = processedValue.gsub(/(?<!\\)\"/, "\\\"") # match " unless there is a \ before
10
+ processedValue = processedValue.gsub(/(%(\d+\$)?@)/, '%\2s') # should match values like %1$s and %s
11
+ processedValue = processedValue.gsub(/(%((\d+\$)?(\d+)?)i)/, '%\2d') # should match values like %i, %3$i, %01i, %1$02i
12
+ processedValue = processedValue.gsub(/%(?!((\d+\$)?(s|(\d+)?d)))/, '%%') # negative lookahead: identifies when user really wants to display a %
13
+ processedValue = processedValue.gsub("\\U", "\\u")
14
+ "\"#{processedValue}\""
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,76 @@
1
+ module AdLocalize
2
+ module Mappers
3
+ class CSVPathToWording
4
+ def map(csv_path:)
5
+ @headers = CSV.foreach(csv_path).first
6
+ return unless valid?(csv_path: csv_path)
7
+ translations = []
8
+
9
+ CSV.foreach(csv_path, headers: true, skip_blanks: true) do |row|
10
+ row_translations = map_row(row: row, locales: locales)
11
+ next if row_translations.blank?
12
+
13
+ existing_keys = translations.map(&:key)
14
+ new_translations = row_translations.reject do |translation|
15
+ existing_keys.any? do |key|
16
+ existing_plural_key = key.label == translation.key.label && key.plural? && translation.key.singular?
17
+ key.same_as?(key: translation.key) || existing_plural_key
18
+ end
19
+ end
20
+ translations.concat(new_translations)
21
+ end
22
+
23
+ locale_wordings = translations.group_by(&:locale).map do |locale, group|
24
+ Entities::LocaleWording.new(locale: locale, translations: group)
25
+ end
26
+ Entities::Wording.new(locale_wordings: locale_wordings, default_locale: locales.first)
27
+ end
28
+
29
+ private
30
+
31
+ def valid?(csv_path:)
32
+ File.exist?(csv_path) && has_key_column? && has_locales?
33
+ end
34
+
35
+ def locales
36
+ @locales ||= compute_locales
37
+ end
38
+
39
+ def compute_locales
40
+ return [] unless has_key_column? && key_column_index < @headers.size.pred
41
+
42
+ @headers.slice(key_column_index.succ..-1).compact.reject do |header|
43
+ header.to_s.include?(Constant::COMMENT_KEY_COLUMN_IDENTIFIER)
44
+ end
45
+ end
46
+
47
+ def has_locales?
48
+ locales.present?
49
+ end
50
+
51
+ def has_key_column?
52
+ key_column_index.present?
53
+ end
54
+
55
+ def key_column_index
56
+ @key_column_index ||= @headers.index(Constant::CSV_WORDING_KEYS_COLUMN)
57
+ end
58
+
59
+ def map_row(row:, locales:)
60
+ csv_wording_key = row.field(Constant::CSV_WORDING_KEYS_COLUMN)
61
+ return if csv_wording_key.blank?
62
+
63
+ key = Entities::Key.new(label: csv_wording_key)
64
+ locales.map do |locale|
65
+ comment_column_name = "#{Constant::COMMENT_KEY_COLUMN_IDENTIFIER} #{locale}"
66
+ Entities::Translation.new(
67
+ locale: locale,
68
+ key: key,
69
+ value: row.field(locale),
70
+ comment: row.field(comment_column_name)
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,12 @@
1
+ module AdLocalize
2
+ module Mappers
3
+ class IOSTranslationMapper < TranslationMapper
4
+ private
5
+
6
+ def sanitize_value(value:)
7
+ return if value.blank?
8
+ value.gsub(/(?<!\\)\"/, "\\\"")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ module AdLocalize
2
+ module Mappers
3
+ class LocaleWordingToHash
4
+ def map(locale_wording:)
5
+ result = locale_wording.translations.each_with_object({}) do |translation, hash|
6
+ inner_keys = translation.key.label.split('.')
7
+ inner_keys.each_with_index do |inner_key, index|
8
+ if index === inner_keys.count - 1
9
+ if translation.key.plural?
10
+ hash[translation.key.label] = {} unless hash.key? translation.key.label
11
+ hash[translation.key.label][translation.key.plural_key] = translation.value
12
+ else
13
+ hash[inner_key.to_s] = translation.value
14
+ end
15
+ else
16
+ hash[inner_key] = {} if hash[inner_key].nil?
17
+ hash = hash[inner_key]
18
+ end
19
+ end
20
+ end
21
+ { locale_wording.locale => result }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ module AdLocalize
2
+ module Mappers
3
+ class OptionsToExportRequest
4
+ def map(options:)
5
+ Requests::ExportRequest.new(
6
+ platform_codes: options[:only],
7
+ g_spreadsheet_options: map_g_spreadsheet_options(options: options),
8
+ verbose: options[:debug],
9
+ output_path: options[:'target-dir'],
10
+ merge_policy: options[:'merge-policy'],
11
+ csv_paths: options[:csv_paths]
12
+ )
13
+ end
14
+
15
+ private
16
+
17
+ def map_g_spreadsheet_options(options:)
18
+ return unless options[:drive_key]
19
+ Requests::GSpreadsheetOptions.new(
20
+ spreadsheet_id: options[:'drive-key'],
21
+ sheet_ids: options[:'sheets'],
22
+ export_all: options[:'export-all-sheets'],
23
+ service_account_config: ENV['GCLOUD_CLIENT_SECRET']
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end