i18n
یک موتور بینالمللیسازی مستقل از فریمورک. طراحیاش آینه مسیریاب است: هستهای کوچک (I18nCore) با یک singleton سراسری اختیاری (createI18n / useI18n) و بدون وابستگی به DOM، پس هرطور بخواهید آن را به رابط کاربری گره میزنید.
کجا به کار میآید: وقتی در یک برنامه ranui به تعویض زبان در زمان اجرا نیاز دارید. یک بار
createI18nرا صدا بزنید، سپس رشتهها را باuseI18n().t(key, params)بخوانید و زبان را باsetLocaleعوض کنید. نه به فریمورکی وابسته است و نه به DOM، پس در جاوااسکریپت ساده، در هر فریمورکی و در SSR کار میکند.
این موتور با نقطه ورود مستقل خودش یعنی ranui/i18n عرضه میشود: وارد کردنش هیچ عنصر سفارشیای ثبت نمیکند، پس صفحهای که تنها به ترجمه نیاز دارد هرگز کتابخانه کامپوننتها را با خود نمیکشد. همین خروجیها از بسته سطحبالای ranui هم در دسترساند.
شروع سریع
هنگام راهاندازی یک بار singleton را بسازید، بعد هرجا خواستید ترجمه کنید:
import { createI18n, useI18n } from 'ranui/i18n';
createI18n({
// هر زبان یک واژهنامه تخت است — کلیدها عیناً جستوجو میشوند، نه تودرتو.
messages: {
en: { 'hero.title': 'Hi {name}', 'nav.home': 'Home' },
zh: { 'hero.title': '你好 {name}', 'nav.home': '首页' },
},
fallbackLocale: 'en', // وقتی کلیدی در زبان فعال نباشد به کار میرود
persist: true, // انتخاب را زیر کلید 'ran-locale' در localStorage به یاد میسپارد
detectNavigator: true, // زبان آغازین را از ترجیحهای زبانی مرورگر برمیدارد
});
const i18n = useI18n();
i18n.t('hero.title', { name: 'Ada' }); // → "Hi Ada"
i18n.setLocale('zh'); // ذخیره میکند و به مشترکان خبر میدهد
i18n.t('hero.title', { name: 'Ada' }); // → "你好 Ada"t(key) نخست messages[activeLocale][key] را میجوید، سپس messages[fallbackLocale][key] را، و اگر هیچکدام نبود خودِ key را برمیگرداند. جایهای {param} درون رشته از آرگومان دوم پر میشوند. چون جستوجو یک دسترسی به نقشه تخت است، کلیدها رشتههای عینیاند: 'hero.title' را یک کلید بنویسید، نه شیئی تودرتو مثل { hero: { title } }.
پارامترها (درج مقدار)
بله، پیامها پارامتر زمان اجرا میگیرند. جایهایی به شکل {name} در رشته بگذارید و مقدارها را بهعنوان آرگومان دوم به t() بدهید؛ هر {param} با مقدار متناظرش جایگزین میشود:
createI18n({
messages: {
en: {
'cart.summary': '{count} items · ${total}',
greeting: 'Welcome back, {user}!',
},
zh: {
'cart.summary': '{count} 件商品 · ¥{total}',
greeting: '欢迎回来,{user}!',
},
},
});
const i18n = useI18n();
i18n.t('cart.summary', { count: 3, total: 59.9 }); // → "3 items · $59.9"
i18n.t('greeting', { user: 'Ada' }); // → "Welcome back, Ada!"جزئیات:
- نحو جاینگهدار
{word}است (حرف، رقم،_). مقدارها میتوانند رشته یا عدد باشند؛ عددها به متن تبدیل میشوند. - جاینگهداری که کلید متناظر نداشته باشد دستنخورده میماند (
{oops}عیناً در خروجی میماند)؛ این کار پارامتر جاافتاده را به چشم میآورد، بهجای اینکه بیصدا خالی بماند. - درج مقدار پس از بازگشت به زبان جایگزین اجرا میشود، پس همان پارامترها فرق نمیکند کدام زبان در نهایت رشته را حل کرده باشد.
- جمعبستن و قالببندی عدد و تاریخ درونساخت نیست؛ آنها را با
Intl.NumberFormat/Intl.PluralRulesبسازید و رشته قالببندیشده را بهعنوان پارامتر بدهید.
نمایش عینی آکولاد
یک { یا } تنها، یا گروهی با فاصله مثل { color: red }، جاینگهدار نیست و دستنخورده رد میشود؛ پس CSS و JSON و تکههای کد درون یک پیام بهطور پیشفرض در اماناند. تنها حالت مبهم یک {word} عینی است که میخواهید همانطور نشان داده شود. برای گریز از آن، آکولادها را دو برابر کنید (همان قراردادی که format! در Rust، str.format در پایتون و String.Format در داتنت دارند):
const i18n = useI18n(); // فرض بر این است که پیامهای زیر ثبت شدهاند
i18n.t('use {{ and }} for literal braces'); // → "use { and } for literal braces"
i18n.t('the {{count}} token'); // → "the {count} token" (مقدار درج نمیشود)
i18n.t('{{{name}}}', { name: 'Ada' }); // → "{Ada}" (مقدار درون آکولاد عینی)| در پیام | خروجی |
|---|---|
{{ | { |
}} | } |
{name} | پارامتر name، یا {name} اگر نباشد |
{ name } | { name } (فاصله دارد → جاینگهدار نیست) |
{ | { (آکولاد تنها) |
گریز در همان یک گذرِ چپبهراستِ درج مقدار انجام میشود و چه پارامتر بدهید و چه ندهید کار میکند، پس {{ و }} همیشه بهمعنای آکولاد عینیاند.
دو برابر کردن همان قرارداد
format!در Rust،str.formatدر پایتون وString.Formatدر داتنت است، پس نیازی به کاراکتر گریز تازه نیست. اگر به دستور زبان واقعی جمع و جنس و عدد نیاز دارید، باIntl.*قالببندی کنید و نتیجه را بهعنوان پارامتر بدهید.
واکنش به تغییر زبان
onChange پس از هر setLocale فرستاده میشود؛ از آن برای بازکشیدن رشتههایی که پیشتر رسم کردهاید استفاده کنید:
const i18n = useI18n();
const unsubscribe = i18n.onChange((locale) => {
document.documentElement.lang = locale;
repaintStrings(); // فراخوانیهای t() را دوباره اجرا کن
});
// بعدها، وقتی نما برچیده میشود
unsubscribe();افزودن پیامها بههنگام نیاز
واژهنامه یک زبان را هنگام نیاز بار کنید (مثلاً با جداسازی کد به تفکیک زبان) و آن را ادغام کنید:
const i18n = useI18n();
const { default: fr } = await import('./locales/fr.js');
i18n.addMessages('fr', fr); // با هر واژهنامه 'fr' موجود ادغام میشود
i18n.setLocale('fr');بومیسازی متن کامپوننتها
کامپوننتها خودشان از این موتور نمیخوانند. این عمدی است: کامپوننتی که مستقیم از یک singleton سراسری بخواند، هر مصرفکنندهای را به یک نمونه و یک شیوه نامگذاری کلید گره میزند و کاری میکند صفحهای که فقط یک دکمه وارد کرده، لایه ترجمه را هم با خود بیاورد. در عوض، هر رشتهای که کاربر میبیند یک ورودی است: یک اتریبیوت، یک ویژگی، یک گزینه، یا محتوای اسلات. پس بومیسازی ranui یعنی دادن خروجی t() به همان جایی که رشته پیشتر میرفت:
const i18n = useI18n(); // فرض بر این است که پیامهای زیر ثبت شدهاند
modal.setAttribute('title', i18n.t('dialog.deleteProject.title'));
themeSwitch.setAttribute('label-dark', i18n.t('theme.dark'));بیشتر کامپوننتها اصلاً متن از خود ندارند: متن از راه اسلاتها و اتریبیوتهایی میآید که خودتان مینویسید. تعداد انگشتشماری برای رشتهای که جای دیگری برای آمدن ندارد یک پیشفرض انگلیسی همراه دارند، که بیشترشان نامهای دسترسپذیرند:
| کامپوننت | انگلیسیِ درونساخت | بازنویسی با |
|---|---|---|
Modal.confirm / Modal.open | عنوان Confirm، دکمههای OK / Cancel | گزینههای title، okText، cancelText |
Modal.info / .success / .warning / .error | عنوانهای Info / Success / Warning / Error | گزینه title |
<r-theme-switch> | aria-labelهای Theme، System theme، Light theme، Dark theme | label، label-system، label-light، label-dark |
<r-voice-button> | aria-labelهای Start voice input / Stop voice input؛ راهنماهای Release to keep · slide up to cancel، Release to cancel | label، active-label، hold-hint، cancel-hint |
<r-reasoning> | برچسب سربرگ Reasoning | label |
<r-token-meter> | برچسب Context | label |
<r-colorpicker> | aria-labelهای Choose color، Hue، Alpha opacity | label، hue-label، alpha-label |
یک الگوی عملی این است که با هر تغییر زبان آنها را از یک جا دوباره اعمال کنید، تا همان کد هم هنگام راهاندازی و هم پس از تعویض اجرا شود:
const i18n = useI18n();
const applyLabels = () => {
document.querySelectorAll('r-voice-button').forEach((el) => {
el.setAttribute('label', i18n.t('voice.start'));
el.setAttribute('active-label', i18n.t('voice.stop'));
});
};
applyLabels();
i18n.onChange(applyLabels);یادتان باشد document.documentElement.lang را هم همگام نگه دارید: مرورگر، صفحهخوانها و گزینشگرهای :lang() به همان نگاه میکنند.
API
createI18n(config) singleton سراسری را میسازد و ثبت میکند (یک بار صدایش بزنید)؛ useI18n() آن را برمیگرداند، یا null اگر createI18n هنوز اجرا نشده باشد.
I18nConfig
| میدان | نوع | پیشفرض | توضیح |
|---|---|---|---|
messages | LocaleMessages | {} | locale → { key → string }. هر واژهنامه تخت است. |
locale | string | زبان جایگزین | زبان آغازین (اگر انتخابی ذخیره شده باشد، آن اولویت دارد). |
fallbackLocale | string | 'en' | زبانی که وقتی کلیدی در زبان فعال نباشد سراغش میروند. |
persist | boolean | false | زبان فعال را در localStorage نگه میدارد. |
storageKey | string | 'ran-locale' | کلید localStorage وقتی persist روشن است. |
detectNavigator | boolean | false | زبان آغازین را از ترجیحهای زبانی مرورگر برمیدارد. کل فهرست مرتب navigator.languages را میخواند، پس خوانندهای که برای انتخاب اولش واژهنامه نیست، بهجای زبان جایگزین انتخاب دومش را میگیرد. |
متدهای I18nCore
| متد | بازگشت | توضیح |
|---|---|---|
t(key, params?) | string | ترجمه میکند؛ به زبان جایگزین و سپس به خود کلید برمیگردد. |
setLocale(locale) | void | زبان را عوض میکند؛ (در صورت روشن بودن) ذخیره و به مشترکان خبر میدهد. |
getLocale() | string | زبان فعال. |
onChange(handler) | () => void | تغییر زبان را مشترک میشود؛ تابعی برای لغو اشتراک میدهد. |
addMessages(locale, dict) | void | پیامهای بیشتری را در یک زبان ادغام میکند. |
getMessages(locale?) | MessageDict | واژهنامه یک زبان را میخواند (پیشفرض: زبان فعال). |
availableLocales | string[] | زبانهایی که واژهنامه ثبتشده دارند. |
destroy() | void | همه مشترکان را برمیدارد. |
تایپها
type MessageDict = Record<string, string>; // تخت: 'hero.title' → 'Hi {name}'
type LocaleMessages = Record<string, MessageDict>; // locale → MessageDict
type TranslateParams = Record<string, string | number>;SSR
هسته در SSR امن است: دسترسی به localStorage و navigator محافظت شده، پس createI18n و t هنگام رندر سمت سرور بدون پرتاب خطا اجرا میشوند. ذخیرهسازی و تشخیص زبان مرورگر روی سرور صرفاً کاری نمیکنند و بهمحض اجرای کد در مرورگر اثر میگذارند.