fame 0.0.1 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aaa07dd2ecd76f2cf02be0a769acc8fe9f982a3d
4
- data.tar.gz: d7a888bfbd6a93c52037b6088249b8c116abbab1
3
+ metadata.gz: 6bd4fe738ca58b090d8fcee6570aa4ba9283febe
4
+ data.tar.gz: e6a6ac82060e519f446919473164734a576e2e17
5
5
  SHA512:
6
- metadata.gz: 48b11deac64f15a42d05ee456f2e167fc04d352228b6d63e992cfb6c4ac6ad64c34d665826f1a9b09a0db309d94533a7b3140bb7834ddcb0a594ec8e56ffd8ef
7
- data.tar.gz: 45064b46d5fb314057bde5b2f0e2452b3b82217ef63659736b164865757dd519c3567bd046ff036d4b14a053c6b3ab57aab3b07cf0e7a71dcffd3a8390b5820d
6
+ metadata.gz: 4f9b0e39bd970cfa26af894e5bf2c049242ea4206a4e94ab49c2bf3d37e6c2e0484cf73f5f388dd31c124b985b6a948d43ded2d183d577e9dca2206e1c9c9b92
7
+ data.tar.gz: 21eb7470a2acec758812dc8aa182976045932cf07550e90fa6d7f9c150022f6652a0d12a8a8c9202d824ae78ee7eadbbe1af10827d07987582edb6f056519f36
data/README.md CHANGED
@@ -1,12 +1,34 @@
1
- # Fame
1
+ ![Fame logo](docs/logo.png)
2
2
 
3
3
  Delightful localization of .storyboard and .xib files, right within Interface Builder.
4
+ Fame makes it exceptionally easy to enable _specific_ UI elements to be translated and exported to localizable .xliff files.
5
+
6
+ -----
7
+
8
+ <p align="center">
9
+ <a href="#introduction">Introduction</a> &bull;
10
+ <a href="#fame-features">Fame Features</a> &bull;
11
+ <a href="#installation">Installation</a> &bull;
12
+ <a href="#usage">Usage</a>
13
+ </p>
14
+
15
+ -----
16
+
17
+ **TL;DR - Get started in less than 50 seconds.**
18
+
19
+ * Add languages to your Xcode project
20
+ * Add [Fame.swift](platform/Fame.swift) file to your project
21
+ * Open a storyboard or XIB file
22
+ * Enable or disable UI elements that should be localized within attribute inspector
23
+ * run `fame --project /path/to/Example.xcodeproj` from the root of your project
24
+ * Use the generated .xliff files to manage translations
25
+ * 🚀
4
26
 
5
- Fame makes it easy to enable specific Interface Builder elements to be translated and exported to localizable .strings files.
6
27
 
7
28
  ## Introduction
8
29
 
9
- Compared to localization in code (`NSLocalizedString`), Storyboard and XIB localizations are a tedious task for both developers and translators.
30
+ Compared to localization in code (i.e. `NSLocalizedString`), Storyboard and XIB localizations are a tedious task for both developers and translators.
31
+ Here's why.
10
32
 
11
33
  #### Static vs. dynamic localization
12
34
 
@@ -14,24 +36,29 @@ Storyboard and XIB files usually contain a mixed set of elements with *static* o
14
36
  * **Static text**: Elements with fixed localization strings that will never change at runtime
15
37
  * **Dynamic text**: Elements that will change their localized text during runtime. For example a label that is populated with data from an API or a status label that is populated using `NSLocalizedString` at runtime.
16
38
 
17
- **Static text elements should be localized using a generated .strings file, dynamic text elements should be ignored during translation.**
39
+ <!-- ![Example of static vs. dynamic (TableViewCell)]() -->
18
40
 
19
- However, Storyboard and XIB .stings files generated by Xcode also contain dynamic text localizations that will always be overridden at runtime. This makes it hard to distinguish between localizations that should be translated and dynamic text that can be safely ignored during translation.
20
- Its up to the app developers to either manually remove generated localizations that should not be translated, or leave them for the translators to waste their time with.
41
+ **Static text elements should be localized, dynamic text elements should be ignored during translation.**
42
+
43
+ However, generated Storyboard and XIB translations also contain dynamic text localizations that will always be overridden at runtime. This makes it hard to distinguish between localizations that should be translated and dynamic text that can be safely ignored during translation.
44
+ It's up to the app developers to either manually remove generated localizations that should not be translated, or leave them for the translators to waste their time with.
21
45
 
22
46
  #### Localization comments
23
47
 
24
48
  Storyboard and XIB .stings files generated by Xcode **do not provide a useful comment that provides context to the translator**.
25
49
 
