ad_localize 5.0.0 → 6.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +6 -3
  3. data/.rubocop.yml +5 -0
  4. data/.rubocop_todo.yml +319 -0
  5. data/CHANGELOG.md +24 -0
  6. data/Gemfile +1 -0
  7. data/Gemfile.lock +54 -48
  8. data/README.md +32 -10
  9. data/Rakefile +1 -0
  10. data/ad_localize.gemspec +10 -9
  11. data/bin/console +1 -0
  12. data/exe/ad_localize +2 -1
  13. data/lib/ad_localize/ad_logger.rb +5 -10
  14. data/lib/ad_localize/cli.rb +10 -3
  15. data/lib/ad_localize/entities/key.rb +3 -76
  16. data/lib/ad_localize/entities/locale_wording.rb +32 -52
  17. data/lib/ad_localize/entities/platform.rb +13 -0
  18. data/lib/ad_localize/entities/simple_wording.rb +6 -0
  19. data/lib/ad_localize/entities/wording_type.rb +11 -0
  20. data/lib/ad_localize/interactors/base_generate_files.rb +37 -0
  21. data/lib/ad_localize/interactors/download_spreadsheets.rb +20 -0
  22. data/lib/ad_localize/interactors/export_wording.rb +26 -20
  23. data/lib/ad_localize/interactors/generate_info_plist.rb +24 -0
  24. data/lib/ad_localize/interactors/generate_ios_files.rb +12 -0
  25. data/lib/ad_localize/interactors/generate_json.rb +20 -0
  26. data/lib/ad_localize/interactors/generate_localizable_strings.rb +24 -0
  27. data/lib/ad_localize/interactors/generate_localizable_strings_dict.rb +24 -0
  28. data/lib/ad_localize/interactors/generate_properties.rb +20 -0
  29. data/lib/ad_localize/interactors/generate_strings.rb +25 -0
  30. data/lib/ad_localize/interactors/generate_yaml.rb +20 -0
  31. data/lib/ad_localize/interactors/merge_wordings.rb +49 -18
  32. data/lib/ad_localize/interactors/parse_csv_files.rb +22 -0
  33. data/lib/ad_localize/interactors/process_export_request.rb +21 -0
  34. data/lib/ad_localize/mappers/locale_wording_to_hash.rb +45 -24
  35. data/lib/ad_localize/mappers/options_to_export_request.rb +14 -22
  36. data/lib/ad_localize/option_handler.rb +50 -27
  37. data/lib/ad_localize/parsers/csv_parser.rb +84 -0
  38. data/lib/ad_localize/parsers/key_parser.rb +62 -0
  39. data/lib/ad_localize/repositories/drive_repository.rb +53 -0
  40. data/lib/ad_localize/repositories/file_system_repository.rb +2 -1
  41. data/lib/ad_localize/requests/export_request.rb +95 -57
  42. data/lib/ad_localize/sanitizers/ios_sanitizer.rb +12 -0
  43. data/lib/ad_localize/{mappers/android_translation_mapper.rb → sanitizers/ios_to_android_sanitizer.rb} +8 -8
  44. data/lib/ad_localize/sanitizers/pass_through_sanitizer.rb +10 -0
  45. data/lib/ad_localize/serializers/info_plist_serializer.rb +9 -11
  46. data/lib/ad_localize/serializers/json_serializer.rb +3 -5
  47. data/lib/ad_localize/serializers/localizable_strings_serializer.rb +9 -15
  48. data/lib/ad_localize/serializers/localizable_stringsdict_serializer.rb +12 -25
  49. data/lib/ad_localize/serializers/properties_serializer.rb +9 -11
  50. data/lib/ad_localize/serializers/strings_serializer.rb +12 -21
  51. data/lib/ad_localize/serializers/templated_serializer.rb +51 -0
  52. data/lib/ad_localize/serializers/yaml_serializer.rb +4 -6
  53. data/lib/ad_localize/templates/android/strings.xml.erb +6 -6
  54. data/lib/ad_localize/templates/ios/Localizable.stringsdict.erb +14 -14
  55. data/lib/ad_localize/version.rb +2 -1
  56. data/lib/ad_localize/view_models/compound_wording_view_model.rb +2 -0
  57. data/lib/ad_localize/view_models/simple_wording_view_model.rb +2 -0
  58. data/lib/ad_localize.rb +33 -35
  59. metadata +70 -47
  60. data/lib/ad_localize/constant.rb +0 -6
  61. data/lib/ad_localize/entities/translation.rb +0 -32
  62. data/lib/ad_localize/entities/wording.rb +0 -24
  63. data/lib/ad_localize/interactors/execute_export_request.rb +0 -45
  64. data/lib/ad_localize/interactors/export_csv_files.rb +0 -20
  65. data/lib/ad_localize/interactors/export_g_spreadsheet.rb +0 -64
  66. data/lib/ad_localize/interactors/platforms/export_android_locale_wording.rb +0 -44
  67. data/lib/ad_localize/interactors/platforms/export_csv_locale_wording.rb +0 -22
  68. data/lib/ad_localize/interactors/platforms/export_ios_locale_wording.rb +0 -75
  69. data/lib/ad_localize/interactors/platforms/export_json_locale_wording.rb +0 -27
  70. data/lib/ad_localize/interactors/platforms/export_platform_factory.rb +0 -50
  71. data/lib/ad_localize/interactors/platforms/export_properties_locale_wording.rb +0 -32
  72. data/lib/ad_localize/interactors/platforms/export_yaml_locale_wording.rb +0 -27
  73. data/lib/ad_localize/mappers/csv_path_to_wording.rb +0 -73
  74. data/lib/ad_localize/mappers/ios_translation_mapper.rb +0 -12
  75. data/lib/ad_localize/mappers/translation_group_mapper.rb +0 -14
  76. data/lib/ad_localize/mappers/translation_mapper.rb +0 -30
  77. data/lib/ad_localize/mappers/value_range_to_wording.rb +0 -67
  78. data/lib/ad_localize/repositories/g_sheets_repository.rb +0 -44
  79. data/lib/ad_localize/requests/export_wording_options.rb +0 -24
  80. data/lib/ad_localize/requests/g_spreadsheet_options.rb +0 -47
  81. data/lib/ad_localize/requests/merge_policy.rb +0 -28
  82. data/lib/ad_localize/serializers/with_template.rb +0 -19
  83. data/lib/ad_localize/validators/key_validator.rb +0 -31
  84. data/lib/ad_localize/view_models/translation_group_view_model.rb +0 -19
  85. data/lib/ad_localize/view_models/translation_view_model.rb +0 -23
