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. CONTINUOUS LOCALIZATION
    HOW WE ACHIEVED

    View Slide

  2. Hello! My name is Lyosha!

    View Slide

  3. View Slide

  4. EPISODES

    View Slide

  5. $ cd /Users/great_automator/scripts

    View Slide

  6. LOCALIZATION

    View Slide

  7. WHY?

    View Slide

  8. View Slide

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

    View Slide

  10. AppStore available in 155 countries on more than 40 languages
    1.3 Billion Active Devices

    View Slide

  11. View Slide

  12. ! ~ 378.2 millions
    " ~ 442.3 millions
    # ~ 329.1 millions
    $ ~ 908.7 millions
    TOP-4 NATIVE SPEAKERS

    View Slide

  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

    View Slide

  14. Test
    Export
    Import
    Translate
    Internationalize Test

    View Slide

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

    View Slide

  16. View Slide

  17. Test
    Export
    Import
    Translate
    NSLocalizedString
    $ genstrings *.swift
    Export/import strings as XLIFF file

    View Slide

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

    View Slide

  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

    View Slide

  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!”;

    View Slide

  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!”;

    View Slide

  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”)

    View Slide

  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"
    }
    }
    }

    View Slide

  24. LocalizationService.swift
    enum Screens {
    enum Tooltips {
    enum Button: String {
    case next = “tooltips.button.next”
    case gotIt = “tooltips.button.got_it”
    }
    }
    }
    func localizedString(_ 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)

    View Slide

  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

    View Slide

  26. View Slide

  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

    View Slide

  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)

    View Slide

  29. SINGLE SOURCE OF TRUTH

    View Slide

  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.

    View Slide

  31. $ brew update
    $ brew install swiftgen
    swiftgen strings -t structured-swift4 /path/to/Localizable.strings -o /path/to/
    output/Localization.swift

    View Slide

  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

    View Slide

  33. AUTOMATE EXPORT OF
    STRING RESOURCES

    View Slide

  34. View Slide

  35. TRANSLATION MANAGEMENT SYSTEM
    POEditor

    View Slide

  36. OUR TRANSLATION WORKFLOW
    (

    View Slide

  37. OUR TRANSLATION WORKFLOW
    )
    (

    View Slide

  38. OUR TRANSLATION WORKFLOW
    )

    View Slide

  39. OUR TRANSLATION WORKFLOW
    )
    *
    +
    ,

    View Slide

  40. OUR TRANSLATION WORKFLOW
    ) ☕

    View Slide

  41. OUR TRANSLATION WORKFLOW
    )
    (
    *+,

    View Slide

  42. OUR TRANSLATION WORKFLOW
    (

    View Slide

  43. OUR TRANSLATION WORKFLOW
    (

    View Slide

  44. BRUTE-FORCE APPROACH
    REST
    + +

    View Slide

  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 fi[email protected]“BetterMe/Resources/Strings/Base.lproj/Localizable.strings" \
    -F language="en"

    export_localization.sh
    $ sh export_localization.sh

    View Slide

  46. ZERO-CODE APPROACH

    View Slide

  47. 1
    2
    3

    View Slide

  48. 1
    2
    3

    View Slide

  49. AUTOMATE IMPORT
    OF TRANSLATIONS

    View Slide

  50. +

    View Slide

  51. ACTIONS

    View Slide

  52. $ fastlane new_action download_lang

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  57. ZERO-CODE APPROACH
    Notes: 1) The export option can be triggered from anywhere, except GitHub.

    View Slide

  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

    View Slide

  59. View Slide

  60. View Slide

  61. THANK YOU!

    View Slide

  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
    [email protected]
    plierslocalgang.bandcamp.com

    View Slide

  63. QUESTIONS? ISSUES? COMMENTS?

    View Slide