ad_localize 4.1.1 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) 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 +70 -15
  6. data/Gemfile +1 -0
  7. data/Gemfile.lock +63 -61
  8. data/README.md +37 -10
  9. data/Rakefile +1 -0
  10. data/ad_localize.gemspec +14 -13
  11. data/bin/console +1 -1
  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 +27 -18
  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 -21
  36. data/lib/ad_localize/option_handler.rb +50 -26
  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 +97 -53
  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 -11
  48. data/lib/ad_localize/serializers/localizable_stringsdict_serializer.rb +12 -19
  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 -34
  59. metadata +86 -69
  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 -43
  64. data/lib/ad_localize/interactors/export_csv_files.rb +0 -17
  65. data/lib/ad_localize/interactors/export_g_spreadsheet.rb +0 -59
  66. data/lib/ad_localize/interactors/platforms/export_android_locale_wording.rb +0 -43
  67. data/lib/ad_localize/interactors/platforms/export_csv_locale_wording.rb +0 -25
  68. data/lib/ad_localize/interactors/platforms/export_ios_locale_wording.rb +0 -66
  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 -33
  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/g_spreadsheet_options.rb +0 -47
  80. data/lib/ad_localize/requests/merge_policy.rb +0 -28
  81. data/lib/ad_localize/serializers/with_template.rb +0 -19
  82. data/lib/ad_localize/validators/key_validator.rb +0 -31
  83. data/lib/ad_localize/view_models/translation_group_view_model.rb +0 -19
  84. data/lib/ad_localize/view_models/translation_view_model.rb +0 -23
@@ -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
@@ -1,90 +1,134 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Requests
3
4
  class ExportRequest
4
- SUPPORTED_PLATFORMS = %w(ios android yml json properties csv).freeze
5
- DEFAULT_EXPORT_FOLDER = 'exports'.freeze
6
- CSV_CONTENT_TYPES = %w(text/csv text/plain application/csv).freeze
7
- EMPTY_CONTENT_TYPE = 'inode/x-empty'.freeze
8
-
9
- def initialize(**args)
10
- @locales = Array(args[:locales].presence)
11
- @platforms = args[:platforms].blank? ? SUPPORTED_PLATFORMS : Array(args[:platforms])
12
- @csv_paths = Array(args[:csv_paths])
13
- @g_spreadsheet_options = args[:g_spreadsheet_options]
14
- @verbose = args[:verbose].presence || false
15
- @output_path = Pathname.new(args[:output_path].presence || DEFAULT_EXPORT_FOLDER)
16
- if @csv_paths.size > 1 || @g_spreadsheet_options&.has_multiple_sheets?
17
- @merge_policy = MergePolicy.new(policy: args[:merge_policy].presence || MergePolicy::DEFAULT_POLICY)
18
- else
19
- @merge_policy = nil
20
- end
21
- end
5
+ DEFAULTS = {
6
+ locales: [],
7
+ bypass_empty_values: false,
8
+ csv_paths: [],
9
+ merge_policy: Interactors::MergeWordings::DEFAULT_POLICY,
10
+ output_path: Pathname.new('exports'),
11
+ spreadsheet_id: nil,
12
+ sheet_ids: %w[0],
13
+ export_all: false,
14
+ verbose: false,
15
+ platforms: Entities::Platform::SUPPORTED_PLATFORMS,
16
+ downloaded_csvs: []
17
+ }
18
+
19
+ attr_accessor :output_dir
22
20
 
23
21
  attr_reader(
24
22
  :locales,
25
- :platforms,
26
- :g_spreadsheet_options,
23
+ :bypass_empty_values,
24
+ :csv_paths,
25
+ :merge_policy,
27
26
  :output_path,
27
+ :platforms,
28
+ :spreadsheet_id,
29
+ :sheet_ids,
30
+ :export_all,
28
31
  :verbose,
29
- :merge_policy
32
+ :downloaded_csvs
30
33
  )
31
34
 
