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
@@ -49,7 +49,9 @@ Gem::Specification.new do |spec|
49
49
  spec.add_dependency 'activesupport', '>= 5.2', '< 7.0'
50
50
  spec.add_dependency 'nokogiri', '~> 1.10'
51
51
  spec.add_dependency 'colorize', '~> 0.8'
52
+ spec.add_dependency 'google-api-client', '~> 0.34'
52
53
  spec.add_dependency 'googleauth', '~> 0.12'
54
+ spec.add_dependency 'mime-types', '~> 3.3'
53
55
 
54
56
  spec.required_ruby_version = '~> 2.3'
55
57
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/setup"
4
4
  require "ad_localize"
5
+ require 'byebug'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -1,3 +1,3 @@
1
1
  #! /usr/bin/env ruby
2
2
  require 'ad_localize'
3
- AdLocalize::Runner.new.run(ARGV)
3
+ AdLocalize::CLI.start(args: ARGV)
@@ -7,24 +7,65 @@ require 'json'
7
7
  require 'csv'
8
8
  require 'logger'
9
9
  require 'colorize'
10
- require 'open-uri'
11
- require 'stringio'
12
- require 'googleauth'
13
10
  require 'optparse'
14
11
  require 'nokogiri'
12
+ require 'mime/types'
13
+ require 'erb'
14
+ require 'googleauth'
15
+ require 'google/apis/sheets_v4'
15
16
 
16
17
  require 'ad_localize/version'
17
18
  require 'ad_localize/ad_logger'
18
19
  require 'ad_localize/constant'
19
- require 'ad_localize/csv_file_manager'
20
- require 'ad_localize/csv_parser'
20
+ require 'ad_localize/cli'
21
21
  require 'ad_localize/option_handler'
22
- require 'ad_localize/runner'
23
- require 'ad_localize/platform/platform_formatter'
24
- require 'ad_localize/platform/android_formatter'
25
- require 'ad_localize/platform/ios_formatter'
26
- require 'ad_localize/platform/json_formatter'
27
- require 'ad_localize/platform/yml_formatter'
22
+
23
+ require 'ad_localize/repositories/g_sheets_repository'
24
+ require 'ad_localize/repositories/file_system_repository'
25
+
26
+ require 'ad_localize/view_models/translation_view_model'
27
+ require 'ad_localize/view_models/translation_group_view_model'
28
+
29
+ require 'ad_localize/mappers/options_to_export_request'
30
+ require 'ad_localize/mappers/csv_path_to_wording'
31
+ require 'ad_localize/mappers/locale_wording_to_hash'
32
+ require 'ad_localize/mappers/value_range_to_wording'
33
+ require 'ad_localize/mappers/translation_mapper'
34
+ require 'ad_localize/mappers/translation_group_mapper'
35
+ require 'ad_localize/mappers/android_translation_mapper'
36
+ require 'ad_localize/mappers/ios_translation_mapper'
37
+
38
+ require 'ad_localize/entities/key'
39
+ require 'ad_localize/entities/translation'
40
+ require 'ad_localize/entities/locale_wording'
41
+ require 'ad_localize/entities/wording'
42
+
43
+ require 'ad_localize/requests/g_spreadsheet_options'
44
+ require 'ad_localize/requests/export_request'
45
+ require 'ad_localize/requests/merge_policy'
46
+
47
+ require 'ad_localize/interactors/merge_wordings'
48
+ require 'ad_localize/interactors/execute_export_request'
49
+ require 'ad_localize/interactors/export_csv_files'
50
+ require 'ad_localize/interactors/export_g_spreadsheet'
51
+ require 'ad_localize/interactors/export_wording'
52
+ require 'ad_localize/interactors/platforms/export_android_locale_wording'
53
+ require 'ad_localize/interactors/platforms/export_ios_locale_wording'
54
+ require 'ad_localize/interactors/platforms/export_json_locale_wording'
55
+ require 'ad_localize/interactors/platforms/export_yaml_locale_wording'
56
+ require 'ad_localize/interactors/platforms/export_properties_locale_wording'
57
+ require 'ad_localize/interactors/platforms/export_platform_factory'
58
+
59
+ require 'ad_localize/serializers/with_template'
60
+ require 'ad_localize/serializers/info_plist_serializer'
61
+ require 'ad_localize/serializers/localizable_strings_serializer'
62
+ require 'ad_localize/serializers/localizable_stringsdict_serializer'
63
+ require 'ad_localize/serializers/strings_serializer'
64
+ require 'ad_localize/serializers/properties_serializer'
65
+ require 'ad_localize/serializers/json_serializer'
66
+ require 'ad_localize/serializers/yaml_serializer'
67
+
68
+
28
69
 
