【Xcode・Swift】スプレッドシートでアプリのローカライズ管理する方法

iOSアプリを多言語に対応しながら開発している時に、もっとラクな方法はないだろうかとつねづね思っていた。NSLocalizedString では入力補完がないし、項目が多くなると同じ翻訳をしていたことにも気づかずかないありさまとなってしまう。

何か良い方法はないかと探していたところ、Googleのスプレッドシートで管理する方法を見つけることができた。

この方法を自分のiPhoneアプリの開発環境にいれてみたところ、たいへん管理がラクになった。作業効率アップまちがいナシなので、ローカライズ作業に翻弄されている方はこの記事で紹介する方法をぜひ試してみて欲しい。

【完成イメージ】スプレッドシートでアプリのローカライズを管理して効率を上げる方法


画像の拡大

画像のようにスプレッドシートでローカライズを管理することを目標にする。そしてスプレッドシートから stringsファイル を書き出し、Xcodeプロジェクトへ半自動でコピーさせる。これを実現するために、Googleドライブをいったん経由することになる。

スプレッドシート → Googleドライブ → Xcodeプロジェクト

まずはスプレッドシートでキーワードと翻訳の管理からやっていこう。

スプレッドシートでローカライズ管理


画像の拡大

スプレッドシートでローカライズの管理をするため、次のように列を構成する。今回紹介するプログラムは、この並び順通りにやらないとうまく動かないので注意が必要。

  • A、B、C列は、large、middle、smallと3つのキーワードで管理する。
  • D列はでA、B、Cをコンマで結合する。
  • E列は日本語、F列は英語の翻訳とする。

ここでD列には数式、=A2 & "." & B2 & "." & C2 を入れればA、B、Cの値をコンマ結合してくれる。

Googleドライブに書き出すための準備

ファイルを書き出す場所を指定するために、あらかじめGoogleドライブにフォルダを作っておく。

ブラウザーからGoogleドライブへアクセスし、localize フォルダを作成。さらにそのフォルダ内にen.lprojja.lproj の名前でフォルダを作成する。これらの名前は、自分のXcodeプロジェクトに合わせて決めている。


画像の拡大

先ほど作ったGoogleドライブの3つのフォルダのURLを調べる。

https://drive.google.com/drive/folders/xxxxxxxxxxxxxxxx

スプレッドシートからGoogleドライブへ書き出すためには、この xxxxxxxxxxxxxxxx が必要になるのでメモしておく。それと、MacとGoogleドライブを自動で同期できるように「Backup and Sync from Google」をインストールしておこう。

スクリプトエディタでスプレッドシートからGoogleドライブへ書き出すプログラム

スプレッドシートのスクリプトエディタを使って Localizable.strings ファイルと Localize.swift をGoogleドライブに書き出していく。Localize.swiftNSLocalizedString のラッパークラスで、使い方はこの記事の最後に説明する。

スプレッドシートからGoogleドライブへ書き出すために、スクリプトエディタの設定を行う。スプレッドシートのメニューから ツールスクリプトエディタ を選択。次のコードを貼り付ける。

function localize() {
  var folder = DriveApp.getFolderById('xxxxxxxxxxxxxxxxxxxxxxxxxx');
  var sheetName = 'SuperMemo'
  var fileName = 'Localizable.strings'
  var jaFolder = DriveApp.getFolderById('xxxxxxxxxxxxxxxxxxxxxxxxxx');
  var enFolder = DriveApp.getFolderById('xxxxxxxxxxxxxxxxxxxxxxxxxx');
  var contentType = 'text/plain';
  var enumFileName = 'Localize.swift';
  var jsonContentType = 'application/json';
  var swiftContentType = 'text/plain';

  var localizeIdNum = 3;
  var jaNum = 4;
  var enNum = 5;
  var nl = '\n';

  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getSheetByName(sheetName);
  var data = sheet.getDataRange().getValues();

  var jaString, enString;
  var notice = "// This file is automatically generated from googlespredsheet.\n// Please do not edit directly\n\n"
  jaString = enString = zhhkString = thString = notice;

  var ids = "";
  for (i in data) {
    var localizeId = data[i][localizeIdNum];
    if (localizeId == "localize_id" || localizeId == "") {
      continue;
    }
    
    var enumStr = periodToCamel(snakeToCamel(localizeId));
    jaString += '/**' + nl + 'Localize.string(.' + enumStr + ', comment:"' + escapeString(data[i][jaNum]) + '")' + nl + '**/' + nl;
    jaString += '"' + localizeId + '" = "' + escapeString(data[i][jaNum]) + '";' + nl + nl;
    
    enString += '"' + localizeId + '" = "' + escapeString(data[i][enNum]) + '";' + nl;

    ids += "    case " + enumStr + " = \"" + localizeId + "\"\n";
  }

  var enumString = notice + "import UIKit\n\nenum Localize: String {\n" + ids + "\n    fileprivate var localized:String {\n        return NSLocalizedString(self.rawValue, comment: self.rawValue)\n    }\n\n    static func string(_ type:Localize, comment: String) -> String {\n        return type.localized\n    }\n}"
  updateFile(swiftContentType, folder, enumFileName, enumString);

  updateFile(contentType, jaFolder, fileName, jaString);
  updateFile(contentType, enFolder, fileName, enString);
}