26
- ```css
27
- /* Class = "UILabel"; text = "Label"; ObjectID = "Rfi-2u-xEd"; */
28
- "Rfi-2u-xEd.text" = "Label";
50
+ ```xml
51
+ <trans-unit id="si2-WH-Hr5.text">
52
+ <source>A fancy Label</source>
53
+ <target>A fancy Label</target>
54
+ <note>Class = "UILabel"; text = "A fancy Label"; ObjectID = "si2-WH-Hr5";</note> <-- This is not so helpful 🙄
55
+ </trans-unit>
29
56
  ```
30
57
 
31
58
  Translators use this comment to make sure their translation fits into the context it is used in. Its again up to the app developers to either manually search for specific translations and add a comment, or leave let the translators figure out how to find the best translation without context.
32
59
 
33
60
 
34
- ## Why Fame?
61
+ ## Fame Features
35
62
 
36
63
  Fame solves the above mentioned issues to help developers and translators get the most out of tedious localization tasks.
37
64
 
@@ -41,19 +68,22 @@ Fame makes it easy for developers to specify which elements of your Storyboard a
41
68
 
42
69
  ![Interface Builder Integration](docs/ib_detail.png)
43
70
 
44
- #### Command line interface
71
+ #### Command Line Interface
45
72
 
46
- Using the fame CLI, developers can export .strings files that only contain localizations for elements previously enabled in Interface Builder.
73
+ Using the fame CLI, developers can export .xliff files that only contain localizations for elements previously enabled in Interface Builder. After all files have been translated, the CLI makes it super easy to batch-import the .xliff files back into Xcode.
47
74
 
48
- ![gif]()
75
+ ![fame CLI](docs/terminal.gif)
49
76
 
50
- #### Generates beautiful .strings files
77
+ #### Generates beautiful .xliff files
51
78
 
52
- Translators only receive the strings that should actually be translated, saving them time (and you potentially lots of money). All generated .strings files also contain each element's name and a useful comment to provide more context by the app developer.
79
+ Translators only receive the strings that should actually be translated, saving them time (and you potentially lots of money). All generated .xliff files also contain each element's name and a useful comment to provide more context by the app developer.
53
80
 
54
- ```css
55
- /* MyViewController.label.text: Explains how to purchase a pro subscription. */
56
- "Rfi-2u-xEd.text" = "Label";
81
+ ```xml
82
+ <trans-unit id="si2-WH-Hr5.text">
83
+ <source>A fancy Label</source>
84
+ <target>A fancy Label</target>
85
+ <note>Explains to the customer how to purchase a pro subscription. Make it catchy.</note> <-- Ahh, much better 😍
86
+ </trans-unit>
57
87
  ```
58
88
 
59
89
  ## Installation
@@ -82,7 +112,7 @@ You should now have a Base Interface Builder file (e.g. `Main.storyboard`) in a
82
112
 
83
113
  That's it, read on to enable fame for localization.
84
114
 
85
- #### Setup Fame for Interface Builder
115
+ #### Setup [Fame.swift](platform/Fame.swift) Interface Builder integration
86
116
 
87
117
  In order to enable the Interface Builder integration to specify the elements that should be translated, add the **[Fame.swift](platform/Fame.swift)** file to your Xcode project. To test the Interface Builder integration, open any Interface Builder file in your project, select an element (e.g. a UILabel) and you should see a new section that lets you configure localization for this element in the Attributes inspector.
88
118
 
@@ -90,41 +120,56 @@ In order to enable the Interface Builder integration to specify the elements tha
90
120
 
91
121
  You can now enable localization for each element you want to have translated.
92
122
 
93
-
94
123
  ## Usage
95
124
 
125
+ ### Export
96
126
 
97
- Once all localizable elements have been configured in Interface Builder, you can export the localizable .strings file using the `fame` command line tool.
98
-
127
+ Once all localizable elements have been configured in Interface Builder, you can export the localizable .xliff file using the `fame` command line tool.
99
128
 
100
129
  First, make sure to commit all local changes, just to be safe. Then open terminal, navigate to the root folder of your project and run
101
130
 
131
+ ```bash
132
+ $ fame --project Example.xcodeproj [--ib-file-path] [--output-path]
133
+ ```
134
+
135
+ In a nutshell, the `fame export` command does the following:
136
+
137
+ * Analyze the given Xcode project file for supported languages
138
+ * Find all .storyboard and .xib files in the `--ib-file-path` (recursively, you may pass a file or folder, defaults to the current directory)
139
+ * Analyze each Interface Builder file and extract the localization settings (set via the [Fame.swift](platform/Fame.swift) integration)
140
+ * Generate the full localizable .xliff file using Apple's `xcodebuild`
141
+ * Filter the `xcodebuild` output based on the analyzed localizable settings
142
+ * Update the generated .xliff with useful comments
143
+ * Save the clean .xliff files to `--output-path` (defaults to the current directory)
144
+
145
+ Enjoy a little snack 🍉
146
+
147
+ ### Import
148
+
149
+ To import one or more .xliff files back into Xcode, run
102
150
 
103
151
  ```bash
104
- $ fame
152
+ $ fame import --project Example.xcodeproj [--xliff-path]
105
153
  ```
106
154
 
107
- In a nutshell, the fame command does the following:
155
+ > Note: The very first import may fail due to limitations in `xcodebuild`, fame will
156
+ > handle the failure gracefully and provide instructions to circumvent this issue.
108
157
 
109
- * Find all .storyboard and .xib files in the current folder (recursively, you can also pass a subpath or file to the fame command)
110
- * Lookup the localizable settings (you have set in Interface Builder) for each file
111
- * Generate the full localizable .strings file using Apple's `ibtool`
112
- * Filter the `ibtool` output based on your licalizable settings
113
- * Save a new `<NameOfStoryboardOrXIB>.strings` file to the `en.lproj` folder
158
+ In a nutshell, the `fame import` command does the following:
159
+
160
+ * Find all .xliff files in the `--xliff-path` (defaults to the current directory)
161
+ * Import all found .xliff files using Apple's `xcodebuild`
162
+
163
+ Enjoy your favorite hot beverage ☕️
114
164
 
115
165
  ## Contributing
116
166
 