@@ -1,13 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Interactors
3
4
  class MergeWordings
4
5
  REPLACE_MERGE_POLICY = 'replace'.freeze
5
6
  KEEP_MERGE_POLICY = 'keep'.freeze
6
7
  MERGE_POLICIES = [KEEP_MERGE_POLICY, REPLACE_MERGE_POLICY]
7
-
8
- def self.valid_policy?(policy:)
9
- MERGE_POLICIES.include?(policy)
10
- end
8
+ DEFAULT_POLICY = KEEP_MERGE_POLICY
11
9
 
12
10
  def call(wordings:, merge_policy:)
13
11
  if wordings.size == 1
@@ -20,24 +18,57 @@ module AdLocalize
20
18
 
21
19
  private
22
20
 
21
+ def merge_simple_wordings(reference_list:, new_list:, merge_policy:)
22
+ new_list.each do |label, new_translation|
23
+ translation_reference = reference_list[label]
24
+ if translation_reference.nil?
25
+ reference_list[label] = new_translation
26
+ elsif merge_policy == REPLACE_MERGE_POLICY
27
+ LOGGER.warn("[MERGE] #{label} value changed from #{translation_reference.value} to #{new_translation.value}")
28
+ reference_list[label].value = new_translation.value
29
+ end
30
+ end
31
+ end
32
+
33
+ def merge_compound_wordings(reference_hash:, new_hash:, merge_policy:)
34
+ new_hash.each do |new_label, new_list|
35
+ if reference_hash[new_label].nil?
36
+ reference_hash[new_label] = new_list
37
+ elsif merge_policy == REPLACE_MERGE_POLICY
38
+ merge_simple_wordings(reference_list: reference_hash[new_label], new_list: new_list, merge_policy: merge_policy)
39
+ end
40
+ end
41
+ end
42
+
23
43
  def merge_many(wordings:, merge_policy:)