function updateFile (contentType, folder, filename, string) {
  try {
    // filename is unique, so we can get first element of iterator
    var file = folder.getFilesByName(filename).next()
    file.setContent(string)
  } catch(e) {
    folder.createFile(filename, string, contentType)
  }
}

function escapeString (string) {
  return string.replace(/"/g, '\\"');
}

function snakeToCamel (p) {
    return p.replace(/_./g,
            function(s) {
                return s.charAt(1).toUpperCase();
            }
    );
}

function periodToCamel (p) {
    return p.replace(/\../g,
            function(s) {
                return s.charAt(1).toUpperCase();
            }
    );
}

sheetName にはスプレッドシートのシート名を、

DriveApp.getFolderById('xxxxxxxxxxxxxxxx'); には、先ほどGoogleドライブでメモしたIDを入力しておく。スプレッドシートからデータを読み込んで整形し、Localize.swiftLocalizable.strings としてファイルをGoogleドライブへ書き出すプログラムとなっている。

これらのソースはこちらにあった記事を元に、自分なりに改造させてもらった。

書き出すプログラムの実行

それではスクリプトを実行してみよう。関数を選択 から localize を選びクリックする。


画像の拡大

設定に間違いがなければ、しばらくするとGoogleドライブにファイルが書き出されているはずだ。いちいちスクリプトエディタを開いて書き出すのは面倒なので、スプレッドシートにボタンを設置しておくと大変便利。


画像の拡大

ボタンの作り方は、スプレッドシートのメニュの 挿入図形描画 から適当にボタン画像を作る。

その後、スプレッドシートに挿入されたボタンを右クリックして、右上に表示される「...」をクリックするとメニューが表示されるので スクリプトを割り当て を選択して先ほど作った関数 localize を入力すればOKだ。

シェルスクリプトでファイルの更新を監視、コピーする

Googleドライブに書き出されたファイルを、Xcodeのプロジェクトへ、いちいち手作業でコピペするのはめんどくさい。

そこで、Googleドライブのファイルが更新されたら、Xcodeプロジェクトへ自動でファイルをコピーしてくれるシェルスクリプトを作ってみた。こういった細かな自動化は作業の効率アップになるのでとても重要、少し手間でもやってみよう。

#!/bin/bash
cd `dirname $0`

SOURCE='/Users/xxxxxx/GoogleDrive/localize/'
DIST='/Users/xxxxxx/Projects/Xcode/memo/memo/Localize'

lastTime=0


while true
do
    tmpTime=0
    lastModifiedFile=""

    for file in `\find $SOURCE -maxdepth 2`; do
        modified=$(date -r $file '+%Y%m%d%H%M%S')
        # echo $modified
        if [ $modified -gt $tmpTime ]; then
            tmpTime=$modified
            lastModifiedFile=$file
        fi
    done
    if [ $lastTime -ne $tmpTime ]; then
        echo "更新あり"
        lastTime=$tmpTime
        cp -R $SOURCE $DIST
    fi    
    sleep 3
done

慣れないシェルスクリプトだと難解に感じるかもしれないが、プログラムでやっていることはとてもシンプルで、3秒おきにファイルの更新時間をチェックし、コピーするかしないかを判定していだけだ。SOURCEDIST はプロジェクトの環境に合わせて設定しよう。

NSLocalizedStringをラッピングするLocalize.swift

それでは最後に Localize.swift の使い方を説明する。Localize.swift は簡単に言えば NSLocalizedString を使いやすくするためのラッパークラスだ。enumで定義しているので入力補完が効く。

import UIKit

enum Localize: String {
    case archiveLabelTitle = "archive.label.title"
    case generalAlertCancel = "general.alert.cancel"
    .....

    fileprivate var localized:String {
        return NSLocalizedString(self.rawValue, comment: self.rawValue)
    }

    static func string(_ type:Localize, comment: String) -> String {
        return type.localized
    }
}

使い方は次のとおり。

Localize.string(.generalAlertShare, comment:"シェアする")

結局 NSLocalizedString とあまり変わらない形になってしまったが、スプレッドシートで翻訳を管理できるメリットとenumによるサジェスト効果の恩恵はたいへん大きい

あとあとコードを見返す時に何をやっているのか分からなくなるため、commentは日本語を書いておいた方が無難だろう。しかしcommentをいちいち入力するのは面倒なので、日本語の Localizable.strings に関数をコピペできるようコメントアウトで表記しておいた。

/**
Localize.string(.generalAlertShare, comment:"シェアする")
**/
"general.alert.share" = "シェアする";

.....
グローバルWebサイト&アプリのススメ グローバルジェネラリストなWeb担当者を目指して
グローバルWebサイト&アプリのススメ グローバルジェネラリストなWeb担当者を目指して

Webサイト・サービス、アプリのグローバライズのノウハウを多くの事例から導き出した書籍です。アクセシビリティ、表示パフォーマンスといったコンテンツの実装要件がほぼほぼ出そろったいま、あらゆるWebサイトが検討すべき事項はコンテンツそのものの質の向上で、その重要なテーマにグローバライセーションとローカライゼーションがあります。

AmazonRakuten

Amazonでお得に購入するなら、Amazonギフト券がオススメ!

\Amazonギフトがお得/

コンビニ・ATM・ネットバンキングで¥5,000以上チャージすると、プライム会員は最大2.5%ポイント、通常会員は最大2%ポイントがもらえます!
Amazonギフト券

\この記事をシェアする/