117
- 1. Fork it ( https://github.com/aschuch/fame/fork )
167
+ 1. Fork it (https://github.com/aschuch/fame/fork)
118
168
  2. Create your feature branch (`git checkout -b my-new-feature`)
119
169
  3. Commit your changes (`git commit -am 'Add some feature'`)
120
170
  4. Push to the branch (`git push origin my-new-feature`)
121
171
  5. Create a new Pull Request
122
172
 
123
- #### TODO
124
-
125
- * Incremental updates of .string files (`ibtool --previous-file <OLD .storyboard> --incremental-file YY --localize-incremental`)
126
- * Write tests with fixtures
127
-
128
173
  ## Contact
129
174
 
130
175
  Feel free to get in touch.
data/bin/fame CHANGED
@@ -7,6 +7,8 @@ require 'commander'
7
7
  require 'colorize'
8
8
  require 'fame/version'
9
9
  require 'fame/interface_builder'
10
+ require 'fame/xliff_import'
11
+ require 'fame/xliff_export'
10
12
 
11
13
  #
12
14
  # The Fame CLI
@@ -17,62 +19,58 @@ class FameApplication
17
19
  def run
18
20
  program :name, 'Fame'
19
21
  program :version, Fame::VERSION
20
- program :description, 'Replace identifiers within Apple Interface Builder files to use nice keys and descriptions for localization.'
21
- default_command :localize
22
+ program :description, 'Delightful localization of .storyboard and .xib files, right within Interface Builder. The fame CLI exports .xliff files based on the settings provided in Interface Builder files.'
23
+ default_command :export
22
24
 
23
25
  #
24
- # Default localize command
26
+ # Import
25
27
  #
26
- command :localize do |c|
27
- c.syntax = 'fame localize [options]'
28
- c.description = 'Replaces generated identifiers of the given Interface Builder file(s) and generates .strings files.'
29
- c.option '--path STRING', String, 'Path to an interface builder file or a folder that contains interface builder files.'
28
+ command :import do |c|
29
+ c.syntax = 'fame import --project /path/to/xcodeproj [options]'
30
+ c.description = 'Imports all given .xliff files into the given Xcode project.'
31
+ c.option '--project STRING', String, 'Path to an Xcode project that should be localized'
32
+ c.option '--xliff-path STRING', String, 'Path to an .xliff file or a folder that contains .xliff files to be imported.'
30
33
 
31
34
  c.action do |args, options|
32
- options.default :path => '.'
35
+ options.default :xliff_path => '.'
33
36
 
34
- files = Fame::InterfaceBuilder.determine_ib_files!(options.path)
35
- puts "\nFound #{files.count} files to localize.\n".light_black
36
-
37
- # Generate localizable strings for each file
38
- files.each_with_index do |f, index|
39
- puts "(#{index+1}/#{files.count}) #{f}".light_blue
37
+ # Import .xliff files
38
+ xliff = Fame::XliffImport.new(options.project)
39
+ xliff.import(options.xliff_path)
40
+ end
41
+ end
40
42
 
41
- ib = Fame::InterfaceBuilder.new
43
+ #
44
+ # Export
45
+ #
46
+ command :export do |c|
47
+ c.syntax = 'fame export --project /path/to/xcodeproj [options]'
48
+ c.description = 'Exports all .xliff files for the current Xcode project based on the settings provided in the Interface Builder files (via Fame.swift).'
49
+ c.option '--project STRING', String, 'Path to an Xcode project that should be localized'
50
+ c.option '--ib-file-path STRING', String, 'Path to an interface builder file or a folder that contains interface builder files that should be localized.'
51
+ c.option '--output-path STRING', String, 'Path to a folder where exported .xliff files should be placed.'
42
52
 
43
- # Generate new localizable.strings file
44
- strings = ib.generate_localizable_strings(f)
53
+ c.action do |args, options|
54
+ options.default :ib_file_path => '.'
55
+ options.default :output_path => '.'
45
56
 
46
- if strings.empty?
47
- puts "✔".green + " (no strings to localize)︎".yellow
48
- else
49
- FileUtils.mkdir_p(strings_folder(f))
50
- File.write(strings_file_path(f), strings)
57
+ ib_files = Fame::InterfaceBuilder.determine_ib_files!(options.ib_file_path)
58
+ puts "Found #{ib_files.count} file(s) to localize".light_black
51
59
 
52
- puts "✔︎".green + " Generated strings file at #{strings_file_path(f)}".black
53
- end
60
+ # Collect IB nodes for each file
61
+ ib_nodes = ib_files.inject([]) do |all, file|
62
+ ib = Fame::InterfaceBuilder.new(file)
63
+ all + ib.nodes
64
+ end
54
65
 
55
- puts "-----------------------------------------------------\n".light_black
56
- end
66
+ # Generate XLIFF translation files
67
+ xliff = Fame::XliffExport.new(options.project)
68
+ xliff.export(options.output_path, ib_nodes)
57
69
  end
58
70
  end
59
71
 
60
72
  run!
61
73
  end
62
-
63
- private
64
-
65
- def strings_folder(path)
66
- folder = File.dirname(path)
67
- File.join(folder, "..", "en.lproj")
68
- end
69
-
70
- def strings_file_path(path)
71
- file_name = File.basename(path, File.extname(path))
72
- file = File.join(strings_folder(path), "#{file_name}.strings")
73
- File.expand_path(file)
74
- end
75
-
76
74
  end
77
75
 
78
76
  # run application
@@ -4,136 +4,67 @@ require 'colorize' # colorful console output
4
4
  require_relative 'models'
5
5
 
6
6
  module Fame
7
-
8
- class InterfaceBuilder
9
- # Keypaths to custom runtime attributes (provided by iOS Extenstion, see Fame.swift)
10
- LOCALIZATION_ENABLED_KEYPATH = "i18n_enabled".freeze
11
- LOCALIZATION_COMMENT_KEYPATH = "i18n_comment".freeze
12
-
13
- # All accepted Interface Builder file types
14
- ACCEPTED_FILE_TYPES = [".storyboard", ".xib"].freeze
15
-
16
- #
17
- # Initialization
18
- #
19
- def self.determine_ib_files!(path = ".")
20
- raise "The provided file or folder does not exist" unless File.exist? path
21
-
22
- if File.directory?(path)
23
- files = Dir.glob(path + "/**/*{#{ACCEPTED_FILE_TYPES.join(',')}}")
24
- raise "The provided folder did not contain any interface files (#{ACCEPTED_FILE_TYPES.join(', ')})" unless files.count > 0
25
- return files
26
- else
27
- raise "The provided file is not an interface file (#{ACCEPTED_FILE_TYPES.join(', ')})" unless ACCEPTED_FILE_TYPES.include? File.extname(path)
28
- return [path]
29
- end
30
- end
31
-
32
- #
33
- # Generates a .strings file for the Interface Builder file at the given path.
34
- # The output only contains elements where localization has been enabled.
35
- #
36
- def generate_localizable_strings(file)
37
- localizable_strings_entries(file)
38
- .sort_by! { |e| e.node.vc_name }
39
- .map(&:formatted_strings_file_entry)
40
- .join("\n\n")
41
- end
42
-
43
- private
44
-
45
- #
46
- # Generates ibtool output in plist format
47
- #
48
- def ibtool(file)
49
- # <dict>
50
- # <key>6lc-A3-0nG</key>
51
- # <dict>
52
- # <key>text</key>
53
- # <string>Empty localization ID</string>
54
- # </dict>
55
- # ...
56
- # </dict>
57
- output = `xcrun ibtool #{file} --localizable-strings --localizable-stringarrays`
58
- plist = Plist::parse_xml(output)
59
- strings = plist['com.apple.ibtool.document.localizable-strings']
60
- string_arrays = plist['com.apple.ibtool.document.localizable-stringarrays']
61
-
62
- [strings, string_arrays]
63
- end
64
-
65
- #
66
- # Returns all XML nodes with a custom localization ID
67
- #
68
- def nodes(file)
69
- storyboard = File.open(file)
70
- doc = Nokogiri::XML(storyboard)
71
-
72
- # Grab raw nokogiri nodes that have a localization keypath
73
- raw_nodes = doc.xpath("//userDefinedRuntimeAttribute[@keyPath='#{LOCALIZATION_ENABLED_KEYPATH}']")
74
-
75
- # Map raw nodes info to instances of LocalizedNode
76
- raw_nodes.map do |node|
77
- parent = node.parent.parent # i.e. UILabel, UISwitch, etc.
78
- vc = parent.xpath("ancestor::viewController") # the view controller of the element (only available in .storyboard files)
79
- element_name = parent.name # i.e. label, switch
80
- original_id = parent['id'] # ugly Xcode ID, e.g. F4z-Kg-ni6
81
- vc_name = vc.attr('customClass').value rescue nil # name of custom view controller class
82
-
83
- i18n_enabled = node.parent.xpath("userDefinedRuntimeAttribute[@keyPath='#{LOCALIZATION_ENABLED_KEYPATH}']").attr('value').value == "YES" rescue false
84
- i18n_comment = node.parent.xpath("userDefinedRuntimeAttribute[@keyPath='#{LOCALIZATION_COMMENT_KEYPATH}']").attr('value').value rescue nil
85
-
86
- LocalizedNode.new(node, original_id, vc_name, element_name, i18n_enabled, i18n_comment)
87
- end
88
- end
89
-
90
- #
91
- # Returns the localizable strings entries for a given
92
- # Interface Builder file.
93
- #
94
- def localizable_strings_entries(file)
95
- # Generate ibtool output
96
- strings, string_arrays = ibtool(file)
97
-
98
- # Get nodes for current file
99
- nodes = nodes(file)
100
-
101
- # Generate new strings file
102
- entries = []
103
- nodes.each do |node|
104
- next unless node.i18n_enabled
105
- unless element = strings[node.original_id] || string_arrays[node.original_id]
106
- puts " ✘ #{node.original_id} (#{node.element_name}): #{node.original_id} not found in ibtool output".red
107
- next
108
- end
109
-
110
- # A localization may contain more than one element.
111
- # e.g. a UITextField has a `text` and a `placeholdertext` localization
112
- element.each do |property, value|
113
- next if property == "ibExternalUserDefinedRuntimeAttributesLocalizableStrings"
114
-
115
- if value.is_a?(Array)
116
- # The localization contains an array of values, e.g. when localizing a UISegmentedControl
117
- value.each_with_index do |v, index|
118
- p = "#{property}[#{index}]"
119
- entry = LocalizableStringsEntry.new(node, p, v)
120
- entries << entry
121
-
122
- puts " ✔︎ #{entry.formatted_info}".green
123
- end
124
- else
125
- # The localization only contains a single value
126
- entry = LocalizableStringsEntry.new(node, property, value)
127
- entries << entry
128
-
129
- puts " ✔︎ #{entry.formatted_info}".green
130
- end
131
-
132
- end
133
- end
134
-
135
- entries
136
- end
137
-
138
- end
7
+ class InterfaceBuilder
8
+ # Keypaths to custom runtime attributes (provided by iOS Extenstion, see Fame.swift)
9
+ LOCALIZATION_ENABLED_KEYPATH = "i18n_enabled".freeze
10
+ LOCALIZATION_COMMENT_KEYPATH = "i18n_comment".freeze
11
+
12
+ # All accepted Interface Builder file types
13
+ ACCEPTED_FILE_TYPES = [".storyboard", ".xib"].freeze
14
+
15
+ #
16
+ # Initializer
17
+ # @param ib_path The path to an Interface Builder file that should be localized.
18
+ #
19
+ def initialize(ib_path)
20
+ @ib_path = ib_path
21
+ end
22
+
23
+ #
24
+ # Searches the current Interface Builder file for XML nodes that should be localized.
25
+ # Localization is only enabled if explicitly set via the fame Interface Builder integration (see Fame.swift file).
26
+ # @return [Array<LocalizedNode>] An array of LocalizedNode objects that represent the localizable elements of the given Interface Builder file
27
+ #
28
+ def nodes
29
+ ib_file = File.open(@ib_path)
30
+ doc = Nokogiri::XML(ib_file)
31
+ ib_file.close
32
+
33
+ # Grab raw nokogiri nodes that have a localization keypath
34
+ raw_nodes = doc.xpath("//userDefinedRuntimeAttribute[@keyPath='#{LOCALIZATION_ENABLED_KEYPATH}']")
35
+
36
+ # Map raw nodes info to instances of LocalizedNode
37
+ raw_nodes.map do |node|
38
+ parent = node.parent.parent # i.e. UILabel, UISwitch, etc.
39
+ vc = parent.xpath("ancestor::viewController") # the view controller of the element (only available in .storyboard files)
40
+ element_name = parent.name # i.e. label, switch
41
+ original_id = parent['id'] # ugly Xcode ID, e.g. F4z-Kg-ni6
42
+ vc_name = vc.attr('customClass').value rescue nil # name of custom view controller class
43
+
44
+ i18n_enabled = node.parent.xpath("userDefinedRuntimeAttribute[@keyPath='#{LOCALIZATION_ENABLED_KEYPATH}']").attr('value').value == "YES" rescue false
45
+ i18n_comment = node.parent.xpath("userDefinedRuntimeAttribute[@keyPath='#{LOCALIZATION_COMMENT_KEYPATH}']").attr('value').value rescue nil
46
+
47
+ LocalizedNode.new(node, original_id, vc_name, element_name, i18n_enabled, i18n_comment)
48
+ end
49
+ end
50
+
51
+ #
52
+ # Searches the given path for Interface Builder files (.storyboard & .xib) and returns their paths.
53
+ # @param path The path that should be searched for Interface Builder files.
54
+ # @return [Array<String>] An array of paths to Interface Builder files.
55
+ #
56
+ def self.determine_ib_files!(path)
57
+ raise "The provided file or folder does not exist" unless File.exist? path
58
+
59
+ if File.directory?(path)
60
+ files = Dir.glob(path + "/**/*{#{ACCEPTED_FILE_TYPES.join(',')}}")
61
+ raise "The provided folder did not contain any interface files (#{ACCEPTED_FILE_TYPES.join(', ')})" unless files.count > 0
62
+ return files
63
+ else
64
+ raise "The provided file is not an interface file (#{ACCEPTED_FILE_TYPES.join(', ')})" unless ACCEPTED_FILE_TYPES.include? File.extname(path)
65
+ return [path]
66
+ end
67
+ end
68
+
69
+ end
139
70
  end
@@ -1,29 +1,15 @@
1
-
2
1
  module Fame
3
-
2
+
4
3
  # nokogiri_node = original nokogiri node
5
4
  # original_id = F4z-Kg-ni6
6
5
  # vc_name = CustomViewController (optional)
7
6
  # element_name = label
8
7
  # i18n_enabled = true
9
8
  # i18n_comment = "Best label ever invented"
10
- LocalizedNode = Struct.new(:nokogiri_node, :original_id, :vc_name, :element_name, :i18n_enabled, :i18n_comment)
11
-
12
- # node = LocalizedNode
13
- # property = localizable element, e.g. text of a label
14
- # value = localizable strings value (i.e. the translation)
15
- LocalizableStringsEntry = Struct.new(:node, :property, :value) do
16
-
17
- # The formatted .strings file entry
18
- def formatted_strings_file_entry
19
- comment = node.i18n_comment || "No comment provided by engineer."
20
- key = "#{node.original_id}.#{property}"
21
- ["/* #{formatted_info}: #{comment} */", "\"#{key}\" = \"#{value}\";"].join("\n")
22
- end
23
-
24
- # The formatted info of this entry
9
+ LocalizedNode = Struct.new(:nokogiri_node, :original_id, :vc_name, :element_name, :i18n_enabled, :i18n_comment) do
25
10
  def formatted_info
26
- [node.vc_name, node.element_name, property].compact.join(" ")
11
+ info = [vc_name, element_name].compact.join(" ")
12
+ "[#{info}] #{i18n_comment}"
27
13
  end
28
14
  end
29
15
 
@@ -1,3 +1,3 @@
1
1
  module Fame
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1"
3
3
  end
@@ -0,0 +1,46 @@
1
+ require 'pbxplorer' # grab localization languages from .xcproj file
2
+
3
+ module Fame
4
+ # Handles the Xcode Project that is subject to localization
5
+ class XcodeProject
6
+ # All accepted Xcode project file types
7
+ ACCEPTED_FILE_TYPES = [".xcodeproj"].freeze
8
+
9
+ attr_accessor :xcode_proj_path
10
+
11
+ #
12
+ # Initializer
13
+ # @param xcode_proj_path A path to a .xcodeproj file whose contents should be localized.
14
+ #
15
+ def initialize(xcode_proj_path)
16
+ @xcode_proj_path = xcode_proj_path
17
+ validate_xcodeproj_path!
18
+ end
19
+
20
+ #
21
+ # Determines all languages that are used in the current Xcode project.
22
+ # @return [Array<String>] An array of language codes, representing all languages used in the current Xcode project.
23
+ #
24
+ def all_languages
25
+ project_file = XCProjectFile.new(@xcode_proj_path)
26
+ project_file.project["knownRegions"].select { |r| r != "Base" }
27
+ end
28
+
29
+ # TODO
30
+ # def self.determine_xcproj_files!(path = ".")
31
+ # raise "The provided file or folder does not exist" unless File.exist? path
32
+ # end
33
+
34
+ private
35
+
36
+ #
37
+ # Validates the xcodeproj path
38
+ #
39
+ def validate_xcodeproj_path!
40
+ raise "[XcodeProject] No project file provided" unless @xcode_proj_path
41
+ raise "[XcodeProject] The provided file does not exist" unless File.exist? @xcode_proj_path
42
+ raise "[XcodeProject] The provided file is not a valid Xcode project (#{ACCEPTED_FILE_TYPES.join(', ')})" unless ACCEPTED_FILE_TYPES.include? File.extname(@xcode_proj_path)
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,126 @@
1
+ require 'nokogiri' # to rewrite the XLIFF file
2
+ require 'colorize' # colorful console output
3
+ require_relative 'models'
4
+ require_relative 'xcode_project'
5
+
6
+ module Fame
7
+ # Handles import and export of .xliff files
8
+ class XliffExport
9
+
10
+ #
11
+ # Initializer
12
+ # @param xcode_proj_path A path to a .xcodeproj file whose contents should be localized.
13
+ #
14
+ def initialize(xcode_proj_path)
15
+ @xcode_proj = XcodeProject.new(xcode_proj_path)
16
+ end
17
+
18
+ #
19
+ # Exports all .xliff files for the current Xcode project
20
+ # @param path A path to a folder where exported .xliff files should be placed.
21
+ # @param ib_nodes An array of `LocalizedNode`s, generated from `InterfaceBuilder.nodes`.
22
+ #
23
+ def export(path, ib_nodes)
24
+ # export localizations
25
+ export_xliffs(path)
26
+
27
+ # update translation units based on the settings provided in Interface Builder
28
+ # Localizations are only exported if explicitly enabled via the fame Interface Builder integration (see Fame.swift file).
29
+ update_xliff_translation_units(path, ib_nodes)
30
+ end
31
+
32
+ private
33
+
34
+ #
35
+ # Exports all .xliff files for the current Xcode project to the given path.
36
+ # @param path A path to a folder where exported .xliff files should be placed.
37
+ #
38
+ def export_xliffs(path)
39
+ # get all languages that should be exported to separate .xliff files
40
+ languages = @xcode_proj.all_languages
41
+ .map { |l| "-exportLanguage #{l}" }
42
+ .join(" ")
43
+
44
+ `xcodebuild -exportLocalizations -localizationPath #{path} -project #{@xcode_proj.xcode_proj_path} #{languages}`
45
+ end
46
+
47
+ #
48
+ # Modifies all .xliff files based on the settings extracted from Interface Builder nodes.
49
+ #
50
+ def update_xliff_translation_units(path, ib_nodes)
51
+ @xcode_proj.all_languages.each do |language|
52
+ xliff_path = File.join(path, "#{language}.xliff")
53
+ puts "Updating translation units for #{language}".blue
54
+
55
+ # Read XLIFF file
56
+ raise "File #{xliff_path} does not exist" unless File.exists? xliff_path
57
+ doc = read_xliff_file(xliff_path)
58
+
59
+ # Extract all translation units from the xliff
60
+ trans_units = doc.xpath('//xmlns:trans-unit')
61
+
62
+ # Loop over the Interface Builder nodes and update the xliff file based on their settings
63
+ ib_nodes.each do |ib_node|
64
+ # Select nodes connected to original_id
65
+ units = trans_units.select do |u|
66
+ u_id = u["id"] rescue ""
67
+ u_id.start_with?(ib_node.original_id)
68
+ end
69
+
70
+ # Update or remove nodes
71
+ units.each do |unit|
72
+ if ib_node.i18n_enabled
73
+ # Update comment
74
+ comment = unit.xpath("xmlns:note")
75
+ comment.children.first.content = ib_node.formatted_info
76
+ else
77
+ # Remove translation unit, since it should not be translated
78
+ unit.remove
79
+ end
80
+ end
81
+
82
+ # Print status
83
+ if units.count > 0
84
+ status = ib_node.i18n_enabled ? "updated".green : "removed".red
85
+ puts [
86
+ " ✔︎".green,
87
+ "#{units.count} translation unit(s)".black,
88
+ status,
89
+ "for".light_black,
90
+ "#{ib_node.original_id}".black,
91
+ "#{ib_node.formatted_info}".light_black
92
+ ].join(" ")
93
+ end
94
+ end
95
+
96
+ # Write updated XLIFF file to disk
97
+ write_xliff_file(doc, xliff_path)
98
+ end
99
+ end
100
+
101
+ #
102
+ # Reads the document at the given path and parses it into a `Nokogiri` XML doc.
103
+ # @param path The path the xliff file that should be parsed
104
+ # @return [Nokogiri::XML] A `Nokogiri` XML document representing the xliff
105
+ #
106
+ def read_xliff_file(path)
107
+ xliff = File.open(path)
108
+ doc = Nokogiri::XML(xliff)
109
+ xliff.close
110
+
111
+ doc
112
+ end
113
+
114
+ #
115
+ # Writes the given `Nokogiri` doc to the given path
116
+ # @param doc A Nokogiri XML document
117
+ # @param path The path the `doc` should be written to
118
+ #
119
+ def write_xliff_file(doc, path)
120
+ file = File.open(path, "w")
121
+ doc.write_xml_to(file)
122
+ file.close
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,83 @@
1
+ require 'colorize' # colorful console output
2
+ require_relative 'xcode_project'
3
+
4
+ module Fame
5
+ # Handles import and export of .xliff files
6
+ class XliffImport
7
+ # All accepted XLIFF file types
8
+ ACCEPTED_FILE_TYPES = [".xliff"].freeze
9
+
10
+ #
11
+ # Initializer
12
+ # @param xcode_proj_path A path to a .xcodeproj file whose contents should be localized.
13
+ #
14
+ def initialize(xcode_proj_path)
15
+ @xcode_proj = XcodeProject.new(xcode_proj_path)
16
+ end
17
+
18
+ #
19
+ # Imports all .xliff files at the given path into the current Xcode project
20
+ # @param path A folder of .xliff files that should be imported into the current Xcode project.
21
+ #
22
+ def import(path)
23
+ xliffs = determine_xliff_files!(path)
24
+ puts "Found #{xliffs.count} xliff file(s) -> #{xliffs.map { |x| File.basename(x, '.*') }}".light_black
25
+
26
+ errors = []
27
+ xliffs.each_with_index do |xliff, index|
28
+ language = File.basename(xliff, '.*')
29
+ puts "(#{index+1}/#{xliffs.count}) [#{language}] Importing #{xliff}".blue
30
+
31
+ # may result in the following error:
32
+ # xcodebuild: error: Importing localizations from en.xliff will make changes to Example. Import with xcodebuild can only modify existing strings files.
33
+ output = `xcodebuild -importLocalizations -localizationPath #{xliff} -project #{@xcode_proj.xcode_proj_path} 2>&1`
34
+ error = output.split("\n").grep(/^xcodebuild: error: Importing localizations/i)
35
+ errors << error
36
+ end
37
+
38
+ report_result(errors)
39
+ end
40
+
41
+ private
42
+
43
+ #
44
+ # Searches the given path for .xliff files and returns their paths.
45
+ # @param path The path that should be searched for .xliff files.
46
+ # @return [Array<String>] An array of paths to .xliff files.
47
+ #
48
+ def determine_xliff_files!(path)
49
+ raise "[XliffImport] The provided file or folder does not exist" unless File.exist? path
50
+
51
+ if File.directory?(path)
52
+ files = Dir.glob(path + "/**/*{#{ACCEPTED_FILE_TYPES.join(',')}}")
53
+ raise "[XliffImport] The provided folder did not contain any XLIFF files (#{ACCEPTED_FILE_TYPES.join(', ')})" unless files.count > 0
54
+ return files
55
+ else
56
+ raise "[XliffImport] The provided file is not an XLIFF (#{ACCEPTED_FILE_TYPES.join(', ')})" unless ACCEPTED_FILE_TYPES.include? File.extname(path)
57
+ return [path]
58
+ end
59
+ end
60
+
61
+ #
62
+ # Prints the result of the import
63
+ #
64
+ def report_result(errors)
65
+ # handle errors
66
+ if errors.count > 0
67
+ puts "\n✘ XLIFF import failed (#{errors.count} error(s))\n".red
68
+ puts errors
69
+ help = "\nOoops! xcodebuild cannot import one or more of the provided .xliff file(s) because the necessary .strings files do not exist yet.\n\n" +
70
+ "Here's how to fix it:\n" +
71
+ " 1. Open Xcode, select the project root (blue icon)\n" +
72
+ " 2. Choose Editor > Import Localizations...\n" +
73
+ " 3. Repeat steps 1 and 2 for every localization\n\n" +
74
+ "Don't worry, you only have to do this manually once.\n" +
75
+ "After the initial Xcode import, this command will be able to import your xliff files."
76
+ puts help.blue
77
+ else
78
+ puts "\n✔︎ Done importing XLIFFs\n".green
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,110 @@
1
+ require 'colorize' # colorful console output
2
+ require_relative 'xcode_project'
3
+
4
+ module Fame
5
+ # Handles import and export of .xliff files
6
+ class XliffImport
7
+ # All accepted XLIFF file types
8
+ ACCEPTED_FILE_TYPES = [".xliff"].freeze
9
+
10
+ # All required localization files
11
+ # If these files are not present in their respective .lproj folder
12
+ # the `xcodebuild -importLocalizations` command will fail
13
+ REQUIRED_LOCALIZATION_FILES = ["Localizable.strings", "InfoPlist.strings"].freeze
14
+
15
+ #
16
+ # Initializer
17
+ # @param xcode_proj_path A path to a .xcodeproj file whose contents should be localized.
18
+ #
19
+ def initialize(xcode_proj_path)
20
+ @xcode_proj = XcodeProject.new(xcode_proj_path)
21
+ end
22
+
23
+ #
24
+ # Imports all .xliff files at the given path into the current Xcode project
25
+ # @param path A folder of .xliff files that should be imported into the current Xcode project.
26
+ #
27
+ def import(path)
28
+ xliffs = determine_xliff_files!(path)
29
+ puts "Found #{xliffs.count} xliff file(s) -> #{xliffs.map { |x| File.basename(x, '.*') }}".light_black
30
+
31
+ xliffs.each_with_index do |xliff, index|
32
+ language = File.basename(xliff, '.*')
33
+ puts "(#{index+1}/#{xliffs.count}) [#{language}] Importing #{xliff}".blue
34
+
35
+ `xcodebuild -importLocalizations -localizationPath #{xliff} -project #{@xcode_proj.xcode_proj_path}`
36
+ end
37
+ end
38
+
39
+ #
40
+ # Import of .xliff files using `xcodebuild` may fail in case the appropriate
41
+ # .strings file do not already exist.
42
+ # ```
43
+ # xcodebuild: error: Importing localizations from en.xliff will make changes to Example.
44
+ # Import with xcodebuild can only modify existing strings files.
45
+ # Please import with Xcode instead of xcodebuild to make the changes to Example.
46
+ # ```
47
+ #
48
+ # This method creates the missing localization .strings files in order to silence this error.
49
+ #
50
+ def create_missing_strings_files
51
+ # search for Base.lproj folder in project
52
+ project_root = File.expand_path("..", @xcode_proj.xcode_proj_path)
53
+ base_lproj = Dir.glob(project_root + "/**/*{Base.lproj}").first # TODO: ask if multiple...
54
+ raise "There is no Base.lproj folder, please make sure to have a base localization" unless base_lproj
55
+ lproj_root = File.expand_path("..", base_lproj)
56
+
57
+ # collect all names of files that need to be generated in each <language>.lproj folder
58
+ # TODO: not sure if we should determine the project_root like this or kust use an input param from the cli?
59
+ ib_file_names = InterfaceBuilder.determine_ib_files!(project_root)
60
+ .map { |f| File.basename(f, ".*") }
61
+ .map { |f| "#{f}.strings"}
62
+ required_files = REQUIRED_LOCALIZATION_FILES + ib_file_names
63
+
64
+ # Create <language>.lproj files for all languages
65
+ @xcode_proj.all_languages.each do |language|
66
+ # create .lproj folder
67
+ lproj_folder_path = File.join(lproj_root, "#{language}.lproj")
68
+ Dir.mkdir(lproj_folder_path) unless File.exists?(lproj_folder_path)
69
+ puts "Checking for missing .strings files for #{lproj_folder_path}".blue
70
+
71
+ required_files.each do |file_name|
72
+ path = File.join(lproj_folder_path, file_name)
73
+ exists = File.exists?(path)
74
+ File.write(path, "") unless exists
75
+
76
+ puts "#{file_name}".colorize(exists ? :yellow : :green) + " at #{path}".light_black
77
+ end
78
+ end
79
+
80
+ # <ib_file>.strings
81
+ # Localizable.strings
82
+ # InfoPlist.strings
83
+
84
+ # search for Base.lproj
85
+ # create .lproj folders if needed
86
+ # create files for each `string_files` entry
87
+ end
88
+
89
+ private
90
+
91
+ #
92
+ # Searches the given path for .xliff files and returns their paths.
93
+ # @param path The path that should be searched for .xliff files.
94
+ # @return [Array<String>] An array of paths to .xliff files.
95
+ #
96
+ def determine_xliff_files!(path)
97
+ raise "[XliffImport] The provided file or folder does not exist" unless File.exist? path
98
+
99
+ if File.directory?(path)
100
+ files = Dir.glob(path + "/**/*{#{ACCEPTED_FILE_TYPES.join(',')}}")
101
+ raise "[XliffImport] The provided folder did not contain any XLIFF files (#{ACCEPTED_FILE_TYPES.join(', ')})" unless files.count > 0
102
+ return files
103
+ else
104
+ raise "[XliffImport] The provided file is not an XLIFF (#{ACCEPTED_FILE_TYPES.join(', ')})" unless ACCEPTED_FILE_TYPES.include? File.extname(path)
105
+ return [path]
106
+ end
107
+ end
108
+
109
+ end
110
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fame
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: '0.1'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Schuch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-02 00:00:00.000000000 Z
11
+ date: 2016-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: commander
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pbxplorer
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: bundler
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -125,7 +139,11 @@ files:
125
139
  - lib/fame/interface_builder.rb
126
140
  - lib/fame/models.rb
127
141
  - lib/fame/version.rb
128
- homepage: https://twitter.com/schuchalexander
142
+ - lib/fame/xcode_project.rb
143
+ - lib/fame/xliff_export.rb
144
+ - lib/fame/xliff_import.rb
145
+ - lib/fame/xliff_import_old.rb
146
+ homepage: https://github.com/aschuch/fame
129
147
  licenses:
130
148
  - MIT
131
149
  metadata: {}