32
- attr_accessor(:csv_paths)
35
+ def initialize
36
+ @locales = DEFAULTS[:locales]
37
+ @bypass_empty_values = DEFAULTS[:bypass_empty_values]
38
+ @csv_paths = DEFAULTS[:csv_paths]
39
+ @merge_policy = DEFAULTS[:merge_policy]
40
+ @output_path = DEFAULTS[:output_path]
41
+ @platforms = DEFAULTS[:platforms]
42
+ @spreadsheet_id = DEFAULTS[:spreadsheet_id]
43
+ @sheet_ids = DEFAULTS[:sheet_ids]
44
+ @export_all = DEFAULTS[:export_all]
45
+ @verbose = DEFAULTS[:verbose]
46
+ @downloaded_csvs = DEFAULTS[:downloaded_csvs]
47
+ end
48
+
49
+ def locales=(value)
50
+ return unless value.is_a? Array
51
+
52
+ @locales = value.compact
53
+ end
54
+
55
+ def bypass_empty_values=(value)
56
+ @bypass_empty_values = [true, 'true'].include?(value)
57
+ end
58
+
59
+ def csv_paths=(value)
60
+ return unless value.is_a? Array
61
+
62
+ @csv_paths = value.compact.map { |path| Pathname.new(path) }
63
+ end
33
64
 
34
- def has_csv_files?
35
- !@csv_paths.blank? && @csv_paths.all? { |csv_path| File.exist?(csv_path) && is_csv?(path: csv_path) }
65
+ def merge_policy=(value)
66
+ @merge_policy = value unless value.blank?
36
67
  end
37
68
 
38
- def has_empty_files?
39
- !@csv_paths.blank? && @csv_paths.all? { |csv_path| File.exist?(csv_path) && is_empty?(path: csv_path) }
69
+ def output_path=(value)
70
+ @output_path = Pathname.new(value) unless value.blank?
40
71
  end
41
72
 
42
- def has_g_spreadsheet_options?
43
- @g_spreadsheet_options.present?
73
+ def platforms=(value)
74
+ return unless value.is_a? Array
75
+
76
+ @platforms = value.compact & DEFAULTS[:platforms]
77
+ end
78
+
79
+ def spreadsheet_id=(value)
80
+ @spreadsheet_id = value unless value.blank?
44
81
  end
45
82
 
46
- def multiple_platforms?
47
- @platforms.size > 1
83
+ def sheet_ids=(value)
84
+ return unless value.is_a? Array
85
+
86
+ @sheet_ids = value.compact
48
87
  end
49
88
 
50
- def valid?
51
- valid_platforms? && (valid_csv_options? || valid_g_spreadsheet_options?)
89
+ def export_all=(value)
90
+ @export_all = [true, 'true'].include?(value)
52
91
  end
53
92
 
54
- def verbose?
55
- verbose
93
+ def verbose=(value)
94
+ @verbose = [true, 'true'].include?(value)
56
95
  end
57
96
 
58
- private
97
+ def downloaded_csvs=(value)
98
+ return unless value.is_a? Array
59
99
 
60
- def valid_csv_options?
61
- has_csv_files? && (@csv_paths.size == 1 || (@csv_paths.size > 1 && @merge_policy&.valid?))
100
+ @downloaded_csvs = value.compact
62
101
  end
63
102
 
64
- def valid_platforms?
65
- @platforms.size.positive? && (@platforms & SUPPORTED_PLATFORMS).size == @platforms.size
103
+ def has_sheets?
104
+ spreadsheet_id.present?
66
105
  end
67
106
 
68
- def is_csv?(path:)
69
- CSV_CONTENT_TYPES.include? content_type(path: path)
107
+ def has_csv_paths?
108
+ all_csv_paths.present?
70
109
  end
71
110
 
72
- def is_empty?(path:)
73
- content_type(path: path) == EMPTY_CONTENT_TYPE
111
+ def many_platforms?
112
+ platforms.size > 1
74
113
  end
75
114
 
76
- def content_type(path:)
77
- `file --brief --mime-type "#{path}"`.strip
115
+ def all_csv_paths
116
+ csv_paths + downloaded_csvs.map(&:path)
78
117
  end
79
118
 
80
- def valid_g_spreadsheet_options?
81
- return false if @g_spreadsheet_options.blank?
82
- if @g_spreadsheet_options.has_multiple_sheets?
83
- @g_spreadsheet_options.valid? && @merge_policy&.valid?
84
- else
85
- @g_spreadsheet_options.valid?
86
- end
119
+ def to_s
120
+ "locales: #{locales}, " \
121
+ "bypass_empty_values: #{bypass_empty_values}, " \
122
+ "csv_paths: #{csv_paths}, " \
123
+ "merge_policy: #{merge_policy}, " \
124
+ "output_path: #{output_path}, " \
125
+ "spreadsheet_id: #{spreadsheet_id}, " \
126
+ "sheet_ids: #{sheet_ids}, " \
127
+ "export_all: #{export_all}, " \
128
+ "verbose: #{verbose}, " \
129
+ "platforms: #{platforms}, " \
130
+ "downloaded_csvs: #{downloaded_csvs}"
87
131
  end
