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 +4 -4
- data/README.md +80 -35
- data/bin/fame +38 -40
- data/lib/fame/interface_builder.rb +63 -132
- data/lib/fame/models.rb +4 -18
- data/lib/fame/version.rb +1 -1
- data/lib/fame/xcode_project.rb +46 -0
- data/lib/fame/xliff_export.rb +126 -0
- data/lib/fame/xliff_import.rb +83 -0
- data/lib/fame/xliff_import_old.rb +110 -0
- metadata +21 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6bd4fe738ca58b090d8fcee6570aa4ba9283febe
|
4
|
+
data.tar.gz: e6a6ac82060e519f446919473164734a576e2e17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f9b0e39bd970cfa26af894e5bf2c049242ea4206a4e94ab49c2bf3d37e6c2e0484cf73f5f388dd31c124b985b6a948d43ded2d183d577e9dca2206e1c9c9b92
|
7
|
+
data.tar.gz: 21eb7470a2acec758812dc8aa182976045932cf07550e90fa6d7f9c150022f6652a0d12a8a8c9202d824ae78ee7eadbbe1af10827d07987582edb6f056519f36
|
data/README.md
CHANGED
@@ -1,12 +1,34 @@
|
|
1
|
-
|
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> •
|
10
|
+
<a href="#fame-features">Fame Features</a> •
|
11
|
+
<a href="#installation">Installation</a> •
|
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
|
-
|
39
|
+
<!-- ![Example of static vs. dynamic (TableViewCell)]() -->
|
18
40
|
|
19
|
-
|
20
|
-
|
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
|
-
```
|
27
|
-
|
28
|
-
|
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
|
-
##
|
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
|
71
|
+
#### Command Line Interface
|
45
72
|
|
46
|
-
Using the fame CLI, developers can export .
|
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
|
-
![
|
75
|
+
![fame CLI](docs/terminal.gif)
|
49
76
|
|
50
|
-
#### Generates beautiful .
|
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 .
|
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
|
-
```
|
55
|
-
|
56
|
-
|
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
|
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 .
|
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
|
-
|
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
|
-
|
110
|
-
|
111
|
-
*
|
112
|
-
*
|
113
|
-
|
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 (
|
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, '
|
21
|
-
default_command :
|
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
|
-
#
|
26
|
+
# Import
|
25
27
|
#
|
26
|
-
command :
|
27
|
-
c.syntax = 'fame
|
28
|
-
c.description = '
|
29
|
-
c.option '--
|
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 :
|
35
|
+
options.default :xliff_path => '.'
|
33
36
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
44
|
-
|
53
|
+
c.action do |args, options|
|
54
|
+
options.default :ib_file_path => '.'
|
55
|
+
options.default :output_path => '.'
|
45
56
|
|
46
|
-
|
47
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
data/lib/fame/models.rb
CHANGED
@@ -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
|
-
[
|
11
|
+
info = [vc_name, element_name].compact.join(" ")
|
12
|
+
"[#{info}] #{i18n_comment}"
|
27
13
|
end
|
28
14
|
end
|
29
15
|
|
data/lib/fame/version.rb
CHANGED
@@ -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.
|
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-
|
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
|
-
|
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: {}
|