29
70
  module AdLocalize
30
71
  class Error < StandardError; end
@@ -1,21 +1,28 @@
1
1
  module AdLocalize
2
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
3
  def initialize
7
4
  @logger = Logger.new(STDOUT)
8
5
  @logger.level = Logger::INFO
9
6
  end
10
7
 
11
- def log(level, color, text)
12
- exit unless text and level and color
8
+ def warn(text)
9
+ log(level: Logger::WARN, text: text.yellow)
10
+ end
11
+
12
+ def info(text)
13
+ log(level: Logger::INFO, text: text.blue)
14
+ end
13
15
 
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)
16
+ def error(text)
17
+ log(level: Logger::ERROR, text: text.red)
18
+ end
17
19
 
18
- @logger.add(SEVERITY.dig(level), text.send(color))
20
+ def debug(text)
21
+ log(level: Logger::DEBUG, text: text.black)
22
+ end
23
+
24
+ def info!
25
+ @logger.level = Logger::INFO
19
26
  end
20
27
 
21
28
  def debug!
@@ -29,5 +36,11 @@ module AdLocalize
29
36
  def close
30
37
  @logger.close
31
38
  end
39
+
40
+ private
41
+
42
+ def log(level:, text:)
43
+ @logger.add(level, text)
44
+ end
32
45
  end
33
46
  end
@@ -0,0 +1,10 @@
1
+ module AdLocalize
2
+ class CLI
3
+ def self.start(args:)
4
+ options = OptionHandler.parse!(args)
5
+ export_request = Mappers::OptionsToExportRequest.new.map(options: options)
6
+ export_request.csv_paths = options[:csv_paths]
7
+ Interactors::ExecuteExportRequest.new.call(export_request: export_request)
8
+ end
9
+ end
10
+ end
@@ -1,25 +1,6 @@
1
1
  module AdLocalize
2
2
  module Constant
3
- SUPPORTED_PLATFORMS = %w(ios android yml json)
4
- PLURAL_KEY_SYMBOL = :plural
5
- ADAPTIVE_KEY_SYMBOL = :adaptive
6
- SINGULAR_KEY_SYMBOL = :singular
7
- COMMENT_KEY_SYMBOL = :comment
8
- INFO_PLIST_KEY_SYMBOL = :info_plist
9
- COMMENT_KEY_COLUMN_IDENTIFIER = "comment"
10
- DEFAULT_CONFIG = {
11
- platforms: {
12
- export_directory_names: {
13
- ios: "%{locale}.lproj",
14
- android: "values%{locale}"
15
- }
16
- }
17
- }
18
- CONFIG = YAML.load_file(Pathname::pwd + 'config.yml').deep_symbolize_keys rescue DEFAULT_CONFIG
19
- EXPORT_FOLDER = 'exports'
20
- IOS_SINGULAR_EXPORT_FILENAME = "Localizable.strings"
21
- IOS_INFO_PLIST_EXPORT_FILENAME = "InfoPlist.strings"
22
- IOS_PLURAL_EXPORT_FILENAME = "Localizable.stringsdict"
23
- ANDROID_EXPORT_FILENAME = "strings.xml"
3
+ COMMENT_KEY_COLUMN_IDENTIFIER = 'comment'.freeze
4
+ CSV_WORDING_KEYS_COLUMN = 'key'.freeze
24
5
  end
25
6
  end