24
- final_wording = wordings.first
44
+ wording_reference = wordings.first
25
45
  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
46
+ wording.each do |locale, locale_wording|
47
+ reference = wording_reference[locale]
48
+ merge_simple_wordings(
49
+ reference_list: reference.singulars,
50
+ new_list: locale_wording.singulars,
51
+ merge_policy: merge_policy
52
+ )
53
+ merge_simple_wordings(
54
+ reference_list: reference.info_plists,
55
+ new_list: locale_wording.info_plists,
56
+ merge_policy: merge_policy
57
+ )
58
+ merge_compound_wordings(
59
+ reference_hash: reference.plurals,
60
+ new_hash: locale_wording.plurals,
61
+ merge_policy: merge_policy
62
+ )
63
+ merge_compound_wordings(
64
+ reference_hash: reference.adaptives,
65
+ new_hash: locale_wording.adaptives,
66
+ merge_policy: merge_policy
67
+ )
37
68
  end
38
69
  end
39
- final_wording
70
+ wording_reference
40
71
  end
41
72
  end
42
73
  end
43
- end
74
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ module AdLocalize
3
+ module Interactors
4
+ class ParseCSVFiles
5
+ def initialize
6
+ @csv_parser = Parsers::CSVParser.new
7
+ end
8
+
9
+ def call(export_request:)
10
+ csv_paths = export_request.all_csv_paths
11
+ LOGGER.debug("Will parse #{csv_paths.size} csv files")
12
+ wordings = csv_paths.filter_map do |csv_path|
13
+ @csv_parser.call(csv_path: csv_path, export_request: export_request)
14
+ end
15
+ LOGGER.debug("#{wordings.size} wording contents detected")
16
+ return if wordings.blank?
17
+
18
+ MergeWordings.new.call(wordings: wordings, merge_policy: export_request.merge_policy)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ module AdLocalize
3
+ module Interactors
4
+ class ProcessExportRequest
5
+ def call(export_request:)
6
+ export_request.verbose ? LOGGER.debug! : LOGGER.info!
7
+ LOGGER.debug(export_request)
8
+ LOGGER.debug("Verify if there are CSV to process")
9
+ return unless export_request.has_csv_paths?
10
+
11
+ LOGGER.debug("Parse CSV files")
12
+ wording = ParseCSVFiles.new.call(export_request: export_request)
13
+ return unless wording
14
+
15
+ LOGGER.debug("Export wording")
16
+ ExportWording.new.call(export_request: export_request, wording: wording)
17
+ LOGGER.debug("End of export request execution")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,33 +1,54 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Mappers
3
4
  class LocaleWordingToHash
4
5
  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
- unless hash.is_a?(Hash)
14
- LOGGER.warn "Corrupted input. Trying to insert a value for key '#{translation.key.label}' but a value already exists for '#{inner_keys[0..index-1].join(".")}'. Skipping."
15
- break # skip this corrupted key
16
- end
17
- previous_value = hash[inner_key.to_s]
18
- if !previous_value.blank? && previous_value.is_a?(Hash)
19
- LOGGER.warn "Corrupted input. Trying to insert a value for key '#{translation.key.label}' but values already exist for keys '#{translation.key.label}.*'. Previous values will be lost."
20
- end
21
- hash[inner_key.to_s] = translation.value
22
- end
23
- else
24
- hash[inner_key] = {} if hash[inner_key].nil?
25
- hash = hash[inner_key]
26
- end
6
+ singulars_hash = map_singulars(simple_wordings: locale_wording.singulars.values)
7
+ plural_hash = map_plurals(coumpound_wordings: locale_wording.plurals)
8
+ locale_hash = singulars_hash.merge(plural_hash)
9
+ { locale_wording.locale => locale_hash }
10
+ end
11
+
12
+ private
13
+
14
+ def inner_keys(label:)
15
+ label.split('.')
16
+ end
17
+
18
+ def map_singulars(simple_wordings:)
19
+ map_translations(translations: simple_wordings) do |inner_keys, translation|
20
+ dotted_key_to_hash(inner_keys, { inner_keys.pop => translation.value })
21
+ end
22
+ end
23
+
24
+ def map_plurals(coumpound_wordings:)
25
+ result = {}
26
+ coumpound_wordings.each do |label, simple_wordings|
27
+ variants_hash = map_translations(translations: simple_wordings.values) do |keys, translation|
28
+ dotted_key_to_hash(keys, { translation.key.variant_name => translation.value })
27
29
  end
