

1/1/1970
मैंने सिर्फ 3 दिनों में i18n को 20 भाषाओं में कैसे लागू किया
हाय! मैंने अभी-अभी एक बहुत बड़ा काम पूरा किया, जिसमें मैंने Foony को 20 अलग-अलग भाषाओं में अनुवाद किया. यह इतना बड़ा काम था कि मुझे कोडबेस की लगभग हर फ़ाइल को छूना पड़ा, लेकिन फिर भी मैं यह सब सिर्फ 3 दिनों में कर पाया.
आगे मैं बताऊँगा कि मैंने यह सब कैसे किया, इस बदलाव के पीछे के असली नंबर क्या हैं, और मैंने इंडस्ट्री स्टैंडर्ड की जगह फिर से अपनी खुद की ट्रांसलेशन लाइब्रेरी बनाने का फैसला क्यों किया.
i18next क्यों नहीं?
जब मैंने पहली बार ट्रांसलेशन जोड़ने के बारे में सोचा, तो मैंने सबसे पहले इंडस्ट्री स्टैंडर्ड पर नज़र डाली: i18next और react-i18next.
इसके बजाय मैंने AI के ज़रिए आसानी से संभलने वाली कोडबेस पर फोकस करने का फैसला किया. i18next ताकतवर है, लेकिन उसकी API की अलग-अलग वैरिएशन की वजह से LLMs कभी-कभी चीज़ें गढ़ लेते हैं या असंगत कोड लिख देते हैं. लाइब्रेरी को सिर्फ दो सिंपल फ़ंक्शन t() और interpolate() तक सीमित रखकर मैंने यह पक्का किया कि 10 से ज़्यादा पैरलल एजेंट लगभग बिना किसी इंसानी दखल के 100% type-safe कोड लिख सकें.
मैं एक बड़े ecosystem पर निर्भर होने से भी थोड़ा सावधान था, जो आगे चलकर breaking changes ला सकता था. React Router v5 और MUI v4 → v5 जैसी दर्दनाक migrations झेलने के बाद मुझे अच्छी तरह पता है कि JavaScript की दुनिया में backward-compatibility को जल्दी-जल्दी तोड़ देना कितना आम है. बाद में pluralization जैसी फीचर्स जोड़ने की कीमत अभी 139k लाइनों के कोड को हाथ से migrate करने की कीमत से कहीं कम है.
मैं कुछ ऐसा चाहता था जो एकदम सिंपल हो, बहुत हल्का हो, और मेरी टीम की ज़रूरतों के हिसाब से बिलकुल फिट बैठता हो.
तो फिर मैंने अपनी खुद की लाइब्रेरी लिख डाली.
मैंने लगभग 3 KB की एक छोटी-सी constrained subset बनाई, जिसे मैंने खास तौर पर high-accuracy, ऑटोनॉमस AI refactoring के लिए डिज़ाइन किया. इसकी मदद से मैं अकेला इंजीनियर होकर भी 5 लोगों की टीम का 3 हफ्तों का काम सिर्फ 3 दिनों में निपटा पाया.
कस्टम इम्प्लीमेंटेशन
मैंने एक मिनिमल i18n लाइब्रेरी बनाई जो gzipped होने पर करीब 3 KB की है. यह दो मेन फ़ंक्शन एक्सपोज़ करती है: non-React कॉन्टेक्स्ट के लिए getTranslation() और React कंपोनेंट्स के लिए useTranslation() hook.
ये फ़ंक्शन मुझे t() देते हैं सिंपल string replacement के लिए, और interpolate() तब जब मुझे किसी ट्रांसलेशन स्ट्रिंग के अंदर React components इंजेक्ट करने होते हैं (जैसे कोई लिंक या आइकन). दोनों फ़ंक्शन variable replacement भी सपोर्ट करते हैं, जैसे "Hello {{thing}}", {thing: 'World'}.
यह रहा core t() फ़ंक्शन:
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;
}
और 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]);
}
पूरी लाइब्रेरी का core सिर्फ करीब 580 लाइनों के कोड का है. यह काम करती है:
- ट्रांसलेशन फ़ाइलों को lazy-load करना ताकि हम हर यूज़र को एक साथ सारी 20 भाषाएँ न भेजें.
- ट्रांसलेशनों को "namespace" के हिसाब से code-split करना (जैसे
common,misc,games/{gameId}). - एक "debug" locale जो raw keys दिखाती है ताकि मैं देख सकूँ कि सब कुछ सही तरह से जुड़ा हुआ है.
सिस्टम को लंबे समय तक आसानी से मेंटेन किया जा सके, इसके लिए मैंने shared/src/i18n/README.md में डिटेल्ड डॉक्यूमेंटेशन भी लिखा, जो फ़ाइल स्ट्रक्चर से लेकर client और server दोनों के लिए usage examples तक सब कुछ कवर करता है. क्योंकि मैं कोई standard लाइब्रेरी इस्तेमाल नहीं कर रहा, इसलिए यह रेफरेंस नए टीम मेंबर्स को ऑनबोर्ड करने के लिए (या भविष्य वाले खुद मुझे या LLMs को याद दिलाने के लिए) बहुत ज़रूरी है कि यह सब कैसे काम करता है.
नंबरों में
आपको इस अपडेट के स्केल का अंदाज़ा देने के लिए, कोडबेस में ये सब चीज़ें बदलीं:
- 20 भाषाओं का सपोर्ट (और dev के लिए एक debug locale अलग से).
- 360 locale फ़ाइलें बनाई गईं.
- 139,031 लाइनों का ट्रांसलेशन कोड.
- पूरे client में कुल 3,938
t()कॉल्स जोड़ी गईं. - 728 source फ़ाइलों में बदलाव हुआ.
- 18 English source फ़ाइलें जो source of truth हैं (16 games + common + misc).
एजेंट्स के साथ ऑर्केस्ट्रेशन
अगर मैं यह सब मैन्युअली करता तो महीनों तक बेहूदा, दिमाग सुन्न कर देने वाला मेकैनिकल काम चलता रहता. इसकी जगह मैंने एक दर्जन से ज़्यादा Cursor agents को एक साथ चला कर सारा भारी उठापटक करवाई.
मैंने शुरुआत की कोडबेस को फ़ोल्डर के हिसाब से अलग-अलग "sections" में तोड़कर. Foony पर हर गेम के लिए उसका अलग फ़ोल्डर और अपना खुद का ट्रांसलेशन namespace रखा. इससे शुरुआती लोड साइज छोटा रहता है, क्योंकि आप सिर्फ उसी गेम की ट्रांसलेशन फ़ाइलें लोड करते हैं जो आप खेल रहे हैं.
मैंने एक साथ कई Cursor agents चलाए. हर एजेंट को मैंने एक खास सा सेक्शन दिया, जैसे "Chess गेम को ट्रांसलेशन सपोर्ट में कनवर्ट करो", और फिर वह फ़ाइल-दर-फ़ाइल जाता हुआ, यूज़र को दिखने वाले सारे स्ट्रिंग ढूँढता और उन्हें t('games/chess/some.key') से रिप्लेस कर देता.
फिर एजेंट उस key को सही वाली English locale फ़ाइल में जोड़ देता, साथ में एक JSDoc कमेंट लिखकर कि यह स्ट्रिंग क्या है और कहाँ दिखती है. यह कॉन्टेक्स्ट दूसरी भाषाओं के लिए ट्रांसलेशन जनरेट करते समय बहुत काम आता है, क्योंकि इससे LLM को समझ आता है कि "Save" का मतलब "Save Game Configuration" है या "Save Your Draw & Guess Drawing".
क्वालिटी कंट्रोल
मैंने जल्दी-जल्दी सारा जेनरेट हुआ कोड रिव्यू किया. एजेंट्स उम्मीद से कहीं बेहतर निकले, लेकिन बीच-बीच में गलती भी कर देते थे, जैसे किसी कंपोनेंट में जल्दी वाले return के बाद useTranslation hook लगा देना.
Strongly-typed ट्रांसलेशन ने यहाँ बहुत मदद की. इससे यह पक्का हो गया कि हर locale के लिए सारी सही keys मौजूद हैं (और ग़लत वाली कोई नहीं). इसने यह भी सुनिश्चित किया कि t() और interpolate() की हर कॉल किसी ऐसी ट्रांसलेशन key पर हो जो सच में मौजूद हो.
type system English source फ़ाइलों से सारी संभव ट्रांसलेशन keys निकाल लेता है:
/**
* 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
इससे TypeScript में परफ़ेक्ट autocomplete मिलता है, और ट्रांसलेशन key में कोई भी टाइपो compile time पर ही पकड़ में आ जाती है. एजेंट्स t('games/ches/name') जैसी ग़लतियाँ नहीं कर सकते, क्योंकि TypeScript तुरंत उस पर एरर दिखा देता है.
लोकलाइज़ेशन
जब English वाली conversion पूरी हो गई, तो मैंने बाकी locale वाले कामों को टुकड़ों में बाँट दिया. हर एजेंट को मैंने एक English locale फ़ाइल दी और कहा कि उसे किसी एक तय भाषा में कनवर्ट करो.
उदाहरण के लिए, मैंने एजेंट्स को कुछ ऐसा prompt दिया:
पक्का कर लो कि ar/games/dinomight.ts में en/games/dinomight.ts की सारी ट्रांसलेशन मौजूद हों।
`export const account: DinomightTranslations = {` का इस्तेमाल करो।
जब तक तुम्हारी ट्रांसलेशन फ़ाइल के लिए type errors पूरी तरह ख़त्म न हो जाएँ, तब तक iterate करते रहो (अगर तुम्हें किसी दूसरी फ़ाइल के लिए errors दिखें तो उन्हें इग्नोर करो--उन फ़ाइलों के लिए दूसरे agents पैरलल में काम कर रहे हैं)।
तुम्हारी ट्रांसलेशन en में दिए गए jsdoc कॉन्टेक्स्ट के हिसाब से बहुत बढ़िया और बिल्कुल सही होनी चाहिए।
यह सब तुम्हें मैन्युअली करना है, कोई "helper" scripts नहीं लिखनी, और कोई शॉर्टकट नहीं लगाने।
मैंने यह भी सोचा था कि Cursor से एक script बनवाऊँ जो हर फ़ाइल को LLM में फीड करे और वहीं से ट्रांसलेशन जेनरेट हो जाए, लेकिन मैं LLM की कॉस्ट थोड़ी बचाना चाहता था. सिर्फ missing ट्रांसलेशन अपडेट करने के लिए script बनाना बेहतर अप्रोच निकला, और आगे भी शायद मैं कुछ ऐसा ही करूँगा. मैं ट्रैक रखना चाहता हूँ कि कौन-कौन सी strings को अपडेट या ट्रांसलेट करने की ज़रूरत है, लेकिन अभी चीज़ों को सिंपल ही रखना चाहता हूँ. हो सकता है बाद में यह सारा ट्रांसलेशन वाला काम किसी database वगैरह पर शिफ्ट कर दूँ.
मैंने एक "debug" locale भी जोड़ा जो सिर्फ development में ही उपलब्ध है. इससे मैं सारी रिप्लेस की गई strings सीधे देख सकता हूँ और चेक कर सकता हूँ कि सब सही चल रहा है (और हाँ, मुझे यह फीचर कूल भी लगता है). debug locale इस्तेमाल करने पर t() key को ब्रैकेट में लपेटकर लौटाता है:
if (targetLocale === 'debug') {
return `⟦${key}⟧`;
}
तो अब जहाँ आपको पहले "Welcome to Foony!" दिखता था, वहाँ debug मोड में ⟦welcome⟧ दिखता है, जिससे missing ट्रांसलेशन को पकड़ना बहुत आसान हो जाता है.
आख़िर में एक और एजेंट ने /{locale}/** routing इम्प्लीमेंट की, ताकि /ja/games/chess जैसी URLs सीधे सही भाषा (इस केस में Japanese) पर रूट हो जाएँ.
ब्लॉग का ट्रांसलेशन
UI वाली strings का ट्रांसलेशन एक बात थी, लेकिन ब्लॉग पोस्ट्स का क्या? मैं अपने सारे ब्लॉग पोस्ट्स ट्रांसलेट करने के लिए और भी ज़्यादा agents चलाकर उन्हें मैनेज नहीं करना चाहता था.
मैंने यह समस्या ऐसे हल की कि एक एजेंट से एक script बनवाई (scripts/src/generateBlogTranslations.ts), जो पूरा प्रोसेस ऑटोमेट कर दे.
यह ऐसे काम करता है:
- यह
client/src/posts/enडायरेक्टरी में English MDX फ़ाइलें स्कैन करता है. - फिर यह बाकी locale फ़ोल्डरों में missing ट्रांसलेशन चेक करता है (जैसे
posts/ja,posts/es). - अगर कोई ट्रांसलेशन missing हो, तो यह English कंटेंट पढ़कर उसे Gemini 3 Pro Preview में एक खास prompt के साथ भेजता है, जो Markdown फॉर्मैटिंग बचाए रखते हुए कंटेंट ट्रांसलेट करता है.
- और फिर नई फ़ाइल को सही जगह पर सेव कर देता है.
फ़्रंटएंड पर मैं import.meta.glob का इस्तेमाल करता हूँ ताकि ये सारी MDX फ़ाइलें डायनैमिकली इम्पोर्ट हो सकें. मेरा PostPage कंपोनेंट बस यूज़र का करंट locale चेक करता है और सही MDX फ़ाइल को lazy-load कर लेता है. अगर कोई ट्रांसलेशन missing हो (क्योंकि मैंने अभी script नहीं चलाया है), तो यह आराम से English पर fallback कर जाता है.
अंत में
इस मुक़ाम पर पहुँचते-पहुँचते मेरे पास एक पूरा चलता-फिरता साइट था जो सारी 20 locales में ट्रांसलेट हो चुका था!
ये पूरे 3 दिन काफ़ी पागलपन भरे थे, लेकिन नतीजा एक ऐसा पूरी तरह लोकलाइज़्ड साइट निकला जो दुनिया भर के यूज़र्स को (ज़्यादातर) बिल्कुल नैटिव जैसा महसूस होता है. एक कस्टम, हल्की-फुल्की लाइब्रेरी बनाकर और बोरिंग refactoring वाले काम के लिए AI agents का सही इस्तेमाल करके मैं वह कर पाया जो शायद एक साल पहले नामुमकिन लगता: सिर्फ 1 इंजीनियर के लिए 3 दिन में एक कॉम्प्लेक्स वेबसाइट पर पूरा i18n. प्रोग्रामिंग का भविष्य सिर्फ तेज़ी से कोड लिखने के बारे में नहीं है. यह AI agents को ऑर्केस्ट्रेट करने और उनके आउटपुट को वेरिफ़ाई करने लायक गहरी domain expertise रखने के बारे में है.