├── Rakefile ├── lib ├── fame │ ├── version.rb │ ├── models.rb │ ├── xcode_project.rb │ ├── interface_builder.rb │ ├── xliff_import.rb │ └── xliff_export.rb └── fame.rb ├── docs ├── ib.png ├── logo.png ├── logo.psd ├── logo.sketch ├── terminal.gif ├── ib_detail.png ├── add_language.png ├── folder_structure.png └── storyboard_setup.png ├── Gemfile ├── platform ├── Example │ ├── Example │ │ ├── en.lproj │ │ │ ├── View.strings │ │ │ └── Main.strings │ │ ├── de.lproj │ │ │ ├── View.strings │ │ │ └── Main.strings │ │ ├── FirstViewController.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Fame.swift │ │ ├── Info.plist │ │ ├── SecondViewController.swift │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ ├── View.xib │ │ │ └── Main.storyboard │ │ └── AppDelegate.swift │ ├── Example.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── project.pbxproj │ ├── ExampleTests │ │ ├── Info.plist │ │ └── ExampleTests.swift │ ├── ExampleUITests │ │ ├── Info.plist │ │ └── ExampleUITests.swift │ ├── de.xliff │ └── en.xliff ├── README.md └── Fame.swift ├── .gitignore ├── LICENSE ├── fame.gemspec ├── bin └── fame └── README.md /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /lib/fame/version.rb: -------------------------------------------------------------------------------- 1 | module Fame 2 | VERSION = "0.1" 3 | end 4 | -------------------------------------------------------------------------------- /docs/ib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/ib.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/logo.png -------------------------------------------------------------------------------- /docs/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/logo.psd -------------------------------------------------------------------------------- /docs/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/logo.sketch -------------------------------------------------------------------------------- /docs/terminal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/terminal.gif -------------------------------------------------------------------------------- /docs/ib_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/ib_detail.png -------------------------------------------------------------------------------- /docs/add_language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/add_language.png -------------------------------------------------------------------------------- /docs/folder_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/folder_structure.png -------------------------------------------------------------------------------- /docs/storyboard_setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschuch/fame/HEAD/docs/storyboard_setup.png -------------------------------------------------------------------------------- /lib/fame.rb: -------------------------------------------------------------------------------- 1 | require "fame/version" 2 | 3 | module Fame 4 | # Your code goes here... 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in fame.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /platform/Example/Example/en.lproj/View.strings: -------------------------------------------------------------------------------- 1 | /* label text: A fancy view in a xib file */ 2 | "Rfi-2u-xEd.text" = "Label in XIB"; -------------------------------------------------------------------------------- /platform/Example/Example/de.lproj/View.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UILabel"; text = "Label in XIB"; ObjectID = "Rfi-2u-xEd"; */ 3 | "Rfi-2u-xEd.text" = "Label in XIB"; 4 | -------------------------------------------------------------------------------- /platform/README.md: -------------------------------------------------------------------------------- 1 | Add the Fame.swift file to your project to enable configuration of localization settings in Interface Builder. 2 | 3 | ![Interface Builder Integration](../docs/ib.png) 4 | 5 | -------------------------------------------------------------------------------- /platform/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /.bundle/ 4 | /.yardoc 5 | /Gemfile.lock 6 | /_yardoc/ 7 | /coverage/ 8 | /doc/ 9 | /pkg/ 10 | /spec/reports/ 11 | /tmp/ 12 | *.bundle 13 | *.so 14 | *.o 15 | *.a 16 | mkmf.log 17 | 18 | ## Build generated 19 | build/ 20 | DerivedData/ 21 | 22 | ## Various settings 23 | *.pbxuser 24 | !default.pbxuser 25 | *.mode1v3 26 | !default.mode1v3 27 | *.mode2v3 28 | !default.mode2v3 29 | *.perspectivev3 30 | !default.perspectivev3 31 | xcuserdata/ 32 | 33 | ## Other 34 | *.moved-aside 35 | *.xccheckout 36 | *.xcscmblueprint 37 | -------------------------------------------------------------------------------- /lib/fame/models.rb: -------------------------------------------------------------------------------- 1 | module Fame 2 | 3 | # nokogiri_node = original nokogiri node 4 | # original_id = F4z-Kg-ni6 5 | # vc_name = CustomViewController (optional) 6 | # element_name = label 7 | # i18n_enabled = true 8 | # i18n_comment = "Best label ever invented" 9 | LocalizedNode = Struct.new(:nokogiri_node, :original_id, :vc_name, :element_name, :i18n_enabled, :i18n_comment) do 10 | def formatted_info 11 | info = [vc_name, element_name].compact.join(" ") 12 | "[#{info}] #{i18n_comment}" 13 | end 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /platform/Fame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fame.swift 3 | // 4 | // Created by Alexander Schuch on 25/02/16. 5 | // Copyright (c) 2016 Alexander Schuch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Enables the fame Interface Builder integration 11 | /// https://github.com/aschuch/fame 12 | extension NSObject { 13 | 14 | @IBInspectable 15 | var i18n_enabled: Bool { 16 | get { return false; } 17 | set { /* do nothing */ } 18 | } 19 | 20 | @IBInspectable 21 | var i18n_comment: String? { 22 | get { return nil; } 23 | set { /* do nothing */ } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /platform/Example/Example/FirstViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirstViewController.swift 3 | // Example 4 | // 5 | // Created by Alexander Schuch on 25/02/16. 6 | // Copyright © 2016 Alexander Schuch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FirstViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /platform/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /platform/Example/ExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /platform/Example/ExampleUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /platform/Example/Example/Fame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtension.swift 3 | // Example 4 | // 5 | // Created by Alexander Schuch on 20/11/14. 6 | // Copyright (c) 2014 Alexander Schuch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSObject { 12 | @IBInspectable 13 | var i18n_enabled: Bool { 14 | get { return false; } 15 | set { /* do nothing */ } 16 | } 17 | 18 | @IBInspectable 19 | var i18n_comment: String? { 20 | get { return nil; } 21 | set { /* do nothing */ } 22 | } 23 | 24 | // @IBInspectable 25 | // var fame_Identifier: String? { 26 | // get { return nil; } 27 | // set { /* do nothing */ } 28 | // } 29 | // 30 | // @IBInspectable 31 | // var fame_Description: String? { 32 | // get { return nil; } 33 | // set { /* do nothing */ } 34 | // } 35 | } -------------------------------------------------------------------------------- /platform/Example/Example/de.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | /* label.text: A fancy Label */ 2 | "R52-bk-905.text" = "Label (de)"; 3 | 4 | /* button.normalTitle: An awesome Button */ 5 | "JOe-j0-s5G.normalTitle" = "Button (de)"; 6 | 7 | /* textField.placeholder: A gorgeous TextField */ 8 | "gwT-Xy-34b.placeholder" = "Placeholder Text (de)"; 9 | 10 | /* textField.text: A gorgeous TextField */ 11 | "gwT-Xy-34b.text" = "Text (de)"; 12 | 13 | /* segmentedControl.segmentTitles[0]: An outstanding SegmentedControl */ 14 | "l0d-C1-RK1.segmentTitles[0]" = "First (de)"; 15 | 16 | /* segmentedControl.segmentTitles[1]: An outstanding SegmentedControl */ 17 | "l0d-C1-RK1.segmentTitles[1]" = "Second (de)"; 18 | 19 | /* barButtonItem.title: An unbelievable BarButtonItem */ 20 | "lFA-pn-E9n.title" = "BarButtonItem (de)"; 21 | 22 | /* navigationItem.title: An uber NavigationItem */ 23 | "4Bk-Mt-SxL.title" = "First ViewController (de)"; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 23 | -------------------------------------------------------------------------------- /platform/Example/ExampleTests/ExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleTests.swift 3 | // ExampleTests 4 | // 5 | // Created by Alexander Schuch on 25/02/16. 6 | // Copyright © 2016 Alexander Schuch. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | class ExampleTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /platform/Example/Example/en.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | /* FirstViewController label text: A fancy Label */ 2 | "R52-bk-905.text" = "Label"; 3 | 4 | /* FirstViewController button normalTitle: An awesome Button */ 5 | "JOe-j0-s5G.normalTitle" = "Button"; 6 | 7 | /* FirstViewController textField placeholder: A gorgeous TextField */ 8 | "gwT-Xy-34b.placeholder" = "Placeholder Text"; 9 | 10 | /* FirstViewController textField text: A gorgeous TextField */ 11 | "gwT-Xy-34b.text" = "Text"; 12 | 13 | /* FirstViewController segmentedControl segmentTitles[0]: An outstanding SegmentedControl */ 14 | "l0d-C1-RK1.segmentTitles[0]" = "First"; 15 | 16 | /* FirstViewController navigationItem title: An uber NavigationItem */ 17 | "4Bk-Mt-SxL.title" = "First ViewController"; 18 | 19 | /* FirstViewController barButtonItem title: An unbelievable BarButtonItem */ 20 | "lFA-pn-E9n.title" = "BarButtonItem"; 21 | 22 | /* FirstViewController segmentedControl segmentTitles[1]: An outstanding SegmentedControl */ 23 | "l0d-C1-RK1.segmentTitles[1]" = "Second"; 24 | 25 | /* SecondViewController label text: A fancy label in a second view controller */ 26 | "uHO-0u-WyN.text" = "Second view controller label"; 27 | 28 | /* SecondViewController navigationItem title: Second navigation item */ 29 | "SAx-7C-11b.title" = "Second ViewController"; -------------------------------------------------------------------------------- /platform/Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /platform/Example/ExampleUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITests.swift 3 | // ExampleUITests 4 | // 5 | // Created by Alexander Schuch on 25/02/16. 6 | // Copyright © 2016 Alexander Schuch. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class ExampleUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /platform/Example/Example/SecondViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecondViewController.swift 3 | // Example 4 | // 5 | // Created by Alexander Schuch on 01/03/16. 6 | // Copyright © 2016 Alexander Schuch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SecondViewController: UIViewController { 12 | 13 | @IBOutlet var dynamicLabel: UILabel? 14 | @IBOutlet var dynamicLabel2: UILabel? 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | dynamicLabel?.text = NSLocalizedString("secondViewController.dynamicLabel.text", value: "First Dynamic Label Text", comment: "First dynamic text, should be short") 20 | dynamicLabel2?.text = NSLocalizedString("secondViewController.dynamicLabel2.text", value: "Second Dynamic Label Text", comment: "Second dynamic text, can be longer") 21 | } 22 | 23 | override func didReceiveMemoryWarning() { 24 | super.didReceiveMemoryWarning() 25 | // Dispose of any resources that can be recreated. 26 | } 27 | 28 | 29 | /* 30 | // MARK: - Navigation 31 | 32 | // In a storyboard-based application, you will often want to do a little preparation before navigation 33 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 34 | // Get the new view controller using segue.destinationViewController. 35 | // Pass the selected object to the new view controller. 36 | } 37 | */ 38 | 39 | } 40 | -------------------------------------------------------------------------------- /fame.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'fame/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "fame" 8 | spec.version = Fame::VERSION 9 | spec.authors = ["Alexander Schuch"] 10 | spec.email = ["alexander@schuch.me"] 11 | spec.summary = %q{Delightful localization of .storyboard and .xib files, right within Interface Builder.} 12 | spec.description = %q{Delightful localization of .storyboard and .xib files, right within Interface Builder. Fame makes it easy to enable specific Interface Builder elements to be translated and exported to localizable .strings files.} 13 | spec.homepage = "https://github.com/aschuch/fame" 14 | spec.license = "MIT" 15 | 16 | spec.files = Dir["lib/**/*"] + %w{ bin/fame README.md LICENSE } 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.post_install_message = "Please add Fame.swift to your project to enable Interface Builder support.\n-> https://github.com/aschuch/fame/blob/master/platform/Fame.swift" 22 | 23 | spec.add_dependency "commander", "~> 4.0" 24 | spec.add_dependency "nokogiri", "~> 1.0" 25 | spec.add_dependency "plist", "~> 3.0" 26 | spec.add_dependency "colorize", "~> 0.7" 27 | spec.add_dependency "pbxplorer", "~> 1.0" 28 | 29 | spec.add_development_dependency "bundler", "~> 1.7" 30 | spec.add_development_dependency "rake", "~> 10.0" 31 | spec.add_development_dependency "pry", "~> 0.10" 32 | end 33 | -------------------------------------------------------------------------------- /lib/fame/xcode_project.rb: -------------------------------------------------------------------------------- 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] 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 47 | -------------------------------------------------------------------------------- /platform/Example/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /platform/Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Alexander Schuch on 25/02/16. 6 | // Copyright © 2016 Alexander Schuch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /platform/Example/Example/Base.lproj/View.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /bin/fame: -------------------------------------------------------------------------------- 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 | require 'fame/xliff_import' 11 | require 'fame/xliff_export' 12 | 13 | # 14 | # The Fame CLI 15 | # 16 | class FameApplication 17 | include Commander::Methods 18 | 19 | def run 20 | program :name, 'Fame' 21 | program :version, Fame::VERSION 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 24 | 25 | # 26 | # Import 27 | # 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.' 33 | 34 | c.action do |args, options| 35 | options.default :xliff_path => '.' 36 | 37 | # Import .xliff files 38 | xliff = Fame::XliffImport.new(options.project) 39 | xliff.import(options.xliff_path) 40 | end 41 | end 42 | 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.' 52 | 53 | c.action do |args, options| 54 | options.default :ib_file_path => '.' 55 | options.default :output_path => '.' 56 | 57 | ib_files = Fame::InterfaceBuilder.determine_ib_files!(options.ib_file_path) 58 | puts "Found #{ib_files.count} file(s) to localize".light_black 59 | 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 65 | 66 | # Generate XLIFF translation files 67 | xliff = Fame::XliffExport.new(options.project) 68 | xliff.export(options.output_path, ib_nodes) 69 | end 70 | end 71 | 72 | run! 73 | end 74 | end 75 | 76 | # run application 77 | FameApplication.new.run 78 | -------------------------------------------------------------------------------- /lib/fame/interface_builder.rb: -------------------------------------------------------------------------------- 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 | 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] 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] 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 70 | end 71 | -------------------------------------------------------------------------------- /lib/fame/xliff_import.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' # colorful console output 2 | require 'open3' # capture stdout, stderr for commandline calls 3 | require_relative 'xcode_project' 4 | 5 | 6 | module Fame 7 | # Handles import and export of .xliff files 8 | class XliffImport 9 | # All accepted XLIFF file types 10 | ACCEPTED_FILE_TYPES = [".xliff"].freeze 11 | 12 | # 13 | # Initializer 14 | # @param xcode_proj_path A path to a .xcodeproj file whose contents should be localized. 15 | # 16 | def initialize(xcode_proj_path) 17 | @xcode_proj = XcodeProject.new(xcode_proj_path) 18 | end 19 | 20 | # 21 | # Imports all .xliff files at the given path into the current Xcode project 22 | # @param path A folder of .xliff files that should be imported into the current Xcode project. 23 | # 24 | def import(path) 25 | xliffs = determine_xliff_files!(path) 26 | puts "Found #{xliffs.count} xliff file(s) -> #{xliffs.map { |x| File.basename(x, '.*') }}".light_black 27 | 28 | errors = [] 29 | xliffs.each_with_index do |xliff, index| 30 | language = File.basename(xliff, '.*') 31 | puts "\n(#{index+1}/#{xliffs.count}) [#{language}] Importing #{xliff}".blue 32 | 33 | # may result in the following error: 34 | # xcodebuild: error: Importing localizations from en.xliff will make changes to Example. Import with xcodebuild can only modify existing strings files. 35 | command = "xcodebuild -importLocalizations -localizationPath \"#{xliff}\" -project \"#{@xcode_proj.xcode_proj_path}\"" 36 | stdout, stderr, status = Open3.capture3(command) 37 | 38 | if status.success? 39 | puts stdout.light_black 40 | puts "✔︎ Successfully imported #{language}".green 41 | else 42 | puts stderr.light_black 43 | puts "✘ Failed to import #{language}".red 44 | # grep the error specific to the initial import issue of xcodebuild 45 | error = stdout.split("\n").grep(/^xcodebuild: error: Importing localizations/i) 46 | errors << error 47 | end 48 | end 49 | 50 | report_result(errors) 51 | end 52 | 53 | private 54 | 55 | # 56 | # Searches the given path for .xliff files and returns their paths. 57 | # @param path The path that should be searched for .xliff files. 58 | # @return [Array] An array of paths to .xliff files. 59 | # 60 | def determine_xliff_files!(path) 61 | raise "[XliffImport] The provided file or folder does not exist" unless File.exist? path 62 | 63 | if File.directory?(path) 64 | files = Dir.glob(path + "/**/*{#{ACCEPTED_FILE_TYPES.join(',')}}") 65 | raise "[XliffImport] The provided folder did not contain any XLIFF files (#{ACCEPTED_FILE_TYPES.join(', ')})" unless files.count > 0 66 | return files 67 | else 68 | raise "[XliffImport] The provided file is not an XLIFF (#{ACCEPTED_FILE_TYPES.join(', ')})" unless ACCEPTED_FILE_TYPES.include? File.extname(path) 69 | return [path] 70 | end 71 | end 72 | 73 | # 74 | # Prints the result of the import 75 | # 76 | def report_result(errors) 77 | # handle errors 78 | if errors.count > 0 79 | 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" + 80 | "Here's how to fix it:\n" + 81 | " 1. Open Xcode, select the project root (blue icon)\n" + 82 | " 2. Choose Editor > Import Localizations...\n" + 83 | " 3. Repeat steps 1 and 2 for every localization\n\n" + 84 | "Don't worry, you only have to do this manually once.\n" + 85 | "After the initial Xcode import, this command will be able to import your xliff files." 86 | puts help.blue 87 | else 88 | puts "\n✔︎ Done importing XLIFFs\n".green 89 | end 90 | end 91 | 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/fame/xliff_export.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' # to rewrite the XLIFF file 2 | require 'colorize' # colorful console output 3 | require 'open3' # to capture stdout, stderr when calling external xcodebuild 4 | require_relative 'models' 5 | require_relative 'xcode_project' 6 | 7 | module Fame 8 | # Handles import and export of .xliff files 9 | class XliffExport 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 = XcodeProject.new(xcode_proj_path) 17 | end 18 | 19 | # 20 | # Exports all .xliff files for the current Xcode project 21 | # @param path A path to a folder where exported .xliff files should be placed. 22 | # @param ib_nodes An array of `LocalizedNode`s, generated from `InterfaceBuilder.nodes`. 23 | # 24 | def export(path, ib_nodes) 25 | # export localizations 26 | export_xliffs(path) 27 | 28 | # update translation units based on the settings provided in Interface Builder 29 | # Localizations are only exported if explicitly enabled via the fame Interface Builder integration (see Fame.swift file). 30 | update_xliff_translation_units(path, ib_nodes) 31 | end 32 | 33 | private 34 | 35 | # 36 | # Exports all .xliff files for the current Xcode project to the given path. 37 | # @param path A path to a folder where exported .xliff files should be placed. 38 | # 39 | def export_xliffs(path) 40 | # get all languages that should be exported to separate .xliff files 41 | languages = @xcode_proj.all_languages 42 | .map { |l| "-exportLanguage #{l}" } 43 | .join(" ") 44 | 45 | command = "xcodebuild -exportLocalizations -localizationPath \"#{path}\" -project \"#{@xcode_proj.xcode_proj_path}\" #{languages}" 46 | stdout, stderr, status = Open3.capture3(command) 47 | 48 | puts stdout.light_black 49 | puts(stderr.yellow) unless status.success? 50 | end 51 | 52 | # 53 | # Modifies all .xliff files based on the settings extracted from Interface Builder nodes. 54 | # 55 | def update_xliff_translation_units(path, ib_nodes) 56 | @xcode_proj.all_languages.each do |language| 57 | xliff_path = File.join(path, "#{language}.xliff") 58 | puts "Updating translation units for #{language}".blue 59 | 60 | # Read XLIFF file 61 | raise "File #{xliff_path} does not exist" unless File.exists? xliff_path 62 | doc = read_xliff_file(xliff_path) 63 | 64 | # Extract all translation units from the xliff 65 | trans_units = doc.xpath('//xmlns:trans-unit') 66 | 67 | # Loop over the Interface Builder nodes and update the xliff file based on their settings 68 | ib_nodes.each do |ib_node| 69 | # Select nodes connected to original_id 70 | units = trans_units.select do |u| 71 | u_id = u["id"] rescue "" 72 | u_id.start_with?(ib_node.original_id) 73 | end 74 | 75 | # Update or remove nodes 76 | units.each do |unit| 77 | if ib_node.i18n_enabled 78 | # Update comment 79 | comment = unit.xpath("xmlns:note") 80 | comment.children.first.content = ib_node.formatted_info 81 | else 82 | # Remove translation unit, since it should not be translated 83 | unit.remove 84 | end 85 | end 86 | 87 | # Print status 88 | if units.count > 0 89 | status = ib_node.i18n_enabled ? "updated".green : "removed".red 90 | puts [ 91 | " ✔︎".green, 92 | "#{units.count} translation unit(s)".black, 93 | status, 94 | "for".light_black, 95 | "#{ib_node.original_id}".black, 96 | "#{ib_node.formatted_info}".light_black 97 | ].join(" ") 98 | end 99 | end 100 | 101 | # Write updated XLIFF file to disk 102 | write_xliff_file(doc, xliff_path) 103 | end 104 | end 105 | 106 | # 107 | # Reads the document at the given path and parses it into a `Nokogiri` XML doc. 108 | # @param path The path the xliff file that should be parsed 109 | # @return [Nokogiri::XML] A `Nokogiri` XML document representing the xliff 110 | # 111 | def read_xliff_file(path) 112 | xliff = File.open(path) 113 | doc = Nokogiri::XML(xliff) 114 | xliff.close 115 | 116 | doc 117 | end 118 | 119 | # 120 | # Writes the given `Nokogiri` doc to the given path 121 | # @param doc A Nokogiri XML document 122 | # @param path The path the `doc` should be written to 123 | # 124 | def write_xliff_file(doc, path) 125 | file = File.open(path, "w") 126 | doc.write_xml_to(file) 127 | file.close 128 | end 129 | 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /platform/Example/de.xliff: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | 14 | 15 | First ViewController 16 | First ViewController (de) 17 | [FirstViewController navigationItem] An uber NavigationItem 18 | 19 | 20 | Button 21 | Button (de) 22 | [FirstViewController button] An awesome Button 23 | 24 | 25 | 26 | Label 27 | Label (de) 28 | [FirstViewController label] A fancy Label 29 | 30 | 31 | Second ViewController 32 | [SecondViewController navigationItem] Second navigation item 33 | 34 | 35 | Placeholder Text 36 | Placeholder Text (de) 37 | [FirstViewController textField] A gorgeous TextField 38 | 39 | 40 | Text 41 | Text (de) 42 | [FirstViewController textField] A gorgeous TextField 43 | 44 | 45 | First 46 | First (de) 47 | [FirstViewController segmentedControl] An outstanding SegmentedControl 48 | 49 | 50 | Second 51 | Second (de) 52 | [FirstViewController segmentedControl] An outstanding SegmentedControl 53 | 54 | 55 | BarButtonItem 56 | BarButtonItem (de) 57 | [FirstViewController barButtonItem] An unbelievable BarButtonItem 58 | 59 | 60 | Dynamic label 2 61 | Class = "UILabel"; text = "Dynamic label 2"; ObjectID = "si2-WH-Hr5"; 62 | 63 | 64 | Second view controller label 65 | [SecondViewController label] A fancy label in a second view controller 66 | 67 | 68 |
69 | 70 |
71 | 72 |
73 | 74 | 75 | Label in XIB 76 | Label in XIB 77 | [label] A fancy view in a xib file 78 | 79 | 80 |
81 | 82 |
83 | 84 |
85 | 86 | 87 | $(PRODUCT_NAME) 88 | 89 | 90 | 1.0 91 | 92 | 93 |
94 | 95 |
96 | 97 |
98 | 99 | 100 | First Dynamic Label Text 101 | First dynamic text, should be short 102 | 103 | 104 | Second Dynamic Label Text 105 | Second dynamic text, can be longer 106 | 107 | 108 |
109 | 110 |
111 | 112 |
113 | 114 | 115 | $(PRODUCT_NAME) 116 | 117 | 118 | 1.0 119 | 120 | 121 |
122 | 123 |
124 | 125 |
126 | 127 | 128 | $(PRODUCT_NAME) 129 | 130 | 131 | 1.0 132 | 133 | 134 |
135 |
136 | -------------------------------------------------------------------------------- /platform/Example/en.xliff: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | 14 | 15 | First ViewController 16 | First ViewController 17 | [FirstViewController navigationItem] An uber NavigationItem 18 | 19 | 20 | Button 21 | Button 22 | [FirstViewController button] An awesome Button 23 | 24 | 25 | 26 | Label 27 | Label 28 | [FirstViewController label] A fancy Label 29 | 30 | 31 | Second ViewController 32 | Second ViewController 33 | [SecondViewController navigationItem] Second navigation item 34 | 35 | 36 | Placeholder Text 37 | Placeholder Text 38 | [FirstViewController textField] A gorgeous TextField 39 | 40 | 41 | Text 42 | Text 43 | [FirstViewController textField] A gorgeous TextField 44 | 45 | 46 | First 47 | First 48 | [FirstViewController segmentedControl] An outstanding SegmentedControl 49 | 50 | 51 | Second 52 | Second 53 | [FirstViewController segmentedControl] An outstanding SegmentedControl 54 | 55 | 56 | BarButtonItem 57 | BarButtonItem 58 | [FirstViewController barButtonItem] An unbelievable BarButtonItem 59 | 60 | 61 | Dynamic label 2 62 | Dynamic label 2 63 | Class = "UILabel"; text = "Dynamic label 2"; ObjectID = "si2-WH-Hr5"; 64 | 65 | 66 | Second view controller label 67 | Second view controller label 68 | [SecondViewController label] A fancy label in a second view controller 69 | 70 | 71 |
72 | 73 |
74 | 75 |
76 | 77 | 78 | Label in XIB 79 | Label in XIB 80 | [label] A fancy view in a xib file 81 | 82 | 83 |
84 | 85 |
86 | 87 |
88 | 89 | 90 | $(PRODUCT_NAME) 91 | $(PRODUCT_NAME) 92 | 93 | 94 | 1.0 95 | 1.0 96 | 97 | 98 |
99 | 100 |
101 | 102 |
103 | 104 | 105 | First Dynamic Label Text 106 | First Dynamic Label Text 107 | First dynamic text, should be short 108 | 109 | 110 | Second Dynamic Label Text 111 | Second Dynamic Label Text 112 | Second dynamic text, can be longer 113 | 114 | 115 |
116 | 117 |
118 | 119 |
120 | 121 | 122 | $(PRODUCT_NAME) 123 | $(PRODUCT_NAME) 124 | 125 | 126 | 1.0 127 | 1.0 128 | 129 | 130 |
131 | 132 |
133 | 134 |
135 | 136 | 137 | $(PRODUCT_NAME) 138 | $(PRODUCT_NAME) 139 | 140 | 141 | 1.0 142 | 1.0 143 | 144 | 145 |
146 |
147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Fame logo](docs/logo.png) 2 | 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 |