88
132
  end
89
133
  end
90
- end
134
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module AdLocalize
3
+ module Sanitizers
4
+ class IOSSanitizer
5
+ def sanitize(value:)
6
+ return if value.blank?
7
+
8
+ value.gsub(/(?<!\\)\"/, "\\\"")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,10 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
- module Mappers
3
- class AndroidTranslationMapper < TranslationMapper
4
- private
5
-
6
- def sanitize_value(value:)
3
+ module Sanitizers
4
+ class IOSToAndroidSanitizer
5
+ def sanitize(value:)
7
6
  return if value.blank?
7
+
8
8
  processedValue = value.gsub(/(?<!\\)'/, '&#39;') # match ' unless there is a \ before
9
9
  processedValue = processedValue.gsub(/(?<!\\)\"/, '&#34;') # match " unless there is a \ before
10
10
  processedValue = processedValue.gsub(">", '&gt;')
@@ -13,9 +13,9 @@ module AdLocalize
13
13
  processedValue = processedValue.gsub(/(%(\d+\$)?@)/, '%\2s') # should match values like %1$s and %s
14
14
  hasFormatting = hasFormatting || processedValue.match(/(%((\d+\$)?(\d+)?)i)/)
15
15
  processedValue = processedValue.gsub(/(%((\d+\$)?(\d+)?)i)/, '%\2d') # should match values like %i, %3$i, %01i, %1$02i
16
- # On Android, '%' must be escaped with a second '%' if and only if the string has at least one formatting pattern. In this specific case,
16
+ # On Android, '%' must be escaped with a second '%' if and only if the string has at least one formatting pattern. In this specific case,
17
17
  # a Java formatting method will be used which interprets every non escaped '%' as the start of a formatting pattern.
18
- if hasFormatting
18
+ if hasFormatting
19
19
  processedValue = processedValue.gsub(/%(?!((\d+\$)?(s|(\d+)?d)))/, '%%') # negative lookahead: identifies when user really wants to display a %
20
20
  else
21
21
  processedValue = processedValue.gsub(/%(?!((\d+\$)?(s|(\d+)?d)))/, '%') # negative lookahead: identifies when user really wants to display a %
@@ -27,4 +27,4 @@ module AdLocalize
27
27
  end
28
28
  end
29
29
  end
30
- end
30
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ module AdLocalize
3
+ module Sanitizers
4
+ class PassThroughSanitizer
5
+ def sanitize(value:)
6
+ value
7
+ end
8
+ end
9
+ end
10
+ end
@@ -1,12 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Serializers
3
- class InfoPlistSerializer
4
- include WithTemplate
5
-
4
+ class InfoPlistSerializer < TemplatedSerializer
6
5
  INFO_PLIST_FILENAME = "InfoPlist.strings".freeze
7
6
 
8
7
  def initialize
9
- @translation_mapper = Mappers::IOSTranslationMapper.new
8
+ super(sanitizer: Sanitizers::IOSSanitizer.new)
10
9
  end
11
10
 
12
11
  private
@@ -15,13 +14,12 @@ module AdLocalize
15
14
  TEMPLATES_DIRECTORY + "/ios/#{INFO_PLIST_FILENAME}.erb"
16
15
  end
17
16
 
18
- def hash_binding(locale_wording:)
19
- { translations: map_translations(translations: locale_wording.info_plists) }
20
- end
21
-
22
- def map_translations(translations:)
23
- translations.map { |translation| @translation_mapper.map(translation: translation) }
17
+ def variable_binding(locale_wording:)
18
+ translations = locale_wording.info_plists.map do |_, translation|
19
+ map_simple_wording(translation: translation)
20
+ end
21
+ { translations: translations }
24
22
  end
25
23
  end
26
24
  end
27
- end
25
+ end
@@ -1,12 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Serializers
3
4
  class JSONSerializer
4
- def initialize
5
- @locale_wording_to_hash = Mappers::LocaleWordingToHash.new
6
- end
7
-
8
5
  def render(locale_wording:)
9
- @locale_wording_to_hash.map(locale_wording: locale_wording).to_json
6
+ content = Mappers::LocaleWordingToHash.new.map(locale_wording: locale_wording)
7
+ content.to_json
10
8
  end
11
9
  end
12
10
  end
@@ -1,12 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Serializers
3
- class LocalizableStringsSerializer
4
- include WithTemplate
5
-
4
+ class LocalizableStringsSerializer < TemplatedSerializer
6
5
  LOCALIZABLE_STRINGS_FILENAME = "Localizable.strings".freeze
7
6
 
8
7
  def initialize
9
- @translation_mapper = Mappers::IOSTranslationMapper.new
8
+ super(sanitizer: Sanitizers::IOSSanitizer.new)
10
9
  end
11
10
 
12
11
  private
@@ -15,13 +14,12 @@ module AdLocalize
15
14
  TEMPLATES_DIRECTORY + "/ios/#{LOCALIZABLE_STRINGS_FILENAME}.erb"
16
15
  end
17
16
 
18
- def hash_binding(locale_wording:)
19
- { translations: map_translations(translations: locale_wording.singulars) }
20
- end
21
-
22
- def map_translations(translations:)
23
- translations.map { |translation| @translation_mapper.map(translation: translation) }
17
+ def variable_binding(locale_wording:)
18
+ translations = locale_wording.singulars.map do |_, translation|
19
+ map_simple_wording(translation: translation)
20
+ end
21
+ { translations: translations }
24
22
  end
25
23
  end
26
24
  end
27
- end
25
+ end
@@ -1,13 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Serializers
3
- class LocalizableStringsdictSerializer
4
- include WithTemplate
5
-
4
+ class LocalizableStringsdictSerializer < TemplatedSerializer
6
5
  LOCALIZABLE_STRINGSDICT_FILENAME = "Localizable.stringsdict".freeze
7
6
 
8
7
  def initialize
9
- @translation_mapper = Mappers::IOSTranslationMapper.new
10
- @translation_group_mapper = Mappers::TranslationGroupMapper.new(translation_mapper: @translation_mapper)
8
+ super(sanitizer: Sanitizers::IOSSanitizer.new)
11
9
  end
12
10
 
13
11
  private
@@ -16,20 +14,15 @@ module AdLocalize
16
14
  TEMPLATES_DIRECTORY + "/ios/#{LOCALIZABLE_STRINGSDICT_FILENAME}.erb"
17
15
  end
18
16
 
19
- def hash_binding(locale_wording:)
20
- {
21
- plurals: map_plurals(plurals: locale_wording.plurals),
22
- adaptives: map_adaptives(adaptives: locale_wording.adaptives)
23
- }
24
- end
25
-
26
- def map_plurals(plurals:)
27
- plurals.map { |label, translations| @translation_group_mapper.map(label: label, translations: translations) }
28
- end
29
-
30
- def map_adaptives(adaptives:)
31
- adaptives.map { |label, translations| @translation_group_mapper.map(label: label, translations: translations) }
17
+ def variable_binding(locale_wording:)
18
+ plurals = locale_wording.plurals.map do |label, translations|
19
+ map_compound_wording(label: label, translations: translations)
20
+ end
21
+ adaptives = locale_wording.adaptives.map do |label, translations|
22
+ map_compound_wording(label: label, translations: translations)
23
+ end
24
+ { plurals: plurals, adaptives: adaptives }
32
25
  end
33
26
  end
34
27
  end
35
- end
28
+ end
@@ -1,10 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module AdLocalize
2
3
  module Serializers
3
- class PropertiesSerializer
4
- include WithTemplate
5
-
4
+ class PropertiesSerializer < TemplatedSerializer
6
5
  def initialize
7
- @translation_mapper = Mappers::TranslationMapper.new
6
+ super(sanitizer: Sanitizers::PassThroughSanitizer.new)
8
7
  end
9
8
 
10
9
  private
@@ -13,13 +12,12 @@ module AdLocalize
13
12
  TEMPLATES_DIRECTORY + "/properties/template.properties.erb"
14
13
  end
15
14
 
16
- def hash_binding(locale_wording:)
17
- { translations: map_translations(translations: locale_wording.singulars) }
18
- end
19
-
20
- def map_translations(translations:)
21
- translations.map { |translation| @translation_mapper.map(translation: translation) }
15
+ def variable_binding(locale_wording:)
16
+ singulars = locale_wording.singulars.map do |_, translation|
17
+ map_simple_wording(translation: translation)
18
+ end
19
+ { translations: singulars }
22
20
  end
23
21
  end
24
22
  end
25
- end
23
+ end