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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b973b5aa391a17f4dc4d001e5a8ab7e0a286b32
4
- data.tar.gz: 6d38f76e465218a22923333650a7e30650a27c14
3
+ metadata.gz: 1891efbb20cc81f439f20f30fb4d74586e09c2c9
4
+ data.tar.gz: 2715fb01c3d8f203c791ffc55357469b077ad793
5
5
  SHA512:
6
- metadata.gz: 22e775bce2767935d0a5f260dd1f8e7a7ca4d6eea1dce18a6c2bff6ca4dfde13fe2b4711cd38bb7f828ec4041b62a4f0b361ca4ecf3616cf51d486a580697d63
7
- data.tar.gz: 07c4cb7b0708431a4a38f3c399451121ee8f8c8287a16ca980a6ff80ea5f2c0f70f6a307d5d7295fda0d9b2b5fbfaa79cf03dce3355d09495ccab615b711378a
6
+ metadata.gz: 83421bf2129630b61d54543cfa484963415151301c345fd624febb67d65de937a45634173c54fb3017c5a3c3a3f95f3ab92ce1f814b147c99a560336f675f390
7
+ data.tar.gz: 3a60ed45b7ee4764a8b809cb297e8a9b9a7bfa395ae683cd335009308b189a8bb56039e39e099abee88c5a637a3e1f9ac635b10bf575b5c40bdfc32b33b562ab
@@ -0,0 +1,52 @@
1
+ AllCops:
2
+ Include:
3
+ - Rakefile
4
+ Exclude:
5
+ - bin/phare
6
+
7
+ Documentation:
8
+ Enabled: false
9
+
10
+ Encoding:
11
+ Enabled: false
12
+
13
+ LineLength:
14
+ Max: 200
15
+
16
+ AccessModifierIndentation:
17
+ EnforcedStyle: outdent
18
+
19
+ IfUnlessModifier:
20
+ Enabled: false
21
+
22
+ CaseIndentation:
23
+ IndentWhenRelativeTo: case
24
+ IndentOneStep: true
25
+
26
+ MethodLength:
27
+ CountComments: false
28
+ Max: 20
29
+
30
+ SignalException:
31
+ Enabled: false
32
+
33
+ ColonMethodCall:
34
+ Enabled: false
35
+
36
+ AsciiComments:
37
+ Enabled: false
38
+
39
+ Lambda:
40
+ Enabled: false
41
+
42
+ RegexpLiteral:
43
+ Enabled: false
44
+
45
+ AssignmentInCondition:
46
+ Enabled: false
47
+
48
+ Metrics/AbcSize:
49
+ Enabled: false
50
+
51
+ ClassLength:
52
+ Enabled: false
data/README.md CHANGED
@@ -1,8 +1,16 @@
1
1
  ## Introduction
2
2
 
3
- Aigu processes Rails YAML localization files and generates a single JSON file
4
- that can be imported into the Accent service. It can also process a JSON file
5
- generated by Accent and convert it back to multiple YAML files.
3
+ Aigu is a set of utility to process localization files to genarate JSON files
4
+ to push to Accent service. Pulling from Accent to update localization files
5
+ is also supported.
6
+
7
+ The following localization file formats are supported: Rails YAML, Android
8
+ XML, Java Enum Core, iOS strings and stringsdict.
9
+
10
+ Merge and unmerge commands allows to merge different source files into a
11
+ single JSON file to send as a single Accent project.
12
+
13
+ Push and pull can be used to interact directly with Accent.
6
14
 
7
15
  ## Installation
8
16
 
@@ -36,6 +44,13 @@ $ aigu export --locale=fr --input-directory=config/locales --output-file=my-proj
36
44
  | `output-file` | The path to the JSON file that will be written by `aigu` |
37
45
  | `ignore` | The patterns `aigu` will use to skip ignored files (eg. `routes.yml`)
38
46
 
47
+ | Command | File format |
48
+ |-------------------|-------------------------------------------------------|
49
+ | `export` | Rails YAML |
50
+ | `andoird_export` | Android XML |
51
+ | `core_export` | Java Enum Core |
52
+ | `ios_export` | iOS strings & stringsdict |
53
+
39
54
  ### Importing the JSON file from Accent
