

1/1/1970
Paano Ko In-implement ang i18n sa 20 Iba't Ibang Wika sa loob ng 3 Araw
Kamusta! Kakatapos ko lang ng isang sobrang laking trabaho: isalin ang Foony sa 20 iba't ibang wika. Malaking proyekto siya na halos bawat file sa codebase ay ginalaw, pero natapos ko lahat sa loob lang ng 3 araw.
Sa ibaba, hahatiin ko kung paano ko ito ginawa, yung mga konkretong numero sa likod ng pagbabago, at kung bakit naisipan ko na naman gumawa ng sarili kong translation library imbes na gumamit ng industry standard.
Bakit hindi i18next?
Nu'ng una kong pinag-isipan ang pagdagdag ng translations, siyempre tiningnan ko muna yung industry standard: i18next at react-i18next.
Pero sa huli, nag-decide akong i-optimize ang lahat para sa maintainability by AI. Malakas at powerful si i18next, pero dahil sobrang dami ng paraan gamitin ang API niya, puwedeng mag-hallucinate ang mga LLM o magsulat ng hindi pare-parehong code. Sa pamamagitan ng pag-limit sa library sa isang simpleng t() at interpolate(), nakasiguro ako na higit 10 sabay-sabay na agent ang makakasulat ng 100% type-safe na code na halos walang kailangang human intervention.
Medyo nagdududa rin ako sa pag-tali ng sarili ko sa isang malaking ecosystem na baka biglang magpakilala ng breaking changes sa hinaharap. Nasunog na ako dati sa masasakit na migration tulad ng React Router v5 at MUI v4 → v5, kaya alam ko na yung mabilis na pag-break ng backwards-compatibility sobrang karaniwan na sa mundo ng JavaScript. Mas mababa ang gastos ng pagdagdag ng pluralization features sa hinaharap kumpara sa mano-manong pag-migrate ng 139k lines of code ngayon.
Gusto ko ng sobrang simple, sobrang magaang, at sakto sa kailangan ng team ko.
Kaya gumawa ako ng sarili ko.
Binuo ko ang isang 3 KB na constrained subset na sadyang dinisenyo para payagan ang high-accuracy, autonomous na AI refactoring. Dahil dito, nakagalaw ako na parang isang engineer na gumagawa ng 3 linggong trabaho ng isang 5-person team sa loob lang ng 3 araw.
Ang Custom na Implementation
Gumawa ako ng isang minimal na i18n library na mga 3 KB gzipped lang. Dalawa ang pangunahing function na ine-expose nito: getTranslation() para sa mga non-React na context at isang useTranslation() hook para sa mga component.
Ibinabalik ng mga ito ang t() para sa simpleng string replacement at interpolate() kapag kailangan kong magpasok ng mga React component sa translation string (tulad ng link o icon). Pareho nilang sinusuportahan ang variable replacement, halimbawa "Hello {{thing}}", {thing: 'World'}.
Hetong core na t() function:
export function t(key: TranslationKeys, values?: Record<string, string | number>, locale?: SupportedLocale): string {
let namespace: string = '';
let translationKey: string = key;
// Check if key contains '/' - this indicates a namespace
const slashIndex = key.indexOf('/');
if (slashIndex !== -1) {
const parts = key.split('/');
namespace = parts.slice(0, -1).join('/');
translationKey = parts[parts.length - 1];
}
const targetLocale = locale ?? currentLocale;
const text = getTranslationValue(targetLocale, namespace, translationKey);
if (values) {
return interpolateString(text, values);
}
return text;
}
At ito naman ang React hook:
export function useTranslation() {
const [language] = useLanguage();
return useMemo(() => ({
t: (key: TranslationKeys, values?: Record<string, string | number>) =>
t(key, values, language),
interpolate: (key: TranslationKeys, components: Record<string, ReactNode>) =>
interpolate(key, components, language),
}), [language, version]);
}
Yung core ng buong library ay mga 580 lines of code lang. Ina-asikaso na nito ang:
- Lazy-loading ng mga translation file para hindi natin ipinapadala ang lahat ng 20 wika sa bawat user.
- Pag-co-code split ng mga translation ayon sa "namespace" (hal.
common,misc,games/{gameId}). - Isang "debug" locale na nagpapakita ng raw keys para ma-check ko kung tama ang pagkaka-wire up ng lahat.
Para manatiling madaling i-maintain ang system, nagdagdag din ako ng malawak na dokumentasyon sa shared/src/i18n/README.md, mula file structure hanggang sa usage examples para sa parehong client at server. Dahil hindi ako gumagamit ng standard library, sobrang importante ng reference na ito para sa pag-onboard ng mga bagong team member (o para paalalahanan ang future self ko o mga LLM kung paano gumagana ang lahat).
Sa Mga Numero
Para mabigyan ka ng idea sa laki ng update na ito, eto ang mga nabago sa codebase:
- 20 suportadong wika (plus isang debug locale para sa dev).
- 360 locale files ang nalikha.
- 139,031 linya ng translation code.
- 3,938 tawag sa
t()ang naidagdag sa buong client. - 728 source files ang nabago.
- 18 English source files na nagsisilbing source of truth (16 games + common + misc).
Pag-o-orchestrate gamit ang mga Agent
Kung mano-mano kong ginawa ito, aabutin ako ng buwan ng sobrang nakaka-boring, paulit-ulit na trabaho. Imbes nun, nag-orchestrate ako ng higit sa isang dosenang Cursor agents nang sabay-sabay para gawin ang mabibigat na parte.
Sinimulan ko sa pamamagitan ng paghahati ng codebase sa mga "section" base sa folders. Bawat game sa Foony may sarili niyang folder at sariling translation namespace. Nakatulong ito para maliit lang ang initial load size, dahil yung translations lang ng game na nilalaro mo ang kailangang i-load.
Nagpaandar ako ng maraming Cursor agents nang sabay-sabay. Bawat agent binigyan ko ng specific na section, tulad ng "i-convert ang Chess game para gumamit ng translations", tapos dadaan siya sa bawat file, hahanapin ang mga user-facing string at papalitan ng t('games/chess/some.key').
Pagkatapos nun, idinadagdag ng agent ang key na yun sa tamang English locale file na may kasamang JSDoc comment na nagpapaliwanag kung ano yung string at saan ito ginagamit. Mahalagang context ito kapag ginagawa na ang translations sa ibang wika, para maintindihan ng LLM kung ang ibig sabihin ng "Save" ay "Save Game Configuration" o "Save Your Draw & Guess Drawing".
Quality Control
Mabilis kong nireview lahat ng code na na-generate. Nakakagulat na maayos ang gawa ng mga agent, pero paminsan-minsan nagkakamali rin sila, tulad ng paglalagay ng useTranslation hook pagkatapos ng maagang return statement.
Sobrang laki ng naitulong ng strongly-typed translations. Dahil dito, nakasiguro akong lahat ng translation sa bawat locale ay may tamang keys (at walang maling keys). Sigurado rin na lahat ng tawag sa t() at interpolate() ay gumagamit ng mga tunay na translation string na talagang meron.
Kinukuha ng type system ang lahat ng posibleng translation keys mula sa English source files:
/**
* Extracts all possible paths from a nested object type, creating dot-notation keys.
* Example: {a: string, b: {c: string, d: {e: string}}} → 'a' | 'b.c' | 'b.d.e'
*/
type ExtractPaths<T, Prefix extends string = ''> = T extends string
? Prefix extends '' ? never : Prefix
: T extends object
? {
[K in keyof T]: K extends string | number
? T[K] extends string
? Prefix extends '' ? `${K}` : `${Prefix}.${K}`
: ExtractPaths<T[K], Prefix extends '' ? `${K}` : `${Prefix}.${K}`>
: never
}[keyof T]
: never;
export type TranslationKeys =
| ExtractPaths<typeof import('./locales/en/index').default>
| `misc/${ExtractPaths<typeof import('./locales/en/misc').default>}`
| `games/chess/${ExtractPaths<typeof import('./locales/en/games/chess').default>}`
| `games/pool/${ExtractPaths<typeof import('./locales/en/games/pool').default>}`
// ... and so on for all games
Nagbibigay ito ng perpektong TypeScript autocomplete, at anumang typo sa translation key ay nahuhuli agad sa compile time. Hindi makakagawa ang mga agent ng mali tulad ng t('games/ches/name') dahil agad itong ifi-flag ng TypeScript.
Localization
Pag natapos na yung English conversion, hinati ko naman yung natitirang locale tasks. Ginawa kong responsable ang bawat agent sa pag-convert ng isang English locale file papunta sa isang partikular na wika.
Halimbawa, binigyan ko ang mga agent ng ganitong prompt:
Please ensure that ar/games/dinomight.ts has all the translations from en/games/dinomight.ts.
Use `export const account: DinomightTranslations = {`.
Iterate until there are no more type errors for your translation file (if you see errors for other files, ignore them--you are running in parallel with other agents that are responsible for those other files).
Your translations must be excellent and correct for the jsdoc context provided in en.
You must do this manually and without writing "helper" scripts, and with no shortcuts.
Naisip ko ring pagawain si Cursor ng script na ipapasa ang bawat file sa isang LLM para ito na ang mag-generate ng translations, pero gusto ko ring magtipid nang kaunti sa LLM cost. Mas magandang approach yung gumamit ng script na ina-update lang ang mga nawawalang translations, at malamang gagamit ulit ako ng kahalintulad na solusyon sa hinaharap. Gusto ko rin sanang matrack kung aling mga string ang kailangan ng update o translation, pero gusto ko munang panatilihing simple ang setup. Posibleng ilipat ko balang araw ang translation work sa isang database o kung ano pa man.
Nagdagdag din ako ng "debug" locale na available lang sa development. Pinapayagan nito na makita ko lahat ng napalitang string para ma-verify kung gumagana ang lahat (plus tingin ko cool siya). Kapag ginamit mo ang debug locale, ibinabalik ng t() ang key na naka-wrap sa brackets:
if (targetLocale === 'debug') {
return `⟦${key}⟧`;
}
Kaya sa halip na makita mo ang "Welcome to Foony!", makikita mo ⟦welcome⟧, na nagpapadali para makita agad kung may nawawalang translations.
Sa huli, isang agent pa ang nag-implement ng /{locale}/** routing para ang mga URL tulad ng /ja/games/chess ay mapunta sa tamang wika (sa kasong ito, Japanese).
Pagsasalin ng Blog
Iba pa yung pagsasalin ng UI strings, pero paano naman yung blog posts? Wala akong balak mag-setup at mag-manage pa ng mas maraming agent para lang isalin lahat ng blog post ko.
Na-solve ko ito sa pamamagitan ng pagpapagawa sa isang agent ng script (scripts/src/generateBlogTranslations.ts) na nag-a-automate ng buong proseso.
Ganito ito gumagana:
- Sini-scan nito ang
client/src/posts/endirectory para sa mga English MDX file. - Tine-check nito kung may nawawalang translations sa ibang locale folders (hal.
posts/ja,posts/es). - Kapag may kulang na translation, babasahin nito ang English content at ipapasa sa Gemini 3 Pro Preview na may specific na prompt para isalin ang content habang pinapanatiling buo ang Markdown formatting.
- Sine-save nito ang bagong file sa tamang lokasyon.
Sa frontend, gumagamit ako ng import.meta.glob para dynamic na i-import lahat ng MDX files na ito. Ang PostPage component ko naman ay simpleng chine-check ang kasalukuyang locale ng user at i-lazy-load ang tamang MDX file. Kapag kulang pa ang translation (dahil hindi ko pa napaandar ang script), maayos itong babalik sa English.
Konklusyon
Sa puntong ito, meron na akong fully-functioning na site na naisalin sa lahat ng 20 locales!
Sobrang wild ng 3 araw na yun, pero ang resulta ay isang fully localized na site na pakiramdam (kadalasan) ay native para sa mga user sa iba't ibang panig ng mundo. Sa paggawa ng custom, magaang library at paggamit ng AI agents para sa mga nakakaantok na refactoring, nagawa ko ang isang bagay na halos imposible pa siguro isang taon lang ang nakalipas: full i18n sa loob ng 3 araw para sa isang komplikadong website, gawa ng isang engineer lang. Ang future ng programming ay hindi lang tungkol sa pagsulat ng code nang mabilis. Tungkol ito sa pag-o-orchestrate ng AI agents at pagkakaroon ng malalim na domain expertise para ma-verify ang output nila.