これまで見てきたあらゆるi18nセットアップには、同じ三つ巴の対立がある。開発者は型安全なJSONをリポジトリに置きたい。翻訳者はプルリクエストではなく使い慣れたツールを望む。プロダクトチームはデプロイなしで誤字を修正したい。そこで、月額50〜500ドルのローカライゼーションSaaSに課金するか、翻訳者のスプレッドシートとJSONファイル間で文字列をコピー&ペーストし続け、どこかで静かに壊れるまでやり続けることになる。
キーが約1,000件未満のプロジェクトでは、より良い中間策がある。スプレッドシート自体をデータベースにするのだ。翻訳者はGoogleスプレッドシートを編集し、Apps ScriptのエンドポイントがそれをきれいなロケールJSONとして提供し、アプリはビルド時にそれを読み込む。以下にコード付きで全体の手法を示す。
小規模プロジェクトで翻訳サービスよりスプレッドシートが優れている理由
ローカライゼーションSaaSは規模が大きくなるとその価値を発揮する——数十人の翻訳者、数千のキー、スクリーンショットやレビュー用のワークフローなど。300キー程度のマーケティングサイトにはそのような問題はなく、調整の問題がある。スプレッドシートは調整を無料で解決する。翻訳者はすでに使い方を知っており、改訂履歴や提案編集機能が最初から備わっており、プロダクトチームは10秒で文字列を変更できる。生のスプレッドシートに欠けている2つの要素——クリーンなJSON APIと欠落翻訳のフォールバック——を追加するだけでよい。
スキーマ:1タブにキーごとに1行
stringsタブを用意し、A列にキーを、以降の列に各ロケールを配置する。
| key | en | tr | es | fr |
|---|---|---|---|---|
| hero.title | Welcome | Hoş geldiniz | Bienvenido | Bienvenue |
| hero.cta | Get started | Başla | Empezar | Commencer |
ドット記法のキー(hero.title)を使うことで、i18nライブラリ内で自然にネストされたJSONが得られる。小さなmetaタブも用意する。B1セルにデフォルトロケール(en)、B3セルにバージョン(1.0.0)を入れる。
Apps Scriptエンドポイント
これをWebアプリとしてデプロイする(通常のApps Script webhookと同じ仕組み)。doGetは1つのロケールまたは全ロケールをJSONで提供し、クエリ内にフォールバックロジックを入れる。空のセルはデフォルトロケールに解決されるため、翻訳が途中のキーでも空白のまま配信されることはない。
// Code.gs
const SHEET_ID = 'your-sheet-id';
function doGet(e) {
const locale = (e.parameter.locale || 'all').toLowerCase();
const result = buildLocaleData(locale);
return ContentService
.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
function buildLocaleData(locale) {
const ss = SpreadsheetApp.openById(SHEET_ID);
const data = ss.getSheetByName('strings').getDataRange().getValues();
const headers = data[0]; // ['key','en','tr','es','fr']
const localeIdx = {};
headers.forEach((h, i) => {
if (i > 0) localeIdx[String(h).toLowerCase()] = i;
});
const meta = ss.getSheetByName('meta');
const defaultLocale = meta.getRange('B1').getValue() || 'en';
const version = meta.getRange('B3').getValue() || '1.0.0';
if (locale !== 'all' && !localeIdx[locale]) {
return { error: 'Unknown locale', available: Object.keys(localeIdx) };
}
const targets = locale === 'all' ? Object.keys(localeIdx) : [locale];
const out = { locale, defaultLocale, version, strings: {} };
if (locale === 'all') targets.forEach(l => out.strings[l] = {});
for (let row = 1; row < data.length; row++) {
const key = data[row][0];
if (!key) continue;
if (locale === 'all') {
targets.forEach(l => {
out.strings[l][key] =
data[row][localeIdx[l]] || data[row][localeIdx[defaultLocale]];
});
} else {
out.strings[key] =
data[row][localeIdx[locale]] || data[row][localeIdx[defaultLocale]];
}
}
return out;
}
Enter fullscreen mode Exit fullscreen mode
buildLocaleDataはリリース前にユニットテスト済みだ。重要なケースは、空のセルがデフォルトロケールにフォールバックすること、空のキー行がスキップされること、不明なロケールが500ではなく利用可能なロケール一覧付きのエラーを返すことである。
フロントエンド統合:ビルド時に同期
ランタイムにエンドポイントを叩くのではなく、ビルド時に一度JSONを取得してpublic/localesにコミットする。アプリは静的ファイルを配信するため、スプレッドシートの障害がサイトをダウンさせることはない。
// scripts/sync-locales.ts
import fs from 'fs';
import path from 'path';
const URL = process.env.LOCALE_SOURCE_URL!;
const LOCALES = ['en', 'tr', 'es', 'fr'];
async function sync() {
for (const locale of LOCALES) {
const res = await fetch(`${URL}?locale=${locale}`);
const data = await res.json();
const out = path.join(process.cwd(), 'public', 'locales', `${locale}.json`);
fs.writeFileSync(out, JSON.stringify(data.strings, null, 2));
console.log(`✓ ${locale}: ${Object.keys(data.strings).length} strings`);
}
}
sync();
Enter fullscreen mode Exit fullscreen mode
これをprebuildに組み込み、出力をnext-intlやi18nextに通常のJSONと同様に渡す。デプロイの合間に、ライブでプレビュー環境を用意したい場合は、エンドポイントの前に5分程度のエッジキャッシュを置けば十分だ。
キャッシュ、バージョン管理、欠落キーの検知
metaタブのversionフィールドにより、推測ではなく意図的にキャッシュを破棄できる。また、日次トリガーを設定すれば、「翻訳者が3つの文字列を忘れていた」という本番でのサプライズをメール通知に変えられる。
// Missing-keys alert — run on a daily time trigger
function alertMissingKeys() {
const ss = SpreadsheetApp.openById(SHEET_ID);
const data = ss.getSheetByName('strings').getDataRange().getValues();
const headers = data[0];
const missing = [];
for (let r = 1; r < data.length; r++) {
headers.forEach((h, c) => {
if (c > 0 && !data[r][c]) missing.push(`${data[r][0]} → ${h}`);
});
}
if (missing.length) {
MailApp.sendEmail('[email protected]',
`${missing.length} missing translations`,
missing.join('\n'));
}
}
Enter fullscreen mode Exit fullscreen mode
スプレッドシートの使用をやめてLokaliseに課金すべきタイミング
上限を正直に認識しよう。スプレッドシートは約1,000キーまでは快適に使える。Apps Scriptは秒間約50〜100リクエストを処理できるが、ビルド時の同期ではその水準に達しない。真の制限は人数だ。5人程度のアクティブな翻訳者を超えると、適切なロール、スクリーンショット、レビューキューが必要になる——その時点でシートをCSVにエクスポートし、LokaliseやCrowdinに移行すべきだ。それより小さい規模では、ツール自体が調整コストであり、価値ではない。
落とし穴
- ランタイムで取得しない。 ビルド時の同期により、スプレッドシートの障害が本番サイトを破壊しない。JSONをコミットする。
- フォールバックはクエリ内に置き、クライアントには置かない。 上記のように空のセルがデフォルトロケールに解決される方が、コンポーネントに散在するフォールバックロジックよりシンプルで安全だ。
-
空のキー行をスキップする。 翻訳者が空行を残すことがある。
if (!key) continue;でガードしないと、JSONに""キーが含まれてしまう。 -
意図的にバージョンを管理する。 タイムスタンプではなく
versionフィールドでキャッシュすることで、誤字修正が完全再ビルドを強制したり、必要なときに再ビルドがスキップされたりするのを防ぐ。 - 複数形:フォームごとに1行。 単純な数値以外は、ICU MessageFormat文字列を保存しクライアント側で解析する——独自の複数形カラムを考案しない。
まとめ
1,000キー未満のプロジェクトでは、i18nにサブスクリプションは必要ない。共有して編集可能なソースと、クリーンなJSON APIがあればよい。Googleスプレッドシートが前者、約40行のApps Scriptが後者を提供する。翻訳者は自分のツールを手に入れ、開発者はJSONを、プロダクトチームは即時編集を手に入れ、何も空白のまま配信されない。
本番版——エッジキャッシュレイヤー、バージョンピニング、完全なNext.js配線——はMageSheetブログに詳しく書かれている。
MageSheetチームにより作成。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.