40
55
 
41
56
  The `import` command takes a generated JSON file from Accent and generates
@@ -53,10 +68,67 @@ $ aigu import --locale=fr --input-file=file-from-accent.json --output-directory=
53
68
  | `input-file` | The path to the Accent-generated JSON file |
54
69
  | `output-directory` | The directory where the localization YAML files will be generated |
55
70
 
56
- ### Using `aigu.yml`
71
+ | Command | File format |
72
+ |-------------------|-------------------------------------------------------|
73
+ | `import` | Rails YAML |
74
+ | `andoird_import` | Android XML |
75
+ | `core_import` | Java Enum Core |
76
+ | `ios_import` | iOS strings & stringsdict |
77
+
78
+ ### Merge
79
+
80
+ The `merge` command will combine several JSON file into a single one to send into a single Accent project. It will prefix keys with
81
+ a pattern embedding the filename to allow later unmerging. It works from an input directory, mergin all json files in it.
82
+ A file can be named `default.json` to avoid prefixes in the resulting json.
83
+
84
+ #### Options
85
+
86
+ | Option | Description |
87
+ |--------------------|---------------------------------------------------------------------------------------------------|
88
+ | `input-directory` | The directory containing json files to merge |
89
+ | `output-file` | The path to the JSON file that will be written to |
90
+
91
+
92
+ ### Unmerge
93
+
94
+ The `unmerge` will split the Accent JSON file into several json files, for further processing. It will look for the prefix pattern
95
+ of the merge command and will extract the filename or use default.json if pattern is not found.
96
+
97
+ #### Options
98
+
99
+ | Option | Description |
100
+ |--------------------|---------------------------------------------------------------------------------------------------|
101
+ | `input-file` | The path to the Accent-generated JSON file |
102
+ | `output-directory` | The directory where the unmerged files will be written |
103
+
104
+ ### Sending to Accent
105
+
106
+ The `push` command takes a JSON file exported by Aigu and send it to Accent
107
+
108
+ #### Options
109
+
110
+ | Option | Description |
111
+ |--------------------|---------------------------------------------------------------------------------------------------|
112
+ | `input-file` | The path to the Aigu-generated JSON file |
113
+ | `accent-api-key` | The API key of the Accent project. (See project config in Accent) |
114
+ | `accent-url` | The URL to the instance of Accent to use (Prod: http://accent.mirego.com, QA: https://mirego-accent-qa.herokuapp.com ) |
115
+
116
+ ### Pulling from Accent
117
+
118
+ The `pull` command GET the Accent generated JSON file
119
+
120
+ #### Options
121
+
122
+ | Option | Description |
123
+ |--------------------|---------------------------------------------------------------------------------------------------|
124
+ | `output-file` | The path to write the Accent JSON file to |
125
+ | `accent-api-key` | The API key of the Accent project. (See project config in Accent) |
126
+ | `accent-url` | The URL to the instance of Accent to use (Prod: http://accent.mirego.com, QA: https://mirego-accent-qa.herokuapp.com ) |
127
+
128
+ ### Using `.aigu.yml`
57
129
 
58
130
  Instead of using command-line arguments when running the `aigu` command, you
59
- can create a `aigu.yml` file at the root of your project and hard-code options
131
+ can create a `.aigu.yml` file at the root of your project and hard-code options
60
132
  in that file.
61
133
 
62
134
  ```yaml
@@ -67,6 +139,18 @@ output-directory: config/locales
67
139
  input-directory: config/locales
68
140
  ```
69
141
 
142
+ ## Contributing
143
+
144
+ We’re using `phare` to make sure all Ruby code adheres to our coding style
145
+ guide. The best way to use `phare` is as a pre-commit hook:
146
+
147
+ ```bash
148
+ $ ln -s "`pwd`/bin/phare" .git/hooks/pre-commit
149
+ ```
150
+
151
+ That way, whenever `git commit` is ran, `phare` will be executed and will abort
152
+ the commit if there are errors.
153
+
70
154
  ## License
71
155
 
72
156
  `Aigu` is © 2014 [Mirego](http://www.mirego.com) and may be freely distributed under the [New BSD license](http://opensource.org/licenses/BSD-3-Clause). See the [`LICENSE.md`](https://github.com/mirego/aigu/blob/master/LICENSE.md) file.
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ end
12
12
 
13
13
  desc 'Start an IRB session with the gem'
14
14
  task :console do
15
- $:.unshift File.expand_path('..', __FILE__)
15
+ $LOAD_PATH.unshift File.expand_path('..', __FILE__)
16
16
  require 'aigu'
17
17
  require 'irb'
18
18
 
@@ -6,19 +6,22 @@ require 'aigu/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'aigu'
8
8
  spec.version = Aigu::VERSION
9
- spec.authors = ['Rémi Prévost']
10
- spec.email = ['rprevost@mirego.com']
9
+ spec.authors = ['Rémi Prévost', 'William Jobin']
10
+ spec.email = ['rprevost@mirego.com', 'wjobin@mirego.com']
11
11
  spec.description = 'Aigu converts a directory of Rails localization files into a single JSON file to import in Accent.'
12
12
  spec.summary = spec.description
13
13
  spec.homepage = 'https://github.com/mirego/aigu'
14
14
  spec.license = 'BSD 3-Clause'
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.executables = 'aigu'
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
21
  spec.add_development_dependency 'bundler', '~> 1.3'
22
22
  spec.add_development_dependency 'rake'
23
- spec.add_development_dependency 'rspec', '~> 3.0.0.beta2'
23
+ spec.add_development_dependency 'rspec', '~> 3.0'
24
+ spec.add_development_dependency 'phare', '~> 0.6'
25
+ spec.add_development_dependency 'rubocop', '~> 0.27.0'
26
+ spec.add_development_dependency 'nokogiri', '~> 1.6.3'
24
27
  end
data/bin/aigu CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
3
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w(.. lib))
4
4
 
5
5
  require 'aigu'
6
6
 
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'phare' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('phare', 'phare')
@@ -4,11 +4,22 @@ require 'optparse'
4
4
  require 'fileutils'
5
5
  require 'json'
6
6
  require 'yaml'
7
+ require 'nokogiri'
7
8
 
8
9
  require 'aigu/extensions/hash'
9
10
  require 'aigu/cli'
10
11
  require 'aigu/importer'
11
12
  require 'aigu/exporter'
13
+ require 'aigu/android_exporter'
14
+ require 'aigu/android_importer'
15
+ require 'aigu/core_exporter'
16
+ require 'aigu/core_importer'
17
+ require 'aigu/ios_exporter'
18
+ require 'aigu/ios_importer'
19
+ require 'aigu/unmergeer'
20
+ require 'aigu/mergeer'
21
+ require 'aigu/puller'
22
+ require 'aigu/pusher'
12
23
 
13
24
  module Aigu
14
25
  end
@@ -0,0 +1,165 @@
1
+ module Aigu
2
+ class AndroidExporter
3
+ def initialize(opts = {})
4
+ @output_file = opts[:'output-file']
5
+ @input_directory = opts[:'input-directory']
6
+ @locale = opts[:locale]
7
+ @ignore = opts[:ignore]
8
+ end
9
+
10
+ def process!
11
+ puts "Generating Accent JSON file `#{@output_file}` based on Android resources files in `#{@input_directory}` directory"
12
+
13
+ if @ignore
14
+ print 'Ignoring '
15
+ puts @ignore.join(', ')
16
+ end
17
+
18
+ puts '---'
19
+
20
+ build_output
21
+ write_json_file
22
+
23
+ puts '---'
24
+ puts 'Done'
25
+ end
26
+
27
+ protected
28
+
29
+ def build_output
30
+ @output = {}
31
+
32
+ pattern = File.join(@input_directory, '*', 'strings.xml')
33
+ Dir[pattern].each do |filepath|
34
+ next unless supported_resource_for_language?(filepath, @locale)
35
+
36
+ if ignored_filepath?(filepath)
37
+ puts "Ignoring #{filepath}"
38
+ else
39
+ puts "Processing #{filepath}"
40
+
41
+ _, _, qualifiers = get_resource_type(filepath)
42
+ puts "Qualifiers: #{qualifiers}"
43
+
44
+ content = File.open(filepath, 'r:bom|utf-8').read
45
+ base_key = qualifiers
46
+
47
+ content = parse_xml(content, base_key)
48
+ @output.merge! content
49
+ end
50
+ end
51
+
52
+ @output
53
+ end
54
+
55
+ def write_json_file
56
+ file_path = @output_file
57
+ puts "Generating #{file_path}"
58
+ FileUtils.mkdir_p(File.dirname(file_path))
59
+
60
+ File.open(file_path, 'w+') do |file|
61
+ file << JSON.pretty_generate(JSON.parse(@output.to_json))
62
+ end
63
+ end
64
+
65
+ def sanitize_value_to_string(value)
66
+ case value
67
+ when true
68
+ '___TRUE___'
69
+ when false
70
+ '___FALSE___'
71
+ when nil
72
+ '___NULL___'
73
+ else
74
+ value
75
+ end
76
+ end
77
+
78
+ def parse_xml(xml, base_key = '')
79
+ xml = cleanup_input_xml(xml)
80
+ doc = Nokogiri::XML(xml) { |config| config.options = Nokogiri::XML::ParseOptions::RECOVER }
81
+ resources = doc.root.children
82
+ string_hash = {}
83
+
84
+ resources.each do |node|
85
+ if node.name == 'string'
86
+ string_hash.merge!(hash_for_key_value(node['name'], node.content, base_key))
87
+ elsif node.name == 'string-array'
88
+ resource_string_prefix = node['name'] + '__ARRAY_ITEM__#'
89
+
90
+ item_index = 0
91
+ node.children.each do |array_item|
92
+ next unless array_item.name == 'item'
93
+
94
+ hash = hash_for_key_value("#{resource_string_prefix}#{item_index}", array_item.content, base_key)
95
+ string_hash.merge! hash
96
+
97
+ item_index += 1
98
+ end
99
+ end
100
+ end
101
+
102
+ string_hash
103
+ end
104
+
105
+ def build_key_name(key_name, base_key)
106
+ base_key.empty? ? key_name : "#{key_name}__@TYPE_#{base_key}"
107
+ end
108
+
109
+ def hash_for_key_value(key, value, base_key)
110
+ { build_key_name(key, base_key) => value }
111
+ end
112
+
113
+ # Try to fix issue
114
+ def cleanup_input_xml(xml)
115
+ replace_html_name(xml, 'quot', '"')
116
+ replace_html_name(xml, 'amp', '&')
117
+ replace_html_name(xml, 'apos', "'")
118
+ replace_html_name(xml, 'lt', '<')
119
+ replace_html_name(xml, 'gt', '>')
120
+ xml
121
+ end
122
+
123
+ def replace_html_name(xml, html_name, character)
124
+ replace_string = "&##{character.ord};"
125
+ xml.gsub!(/&#{html_name};/, replace_string)
126
+ end
127
+
128
+ # Parse out resource meta from filename
129
+ # Assumes first qualifer is locale if 2 chars
130
+ def get_resource_type(filepath)
131
+ filepath_parts = filepath.split(File::SEPARATOR)
132
+ filepath = filepath_parts[-2]
133
+
134
+ resource_qualifiers = filepath.split('-')
135
+
136
+ resource_type, first_qualifier = resource_qualifiers
137
+
138
+ first_qualifier_is_locale = !first_qualifier || (first_qualifier.length == 2)
139
+
140
+ if first_qualifier_is_locale
141
+ resource_qualifiers.shift(2)
142
+ locale = first_qualifier
143
+ else
144
+ resource_qualifiers.shift(1)
145
+ locale = nil
146
+ end
147
+
148
+ [resource_type, locale, resource_qualifiers.join(' ')]
149
+ end
150
+
151
+ def supported_resource_for_language?(filepath, expected_locale)
152
+ _, locale = get_resource_type(filepath)
153
+ expected_locale == locale
154
+ end
155
+
156
+ def ignored_filepath?(filepath)
157
+ @ignore && @ignore.any? do |pattern|
158
+ File.fnmatch(pattern, filepath, File::FNM_PATHNAME | File::FNM_DOTMATCH)
159
+ end
160
+
161
+ _, locale = get_resource_type(filepath)
162
+ !(@locale == locale)
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,130 @@
1
+ module Aigu
2
+ class AndroidImporter
3
+ ARRAY_REGEX = /__ARRAY_ITEM__#(?<index>\d+)$/
4
+ TYPE_REGEX = /__@TYPE_(?<type_key>.+)$/
5
+
6
+ def initialize(opts = {})
7
+ @input_file = opts[:'input-file']
8
+ @output_directory = opts[:'output-directory']
9
+ @locale = opts[:locale]
10
+ end
11
+
12
+ def process!
13
+ puts "Generating Android XML files in `#{@output_directory}` based on Accent-generated `#{@input_file}` file"
14
+ puts '---'
15
+
16
+ parse_json
17
+ @blob = split_res_types(@object)
18
+ write_xml_files
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
+ @object = expand_content_values(@object)
30
+ end
31
+
32
+ def write_xml_files
33
+ @blob.each_pair do |type, hash|
34
+ # TODO: add locale to filename
35
+ qualifier = type.gsub(/_/, '-')
36
+ if qualifier != ''
37
+ qualifier.prepend('-')
38
+ end
39
+ if @locale
40
+ language = '-' + @locale
41
+ else
42
+ language = ''
43
+ end
44
+
45
+ file_path = File.join(@output_directory, "values#{language}#{qualifier}", 'strings.xml')
46
+ puts "Generating #{file_path}"
47
+ FileUtils.mkdir_p(File.dirname(file_path))
48
+
49
+ File.open(file_path, 'w+:bom|utf-8') do |file|
50
+ file << res_to_xml(hash)
51
+ end
52
+ end
53
+ end
54
+
55
+ # TODO: Use XML tooling
56
+ def res_to_xml(content)
57
+ xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
58
+ xml << "<resources>\n"
59
+
60
+ content.each_pair do |key, value|
61
+ if value.is_a?(Array)
62
+ xml << ' <string-array name=' << key.encode(xml: :attr) << ">\n"
63
+ value.each do |item|
64
+ xml << ' <item>' << item.encode(xml: :text) << "</item>\n"
65
+ end
66
+ xml << " </string-array>\n"
67
+ elsif value.is_a?(String)
68
+ xml << ' <string name=' << key.encode(xml: :attr) << '>' << value.encode(xml: :text) << "</string>\n"
69
+ end
70
+ end
71
+
72
+ xml << "</resources>\n"
73
+ xml
74
+ end
75
+
76
+ def localize_content_keys(content)
77
+ content.reduce({}) do |memo, (key, value)|
78
+ localized_key = key.gsub(/^([^|]+)\|/, "\\1.#{@locale}|#{@locale}.")
79
+ memo.merge localized_key => value
80
+ end
81
+ end
82
+
83
+ def expand_content_values(content)
84
+ content.each_with_object({}) do |(key, value), memo|
85
+ match_data = key.match(ARRAY_REGEX)
86
+
87
+ if match_data
88
+ canonical_key = key.gsub(ARRAY_REGEX, '')
89
+ value_index = match_data[:index].to_i
90
+ memo[canonical_key] ||= []
91
+ memo[canonical_key][value_index] = sanitize_string_to_value(value)
92
+ else
93
+ memo[key] = sanitize_string_to_value(value)
94
+ end
95
+ end
96
+ end
97
+
98
+ def split_res_types(content)
99
+ res_types = {}
100
+
101
+ content.each_pair do |key, value|
102
+ match_data = key.match(TYPE_REGEX)
103
+ if match_data
104
+ canonical_key = key.gsub(TYPE_REGEX, '')
105
+ res_type = match_data[:type_key]
106
+ else
107
+ canonical_key = key
108
+ res_type = ''
109
+ end
110
+ res_types[res_type] ||= {}
111
+ res_types[res_type][canonical_key] = value
112
+ end
113
+
114
+ res_types
115
+ end
116
+
117
+ def sanitize_string_to_value(string)
118
+ case string
119
+ when '___TRUE___'
120
+ true
121
+ when '___FALSE___'
122
+ false
123
+ when '___NULL___'
124
+ nil
125
+ else
126
+ string
127
+ end
128
+ end
129
+ end
130
+ end