Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How we achieved Continuous Localization

How we achieved Continuous Localization

Originally posted here: https://speakerdeck.com/huralnyk/how-we-achieved-continuous-localization

This talk's about automating localization workflow at BetterMe iOS projects, using SwiftGen and POEditor.

This talk was made for CocoaHeads Kyiv #14 which took place Oct 6 2018.

CocoaHeads Ukraine

October 06, 2018
Tweet

More Decks by CocoaHeads Ukraine

Other Decks in Programming

Transcript

  1. ! ~ 378.2 millions " ~ 442.3 millions # ~

    329.1 millions $ ~ 908.7 millions TOP-4 NATIVE SPEAKERS
  2. % ~ (40%) & ~ (45%) ' ~ (30%) "

    ~ (60%) TOP-4 LANGUAGES FOR APP TRANSLATION https://www.pangeanic.com/knowledge_center/top-5-languages-to-translate-an-app-for-android-or-iphone
  3. let nextButtonTitle = NSLocalizedString("NEXT", comment: "Title of the next button

    on tooltips screen") TooltipsViewController.swift let gotItButtonTitle = NSLocalizedString(“GOT IT!”, comment: "Title of the last tooltip button") $ genstrings *.swift
  4. /* Title of the next button on tooltips screen */

    “NEXT” = “NEXT”; Base.lproj/Localizable.strings /* Title of the last tooltip button */ “GOT IT!” = “GOT IT!”;
  5. /* Title of the next button on tooltips screen */

    “NEXT” = “NEXT”; Base.lproj/Localizable.strings /* Title of the last tooltip button */ “GOT IT!” = “GOT IT!”; /* Title of the next button on tooltips screen */ “NEXT” = “WEITER”; de.lproj/Localizable.strings /* Title of the last tooltip button */ “GOT IT!” = “VERSTANDEN!”; /* Title of the next button on tooltips screen */ “NEXT” = “SIGUIENTE”; es.lproj/Localizable.strings /* Title of the last tooltip button */ “GOT IT!” = “¡LO TENGO!”;
  6. /* Run a device built in test button title */

    “RUN” = “RUN”; /* Start running activity button title */ “RUN” = “RUN”; let runWearableTestTitle = NSLocalizedString(“RUN", comment: “Run a device built in test button title”) let startRunningTitle = NSLocalizedString(“RUN”, comment: “Start running activity title”)
  7. /* Run a device built in test button title */

    “debug_info.connected_device.button.run_built_in_test” = “RUN”; /* Start running activity button title */ “activities.button.start_run” = “RUN”; enum Localization { enum DebugInfo { enum ConnectedDevice { enum Button { static let run = "debug_info.connected_device.button.run_built_in_test" } } } enum Activities { enum Button { static let run = "activities.button.start_run" } } }
  8. LocalizationService.swift enum Screens { enum Tooltips { enum Button: String

    { case next = “tooltips.button.next” case gotIt = “tooltips.button.got_it” } } } func localizedString<K: RawRepresentable>(_ key: K) -> String where K.RawValue == String { return NSLocalizedString(key.rawValue, comment: "") } let nextButtonTitle = localizedString(Screens.Tooltips.Button.next) let gotItButtonTitle = localizedString(Screens.Tooltips.Button.next)
  9. LocalizationService.swift enum Screens { enum Tooltips { enum Button: String

    { case next = “tootlips.button.next” } } } /* Title of the next button on tooltips screen */ “tooltips.button.next” = “NEXT”; Localizable.strings
  10. PLAN 1. Single source of truth. Generate enum from Localizable.strings

    or vice versa 2. Automate export of string resources 3. Automate import of all translations back to the project
  11. https://serge.io/docs/continuous-localization/ Continuous localization is a way of automatically, seamlessly gathering

    new source material, publishing it for translation, acquiring translations and integrating them back into the product. (c)
  12. SWIFTGEN 1. Assets Catalogs 2. Colors 3. Fonts 4. Interface

    Builder files 5. JSON and YAML files 6. Plists 7. Localizable.strings SwiftGen is a tool to auto-generate Swift code for resources of your projects, to make them type-safe to use.
  13. $ brew update $ brew install swiftgen swiftgen strings -t

    structured-swift4 /path/to/Localizable.strings -o /path/to/ output/Localization.swift
  14. /* $1 - number of training completed, $2 - total

    number of trainings */ "trainings.label.number_of_completed" = "Training %lu out of %lu completed"; Localizable.strings /* Title of the next button on tooltips screen */ “button.next” = “NEXT”; internal enum L10n { internal enum Button { /// NEXT! internal static let next = L10n.tr("Localizable", "button.next") } internal enum Trainings { internal enum Label { /// Training %lu out of %lu completed internal static func numberOfCompleted(_ p1: Int, _ p2: Int) -> String { return L10n.tr("Localizable", "trainings.label.number_of_completed", p1, p2) } } } } Localization.swift
  15. #!/bin/bash if [[ $(git diff --name-only HEAD~1 HEAD | grep

    Base.lproj/Localizable.strings) ]] then curl -X POST https://api.poeditor.com/v2/projects/upload \ -F api_token={YOUR_API_TOKEN} \ -F id={YOUR_PROJECT_ID} \ -F updating="terms_translations" \ -F file=@“BetterMe/Resources/Strings/Base.lproj/Localizable.strings" \ -F language="en" fi export_localization.sh $ sh export_localization.sh
  16. +

  17. def self.run(params) require 'net/http' require 'json' # Make POST request

    to fetch language download link uri = URI("https://api.poeditor.com/v2/projects/export") res = Net::HTTP.post_form(uri, api_token: params[:api_token], id: params[:project_id], language: params[:lang], type: “apple_strings") # Parse download link from response json = JSON.parse(res.body) translation_uri = URI(json["result"]["url"]) # Download transtlation to a file transtalion_res = Net::HTTP.get(translation_uri) file = File.new("#{params[:strings_path]}/#{params[:lang]}.lproj/Localizable.strings", "w") file.puts(transtalion_res) file.close # Print a pretty message about action success flag = params[:lang].upcase.tr('A-Z', "\u{1F1E6}-\u{1F1FF}") UI.success "Successfuly imported #{flag} translation" end download_lang.rb
  18. .env POEDITOR_API_TOKEN = "abc1de3" POEDITOR_PROJECT_ID = "123456" XCODE_STRING_RESOURCES_PATH = “BetterMe/Resources/Strings"

    def self.available_options [ FastlaneCore::ConfigItem.new(key: :api_token, env_name: “POEDITOR_API_TOKEN”), FastlaneCore::ConfigItem.new(key: :project_id, env_name: “POEDITOR_PROJECT_ID”), FastlaneCore::ConfigItem.new(key: :strings_path, env_name: “XCODE_STRING_RESOURCES_PATH”) ] end download_lang.rb
  19. Fastfile default_platform(:ios) platform :ios do desc "Imports translations from POEditor

    to Xcode project" lane :import_translations do download_lang(lang: "de") download_lang(lang: "es") end end
  20. $ fastlane import_translations [23:56:42]: ------------------------------ [23:56:42]: --- Step: default_platform ---

    [23:56:42]: ------------------------------ [23:56:42]: Driving the lane 'ios import_translations' [23:56:42]: --------------------------- [23:56:42]: --- Step: download_lang --- [23:56:42]: --------------------------- [23:56:43]: Successfully imported & translation [23:56:43]: --------------------------- [23:56:43]: --- Step: download_lang --- [23:56:43]: --------------------------- [23:56:43]: Successfully imported " translation [23:56:43]: fastlane.tools finished successfully
  21. SUMMARY 1. Internationalization and localization are different things 2. SwiftGen

    - easy tool to make access to your resources type-safe 3. Automate export of resources to TMS via it’s API or using webhooks 4. Automate import of translations from TMS back to the project using Fastlane or webhooks 5. Don’t automate translations