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

iOSDC2019 多言語対応と戦う2019年版

5405b3d871f38164ef419e95e9467197?s=47 Hideki Matsuoka
September 07, 2019

iOSDC2019 多言語対応と戦う2019年版

多言語対応と戦う2019年版 (2019/09/07 14:20〜 Track B)

スライド: https://speakerdeck.com/matsuokah/iosdc2019-duo-yan-yu-dui-ying-tozhan-u2019nian-ban

サンプルコード: https://github.com/matsuokah/GetBetterLocalize
サンプルコードはXcode11でも動きます

#iosdc #b

5405b3d871f38164ef419e95e9467197?s=128

Hideki Matsuoka

September 07, 2019
Tweet

Transcript

  1. 多⾔語対応と戦う 2019年版

  2. https://github.com/matsuokah/GetBetterLocalize サンプルコード

  3. • まつおか(@matsuokah_) • タップル誕⽣の開発 • 最近、料理にはまっています ⾃⼰紹介

  4. 技術書典7でXcodeGen導⼊ガイドについて書いてます

  5. 技術書典7でXcodeGen導⼊ガイドについて書いてます あ12c

  6. • 基礎は「多⾔語対応のススメ」 • ローカライズの基本 • ローカライズ実践テクニック

  7. • 基礎は「多⾔語対応のススメ」 • ローカライズの基本 • ローカライズ実践テクニック

  8. • 複数形対応(stringsdict) • RtL • ⾔語が変わることによる表⽰崩れ • 単位(重さ、⻑さ、通貨、温度)や時間への対応 • ユニットテスト

    iOSDC 「多⾔語対応のススメ」 ikkan_chin
  9. • 基礎は「多⾔語対応のススメ」 • ローカライズの基本 • ローカライズ実践テクニック

  10. 実践に移る前に 最低限の基本を紹介

  11. まずは⽂⾔を .strignsファイル化

  12. ローカライズの第⼀歩 “top.title” = “こんにちは、世界!”; “top.title” = “Hello, World!”; English Japanese

  13. None
  14. 基本的なローカライズはこちら

  15. 複数形、画⾯サイズ、 デバイス依存はこちら

  16. Stringsファイル

  17. None
  18. None
  19. 全ての⽂⾔を集約することがスタート label.text = "͜Μʹͪ͸ɺੈք" コードで 直接指定している⽂⾔

  20. label.text = "͜Μʹͪ͸ɺੈք" Storyboard/InterfaceBuilder で直接⼊⼒している⽂⾔ 全ての⽂⾔を集約することがスタート

  21. 第1⾔語だけでも Localizable.stringsに書き出そう

  22. • 基礎は「多⾔語対応のススメ」 • ローカライズの基本 • ローカライズと実践テクニック

  23. 基本だけでは 対応できないケースが多々

  24. どう進めていくか? という話をします

  25. おすすめ度 .stringsファイルのキーに ネームスペースを使おう ★★★☆☆

  26. ネームスペース “top.description.label.text” = “こんにちは、世界!”; top description text / title 画⾯

    / コンポーネント 役割 句読点あり→ text 句読点なし → title label UI
  27. • OK, NG, YES, NO, 了解, 完了などコンポーネント 化で利⽤されやすいものがおすすめ • 抜き出した単語を組み合わせて⽂章を作るのはNG

    • キーの接頭辞に”common”をつけて、組み合わせ NGというルールを作ったりすればいい 共通の単語を抜き出す
  28. 単語分けすぎ %d個 あなたはリンゴを 持っています %d You have apples 分けすぎるとどうなる?

  29. コンテキストを知らないと 翻訳できない %d個 あなたはリンゴを 持っています %d You have apples

  30. あなたはリンゴを%d個もっています You have %d apple

  31. ネームスペース “top.description.label.text” = “こんにちは、世界!”; top description text / title 画⾯

    / コンポーネント 役割 句読点あり→ text 句読点なし → title label UI ① キーのコンフリクト対策 ② キーから⽤途を明確に
  32. この⽂⾔、、、対応漏れ?って思ったら ローカライズファイルを コンパイル対象から外してみよう おすすめ度 ★★★☆☆

  33. テキストが 表⽰されている場合 対応漏れ or 外部から取得した⽂字 or 画像

  34. .stringsファイルに 書き出し終わったら いよいよ実践!

  35. .stringsを タイプセーフに扱おう おすすめ度 ★★★★☆

  36. • SwiftGenやR.swiftを使いましょう • SwiftGenを採⽤。Stencilというテンプレート ベースなので、表現がフレキシブル。 • SwiftGenでも複数形を扱うことはできます ⽂⾔取得をタイプセーフに

  37. SwiftGenによるアセットのタイプセーフ化 “auth.password_reset.title” = “Ϧηοτ”; strings: - inputs: path/to/Base.lproj/Localizable.strings outputs: -

    templateName: structured-swift4 output: path/to/Gens/Strings.swift Localizable.strings swiftgen.yml
  38. 書き出す CocoaPodsでインストールしている場合 Pods/SwiftGen/binに実⾏ファイルがあります

  39. 書き出し結果 enum L10n { enum Auth { enum PasswordReset {

    static let title = L10n.tr(…) } } } path/to/Gens/Strings.swift “auth.password_reset.title” = “Ϧηοτ”; Localizable.strings
  40. 書き出し結果 enum L10n { enum Auth { enum PasswordReset {

    static let title = L10n.tr(…) } } } path/to/Gens/Strings.swift “auth.password_reset.title” = “Ϧηοτ”; Localizable.strings L n.Auth.PasswordRest.title で、「パスワードリセット」を解決できる
  41. ⽂⾔がない時、 気づけるようになった!

  42. デバッグと本番⽤⽂⾔を分離しよう おすすめ度 ★★★☆☆

  43. デバッグ⽂⾔を.strings化 Localizable+Debug.strings swiftgen.yml “debug.login.title” = “σόοάϩάΠϯ”; strings: ɹ- inputs: Base.lproj/Localizable+Debug.strings

    outputs: - templateName: structured-swift4 output: path/to/Gens/Localizable+Debug.swift
  44. デバッグ⽂⾔を.strings化 Localizable+Debug.strings swiftgen.yml “debug.login.title” = “σόοάϩάΠϯ”; strings: ɹ- inputs: Base.lproj/Localizable+Debug.strings

    outputs: - templateName: structured-swift4 output: path/to/Gens/Localizable+Debug.swift ファイルを分けてコンパイル対象外にしやすくする
  45. コンパイル対象外とは?

  46. xcconfigもしくはprojectで EXCLUDED_SOURCE_FILE_NAMESに除外設定 を追加する 除外設定 EXCLUDED_SOURCE_FILE_NAMES = *Mock.* *Debug.* ☝MockやDebugが含まれる名前のファイルを全て、 コンパイル対象から除外する設定

  47. Xcodeで設定する場合

  48. 書き出し結果 “debug.login.title” = “σόοάϩάΠϯ”; Localizable+Debug.strings enum L10n { enum Debug

    { enum Login { static let title = L10n.tr(…) } } } Localizable+Debug.swift
  49. 書き出し結果 “debug.login.title” = “σόοάϩάΠϯ”; Localizable+Debug.strings enum L10n { enum Debug

    { enum Login { static let title = L10n.tr(…) } } } Localizable+Debug.swift enumが競合する
  50. SwiftGenのstencilを カスタムしよう!

  51. • SwiftGenで使われているテンプレートエンジン • SwiftGenは.stringsファイルやAssetsなどの構 造をパースして、StencilでSwiftファイルを書き 出している Stencilとは

  52. ベースのL nを定義 enum L10n { static func tr(…) -> String

    { let format = NSLocalizedString(…) return String(format: format, …) } } Strings.swift
  53. ベースのL nを定義 enum L10n { static func tr(…) -> String

    { let format = NSLocalizedString(…) return String(format: format, …) } } Strings.swift ⽂⾔を解決する関数
  54. プリセットをコピーする {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} {{accessModifier}} enum {{enumName}}

    { {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} {{accessModifier}} extension {{enumName}} { structured-swift .stencil strings.stencil structured-swift .stencilをコピーして、 extensionに変更
  55. strings: - inputs: Base.lproj/Localizable.strings outputs: - templatePath: strings.stencil output: path/to/Gens/Strings.swift

    strings: - inputs: Base.lproj/Localizable.strings outputs: - templateName: structured-swift4 output: path/to/Gens/Strings.swift swiftgen.yml(Before) stencilのパスを指定 swiftgen.yml(After)
  56. 書き出し結果 extension L10n { enum Debug { enum Login {

    … } } } extension L10n { enum Auth { enum PasswordReset { … } } } Strings+Localizable+Debug.swift Strings+Localizable.swift
  57. 絶対に動くデモ①

  58. デバッグの⽂⾔が 流出しにくくなった!

  59. アプリで⾔語を 切り替えられるようにしよう おすすめ度 ★★★★☆

  60. • テスト/デバッグしやすさの向上 • iOS 以降ではiOSの機能として盛り込まれる • iOS 以前では UserDefault.standard.array(forKey: “AppleLanguages”)を変更することで再起動時に⾔

    語を切り替えることが可能 アプリの⾔語を変更する
  61. • 取得 UserDefault.standard.array(forKey: “AppleLanguages”) [String]で定義されている。[“en”, “ja-JP”]のような形。 定義されている⾔語は設定アプリで追加されている⾔語 配列の先頭の⾔語コードが起動時に適⽤される • 変更

    [“ja-JP”, “en”]に変更すれば、強制的に⽇本語に切り替え可能に。 [“zh-Hans”, “ja-JP”, “en”]のように、新たな⾔語を追加すれば、それも適⽤される • リセット UserDefaults.standard.removeObject(forKey: “AppleLanguages") アプリの⾔語を変更する
  62. コード @IBAction func applyEnglish(_ sender: Any) { applyLanguage(code: "en") }

    @IBAction func applyJapanese(_ sender: Any) { applyLanguage(code: "ja") } @IBAction func applyChinese(_ sender: Any) { applyLanguage(code: "zh-Hans") } @IBAction func applyDeviceDependency(_ sender: Any) { UserDefaults.standard.removeObject(forKey: "AppleLanguages") } func applyLanguage(code: String) { let language = Locale.preferredLanguages.filter { $0.contains(code) }.first guard let selectedLanguage = language else { UserDefaults.standard.set([code] + Locale.preferredLanguages, forKey: "AppleLanguages") return } let other = Locale.preferredLanguages.filter { $0 != selectedLanguage } UserDefaults.standard.set([selectedLanguage] + other, forKey: "AppleLanguages") }
  63. 絶対に動くデモ②

  64. レイアウトが 確認しやすくなった

  65. iOS のアプリ⾔語切り替えも 同じメカニズム

  66. 余談 複数形対応

  67. 同じキーが定義され ている場合 ①Stringsdict ②Strings の順に読みこまれる

  68. let text = L10n.Top.Apples.text(apples) // Alias to stringsdict "top.apples.text" =

    "You have %d apples"; SwiftGenで enumだけ作っておく Localizable.strings Swift
  69. // Alias to stringsdict "top.apples.text" = "You have %d apples";

    Localizable.strings Localizable.stringsdict
  70. // Alias to stringsdict "top.apples.text" = "You have %d apples";

    Localizable.strings Localizable.stringsdict キーが⼀致してれば stringsdictの⽅が採⽤さ れる
  71. ※ワークアラウンドは ほどほどに

  72. ⽂字の装飾もローカライズしよう おすすめ度 ★★★★☆

  73. あなたはリンゴを5個もっています You have Apples

  74. あなたはリンゴを5個もっています You have Apples ⾚く太字 ⾔語によって 異なる装飾範囲

  75. attributedText.addAttributes([ .foregroundColor: UIColor.red, .font: UIFont.boldSystemFont(ofSize: 18)], range: NSRange(location: 8, length:

    2)) NSRangeを直指定は絶対ダメ!
  76. None
  77. 英語は全滅

  78. stringsdictを使った場合 0の時も装飾されてしまう

  79. NSRangeが⽂字数の範囲外だと 強制終了します

  80. "top.apples.text" = "͋ͳͨ͸ϦϯΰΛ%dݸ͍࣋ͬͯ·͢ɻ"; "top.apples.text.decoration" = "%dݸ"; "top.apples.text" = "You have

    %d apples"; "top.apples.text.decoration" = "%d apples"; English Japanese
  81. "top.apples.text" = "͋ͳͨ͸ϦϯΰΛ%dݸ͍࣋ͬͯ·͢ɻ"; "top.apples.text.decoration" = "%dݸ"; "top.apples.text" = "You have

    %d apples"; "top.apples.text.decoration" = "%d apples"; English Japanese text.range(of:decoration) で装飾範囲を解決する
  82. 絶対に動くデモ③

  83. 注意! 繰り返しは未対応 String.range(of)のファーストヒットのみ

  84. おすすめ度 コスト ★★★★☆ ★★★☆☆ 翻訳SaaSの導⼊で 翻訳者にプルリク出してもらおう

  85. Crowdinを使う

  86. None
  87. GitHub連携対応 リポジトリ内にある 翻訳元のテキストファイルを指定

  88. None
  89. 翻訳リストが作成されて

  90. 翻訳すると

  91. None
  92. プルリクがでる

  93. 翻訳者さんにアカウントを発⾏すれば Gitが使えなくてもプルリクがだせる✌

  94. 画像をローカライズしよう おすすめ度★★★★★

  95. 出典: https://developer.apple.com/videos/play/wwdc / /

  96. 出典: https://developer.apple.com/videos/play/wwdc / / WWDC で発表されました

  97. 出典: https://developer.apple.com/videos/play/wwdc / / “同名で”⾔語毎にアセットを⽤意できる

  98. SwiftGenでLocalized Assetも扱えます Xcode で試したら、 対応⾔語のアセットが選ばれました

  99. 結構やってきたけど まだまだ⾜りない…

  100. 装飾したいがために 1つの⽂章でラベルが分かれてる問題

  101. ⾔語によって語順が変わってしまう SaaSを使うと前後の⽂脈が抜けがち

  102. ⾔語によって、単位変わる問題

  103. i nとL nはなんだかんだ切り離せない

  104. ⾔語によって⻑さが変わってしまっ て

  105. 繁体字、⽇本語は短い⽅ ロシア語やフランス語に対応すると⻑くなる

  106. 固有名詞の直訳問題

  107. 直訳すると、異⽂化に 刺さらないワードになることも

  108. 翻訳が必要なのは アプリ内だけじゃない!

  109. ストア掲載⽂⾔、パーミッション許可⽂⾔、 Settings.bundleなどなど

  110. まだまだ俺たちの L nそして、i nは続く!

  111. 同じ課題をお持ちの⽅, これから多⾔語対応を控えている⽅ 懇親会‧Ask the Speakerで 話しましょう!

  112. • 多⾔語対応のススメ( , ikkan_chin) • Strings Dict Format(Apple) • i

    n and L n videos(Apple) • Internationalization(Apple) • SwiftGen(GitHub) • サンプルプロジェクト(GitHub, matsuokah) • Crowdin(ローカライズSaaS, Crowdin) 参考⽂献, リンク
  113. • https://github.com/taggon/highlight ソースコードハイライトツール

  114. ご静聴ありがとうございました