9 | Introduction • 10 | Fame Features • 11 | Installation • 12 | Usage 13 |

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 | * 🚀 26 | 27 | 28 | ## Introduction 29 | 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. 32 | 33 | #### Static vs. dynamic localization 34 | 35 | Storyboard and XIB files usually contain a mixed set of elements with *static* or *dynamic* text: 36 | * **Static text**: Elements with fixed localization strings that will never change at runtime 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. 38 | 39 | 40 | 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. 45 | 46 | #### Localization comments 47 | 48 | Storyboard and XIB .stings files generated by Xcode **do not provide a useful comment that provides context to the translator**. 49 | 50 | ```xml 51 | 52 | A fancy Label 53 | A fancy Label 54 | Class = "UILabel"; text = "A fancy Label"; ObjectID = "si2-WH-Hr5"; <-- This is not so helpful 🙄 55 | 56 | ``` 57 | 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. 59 | 60 | 61 | ## Fame Features 62 | 63 | Fame solves the above mentioned issues to help developers and translators get the most out of tedious localization tasks. 64 | 65 | #### Integrated into Interface Builder 66 | 67 | 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. 68 | 69 | ![Interface Builder Integration](docs/ib_detail.png) 70 | 71 | #### Command Line Interface 72 | 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. 74 | 75 | ![fame CLI](docs/terminal.gif) 76 | 77 | #### Generates beautiful .xliff files 78 | 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. 80 | 81 | ```xml 82 | 83 | A fancy Label 84 | A fancy Label 85 | Explains to the customer how to purchase a pro subscription. Make it catchy. <-- Ahh, much better 😍 86 | 87 | ``` 88 | 89 | ## Installation 90 | 91 | Install the fame ruby gem using your terminal to get access to the fame command line interface. 92 | 93 | ```bash 94 | $ gem install fame 95 | ``` 96 | 97 | #### Enable localization for Interface Builder files 98 | 99 | > **Note**: You may skip to the next section if you have already setup your project and Interface Builder files for localization. 100 | 101 | 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. 102 | 103 | ![Add new language to project](docs/add_language.png) 104 | 105 | Next, make sure to enable localization for your Interface Builder files and choose the "Localizable Strings" option from the dropdown. 106 | 107 | ![Add new language to project](docs/storyboard_setup.png) 108 | 109 | 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`). 110 | 111 | ![Add new language to project](docs/folder_structure.png) 112 | 113 | That's it, read on to enable fame for localization. 114 | 115 | #### Setup [Fame.swift](platform/Fame.swift) Interface Builder integration 116 | 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. 118 | 119 | ![Interface Builder Integration](docs/ib.png) 120 | 121 | You can now enable localization for each element you want to have translated. 122 | 123 | ## Usage 124 | 125 | ### Export 126 | 127 | Once all localizable elements have been configured in Interface Builder, you can export the localizable .xliff file using the `fame` command line tool. 128 | 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 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 150 | 151 | ```bash 152 | $ fame import --project Example.xcodeproj [--xliff-path] 153 | ``` 154 | 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. 157 | 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 ☕️ 164 | 165 | ## Contributing 166 | 167 | 1. Fork it (https://github.com/aschuch/fame/fork) 168 | 2. Create your feature branch (`git checkout -b my-new-feature`) 169 | 3. Commit your changes (`git commit -am 'Add some feature'`) 170 | 4. Push to the branch (`git push origin my-new-feature`) 171 | 5. Create a new Pull Request 172 | 173 | ## Contact 174 | 175 | Feel free to get in touch. 176 | 177 | * Website: 178 | * Twitter: [@schuchalexander](http://twitter.com/schuchalexander) 179 | -------------------------------------------------------------------------------- /platform/Example/Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 30 | 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 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 116 | 126 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /platform/Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4D01B0B81C7EFB0F00AAD90C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D01B0B71C7EFB0F00AAD90C /* AppDelegate.swift */; }; 11 | 4D01B0BA1C7EFB0F00AAD90C /* FirstViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D01B0B91C7EFB0F00AAD90C /* FirstViewController.swift */; }; 12 | 4D01B0BF1C7EFB1000AAD90C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4D01B0BE1C7EFB1000AAD90C /* Assets.xcassets */; }; 13 | 4D01B0CD1C7EFB1000AAD90C /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D01B0CC1C7EFB1000AAD90C /* ExampleTests.swift */; }; 14 | 4D01B0D81C7EFB1000AAD90C /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D01B0D71C7EFB1000AAD90C /* ExampleUITests.swift */; }; 15 | 4D01B0F81C7F15D500AAD90C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4D01B0F41C7F15D500AAD90C /* LaunchScreen.storyboard */; }; 16 | 4D01B0F91C7F15D500AAD90C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4D01B0F61C7F15D500AAD90C /* Main.storyboard */; }; 17 | 4D8D894B1C8073FE004DD23A /* View.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4D8D89491C8073FE004DD23A /* View.xib */; }; 18 | 4DA64ECD1C85E53A00894E22 /* SecondViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DA64ECC1C85E53A00894E22 /* SecondViewController.swift */; }; 19 | 4DE85E1D1C8478D50073D5A0 /* Fame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DE85E1C1C8478D50073D5A0 /* Fame.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 4D01B0C91C7EFB1000AAD90C /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 4D01B0AC1C7EFB0F00AAD90C /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 4D01B0B31C7EFB0F00AAD90C; 28 | remoteInfo = Example; 29 | }; 30 | 4D01B0D41C7EFB1000AAD90C /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 4D01B0AC1C7EFB0F00AAD90C /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 4D01B0B31C7EFB0F00AAD90C; 35 | remoteInfo = Example; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 4D01B0B41C7EFB0F00AAD90C /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 4D01B0B71C7EFB0F00AAD90C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | 4D01B0B91C7EFB0F00AAD90C /* FirstViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstViewController.swift; sourceTree = ""; }; 43 | 4D01B0BE1C7EFB1000AAD90C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 44 | 4D01B0C31C7EFB1000AAD90C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | 4D01B0C81C7EFB1000AAD90C /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 4D01B0CC1C7EFB1000AAD90C /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; 47 | 4D01B0CE1C7EFB1000AAD90C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 4D01B0D31C7EFB1000AAD90C /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 4D01B0D71C7EFB1000AAD90C /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; 50 | 4D01B0D91C7EFB1000AAD90C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 4D01B0F71C7F15D500AAD90C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 52 | 4D01B0FF1C7F15E700AAD90C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; 53 | 4D01B1011C7F15E800AAD90C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Main.strings; sourceTree = ""; }; 54 | 4D01B1041C7F160000AAD90C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 55 | 4D8D894A1C8073FE004DD23A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/View.xib; sourceTree = ""; }; 56 | 4D8D894D1C80743A004DD23A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/View.strings; sourceTree = ""; }; 57 | 4D8D894F1C80743C004DD23A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/View.strings; sourceTree = ""; }; 58 | 4DA64ECC1C85E53A00894E22 /* SecondViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = ""; }; 59 | 4DE85E1C1C8478D50073D5A0 /* Fame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Fame.swift; path = ../../Fame.swift; sourceTree = ""; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | 4D01B0B11C7EFB0F00AAD90C /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | 4D01B0C51C7EFB1000AAD90C /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | 4D01B0D01C7EFB1000AAD90C /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | 4D01B0AB1C7EFB0F00AAD90C = { 88 | isa = PBXGroup; 89 | children = ( 90 | 4D01B0B61C7EFB0F00AAD90C /* Example */, 91 | 4D01B0CB1C7EFB1000AAD90C /* ExampleTests */, 92 | 4D01B0D61C7EFB1000AAD90C /* ExampleUITests */, 93 | 4D01B0B51C7EFB0F00AAD90C /* Products */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 4D01B0B51C7EFB0F00AAD90C /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 4D01B0B41C7EFB0F00AAD90C /* Example.app */, 101 | 4D01B0C81C7EFB1000AAD90C /* ExampleTests.xctest */, 102 | 4D01B0D31C7EFB1000AAD90C /* ExampleUITests.xctest */, 103 | ); 104 | name = Products; 105 | sourceTree = ""; 106 | }; 107 | 4D01B0B61C7EFB0F00AAD90C /* Example */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 4D01B0B71C7EFB0F00AAD90C /* AppDelegate.swift */, 111 | 4D01B0B91C7EFB0F00AAD90C /* FirstViewController.swift */, 112 | 4DA64ECC1C85E53A00894E22 /* SecondViewController.swift */, 113 | 4DE85E1C1C8478D50073D5A0 /* Fame.swift */, 114 | 4D01B0F61C7F15D500AAD90C /* Main.storyboard */, 115 | 4D8D89491C8073FE004DD23A /* View.xib */, 116 | 4D01B0F41C7F15D500AAD90C /* LaunchScreen.storyboard */, 117 | 4D01B0BE1C7EFB1000AAD90C /* Assets.xcassets */, 118 | 4D01B0C31C7EFB1000AAD90C /* Info.plist */, 119 | ); 120 | path = Example; 121 | sourceTree = ""; 122 | }; 123 | 4D01B0CB1C7EFB1000AAD90C /* ExampleTests */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 4D01B0CC1C7EFB1000AAD90C /* ExampleTests.swift */, 127 | 4D01B0CE1C7EFB1000AAD90C /* Info.plist */, 128 | ); 129 | path = ExampleTests; 130 | sourceTree = ""; 131 | }; 132 | 4D01B0D61C7EFB1000AAD90C /* ExampleUITests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 4D01B0D71C7EFB1000AAD90C /* ExampleUITests.swift */, 136 | 4D01B0D91C7EFB1000AAD90C /* Info.plist */, 137 | ); 138 | path = ExampleUITests; 139 | sourceTree = ""; 140 | }; 141 | /* End PBXGroup section */ 142 | 143 | /* Begin PBXNativeTarget section */ 144 | 4D01B0B31C7EFB0F00AAD90C /* Example */ = { 145 | isa = PBXNativeTarget; 146 | buildConfigurationList = 4D01B0DC1C7EFB1000AAD90C /* Build configuration list for PBXNativeTarget "Example" */; 147 | buildPhases = ( 148 | 4D01B0B01C7EFB0F00AAD90C /* Sources */, 149 | 4D01B0B11C7EFB0F00AAD90C /* Frameworks */, 150 | 4D01B0B21C7EFB0F00AAD90C /* Resources */, 151 | ); 152 | buildRules = ( 153 | ); 154 | dependencies = ( 155 | ); 156 | name = Example; 157 | productName = Example; 158 | productReference = 4D01B0B41C7EFB0F00AAD90C /* Example.app */; 159 | productType = "com.apple.product-type.application"; 160 | }; 161 | 4D01B0C71C7EFB1000AAD90C /* ExampleTests */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = 4D01B0DF1C7EFB1000AAD90C /* Build configuration list for PBXNativeTarget "ExampleTests" */; 164 | buildPhases = ( 165 | 4D01B0C41C7EFB1000AAD90C /* Sources */, 166 | 4D01B0C51C7EFB1000AAD90C /* Frameworks */, 167 | 4D01B0C61C7EFB1000AAD90C /* Resources */, 168 | ); 169 | buildRules = ( 170 | ); 171 | dependencies = ( 172 | 4D01B0CA1C7EFB1000AAD90C /* PBXTargetDependency */, 173 | ); 174 | name = ExampleTests; 175 | productName = ExampleTests; 176 | productReference = 4D01B0C81C7EFB1000AAD90C /* ExampleTests.xctest */; 177 | productType = "com.apple.product-type.bundle.unit-test"; 178 | }; 179 | 4D01B0D21C7EFB1000AAD90C /* ExampleUITests */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = 4D01B0E21C7EFB1000AAD90C /* Build configuration list for PBXNativeTarget "ExampleUITests" */; 182 | buildPhases = ( 183 | 4D01B0CF1C7EFB1000AAD90C /* Sources */, 184 | 4D01B0D01C7EFB1000AAD90C /* Frameworks */, 185 | 4D01B0D11C7EFB1000AAD90C /* Resources */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | 4D01B0D51C7EFB1000AAD90C /* PBXTargetDependency */, 191 | ); 192 | name = ExampleUITests; 193 | productName = ExampleUITests; 194 | productReference = 4D01B0D31C7EFB1000AAD90C /* ExampleUITests.xctest */; 195 | productType = "com.apple.product-type.bundle.ui-testing"; 196 | }; 197 | /* End PBXNativeTarget section */ 198 | 199 | /* Begin PBXProject section */ 200 | 4D01B0AC1C7EFB0F00AAD90C /* Project object */ = { 201 | isa = PBXProject; 202 | attributes = { 203 | LastSwiftUpdateCheck = 0720; 204 | LastUpgradeCheck = 0720; 205 | ORGANIZATIONNAME = "Alexander Schuch"; 206 | TargetAttributes = { 207 | 4D01B0B31C7EFB0F00AAD90C = { 208 | CreatedOnToolsVersion = 7.2.1; 209 | }; 210 | 4D01B0C71C7EFB1000AAD90C = { 211 | CreatedOnToolsVersion = 7.2.1; 212 | TestTargetID = 4D01B0B31C7EFB0F00AAD90C; 213 | }; 214 | 4D01B0D21C7EFB1000AAD90C = { 215 | CreatedOnToolsVersion = 7.2.1; 216 | TestTargetID = 4D01B0B31C7EFB0F00AAD90C; 217 | }; 218 | }; 219 | }; 220 | buildConfigurationList = 4D01B0AF1C7EFB0F00AAD90C /* Build configuration list for PBXProject "Example" */; 221 | compatibilityVersion = "Xcode 3.2"; 222 | developmentRegion = English; 223 | hasScannedForEncodings = 0; 224 | knownRegions = ( 225 | en, 226 | Base, 227 | de, 228 | ); 229 | mainGroup = 4D01B0AB1C7EFB0F00AAD90C; 230 | productRefGroup = 4D01B0B51C7EFB0F00AAD90C /* Products */; 231 | projectDirPath = ""; 232 | projectRoot = ""; 233 | targets = ( 234 | 4D01B0B31C7EFB0F00AAD90C /* Example */, 235 | 4D01B0C71C7EFB1000AAD90C /* ExampleTests */, 236 | 4D01B0D21C7EFB1000AAD90C /* ExampleUITests */, 237 | ); 238 | }; 239 | /* End PBXProject section */ 240 | 241 | /* Begin PBXResourcesBuildPhase section */ 242 | 4D01B0B21C7EFB0F00AAD90C /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | 4D8D894B1C8073FE004DD23A /* View.xib in Resources */, 247 | 4D01B0F91C7F15D500AAD90C /* Main.storyboard in Resources */, 248 | 4D01B0BF1C7EFB1000AAD90C /* Assets.xcassets in Resources */, 249 | 4D01B0F81C7F15D500AAD90C /* LaunchScreen.storyboard in Resources */, 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | }; 253 | 4D01B0C61C7EFB1000AAD90C /* Resources */ = { 254 | isa = PBXResourcesBuildPhase; 255 | buildActionMask = 2147483647; 256 | files = ( 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | }; 260 | 4D01B0D11C7EFB1000AAD90C /* Resources */ = { 261 | isa = PBXResourcesBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | /* End PBXResourcesBuildPhase section */ 268 | 269 | /* Begin PBXSourcesBuildPhase section */ 270 | 4D01B0B01C7EFB0F00AAD90C /* Sources */ = { 271 | isa = PBXSourcesBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | 4DA64ECD1C85E53A00894E22 /* SecondViewController.swift in Sources */, 275 | 4DE85E1D1C8478D50073D5A0 /* Fame.swift in Sources */, 276 | 4D01B0BA1C7EFB0F00AAD90C /* FirstViewController.swift in Sources */, 277 | 4D01B0B81C7EFB0F00AAD90C /* AppDelegate.swift in Sources */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | 4D01B0C41C7EFB1000AAD90C /* Sources */ = { 282 | isa = PBXSourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | 4D01B0CD1C7EFB1000AAD90C /* ExampleTests.swift in Sources */, 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | }; 289 | 4D01B0CF1C7EFB1000AAD90C /* Sources */ = { 290 | isa = PBXSourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | 4D01B0D81C7EFB1000AAD90C /* ExampleUITests.swift in Sources */, 294 | ); 295 | runOnlyForDeploymentPostprocessing = 0; 296 | }; 297 | /* End PBXSourcesBuildPhase section */ 298 | 299 | /* Begin PBXTargetDependency section */ 300 | 4D01B0CA1C7EFB1000AAD90C /* PBXTargetDependency */ = { 301 | isa = PBXTargetDependency; 302 | target = 4D01B0B31C7EFB0F00AAD90C /* Example */; 303 | targetProxy = 4D01B0C91C7EFB1000AAD90C /* PBXContainerItemProxy */; 304 | }; 305 | 4D01B0D51C7EFB1000AAD90C /* PBXTargetDependency */ = { 306 | isa = PBXTargetDependency; 307 | target = 4D01B0B31C7EFB0F00AAD90C /* Example */; 308 | targetProxy = 4D01B0D41C7EFB1000AAD90C /* PBXContainerItemProxy */; 309 | }; 310 | /* End PBXTargetDependency section */ 311 | 312 | /* Begin PBXVariantGroup section */ 313 | 4D01B0F41C7F15D500AAD90C /* LaunchScreen.storyboard */ = { 314 | isa = PBXVariantGroup; 315 | children = ( 316 | 4D01B1041C7F160000AAD90C /* Base */, 317 | ); 318 | name = LaunchScreen.storyboard; 319 | sourceTree = ""; 320 | }; 321 | 4D01B0F61C7F15D500AAD90C /* Main.storyboard */ = { 322 | isa = PBXVariantGroup; 323 | children = ( 324 | 4D01B0F71C7F15D500AAD90C /* Base */, 325 | 4D01B0FF1C7F15E700AAD90C /* en */, 326 | 4D01B1011C7F15E800AAD90C /* de */, 327 | ); 328 | name = Main.storyboard; 329 | sourceTree = ""; 330 | }; 331 | 4D8D89491C8073FE004DD23A /* View.xib */ = { 332 | isa = PBXVariantGroup; 333 | children = ( 334 | 4D8D894A1C8073FE004DD23A /* Base */, 335 | 4D8D894D1C80743A004DD23A /* en */, 336 | 4D8D894F1C80743C004DD23A /* de */, 337 | ); 338 | name = View.xib; 339 | sourceTree = ""; 340 | }; 341 | /* End PBXVariantGroup section */ 342 | 343 | /* Begin XCBuildConfiguration section */ 344 | 4D01B0DA1C7EFB1000AAD90C /* Debug */ = { 345 | isa = XCBuildConfiguration; 346 | buildSettings = { 347 | ALWAYS_SEARCH_USER_PATHS = NO; 348 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 349 | CLANG_CXX_LIBRARY = "libc++"; 350 | CLANG_ENABLE_MODULES = YES; 351 | CLANG_ENABLE_OBJC_ARC = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_CONSTANT_CONVERSION = YES; 354 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 355 | CLANG_WARN_EMPTY_BODY = YES; 356 | CLANG_WARN_ENUM_CONVERSION = YES; 357 | CLANG_WARN_INT_CONVERSION = YES; 358 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 362 | COPY_PHASE_STRIP = NO; 363 | DEBUG_INFORMATION_FORMAT = dwarf; 364 | ENABLE_STRICT_OBJC_MSGSEND = YES; 365 | ENABLE_TESTABILITY = YES; 366 | GCC_C_LANGUAGE_STANDARD = gnu99; 367 | GCC_DYNAMIC_NO_PIC = NO; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_OPTIMIZATION_LEVEL = 0; 370 | GCC_PREPROCESSOR_DEFINITIONS = ( 371 | "DEBUG=1", 372 | "$(inherited)", 373 | ); 374 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 375 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 376 | GCC_WARN_UNDECLARED_SELECTOR = YES; 377 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 378 | GCC_WARN_UNUSED_FUNCTION = YES; 379 | GCC_WARN_UNUSED_VARIABLE = YES; 380 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 381 | MTL_ENABLE_DEBUG_INFO = YES; 382 | ONLY_ACTIVE_ARCH = YES; 383 | SDKROOT = iphoneos; 384 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 385 | }; 386 | name = Debug; 387 | }; 388 | 4D01B0DB1C7EFB1000AAD90C /* Release */ = { 389 | isa = XCBuildConfiguration; 390 | buildSettings = { 391 | ALWAYS_SEARCH_USER_PATHS = NO; 392 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 393 | CLANG_CXX_LIBRARY = "libc++"; 394 | CLANG_ENABLE_MODULES = YES; 395 | CLANG_ENABLE_OBJC_ARC = YES; 396 | CLANG_WARN_BOOL_CONVERSION = YES; 397 | CLANG_WARN_CONSTANT_CONVERSION = YES; 398 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 399 | CLANG_WARN_EMPTY_BODY = YES; 400 | CLANG_WARN_ENUM_CONVERSION = YES; 401 | CLANG_WARN_INT_CONVERSION = YES; 402 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 403 | CLANG_WARN_UNREACHABLE_CODE = YES; 404 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 405 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 406 | COPY_PHASE_STRIP = NO; 407 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 408 | ENABLE_NS_ASSERTIONS = NO; 409 | ENABLE_STRICT_OBJC_MSGSEND = YES; 410 | GCC_C_LANGUAGE_STANDARD = gnu99; 411 | GCC_NO_COMMON_BLOCKS = YES; 412 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 413 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 414 | GCC_WARN_UNDECLARED_SELECTOR = YES; 415 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 416 | GCC_WARN_UNUSED_FUNCTION = YES; 417 | GCC_WARN_UNUSED_VARIABLE = YES; 418 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 419 | MTL_ENABLE_DEBUG_INFO = NO; 420 | SDKROOT = iphoneos; 421 | VALIDATE_PRODUCT = YES; 422 | }; 423 | name = Release; 424 | }; 425 | 4D01B0DD1C7EFB1000AAD90C /* Debug */ = { 426 | isa = XCBuildConfiguration; 427 | buildSettings = { 428 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 429 | INFOPLIST_FILE = Example/Info.plist; 430 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 431 | PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.Example; 432 | PRODUCT_NAME = "$(TARGET_NAME)"; 433 | }; 434 | name = Debug; 435 | }; 436 | 4D01B0DE1C7EFB1000AAD90C /* Release */ = { 437 | isa = XCBuildConfiguration; 438 | buildSettings = { 439 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 440 | INFOPLIST_FILE = Example/Info.plist; 441 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 442 | PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.Example; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | }; 445 | name = Release; 446 | }; 447 | 4D01B0E01C7EFB1000AAD90C /* Debug */ = { 448 | isa = XCBuildConfiguration; 449 | buildSettings = { 450 | BUNDLE_LOADER = "$(TEST_HOST)"; 451 | INFOPLIST_FILE = ExampleTests/Info.plist; 452 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 453 | PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.ExampleTests; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 456 | }; 457 | name = Debug; 458 | }; 459 | 4D01B0E11C7EFB1000AAD90C /* Release */ = { 460 | isa = XCBuildConfiguration; 461 | buildSettings = { 462 | BUNDLE_LOADER = "$(TEST_HOST)"; 463 | INFOPLIST_FILE = ExampleTests/Info.plist; 464 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 465 | PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.ExampleTests; 466 | PRODUCT_NAME = "$(TARGET_NAME)"; 467 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 468 | }; 469 | name = Release; 470 | }; 471 | 4D01B0E31C7EFB1000AAD90C /* Debug */ = { 472 | isa = XCBuildConfiguration; 473 | buildSettings = { 474 | INFOPLIST_FILE = ExampleUITests/Info.plist; 475 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 476 | PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.ExampleUITests; 477 | PRODUCT_NAME = "$(TARGET_NAME)"; 478 | TEST_TARGET_NAME = Example; 479 | USES_XCTRUNNER = YES; 480 | }; 481 | name = Debug; 482 | }; 483 | 4D01B0E41C7EFB1000AAD90C /* Release */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | INFOPLIST_FILE = ExampleUITests/Info.plist; 487 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 488 | PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.ExampleUITests; 489 | PRODUCT_NAME = "$(TARGET_NAME)"; 490 | TEST_TARGET_NAME = Example; 491 | USES_XCTRUNNER = YES; 492 | }; 493 | name = Release; 494 | }; 495 | /* End XCBuildConfiguration section */ 496 | 497 | /* Begin XCConfigurationList section */ 498 | 4D01B0AF1C7EFB0F00AAD90C /* Build configuration list for PBXProject "Example" */ = { 499 | isa = XCConfigurationList; 500 | buildConfigurations = ( 501 | 4D01B0DA1C7EFB1000AAD90C /* Debug */, 502 | 4D01B0DB1C7EFB1000AAD90C /* Release */, 503 | ); 504 | defaultConfigurationIsVisible = 0; 505 | defaultConfigurationName = Release; 506 | }; 507 | 4D01B0DC1C7EFB1000AAD90C /* Build configuration list for PBXNativeTarget "Example" */ = { 508 | isa = XCConfigurationList; 509 | buildConfigurations = ( 510 | 4D01B0DD1C7EFB1000AAD90C /* Debug */, 511 | 4D01B0DE1C7EFB1000AAD90C /* Release */, 512 | ); 513 | defaultConfigurationIsVisible = 0; 514 | defaultConfigurationName = Release; 515 | }; 516 | 4D01B0DF1C7EFB1000AAD90C /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { 517 | isa = XCConfigurationList; 518 | buildConfigurations = ( 519 | 4D01B0E01C7EFB1000AAD90C /* Debug */, 520 | 4D01B0E11C7EFB1000AAD90C /* Release */, 521 | ); 522 | defaultConfigurationIsVisible = 0; 523 | defaultConfigurationName = Release; 524 | }; 525 | 4D01B0E21C7EFB1000AAD90C /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { 526 | isa = XCConfigurationList; 527 | buildConfigurations = ( 528 | 4D01B0E31C7EFB1000AAD90C /* Debug */, 529 | 4D01B0E41C7EFB1000AAD90C /* Release */, 530 | ); 531 | defaultConfigurationIsVisible = 0; 532 | defaultConfigurationName = Release; 533 | }; 534 | /* End XCConfigurationList section */ 535 | }; 536 | rootObject = 4D01B0AC1C7EFB0F00AAD90C /* Project object */; 537 | } 538 | --------------------------------------------------------------------------------