

1/1/1970
আমি কীভাবে মাত্র ৩ দিনে ২০টা ভাষায় i18n ইমপ্লিমেন্ট করলাম
হ্যালো! একদম এখনই আমি একটা বিশাল কাজ শেষ করলাম, যেখানে আমি Foony‑কে ২০টা আলাদা ভাষায় নিয়ে গেছি। পুরো কোডবেসের প্রায় প্রতিটা ফাইলে হাত দিতে হয়েছে, কিন্তু তবু সবকিছু গুছিয়ে ফেলতে পেরেছি মাত্র ৩ দিনেই।
নিচে আমি ভেঙে বলছি কীভাবে করেছি, পেছনের সংখ্যাগুলো কেমন ছিল, আর কেন আবারও ইন্ডাস্ট্রি স্ট্যান্ডার্ড লাইব্রেরি না নিয়ে নিজের ট্রান্সলেশন লাইব্রেরি বানানোর পথেই হাঁটলাম।
কেন i18next নয়?
প্রথম যখন ট্রান্সলেশন যোগ করার কথা ভাবলাম, তখন সবার আগে মাথায় এল ইন্ডাস্ট্রি স্ট্যান্ডার্ড i18next আর react-i18next।
কিন্তু শেষ পর্যন্ত আমি অপটিমাইজ করেছি AI দিয়ে যতটা সহজে মেইনটেইন করা যায় সেটা ভেবে। i18next শক্তিশালী ঠিকই, কিন্তু এর API খুব বৈচিত্র্যময়, এগুলো LLM গুলোর অনেক সময় হ্যালুসিনেশন বা অদ্ভুত, অমিল কোড লিখে ফেলতে বাধ্য করে। আমি লাইব্রেরিটাকে শুধু দুইটা জিনিসে সীমাবদ্ধ রেখেছি: t() আর interpolate()। এতে করে ১০টার বেশি প্যারালাল এজেন্ট একসাথে কাজ করেও একদম টাইপ-সেইফ কোড লিখতে পারে, আর মানুষের হস্তক্ষেপও প্রায় লাগেনি।
আরেকটা দিক থেকে আমি একটু ভয় পাচ্ছিলাম যে বড় একটা ইকোসিস্টেমে ঢুকে পড়ে পরে হঠাৎ কোনো বড় ব্রেকিং চেঞ্জ ফেস করতে হতে পারে। আগে থেকেই React Router v5 থেকে মাইগ্রেশন, বা React Router v5 আর MUI v4 → v5 টাইপের কষ্টকর অভিজ্ঞতা আছে, তাই JavaScript দুনিয়ায় দ্রুত ব্যাকওয়ার্ড-কম্প্যাটিবিলিটি ভেঙে ফেলার অভ্যাসটা খুব ভালো করেই জানি। ভবিষ্যতে আলাদা করে pluralization ফিচার যোগ করার খরচ এখনই ১,৩৯,০০০ লাইনের কোড ম্যানুয়ালি মাইগ্রেট করার খরচের চেয়ে অনেক কম।
আমার দরকার ছিল যতটা সম্ভব সিম্পল, একদম হালকা, আর আমাদের টিমের দরকার অনুযায়ী কাস্টমাইজ করা কিছু।
তাই নিজেই বানিয়ে ফেললাম।
আমি প্রায় ৩ KB সাইজের একটা কনস্ট্রেইনড সাবসেট বানালাম, যেটা হাই-অ্যাকিউরেসি, অটোনমাস AI রিফ্যাক্টরিং টার্গেট করেই ডিজাইন করা। এর ফলে আমি একা একজন ইঞ্জিনিয়ার হয়েও ৫ জনের টিমের ৩ সপ্তাহের কাজ গুছিয়ে ফেলতে পেরেছি ৩ দিনেই।
কাস্টম ইমপ্লিমেন্টেশন
আমি একটা মিনিমাল i18n লাইব্রেরি বানালাম, কমপ্রেস করলে প্রায় ৩ KB gzipped হয়। এটা মূলত দুইটা ফাংশন এক্সপোজ করে: React এর বাইরের কনটেক্স্টের জন্য getTranslation() আর কম্পোনেন্টের জন্য একটা useTranslation() হুক।
দুটোই t() রিটার্ন করে সিম্পল স্ট্রিং রিপ্লেসমেন্টের জন্য, আর interpolate() রিটার্ন করে যখন কোনো ট্রান্সলেশন স্ট্রিংয়ের মধ্যে React কম্পোনেন্ট (ধরো লিংক বা আইকন) ইনজেক্ট করতে হয়। দুইটাই ভ্যারিয়েবল রিপ্লেসমেন্ট সাপোর্ট করে, যেমন "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();
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]);
}
পুরো লাইব্রেরির কোর অংশটা মোটামুটি ৫৮০ লাইনের কোড। এটা যেগুলো হ্যান্ডেল করে:
- ট্রান্সলেশন ফাইলগুলো lazy-load করে, যাতে প্রত্যেক ইউজারের কাছে একসাথে সব ২০টা ভাষা পাঠাতে না হয়।
- "namespace" ধরে কোড-স্প্লিট করে (যেমন
common,misc,games/{gameId})। - একটা "debug" locale আছে, যেটা কাঁচা কী গুলো দেখায়, যাতে সহজে চেক করতে পারি সব ঠিকমতো ওয়্যারড আপ হয়েছে কি না।
সিস্টেমটা যেন ভবিষ্যতেও সহজে মেইনটেইন করা যায়, তাই shared/src/i18n/README.md ফাইলের মধ্যে বেশ ডিটেইল ডকুমেন্টেশন লিখে রেখেছি। সেখানে ফাইল স্ট্রাকচার থেকে শুরু করে client আর server দুই জায়গায় ব্যবহারের উদাহরণ সবই আছে। আমি যেহেতু স্ট্যান্ডার্ড লাইব্রেরি ইউজ করছি না, তাই নতুন টিমমেট অনবোর্ড করাতে, বা ভবিষ্যতের নিজেকে (বা LLM গুলোকে) মনে করিয়ে দিতে এই রেফারেন্সটা খুব জরুরি।
সংখ্যার হিসাবে
এই আপডেটটার স্কেলটা একটু বোঝানোর জন্য, কোডবেসে মোটামুটি যা যা বদলেছে:
- ২০টা ভাষা সাপোর্ট (এর বাইরে dev এর জন্য অতিরিক্ত একটা debug locale)।
- ৩৬০টা locale ফাইল তৈরি।
- ১,৩৯,০৩১ লাইনের ট্রান্সলেশন কোড।
- ক্লায়েন্ট জুড়ে ৩,৯৩৮টা
t()কল যোগ হয়েছে। - ৭২৮টা সোর্স ফাইল মডিফাই হয়েছে।
- ১৮টা ইংরেজি সোর্স ফাইলই সব কিছুর সোর্স অফ ট্রুথ (১৬টা গেম + common + misc)।
এজেন্ট দিয়ে সমন্বয় করা
এগুলো যদি সব ম্যানুয়ালি করতাম, তাহলে কয়েক মাসের একঘেয়ে, যান্ত্রিক কাজ হয়ে যেত। তার বদলে আমি একসাথে ডজনেরও বেশি Cursor এজেন্ট চালিয়েছি, আর তারাই ভারী কাজগুলো সামলেছে।
প্রথমে আমি কোডবেসটাকে ফোল্ডার ধরে ধরে "সেকশন" এ ভাগ করেছি। Foony তে প্রতিটা গেমের জন্য আলাদা ফোল্ডার, আর আলাদা ট্রান্সলেশন namespace রেখেছি। এতে করে ইনিশিয়াল লোড সাইজ ছোট থাকে, কারণ তুমি শুধু যে গেমটা খেলছো, তার ট্রান্সলেশনটাই লোড হয়।
আমি একসাথে একাধিক Cursor এজেন্ট চালিয়েছি। প্রতিটা এজেন্টকে আলাদা কাজ দিয়েছি, যেমন "Chess গেমটাকে ট্রান্সলেশনে কনভার্ট করো"। তারপর ওরা ফাইল ধরে ধরে গেছে, সব user-facing স্ট্রিং খুঁজে বের করেছে, আর সেগুলোকে t('games/chess/some.key') দিয়ে রিপ্লেস করেছে।
তারপর এজেন্ট সেই কীটাকে সঠিক ইংরেজি locale ফাইলে অ্যাড করেছে, আর সেই স্ট্রিংটা কী আর কোথায় ব্যবহার হচ্ছে তা নিয়ে একটা ছোট JSDoc কমেন্ট লিখেছে। এই কনটেক্স্টটা পরের ভাষাগুলোর ট্রান্সলেশন জেনারেট করার সময় অনেক কাজে লাগে, কারণ তখন LLM সহজে বুঝতে পারে "Save" বলতে কি বোঝানো হয়েছে, "Save Game Configuration", নাকি "Save Your Draw & Guess Drawing"।
কোয়ালিটি কন্ট্রোল
এজেন্টগুলো যে কোড জেনারেট করেছে, সব আমি ঝটপট রিভিউ করেছি। এজেন্টগুলো আশার চেয়ে ভালো কাজ করেছে, কিন্তু ভুল একেবারেই করেনি তা না, যেমন মাঝে মাঝে useTranslation হুককে আগের দিকেই এমন জায়গায় বসিয়ে দিয়েছে, যেখানে তার আগে একটা return হয়ে গেছে।
স্ট্রংলি-টাইপড ট্রান্সলেশন এখানে দারুণ হেল্প করেছে। এতে নিশ্চিত হওয়া গেছে, প্রতিটা locale এর জন্য সব সঠিক কী আছে (আর অপ্রাসঙ্গিক কিছু নেই)। এটাও নিশ্চিৎ হয়েছে যে 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 সাথে সাথেই ফ্ল্যাগ করে দেয়।
লোকালাইজেশন
ইংরেজি অংশটা শেষ হওয়ার পরে, বাকি locale এর কাজগুলো ভাগ করে দিলাম। প্রতিটা এজেন্টকে একটা ইংরেজি locale ফাইল থেকে একটি নির্দিষ্ট ভাষায় কনভার্ট করার দায়িত্ব দিলাম।
উদাহরণ হিসেবে, আমি এজেন্টদের এমন একটা প্রম্পট দিয়েছিলাম:
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 কস্টও বাঁচাতে চেয়েছি। শুধু মিসিং ট্রান্সলেশনগুলো আপডেট করার জন্য স্ক্রিপ্ট ইউজ করাই এখানে বেশি ভালো অপশন ছিল, আর ভবিষ্যতেও হয়তো এ রকম সল্যুশনই ব্যবহার করব। কোন কোন স্ট্রিং আপডেট বা ট্রান্সলেট করতে হবে সেটা ট্র্যাক করতে চাই, কিন্তু জিনিসটা যতটা সম্ভব সিম্পল রাখতে চাই। হয়তো ট্রান্সলেশন ডেটা কোনো ডাটাবেসে শিফট করব বা এ রকম কিছু।
আমি শুধু dev এর জন্য আলাদা একটা "debug" localeও যোগ করেছি। এটা দিয়ে সব রিপ্লেসড স্ট্রিং ঠিকমতো কাজ করছে কি না, সেটা চোখে দেখে বুঝতে পারি (আর দেখতে কুলও লাগে)। debug locale ব্যবহার করলে t() ওই কীটাকে ব্র্যাকেটের মধ্যে রিটার্ন করে:
if (targetLocale === 'debug') {
return `⟦${key}⟧`;
}
তাই উদাহরণ হিসেবে, "Welcome to Foony!" দেখার বদলে তুমি দেখবে ⟦welcome⟧, ফলে কোথাও ট্রান্সলেশন মিসিং আছে কি না সহজেই চোখে পড়ে।
সবশেষে, আরেকটা এজেন্ট /{locale}/** রাউটিং ইমপ্লিমেন্ট করেছে, যাতে /ja/games/chess টাইপের URL ঠিকমতো সেই ভাষার ভার্সনেই যায় (এখানে ধরা যাক জাপানিজ)।
ব্লগ ট্রান্সলেট করা
UI স্ট্রিংগুলো ট্রান্সলেট করা এক ধরনের কাজ, কিন্তু ব্লগ পোস্টগুলোর কী হবে? আমি আবার নতুন করে আরও এজেন্ট স্পিন আপ করে ব্লগ পোস্টগুলো ম্যানেজ করতে চাইনি।
আমি এটার সমাধান করেছি একটা এজেন্ট দিয়ে এমন একটা স্ক্রিপ্ট বানিয়ে (scripts/src/generateBlogTranslations.ts), যেটা পুরো প্রসেসটাই অটোমেট করে।
ওটা কাজ করে এভাবে:
- এটা
client/src/posts/enডিরেক্টরির সব ইংরেজি MDX ফাইল স্ক্যান করে। - অন্য locale ফোল্ডারগুলোতে (যেমন
posts/ja,posts/es) কোন কোন ট্রান্সলেশন মিসিং আছে সেটা চেক করে। - যেখানে ট্রান্সলেশন নেই, সেখানে ওটা ইংরেজি কনটেন্ট পড়েছে আর সেটাকে নির্দিষ্ট প্রম্পট সহ Gemini 3 Pro Preview এর কাছে পাঠিয়েছে, যাতে Markdown ফরম্যাট অক্ষুণ্ণ রেখে কনটেন্ট ট্রান্সলেট করে।
- তারপর নতুন ফাইলটাকে সঠিক লোকেশনে সেভ করে।
ফ্রন্টএন্ডে আমি import.meta.glob দিয়ে সব MDX ফাইল ডায়নামিক্যালি ইমপোর্ট করি। আমার PostPage কম্পোনেন্ট ইউজারের কারেন্ট locale দেখে ঠিক সেই MDX ফাইলটাই lazy-load করে। যদি কোনো ভাষার ট্রান্সলেশন এখনো না থাকে (মানে আমি এখনো স্ক্রিপ্টটা রান করিনি), তাহলে সেটা সুন্দর করে ইংরেজিতেই ফ্যালব্যাক করে।
শেষ কথা
এ পর্যায়ে এসে আমার হাতে ছিল পুরোপুরি ফাংশনাল, ২০টা locale এ ট্রান্সলেটেড একটা সাইট!
এই ৩ দিন ছিল একদম পাগল করা, কিন্তু ফলাফলটা হচ্ছে ফুলি লোকালাইজড এমন একটা সাইট, যেটা দুনিয়ার বিভিন্ন প্রান্তের ইউজারদের কাছে বেশিরভাগ সময়ই নেটিভের মতোই লাগে। হালকা, কাস্টম লাইব্রেরি বানিয়ে আর একগাদা AI এজেন্টকে বিরক্তিকর রিফ্যাক্টরিং এর কাজ ধরিয়ে দিয়ে আমি এমন কিছু করতে পেরেছি, যেটা এক বছর আগেও প্রায় অসম্ভব মনে হত: একা একজন ইঞ্জিনিয়ার হয়ে, জটিল একটা ওয়েবসাইটের ফুল i18n সেটআপ, সেটাও আবার ৩ দিনে।
প্রোগ্রামিং এর ভবিষ্যত আর শুধু দ্রুত কোড লেখার মধ্যে নেই। এখন ভবিষ্যতটা হচ্ছে কীভাবে তুমি AI এজেন্টদের অর্কেস্ট্রেট করছো, আর তাদের আউটপুট যাচাই করার মতো গভীর ডোমেইন এক্সপার্টাইজ তোমার আছে কি না, সেটার মধ্যে।