aigu 0.3.1 → 0.4

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.
@@ -10,7 +10,7 @@ module Aigu
10
10
  end
11
11
 
12
12
  def run
13
- service_name = "#{@command}er".capitalize
13
+ service_name = "#{@command}er".split('_').map(&:capitalize).join
14
14
 
15
15
  begin
16
16
  service_class = Aigu.const_get(service_name)
@@ -28,22 +28,18 @@ module Aigu
28
28
  def parse_options_from_yaml(options)
29
29
  file = File.join(Dir.pwd, '.aigu.yml')
30
30
 
31
- if File.exists?(file)
31
+ if File.exist?(file)
32
32
  # Load YAML content
33
33
  content = YAML.load_file(file)
34
34
 
35
- # Symbolize keys
36
- content = content.reduce({}) do |memo, (key, value)|
37
- memo.merge! key.to_sym => value
38
- end
39
-
40
- # Merge with existing options
41
- options = options.merge(content)
35
+ # Symbolize keys and merge with existing options
36
+ options = options.merge(content.symbolize_keys)
42
37
  end
43
38
 
44
39
  options
45
40
  end
46
41
 
42
+ # rubocop:disable Metrics/MethodLength
47
43
  def parse_options_from_arguments(options)
48
44
  OptionParser.new do |opts|
49
45
  opts.banner = 'Usage: aigu [options]'
@@ -72,6 +68,14 @@ module Aigu
72
68
  options[:ignore] = ignore.split(',')
73
69
  end
74
70
 
71
+ opts.on('--accent-api-key=', 'Accent API key') do |key|
72
+ options[:'accent-api-key'] = key
73
+ end
74
+
75
+ opts.on('--accent-url=', 'Accent URL (ex: http://accent.mirego.com)') do |url|
76
+ options[:'accent-url'] = url
77
+ end
78
+
75
79
  opts.on_tail('-h', '--help', 'Show this message') do
76
80
  puts opts
77
81
  exit
@@ -81,5 +85,6 @@ module Aigu
81
85
 
82
86
  options
83
87
  end
88
+ # rubocop:enable Metrics/MethodLength
84
89
  end
85
90
  end
