چندزبانه
مرجع زنده. زبانِ پیشفرض فارسی است؛ انگلیسی زبان دوم. زیرساخت برای زبان سوم آماده است: افزودنش از یک ردیف در
LOCALESشروع میشود و تیپاسکریپت بقیه را میخواهد («افزودن زبان سوم»).
معماری در یک نگاه
Section titled “معماری در یک نگاه”packages/core پیام = شیءِ /*i18n*/ { id, message } بی وابستگیِ زبانی │ خروجی: Msg = { ref, values } با مقدارِ تیپدار: mm · count · decimal · │ money · percent · date · moment · text · ref · list (core/src/message.ts) ▼packages/i18n LOCALES · formatter(locale) · render(i18n, msg) · createI18n · errorText │ کاتالوگها: src/locales/{fa-IR,en-US}.po اصلی — هسته، برگه، رابط │ src/locales/cli.{fa-IR,en-US}.po فقط خط فرمان │ و کنارِ هر کدام کامپایلش .ts │ ├─ apps/web زبانِ رابط — فقط کاتالوگِ اصلی، `.po` تنبل از راهِ @lingui/vite-plugin ├─ packages/report زبانِ برگه — نمونهٔ جدا (`docOf`)؛ مرکز چاپ ▸ «زبان برگه» └─ apps/cli `CATALOGS` + `CLI_CATALOGS` از `@darzsaz/i18n/catalogs` (کامپایلشده، ادغام)دو کاتالوگ
Section titled “دو کاتالوگ”پیامی که در apps/cli/src تعریف شده — راهنمای --help، سطرهای خروجیِ متنی، خطای گزینه — در
cli.<زبان>.po است؛ بقیه در کاتالوگِ اصلی. معیار جای تعریف است، نه جای مصرف: پیامِ قاعده یا خطای هسته
که خط فرمان هم چاپ میکند در کاتالوگِ اصلی میماند و خط فرمان هر دو را ادغام میکند
(apps/cli/src/lang.ts).
چرا: رابط کاتالوگِ اصلیِ زبانش را تنبل دانلود میکند و هر پیام ≈۲۰ بایتِ gzip است. تا ۳.۶ ِ نسل
پنجم ۳۲ پیامِ cli.* (و بعد از ترجمهٔ خط فرمان صدها) در همان کاتالوگ برای کاربری میآمد که هرگز
darzsaz را اجرا نمیکند.
چرا پروندهٔ همردیف و نه زیرپوشه: locales/cli.fa-IR.po زیرِ همان الگوهایی است که از پیش هست —
نادیدهگیریِ ESLint و Prettier برای locales/*.ts ِ کامپایلشده، صادراتِ ./locales/*.po، و «کاتالوگِ
ترجمه: packages/i18n/src/locales/*.po». زیرپوشه هر سه را بیصدا از قلم میانداخت.
شناسهٔ مشترک ممنوع است. ادغامِ خط فرمان پیامِ همشناسه را بیصدا میپوشاند و رابط بایتِ پیامی را
میکشید که فقط خط فرمان میخواست؛ i18n:check هر شناسهای را که در دو کاتالوگ باشد رد میکند. پیامی
که خط فرمان از هسته یا برگه لازم دارد وارد میشود، نه دوباره تعریف.
چرا هسته پیام برمیگرداند: تا نسل چهارم هسته جملهٔ فارسی میساخت —
«${label}» سینک دارد و ${num(n)} کشو — و رابطِ انگلیسی خطا، قاعده و سطرِ صورتحساب را
فارسی نشان میداد. عدد هم پیش از رسیدن به لبه رشته شده بود و رقم و گروهبندیِ زبانِ دیگر از
دست رفته بود. حالا هسته شناسه و مقدار میدهد و هر لبه به زبانِ خودش رندر میکند.
چرا /*i18n*/ و نه وابستگیِ هسته به Lingui: استخراجگرِ Lingui شیءِ دارای این کامنت را
پیام میشناسد، پس هسته بی هیچ کتابخانهای پیام تعریف میکند و زبان نمیداند.
چرا یک فهرستِ زبان: زبان در چند جای مستقل اثر دارد — optimizeLocales ِ Vite، کاتالوگها،
کامپایلشان، تشخیصدهندهٔ رابط. اگر زبان سومی اضافه شود و یکی جا بماند، React Aria بیصدا به
انگلیسی میافتد یا خط فرمان ترجمهٔ دیروز را چاپ میکند. scripts/i18n-check.mjs Vite، کاتالوگها و
کامپایلشان را میسنجد؛ تشخیصدهندهٔ رابط خودش از LOCALES میخواند (isLocale) و نقشهٔ کاتالوگش
Record<Locale, …> است.
چرا LOCALES در packages/i18n و نه در ui یا هسته: سیستم طراحی نباید بداند این محصول چه
زبانهایی حرف میزند (UiProvider یک رشتهٔ BCP-47 و یک جهت میگیرد) و هسته زبان نمیداند.
locales.ts هیچ وارداتی ندارد تا lingui.config.js، vite.config.ts و اسکریپتها مستقیم
واردش کنند. سیاستِ رابط (?lang=، ترجیحِ ذخیرهشده، بارِ تنبل) در apps/web/src/lib/locale.ts
است.
پیام در هسته
Section titled “پیام در هسته”import { unitValue } from '../catalog/preset-names.js'import { mm, msg } from '../message.js'
const M = { sag: /*i18n*/ { id: 'rule.shelf-sag', message: 'طبقهٔ {span} میلیمتری «{unit}» خم میشود' },}fail('shelf/sag', msg(M.sag, { span: mm(968), unit: unitValue(unit) }))| مقدار | برای | رندر |
|---|---|---|
mm(v) |
اندازه | بی گروهبندی: «۲۱۰۰» |
count(v) |
شمارنده؛ جمعِ ICU رویش انتخاب میشود | بی گروهبندی |
decimal(v, d) |
متر، متر مربع | d رقمِ اعشار |
money(v) |
مبلغ | با گروهبندی: «۱۲٬۵۰۰٬۰۰۰» |
percent(v) |
درصد از ۰ تا ۱۰۰ | یک رقمِ اعشار؛ نشانهٔ «٪» را پیام مینویسد |
date(iso) |
تاریخِ ذخیرهشده | تقویمِ زبان |
moment(ms) |
لحظه در نامی که میماند (قدمِ تاریخچه) | روز و ساعت با ماهِ کوتاهِ زبان |
text(s) |
نامی که کاربر نوشته، نامِ محصولِ کاتالوگ | همانطور، با جداسازیِ جهت (FSI/PDI) |
ref(msg) |
پیامِ تو در تو («نوع جنس «x»») | به همان زبان |
list([msg]) |
چند پیام با «و» | Intl.ListFormat ِ زبان |
چهار قاعده:
۱. جملهٔ کامل، نه چسباندن. msg(A) + ' و ' + msg(B) در زبانِ دیگر ترتیب را میشکند؛ یک پیام
با دو جاینگار یا list. message رشتهٔ ثابت است — استخراجگر عبارت را نمیخواند.
۲. شناسه معنادار و گروهدار: error.<ناحیه>.<نام> برای DarzError، rule.<قاعده>.<نام>،
catalog.kind.<گونه>. یک پیام در دو جا یک شناسه دارد (مثل error.part.no-back-material).
۳. عدد عدد میماند. هیچ num() یا String(n) درون پیام؛ عددی که کاربر نوشته و نامعتبر است
(NaN، 2.5 ِ خام) text(String(v)) است.
۴. متنِ کاربر text است تا جهتش جدا شود و ترجمه نشود. نامِ یونیت unitValue(u) است: برچسبِ کاربر
text، نامِ پیشتنظیم ref.
واحد کلید است، نه واژه. HardwareUnit ('piece' | 'pair' | 'meter') و CostUnit ِ صورتحساب
(بهاضافهٔ 'sheet' | 'sqm' | 'cut') شمارشِ ماشینیاند. تا نسل چهارم 'عدد' | 'جفت' | 'متر' بودند و
شاخه رویشان تصمیم میگرفت (n.unit === 'متر')؛ ترجمهشان شاخه را میشکست (I3). واژه را لبه میسازد،
با دو پیام برای هر واحد (costing/bom.ts): unitName(u) برای ستونِ «واحد» («ورق»، «sheet») و
unitQty(u, n) کنارِ عدد («۲ ورق»، «2 sheets»). چسباندنِ عدد به واژهٔ تنها در انگلیسی «2 sheet» میداد.
نامِ محصول plain است. سطرِ صورتحساب و یراق نامِ کاتالوگ را plain(name) میگیرند — ترجمه
نمیشود (واژهنامه، قاعدهٔ ۲). materialLabel(name, thickness) ضخامت را mm میگذارد و «اسم ضخامت را
دارد؟» را روی رقمهای خودِ اسم میسنجد (فارسی و عربی به لاتین)، نه روی «۱۶» ای که هسته با قالبگرِ
فارسی میساخت. یادداشتِ «+ ۱۰٪» هم درصدش را از همان ثابتی میگیرد که شمارش ضرب میکند.
نامِ قطعه هم پیام است، نه متن: Part.name ساختار است — { owner, part }، یا { owners, role } برای ردیفِ
ادغامشده — و برچسب فقط با partLabel(p) رندر
میشود — در پیامِ دیگر ref(partLabel(p))، هرگز text(...) ِ برچسبی ساختهشده. برابریِ دو نام با
partNameKey، نه با متنِ رندرشده. مدل و دلیل: data-model.md.
DarzError پیام دارد (e.msg)؛ Error.message متنِ منبع با مقدارِ خام است (sourceText) و
فقط برای پشتهٔ خطا و لاگ. نمایشِ هر خطا با errorText(i18n, e)؛ خطایی که در وضعیت میماند و بعد
رندر میشود (نوار پایین، نشانِ ذخیره، «یک ورق کمتر») errorMsg(e) ِ هسته را نگه میدارد تا
تعویضِ زبان آن را هم عوض کند.
قاعدههای دستیار
Section titled “قاعدههای دستیار”Issue.message و Fix.label پیاماند؛ RuleMeta.why پیامِ بی مقدار (MessageDescriptor)؛
CATEGORY_NAME و SEVERITY_NAME هم (validate/types.ts). شناسهها:
| چه | شناسه |
|---|---|
| پیامِ قاعده | rule.<قاعده> یا با شکلش rule.run-gap.base.small |
| راهحل | rule.<قاعده>.fix یا rule.<قاعده>.fix.<نام>؛ مشترک: rule.fix.split-doors |
| چرا | rule.<قاعده>.why — rules-v3.test.ts میسنجد |
| دسته، شدت | rule.category.<دسته>، rule.severity.<شدت> |
| «بررسیِ X اجرا نشد» | rule.failed؛ خطای هسته درونش ref است، خطای دیگر متنِ خودش |
- صفت و گونه جملهٔ جدا میگیرند، نه جاینگار. «ردیف {زمینی/دیواری}» و «روی {پنجره/در}» دو
جملهٔ کاملاند (
rule.run-overflow.base/.wall): صفتی که جدا ترجمه شود در زبانِ دیگر جای خودش را در جمله ندارد. فهرستِ اسم («کف و پشتبند»، ابعادِ ورق)listاست. - مقدار همان است که پیام میگوید. درِ بالابر شمار ندارد، پس
nنمیگیرد؛ وگرنه--jsonشماری میداد که هیچ زبانی نشان نمیدهد — آزمونِ رقم آن را گرفت. - دلیل پیام است، نه
error.message.unit-unbuildableپیامِ خودِ سازنده راrefمیگیرد؛ متنِ خامِ خطا «(900)» با رقمِ لاتین درونِ جملهٔ فارسی مینشاند. docs/reference/rules.mdازmessageِ «چرا» و نامِ دسته ساخته میشود (scripts/rules-doc.mjs) و متنش با پیام شدن عوض نشد؛docs/reference/rules.en.mdاز همان فهرست با کاتالوگِen-US(۱۰.۵) و بهen/reference/ِ سایت میرود (apps/site/scripts/sync-reference.mjs).
آزمون: packages/i18n/test/rules.test.ts روی پیکرهای که هر قاعده و هر شکلِ پیامش را آتش میزند
(rule-corpus.ts) میسنجد که هر پیامِ rule.* ِ کاتالوگ ساخته میشود، در انگلیسی حرفِ فارسی ندارد
(متنِ کاربر و نامِ کاتالوگ دادهاند و کنار میروند) و عددش لاتین است، و آشپزخانهٔ نمونه در فارسی همان
متنِ پیشین را میگیرد. قاعدهٔ تازه بی موردِ پیکره این آزمون را قرمز میکند.
رندر در لبه
Section titled “رندر در لبه”| لبه | نمونهٔ i18n | رندر |
|---|---|---|
| رابط | در جزء useLingui() (زیرِ UiProvider)؛ بیرون از رندر سراسریِ @lingui/core در لحظهٔ کار |
render(i18n, msg)، خطا errorText(i18n, e)، عدد useFmt() |
| برگه | ReportInput.i18n — زبانِ سند، جدا از رابط |
docOf(i18n): d.t، d.h، d.m |
| خط فرمان | cliI18n در apps/cli/src/lang.ts — زبانِ --lang |
say(msg) — بی نویسهٔ جداسازِ جهت |
رابط: useLingui و useFmt
Section titled “رابط: useLingui و useFmt”import { useLingui } from '@lingui/react'import { useFmt } from '../lib/useFmt.js'
function Rulers({ length }: { length: number }): JSX.Element { const { i18n } = useLingui() const fmt = useFmt() return <text aria-label={i18n._({ id: 'x', message: 'خطکش' })}>{fmt.mm(length)}</text>}چرا هوک و نه نمونهٔ سراسری: تا فاز ۳ ِ نسل پنجم ۳۲ جزء i18n را از @lingui/core وارد
میکردند و عدد را با num ِ هسته (پیشفرضِ فارسی) مینوشتند (I5، I6). تعویضِ زبان فقط کار میکرد چون
App با locale ِ تازه کلِ درخت را از نو رندر میکرد. جزئی که memo شده — یا کامپایلرِ React برایش
خانهٔ حافظه گذاشته — اجرا نمیشد و متن و رقمِ زبانِ قبل را نگه میداشت: نمونهٔ سراسری واردات است، نه
مقدارِ هوک، و برای کامپایلر وابستگی نیست. useLingui() جزء را مشترکِ I18nProvider میکند: با هر
load/activate جزء — حتی زیرِ memo — رندر میشود و i18n ِ برگشتی شیءِ تازه است، پس هر حافظهای که
به آن وابسته است باطل میشود. useFmt() همان formatter(locale) ِ زبانِ همان نمونه است؛ برای هر زبان
یک شیء، پس فقط با تعویضِ زبان عوض میشود.
- کمکیِ رندر
i18nرا پارامتر میگیرد، نه سراسری:groupName(i18n, g)،keymap(i18n)،issueKind(i18n, issue)،canvasCommands(i18n, ui). تابعی که سراسری را صدا میزند برای حافظهٔ جزء به زبان وابسته نیست؛useCommandsنمونه را در وابستگیهایuseMemoدارد. - بیرون از رندر سراسری در لحظهٔ کار — اعلان (
lib/actions.ts)، رویداد، و دادهٔ تازهای که به زبانِ همان لحظه ساخته میشود (sayData، نامِ دیوارِ تازه):fmtOf(i18n)برای عدد. هرگز در ثابتِ ماژول. - وضعیت متن نگه نمیدارد، داده نگه میدارد. خطایی که در وضعیت میماند و دیرتر نشان داده میشود
(نوار پایین، نشانِ ذخیره، «یک ورق کمتر»، دروازهٔ پروژه، خطای نرخِ دلار و رندر)
errorMsg(e)ِ هسته را نگه میدارد، نه متن؛ یادداشتِ نرخ، پیشنمای پیامدِ راهحل و ردیفهای مقایسهٔ گونه عدد، و «پروندهای نرسید» یک حالت. رندر به زبانِ جاری مینویسد. همین برای هر پیامی که از Worker میآید:DeriveSummary.problemsوSuggestionData.messageپیاماند؛ و علتِ شکستِ پروندهٔ متصل (SaveStatus.fileError) و علتِ ردیفِ آسیبدیدهٔ خانه (RowHealth.message، «جزئیات» ِ کارت) — تا ۳.۱۲ متن بودند و دومی متنِ منبعِ فارسیِDarzErrorرا در رابطِ انگلیسی نشان میداد. - قاعدهٔ لینت (
globalI18nدرeslint.config.js): درapps/web/src/**/*.tsxواردکردنِi18nاز@lingui/coreخطاست. استثنای موجه (جزءِ کلاسی)eslint-disable-next-lineبا دلیل. آزمون:scripts/test/eslint-web-i18n.test.mjs. قالبگرِ بیزبان قاعده نمیخواهد — در هسته نیست (پایینِ «عدد، پول، تاریخ»). - درونِ صحنهٔ سهبعدی زمینه از بیرون پل میخورد (
SceneCanvas)، ولیHtmlِ drei ریشهٔ DOM ِ جدا میسازد: هوک بیرون ازHtmlصدا زده میشود و متنِ آماده به آن میرسد (scene/Measure.tsx). - آزمون: جزئی که
useLinguiدارد بی ارائهدهنده میافتد («useLingui hook was used without I18nProvider») —renderِtest/render.tsxیاUiLocale.test/locale-memo.dom.test.tsxجزءِmemoدرmemoو شکلِ حافظهٔ کامپایلر را پس از تعویضِ زبان، بی رندرِ دوبارهٔ ریشه، میسنجد؛locale-digitsهر سطحی که قالبگرِ فارسی داشت را درen-USبی رقمِ فارسی (جز دادهٔ کاتالوگ)؛locale-stateمتنِ ماندگار در وضعیت را.
برگه به زبانِ سند (۳.۵). bundleReport و assemblyDocument از ReportInput.i18n یک Doc میسازند
(docOf(i18n, paper)، packages/report/src/doc.ts) و هر برگه آن را میگیرد: bomPages(d, …)،
cutMapPages({ doc, … }). locale ِ سند از خودِ i18n.locale است — فیلدِ locale ِ جدا نیست تا نمونهٔ
فارسی با locale: 'en-US' ساختنی نباشد — و lang، dir، قالبگر (d.fmt) و پیام از همان.
ReportInput.date روزِ ایزو است و برگه به تقویمِ خودش قالبش میزند؛ رشتهٔ ازپیشقالبخورده خطاست.
- رشتهٔ برگه پیامِ
report.*است؛d.tهمهاش را فرار میدهد. جملهای که تأکیدِ میانش جزئی از جمله است («همهٔ ابعاد اندازهٔ برش است») باd.hرندر میشود: نشانهگذاری از کاتالوگ، فقط مقدارها فرار. تنها<b>،</b>و<br>مجازند (packages/report/test/messages.test.ts). - داده درونِ متنِ روان با
iso()جدا میشود (همان FSI/PDI ِtext())؛ عنوانِ سند بی جداساز، چون نامِ پروندهٔ «Save as PDF» است. - جداکنندهٔ فهرستِ بی «و» پیام است (
report.list-separator:،/,):Intl.ListFormatِ فارسی «و» و RLM میگذاشت. - مرکز چاپ زبانِ رابط را عوض نمیکند. «زبان برگه» (پیشفرض زبانِ رابط) کاتالوگ را با همان نقشهٔ
تنبل بار میکند (
loadCatalogِlib/locale.ts) و نمونهٔ جدا میسازد (sheetI18nدرlib/print-sheet.ts)؛ رابطِ فارسی برگهٔ انگلیسی میگیرد و منوها فارسی میمانند (آزمون:test/print-center.dom.test.tsx). جزئیاتِ کاغذ و سنجش: print.md.
CSV و اکسل بی جداسازِ جهت. bomCsv(i18n, report) و برگهٔ «لیست خرید» ِ darzsaz export --xlsx
با isolate: false رندر میکنند: خانهٔ جدول جمله نیست و FSI/PDI ِ نامرئی جستوجو و فرمولِ روی نام را
میشکست. برگهٔ HTML و رابط جداسازی را نگه میدارند.
--json ِ خط فرمان متنِ رندرشده را در همان کلیدِ دیروز میگذارد و شناسه و مقدارِ پیام را کنارش
(cli.md، «--json و پیام»).
render هر مقدار را با formatter(locale) ِ همان زبان رشته میکند. عددِ count/mm/… دو چهره
دارد (Symbol.toPrimitive): در جاینگارِ ساده متنِ قالبخورده و در جمعِ ICU عددِ واقعی — پس
{n, plural, one {# sheet} other {# sheets}} در ترجمه کار میکند بی آنکه هسته بداند. رشتهٔ
ازپیشقالبخورده جمع را میشکست و عددِ خام رقمِ فارسی را.
در بیلدِ تولیدی متنِ منبعِ پیام (message) از کد برداشته میشود (apps/web/vite/messages.ts):
کاتالوگ پیش از اولین رندر نشسته و i18n:check هر شناسه را در کاتالوگ میخواهد، پس آن متن فقط بایتِ
دوم بود. sourceText و render نبودنش را میپذیرند. آزمون و vite dev دست نمیخورند.
و شناسه کوتاه میشود (apps/web/vite/message-ids.ts): هر شناسهٔ کاتالوگِ منبع به ترتیبِ الفبایی اندیسی
میگیرد و در کد اندیسِ مبنای ۳۶ ِ آن مینشیند (history.unit.add ← ke در کاتالوگِ ۲٬۰۴۹ پیامیِ امروز؛ با هر
پیامِ تازه جابهجا میشود) — در شیءِ /*i18n*/، اولین
آرگومانِ i18n._ — و در نتیجه هر ارجاعِ msg(M.x) — در بستهٔ اصلی و Worker. کاتالوگی که بارکنندهٔ
@lingui/vite-plugin میسازد آرایهٔ بی کلید میشود و کلیدش در زمانِ بار از اندیس ساخته میشود. در ۱٬۰۷۹ پیام
js ِ هر زبان ۸٬۸۰۸ بایت (en-US، ۸۴۸٬۲۶۸ ← ۸۳۹٬۴۶۰) و ۹٬۸۲۸ بایت (fa-IR، ۸۴۹٬۹۷۰ ← ۸۴۰٬۱۴۲) کمتر شد و
کاتالوگ ۲۱٫۳ ← ۱۵٫۴ و ۲۳٫۰ ← ۱۶٫۱ کیلوبایت؛ تکهٔ اولیه ۱۶۱٬۱۱۲ ← ۱۶۰٬۸۱۳. طرحهای دیگر (درهمساز، شیء با
کلیدِ کوتاه، شناسهٔ دهدهی) با عددشان کنارِ افزونهاند.
- نگهبان: شناسهای که در کاتالوگ نیست (
pnpm i18n:extractنخورده)، شناسهٔ پویا (الگو با جاینگار، متغیر، کوتاهنویس)، شیءِ بی/*i18n*/با شناسهٔ کاتالوگ، و هر رشتهٔ دیگری که شناسه یا پیشوندِ شناسه است (m.ref.id === 'rule.x'،`error.${x}`) بیلد را با نامِ پرونده و خط میاندازند — در زمانِ اجرا بیصدا پیامِ دیگری میدادند. کاتالوگی که شناسههایش با فهرست یکی نیست هم. - شناسه داده نیست:
m.ref.idفقط در همان بیلد معنا دارد — کلیدِ تراکنش (label.ref.id) درست است، ذخیره در پرونده یا IndexedDB نه (بیلدِ بعدی شناسهٔ دیگری به همان پیام میدهد). هیچ ردیفِ پایگاهMsgندارد: دفترِ رویداد تا شکلِ ۲ ِ ردیف (نسخهٔ ۶ ِ Dexie) پیامِ شرح را نگه میداشت و پس از بیلدی که شناسهها را جابهجا کرد متنِ پیامِ همسایه را نشان میداد؛ حالا شرحِ تیپدار دارد (زمانِ پشتیبان و نسخهٔ برنامه، شمار و بایتِ تصویرها) و پیامش در نمایش ساخته میشود (components/settings/AuditLog.tsx؛ data-layer.md). نگهبان:test/rows-no-message.test.tsهر جای مقدارِ هر اسکیمایdata/rows.tsرا با پیامِ نمونه میسنجد و تیپِ هر ردیف را بیMsg؛test/audit-detail.dom.test.tsxردیفِ بیلدِ A را در بیلدِ B (پیامی تازه که همهٔ شناسهها را جابهجا کرد) با همان متن میخواند. متنِError.messageدر تولید (sourceText) شناسهٔ کوتاه دارد؛ نقشهٔ کد (.map) منبع را دارد. - آزمون:
apps/web/test/message-ids.test.ts— ماژول و کاتالوگِ نمونه پیش و پس از هر دو تبدیل با Lingui همان متن را در هر دو زبان میدهند (جمع، جاینگار،ref)، هر پیامِ دو کاتالوگِ مخزن زیرِ شناسهٔ کوتاهش همان پیامِ کامپایلشده است، و هر شکلِ نگاشتنی بیلد را میاندازد.
رابط هم برای رشتهٔ خودش که مقدار دارد همین راه را میرود:
render(i18n, msg(/*i18n*/ { id: 'status.nestProblems', message: '{n} مشکل در نقشهٔ برش' }, { n: count(k) })).
i18n._ فقط برای پیامِ بی مقدار است — values ِ آن هر چه بگیرد خام میچسباند. نامِ کاربر text(…)،
علتِ خطا ref(errorMsg(e))، عدد count/mm/…، تاریخ date(iso)، و فهرست list(…) (نه join(' · ')).
قاعدهٔ لینتِ untypedValues (eslint.config.js) values و آرگومانِ دومِ _ را در apps/web/src میگیرد؛
کیت به هسته وابسته نیست و قاعده ندارد. آزمون: scripts/test/eslint-web-i18n.test.mjs،
test/message-values.dom.test.tsx.
- جدولِ برچسبِ سطحِ ماژول جدولِ پیام است (
catalog/catalog-fields.ts، تبهای کارگاه، گروههای هزینه) و درونِ جزء رندر میشود. تیپشsatisfies Record<K, MessageDescriptor>است، نهRecord<…>:MessageDescriptorِ هستهmessageِ اختیاری دارد وi18n._ِ Lingui زیرِexactOptionalPropertyTypesآن را نمیپذیرد؛ یاrender(i18n, msg(d)). - برچسبِ هممعنای برگه با همان شناسه و متن در رابط تعریف میشود — نامِ گروهِ صورتحساب، واحدِ پول، «جمع»،
«٪» (
cost/cost-messages.ts)، ستونهای «ضخامت/طول/عرض» ِ کاتالوگ، نوعِ صفحه: یک ترجمه، و کاتالوگِ تنبلِ هر زبان یک بار میپردازد. «بستن» ِ پنجرهها از خودِDialogِ کیت است (ui.dialog.close). - تأکیدِ میانِ جمله (
cost/emphasize.tsx): جمله یک پیام میماند و واژه یا عددِ پررنگ مقدارِtext(…)است؛emphasize(render(…))هر تکهٔ جداشده (FSI…PDI) را<b>میکند — «پایهٔ قیمتها: … {date} … {rate} … {approx}، نه قیمت واقعی» ِ پنلِ هزینه. بریدنِ جمله دورِ<b>ترتیبِ واژهٔ انگلیسی را از مترجم میگرفت. - نامِ پیشفرضِ دادهٔ تازه («قلم تازه» ِ کاتالوگ) یک بار به زبانِ لحظهٔ کلیک نوشته میشود و بعد دادهٔ
کاربر است؛ ردیفِ «پروژهٔ فعلی» ِ مقایسهٔ گونهها در وضعیت
name: nullاست و هنگامِ رندر برچسب میگیرد.
صفحههای هزینه، کارگاه، کاتالوگ، تنظیمات، راهنما و رندرهای ذخیرهشده (موجِ ۱ ِ فاز ۱۰) پیام شدهاند؛ آزمونِ
هر کدام در en-US متنِ رابط را بی حرفِ فارسی میخواهد (دادهٔ translate="no" و مقدارِ جداشده کنار میروند):
test/{cost,catalog,settings,workshop}-locale.dom.test.tsx.
نامِ قدمِ تاریخچه: پیام، نه متن
Section titled “نامِ قدمِ تاریخچه: پیام، نه متن”«برچسب ذخیره نکن»: هر چه در وضعیت میماند و بعد نشان داده میشود، پیام است. قدمِ تاریخچه
(HistoryEntry.label در state/store.ts) هم: edit(label: Msg, …)، beginDrag(label?: Msg)،
replaceProject(p, label?: Msg)، و اثرِ beginDrag ِ ماشینِ حالتِ بوم (editor/interaction.ts). پنجرهٔ
تاریخچه آن را به زبانِ جاری رندر میکند. تا ۳.۱۲ پنجاهوشش جا رشتهٔ فارسیِ ثابت میدادند و «پیشنهادِ راهحل» و
«برگرداندنِ نسخه» متنِ رندرشده به زبانِ لحظهٔ ویرایش — رابطِ انگلیسی تاریخچهٔ فارسی داشت.
- قدمِ مشترک (از چند جا ثبت میشود: «افزودن یونیت» از کشو، سازنده و کتابخانه) یک پیام در
lib/steps.tsاست:historyStep('unitAdd'). قدمِ یکجایی کنارِ همان کد:STEPبا شناسهٔhistory.<ناحیه>.<کار>. نامِ دیدنیِ فیلد جداست (فاز ۱۰) — «قدم» و «برچسب» در انگلیسی همشکل نیستند. - کلیدِ تراکنش (
coalesce) از شناسه است، نه متن:`wall:${label.ref.id}`. - راهحلِ دستیار خودِ پیامش را نامِ قدم میکند (
replaceProject(fix.apply(p), fix.label))؛ نامِ گونه و دفترِ قیمتtext؛ قدمِ گروهیrefِ کار وcountِ یونیتها؛ «برگرداندنِ نسخهٔ …»moment. - آزمون:
test/history-panel.dom.test.tsx— «افزودن یونیت» پس از تعویضِ زبان «Add cabinet» است و لحظهٔ نسخه رقمِ لاتین دارد؛ روی سورسِ پیشین افتاد.
متنی که داده میشود: Say
Section titled “متنی که داده میشود: Say”نامِ پروژه و اتاق و یادداشتِ آشپزخانهٔ نمونه دادهٔ کاربراند: در پرونده میمانند، کاربر عوضشان
میکند و به لیست برش و برگه میروند. پس Msg نمیمانند؛ سازنده آنها را با Say = (m: Msg) => string به
زبانِ لحظهٔ ساخت رشته میکند و بعد همان میماند. نامِ یونیت، دیوار، مانع و برشِ نمونه نوشته نمیشود
(موجِ ۶): نام در لحظهٔ نمایش از presetId (UNIT_PRESET در units/names.ts)، شمارهٔ دیوار یا نوع میآید
(unitLabel، wallLabel، obstacleLabel، cutoutLabel — data-model.md)، پس تعویضِ زبان همهٔ برچسبهای
نمونه را عوض میکند (۹.۳).
| سازنده (هسته) | رابط | خط فرمان |
|---|---|---|
myKitchen(cat, say) |
sayData (lib/locale.ts) — «آشپزخانهٔ نمونه» |
say (lang.ts) — demo، بیپرونده |
blankProject(cat, len, hgt, say) |
sayData — «پروژهٔ تازه» |
— |
fromTemplate(id, cat, { say }) |
sayData — کارتِ الگو |
— |
autofill(wall, cat, style, say) |
i18n ِ useLingui در حافظهٔ پنجره — «این دیوار را پر کن» |
— |
KITCHENS[i].make(cat, say) |
— | say — bench |
lShapedProject(cat, a, b, h, say) |
— | — |
- پیشفرضِ هسته
sourceTextاست، برای آزمون و اسکریپت: متنِ منبع با عددِ خام. در بیلدِ تولیدیِ رابط متنِ منبع برداشته شده و همان پیشفرض شناسهٔ پیام را نامِ پروژه میکرد؛ قاعدهٔ لینتِsampleSay(eslint.config.js) فراخوانیِ بی رندرگر را درapps/*وpackages/*/srcِ غیرِ هسته میگیرد. sayDataجهت را جدا نمیکند (isolate: false): نویسهٔ FSI/PDI در نامِ ذخیرهشده فقط خرابی است. جداسازی مالِ نمایش است.- عددِ درونِ نام جاینگار است: «آشپزخانه — دیوار {length}» طولِ واقعیِ دیوار را به سانتیمتر میگوید. تا نسل چهارم الگوی ۲۱۰ «دیوار 2100» (میلیمتر، رقمِ لاتین) و الگوی ۳۰۰ با هر طولی «دیوار ۳۰۰» مینوشت.
- نامِ کوتاه و کارت
Msgمیمانند، چون داده نیستند:TEMPLATES[i].name/descriptionدر خانه و «پروژهٔ تازه»،KITCHENS[i].nameدرdarzsaz bench،Proposal.titleدر «پر کردن دیوار». ترتیبِ یونیتها با عرضِ پیشنهاد را رابط ازproposal.baseمیسازد، نه هسته. - خانهٔ نمای کنج برچسب ندارد؛ نامِ نمای کور و لنگه را ساختِ قطعه میدهد. برچسبِ خانه برای نامی است که کاربر میدهد.
مسئلهٔ zod
Section titled “مسئلهٔ zod”«پرونده با قالب نمیخواند» تا نسل چهارم متنِ انگلیسیِ zod را درونِ جملهٔ فارسی میگذاشت (I13).
schema/issues.ts هر مسئله را از کد و مقدارهایش پیام میکند (issueMsg): «نیامده» در برابرِ
«باید عدد باشد»، مرزِ باز یا بسته، نویسه و مورد با جمعِ ICU، گزینه و کلیدِ ناشناخته با text.
- سنجشی که مسئلهاش به کاربر میرسد با
SCHEMA_PARSE(reportInput) است؛ بی آن zod «نیامده» و «نوعِ دیگر» را یکی میگوید. - اسکیمایی که پیامِ خودش را دارد شناسه را به zod میدهد:
.regex(re, SCHEMA_MESSAGE.imageDataUrl.id). - نقشه
Recordِ کامل روی کدهای zod است؛ کدِ تازهای که zod بیفزاید تا در نقشه نیاید تیپاسکریپت قرمز است. .describe()ِ اسکیما پیام نیست: سندِ قالبِ پرونده است کهscripts/darz-format.mjsاز آنdarz-format.mdمیسازد و هرگز در رابط دیده نمیشود. کنارِ اسکیما میماند تا سند از کد دور نشود؛ همان دلیلی که آن سند تولیدی است.
جداسازیِ جهتِ داده
Section titled “جداسازیِ جهتِ داده”نامِ محصولِ کاتالوگ، نامِ پروژه، دیوار و یونیت و هر متنِ کاربر در رابطِ انگلیسی فارسی میماند. کنارِ متن یا عدد، بی جداسازی کلِ جمله در پاراگرافِ چپبهراست راستبهچپ چیده میشد: «ملامینه سفید ۱۶: 3» به «3 :ملامینه سفید ۱۶» میپیچید (۳.۱۲).
- در پیام خودکار است: مقدارِ
text(…)با FSI/PDI رندر میشود (render؛ در داده و ترمینالisolate: false).i18n._({ …, values })جدا نمیکند — در رابط ممنوع است («رندر در لبه»). سنجیده در کروم روی پیامهای واقعیِ رابط: ۱۵ از ۱۰۰ ترکیبِ نام و پیام بی جداسازی جابهجا خوانده میشد — پروژهٔ «2 Kitchen» در رابطِ فارسی «Kitchen 2»، و پروندهٔ «آشپزخانه-۲.darz» در رابطِ انگلیسی با.darzآن سوی نام. - در JSX، هر جا دادهٔ کاربر یا کاتالوگ با متن یا مقدارِ دیگری در یک عنصر است:
<bdi>{name}</bdi>. - دادهٔ تنها در یک جعبه هم جدا میشود:
dir="auto"روی همان عنصر، یا<bdi>درونش. جعبهٔ بلوکی پاراگرافِ جداست ولی جهتش را از صفحه میگیرد، نه از داده — سنجیده در کروم:<div>2 Kitchen</div>در صفحهٔ راستبهچپ «Kitchen 2» دیده میشود. نسخهٔ پیشینِ همین سند میگفت چنین جعبهایbdiنمیخواهد؛ غلط بود. - در SVG
bdiوdir="auto"نیست:unicodeBidi="plaintext"روی<tspan>ِ داده کنارِ متنِ دیگر (نامِ دیوار کنارِ طول در پلان) یا روی خودِ<text>ِ برچسبِ تنها (unicode-bidi: plaintextِ کلاسهای.obstacleInfoText،.obstacleDangerTextو.lockedLabelدرwall-canvas/drawing.module.css— برچسبِ مانع و یونیتِ قفل روی بوم). نهisolate: سنجیده در کروم،isolateدر SVG فقط از متنِ کنار جدا میکند و جهتِ درونش همان جهتِ صفحه میماند — «2 Kitchen» در راستبهچپ «Kitchen 2» و «آشپزخانه-۲.darz» در چپبهراست با.darzآن سو (۲ از ۴ ترکیب)؛plaintextجهت را از نخستین حرفِ خودِ داده میگیرد، مثلِ<bdi>(۴ از ۴). ۳.۱۲ همینisolateرا نوشته بود.
آزمون: e2e/locale.spec.ts جای دیداریِ نویسهها را در نوار پایین میسنجد — در بیلدِ بی bdi عدد در
x=۱۷۴ و آغازِ نام در x=۲۹۸ بود و افتاد. e2e/bidi.spec.ts پروژهای با دیوار، مانع و نامِ «2 Kitchen» را در رابطِ
فارسی باز میکند و رقم را چپِ «K» میخواهد: برچسبِ مانع روی بوم، سرِ پنل و SVG ِ پلان، و کارتِ خانه — روی
بیلدِ پیشین در همان اولی «2» در x=۷۳۳ و «K» در x=۷۱۱ بود و افتاد.
شبهزبان و جاروی en-US
Section titled “شبهزبان و جاروی en-US”دو جاروی e2e متنی را میشمارند که روی صفحه هست و از کاتالوگ نیامده؛ شمارِ هر سطح فهرستِ پایهای
است که فقط کوچک میشود، و فاز ۱۰ آن را صفر کرد (۱۰.۴): هر فهرستِ en-US.* خالی است و در شبهزبان فقط دو متنِ
غیرِ پیام مانده — شمارهٔ نسخه در راهنما (#.#.#-alpha.#) و نشانیِ ۴۰۴ (/no-such-route). نگهبانِ ایستای ۳.۸ رشتهٔ کد را میشمارد؛ این دو آنچه
کاربر میبیند: متنی که از هسته، کیت یا کاتالوگِ ایران میآید و متنی که از چند تکه ساخته شده.
شبهزبان (۳.۹)
Section titled “شبهزبان (۳.۹)”pseudoLocale ِ خودِ Lingui (lingui.config.js): هر تکهٔ متنِ هر پیام ⟦…⟧ و ۴۰٪ بلندتر —
{ locale: 'pseudo', prepend: '⟦', append: '⟧', extend: 0.4, extendCharacter: '•' }. روزِ ۳.۹ روی ۹۲۵ پیام
(۱٬۳۱۶ تکهٔ متن) ۲۶٬۶۵۹ نویسه ← ۳۹٬۱۲۷، یعنی ×۱٫۴۷. حرفِ لاتینِ درونِ پیام را pseudolocale آکساندار
میکند (.darz ← .ďàŕź)؛ فارسی همان میماند.
pnpm --filter @darzsaz/web build:pseudo # vite build --mode pseudo → apps/web/pseudo/dist، ۳ ثانیهpnpm e2e # همین را پیش از Playwright میسازد؛ /darzsaz-pseudo/ ِ همان سرور- فقط بیلدِ e2e.
vite/pseudo.tsدر--mode pseudoواردکردنِ@darzsaz/i18n/locales/fa-IR.poرا بهpseudo.poِ کنارش میفرستد — پروندهای که روی دیسک نیست. بارکنندهٔ@lingui/vite-pluginزبان را از نامِ پرونده میگیرد و پیامِpseudoرا از زبانِ منبع، و چونpseudoزبانِpseudoLocaleاست همانجا شبهزبانش میکند. برنامه زبانِ تازهای نمیشناسد (lang="fa-IR"، راستبهچپ) و بیلدِ تولیدی این افزونه را ندارد: ۱۵۱ از ۱۵۲ پروندهٔdistبایتبهبایت همان است وsw.js.mapفقط در مسیرِ موقتِ Workbox فرق دارد — که میانِ دو بیلدِ تولیدیِ پیاپی هم فرق دارد. - چرا
pseudoدرlocalesنیست:lingui extractوcompileفقطlocalesرا میگردند؛ پس نهpseudo.poِ خالی (امروز ۲٬۰۴۹ مدخل، وcli.pseudo.poِ ۱۶۵ مدخلی) ساخته میشود و نهi18n:checkبرایش ترجمه میخواهد (آزمون:scripts/test/lingui-config.test.mjs).LOCALESِ رابط هم از@darzsaz/i18nاست، نه از Lingui. - چرا از فارسی و نه از انگلیسی: چیدمانِ اصلیِ محصول راستبهچپ است، و نامِ نقشِ دکمهها («آشپزخانهٔ نمونه») درونِ نشان هم پیدا میشود — کمکیهای e2e هماناند. متنِ سختکد در هر دو یکی درمیآید.
extendCharacter: پیشفرضش فاصله است و HTML فاصلهٔ کنارِ متن را جمع میکند.•در زیرمجموعهٔ قلمِ رابط هست (۰٫۵۸em در برابرِ میانگینِ ۰٫۴۲em ِ نویسهٔ فارسی — روی صفحه کمی سختتر از ۴۰٪) و در هیچ پیامی نیست؛~اول امتحان شد و در «هر ~{spacing} میلیمتر» خودِ پیام بود.- تکه، نه پیام: Lingui هر تکهٔ متن را جدا نشان میزند (
«{name}» حذف شد←⟦«⟧name⟦» حذف شد⟧). گرهای که نشان دارد از کاتالوگ است، حتی اگر مقدارِ جاینگارش حرف داشته باشد (تاریخ، «و» ِ فهرست). - بریدگی: عنصری با
overflowِ پنهان یاtext-overflowیاline-clampکه متنش از جعبه بزرگتر است عکسش پیوستِ گزارشِ آزمون میشود (--reporter=html)؛ کامیت نمیشود و آزمون را نمیاندازد. جعبهٔ ۱ پیکسلیِ «پنهان از دید» و جعبهٔ پیمایششونده بریدگی نیست. روزِ ۳.۹ یکی بود: نامِ پروژه در سرتیترِ کارگاهِ گوشی. همان روز سرتیتر ۴۵۳ پیکسل در صفحهٔ ۳۹۰ بود و صفحه افقی پیمایش میشد — بیرون زدن، نه بریدن؛ شمرده نمیشود.
سطحها (e2e/pseudo-sweep.spec.ts، پروژهٔ Playwright ِ pseudo): خانه، ویرایشگر با پنلها و یونیتِ
انتخابشده، و کارگاه روی گوشی — آخری چون روزِ ۳.۹ تنها بریدگی آنجا بود و پوستهٔ ویرایشگر هنوز سختکد بود —
و از ۱۰.۴ مسیرها، هزینه، هر تبِ کارگاه و هر پنجرهٔ ویرایشگر (پایین). آزمونِ اول میسنجد بیلد سرو میشود و
سرتیترِ خانه ⟦•درزساز•⟧ است.
جاروی en-US (۳.۱۰)
Section titled “جاروی en-US (۳.۱۰)”e2e/en-sweep.spec.ts در ?lang=en-US: ویرایشگر، مرکز چاپ (فقط پنجره؛ برگهٔ درونِ قابِ sandbox را
packages/report میسنجد، ۳.۵)، هزینه، و هر سه تبِ کارگاه روی گوشی. متنِ دیدهشدهای که حرفِ خطِ عربی
دارد شمرده میشود. پروژه در en-US ساخته میشود تا نامِ نمونه (sayData) انگلیسی باشد.
| سطح (۱۴۰۵/۰۶/۲۳) | متنِ بیجا (جا) | از متنِ دیدهشده | translate="no" |
|---|---|---|---|
| شبهزبان: خانه | ۰ | ۱۵ | ۱ |
| شبهزبان: ویرایشگر | ۸۰ (۸۳) | ۹۷ | ۷ |
| شبهزبان: کارگاه، قطعات | ۸ (۳۸) | ۶۹ | ۳۶ |
| en-US: ویرایشگر | ۸۰ (۸۳) | ۹۷ | ۷ |
| en-US: مرکز چاپ | ۱ (۱) | ۲۱ | ۲ |
| en-US: هزینه | ۳۸ (۳۹) | ۳۹ | ۱ |
| en-US: کارگاه، قطعات | ۸ (۳۸) | ۶۹ | ۳۶ |
| en-US: کارگاه، برچسب | ۹ (۹) | ۹ | ۱ |
| en-US: کارگاه، مونتاژ | ۸ (۹) | ۲۸ | ۱ |
بیشترینِ ویرایشگر به ناحیه: کشوی یونیت ۲۵ (نامِ پیشتنظیمها در lib/unit-presets/)، بوم ۱۷ (نوارِ مانع،
«سقف»، «عرض قفل»)، نوار پایین ۱۰، بازرس ۲۰ (تبها، SizeTab)، نوارِ سهبعدی ۷، سرِ بوم ۶ (EditorPage)؛
هزینه: داشبورد ۱۶، دفترهای قیمت ۸، گونهها ۷.
هر مسیر و هر پنجره (۱۰.۴): فهرستِ مشترکِ e2e/surfaces.ts — مسیرهای راهنما، اشتراک و ۴۰۴، و هر
پنجرهٔ ویرایشگر (تنظیمات، تاریخچه، کاتالوگ، دستیار، عکس، رندر، مرکز چاپ) که از پالت فرمان باز میشود
تا تغییرِ منو و میانبرِ فاز ۶ و ۷ جارو را نشکند. هر دو جارو (شبهزبان و en-US) همان فهرست را میروند؛
شبهزبان هزینه و هر سه تبِ کارگاه را هم. فهرستِ پایهٔ سطحِ تازه را pnpm e2e sweep -u میسازد.
translate="no": داده، نامِ محصول، نامِ زبان
Section titled “translate="no": داده، نامِ محصول، نامِ زبان”دادهای که ترجمه نمیشود — نامِ جنس از کاتالوگ، نامِ پروژه و دیوار و یونیت، نامِ هر زبان به خطِ خودش — درونِ
translate="no" است: ابزارِ ترجمهٔ مرورگر به آن دست نمیزند و جارو آن را نمیشمارد. همان عنصر جهت را هم جدا
میکند — <bdi translate="no"> (درونخطی، و در سرتیتر تا چینشِ سرتیتر عوض نشود) یا dir="auto" روی عنصری
که فقط داده دارد؛ هر دو unicode-bidi: isolate ِ مرورگر را میگیرند و هر دو جارو برای هر translate="no" ِ
دیدهشده همین را میسنجند.
- جاها: نامِ جنس در نوار پایین و کارگاه؛ نامِ پروژه در نوار بالا، کارتهای خانه، هزینه، کارگاه و پنجرهٔ دوم؛
تبِ دیوار؛ سرتیترِ بازرس و مانع؛ نامِ زبان در خانه و مرکز چاپ؛ نامِ کلید (
KeyCaps)؛ نامِ یونیت در پالت فرمان و موانعِ عکس؛ و نامِ کاتالوگی یا کاربری در کاتالوگ، لوازم، دفترِ قیمت، گونهها، رندرها و نماهای ذخیرهشده، دستیار، پلان، دفترِ رویداد و اندازهٔ ورق. نامِ دیوار در تنظیمِ دیوار دیگرbdiنیست:text()ِ پیام است. - داده بیرون از شمارش هم سنجیده میشود: نامِ نمونهٔ انگلیسی را جارو نمیبیند، پس
en-sweepجای هر داده را جدا میخواهد (bdi[translate="no"]در نامِ پروژه، سرتیترِ بازرس، هزینه و کارگاه). فهرست از یک بار بازکردنِ پروژهٔ فارسی با?lang=en-USآمد. تبِ دیوار از نسل ششم (فاز ۰) در این فهرست نیست: دیوارِ نمونه از موجِ ۶ بینام است و تبش «Wall 1» ِ رابط را نشان میدهد؛ نامی که کاربر بدهدisDataِ کیت است (packages/ui/test/toggle.dom.test.tsx،apps/web/test/sample-names-locale.dom.test.tsx). - ماند: برچسبِ مانع و یونیتِ قفل روی بومِ SVG — عنصرِ SVG صفتِ
translateندارد (در HTML فقط عنصرِ HTML حالتِ ترجمهٔ خودش را دارد و SVG از پدر میگیرد) و تیپِ React هم آن را نمیپذیرد؛ و گزینهٔ یونیت در تبِ مونتاژ، که رشتهای است از نام و شمار برایSelectِ کیت. - FSI…PDI ِ پیام داده است: مقدارِ
text(…)درونِ پیام را هر دو جارو کنار میگذارند — همان قراردادِ «متنِ کاربرtextاست».
فهرستِ پایه
Section titled “فهرستِ پایه”apps/web/e2e/i18n-baseline/<جارو>.<سطح>.json (pseudo یا en-US): متنِ نرمالشده (فاصله یکی، هر رقم #) ← شمارِ جای دیدهشده.
- متنِ تازه ← شکست، با فهرستِ همان متنها؛ ترجمه کن، یا اگر داده است
translate="no". - متنی که رفت ← سبز، با یادداشت و فرمانِ پایین آوردن. کارِ همزمان (ترجمهٔ فاز ۱۰) شمار را پایین میآورد و ادغام نباید برای «بهتر شد» قرمز شود.
pnpm e2e sweep -u(همان--update-snapshotsِ Playwright) فهرست را امروز میکند و هرگز بزرگتر: متنِ تازه با-uهم شکست است و افزودنِ آگاهانه دستی و در diff. نبودنِ فهرست شکست است؛ CI-uندارد.- شمارِ جای هر متن نوشته میشود ولی سنجیده نمیشود: تکرارِ برچسب در ردیفها به دادهٔ نمونه بسته است.
- پس از ادغام:
pnpm -r build && pnpm e2e sweep -u.
لرزان نیست: صبر تا سه پیمایشِ پیاپیِ یکسان (هر ۳۰۰ میلیثانیه) پس از نشانِ ساختاریِ هر سطح (نوارِ سهبعدی و عددهای نوار پایین، جدولِ هزینه، لیستِ قطعات)، اندازهٔ ثابتِ صفحه، و اعلان بیرون از شمارش: «برنامه برای کار آفلاین آماده شد» با کشِ سرویسورکر میآید و در اجراهای اول یک بار روی هزینه و یک بار روی خانه نشسته بود.
عنوانِ زبانه و مانیفست
Section titled “عنوانِ زبانه و مانیفست”هر صفحه نامِ خودش را با useDocumentTitle(PAGE_TITLE.x(…)) میدهد (lib/useDocumentTitle.ts): خانه
«درزساز»، صفحهٔ پروژه «{نامِ پروژه} — درزساز»، هزینه و کارگاه با نامِ بخش. نامِ پروژه text است — ترجمه
نمیشود و جهتش جداست. مانیفستِ PWA برای هر زبان پروندهٔ خودش را دارد و پیوندش با زبان عوض میشود
(storage.md). آزمون: test/document-title.dom.test.tsx، test/manifest.test.ts،
e2e/locale.spec.ts.
انتخاب زبان
Section titled “انتخاب زبان”رابط: ?lang=en-US → ترجیح ذخیرهشده (darzsaz.locale) → فارسیخط فرمان: --lang en-US → DARZSAZ_LANG → فارسیخط فرمان یک جا تصمیم میگیرد (apps/cli/src/lang.ts) و say، قالبگرِ عدد، نمونهٔ i18n ِ برگه و Say ِ
آشپزخانهٔ نمونه با هم عوض میشوند؛ زبانِ ناشناخته با خطا به همهٔ زبانها و کدِ ۲ رد میشود. جزئیات و
دلیل: cli.md. پیامِ خودِ خط فرمان در کاتالوگِ جداست («دو کاتالوگ»).
زبان مرورگر خوانده نمیشود. نسخهٔ اول navigator.languages را سوم
میگذاشت و روی ویندوزِ انگلیسی، درزساز انگلیسی بالا میآمد. مرورگرِ انگلیسی
در ایران رایج است و نشانهٔ زبانِ کاربر نیست؛ انگلیسی انتخاب است، نه حدس.
آزمون قبلیِ «پیشفرض فارسی» فهرستِ خالی بهجای navigator.languages میداد،
پس هیچوقت حالتِ یک مرورگرِ واقعی را نمیسنجید — زبانِ پیشفرضِ خودِ jsdom
en-US است و همان آزمون بی آن فهرستِ خالی میافتاد. حالا آزمون واحد
navigator ِ انگلیسی میگذارد و e2e/locale.spec.ts با locale: 'en-US' باز
میکند.
کلید کجاست:
| جا | شکل | چرا |
|---|---|---|
| ویرایشگر | منوی «نما»، بخش «زبان»، گزینهٔ رادیویی | ترجیحِ نمایش است، کنار «پوسته»؛ نه کارِ روی پروژه |
| پالت فرمان (Ctrl+K) | «زبان: English» | همان فرمانهای منو (display-commands.ts) |
| خانه | یک دکمه با نامِ زبانِ دیگر | خانه منو ندارد؛ بی آن کسی که فارسی نمیداند اول باید پروژهای باز کند |
نام هر زبان به خطِ خودش است («English»، نه «انگلیسی»)؛ دکمهٔ خانه و «زبان برگه» ِ مرکز چاپ lang هم
دارند، گزینهٔ منو و پالت رشتهٔ سادهٔ label است.
چرا خانه دکمه دارد و نه فهرست: Select ِ React Aria پنجرهٔ شناور،
ListBox و FocusScope را به تکهٔ اولیه میآورد — ۱۸۰ کیلوبایت در برابر بودجهٔ
۱۶۵. دکمه آن را به ۱۵۵ برگرداند.
ترتیبِ تعویض: کاتالوگ اول بار میشود، بعد وضعیت. I18nProvider فقط
مصرفکنندگانِ useLingui را از نو رندر میکند — از فاز ۳ ِ نسل پنجم هر جزئی که متن یا عدد
مینویسد مصرفکننده است (بالاتر، «رابط: useLingui و useFmt»)؛ اگر locale پیش از رسیدنِ
کاتالوگ عوض شود، جهت برمیگردد و متن فارسی میماند.
نوشتن رشتهٔ ترجمهشدنی
Section titled “نوشتن رشتهٔ ترجمهشدنی”const { i18n } = useLingui() // در جزء؛ بیرون از رندر `import { i18n } from '@lingui/core'`
i18n._({ id: 'ui.dialog.close', message: 'بستن' })سه قاعده:
۱. شکل صریح، بدون ماکرو. ماکروی t ِ Lingui به babel نیاز دارد و این
build روی rolldown است. استخراجگر شکل صریح را هم میشناسد — به شرطِ آنکه نامِ نمونه
i18n باشد: استخراجگر و افزونهٔ برداشتنِ متنِ منبع (vite/messages.ts) فقط i18n._(…) را
میشناسند، پس const { i18n } = useLingui()، نه const { _ } = useLingui().
۲. شناسه معنادار. ui.dialog.close، نه هشِ خودکار — در diff ِ کاتالوگ
باید معلوم باشد چه چیزی عوض شده.
۳. نمونه از هوک یا پارامتر، نه ثابتِ سطح ماژول و نه سراسری در رندر. ثابت با زبانِ لحظهٔ
بارگذاری قفل میشود؛ تابعی که سراسری را صدا میزند در جزءِ memo شده یا حافظهٔ کامپایلر همان
متنِ قبلی میماند:
// ✗ با تعویض زبان عوض نمیشودconst TITLE = i18n._({ id: 'x', message: 'عنوان' })// ✗ در جزء: نمونهٔ سراسری وابستگیِ حافظه نیستconst title = (): string => i18n._({ id: 'x', message: 'عنوان' })// ✓const title = (i18n: I18n): string => i18n._({ id: 'x', message: 'عنوان' })message منبعِ ترجمه است و در آزمون و vite dev پشتیبانِ پیامی که هنوز استخراج نشده؛ بیلدِ تولیدی آن را
برمیدارد (بالاتر، vite/messages.ts) و شناسهٔ خام دیده نمیشود چون کاتالوگ پیش از اولین رندر مینشیند و
i18n:check هر شناسه را در کاتالوگ میخواهد.
عدد، پول، تاریخ
Section titled “عدد، پول، تاریخ”هرگز دستی؛ همیشه از formatter(locale) ِ @darzsaz/i18n — بی زبان ساخته نمیشود. در رابط
useFmt() در جزء و fmtOf(i18n) بیرون از React (apps/web/src/lib/useFmt.ts)؛ عددی که درونِ پیام
است مقدارِ تیپدار میماند (count، mm، decimal) تا جمعِ ICU کار کند:
| کار | تابع | چرا |
|---|---|---|
| اندازه، شمارنده | fmt.mm(v)، count |
بی گروهبندی — «۲٬۱۰۰ میلیمتر» غلط است |
| متر، متر مربع | fmt.decimal(v, d) |
رقمِ اعشارِ ثابت |
| مبلغ | fmt.money(v) |
با گروهبندی؛ واحد را پیام مینویسد |
| درصد | fmt.percent(v) |
یک رقم اعشار |
| تاریخ | fmt.date(iso) |
ذخیره ایزوی میلادی، نمایش تقویمِ زبان |
| لحظه و ساعت | fmt.moment، clock |
روز و ساعت؛ ساعت با یا بی ثانیه |
| زمانِ نسبی | fmt.relative(v, u) |
«۲ دقیقه پیش»، «دیروز» |
| فهرست | fmt.list(items) |
«الف، ب و ج» |
| رشتهٔ قالبدار | fmt.digits(s) |
شمارهٔ نسخه، شناسهٔ قطعه — عدد نیست |
چرا زبان اجباری است: num، money، percent، isoDate و localizeDigits ِ هسته (intl.ts) و
faNum پیشفرضِ fa-IR داشتند و ۳۲۷ فراخوانی زبان نمیداد: رابطِ انگلیسی «۲۱۰۰» نشان میداد، و
HistoryPanel قالبگرِ ساعت را در سطحِ ماژول میساخت — قفل با زبانِ لحظهٔ بارگذاری. رابط، برگه و خط
فرمان در فاز ۳ ِ نسل پنجم از آنها کنده شدند (در رابط ۱۲۰ num، ۱۰ money، ۹ isoDate، ۲
localizeDigits، ۳ toLocale… و DEFAULT_LOCALE ِ قالبگر ← ۰) و بعد خودشان از هسته رفتند: قالبگرِ
بیزبان دیگر وجود ندارد و فراخوانیاش خطای کامپایل است. از هسته فقط اینها ماند — هیچکدام قالب نمیزند:
today() و nowIso() (core/src/today.ts، روزِ محلیِ ذخیرهای به شکلِ YYYY-MM-DD و لحظهٔ ISO ِ محلی با
منطقه برای meta) و toLatinDigits/digitsOnly
(core/src/digits.ts، رقمِ فارسی و عربیِ ورودی به لاتین). و نامهای money، percent و date ِ بارولِ
هسته حالا همان مقدارِ تیپدارِ پیاماند؛ packages/core/test/message.test.ts هر دو را میسنجد.
تاریخ درونِ پیام مقدارِ تیپدارِ date(iso) است («نسخهٔ مرورگر: {browser}» در
ImportConflictDialog) و بیرون از پیام fmt.date(iso). لحظه (روز و ساعت) درونِ پیام مقدارِ تیپدارِ
moment(ms) است («برگرداندنِ نسخهٔ {when}» در AutoVersions، با ماهِ کوتاهِ زبان) و بیرون از پیام fmt.moment.
تقویم خودکار است: Intl برای fa-IR شمسی و برای en-US میلادی میدهد.
افزودن زبان سوم
Section titled “افزودن زبان سوم”۱. یک ردیف در LOCALES (packages/i18n/src/locales.ts).
۲. LOCALE_NAME و LOCALE_DIR در همان پرونده، CATALOGS و CLI_CATALOGS در
packages/i18n/src/catalogs.ts، CATALOG و PAPER_OF (کاغذِ پیشفرضِ برگه) در apps/web/src/lib/locale.ts و
DOCS_LOCALE (پیشوندِ سایتِ مستندات) در apps/web/src/lib/rule-doc.ts — تیپ Record<Locale, …> است، پس
بیآنها کامپایل نمیشود.
۳. pnpm i18n:extract، ترجمهٔ packages/i18n/src/locales/<locale>.po و cli.<locale>.po، و
pnpm i18n:compile.
۴. رقمِ زبان را Intl خودش میدهد — formatter(locale).digits رقمِ صفرِ همان زبان را میخواند.
۵. کلید خانه را منو کن. دکمهٔ «زبانِ دیگر» فقط با دو زبان معنا دارد؛
test/locale-switch.dom.test.tsx با زبان سوم میافتد تا یادآوری کند. منو در
تکهٔ تنبل، نه Select در تکهٔ اولیه. منوی «نما» خودش از LOCALES میخواند.
vite.config، lingui.config و scripts/font-coverage.mjs خودشان LOCALES را وارد میکنند —
نه با الگو از متنِ پرونده، که کامنت را هم میگرفت و فهرست را برعکس میداد.
فرمانها و نگهبانها
Section titled “فرمانها و نگهبانها”pnpm i18n:extract # هر کاتالوگِ lingui.config.js را از کد پر میکند (اصلی و cli.*)pnpm i18n:compile # کنارِ هر .po یک .ts ِ کامپایلشده برای خط فرمان و آزمونِ Nodepnpm i18n:check # در verifypnpm lint:baseline # رشتهٔ سختکد و kit/* بی استثنا — در verify و CI (پایین، «رشتهٔ سختکد»)pnpm lint:baseline --update # پس از درست کردنِ نقض یا ادغام: هرسِ ESLint، هرگز بزرگترpnpm e2e sweep -u # پس از ترجمه: فهرستِ پایهٔ شبهزبان و جاروی en-US = امروز (بالاتر، «فهرستِ پایه»)node scripts/i18n-glossary.mjs # نگهبانِ واژهنامه — در verify و CI (پایین)نگهبانِ واژهنامه (۱۰.۳): هر پیامِ فارسی که اصطلاحی از i18n-glossary.md دارد، در
en-US.po (و cli.en-US.po) همان برابرنهاده را دارد — «درز» ← reveal، نه gap. سطرهای خودِ سند منبعاند:
جدولِ اصطلاحها، جدولِ «معناهای دیگر» (عبارتِ بلندتر برنده است: «نمای سهبعدی» 3D view میخواهد، نه
front) و «استثنا به شناسه» با «چرا». نخستین اجرا ۱۱۸ برخورد داد که بیشترش معنای دیگرِ همان واژه بود؛
پنج ترجمهٔ غلط درست شد («تاج» crown molding ← cornice، «مانع» obstacle ← obstruction، «قید قاب»).
آزمون: scripts/test/i18n-glossary.test.mjs.
سرورِ dev با تغییرِ lingui.config.js از نو راه میافتد (apps/web/vite/lingui-config-watch.ts):
@lingui/vite-plugin پیکربندی را فقط در آغاز میخواند و vite آن پرونده را نمیپایید. سرورِ dev ای که پیش از
جابهجاییِ کاتالوگها به packages/i18n روشن بود کد را با HMR تازه گرفت و پیکربندی را نه، و روی
«Requested resource … fa-IR.po is not matched to any of your catalogs paths» ایستاد. آزمون:
test/lingui-config-watch.test.ts.
i18n:check اینها را برای هر کاتالوگِ lingui.config.js میسنجد — فهرست از خودِ پیکربندی،
نه از پوشه — بی آنکه پروندهای بنویسد (آزمون: scripts/test/i18n-check.test.mjs):
۱. vite.config.ts از LOCALES میخواند، نه فهرستِ دستی.
۲. هر کاتالوگ برای هر زبان یک .po و یک .ts ِ کامپایلشده دارد، و هر پروندهٔ کاتالوگ در آن پوشه مالِ
یک کاتالوگ و یک زبان است.
۳. هیچ پیامی ترجمهنشده نیست و جاینگارِ هر ترجمه همان منبع است — با همان تجزیهگرِ ICU
که Lingui کامپایل میکند: «{name} حذف شد» که در انگلیسی «deleted» شده باشد بیصدا نامِ پروژه
را میانداخت. جمعِ ICU ({n, plural, …}) همان جاینگار شمرده میشود.
۴. هیچ شناسهای در دو کاتالوگ نیست (بالا، «دو کاتالوگ»).
۵. استخراج و کامپایل هیچ چیزی را عوض نمیکنند — در پوشهٔ موقت، هر کاتالوگ در زیرپوشهٔ خودش.
ترجمهای که همین حالا نوشتهای «تغییر» است ولی کهنگی نیست. نسخهٔ نسل چهارم روی کاتالوگِ ردیابیشده
استخراج میکرد: اجرای اول قرمز و پرونده درستشده، اجرای دوم سبز. نسخهٔ یککاتالوگی هر کاتالوگ را به یک
مسیرِ موقت میبرد و با کاتالوگِ دوم هر cli.fa-IR.po را «بی زبان» میخواند. کاتالوگِ تازهساخته هم
همینجا گرفته شد: Lingui سرآیندِ کامل را در استخراجِ دوم مینویسد، پس پس از افزودنِ کاتالوگ دو بار
i18n:extract لازم است.
--overwrite در استخراج (هم pnpm i18n:extract، هم نگهبان): msgstr ِ زبانِ منبع (فارسی) همیشه
همان message ِ کد است. بی آن، Lingui با شناسهٔ صریح متنِ دیروز را نگه میداشت — PO پیامِ پیشین را
ندارد که با ترجمه مقایسه کند (mergeCatalog) — و پیامی که در کد عوض شد، فارسیِ کهنه رندر میشد و نگهبان
سبز بود. در ۳.۶ «بازده ۸۵٪» ِ parts که جاینگار شد در کاتالوگِ فارسی عددِ ثابت ماند؛ آزمونِ
i18n-check حالا msgstr ِ منبعِ کهنه را کهنگی میشمارد. زبانِ غیرمنبع با --overwrite دست نمیخورد.
--clean هم، در هر دو: پیامی که از کد رفت از کاتالوگ هم میرود. فرمان و نگهبان یک فهرستِ آرگومان
dارند (EXTRACT_ARGS در scripts/i18n-check.mjs، آزمون package.json را با آن میسنجد): تا ادغامِ ۳.۶
فقط نگهبان --clean داشت و استخراجِ دستی مدخلِ کهنهٔ #~ میگذاشت که همان نگهبان «کهنه» میخواندش.
پیامِ چندسطری ننویس (\n درونِ message): Lingui آن را مدخلِ چندسطریِ PO مینویسد و الگوهای
تکسطریِ نگهبان نیمهاش را میخوانند؛ شکستِ دستیِ سطر هم در زبانِ دیگر جای دیگری میافتد. دو پیام، یا
یک سطر که ترمینال خودش بپیچد.
در ESLint، TSX ِ رابط i18n ِ سراسری را وارد نمیکند (بالاتر، «رابط: useLingui و useFmt»؛ آزمون
scripts/test/eslint-web-i18n.test.mjs)؛ منعِ قالبگرهای بیزبانِ هسته با رفتنِ خودشان برداشته شد و نبودنشان را
packages/core/test/message.test.ts میسنجد.
رشتهٔ سختکد: قاعده و فهرستِ پایه (۳.۸)
Section titled “رشتهٔ سختکد: قاعده و فهرستِ پایه (۳.۸)”lingui/no-unlocalized-strings روی همهٔ کدِ تولیدی روشن است — packages/*/src و apps/*/src، بستهای
که فردا بیاید هم (eslint.config.js، کنارِ unlocalized). هر رشتهای که حرف دارد خطاست، مگر فهرستِ مجازِ
فنی بشناسدش. بیروناند: آزمون و e2e (پیکره و ادعا روی متن)، packages/ui/stories (هرگز پخش نمیشود)،
افزونههای ساختِ apps/web/vite (متنِ مانیفست را از کاتالوگ میخوانند) و سایتِ مستندات (متنش Markdown است
و ترجمهاش locales ِ Starlight، ۱۰٫۵).
بی الگوی «فقط فارسی». تا ۳.۸ قاعده پنج پرونده را میدید با ignore: ['^[^-ۿ]*$']: هر رشتهٔ
بی حرفِ فارسی — aria-label="Close"، toast('Saved') — نادیده بود (I7). رشتهٔ انگلیسیِ سختکد در رابطِ
فارسی همانقدر ترجمهنشده است که فارسی در رابطِ انگلیسی.
فهرستِ مجاز: اول شکل، بعد جا. متنِ دیدنیِ این مخزن فارسی است (حالتِ حرف ندارد) یا عبارتِ انگلیسی (با
فاصله یا حرفِ بزرگ)؛ شناسه، کلید، کلاس، مسیر، MIME، نامِ رویداد و کلیدِ ذخیره یک نشانهٔ ASCII ِ بیفاصلهاند.
پس اول شکلِ رشته (ignore) — که به نامِ صفت یا تابعِ هیچ جزئی بسته نیست — و فقط آنچه شکل نمیشناسد با
جا (ignoreNames، ignoreFunctions). هر مدخل دلیلش را کنارِ خودش دارد و هر کدام را دستِکم یک رشتهٔ
امروز لازم دارد — با برداشتنِ تکتکشان سنجیده شد (ستونِ آخر: خطای تازه بی آن مدخل):
| مدخل | چه | بی آن |
|---|---|---|
| نشانهٔ ASCII ِ بیفاصله که با حرفِ بزرگ شروع نشود | شناسه، کلید، مسیر، رنگ، MIME — الگوی آغازینِ مستندِ افزونه، فقط ASCII | ۱٬۵۵۰ |
| نشانهٔ تمامبزرگ | متغیرِ محیط، ثابت، نامِ لایهٔ DXF، A4، QR — الگوی دومِ مستندِ افزونه | ۲۴ |
| چند واژهٔ چسبیده با حرفِ بزرگ | نامِ کلاس و خطا (DarzError، AbortError) |
۳ |
| مسیرِ مطلقِ دوبخشی | نشانیِ Chrome در خط فرمان | ۶ |
| نشانهگذاریِ بی متن | HTML/SVG/XML ِ برگه و xlsx؛ حرف میانِ برچسب یا در صفتِ متنی خطاست | ۱۹۱ |
نام="مقدار" ِ بی متن |
میانهٔ برچسبی که در چند قالبِ بههمچسبیده نوشته شده | ۵ |
className، style، placement |
چند کلاسِ CSS با فاصله، مقدارِ CSS، جای شناورِ React Aria | ۴+۱+۱ |
shortcut، keys |
نامِ کلید («Ctrl+Z»، «Delete») — در هر زبان یکی | ۱۱+۹ |
console.* (جز خط فرمان) |
گزارشِ توسعهدهنده؛ در خط فرمان کنسول خودِ رابط است | ۳ |
text |
text() ِ پیام متنِ عینی است (فرمانِ نمونه) |
۳ |
*.querySelectorAll، matchMedia، *.matchMedia |
گزینشگرِ CSS، پرسوجوی رسانه | ۱+۲+۱ |
*.waitForFunction |
جاوااسکریپتی که خط فرمان در Chrome اجرا میکند | ۲ |
useTsTypes |
مقدارِ اتحادِ رشتهٔ ثابت ('A4' | 'Letter') |
۱ |
*.describe فقط در core/src/schema |
سندِ قالبِ پرونده (پایینِ «مسئلهٔ zod») | ۴۹ |
| چهار پرونده | منبعِ توکن (ui/src/{themes,tokens}.ts)، sheet-css.ts ِ تولیدی، GLSL |
۳۸ |
id، key، type، role، data-* و نامِ رویداد مدخل نشدند: مقدارشان همیشه نشانه است و شکل میگیردش.
الگوها بی پرچمِ u ساخته میشوند (new RegExp(s) در افزونه)، پس «حرف» بازهٔ نوشتهشده است، نه \p{L}.
چهار کمبودِ افزونه (نسخهٔ سنجاقشدهٔ ۰٫۱۵٫۰) با پوششی کوچک روی خودِ قاعده (withProjectMessages) بسته شد،
هر کدام با آزمون در scripts/test/eslint-strings.test.mjs که بی آن میافتد:
۱. پیامِ /*i18n*/. افزونه فقط i18n._/t/msg را پیام میشناسد؛ متنِ منبعِ هر شیءِ نشاندارِ هسته
۹۴۷ خطای دروغ بود. معیار همان معیارِ استخراجگر و vite/messages.ts است: کامنتِ i18n درست پیش از شیء،
و فقط مقدارِ ثابتِ id/message/comment/context ِ خودش. ignoreNames: ['message'] هر
{ message: 'متن' } ِ دیگری را هم پنهان میکرد.
۲. صفتِ متنی روی عنصرِ DOM و SVG. افزونه جز placeholder/alt/aria-label/value هر صفتِ عنصرِ DOM
و هر صفتِ SVG را آزاد میداند: title ِ دکمه و aria-label ِ <svg> — ۲۹ رشته در web.
۳. کدِ درونِ صفت. همان مسیر هر رشتهٔ درونِ صفت را مقدارِ صفت میگرفت، حتی درونِ تابع:
<button onClick={() => edit('حذف یونیت')}> دیده نمیشد. رشتهٔ درونِ تابع کد است و سنجیده میشود، جز در
صفتِ ignoreNames (className={({ isSelected }) => …}).
۴. <Select> ِ کیت. افزونه هر چه درونِ عنصرِ Trans/Plural/Select/SelectOrdinal است را ماکروی
JSX ِ Lingui میپندارد؛ این مخزن ماکرو ندارد و label و گزینههای Select ِ @darzsaz/ui بیرون میماند.
۳ و ۴ روی هم ۳۱ رشتهٔ دیدنیِ web (۷ و ۲۴)؛ هیچ آزمونی نمیدیدشان و فقط شمارشِ دوباره با درختِ نحو (روشِ پلن) پیدایشان کرد.
۲ تا ۴ به نامِ بازدیدکنندههای درونیِ افزونه تکیه دارند؛ نسخهٔ تازهای که عوضشان کند آزمون را قرمز میکند. بی
برنامهٔ TypeScript (آزمونِ قاعدههای دیگر با disableTypeChecked) قاعده بی useTsTypes اجرا میشود؛
وگرنه افزونه پرتاب میکرد و کلِ لینتِ آن پرونده میافتاد.
new Error('…') استثنا نیست. الگوی آغازینِ افزونه Error را آزاد میداند؛ اینجا errorText پیامِ Error
ِ ساده را به کاربر نشان میدهد (packages/i18n/src/error.ts): تا فاز ۱۰ web ۱۱ Error ِ فارسی داشت و
درونریزیِ کاتالوگ همین را در اعلان میگذاشت؛ حالا خطای بوم و تصویر (lib/render/errors.ts) و «شیء نیست» ِ
کاتالوگ (catalog.panel.not-object) DarzError با پیاماند. خطای کاربر DarzError با پیام است؛ خطای
برنامهنویس (نمونهٔ i18n ِ ناشناخته، unreachable) eslint-disable-next-line lingui/no-unlocalized-strings -- <چرا>
کنارِ خودش.
استثنای درونخط، هر کدام با دلیل: خطای برنامهنویس (nest.ts، render.ts، doc.ts ×۲، useFmt.ts، و در
کیت Provider.tsx و contrast.ts ×۴)، نامِ زبان به خطِ خودش (locales.ts)، نمادِ مختصاتِ «x=»/«y=»
(drill-svg.ts)، متنِ DXF (dxf.ts)، متنِ CSS و دادهٔ مسیرِ SVG (paper.ts، qr.ts، و مقدارِ CSS ِ Layout.tsx
×۲ در کیت)، نامِ میدانِ --json و جاوااسکریپتِ صفحه در خط فرمان (lang.ts، render-page.ts)، اسکریپتِ
درونخطیِ پوسته در کیت (theme.ts)، و در web نامِ کلید (keys.ts)، نشانههای sandbox (print.ts) و نامِ
افزونهٔ WebGL (webgl.ts).
بی استثنا — فهرستِ پایه مدخل نمیپذیرد (۱۰.۴)
Section titled “بی استثنا — فهرستِ پایه مدخل نمیپذیرد (۱۰.۴)”آنچه روزِ روشن شدن پیام نبود در scripts/lint-baseline.json (suppressions ِ خودِ ESLint، هر پرونده یک عدد؛
pnpm lint و lint:fix و قلابِ pre-commit آن را با --suppressions-location میخوانند) شمرده شد و فاز ۱۰ آن را
به {} رساند. از آن روز pnpm lint:baseline (در verify و CI) هر مدخلی برای این قاعده و هر قاعدهٔ kit/* را
رد میکند — در هر بسته، و برای قاعدهٔ کیتِ فردا هم (exceptionFree، پیشوند و نه فهرست): رشتهٔ تازه پیام
میشود، کنترلِ تازه از کیت میآید.
چرا شکلِ فهرست: نگهبانِ پیشین جمعِ هر قاعده را با فهرستِ شاخهٔ پایه میسنجید، و فهرستِ v5 تهی است و main
فهرست ندارد — eslint --suppress-rule رشتهٔ تازهٔ apps/web را بیصدا فهرست میکرد و جز diff چیزی
نمیگرفتش؛ «بستهٔ تبدیلشده» (CONVERTED: هسته، i18n، برگه، خط فرمان) همین را فقط برای چهار بسته میگفت.
سازوکارِ فهرستِ فقطکوچکشونده برای قاعدهٔ دیگری است که با نقضِ قدیمی روشن میشود (BASELINED؛ از نسل
ششم darzsaz/no-persian-comment — کامنتِ انگلیسی، conventions.md): ESLint خودش یکی بیشتر از عددِ پرونده (همهٔ خطاهای آن پرونده قرمز) و یکی کمتر («suppressions
left that do not occur anymore»، کدِ ۲) را میگیرد، و pnpm lint:baseline بقیه را — قاعدهٔ بیرون از
BASELINED، پروندهٔ ناموجود یا بی قاعده (ESLint فقط پروندهٔ لینتشده را با فهرست میسنجد و عددش تا ابد
میماند)، و جمعِ هر قاعده بزرگتر از شاخهٔ پایه (--base origin/main؛ جمع و نه هر پرونده، همان تصمیمِ
css-lint). --update هرسِ خودِ ESLint (--prune-suppressions) روی رونوشت است و فقط وقتی مینویسد که هیچ
پروندهای تازه یا بیشتر نشده. آزمون: scripts/test/lint-baseline.test.mjs.
پیشرفت ترجمه
Section titled “پیشرفت ترجمه”امروز صفر و بی استثنا (pnpm lint:baseline، بالا). روزِ ۳.۸ (۱۴۰۵/۰۶/۲۳):
| بسته | رشته در فهرستِ پایه | وضعیت |
|---|---|---|
packages/core، packages/i18n، packages/report، apps/cli |
۰ — بیرون از فهرست | ✅ |
packages/ui |
۰ | ✅ |
packages/geometry |
۴۳ در ۶ پرونده | ⏳ |
apps/web |
۱٬۱۷۴ در ۱۱۷ پرونده | ⏳ |
از ۱٬۱۷۴ ِ web، ۱۱ فارسی نبودند و فاز ۱۰ برایشان استثنا گذاشت یا شکلشان را عوض کرد؛ فهرستِ پایه امروز {}
است: گزینشگرِ focus(…)، «Ctrl+Z» ِ
SettingsPanel، نگهبانِ \0none، پیشوندِ کلیدِ Arrow، سه نمایهٔ Dexie، sandbox ِ قابِ چاپ، دو خطای
برنامهنویس (useFmt، main.tsx) و transform ِ viewport.ts.
چرا کمتر از ۲٬۶۶۴ ِ پلن: آن عدد رشتهٔ فارسیِ بیرون از i18n._ در کلِ src با درختِ نحو بود. از آن روز
۳.۲ تا ۳.۶ هسته (۸۵۶)، برگه (۲۶۷)، خط فرمان (۲۴۸) و ui (۱) را پیام کردند؛ همان شمارش امروز ۱٬۲۷۵ است. آنچه
از آن در فهرست نیست (۶۹): ۴۹ .describe، ۹ رشتهٔ بی حرف (رقم، «٪»، «، »، «؟»)، ۳ console، ۲ کامنتِ GLSL، ۲
کامنتِ CSS ِ تولیدی، ۳ خطای برنامهنویسِ کیت و نامِ «فارسی». قاعده بهجایش ۱۱ رشتهٔ غیرفارسیِ بالا را میشمارد:
۱٬۲۷۵ − ۶۹ + ۱۱ = ۱٬۲۱۷.
هر پیامِ en-US ترجمه شده است (۲٬۰۴۹ ِ اصلی و ۱۶۵ ِ خط فرمان) و i18n:check پیامِ ترجمهنشده را رد میکند؛
نسخهٔ انگلیسی دیگر نیمهفارسی نیست.
واژهنامهٔ اصطلاحات: i18n-glossary.md