30
+ result.deep_merge!(variants_hash)
31
+ end
32
+ result
33
+ end
34
+
35
+ def map_translations(translations:)
36
+ translations.each_with_object({}) do |translation, hash|
37
+ keys = inner_keys(label: translation.key.label)
38
+ inner_keys_hash = yield(keys, translation)
39
+ hash.deep_merge!(inner_keys_hash)
40
+ end
41
+ end
42
+
43
+ def dotted_key_to_hash(array, h)
44
+ if array.size.zero?
45
+ h
46
+ elsif array.size == 1
47
+ { array.first => h }
48
+ elsif array.size > 1
49
+ { array.first => dotted_key_to_hash(array[1..-1], h) }
28
50
  end
29
- { locale_wording.locale => result }
30
51
  end
31
52
  end
32
53
  end
33
- end
54
+ end
@@ -1,29 +1,21 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Mappers
3
4
  class OptionsToExportRequest
4
5
  def map(options:)
5
- Requests::ExportRequest.new(
6
- platforms: options[:only],
7
- g_spreadsheet_options: map_g_spreadsheet_options(options: options),
8
- verbose: options[:debug],
9
- non_empty_values: options[:'non-empty-values'],
10
- output_path: options[:'target-dir'],
11
- merge_policy: options[:'merge-policy'],
12
- csv_paths: options[:csv_paths]
13
- )
14
- end
15
-
16
- private
17
-
18
- def map_g_spreadsheet_options(options:)
19
- return unless options[:'drive-key']
20
- Requests::GSpreadsheetOptions.new(
21
- spreadsheet_id: options[:'drive-key'],
22
- sheet_ids: options[:'sheets'],
23
- export_all: options[:'export-all-sheets'],
24
- service_account_config: ENV['GCLOUD_CLIENT_SECRET']
25
- )
6
+ request = Requests::ExportRequest.new
7
+ request.locales = options[:locales]
8
+ request.bypass_empty_values = options[:'non-empty-values']
9
+ request.csv_paths = options[:csv_paths]
10
+ request.merge_policy = options[:'merge-policy']
11
+ request.output_path = options[:'target-dir']
12
+ request.platforms = options[:only]
13
+ request.spreadsheet_id = options[:'drive-key']
14
+ request.sheet_ids = options[:sheets]
15
+ request.export_all = options[:'export-all-sheets']
16
+ request.verbose = options[:debug]
17
+ request
26
18
  end
27
19
  end
28
20
  end
29
- end
21
+ end
@@ -1,39 +1,62 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  class OptionHandler
4
+ DEFAULT_OPTIONS = {
5
+ locales: Requests::ExportRequest::DEFAULTS[:locales],
6
+ :'non-empty-values' => Requests::ExportRequest::DEFAULTS[:bypass_empty_values],
7
+ csv_paths: Requests::ExportRequest::DEFAULTS[:csv_paths],
8
+ :'merge-policy' => Requests::ExportRequest::DEFAULTS[:merge_policy],
9
+ :'target-dir' => Requests::ExportRequest::DEFAULTS[:output_path],
10
+ :'drive-key' => Requests::ExportRequest::DEFAULTS[:spreadsheet_id],
11
+ sheets: Requests::ExportRequest::DEFAULTS[:sheet_ids],
12
+ :'export-all-sheets' => Requests::ExportRequest::DEFAULTS[:export_all],
13
+ debug: Requests::ExportRequest::DEFAULTS[:verbose],
14
+ only: Requests::ExportRequest::DEFAULTS[:platforms]
15
+ }
16
+
3
17
  def self.parse!(options)
