fame 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aaa07dd2ecd76f2cf02be0a769acc8fe9f982a3d
4
+ data.tar.gz: d7a888bfbd6a93c52037b6088249b8c116abbab1
5
+ SHA512:
6
+ metadata.gz: 48b11deac64f15a42d05ee456f2e167fc04d352228b6d63e992cfb6c4ac6ad64c34d665826f1a9b09a0db309d94533a7b3140bb7834ddcb0a594ec8e56ffd8ef
7
+ data.tar.gz: 45064b46d5fb314057bde5b2f0e2452b3b82217ef63659736b164865757dd519c3567bd046ff036d4b14a053c6b3ab57aab3b07cf0e7a71dcffd3a8390b5820d
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 Alexander Schuch
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,133 @@
1
+ # Fame
2
+
3
+ Delightful localization of .storyboard and .xib files, right within Interface Builder.
4
+
5
+ Fame makes it easy to enable specific Interface Builder elements to be translated and exported to localizable .strings files.
6
+
7
+ ## Introduction
8
+
9
+ Compared to localization in code (`NSLocalizedString`), Storyboard and XIB localizations are a tedious task for both developers and translators.
10
+
11
+ #### Static vs. dynamic localization
12
+
13
+ Storyboard and XIB files usually contain a mixed set of elements with *static* or *dynamic* text:
14
+ * **Static text**: Elements with fixed localization strings that will never change at runtime
15
+ * **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
+
17
+ **Static text elements should be localized using a generated .strings file, dynamic text elements should be ignored during translation.**
18
+
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.
21
+
22
+ #### Localization comments
23
+
24
+ Storyboard and XIB .stings files generated by Xcode **do not provide a useful comment that provides context to the translator**.
25
+
26
+ ```css
27
+ /* Class = "UILabel"; text = "Label"; ObjectID = "Rfi-2u-xEd"; */
28
+ "Rfi-2u-xEd.text" = "Label";
29
+ ```
30
+
31
+ 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
+
33
+
34
+ ## Why Fame?
35
+
36
+ Fame solves the above mentioned issues to help developers and translators get the most out of tedious localization tasks.
37
+
38
+ #### Integrated into Interface Builder
39
+
40
+ Fame makes it easy for developers to specify which elements of your Storyboard and XIB files should be translated, all right within interface builder. Developers may also add a comment for each element to provide additional context to translators.
41
+
42
+ ![Interface Builder Integration](docs/ib_detail.png)
43
+
44
+ #### Command line interface
45
+
46
+ Using the fame CLI, developers can export .strings files that only contain localizations for elements previously enabled in Interface Builder.
47
+
48
+ ![gif]()
49
+
50
+ #### Generates beautiful .strings files
51
+
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.
53
+
54
+ ```css
55
+ /* MyViewController.label.text: Explains how to purchase a pro subscription. */
56
+ "Rfi-2u-xEd.text" = "Label";
57
+ ```
58
+
59
+ ## Installation
60
+
61
+ Install the fame ruby gem using your terminal to get access to the fame command line interface.
62
+
63
+ ```bash
64
+ $ gem install fame
65
+ ```
66
+
67
+ #### Enable localization for Interface Builder files
68
+
69
+ > **Note**: You may skip to the next section if you have already setup your project and Interface Builder files for localization.
70
+
71
+ First off, add all supported languages to your project. This can be done by clicking the + button under your project's Localizations configuration. Go ahead and add as many languages as you want.
72
+
73
+ ![Add new language to project](docs/add_language.png)
74
+
75
+ Next, make sure to enable localization for your Interface Builder files and choose the "Localizable Strings" option from the dropdown.
76
+
77
+ ![Add new language to project](docs/storyboard_setup.png)
78
+
79
+ You should now have a Base Interface Builder file (e.g. `Main.storyboard`) in a `Base.lproj` folder and multiple localizable strings files (e.g. `Main.strings`) each within a language folder (e.g. `en.lproj` and `de.lproj`).
80
+
81
+ ![Add new language to project](docs/folder_structure.png)
82
+
83
+ That's it, read on to enable fame for localization.
84
+
85
+ #### Setup Fame for Interface Builder
86
+
87
+ 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
+
89
+ ![Interface Builder Integration](docs/ib.png)
90
+
91
+ You can now enable localization for each element you want to have translated.
92
+
93
+
94
+ ## Usage
95
+
96
+
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
+
99
+
100
+ 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
+
102
+
103
+ ```bash
104
+ $ fame
105
+ ```
106
+
107
+ In a nutshell, the fame command does the following:
108
+
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
114
+
115
+ ## Contributing
116
+
117
+ 1. Fork it ( https://github.com/aschuch/fame/fork )
118
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
119
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
120
+ 4. Push to the branch (`git push origin my-new-feature`)
121
+ 5. Create a new Pull Request
122
+
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
+ ## Contact
129
+
130
+ Feel free to get in touch.
131
+
132
+ * Website: <http://schuch.me>
133
+ * Twitter: [@schuchalexander](http://twitter.com/schuchalexander)
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.push File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'rubygems'
6
+ require 'commander'
7
+ require 'colorize'
8
+ require 'fame/version'
9
+ require 'fame/interface_builder'
10
+
11
+ #
12
+ # The Fame CLI
13
+ #
14
+ class FameApplication
15
+ include Commander::Methods
16
+
17
+ def run
18
+ program :name, 'Fame'
19
+ 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
+
23
+ #
24
+ # Default localize command
25
+ #
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.'
30
+
31
+ c.action do |args, options|
32
+ options.default :path => '.'
33
+
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
40
+
41
+ ib = Fame::InterfaceBuilder.new
42
+
43
+ # Generate new localizable.strings file
44
+ strings = ib.generate_localizable_strings(f)
45
+
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)
51
+
52
+ puts "✔︎".green + " Generated strings file at #{strings_file_path(f)}".black
53
+ end
54
+
55
+ puts "-----------------------------------------------------\n".light_black
56
+ end
57
+ end
58
+ end
59
+
60
+ run!
61
+ 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
+ end
77
+
78
+ # run application
79
+ FameApplication.new.run
@@ -0,0 +1,5 @@
1
+ require "fame/version"
2
+
3
+ module Fame
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,139 @@
1
+ require 'nokogiri' # to rewrite the storyboard
2
+ require 'plist' # to parse the localizable.plist file
3
+ require 'colorize' # colorful console output
4
+ require_relative 'models'
5
+
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
139
+ end
@@ -0,0 +1,30 @@
1
+
2
+ module Fame
3
+
4
+ # nokogiri_node = original nokogiri node
5
+ # original_id = F4z-Kg-ni6
6
+ # vc_name = CustomViewController (optional)
7
+ # element_name = label
8
+ # i18n_enabled = true
9
+ # 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
25
+ def formatted_info
26
+ [node.vc_name, node.element_name, property].compact.join(" ")
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,3 @@
1
+ module Fame
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fame
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Schuch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: commander
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: plist
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: colorize
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.10'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.10'
111
+ description: Delightful localization of .storyboard and .xib files, right within Interface
112
+ Builder. Fame makes it easy to enable specific Interface Builder elements to be
113
+ translated and exported to localizable .strings files.
114
+ email:
115
+ - alexander@schuch.me
116
+ executables:
117
+ - fame
118
+ extensions: []
119
+ extra_rdoc_files: []
120
+ files:
121
+ - LICENSE
122
+ - README.md
123
+ - bin/fame
124
+ - lib/fame.rb
125
+ - lib/fame/interface_builder.rb
126
+ - lib/fame/models.rb
127
+ - lib/fame/version.rb
128
+ homepage: https://twitter.com/schuchalexander
129
+ licenses:
130
+ - MIT
131
+ metadata: {}
132
+ post_install_message: |-
133
+ Please add Fame.swift to your project to enable Interface Builder support.
134
+ -> https://github.com/aschuch/fame/blob/master/platform/Fame.swift
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 2.4.2
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: Delightful localization of .storyboard and .xib files, right within Interface
154
+ Builder.
155
+ test_files: []
156
+ has_rdoc: