【Xcode】Google Spreadsheetでアプリのローカライズ管理
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.lproj と ja.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.swift は NSLocalizedString のラッパークラスで、使い方はこの記事の最後に説明する。
スプレッドシートから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();
}
);
}
これらのソースはこちらにあった記事を元に、自分なりに改造させてもらった。 多言語リソース管理がめんどくさいので、どうにかして楽がしたいと思った話iOS編-Qiita
書き出すプログラムの実行
それではスクリプトを実行してみよう。関数を選択 から 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秒おきにファイルの更新時間をチェックし、コピーするかしないかを判定していだけだ。SOURCE と DIST はプロジェクトの環境に合わせて設定しよう。
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" = "シェアする";
.....