4
- option_parser = OptionParser.new do |parser|
5
- parser.banner = "Usage: exe/ad_localize [options] file(s)"
18
+ args = DEFAULT_OPTIONS
19
+ export_all_option = <<~DOC
20
+ Export all sheets from spreadsheet specified by --drive-key option.
21
+ \tBy default, generates one export directory per sheet (see -m|--merge-sheets option to merge them).
22
+ \tAn GCLOUD_CLIENT_SECRET environment variable containing the client_secret.json content is needed.
23
+ DOC
24
+ merge_policy_option = <<~DOC
25
+ Merge specified csv (or sheets from --export-all) instead of exporting each csv.
26
+ \treplace: if a key is already defined, replace its value.
27
+ \tkeep: if a key is already defined, keep the previous value.
28
+ DOC
29
+ platforms_option = <<~DOC
30
+ PLATFORMS is a comma separated list.
31
+ \tOnly generate localisation files for the specified platforms.
32
+ \tSupported platforms : #{Entities::Platform::SUPPORTED_PLATFORMS.to_sentence}
33
+ DOC
6
34
 
7
- parser.on("-d", "--debug", TrueClass, "Run in debug mode")
8
- parser.on("-e", "--export-all-sheets", TrueClass,
9
- <<~DOC
10
- Export all sheets from spreadsheet specified by --drive-key option.
11
- \tBy default, generates one export directory per sheet (see -m|--merge-sheets option to merge them).
12
- \tAn GCLOUD_CLIENT_SECRET environment variable containing the client_secret.json content is needed.
13
- DOC
14
- )
15
- parser.on("-h", "--help", "Prints help") do
35
+ OptionParser.new do |parser|
36
+ parser.banner = 'Usage: exe/ad_localize [options] file(s)'
37
+ parser.on("-d", "--debug", TrueClass, 'Run in debug mode')
38
+ parser.on("-e", "--export-all-sheets", TrueClass, export_all_option)
39
+ parser.on("-h", "--help", 'Prints help') do
16
40
  puts parser
17
41
  exit
18
42
  end
19
- parser.on("-k", "--drive-key SPREADSHEET_ID", String, "Use google drive spreadsheets")
20
- parser.on("-m", "--merge-policy POLICY", String,
21
- <<~DOC
22
- Merge specified csv (or sheets from --export-all) instead of exporting each csv.
23
- \treplace: if a key is already defined, replace its value.
24
- \tkeep: if a key is already defined, keep the previous value.
25
- DOC
26
- )
27
- parser.on("-o", "--only PLATFORMS", Array, "PLATFORMS is a comma separated list. Only generate localisation files for the specified platforms. Supported platforms : #{Requests::ExportRequest::SUPPORTED_PLATFORMS.to_sentence}")
28
- parser.on("-s", "--sheets SHEET_IDS", Array, "SHEET_IDS is a comma separated list. Use a specific sheet id for Google Drive spreadsheets with several sheets")
29
- parser.on("-t", "--target-dir PATH", String, "Path to the target directory")
30
- parser.on("-x", "--non-empty-values", TrueClass, "Do not export keys with empty values (iOS only)")
31
- end
43
+ parser.on("-k", "--drive-key SPREADSHEET_ID", String, 'Use google drive spreadsheets')
44
+ parser.on("-l", "--locales LOCALES", Array,
45
+ 'LOCALES is a comma separated list. Only generate localisation files for the specified locales')
46
+ parser.on("-m", "--merge-policy POLICY", Interactors::MergeWordings::MERGE_POLICIES, merge_policy_option)
47
+ parser.on("-o", "--only PLATFORMS", Array, platforms_option)
48
+ parser.on("-s", "--sheets SHEET_IDS", Array,
49
+ 'SHEET_IDS is a comma separated list. Use a specific sheet id for Google Drive spreadsheets with several sheets')
50
+ parser.on("-t", "--target-dir PATH", String, 'Path to the target directory')
51
+ parser.on("-v", "--version", 'Prints current version') do
52
+ puts AdLocalize::VERSION
53
+ exit
54
+ end
55
+ parser.on("-x", "--non-empty-values", TrueClass, 'Do not export keys with empty values (iOS only)')
56
+ end.parse!(options, into: args)
32
57
 
33
- args = {}
34
- option_parser.parse!(options, into: args)
35
58
  args[:csv_paths] = options
36
59
  return args
37
60
  end
38
61
  end
