aigu 0.3.1 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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