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.

Db84cf61fdada06b63f43f310b68b462?s=128

CocoaHeads Ukraine

October 06, 2018
Tweet

More Decks by CocoaHeads Ukraine

Other Decks in Programming

Transcript

  1. CONTINUOUS LOCALIZATION HOW WE ACHIEVED

  2. Hello! My name is Lyosha!

  3. None
  4. EPISODES

  5. $ cd /Users/great_automator/scripts

  6. LOCALIZATION

  7. WHY?

  8. None
  9. VIPER ARKit2 CoreML2 CreateML Redux RxSwift MVC KVO UIKit DDD

  10. AppStore available in 155 countries on more than 40 languages

    1.3 Billion Active Devices
  11. None
  12. ! ~ 378.2 millions " ~ 442.3 millions # ~

    329.1 millions $ ~ 908.7 millions TOP-4 NATIVE SPEAKERS
  13. % ~ (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
  14. Test Export Import Translate Internationalize Test

  15. Internationalizing user interface Formatting dates, names, units Support of righ-to-left

    languages Internationalize Test
  16. None
  17. Test Export Import Translate NSLocalizedString $ genstrings *.swift Export/import strings

    as XLIFF file
  18. let nextButtonTitle = "NEXT" TooltipsViewController.swift let gotItButtonTitle = "GOT IT!"

  19. 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
  20. /* Title of the next button on tooltips screen */

    “NEXT” = “NEXT”; Base.lproj/Localizable.strings /* Title of the last tooltip button */ “GOT IT!” = “GOT IT!”;
  21. /* 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!”;
  22. /* 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”)
  23. /* 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" } } }
  24. 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)
  25. 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
  26. None
  27. 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
  28. 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)
  29. SINGLE SOURCE OF TRUTH

  30. 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.
  31. $ brew update $ brew install swiftgen swiftgen strings -t

    structured-swift4 /path/to/Localizable.strings -o /path/to/ output/Localization.swift
  32. /* $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
  33. AUTOMATE EXPORT OF STRING RESOURCES

  34. None
  35. TRANSLATION MANAGEMENT SYSTEM POEditor

  36. OUR TRANSLATION WORKFLOW (

  37. OUR TRANSLATION WORKFLOW ) (

  38. OUR TRANSLATION WORKFLOW )

  39. OUR TRANSLATION WORKFLOW ) * + ,

  40. OUR TRANSLATION WORKFLOW ) ☕

  41. OUR TRANSLATION WORKFLOW ) ( *+, ☕

  42. OUR TRANSLATION WORKFLOW (

  43. OUR TRANSLATION WORKFLOW (

  44. BRUTE-FORCE APPROACH REST + +

  45. #!/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
  46. ZERO-CODE APPROACH

  47. 1 2 3

  48. 1 2 3

  49. AUTOMATE IMPORT OF TRANSLATIONS

  50. +

  51. ACTIONS

  52. $ fastlane new_action download_lang

  53. 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
  54. .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
  55. 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
  56. $ 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
  57. ZERO-CODE APPROACH Notes: 1) The export option can be triggered

    from anywhere, except GitHub.
  58. 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
  59. None
  60. None
  61. THANK YOU!

  62. LINKS https://github.com/Huralnyk/continuous-localization-sample https://github.com/SwiftGen https://poeditor.com https://fastlane.tools https://www.objc.io/issues/9-strings/string-localization/ https://academy.realm.io/posts/altconf-ayaka-nonaka-swift-scripting-redux- localization/ ME facebook.com/alexey.huralnyk

    alexey.huralnyk@gmail.com plierslocalgang.bandcamp.com
  63. QUESTIONS? ISSUES? COMMENTS?