Slide 1

Slide 1 text

多⾔語対応と戦う 2019年版

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

基本的なローカライズはこちら

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Stringsファイル

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

ネームスペース “top.description.label.text” = “こんにちは、世界!”; top description text / title 画⾯ / コンポーネント 役割 句読点あり→ text 句読点なし → title label UI

Slide 27

Slide 27 text

• OK, NG, YES, NO, 了解, 完了などコンポーネント 化で利⽤されやすいものがおすすめ • 抜き出した単語を組み合わせて⽂章を作るのはNG • キーの接頭辞に”common”をつけて、組み合わせ NGというルールを作ったりすればいい 共通の単語を抜き出す

Slide 28

Slide 28 text

単語分けすぎ %d個 あなたはリンゴを 持っています %d You have apples 分けすぎるとどうなる?

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

ネームスペース “top.description.label.text” = “こんにちは、世界!”; top description text / title 画⾯ / コンポーネント 役割 句読点あり→ text 句読点なし → title label UI ① キーのコンフリクト対策 ② キーから⽤途を明確に

Slide 32

Slide 32 text

この⽂⾔、、、対応漏れ?って思ったら ローカライズファイルを コンパイル対象から外してみよう おすすめ度 ★★★☆☆

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

書き出す CocoaPodsでインストールしている場合 Pods/SwiftGen/binに実⾏ファイルがあります

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

書き出し結果 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 で、「パスワードリセット」を解決できる

Slide 41

Slide 41 text

⽂⾔がない時、 気づけるようになった!

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

デバッグ⽂⾔を.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

Slide 44

Slide 44 text

デバッグ⽂⾔を.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 ファイルを分けてコンパイル対象外にしやすくする

Slide 45

Slide 45 text

コンパイル対象外とは?

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Xcodeで設定する場合

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

SwiftGenのstencilを カスタムしよう!

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

ベースのL nを定義 enum L10n { static func tr(…) -> String { let format = NSLocalizedString(…) return String(format: format, …) } } Strings.swift ⽂⾔を解決する関数

Slide 54

Slide 54 text

プリセットをコピーする {% 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に変更

Slide 55

Slide 55 text

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)

Slide 56

Slide 56 text

書き出し結果 extension L10n { enum Debug { enum Login { … } } } extension L10n { enum Auth { enum PasswordReset { … } } } Strings+Localizable+Debug.swift Strings+Localizable.swift

Slide 57

Slide 57 text

絶対に動くデモ①

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

• 取得 UserDefault.standard.array(forKey: “AppleLanguages”) [String]で定義されている。[“en”, “ja-JP”]のような形。 定義されている⾔語は設定アプリで追加されている⾔語 配列の先頭の⾔語コードが起動時に適⽤される • 変更 [“ja-JP”, “en”]に変更すれば、強制的に⽇本語に切り替え可能に。 [“zh-Hans”, “ja-JP”, “en”]のように、新たな⾔語を追加すれば、それも適⽤される • リセット UserDefaults.standard.removeObject(forKey: “AppleLanguages") アプリの⾔語を変更する

Slide 62

Slide 62 text

コード @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") }

Slide 63

Slide 63 text

絶対に動くデモ②

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

余談 複数形対応

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

// Alias to stringsdict "top.apples.text" = "You have %d apples"; Localizable.strings Localizable.stringsdict キーが⼀致してれば stringsdictの⽅が採⽤さ れる

Slide 71

Slide 71 text

※ワークアラウンドは ほどほどに

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

attributedText.addAttributes([ .foregroundColor: UIColor.red, .font: UIFont.boldSystemFont(ofSize: 18)], range: NSRange(location: 8, length: 2)) NSRangeを直指定は絶対ダメ!

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

英語は全滅

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

"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) で装飾範囲を解決する

Slide 82

Slide 82 text

絶対に動くデモ③

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

Crowdinを使う

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

翻訳リストが作成されて

Slide 90

Slide 90 text

翻訳すると

Slide 91

Slide 91 text

No content

Slide 92

Slide 92 text

プルリクがでる

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

固有名詞の直訳問題

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

• 多⾔語対応のススメ( , ikkan_chin) • Strings Dict Format(Apple) • i n and L n videos(Apple) • Internationalization(Apple) • SwiftGen(GitHub) • サンプルプロジェクト(GitHub, matsuokah) • Crowdin(ローカライズSaaS, Crowdin) 参考⽂献, リンク

Slide 113

Slide 113 text

• https://github.com/taggon/highlight ソースコードハイライトツール

Slide 114

Slide 114 text

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