@@ -0,0 +1,74 @@
1
+ module AdLocalize
2
+ module Entities
3
+ class Key
4
+ PLURAL_KEY_REGEXP = /\#\#\{([A-Za-z]+)\}/.freeze
5
+ ADAPTIVE_KEY_REGEXP = /\#\#\{(\d+)\}/.freeze
6
+ # see https://developer.apple.com/documentation/bundleresources/information_property_list
7
+ INFO_PLIST_KEY_REGEXP = /(NS.+UsageDescription)|(CF.+Name)/.freeze
8
+
9
+ def initialize(label:)
10
+ @label = label
11
+ end
12
+
13
+ def plural_key
14
+ @plural_key ||= compute_plural_key
15
+ end
16
+
17
+ def plural?
18
+ plural_key.present?
19
+ end
20
+
21
+ def adaptive?
22
+ adaptive_key.present?
23
+ end
24
+
25
+ def adaptive_key
26
+ @adaptive_key ||= compute_adaptive_key
27
+ end
28
+
29
+ def info_plist?
30
+ @label.match(INFO_PLIST_KEY_REGEXP).present?
31
+ end
32
+
33
+ def singular?
34
+ !(plural? || adaptive? || info_plist?)
35
+ end
36
+
37
+ def label
38
+ compute_label.strip
39
+ end
40
+
41
+ def raw_label
42
+ @label
43
+ end
44
+
45
+ def same_as?(key:)
46
+ raw_label == key.raw_label
47
+ end
48
+
49
+ private
50
+
51
+ def compute_label
52
+ if plural?
53
+ @label.gsub(PLURAL_KEY_REGEXP, '')
54
+ elsif adaptive?
55
+ @label.gsub(ADAPTIVE_KEY_REGEXP, '')
56
+ else
57
+ @label
58
+ end
59
+ end
60
+
61
+ def compute_plural_key
62
+ match = @label.match(PLURAL_KEY_REGEXP)
63
+ return unless match
64
+ match.captures.first
65
+ end
66
+
67
+ def compute_adaptive_key
68
+ match = @label.match(ADAPTIVE_KEY_REGEXP)
69
+ return unless match
70
+ match.captures.first
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,60 @@
1
+ module AdLocalize
2
+ module Entities
3
+ class LocaleWording
4
+ attr_reader(:locale, :translations)
5
+
6
+ def initialize(locale:, translations:)
7
+ @locale = locale
8
+ @translations = translations
9
+ end
10
+
11
+ def has_plural_keys?
12
+ plurals.present?
13
+ end
14
+
15
+ def has_info_plist_keys?
16
+ info_plists.present?
17
+ end
18
+
19
+ def has_singular_keys?
20
+ singulars.present?
21
+ end
22
+
23
+ def has_adaptive_keys?
24
+ adaptives.present?
25
+ end
26
+
27
+ def plurals
28
+ @plurals ||= @translations.select { |translation| translation.key.plural? }.group_by { |translation| translation.key.label }
29
+ end
30
+
31
+ def info_plists
32
+ @info_plists ||= @translations.select { |translation| translation.key.info_plist? }
33
+ end
34
+
35
+ def singulars
36
+ @singulars ||= @translations.select { |translation| translation.key.singular? }
37
+ end
38
+
39
+ def adaptives
40
+ @adaptives ||= @translations.select { |translation| translation.key.adaptive? }.group_by { |translation| translation.key.label }
41
+ end
42
+
43
+ def add_translation(translation:)
44
+ @translations.push(translation)
45
+ end
46
+
47
+ def keys
48
+ @translations.map(&:key)
49
+ end
50
+
51
+ def has_key?(key:)
52
+ translation_for(key: key).present?
53
+ end
54
+
55
+ def translation_for(key:)
56
+ @translations.find { |translation| translation.key.same_as?(key: key) }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,20 @@
1
+ module AdLocalize
2
+ module Entities
3
+ class Translation
4
+ attr_reader(
5
+ :locale,
6
+ :key,
7
+ :comment
8
+ )
9
+
10
+ attr_accessor(:value)
11
+
12
+ def initialize(locale:, key:, value:, comment: nil)
13
+ @locale = locale
14
+ @key = key
15
+ @value = value
16
+ @comment = comment
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module AdLocalize
2
+ module Entities
3
+ class Wording
4
+ attr_reader(:default_locale, :locale_wordings)
5
+
6
+ def initialize(locale_wordings:, default_locale:)
7
+ @locale_wordings = locale_wordings
8
+ @default_locale = default_locale
9
+ end
10
+
11
+ def translations_for(locale:)
12
+ @locale_wordings.find { |locale_wording| locale_wording.locale == locale }
13
+ end
14
+
15
+ def locales
16
+ @locale_wordings.map(&:locale)
17
+ end
18
+
19
+ def default_locale?(locale:)
20
+ locale == @default_locale
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ class ExecuteExportRequest
4
+ def initialize(csv_path_to_wording: nil, value_range_to_wording: nil)
5
+ @csv_path_to_wording = csv_path_to_wording
6
+ @value_range_to_wording = value_range_to_wording
7
+ end
8
+
9
+ def call(export_request:)
10
+ export_request.verbose? ? LOGGER.debug! : LOGGER.info!
11
+ print_export_request(export_request: export_request) if export_request.verbose?
12
+ LOGGER.debug("Checking request validity")
13
+ return unless export_request.valid?
14
+ if export_request.has_csv_files?
15
+ ExportCSVFiles.new(csv_path_to_wording: @csv_path_to_wording).call(export_request: export_request)
16
+ elsif export_request.has_g_spreadsheet_options?
17
+ ExportGSpreadsheet.new(value_range_to_wording: @value_range_to_wording).call(export_request: export_request)
18
+ end
19
+ LOGGER.debug("End of export request execution")
20
+ end
21
+
22
+ private
23
+
24
+ def print_export_request(export_request:)
25
+ LOGGER.debug("Export Request info")
26
+ LOGGER.debug("locales : #{export_request.locales.to_sentence}")
27
+ LOGGER.debug("platforms : #{export_request.platforms.to_sentence}")
28
+ LOGGER.debug("output_path : #{export_request.output_path}")
29
+ LOGGER.debug("verbose : #{export_request.verbose}")
30
+ LOGGER.debug("merge_policy : #{export_request.merge_policy&.policy}")
31
+ LOGGER.debug("csv_paths : #{export_request.csv_paths.to_sentence}")
32
+ if export_request.has_g_spreadsheet_options?
33
+ g_options = export_request.g_spreadsheet_options
34
+ LOGGER.debug("spreadsheet_id : #{g_options.spreadsheet_id}")
35
+ LOGGER.debug("sheet_ids : #{g_options.sheet_ids.to_sentence}")
36
+ LOGGER.debug("export_all: #{g_options.export_all}")
37
+ LOGGER.debug("service_account: #{g_options.service_account_config.present?}")
38
+ end
39
+ LOGGER.debug("End Export Request info")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ class ExportCSVFiles
4
+ def initialize(csv_path_to_wording: nil)
5
+ @csv_path_to_wording = csv_path_to_wording.presence || Mappers::CSVPathToWording.new
6
+ @merge_wordings = MergeWordings.new
7
+ end
8
+
9
+ def call(export_request:)
10
+ LOGGER.debug("Starting export csv files : #{export_request.csv_paths.to_sentence}")
11
+ wordings = export_request.csv_paths.map { |csv_path| @csv_path_to_wording.map(csv_path: csv_path) }
12
+ wording = @merge_wordings.call(wordings: wordings.compact, merge_policy: export_request.merge_policy)
13
+ ExportWording.new.call(export_request: export_request, wording: wording)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ module AdLocalize
2
+ module Interactors
3
+ class ExportGSpreadsheet
4
+ def initialize(value_range_to_wording: nil)
5
+ @value_range_to_wording = value_range_to_wording.presence || Mappers::ValueRangeToWording.new
6
+ @g_sheets_repository = Repositories::GSheetsRepository.new
7
+ end
8
+
9
+ def call(export_request:)
10
+ LOGGER.debug("Starting export google spreadsheet")
11
+ if export_request.g_spreadsheet_options.service_account_config
12
+ export_with_service_account(export_request: export_request)
13
+ else
14
+ export_without_service_account(export_request: export_request)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def export_with_service_account(export_request:)
21
+ LOGGER.debug("Using service account")
22
+ value_ranges = @g_sheets_repository.get_sheets_values(g_spreadsheet_options: export_request.g_spreadsheet_options)
23
+ wordings = value_ranges.map { |value_range| @value_range_to_wording.map(value_range: value_range) }
24
+ wording = MergeWordings.new.call(wordings: wordings.compact, merge_policy: export_request.merge_policy)
25
+ ExportWording.new.call(export_request: export_request, wording: wording)
26
+ end
27
+
28
+ def export_without_service_account(export_request:)
29
+ LOGGER.debug("Using direct access to spreadsheet")
30
+ downloaded_files = export_request.g_spreadsheet_options.public_download_urls.map do |sheet_download_url|
31
+ download_public_sheet(url: sheet_download_url)
32
+ end
33
+ export_request.csv_paths = downloaded_files.map(&:path)
34
+ if export_request.has_csv_files?
35
+ ExportCSVFiles.new.call(export_request: export_request)
36
+ downloaded_files.select { |downloaded_file| File.exist?(downloaded_file.path) }.each do |downloaded_file|
37
+ downloaded_file.close
38
+ downloaded_file.unlink
39
+ end
40
+ else
41
+ LOGGER.error("invalid export request. check the spreadsheet share configuration")
42
+ end
43
+ end
44
+
45
+ def download_public_sheet(url:)
46
+ tempfile = Tempfile.new
47
+ URI.open(url) do |uri_io|
48
+ tempfile.write uri_io.read
49
+ tempfile.rewind
50
+ end
51
+ tempfile
52
+ end
53
+ end
54
+ end
55
+ end