@@ -0,0 +1,82 @@
1
+ module Aigu
2
+ class CoreExporter
3
+ ENUM_LINE_REGEX = /^\s*(?<key>\w+)\s?\("(?<value_en>.*)",\s?"(?<value_fr>.*)"\),?\s$/
4
+
5
+ def initialize(opts = {})
6
+ @output_file = opts[:'output-file']
7
+ @input_directory = opts[:'input-directory']
8
+ @locale = opts[:locale]
9
+ @ignore = opts[:ignore]
10
+ end
11
+
12
+ def process!
13
+ puts "Generating Accent JSON file `#{@output_file}` based on Core Java Enum files in `#{@input_directory}` directory"
14
+
15
+ if @ignore
16
+ print 'Ignoring '
17
+ puts @ignore.join(', ')
18
+ end
19
+
20
+ puts '---'
21
+
22
+ build_output
23
+ write_json_file
24
+
25
+ puts '---'
26
+ puts 'Done'
27
+ end
28
+
29
+ protected
30
+
31
+ def build_output
32
+ @output = {}
33
+
34
+ pattern = File.join(@input_directory, '**', 'CoreLocalizedStrings.java')
35
+ Dir[pattern].each do |filepath|
36
+ if ignored_filepath?(filepath)
37
+ puts "Ignoring #{filepath}"
38
+ else
39
+ puts "Processing #{filepath}"
40
+
41
+ content = File.open(filepath, 'rb:bom|utf-8').read
42
+
43
+ content = parse_java_file(content, @locale)
44
+ @output.merge! content
45
+ end
46
+ end
47
+
48
+ @output
49
+ end
50
+
51
+ def write_json_file
52
+ file_path = @output_file
53
+ puts "Generating #{file_path}"
54
+ FileUtils.mkdir_p(File.dirname(file_path))
55
+
56
+ File.open(file_path, 'w+') do |file|
57
+ file << JSON.pretty_generate(JSON.parse(@output.to_json))
58
+ end
59
+ end
60
+
61
+ def parse_java_file(java_file_content, locale = '')
62
+ string_hash = {}
63
+
64
+ java_file_content.each_line do |line|
65
+ next if line.include?('/* <-- !!LOCALIZED STRINGS!! */')
66
+
67
+ match_data = line.match(ENUM_LINE_REGEX)
68
+ if match_data
69
+ string_hash[match_data[:key]] = locale == 'fr' ? match_data[:value_fr] : match_data[:value_en]
70
+ end
71
+ end
72
+
73
+ string_hash
74
+ end
75
+
76
+ def ignored_filepath?(filepath)
77
+ @ignore && @ignore.any? do |pattern|
78
+ return File.fnmatch(pattern, filepath, File::FNM_PATHNAME | File::FNM_DOTMATCH)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,82 @@
1
+ module Aigu
2
+ class CoreImporter
3
+ ENUM_LINE_REGEX = /^\s*(?<key>\w+)\s?\("(?<value_en>.*)",\s?"(?<value_fr>.*)"\)(?<comma>,?)\s$/
4
+
5
+ def initialize(opts = {})
6
+ @input_file = opts[:'input-file']
7
+ @output_directory = opts[:'output-directory']
8
+ @locale = opts[:locale]
9
+ end
10
+
11
+ def process!
12
+ puts "Updating Core Java Enum files in `#{@output_directory}` based on Accent-generated `#{@input_file}` file"
13
+ puts '---'
14
+
15
+ parse_json
16
+ read_java
17
+ @java_content = update_in_memory(@java_content, @object, @locale)
18
+ write_java
19
+
20
+ puts '---'
21
+ puts 'Done'
22
+ end
23
+
24
+ protected
25
+
26
+ def parse_json
27
+ json = File.read(@input_file)
28
+ @object = JSON.parse(json)
29
+ end
30
+
31
+ def read_java
32
+ pattern = File.join(@output_directory, '**', 'CoreLocalizedStrings.java')
33
+ @filepath = Dir[pattern].first
34
+ puts "Processing #{@filepath}"
35
+
36
+ @java_content = File.open(@filepath, 'rb:bom|utf-8').read
37
+ end
38
+
39
+ def write_java
40
+ puts "Updating #{@filepath}"
41
+ File.open(@filepath, 'w+:bom|utf-8') do |file|
42
+ file << @java_content
43
+ end
44
+ end
45
+
46
+ def update_in_memory(java_file_content, content, locale = '')
47
+ in_section = false
48
+ new_content = ''
49
+
50
+ java_file_content.each_line do |line|
51
+ if line.include?('/* !!LOCALIZED STRINGS!! --> */')
52
+ in_section = true
53
+ elsif line.include?('/* <-- !!LOCALIZED STRINGS!! */')
54
+ in_section = false
55
+ end
56
+
57
+ if in_section
58
+ line_content = build_line_inside_localized_section(line, content, locale)
59
+ end
60
+ new_content << (line_content || line)
61
+ end
62
+ new_content
63
+ end
64
+
65
+ def build_line_inside_localized_section(line, content, locale)
66
+ line_content = nil
67
+ match_data = line.match(ENUM_LINE_REGEX)
68
+
69
+ if match_data
70
+ key = match_data[:key]
71
+
72
+ line_content = " #{key}(\""
73
+ line_content << (locale != 'fr' ? content[key] : match_data[:value_en])
74
+ line_content << '", "'
75
+ line_content << (locale == 'fr' ? content[key] : match_data[:value_fr])
76
+ line_content << "\")#{match_data[:comma]}\n"
77
+ end
78
+
79
+ line_content
80
+ end
81
+ end
82
+ end
@@ -66,9 +66,7 @@ module Aigu
66
66
  end
67
67
 
68
68
  def flattenize_content_values(hash)
69
- result = {}
70
-
71
- hash.reduce({}) do |memo, (key, value)|
69
+ hash.each_with_object({}) do |(key, value), memo|
72
70
  if value.is_a?(Array)
73
71
  value.each_with_index do |array_value, index|
74
72
  tainted_key = "#{key}___KEY___#{index}"
@@ -77,8 +75,6 @@ module Aigu
77
75
  else
78
76
  memo[key] = sanitize_value_to_string(value)
79
77
  end
80
-
81
- memo
82
78
  end
83
79
  end
84
80
 
@@ -4,4 +4,10 @@ class Hash
4
4
  hash[key] = recursive
5
5
  end
6
6
  end
