[SwiftUI][Xcode] 文字列を Internationalization I18N する

     
⌛️ 3 min.
SwiftUI で扱われる文字列の 国際化/Internationalization/I18N する方法を説明します

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

国際化対応言語の追加

最初に、アプリが対応する言語を設定することが必要です。

プロジェクト – Info で Localizations に、対応する言語設定を追加します。

setupLanguage
setupLanguage

上記で、ベース言語以外の言語対応の準備は完了です。

この記事では、文字列を各種言語に対応させる方法を説明していきます。

追加言語向け翻訳対象文字列設定

SwiftUIに渡される文字列

SwiftUI の要素に渡される文字列の国際化対象の文字列として扱われます。

例えば、Text に渡す文字列は、そのまま国際化対象となります。


Text("この文字列は、デフォルトで国際化対象").padding()

Text の initializer 定義を ドキュメントで参照すると 以下のようになっています。


init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

この LocalizedStringKey という型がポイントです。この型で受け取っている文字列は 国際化対象になります。

変数定義された文字列

プログラムコード中で、以下のように定義されている文字列はそのままでは、国際化対応の文字列としては扱われません。


var message = "String 変数"

以降で SwiftUI の Text に渡されているとしても、通常の文字列として扱われ国際化対応の文字列としては扱われません。

この変数定義を以下のように変更すると、国際化対応の文字列として扱われるようになります。


var message = NSLocalizedString("String 変数", comment: "")

文字列を選択後、Xcode のメニュー “Editor”-“Refactor”-“Wrap in NSLocalizedString” を選択すると、選択文字列を使った NSLocalizedString に変更してくれます。


var message = "String 変数"
// "Editor"-"Refactor"-"Wrap in NSLocalizedString"
var message = NSLocalizedString("String 変数", comment: "")

国際化対応文字列の翻訳

Xcode では、コード中の翻訳が必要な文字列を 別ファイルに抜き出す機能が付いています。

抜き出されたファイル(XMLファイルです)を翻訳者に渡すことで、Xcode を使わずとも翻訳することができるようになります。

国際化対応文字列の export

Xcode の メニュー “Product”-“Export localizations…” を選択すると、出力する言語と合わせて出力フォルダを聞いてきます。

ExportLocalizations
ExportLocalizations

以下のような ContentView.swift を持つプロジェクトを使って、Export してみます。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/05/25
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    let message = NSLocalizedString("Hello from variable", comment: "")
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
            Text(message)
                .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

プロジェクトと同じ名前のフォルダ として出力されたデータは、以下のような構造を持ちます。


<project name> -+- en.xcloc (ja.xcloc と同様の構造)
                |
                +- ja.xcloc  -+- Localized Contents --- ja.xliff
                              |
                              +- Notes
                              |
                              +- Source Contents
                              |
                              +- Contents.json

ja.xliff を例に説明しますので、他は省略してます。 ja.xliff は、以下のような内容になっています。


<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
  <file original="I18NExample/en.lproj/InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext">
    <header>
      <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="12.5" build-num="12E262"/>
    </header>
    <body>
      <trans-unit id="CFBundleName" xml:space="preserve">
        <!-- (1) -->
        <source>I18NExample</source>
        <note>Bundle name</note>
      </trans-unit>
    </body>
  </file>
  <file original="I18NExample/en.lproj/Localizable.strings" source-language="en" target-language="ja" datatype="plaintext">
    <header>
      <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="12.5" build-num="12E262"/>
    </header>
    <body>
      <!-- (2) -->
      <trans-unit id="Hello from variable" xml:space="preserve">
        <source>Hello from variable</source>
        <note>No comment provided by engineer.</note>
      </trans-unit>
      <!-- (3) -->
      <trans-unit id="Hello, world!" xml:space="preserve">
        <source>Hello, world!</source>
        <note>No comment provided by engineer.</note>
      </trans-unit>
    </body>
  </file>
</xliff>
コード解説
  1. アプリ名も 国際化対象文字列に該当します
  2. NSLocalizedString を使って宣言している文字列は、国際化対象文字列になります
  3. SwiftUI の Text に渡す文字列も、国際化対象文字列になります

国際化対応文字列の 翻訳

先の ja.xliff を日本語に翻訳していきます。

“Hello from variable” を “変数から こんにちわ世界” と翻訳するには、以下のように翻訳を追加します。


      <trans-unit id="Hello from variable" xml:space="preserve">
        <source>Hello from variable</source>
        <!-- (1) -->
        <target>変数から こんにちわ世界</target>
        <note>No comment provided by engineer.</note>
      </trans-unit>
コード解説
  1. target 要素として、翻訳を追加します

上記のように、翻訳をそれぞれに追加していきます。

国際化対応文字列の import