39
- end
62
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+ module AdLocalize
3
+ module Parsers
4
+ class CSVParser
5
+ COMMENT_KEY_COLUMN_IDENTIFIER = 'comment'.freeze
6
+ CSV_WORDING_KEYS_COLUMN = 'key'.freeze
7
+
8
+ def initialize(key_parser: nil)
9
+ @key_parser = key_parser.presence || KeyParser.new
10
+ end
11
+
12
+ def call(csv_path:, export_request:)
13
+ locales = find_locales(csv_path: csv_path, export_request: export_request)
14
+ LOGGER.debug("#{csv_path} - locales : #{locales.to_sentence}")
15
+ return if locales.blank?
16
+
17
+ keys = find_keys(csv_path: csv_path)
18
+ wording = build_wording(
19
+ csv_path: csv_path,
20
+ locales: locales,
21
+ keys: keys,
22
+ export_request: export_request
23
+ )
24
+ wording
25
+ end
26
+
27
+ private
28
+
29
+ def find_locales(csv_path:, export_request:)
30
+ csv = CSV.open(csv_path, headers: true, skip_blanks: true)
31
+ headers = csv.first.headers
32
+ locales = headers[headers.index(CSV_WORDING_KEYS_COLUMN).succ..-1].compact.reject do |header|
33
+ header.include?(COMMENT_KEY_COLUMN_IDENTIFIER)
34
+ end
35
+ export_request.locales.empty? ? locales : locales & export_request.locales
36
+ end
37
+
38
+ def find_keys(csv_path:)
39
+ keys = {}
40
+ CSV.foreach(csv_path, headers: true, skip_blanks: true, skip_lines: /^#/) do |row|
41
+ next if row[CSV_WORDING_KEYS_COLUMN].blank?
42
+
43
+ raw_key = row[CSV_WORDING_KEYS_COLUMN].strip
44
+ key = @key_parser.call(raw_key: raw_key)
45
+
46
+ existing_key = keys.values.detect do |k|
47
+ k.id == key.id || (k.label == key.label && (k.variant_name.nil? || key.variant_name.nil?))
48
+ end
49
+ if existing_key
50
+ LOGGER.warn "A #{existing_key.type} value already exist for key '#{existing_key.label}'. Will skip new #{key.type} value. Remove duplicates."
51
+ else
52
+ keys[raw_key] = key
53
+ end
54
+ end
55
+ keys
56
+ end
57
+
58
+ def build_wording(csv_path:, locales:, keys:, export_request:)
59
+ default_locale = locales.first
60
+ wording = Hash.new { |hash, key|
61
+ hash[key] = Entities::LocaleWording.new(locale: key, is_default: key == default_locale)
62
+ }
63
+ added_keys = Hash.new { |hash, key| hash[key] = false }
64
+ CSV.foreach(csv_path, headers: true, skip_blanks: true, skip_lines: /^#/) do |row|
65
+ next if row[CSV_WORDING_KEYS_COLUMN].blank?
66
+
67
+ raw_key = row[CSV_WORDING_KEYS_COLUMN].strip
68
+ key = keys[raw_key]
69
+ next if key.nil? || added_keys[raw_key]
70
+
71
+ locales.each do |locale|
72
+ value = row[locale]
73
+ next if export_request.bypass_empty_values && value.blank?
74
+
75
+ comment = row["#{COMMENT_KEY_COLUMN_IDENTIFIER} #{locale}"]
76
+ wording[locale].add_wording(key: key, value: value&.strip, comment: comment)
77
+ end
78
+ added_keys[raw_key] = true
79
+ end
80
+ wording
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ module AdLocalize
3
+ module Parsers
4
+ class KeyParser
5
+ PLURAL_KEY_REGEXP = /\#\#\{([A-Za-z]+)\}/.freeze
6
+ ADAPTIVE_KEY_REGEXP = /\#\#\{(\d+)\}/.freeze
7
+ # see https://developer.apple.com/documentation/bundleresources/information_property_list
8
+ INFO_PLIST_KEY_REGEXP = /(NS.+UsageDescription)|(CF.+Name)|NFCReaderUsageDescription/.freeze
9
+
10
+ def call(raw_key:)
11
+ type = compute_type(raw_key: raw_key)
12
+ label = compute_label(raw_key: raw_key, type: type)
13
+ variant_name = compute_variant_name(raw_key: raw_key, type: type)
14
+ Entities::Key.new(id: raw_key, label: label, type: type, variant_name: variant_name)
15
+ end
16
+
17
+ def plural?(raw_key:)
18
+ !raw_key.match(PLURAL_KEY_REGEXP).nil?
19
+ end
20
+
21
+ def adaptive?(raw_key:)
22
+ !raw_key.match(ADAPTIVE_KEY_REGEXP).nil?
23
+ end
24
+
25
+ def info_plist?(raw_key:)
26
+ !raw_key.match(INFO_PLIST_KEY_REGEXP).nil?
27
+ end
28
+
29
+ def compute_type(raw_key:)
30
+ if plural?(raw_key: raw_key)
31
+ Entities::WordingType::PLURAL
32
+ elsif adaptive?(raw_key: raw_key)
33
+ Entities::WordingType::ADAPTIVE
34
+ elsif info_plist?(raw_key: raw_key)
35
+ Entities::WordingType::INFO_PLIST
36
+ else
37
+ Entities::WordingType::SINGULAR
38
+ end
39
+ end
40
+
41
+ def compute_label(raw_key:, type:)
42
+ case type
43
+ when Entities::WordingType::PLURAL
44
+ raw_key.gsub(PLURAL_KEY_REGEXP, '')
45
+ when Entities::WordingType::ADAPTIVE
46
+ raw_key.gsub(ADAPTIVE_KEY_REGEXP, '')
47
+ else
48
+ raw_key
49
+ end
50
+ end
51
+
52
+ def compute_variant_name(raw_key:, type:)
53
+ case type
54
+ when Entities::WordingType::PLURAL
55
+ raw_key.match(PLURAL_KEY_REGEXP)&.captures&.first
56
+ when Entities::WordingType::ADAPTIVE
57
+ raw_key.match(ADAPTIVE_KEY_REGEXP)&.captures&.first
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ module AdLocalize
3
+ module Repositories
4
+ class DriveRepository
5
+ def initialize
6
+ @drive_service = Google::Apis::DriveV3::DriveService.new
7
+ @sheet_service = Google::Apis::SheetsV4::SheetsService.new
8
+ if ENV['GOOGLE_APPLICATION_CREDENTIALS']
9
+ drive_scope = [Google::Apis::DriveV3::AUTH_DRIVE_READONLY]
10
+ sheet_scope = [Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY]
11
+ @drive_service.authorization = Google::Auth.get_application_default(drive_scope)
12
+ @sheet_service.authorization = Google::Auth.get_application_default(sheet_scope)
13
+ end
14
+ end
15
+
16
+ def download_sheets_by_id(spreadsheet_id:, sheet_ids:)
17
+ sheet_ids.filter_map do |sheet_id|
18
+ begin
19
+ url = export_url(spreadsheet_id: spreadsheet_id, sheet_id: sheet_id)
20
+ string = @drive_service.http(:get, url, options: { retries: 3, max_elapsed_time: 60 })
21
+ next unless string
22
+
23
+ tempfile = Tempfile.new
24
+ tempfile.write(string)
25
+ tempfile.rewind
26
+ tempfile
27
+ rescue => e
28
+ LOGGER.error("Cannot download sheet with id #{sheet_id}. Error: #{e.message}")
29
+ nil
30
+ end
31
+ end
32
+ end
33
+
34
+ def download_all_sheets(spreadsheet_id:)
35
+ begin
36
+ spreadsheet = @sheet_service.get_spreadsheet(spreadsheet_id)
37
+ sheet_ids = spreadsheet.sheets.map { |sheet| sheet.properties.sheet_id }
38
+ LOGGER.debug("#{sheet_ids.size} sheets in the spreadsheet")
39
+ download_sheets_by_id(spreadsheet_id: spreadsheet_id, sheet_ids: sheet_ids)
40
+ rescue => e
41
+ LOGGER.error("Cannot download sheets. Error: #{e.message}")
42
+ []
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def export_url(spreadsheet_id:, sheet_id:)
49
+ "https://docs.google.com/spreadsheets/d/#{spreadsheet_id}/export?format=csv&gid=#{sheet_id}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Repositories
3
4
  class FileSystemRepository
@@ -10,4 +11,4 @@ module AdLocalize
10
11
  end
11
12
  end
12
13
  end
13
- end
14
+ end