From 663c4b06482360b414a9b1c01e212d46c50a1532 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:48:08 -0400 Subject: [PATCH] Fix: use the Persian terms shipped software uses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Persian catalogue reached for dictionary coinages that translated dashboards do not use, and named one concept two ways. درون‌ریزی and برون‌بری for import and export appear in none of Immich, Jellyfin or Uptime Kuma. All three build import on وارد کردن, and Immich ships وارد کردن پیکربندی for "Import configuration" and خروجی for "Export". A widget was both ابزارک and ویجت, and a file both فایل and پرونده, in the same interface. One message left the English word "Import" inside the Persian, which the coverage test could not see because the value as a whole differs from the English. Two headings read as adjectives rather than as what is about to happen. The glossary is recorded in docs/i18n.md, and a test fails on any of the rejected terms in the dashboard catalogue or any widget catalogue. --- CHANGELOG.md | 4 +++ docs/i18n.md | 18 ++++++++++ ui/i18n/fa.json | 48 ++++++++++++------------- ui/test/i18n-coverage.test.mjs | 50 ++++++++++++++++++++++++++ ui/widgets/dns/i18n/fa.json | 2 +- ui/widgets/github/i18n/fa.json | 4 +-- ui/widgets/system-summary/i18n/fa.json | 4 +-- 7 files changed, 101 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a82aa7b..ea5de9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Counted messages now use each language's own plural rules. French and Persian put a count of zero in the plural form. +- Persian now uses the import, export, widget and file terms that shipped + software uses, one word per concept, and no longer leaves "Import" in English + in one message. + - Opening and saving an app no longer rewrites a colour that was not changed. - Admin form toggles now carry accessible names. diff --git a/docs/i18n.md b/docs/i18n.md index dce59925..75a9e80d 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -115,6 +115,24 @@ Terms with a fixed meaning here: | stale | Last reading is old; the service was not reached this time. | | unavailable | The service answered, and it is not working. | +### Persian + +One word per concept, taken from Persian that shipped software actually uses +rather than from the dictionary. Immich, Jellyfin and Uptime Kuma were read for +these; Immich is the closest to this project and ships +`وارد کردن پیکربندی` for "Import configuration" and `خروجی` for "Export". + +| English | Use | Not | +| --- | --- | --- | +| Import | وارد کردن | درون‌ریزی | +| Export | خروجی | برون‌بری | +| Widget | ویجت | ابزارک | +| File | فایل | پرونده | + +`ui/test/i18n-coverage.test.mjs` fails on any term in the "Not" column, in the +dashboard catalogue and in every widget catalogue. Add a row there and here +together when a term is decided. + ## Development locales Two locales exist for testing and are never offered in the selector. Reach them diff --git a/ui/i18n/fa.json b/ui/i18n/fa.json index 45f1d701..a67cf22c 100644 --- a/ui/i18n/fa.json +++ b/ui/i18n/fa.json @@ -13,8 +13,8 @@ }, "common": { "save": "ذخیره", - "import": "درون‌ریزی", - "export": "برون‌بری", + "import": "وارد کردن", + "export": "خروجی", "add": "+افزودن", "back": "بازگشت", "cancel": "لغو", @@ -55,7 +55,7 @@ "backup": "پشتیبان‌گیری", "importExport": "وارد کردن / خروجی گرفتن", "signOut": "خروج از حساب", - "importForeign": "درون‌ریزی از داشبورد دیگر", + "importForeign": "وارد کردن از داشبورد دیگر", "importForeignTip": "فایل‌های YAML مربوط به gethomepage و Dashy را می‌خواند. پیوندها و پوشه‌ها به داشبورد شما افزوده می‌شوند؛ ویجت‌های داشبورد دیگر به پیوند ساده تبدیل می‌شوند.", "logDebug": "اشکال‌زدایی", "logInfo": "اطلاعات", @@ -108,13 +108,13 @@ "searchPh": "جستجو", "filterAll": "همه", "filterApps": "برنامه‌ها", - "filterWidgets": "ابزارک‌ها", + "filterWidgets": "ویجت‌ها", "filterFolders": "پوشه‌ها" }, "type": { "addNew": "افزودن جدید", "app": "برنامه", - "widget": "ابزارک", + "widget": "ویجت", "folder": "پوشه" }, "folder": { @@ -223,12 +223,12 @@ "imported": "وارد شد", "importFailed": "Import ناموفق بود: {err}", "exportFailed": "Export ناموفق بود: {err}", - "importNoChange": "چیزی برای Import نیست — فایل با تنظیمات فعلی شما مطابقت دارد.", + "importNoChange": "چیزی برای وارد کردن نیست — فایل با تنظیمات فعلی شما مطابقت دارد.", "pwWeak": "رمز عبور ضعیف است: {label}", "sessionsRevoked": "همه دستگاه‌ها خارج شدند.", "authNeedsPassword": "پیش از فعال‌سازی احراز هویت، یک گذرواژه تعیین کنید.", - "importForeignDone": "درون‌ریزی پایان یافت. برنامه‌ها: {apps}. پوشه‌ها: {folders}.", - "importForeignNothing": "در این فایل‌ها چیزی برای درون‌ریزی نیست.", + "importForeignDone": "وارد کردن پایان یافت. برنامه‌ها: {apps}. پوشه‌ها: {folders}.", + "importForeignNothing": "در این فایل‌ها چیزی برای وارد کردن نیست.", "importYamlUnsupported": "{file}: {reason} (خط {line})", "importUnknownFormat": "{file} پیکربندی gethomepage یا Dashy نیست.", "secretsWithheld": "اعتبارنامه را دوباره وارد کنید برای: {items}. نگه داشته نشد، چون درخواستِ مربوط به آن تغییر کرده است.", @@ -252,8 +252,8 @@ "noMatches": "نتیجه‌ای یافت نشد." }, "import": { - "confirm": "{n} مورد درون‌ریزی شود؟ {added} افزوده، {updated} به‌روزرسانی، {deleted} حذف.", - "confirmTitle": "درون‌ریزی پیکربندی" + "confirm": "{n} مورد وارد شود؟ {added} افزوده، {updated} به‌روزرسانی، {deleted} حذف.", + "confirmTitle": "وارد کردن پیکربندی" }, "status": { "needsAttention": "وضعیت: نیاز به رسیدگی", @@ -292,12 +292,12 @@ }, "widgetCfg": { "unavailable": "تنظیمات در دسترس نیست", - "unavailableTip": "تعریف این ابزارک ({type}) بارگذاری نشد، بنابراین تنظیمات آن قابل نمایش یا ویرایش نیست. این تنظیمات روی سرور نگهداری می‌شوند و ذخیره‌کردن آن‌ها را تغییر نمی‌دهد.", - "refused_one": "‏{count} ابزارک بارگذاری نشد، بنابراین در فهرست زیر نیست.", - "refused_other": "‏{count} ابزارک بارگذاری نشدند، بنابراین در فهرست زیر نیستند.", + "unavailableTip": "تعریف این ویجت ({type}) بارگذاری نشد، بنابراین تنظیمات آن قابل نمایش یا ویرایش نیست. این تنظیمات روی سرور نگهداری می‌شوند و ذخیره‌کردن آن‌ها را تغییر نمی‌دهد.", + "refused_one": "‏{count} ویجت بارگذاری نشد، بنابراین در فهرست زیر نیست.", + "refused_other": "‏{count} ویجت بارگذاری نشدند، بنابراین در فهرست زیر نیستند.", "name": "نام", - "type": "نوع ابزارک", - "namePh": "ابزارک من", + "type": "نوع ویجت", + "namePh": "ویجت من", "editName": "ویرایش نام", "size": "اندازه", "iframeTip": "این نشانی به‌صورت آی‌فریم در داشبورد جاسازی می‌شود.", @@ -314,12 +314,12 @@ "loaded_other": "{count} گزینه بارگیری شد" }, "importForeign": { - "title": "درون‌ریزی از داشبورد دیگر", + "title": "وارد کردن از داشبورد دیگر", "lead": "آنچه از پیش در داشبورد دارید تغییر نمی‌کند.", - "willAdd": "برنامه‌های افزودنی", - "willCreate": "پوشه‌های ساختنی", - "changed": "با تغییر درون‌ریزی شد", - "notImported": "درون‌ریزی نشد", + "willAdd": "برنامه‌هایی که افزوده می‌شوند", + "willCreate": "پوشه‌هایی که ساخته می‌شوند", + "changed": "با تغییر وارد شد", + "notImported": "وارد نشد", "appCount": "({n})", "skipNoLabel": "بدون نام", "skipNoHref": "بدون پیوند", @@ -330,15 +330,15 @@ "noteRemoteContainer": "کانتینر نادیده گرفته شد، روی {detail} اجرا می‌شود", "noteFlattened": "گروه تودرتو به پوشه‌ای جداگانه تبدیل شد", "noteSubItems": "زیرمورد به پوشه منتقل شد", - "noteWidgetLink": "ویجت به‌صورت پیوند درون‌ریزی شد", + "noteWidgetLink": "ویجت به‌صورت پیوند وارد شد", "noteWidgetDropped": "{detail} ویجت بخش کنار گذاشته شد", "noteFields": "فیلدهای کنارگذاشته: {detail}", "notePages": "{detail} صفحه دیگر دنبال نشد", "noteLocalUrl": "نشانی محلی دوم کنار گذاشته شد: {detail}", - "wholeFile": "همین پرونده", - "untitledFolder": "درون‌ریزی‌شده", + "wholeFile": "همین فایل", + "untitledFolder": "واردشده", "skipRelativeHref": "پیوند نشانی ندارد، فقط یک مسیر", - "skipUnreadable": "این ورودی ساختاری ندارد که قالب پرونده بپذیرد", + "skipUnreadable": "این ورودی ساختاری ندارد که قالب فایل بپذیرد", "skipUnparsable": "خط {detail} خوانده نشد", "monitors": "· {url} را می‌پاید" }, diff --git a/ui/test/i18n-coverage.test.mjs b/ui/test/i18n-coverage.test.mjs index e340a6be..c9972a34 100644 --- a/ui/test/i18n-coverage.test.mjs +++ b/ui/test/i18n-coverage.test.mjs @@ -103,3 +103,53 @@ for (const [file, allowed] of Object.entries(SAME_ON_PURPOSE)) { assert.deepEqual(untranslated, [], `${file}: still English. Translate, or add to SAME_ON_PURPOSE with a reason.`); }); } + +/* ── one word per concept ─────────────────────────────────────────────────── */ + +/* A machine translates each string on its own, so the same concept comes back + under a different word in each. The Persian catalogue named a widget both + ابزارک and ویجت, and a file both فایل and پرونده, in one interface. + + It also reached for فرهنگستان coinages that correct Persian dictionaries + carry but shipped software does not use. درون‌ریزی and برون‌بری for import and + export appear in none of Immich, Jellyfin or Uptime Kuma, all of which build + import on وارد کردن; Immich, the closest of the three to this project, ships + وارد کردن پیکربندی for "Import configuration" and خروجی for "Export". + + Each entry is the rejected term and what to write instead. Adding one is how + a terminology decision is recorded, so it survives the next bulk retranslation. + The glossary in docs/i18n.md carries the same list in prose. */ +const REJECTED = { + 'fa.json': [ + ['درون‌ریزی', 'وارد کردن'], + ['برون‌بری', 'خروجی'], + ['ابزارک', 'ویجت'], + ['پرونده', 'فایل'], + ], +}; + +const widgetCatalogues = locale => { + const dir = path.join(DIR, '..', 'widgets'); + return fs + .readdirSync(dir, { withFileTypes: true }) + .filter(e => e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'i18n', locale))) + .map(e => [ + `widgets/${e.name}/i18n/${locale}`, + JSON.parse(fs.readFileSync(path.join(dir, e.name, 'i18n', locale), 'utf8')), + ]); +}; + +for (const [file, pairs] of Object.entries(REJECTED)) { + test(`${file} uses one agreed word per concept`, () => { + const catalogues = [[file, read(file)], ...widgetCatalogues(file)]; + const found = []; + for (const [name, catalogue] of catalogues) { + for (const [key, value] of flat(catalogue)) { + for (const [rejected, instead] of pairs) { + if (String(value).includes(rejected)) found.push(`${name} ${key}: ${rejected} -> ${instead}`); + } + } + } + assert.deepEqual(found, [], `A term the glossary rules out. See docs/i18n.md:\n ${found.join('\n ')}`); + }); +} diff --git a/ui/widgets/dns/i18n/fa.json b/ui/widgets/dns/i18n/fa.json index 28c31a30..b8651f3b 100644 --- a/ui/widgets/dns/i18n/fa.json +++ b/ui/widgets/dns/i18n/fa.json @@ -5,7 +5,7 @@ "dnsUrl.placeholder": "dns-host:80", "dnsUrl.hint": "نام کانتینر و پورت، یا یک نشانی کامل.", "dnsHref.label": "نشانی کلیک", - "dnsHref.hint": "جایی که با کلیک روی ابزارک باز می‌شود. برای غیرفعال کردن خالی بگذارید.", + "dnsHref.hint": "جایی که با کلیک روی ویجت باز می‌شود. برای غیرفعال کردن خالی بگذارید.", "dnsUser.label": "نام کاربری", "dnsPass.label": "گذرواژه", "dnsPiholePassword.label": "گذرواژه Pi-hole", diff --git a/ui/widgets/github/i18n/fa.json b/ui/widgets/github/i18n/fa.json index 9d7a0b78..cde42c60 100644 --- a/ui/widgets/github/i18n/fa.json +++ b/ui/widgets/github/i18n/fa.json @@ -8,9 +8,9 @@ "githubUser.label": "نام کاربری GitHub", "githubUser.placeholder": "octocat", "githubToken.label": "توکن دسترسی شخصی", - "githubToken.hint": "توکن کلاسیک با دسترسی repo و read:user (یا توکن دقیق با همان سطح خواندن). فقط همین ابزارک از آن استفاده می‌کند.", + "githubToken.hint": "توکن کلاسیک با دسترسی repo و read:user (یا توکن دقیق با همان سطح خواندن). فقط همین ویجت از آن استفاده می‌کند.", "githubHref.label": "نشانی کلیک", - "githubHref.hint": "جایی که با کلیک روی ابزارک باز می‌شود. برای غیرفعال کردن خالی بگذارید.", + "githubHref.hint": "جایی که با کلیک روی ویجت باز می‌شود. برای غیرفعال کردن خالی بگذارید.", "githubPrFilters.label": "پالایه‌های درخواست ادغام", "githubPrFilters.hint": "کدام درخواست‌های باز نمایش داده شوند. چند پالایه با هم ترکیب می‌شوند.", "githubPrFilters.opt.created": "ساخته‌شده", diff --git a/ui/widgets/system-summary/i18n/fa.json b/ui/widgets/system-summary/i18n/fa.json index 8c53f6d3..f73cc120 100644 --- a/ui/widgets/system-summary/i18n/fa.json +++ b/ui/widgets/system-summary/i18n/fa.json @@ -31,9 +31,9 @@ "slots.primary.placeholder": "/", "slots.secondary.label": "دومین مسیر اتصال", "slots.secondary.placeholder": "/mnt/data", - "slots.fsPrimary.label": "نخستین سامانهٔ پرونده", + "slots.fsPrimary.label": "نخستین سیستم فایل", "slots.fsPrimary.placeholder": "root", - "slots.fsSecondary.label": "دومین سامانهٔ پرونده", + "slots.fsSecondary.label": "دومین سیستم فایل", "slots.fsSecondary.placeholder": "media", "slots.sharePrimary.label": "نخستین اشتراک", "slots.sharePrimary.placeholder": "media",