Back to list
30 ページ、12 の言語、精神崩壊しない翻訳方法
Translating 30 Pages into 12 Languages Without Losing Your Mind
Translated: 2026/4/17 11:14:19
Japanese Translation
30 ページを全部英語、そしてハードコードされた文字列のみにしていた。ユーザーがストレートに指摘した。「メニューだけ翻訳したのか?他はどうなの?」正しい指摘だ。実際にやろう。12 言語:英語、ドイツ語、フランス語、スペイン語(スペイン)、スペイン語(ラテンアメリカ)、イタリア語、ポルトガル語、ロシア語、ポーランド語、日本語、韓国語、アラビア語。アラビア語は縦書き対応を追加。日本語と韓国語は西側言語とは異なるワードラップ処理を必要とする。ラテンアメリカ語はスペイン語(スペイン)とは十分に異なり、別ファイルとするのが適当。30 ページ × 12 言語 × ページあたり〜30 文字列 = 約 10,800 文字条。これは多いキーだ。i18n システムは既に部分的に機能していた。ナビゲーション項目は翻訳されていた。インフラは存在した:// src/lib/i18n.tsx
const I18nContext = createContext(null);
export function useI18n() {
return useContext(I18nContext);
}// コンポーネントでの使用
const { t } = useI18n();
return {
T('DASHBOARD.TITLE')
};
翻訳ファイルは JSON ではなく TypeScript オブジェクトだった。これは重要だ。TypeScript はキーに対して自動補完を提供し、タイポを実行時ではなくコンパイル時に検出する。// src/lib/translations/en.ts
export const en = {
nav: {
dashboard: 'Dashboard',
business: 'Business',
// ...
},
dashboard: {
title: 'Dashboard',
// ...
}
};
何かが不足していた:多くのページがハードコードされた文字列を使用し、t() 関数を無視していた。最初に UI を構築し、後に i18n を追加すると、文字列を抽出する难易さが異なるのが判る。容易:静的ラベル、見出し、ボタンテキスト、プレースホルダーテキスト。これらは t() 呼び出しに直接降下する。面倒:動的値を持つ文字列。
// Before
Processing {count} items
// After — ナイーブなアプローチ(一部の言語で破綻)
{t('processing')} {count} {t('items')}
// 改善 — 補間
{t('processing_count', { count })}
// en.ts: processing_count: 'Processing {count} items'
// de.ts: processing_count: '{count} Elemente werden verarbeitet'
ドイツ語は動詞を移動させる。日本語は単語順序を完全に変える。文字列を分割して結合すると、単語順序はコードに焼き付けられてしまい、翻訳で修正できない。難しい:複数形。英語は単数・複数形。ロシア語は 4 つの複数形。ポーランド語は 3 つ。アラビア語は 6 つ。v1 では完全な複数形処理を見送った。大半の数の文字列はラベルと共に表示されるコンテキストにあるため、複数形が視覚的に重要でないからだ。後にこれを正しく修正する。完全に除外:エージェント名、技術識別子、API エンドポイントラベル、アイコン名、CSS クラス。これらは翻訳可能なテキストのように見えるが、実はそうではない。「GPT-4o」や「webhook_url」を翻訳するとシステムが破綻する。30 ページのファイルを手動で読み込んで文字列を抽出し、12 × 30 の翻訳ファイル追加を行うのは、この体積ではエラーの多い作業だ。私たちのアプローチ:各ページファイルを読み取り、全てのハードコード文字列を抽出する。en.ts に適切な値でキーを追加する。同じキーを全ての 11 つの他の言語ファイルに翻訳を追加する。ページを t() にハードコード文字列を Wire する。We ran extraction and translation in parallel using multiple agents — one auditing pages, others updating language files. The bottleneck was key naming: you need consistent conventions before parallelizing or you get collisions. Key naming convention we settled on: page.element — e.g. dashboard.title, pricing.enterprisePlan, chat.placeholder. For shared components: common.save, common.cancel. Flat enough to read, nested enough to avoid collisions. Duplicate keys. When adding keys in parallel, two passes at the same file can create:
export const en = {
dashboard: {
title: 'Dashboard', // added in pass 1
title: 'Dashboard', // added again in pass 2
}
}
TypeScript はデフォルトで重複オブジェクトキーにエラーを出さない。2 番目のものが静かに勝ってしまう。compile check 時にこれらを捕捉した。
tsc --noEmit with "forceConsistentCasingInFileNames": true in tsconfig found them.
非英語ファイルでの欠落キー。We added keys to en.ts and forgot to add them to one of the 11 others. At runtime this fails silently — the key path returns undefined unless explicitly handled, leading to broken UI if not caught during the build. The final build succeeded thanks to rigorous type checking, producing a fully localized application without manual string intervention.
Original Content
We had 30 pages. All in English. All with hardcoded strings. A user pointed it out bluntly: "You translated the menu. What about everything else?" Fair. Time to actually do it. 12 languages: English, German, French, Spanish (Spain), Spanish (Latin America), Italian, Portuguese, Russian, Polish, Japanese, Korean, Arabic. Arabic adds RTL support. Japanese and Korean don't word-wrap the same way Western languages do. Latin American Spanish is different enough from Spain Spanish to warrant separate files. 30 pages × 12 languages × ~30 strings per page = roughly 10,800 translation entries. That's a lot of keys. The i18n system was already partially in place — nav items were translated. The infrastructure existed: // src/lib/i18n.tsx const I18nContext = createContext(null); export function useI18n() { return useContext(I18nContext); } // Usage in a component const { t } = useI18n(); return
{T('DASHBOARD.TITLE')}
Translation files were TypeScript objects, not JSON. This matters: TypeScript gives you autocomplete on keys and catches typos at compile time, not runtime. // src/lib/translations/en.ts export const en = { nav: { dashboard: 'Dashboard', business: 'Business', // ... }, dashboard: { title: 'Dashboard', // ... } }; What was missing: most pages were using hardcoded strings and ignoring the t() function entirely. When you build UI first and add i18n later, you discover that not every string is equally easy to extract. Easy: Static labels, headings, button text, placeholder text. These drop into t() calls directly. Annoying: Strings with dynamic values. // Before
Processing {count} items
// After — naive approach that breaks in some languages
{t('processing')} {count} {t('items')}
// Better — interpolation
{t('processing_count', { count })}
// en.ts: processing_count: 'Processing {count} items' // de.ts: processing_count: '{count} Elemente werden verarbeitet' German moves the verb. Japanese changes the word order entirely. If you split strings and concatenate them, word order is baked into code and you can't fix it in translations. Tricky: Plural forms. English has singular/plural. Russian has four plural forms. Polish has three. Arabic has six. We punted on full plural handling for v1 — most of our count strings are in contexts where the number is shown alongside the label and pluralization doesn't visually matter. We'll fix this properly later. Skip entirely: Agent names, technical identifiers, API endpoint labels, icon names, CSS classes. These look like translatable text but aren't. Translating "GPT-4o" or "webhook_url" would break things. Reading 30 page files manually to extract strings, then writing 12 × 30 translation file additions, is error-prone at this volume. Our approach: Read each page file and extract all hardcoded strings Add keys to en.ts with appropriate values Add the same keys to all 11 other language files with translations Wire the pages to use t() instead of hardcoded strings We ran extraction and translation in parallel using multiple agents — one auditing pages, others updating language files. The bottleneck was key naming: you need consistent conventions before parallelizing or you get collisions. Key naming convention we settled on: page.element — e.g. dashboard.title, pricing.enterprisePlan, chat.placeholder. For shared components: common.save, common.cancel. Flat enough to read, nested enough to avoid collisions. Duplicate keys. When adding keys in parallel, two passes at the same file can create: export const en = { dashboard: { title: 'Dashboard', // added in pass 1 title: 'Dashboard', // added again in pass 2 } } TypeScript doesn't error on duplicate object keys by default — the second one silently wins. We caught these during the compile check. tsc --noEmit with "forceConsistentCasingInFileNames": true in tsconfig found them. Missing keys in non-English files. We added keys to en.ts and forgot to add them to one of the 11 others. At runtime this fails silently — the key path returns undefined and you get nothing rendered. Fix: after every batch of additions to en.ts, run a script that diffs the key structure against all other locale files and reports missing keys. # Quick audit: count keys per file for f in src/lib/translations/*.ts; do echo "$f: $(grep -c "'" $f) keys" done Not perfect but good enough to spot files that fell way behind. RTL layout. Arabic needs dir="rtl" on the root element. We detect the locale and set it: But Tailwind's space-x-* and flex direction utilities don't automatically flip for RTL. We had a few layouts that looked wrong in Arabic because icons and text were in the wrong order. Most of these are still open — RTL is hard to get right without an Arabic speaker reviewing it. Start with i18n scaffolding before building UI. Adding it after means touching every file twice — once to build, once to extract. If the t() call is part of your component template from day one, extraction becomes trivial: just add the translation value. Use a dedicated i18n library for complex cases. We rolled our own minimal context provider. It's 80 lines and covers 90% of cases. But react-intl or next-intl handles pluralization, date/number formatting, and RTL better than our homebrew. For a product with global ambitions, the extra dependency is worth it. Machine translate first, human edit later. We used AI translation for all 11 non-English files. Quality varies — French and German are solid, Japanese and Arabic need review by a native speaker. The right approach: MT gives you a baseline that's 80% correct, human review catches the errors. Don't ship MT output to production without review for languages you can't read. 30 pages fully wired to t() 12 languages with complete key coverage ~800 translation keys in en.ts RTL layout for Arabic (basic — needs review) Zero hardcoded English strings in page files What's still rough: plural forms, RTL edge cases, and translation quality review for non-Latin-script languages. The user-facing result: switch the language in the top nav and every label, heading, button, and placeholder updates immediately. No page reload. The locale preference persists across sessions. Kepion is an AI-powered company builder. One subscription gets you a full team of 31 specialized AI agents — strategy, content, development, marketing, finance — all orchestrated to build and run real businesses autonomously. Three things I'd love to hear from the community: How do you handle plural forms across languages? We punted on this for v1 — the four Russian forms and six Arabic forms are still TODO. Are you using Intl.PluralRules directly, a library like react-intl, or something else? What's the minimal solution that actually works? Do you use AI/MT for translation and then human review, or go straight to native speakers? We used AI translation for all 11 non-English files. Quality is uneven — I can't evaluate Japanese or Arabic without help. Curious what workflows others have found sustainable. Any tooling for keeping translation files in sync? We caught missing keys with a manual grep count. There's got to be a better way — i18n key audits, extract scripts, CI checks. What's in your pipeline?