Skip to content

چندزبانه

This content is not available in your language yet.

مرجع زنده. زبانِ پیش‌فرض فارسی است؛ انگلیسی زبان دوم. زیرساخت برای زبان سوم آماده است: افزودنش از یک ردیف در LOCALES شروع می‌شود و تیپ‌اسکریپت بقیه را می‌خواهد («افزودن زبان سوم»).

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` (کامپایل‌شده، ادغام)

پیامی که در 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 است.

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.msgError.message متنِ منبع با مقدارِ خام است (sourceText) و فقط برای پشتهٔ خطا و لاگ. نمایشِ هر خطا با errorText(i18n, e)؛ خطایی که در وضعیت می‌ماند و بعد رندر می‌شود (نوار پایین، نشانِ ذخیره، «یک ورق کمتر») errorMsg(e) ِ هسته را نگه می‌دارد تا تعویضِ زبان آن را هم عوض کند.

Issue.message و Fix.label پیام‌اند؛ RuleMeta.why پیامِ بی مقدار (MessageDescriptorCATEGORY_NAME و SEVERITY_NAME هم (validate/types.ts). شناسه‌ها:

چه شناسه
پیامِ قاعده rule.<قاعده> یا با شکلش rule.run-gap.base.small
راه‌حل rule.<قاعده>.fix یا rule.<قاعده>.fix.<نام>؛ مشترک: rule.fix.split-doors
چرا rule.<قاعده>.whyrules-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.* ِ کاتالوگ ساخته می‌شود، در انگلیسی حرفِ فارسی ندارد (متنِ کاربر و نامِ کاتالوگ داده‌اند و کنار می‌روند) و عددش لاتین است، و آشپزخانهٔ نمونه در فارسی همان متنِ پیشین را می‌گیرد. قاعدهٔ تازه بی موردِ پیکره این آزمون را قرمز می‌کند.

لبه نمونهٔ 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) — بی نویسهٔ جداسازِ جهت
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.addke در کاتالوگِ ۲٬۰۴۹ پیامیِ امروز؛ با هر پیامِ تازه جابه‌جا می‌شود) — در شیءِ /*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» است و لحظهٔ نسخه رقمِ لاتین دارد؛ روی سورسِ پیشین افتاد.

نامِ پروژه و اتاق و یادداشتِ آشپزخانهٔ نمونه دادهٔ کاربر‌اند: در پرونده می‌مانند، کاربر عوضشان می‌کند و به لیست برش و برگه می‌روند. پس Msg نمی‌مانند؛ سازنده آن‌ها را با Say = (m: Msg) => string به زبانِ لحظهٔ ساخت رشته می‌کند و بعد همان می‌ماند. نامِ یونیت، دیوار، مانع و برشِ نمونه نوشته نمی‌شود (موجِ ۶): نام در لحظهٔ نمایش از presetId (UNIT_PRESET در units/names.ts)، شمارهٔ دیوار یا نوع می‌آید (unitLabel، wallLabel، obstacleLabel، cutoutLabeldata-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) saybench
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 را درونِ جملهٔ فارسی می‌گذاشت (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 می‌سازد و هرگز در رابط دیده نمی‌شود. کنارِ اسکیما می‌ماند تا سند از کد دور نشود؛ همان دلیلی که آن سند تولیدی است.

نامِ محصولِ کاتالوگ، نامِ پروژه، دیوار و یونیت و هر متنِ کاربر در رابطِ انگلیسی فارسی می‌ماند. کنارِ متن یا عدد، بی جداسازی کلِ جمله در پاراگرافِ چپ‌به‌راست راست‌به‌چپ چیده می‌شد: «ملامینه سفید ۱۶: 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=۷۱۱ بود و افتاد.

دو جاروی e2e متنی را می‌شمارند که روی صفحه هست و از کاتالوگ نیامده؛ شمارِ هر سطح فهرستِ پایه‌ای است که فقط کوچک می‌شود، و فاز ۱۰ آن را صفر کرد (۱۰.۴): هر فهرستِ en-US.* خالی است و در شبه‌زبان فقط دو متنِ غیرِ پیام مانده — شمارهٔ نسخه در راهنما (#.#.#-alpha.#) و نشانیِ ۴۰۴ (/no-such-route). نگهبانِ ایستای ۳.۸ رشتهٔ کد را می‌شمارد؛ این دو آنچه کاربر می‌بیند: متنی که از هسته، کیت یا کاتالوگِ ایران می‌آید و متنی که از چند تکه ساخته شده.

pseudoLocale ِ خودِ Lingui (lingui.config.js): هر تکهٔ متنِ هر پیام ⟦…⟧ و ۴۰٪ بلندتر — { locale: 'pseudo', prepend: '⟦', append: '⟧', extend: 0.4, extendCharacter: '•' }. روزِ ۳.۹ روی ۹۲۵ پیام (۱٬۳۱۶ تکهٔ متن) ۲۶٬۶۵۹ نویسه ← ۳۹٬۱۲۷، یعنی ×۱٫۴۷. حرفِ لاتینِ درونِ پیام را pseudolocale آکسان‌دار می‌کند (.darz.ďàŕź)؛ فارسی همان می‌ماند.

Terminal window
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): خانه، ویرایشگر با پنل‌ها و یونیتِ انتخاب‌شده، و کارگاه روی گوشی — آخری چون روزِ ۳.۹ تنها بریدگی آنجا بود و پوستهٔ ویرایشگر هنوز سخت‌کد بود — و از ۱۰.۴ مسیرها، هزینه، هر تبِ کارگاه و هر پنجرهٔ ویرایشگر (پایین). آزمونِ اول می‌سنجد بیلد سرو می‌شود و سرتیترِ خانه ⟦•درزساز•⟧ است.

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 است».

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.

لرزان نیست: صبر تا سه پیمایشِ پیاپیِ یکسان (هر ۳۰۰ میلی‌ثانیه) پس از نشانِ ساختاریِ هر سطح (نوارِ سه‌بعدی و عددهای نوار پایین، جدولِ هزینه، لیستِ قطعات)، اندازهٔ ثابتِ صفحه، و اعلان بیرون از شمارش: «برنامه برای کار آفلاین آماده شد» با کشِ سرویس‌ورکر می‌آید و در اجراهای اول یک بار روی هزینه و یک بار روی خانه نشسته بود.

هر صفحه نامِ خودش را با 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.

رابط: ?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 هر شناسه را در کاتالوگ می‌خواهد.

هرگز دستی؛ همیشه از 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 میلادی می‌دهد.

۱. یک ردیف در 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 را وارد می‌کنند — نه با الگو از متنِ پرونده، که کامنت را هم می‌گرفت و فهرست را برعکس می‌داد.

Terminal window
pnpm i18n:extract # هر کاتالوگِ lingui.config.js را از کد پر می‌کند (اصلی و cli.*)
pnpm i18n:compile # کنارِ هر .po یک .ts ِ کامپایل‌شده برای خط فرمان و آزمونِ Node
pnpm i18n:check # در verify
pnpm 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.pocli.en-US.po) همان برابرنهاده را دارد — «درز» ← reveal، نه gap. سطرهای خودِ سند منبع‌اند: جدولِ اصطلاح‌ها، جدولِ «معناهای دیگر» (عبارتِ بلندتر برنده است: «نمای سه‌بعدی» 3D view می‌خواهد، نه front) و «استثنا به شناسه» با «چرا». نخستین اجرا ۱۱۸ برخورد داد که بیشترش معنای دیگرِ همان واژه بود؛ پنج ترجمهٔ غلط درست شد («تاج» crown moldingcornice، «مانع» obstacleobstruction، «قید قاب»). آزمون: 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}.tssheet-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.

امروز صفر و بی استثنا (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