7
+
8
+ def symbolize_keys
9
+ reduce({}) do |memo, (key, value)|
10
+ memo.merge! key.to_sym => value
11
+ end
12
+ end
7
13
  end
@@ -68,7 +68,7 @@ module Aigu
68
68
  end
69
69
 
70
70
  def expand_content_values(content)
71
- content.reduce({}) do |memo, (key, value)|
71
+ content.each_with_object({}) do |(key, value), memo|
72
72
  match_data = key.match(ARRAY_REGEX)
73
73
 
74
74
  if match_data
@@ -79,8 +79,6 @@ module Aigu
79
79
  else
80
80
  memo[key] = sanitize_string_to_value(value)
81
81
  end
82
-
83
- memo
84
82
  end
85
83
  end
86
84
 
@@ -0,0 +1,165 @@
1
+ module Aigu
2
+ class IosExporter
3
+ PROP_LINE_REGEX = /^\s*"(?<key>.+)"\s?=\s?"(?<value>.*)";\s$/
4
+
5
+ DICT_DICT_OPEN_REGEX = /^\s*<dict>\s*$/
6
+ DICT_DICT_CLOSE_REGEX = /^\s*<\/dict>\s*$/
7
+ DICT_KEY_REGEX = /^\s*<key>(?<text>.*)<\/key>\s*$/
8
+ DICT_STRING_REGEX = /^\s*<string>(?<text>.*)<\/string>\s*$/
9
+
10
+ def initialize(opts = {})
11
+ @output_file = opts[:'output-file']
12
+ @input_directory = opts[:'input-directory']
13
+ @locale = opts[:locale]
14
+ @ignore = opts[:ignore]
15
+ end
16
+
17
+ def process!
18
+ puts "Generating Accent JSON file `#{@output_file}` based on IOS strings files in `#{@input_directory}` directory"
19
+
20
+ if @ignore
21
+ print 'Ignoring '
22
+ puts @ignore.join(', ')
23
+ end
24
+
25
+ puts '---'
26
+
27
+ build_output
28
+ write_json_file
29
+
30
+ puts '---'
31
+ puts 'Done'
32
+ end
33
+
34
+ protected
35
+
36
+ def build_output
37
+ @output = {}
38
+ @locale || @locale = 'en'
39
+
40
+ pattern = File.join(@input_directory, "#{@locale}.lproj", 'Localizable.strings')
41
+ filepath = Dir[pattern].first
42
+
43
+ assign_output_from_file!(filepath)
44
+
45
+ pattern = File.join(@input_directory, "#{@locale}.lproj", 'Localizable.stringsdict')
46
+ filepath = Dir[pattern].first
47
+
48
+ assign_output_from_file!(filepath)
49
+
50
+ @output
51
+ end
52
+
53
+ def assign_output_from_file!(filepath)
54
+ if filepath
55
+ puts "Processing #{filepath}"
56
+
57
+ file_content = File.open(filepath, 'rt').read
58
+
59
+ @output.merge! parse_strings_file(file_content)
60
+ else
61
+ puts 'Strings file not found'
62
+ end
63
+ end
64
+
65
+ def parse_strings_file(file_content)
66
+ file_content.each_line.each_with_object({}) do |line, string_hash|
67
+ match_data = line.match(PROP_LINE_REGEX)
68
+
69
+ if match_data
70
+ string_hash[match_data[:key]] = replace_string_interpolations(match_data[:value])
71
+ end
72
+ end
73
+ end
74
+
75
+ # This takes a string and replaces Android interpolations to iOS interpolations.
76
+ # Note that %s works on iOS but it doesn’t work with NSString because they’re objects.
77
+ # To make it work with objects, we need to convert all %s to %@.
78
+ # Example: 'Android interpolation %s or %1$s' => 'iOS interpolation %@ or %1$@'.
79
+ def replace_string_interpolations(string)
80
+ string.gsub(/%([0-9]+\$)?s/, '%\\1@')
81
+ end
82
+
83
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/BlockNesting
84
+ def parse_stringsdict_file(file_content)
85
+ # Quite custom parsing to reuse same logic on importer
86
+ # Assume a clean structure of file to KISS
87
+
88
+ in_root_dict = false
89
+ bundle_key = nil
90
+ string_key = nil
91
+ in_string_dict = false
92
+ plural_key = nil
93
+
94
+ file_content.each_line.each_with_object({}) do |line, string_hash|
95
+ if line.match(DICT_DICT_OPEN_REGEX)
96
+ # <dict>
97
+ if !in_root_dict
98
+ in_root_dict = true
99
+ elsif string_key
100
+ in_string_dict = true
101
+ end
102
+ elsif line.match(DICT_DICT_CLOSE_REGEX)
103
+ # </dict>
104
+ if in_string_dict
105
+ in_string_dict = false
106
+ string_key = nil
107
+ plural_key = nil
108
+ elsif bundle_key
109
+ bundle_key = nil
110
+ else
111
+ in_root_dict = false
112
+ end
113
+ elsif matched_data = line.match(DICT_KEY_REGEX)
114
+ # <key>..</key>
115
+ if in_root_dict
116
+ if bundle_key
117
+ if string_key
118
+ plural_key = matched_data[:text]
119
+ else
120
+ string_key = matched_data[:text]
121
+ end
122
+ else
123
+ bundle_key = matched_data[:text]
124
+ end
125
+ end
126
+ elsif matched_data = line.match(DICT_STRING_REGEX)
127
+ # <string>..</string>
128
+ if string_key && !in_string_dict
129
+ string_key = nil
130
+ elsif in_string_dict && plural_key
131
+ if %w(zero one other).include? plural_key
132
+ hash_key = "__@DICT__#{bundle_key}__@STRING__#{string_key}__@#{plural_key.upcase}"
133
+ string_hash[hash_key] = cleanup_input_xml(matched_data[:text])
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/BlockNesting
140
+
141
+ def write_json_file
142
+ file_path = @output_file
143
+ puts "Generating #{file_path}"
144
+ FileUtils.mkdir_p(File.dirname(file_path))
145
+
146
+ File.open(file_path, 'w+') do |file|
147
+ file << JSON.pretty_generate(JSON.parse(@output.to_json))
148
+ end
149
+ end
150
+
151
+ # Try to fix issue
152
+ def cleanup_input_xml(xml)
153
+ replace_html_name(xml, 'quot', '"')
154
+ replace_html_name(xml, 'amp', '&')
155
+ replace_html_name(xml, 'apos', "'")
156
+ replace_html_name(xml, 'lt', '<')
157
+ replace_html_name(xml, 'gt', '>')
158
+ xml
159
+ end
160
+
161
+ def replace_html_name(xml, html_name, character)
162
+ xml.gsub!(/&#{html_name};/, character)
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,181 @@
1
+ module Aigu
2
+ class IosImporter
3
+ DICT_DICT_OPEN_REGEX = /^\s*<dict>\s*$/
4
+ DICT_DICT_CLOSE_REGEX = /^\s*<\/dict>\s*$/
5
+ DICT_KEY_REGEX = /^\s*<key>(?<text>.*)<\/key>\s*$/
6
+ DICT_STRING_REGEX = /^(?<left>\s*<string>)(?<text>.*)(?<right><\/string>\s*)$/
7
+
8
+ def initialize(opts = {})
9
+ @input_file = opts[:'input-file']
10
+ @output_directory = opts[:'output-directory']
11
+ @locale = opts[:locale]
12
+ end
13
+
14
+ def process!
15
+ puts "Generating IOS strings files in `#{@output_directory}` based on Accent-generated `#{@input_file}` file"
16
+ puts '---'
17
+
18
+ parse_json
19
+ strings, dict = split_dict
20
+ write_strings_files(strings)
21
+ write_stringsdict_file(dict)
22
+
23
+ puts '---'
24
+ puts 'Done'
25
+ end
26
+
27
+ protected
28
+
29
+ def parse_json
30
+ json = File.read(@input_file)
31
+ @object = JSON.parse(json)
32
+ end
33
+
34
+ def split_dict
35
+ strings = {}
36
+ dict = {}
37
+
38
+ @object.each_pair do |key, value|
39
+ match_data = key.match(/^__@DICT__.*/)
40
+
41
+ if match_data
42
+ dict[key] = value
43
+ else
44
+ strings[key] = value
45
+ end
46
+ end
47
+
48
+ [strings, dict]
49
+ end
50
+
51
+ def write_strings_files(content)
52
+ @locale || @locale = 'en'
53
+
54
+ file_path = File.join(@output_directory, "#{@locale}.lproj", 'Localizable.strings')
55
+ puts "Generating #{file_path}"
56
+ FileUtils.mkdir_p(File.dirname(file_path))
57
+
58
+ File.open(file_path, 'w+:bom|utf-8') do |file|
59
+ file << format_strings_file(content)
60
+ end
61
+ end
62
+
63
+ def format_strings_file(content)
64
+ file_content = ''
65
+
66
+ content.each_pair do |key, value|
67
+ file_content << '"' << key << '" = "' << replace_string_interpolations(value) << '";' << "\n"
68
+ end
69
+
70
+ file_content
71
+ end
72
+
73
+ # This takes a string and replaces iOS interpolations to Android interpolations
74
+ # so that it is reusable across platforms.
75
+ # Example: 'iOS interpolation %@ or %1$@' => 'Android interpolation %s or %1$s'.
76
+ def replace_string_interpolations(string)
77
+ string.gsub(/%([0-9]+\$)?@/, '%\\1s')
78
+ end
79
+
80
+ def write_stringsdict_file(content)
81
+ # uses same logic as exporter to parse current file & update in memory
82
+
83
+ # read file
84
+ file_path = File.join(@output_directory, "#{@locale}.lproj", 'Localizable.stringsdict')
85
+ puts "Updating #{file_path}"
86
+
87
+ file_content = File.open(file_path, 'rb:bom|utf-8').read
88
+
89
+ # in memory update
90
+ file_content = update_stringsdict_content(file_content, content)
91
+
92
+ # write back
93
+ File.open(file_path, 'w+:bom|utf-8') do |file|
94
+ file << file_content
95
+ end
96
+ end
97
+
98
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/BlockNesting
99
+ def update_stringsdict_content(file_content, new_hash)
100
+ # Custom parsing in order to keep the file as-is, with any comments and spaces that dev used
101
+
102
+ new_content = ''
103
+ in_root_dict = false
104
+ bundle_key = nil
105
+ string_key = nil
106
+ in_string_dict = false
107
+ plural_key = nil
108
+
109
+ file_content.each_line do |line|
110
+ if line.match(DICT_DICT_OPEN_REGEX)
111
+ # <dict>
112
+ if !in_root_dict
113
+ in_root_dict = true
114
+ elsif string_key
115
+ in_string_dict = true
116
+ end
117
+
118
+ # dict open lines, keep as is
119
+ new_content << line
120
+ elsif line.match(DICT_DICT_CLOSE_REGEX)
121
+ # </dict>
122
+ if in_string_dict
123
+ in_string_dict = false
124
+ string_key = nil
125
+ plural_key = nil
126
+ elsif bundle_key
127
+ bundle_key = nil
128
+ else
129
+ in_root_dict = false
130
+ end
131
+
132
+ # dict close lines, keep as is
133
+ new_content << line
134
+ elsif matched_data = line.match(DICT_KEY_REGEX)
135
+ # <key>..</key>
136
+ if in_root_dict
137
+ if bundle_key
138
+ if string_key
139
+ plural_key = matched_data[:text]
140
+ else
141
+ string_key = matched_data[:text]
142
+ end
143
+ else
144
+ bundle_key = matched_data[:text]
145
+ end
146
+ end
147
+
148
+ # key lines, keep as is
149
+ new_content << line
150
+ elsif matched_data = line.match(DICT_STRING_REGEX)
151
+ # <string>..</string>
152
+ if string_key && !in_string_dict
153
+ string_key = nil
154
+
155
+ # useless string lines, keep as is
156
+ new_content << line
157
+ elsif in_string_dict && plural_key
158
+ if %w(zero one other).include? plural_key
159
+ hash_key = "__@DICT__#{bundle_key}__@STRING__#{string_key}__@#{plural_key.upcase}"
160
+
161
+ # replace value, keeps other parts of the line as is
162
+ new_content << matched_data[:left] << new_hash[hash_key].encode(xml: :text) << matched_data[:right]
163
+ else
164
+ # useless string lines, keep as is
165
+ new_content << line
166
+ end
167
+ else
168
+ # useless string lines, keep as is
169
+ new_content << line
170
+ end
171
+ else
172
+ # unknown line, keep as is
173
+ new_content << line
174
+ end
175
+ end
176
+
177
+ new_content
178
+ end
179
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/BlockNesting
180
+ end
181
+ end