

1/1/1970
मैंने 3 दिनों में 20 भाषाओं के लिए i18n कैसे लागू किया
नमस्ते! मैंने अभी एक विशाल काम पूरा किया है जिसमें मैंने Foony को 20 अलग-अलग भाषाओं में अनुवादित किया। यह एक बहुत बड़ा काम था जिसमें कोडबेस की लगभग हर फाइल को छूना पड़ा, लेकिन मैंने इसे केवल 3 दिनों में पूरा कर लिया।
नीचे मैं समझाऊंगा कि मैंने यह कैसे किया, बदलाव के पीछे के विशिष्ट आँकड़े, और मैंने उद्योग मानक का उपयोग करने के बजाय अपनी खुद की अनुवाद लाइब्रेरी (एक बार फिर) क्यों बनाने का फैसला किया।
i18next क्यों नहीं?
जब मैंने पहली बार अनुवाद जोड़ने पर विचार किया, तो मैंने उद्योग मानक पर सोचा: i18next और react-i18next।
इसके बजाय, मैंने AI द्वारा अनुरक्षणीयता के लिए ऑप्टिमाइज़ करने का फैसला किया। i18next शक्तिशाली है, लेकिन इसकी API विविधता LLMs को भ्रमित कर सकती है या असंगत कोड लिखवा सकती है। लाइब्रेरी को एक सरल t() और interpolate() तक सीमित करके, मैंने सुनिश्चित किया कि 10+ समानांतर एजेंट लगभग शून्य मानवीय हस्तक्षेप के साथ 100% टाइप-सुरक्षित कोड लिख सकें।
मैं एक बड़े इकोसिस्टम में फँसने को लेकर भी सावधान था जो बाद में ब्रेकिंग चेंजेस ला सकता है। React Router v5 और MUI v4 → v5 जैसे दर्दनाक माइग्रेशन से जलने के बाद, मुझे पता है कि JavaScript की दुनिया में बैकवर्ड-कंपैटिबिलिटी का तेज़ी से टूटना बहुत आम है। बाद में प्लूरलाइज़ेशन फीचर्स जोड़ने की लागत अभी 139k लाइन कोड मैन्युअली माइग्रेट करने की लागत से कम है।
मुझे कुछ बिल्कुल सरल, बेहद हल्का, और अपनी टीम की ज़रूरतों के अनुरूप चाहिए था।
तो मैंने अपनी खुद की लिख डाली।
मैंने एक 3 KB का सीमित सबसेट बनाया जो विशेष रूप से उच्च-सटीकता वाले, स्वायत्त AI रीफैक्टरिंग को सक्षम करने के लिए डिज़ाइन किया गया था। इसने मुझे एक अकेले इंजीनियर के रूप में 5 लोगों की टीम का 3 हफ्ते का काम केवल 3 दिनों में पूरा करने की अनुमति दी।
कस्टम कार्यान्वयन
मैंने एक न्यूनतम i18n लाइब्रेरी तैयार की जो लगभग 3 KB gzipped है। यह दो मुख्य फंक्शन एक्सपोज़ करती है: नॉन-React संदर्भों के लिए getTranslation() और कंपोनेंट्स के लिए एक useTranslation() हुक।
ये सरल स्ट्रिंग प्रतिस्थापन के लिए t() और जब मुझे अनुवाद स्ट्रिंग में React कंपोनेंट्स इंजेक्ट करने हों (जैसे लिंक या आइकॉन) तब interpolate() लौटाते हैं। दोनों फंक्शन वेरिएबल प्रतिस्थापन का समर्थन करते हैं, जैसे "Hello {{thing}}", {thing: 'World'}।
कीज़ "स्लैश-डॉट" नोटेशन का पालन करती हैं (लोकलाइज़ेशन फाइल के पथ के लिए स्लैश, फाइल में नेस्टेड ऑब्जेक्ट्स के लिए डॉट्स)। विशिष्टता सुनिश्चित करने के लिए, एक फाइल में अनुवाद कीज़ में फॉरवर्ड-स्लैश नहीं हो सकते।
यहाँ कोर 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 हुक:
export function useTranslation() {
const [language] = useLanguage();
// Subscribe to locale loading events to trigger re-renders when translations are loaded
const version = useSyncExternalStore(
(callback) => LocaleQueryer.onLoad(callback),
() => LocaleQueryer.getVersion(),
() => LocaleQueryer.getVersion()
);
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]);
}
पूरी लाइब्रेरी का कोर केवल लगभग 580 लाइनों का कोड है। यह संभालता है:
- अनुवाद फाइलों की लेज़ी-लोडिंग ताकि हम हर उपयोगकर्ता को सभी 20 भाषाएं न भेजें।
- "नेमस्पेस" द्वारा अनुवादों का कोड-स्प्लिटिंग (जैसे
common,misc,games/{gameId})। - एक "debug" लोकेल जो रॉ कीज़ दिखाता है ताकि मैं सत्यापित कर सकूँ कि सब कुछ सही ढंग से जुड़ा हुआ है।
सिस्टम को बनाए रखना आसान बनाए रखने के लिए, मैंने shared/src/i18n/README.md में व्यापक डॉक्यूमेंटेशन भी जोड़ा, जो फाइल संरचना से लेकर क्लाइंट और सर्वर दोनों के लिए उपयोग के उदाहरणों तक सब कुछ कवर करता है। चूंकि मैं मानक लाइब्रेरी का उपयोग नहीं कर रहा हूँ, इसलिए नए टीम सदस्यों को ऑनबोर्ड करने (या बस अपने भविष्य के स्वयं या LLMs को याद दिलाने) के लिए यह संदर्भ होना महत्वपूर्ण है।
आँकड़ों में
इस अपडेट के पैमाने का अंदाज़ा देने के लिए, यह है कि कोडबेस में क्या बदला:
- 20 भाषाएँ समर्थित (साथ ही डेव के लिए एक डिबग लोकेल)।
- 360 लोकेल फाइलें बनाई गईं।
- 139,031 लाइनें अनुवाद कोड।
- क्लाइंट में जोड़े गए 3,938
t()कॉल्स। - 728 सोर्स फाइलें संशोधित।
- 18 अंग्रेज़ी सोर्स फाइलें जो सत्य के स्रोत के रूप में काम करती हैं (16 गेम्स + common + misc)।
एजेंट्स के साथ ऑर्केस्ट्रेशन
इसे मैन्युअली करने में महीनों का दिमाग सुन्न कर देने वाला, यांत्रिक काम लगता। इसके बजाय, मैंने एक दर्जन से अधिक Cursor एजेंट्स को एक साथ ऑर्केस्ट्रेट किया ताकि वे भारी काम कर सकें।
मैंने कोडबेस को फ़ोल्डर्स के आधार पर "सेक्शन्स" में बाँटकर शुरुआत की। Foony पर हर गेम को अपना फ़ोल्डर और अपना अनुवाद नेमस्पेस मिला। यह प्रारंभिक लोड साइज़ को छोटा रखता है क्योंकि आप केवल उसी गेम के अनुवाद लोड करते हैं जो आप खेल रहे हैं।
मैंने एक साथ कई Cursor एजेंट्स चलाए। मैंने प्रत्येक एजेंट को एक विशिष्ट सेक्शन सौंपा, जैसे "Chess गेम को अनुवादों का उपयोग करने में बदलें," और यह फाइल-दर-फाइल जाता, उपयोगकर्ता-सामना करने वाली स्ट्रिंग्स ढूंढता और उन्हें t('games/chess/some.key') से बदलता।
फिर एजेंट उस की को उपयुक्त अंग्रेज़ी लोकेल फाइल में एक JSDoc टिप्पणी के साथ जोड़ देता जो स्ट्रिंग के "क्या" और "कहाँ" को समझाती। यह संदर्भ अन्य भाषाओं के लिए अनुवाद उत्पन्न करते समय महत्वपूर्ण है, क्योंकि यह LLM को समझने में मदद करता है कि "Save" का मतलब "Save Game Configuration" है या "Save Your Draw & Guess Drawing"।
गुणवत्ता नियंत्रण
मैंने जल्दी से जनरेट किए गए सभी कोड की समीक्षा की। एजेंट्स आश्चर्यजनक रूप से अच्छे थे, लेकिन उन्होंने कभी-कभी गलतियाँ कीं, जैसे useTranslation हुक को एक प्रारंभिक return स्टेटमेंट के बाद रखना।
मज़बूती से टाइप किए गए अनुवादों ने बहुत मदद की। इसने सुनिश्चित किया कि प्रत्येक लोकेल के लिए सभी अनुवादों में सभी सही कीज़ हों (और कोई गलत न हो)। इसने यह भी सुनिश्चित किया कि t() और interpolate() के कॉल्स वास्तविक अनुवाद स्ट्रिंग्स का उपयोग करें जो मौजूद हों।
टाइप सिस्टम अंग्रेज़ी सोर्स फाइलों से सभी संभावित अनुवाद कीज़ निकालता है:
/**
* 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 ऑटोकंप्लीट देता है, और अनुवाद की में कोई भी टाइपो कंपाइल टाइम पर पकड़ी जाती है। एजेंट्स t('games/ches/name') जैसी गलतियाँ नहीं कर सकते क्योंकि TypeScript तुरंत इसे फ्लैग करता है।
लोकलाइज़ेशन
जब अंग्रेज़ी रूपांतरण हो गया, तो मैंने बाकी लोकेल कार्यों को विभाजित किया। मैंने प्रत्येक एजेंट को एक एकल अंग्रेज़ी लोकेल फाइल को निर्दिष्ट भाषा में बदलने के लिए ज़िम्मेदार बनाया।
उदाहरण के लिए, मैंने एजेंट्स को इस तरह का प्रॉम्प्ट दिया:
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.
मैंने सोचा था कि Cursor से एक स्क्रिप्ट बनवाऊँ जो इन फाइलों को LLM में फीड करे और चीज़ें जनरेट करवाए, लेकिन मैं LLM की लागत पर थोड़ी बचत करना चाहता था। केवल गायब अनुवादों को अपडेट करने के लिए स्क्रिप्ट का उपयोग करना बेहतर तरीका था, और मैं भविष्य में शायद ऐसा ही समाधान अपनाऊँगा। मैं ट्रैक करना चाहता हूँ कि कौन सी स्ट्रिंग्स को अपडेट / अनुवाद की ज़रूरत है, लेकिन चीज़ें सरल रखना चाहता हूँ। मैं अनुवाद का काम डेटाबेस या कुछ और में ले जा सकता हूँ।
मैंने एक "debug" लोकेल भी जोड़ा जो केवल विकास में उपलब्ध है। यह मुझे यह सत्यापित करने के लिए सभी प्रतिस्थापित स्ट्रिंग्स देखने देता है कि चीज़ें काम कर रही हैं (साथ ही मुझे लगता है यह कूल है)। जब आप डिबग लोकेल का उपयोग करते हैं, तो t() ब्रैकेट्स में लिपटी हुई की लौटाता है:
if (targetLocale === 'debug') {
return `⟦${key}⟧`;
}
तो "Welcome to Foony!" देखने के बजाय, आप ⟦welcome⟧ देखेंगे, जिससे किसी भी गायब अनुवाद को पहचानना आसान हो जाता है।
अंत में, एक अन्य एजेंट ने /{locale}/** रूटिंग लागू की ताकि /ja/games/chess जैसी चीज़ें सही भाषा में रूट हों (इस मामले में जापानी)।
ब्लॉग का अनुवाद
UI स्ट्रिंग्स का अनुवाद एक बात थी, लेकिन ब्लॉग पोस्ट्स का क्या? मैं अपनी सभी ब्लॉग पोस्ट्स का अनुवाद करने के लिए और एजेंट्स को स्पिन-अप और मैनेज नहीं करना चाहता था।
मैंने एक एजेंट से एक स्क्रिप्ट (scripts/src/generateBlogTranslations.ts) बनवाकर इसे हल किया जो पूरी प्रक्रिया को स्वचालित करती है।
यह इस तरह काम करता है:
- यह
client/src/posts/enडायरेक्टरी को अंग्रेज़ी MDX फाइलों के लिए स्कैन करता है। - यह अन्य लोकेल फ़ोल्डर्स (जैसे
posts/ja,posts/es) में गायब अनुवादों की जाँच करता है। - यदि कोई अनुवाद गायब है, तो यह अंग्रेज़ी सामग्री पढ़ता है और इसे Gemini 3 Pro Preview में एक विशिष्ट प्रॉम्प्ट के साथ फीड करता है ताकि Markdown फॉर्मेटिंग को संरक्षित करते हुए सामग्री का अनुवाद किया जा सके।
- यह नई फाइल को सही स्थान पर सहेजता है।
फ्रंटएंड पर, मैं इन सभी MDX फाइलों को डायनामिक रूप से इंपोर्ट करने के लिए import.meta.glob का उपयोग करता हूँ। मेरा PostPage कंपोनेंट तब बस उपयोगकर्ता के वर्तमान लोकेल की जाँच करता है और सही MDX फाइल को लेज़ी-लोड करता है। यदि कोई अनुवाद गायब है (क्योंकि मैंने अभी तक स्क्रिप्ट नहीं चलाई है), तो यह सहजता से अंग्रेज़ी पर वापस आ जाता है।
दिन 4: स्वचालित अनुवाद जनरेशन
मुझे पता था कि मूल समाधान स्केल नहीं करेगा। तो, अब जब मेरे पास i18n था, इसे डेटाबेस-संचालित दृष्टिकोण के साथ थोड़ा मज़बूत करने का समय आ गया था।
संक्षेप में: जब अंग्रेज़ी टेक्स्ट या JSDoc टिप्पणियाँ बदलीं, तो अनुवादों को फिर से जनरेट करने की ज़रूरत थी। क्या अपडेट करने की ज़रूरत है इसका मैन्युअल ट्रैकिंग गलतियों से भरी और डेवलपर के समय की बर्बादी होती।
तो मैंने वह समाधान बनाया जिसकी मैंने मूल रूप से योजना बनाई थी: एक PostgreSQL-समर्थित अनुवाद जनरेशन सिस्टम।
डेटाबेस स्कीमा
मैंने अपने PostgreSQL डेटाबेस में निम्न संरचना के साथ एक translations टेबल जोड़ा:
key: "स्लैश-डॉट" नोटेशन में अनुवाद की (जैसे"games/yacht/nested.name","config.timeLimit.label")।en_value: अंग्रेज़ी सोर्स वैल्यूtarget_locale: टार्गेट लोकेल कोड (जैसे"es","fr","zh")target_value: अनुवादित वैल्यूcontext: एक JSONB फील्ड जिसमें इस की और सभी पूर्वज कीज़ के लिए JSDoc हैcreated_atऔरupdated_at: ट्रैकिंग के लिए टाइमस्टैम्प्स
विशिष्ट इंडेक्स (key, target_locale, en_value, context) पर है। यह महत्वपूर्ण है: विशिष्ट कन्स्ट्रेंट में context शामिल करके, हम स्वचालित रूप से पहचान सकते हैं कि JSDoc टिप्पणियाँ कब बदलती हैं और अनुवादों को फिर से जनरेट कर सकते हैं। पुराने अनुवाद ऐतिहासिक संदर्भ के लिए रखे जाते हैं।
जनरेशन स्क्रिप्ट
मैंने scripts/src/generateLocalizations.ts बनाया जो पूरे अनुवाद वर्कफ़्लो को स्वचालित करता है:
- अंग्रेज़ी कीज़ निकालता है:
shared/src/i18n/locales/en/**फाइलों से सभी अनुवाद कीज़ निकालने के लिए AST पार्सिंग (ts-morph) का उपयोग करता है, केवल डिफ़ॉल्ट एक्सपोर्ट्स को प्रोसेस करता है - JSDoc संदर्भ निकालता है: समृद्ध संदर्भ प्रदान करने के लिए प्रत्येक की और सभी पूर्वज कीज़ (पैरेंट ऑब्जेक्ट्स) के लिए JSDoc टिप्पणियों को पार्स करता है
- डेटाबेस को क्वेरी करता है: PostgreSQL में मौजूदा अनुवादों की जाँच करता है,
key,target_locale,en_value, औरcontextपर मिलान करता है: यदि इनमें से कुछ भी बदलता है, तो अनुवाद फिर से जनरेट होता है। - गायब/बदली हुई कीज़ की पहचान करता है: ऐसी कीज़ ढूंढता है जिन्हें अनुवाद की ज़रूरत है या जिनकी अंग्रेज़ी वैल्यू/टिप्पणियाँ बदल गई हैं
- अनुवादों को बैच करता है: अधिक कुशल LLM कॉल्स के लिए लोकेल और नेमस्पेस प्रिफिक्स द्वारा समूह बनाता है (अनुवादों को तेज़ भी बनाता है)। हालाँकि, यदि बैच बहुत बड़ा है, तो अनुवाद की गुणवत्ता खराब होगी।
- अनुवाद जनरेट करता है: व्यापक संदर्भ (JSDoc, भाषा+क्षेत्र, टोन, शब्दावली, उदाहरण) के साथ GPT 5.1 का उपयोग करता है। मैंने पढ़ा है कि लेखन के लिए 5.1 5.2 से बेहतर है (फीका नहीं लगता), लेकिन इसकी पुष्टि नहीं की है।
- QA जाँच: प्लेसहोल्डर संरक्षण, जैसे
{{name}}, की अखंडता, JSON फ़ॉर्मेट को मान्य करता है - डेटाबेस में स्टोर करता है: पूर्ण संदर्भ (JSDoc + पूर्वज JSDoc) के साथ अनुवाद सहेजता है
- लोकेल फाइलें जनरेट करता है: डेटाबेस से पढ़ता है और
RecursivePartialटाइप्स के साथ ठीक से फॉर्मेट की गई TypeScript लोकेल फाइलें लिखता है
मुख्य लाभ
यह दृष्टिकोण हमें कई DevEx सुधार देता है:
- स्वचालित पुनर्जनन: जब अंग्रेज़ी टेक्स्ट या JSDoc टिप्पणियाँ बदलती हैं, तो अनुवाद स्वचालित रूप से पुनर्जनन होते हैं। तो यदि कोई कहता है कि एक अनुवाद खराब है, तो टिप्पणी के रूप में अधिक संदर्भ प्रदान करके अनुवादों को पुनर्जनन करना वास्तव में आसान है।
- समृद्ध संदर्भ: JSDoc टिप्पणियाँ अनुवाद संदर्भ प्रदान करती हैं (जैसे "खिलाड़ियों को दिखाया गया त्रुटि संदेश, अधिकतम 15 अक्षर"), LLM को अधिक सटीक अनुवाद बनाने में मदद करती हैं
- पूर्वज संदर्भ: पैरेंट ऑब्जेक्ट JSDoc नेमस्पेस संदर्भ प्रदान करता है (जैसे "एक गेम में होने के लिए उपलब्धि जहाँ सभी अंडे नष्ट हो जाते हैं"), थोड़ी अधिक स्पष्टता देता है
- ऐतिहासिक ट्रैकिंग: पुराने अनुवाद डेटाबेस में सहेजे जाते हैं। वे ज़्यादा जगह नहीं लेते, इसलिए मुझे अभी उन्हें हटाने का कोई कारण नहीं दिखता, और इतिहास देखना कूल है।
तकनीकी विवरण
विश्वसनीयता और दक्षता सुनिश्चित करने के लिए कार्यान्वयन कई तकनीकों का उपयोग करता है:
- सही टिप्पणियाँ प्राप्त करने के लिए AST-आधारित निष्कर्षण
- समवर्ती बैच अनुवाद के लिए Semaphore का उपयोग करके समानांतर प्रसंस्करण
- API विफलताओं के लिए एक्सपोनेंशियल बैकऑफ रीट्राई लॉजिक। LLM कॉल्स कुख्यात रूप से अस्थिर हैं।
स्क्रिप्ट को scripts डायरेक्टरी से npm run generate-localizations के साथ चलाया जा सकता है। यह PostgreSQL से कनेक्ट होता है और चलने पर सभी समर्थित लोकेल्स के लिए सभी गायब या बदले हुए अनुवादों को प्रोसेस करता है।
निष्कर्ष
इस बिंदु पर, मेरे पास सभी 20 लोकेल्स में अनुवादित एक पूरी तरह से कार्यशील साइट थी!
ये पागल 3 दिन थे, लेकिन परिणाम एक पूरी तरह से लोकलाइज़्ड साइट है जो दुनिया भर के उपयोगकर्ताओं के लिए (अधिकांशतः) नेटिव लगती है। एक कस्टम, हल्की लाइब्रेरी बनाकर और थकाऊ रीफैक्टरिंग कार्य के लिए AI एजेंट्स का लाभ उठाकर, मैंने वह कर दिखाया जो केवल एक साल पहले असंभव होता: 1 इंजीनियर द्वारा एक जटिल वेबसाइट के लिए 3 दिनों में पूर्ण i18n। प्रोग्रामिंग का भविष्य तेज़ी से कोड लिखने के बारे में नहीं है। यह AI एजेंट्स को ऑर्केस्ट्रेट करने और उनके आउटपुट को सत्यापित करने के लिए गहरी डोमेन विशेषज्ञता रखने के बारे में है।