翻訳が終了したら、翻訳したデータを Xcode に取り込みます。

Xcode の メニュー “Product”-“Import localizations…” を選択すると、import するフォルダを聞いてきます。

この時には、翻訳した言語のフォルダを選択する必要があることに注意です。 日本語を翻訳した場合は、ja.xloc フォルダを選択して import します。

国際化対応の import 結果の確認

Import すると、プロジェクトに Localizable.strings が追加されていることを確認できます。以下のような内容になっています。


/* No comment provided by engineer. */
"Hello from variable" = "変数から こんにちわ世界";

/* No comment provided by engineer. */
"Hello, world!" = "こんにちわ世界";

従来の国際化対応の方法は、上記のファイルを編集することでしたが、見てきたように、Xcode の外部に出力したファイルを使って翻訳できるようになっています。

# strings ファイルがあることからわかるように strings ファイルが便利な場合には、従来通り strings ファイルを使うこともできます。

実行時に選択される言語

OS 設定で指定されている言語があれば、その言語が使用されます。

アプリにはベース言語を指定でき、OS で設定されている言語がアプリでサポートされていない場合には、ベース言語が使用されます。

iOS13 からは、アプリごとに言語設定を行うことができますので、アプリにリクエストされる言語が、OS の言語設定と異なることもあります。通常、その相違についてはアプリは気にしなくて良くなっています。

翻訳 Tips: 文字列のグループ分け

アプリで多くの文字列が使用されると、文字列をグループ分けしたくなります。

理由としては、以下のようなものが考えられます。

  • 機能毎に担当者が異なるので、異なるテーブルとして管理したい
  • 特定の文字列は、複数アプリで共有しているので管理を別としたい

グループ分けしないと、Localizable.strings で一括して管理されることになるので、数が増えてくるに従って管理がしづらくなることが予想されます。

tableName を使ったテーブル管理

NSLocalizedString という関数には、引数がいくつかあります。

Apple のドキュメントは、こちら


func NSLocalizedString(_ key: String, tableName: String? = nil, bundle: Bundle = Bundle.main, value: String = "", comment: String) -> String

2つ目の引数 tableName を指定することで、Localizable.strings というファイル名を別の XXXX.strings という名前にすることができます。(省略すると Localizable.strings が使われます)


Text(NSLocalizedString("Hello, world!", tableName: "myTable"))

例えば上記のような Text が使われていて、”Export Localizations” -> ”翻訳” -> “Import Localizations” を行うと、プロジェクトに、myTable.strings というファイルが追加されます。翻訳された情報は、myTable.strings に設定されることになります。

こうすることで、例えば、myTable.strings を別プロジェクトにコピーして再利用する等が可能になります。

comment を使った翻訳メモ

上記の NSLocalizedString の最後の引数に comment があります。このコメントを記述すると、”Export Localizations” した .xliff ファイルに note として転記されます。


Text(NSLocalizedString("Hello, world!", tableName: "myTable", comment:"きをつけて”))

  <file original="I18NExample/en.lproj/myTable.strings" source-language="en" target-language="ja" datatype="plaintext">
    <header>
      <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="12.5" build-num="12E262"/>
    </header>
    <body>
      <trans-unit id="Hello again." xml:space="preserve">
        <source>Hello again.</source>
        <note>きをつけて</note>  
      </trans-unit>
    </body>
  </file>

翻訳する時に注意が必要な文字列については、この comment を使うとよさそうです。

デバッグ方法1:設定言語を指定してアプリを起動する

Scheme 設定中の “App Language” で言語を選択することで、特定言語設定でアプリを起動することができます。

Debug1.Language
Debug1.Language

デバッグ方法2:翻訳されていない文字列の有無をチェック

Scheme 設定中の “Show non-localized strings” のチェックボックにチェックを入れることで、翻訳されていない文字列をチェックすることができます。

Debug2.transCheck
Debug2.transCheck

設定した Scheme でアプリを実行すると以下のように 翻訳設定されていない文字列がコンソールに表示されるようになります。


2021-05-26 09:02:37.653448+0900 I18NExample[21562:1178086] [strings] ERROR: again again not found in table Localizable of bundle CFBundle 0x7fd927d047e0  (executable, loaded)

# この場合は、”again again” という文字列が、Localation テーブルに設定されていないというエラーになっています。

ちなみに、なぜか AGAIN AGAIN と大文字に変換されて表示されました。

まとめ:文字列を I18N する方法のまとめ

文字列を I18N する方法のまとめ
  • 言語を追加することで、さまざまな言語設定に対応できる
  • Xcode の機能を使うと文字列ファイルを外部化できる
  • ユーザーが設定している言語設定に合わせた言語が選択される
  • Scheme を設定することで、特定言語での起動や翻訳されていない文字列のチェックが可能

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です