アプリ内のコンテンツについては、例えばOnsenUI+Vue.jsで作っている場合はvue-i18nなどのライブラリを使って簡単に多言語化できますが、アプリ名の多言語化でハマったので対応した結果をまとめました。
多言語化したい箇所
ネイティブアプリの場合
ネイティブアプリは以下の様にしてアプリ名の多言語化を行います。
Android
- resにvalues-enなど、言語ごとのディレクトリを作成
- strings.xmlを作成し、「app_name」にアプリ名を記述
iOS
- XcodeからInfoPlist.stringを作成し、Localizationをクリックして各言語をチェックする
- InfoPlist.stringの「CFBundleDisplayName」にアプリ名を記述
じゃあ、strings.xmlとInfoPlist.stringをコピーすればよいのでは?と考えました。
CordovaのHook機能を使ってみる
Hook機能とはCordovaの各種操作時(プラグイン追加時、プリペア時など)に処理を挟み込むことができる機能です。
この機能を使用して、after_prepare(プリペア後)に、strings.xmlとInfoPlist.stringをコピーするHookスクリプトを作成してみました。
Hookスクリプト
汎用的に他のプロジェクトでも使いたいので、プラグインとして作成します。
※直接プロジェクトにHookスクリプトを配置することもできます。その場合は、ルート直下のhookディレクトリにHookスクリプトを配置することが一般的ですが、Monacaはwww配下以外のファイルをビルド時に参照できないので、www以下に配置する必要があります。
ディレクトリ構成
plugin.xml
<?xml version='1.0' encoding='utf-8'?>
<plugin id="cordova-plugin-i18n-app-name" version="0.0.1" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<name>cordova-plugin-i18n-app-name</name>
<platform name="android">
<hook src="scripts/copy_android_strings.js" type="after_prepare" />
</platform>
<platform name="ios">
<hook src="scripts/copy_ios_strings.js" type="after_prepare" />
</platform>
</plugin>
Hookスクリプト(Android)
var fs = require('fs-extra');
function fileExists(file) {
try {
return fs.statSync(file).isFile();
} catch (error) {
return false;
}
}
module.exports = function(context) {
var q = context.requireCordovaModule('q');
var glob = context.requireCordovaModule('glob');
var deferred = q.defer();
// globで言語ごとに走査
glob('www/locales/android/values-*', function (error, files) {
if (error) {
deferred.reject(error);
} else {
files.forEach(function (file) {
var stringFile = file + '/strings.xml';
if (fileExists(stringFile)) {
var lang = file.match(/\/values-(.+)$/)[1];
var distDir = 'platforms/android/res/values-' + lang;
fs.ensureDirSync(distDir);
// strings.xmlをコピー
fs.copySync(stringFile, distDir + '/strings.xml', { replace: true });
console.log('copyFrom: ' + stringFile);
console.log('copyTo: ' + distDir + '/strings.xml');
}
});
deferred.resolve();
}
});
return deferred.promise;
};
コピーするリソースファイル
先述した通り、Monacaはビルド時にwww配下以外のファイルを参照できないので、コピーするリソースファイル(strings.xml, InfoPlist.strings)はwwwに配置します。
ディレクトリ構成はPhoneGap Buildが似た様な機能を標準で用意していたので、同じ様なディレクトリ構成にしました。
project.pbxprojの罠
Androidはstrings.xmlをコピーするだけでうまくいったのですが、iOSはコピーするだけだとうまくいきませんでした。
iOSはXcodeに認識させるために、リソースファイル(InfoPlist.string)とそのLocalization設定をproject.pbxproj(Xcode使ってると、よくコンフリクトする謎フォーマットのアレです)に定義する必要があるようです。
project.pbxprojの構成
node-xcode
project.pbxprojをパース・編集できるnode.jsのライブラリです。
このライブラリを使って、 PBXFireReferenceとPBXVariantGroupを設定します。
node-xcodeのインストール
CustomConfigプラグインを使用していればインストール不要と思っていたのですが、Monaca標準のCustomConfigプラグインのバージョン(2.0.3)だとnode-xcodeのバージョンが0.8.xと古く、PBXVariantGroupの編集ができませんでした。(PBXVariantGroupの編集は0.9.x以降対応)
CustomConfigプラグインを最新(4.0.2)にしてみるも、なぜかビルドでコケるので、npm install xcode
を今回作成したプラグインのルート直下で実行して最新のnode-xcodeをインストールし、node_modules以下をまるごとアップロードしました。
Hookスクリプト(iOS)
以下のHookスクリプトを実行することで、iOSもアプリ名の多言語化ができました!
var fs = require('fs-extra');
var xcode = require('xcode');
function fileExists(file) {
try {
return fs.statSync(file).isFile();
} catch (error) {
return false;
}
}
function values(object) {
return Object.keys(object).map(function (k) {
return object[k];
});
}
// from: https://github.com/kelvinhokk/cordova-plugin-localization-strings
function writeLocalizationFieldsToXcodeProj(filePaths, groupname, proj) {
var fileRefSection = proj.pbxFileReferenceSection();
var fileRefValues = values(fileRefSection);
if (filePaths.length > 0) {
// var groupKey;
var groupKey = proj.findPBXVariantGroupKey({name: groupname});
if (!groupKey) {
// findPBXVariantGroupKey with name InfoPlist.strings not found. creating new group
var localizableStringVarGroup = proj.addLocalizationVariantGroup(groupname);
groupKey = localizableStringVarGroup.fileRef;
}
filePaths.forEach(function (path) {
var results = fileRefValues.filter(function (v) {
return v.path === '"' + path + '"';
});
if (Array.isArray(results) && results.length === 0) {
//not found in pbxFileReference yet
proj.addResourceFile('Resources/' + path, {variantGroup: true}, groupKey);
}
});
}
}
module.exports = function(context) {
var q = context.requireCordovaModule('q');
var glob = context.requireCordovaModule('glob');
var configXml = fs.readFileSync('config.xml').toString();
var appName = configXml.match(/<name>(.*)<\/name>/i)[1];
var projDir = 'platforms/ios/' + appName;
var pbxProjFile = projDir + '.xcodeproj/project.pbxproj';
var deferred = q.defer();
// globで言語ごとに走査
glob('www/locales/ios/*.lproj', function (error, files) {
if (error) {
deferred.reject(error);
} else {
var stringFiles = [];
files.forEach(function (file) {
var stringFile = file + '/InfoPlist.strings';
if (fileExists(stringFile)) {
var lang = file.match(/\/([^/]+)\.lproj$/)[1];
var distDir = projDir + '/Resources/' + lang + '.lproj';
fs.ensureDirSync(distDir);
// InfoPlist.stringsをコピー
fs.copySync(stringFile, distDir + '/InfoPlist.strings', { replace: true });
console.log('copyFrom: ' + stringFile);
console.log('copyTo: ' + distDir + '/InfoPlist.strings');
stringFiles.push(lang + '.lproj/InfoPlist.strings');
}
});
var proj = xcode.project(pbxProjFile);
// project.pbxprojをパース
proj.parse(function (err) {
if (err) {
deferred.reject(err);
} else {
// project.pbxprojにPBXFireReferenceとPBXVariantGroupを設定
writeLocalizationFieldsToXcodeProj(stringFiles, 'InfoPlist.strings', proj);
fs.writeFileSync(pbxProjFile, proj.writeSync());
console.log('ok!!');
deferred.resolve();
}
});
deferred.resolve();
}
});
return deferred.promise;
};
所感
思ったより面倒でしたが、Hookスクリプトの作成方法の良い勉強になりました。
iBeaconのプラグインなどを使っていると、位置情報の権限をユーザに許可してもらう際のメッセージが英語固定になっていることがありますが、これも同じ方法で多言語化できる気がするので試してみたいと思います。
参考
[cordova-plugin-localization-strings]
(https://github.com/kelvinhokk/cordova-plugin-localizationstrings)
node-xcodeの使い方の参考にさせて頂きました。(一部コードを流用しています)
.xcodeproj/project.pbxproj を解読する
project.pbxprojの仕様の参考にさせて頂きました。
ソースコード
GitHubで公開してます。
https://github.com/kishino/cordova-plugin-i18n-app-name