bug-builtin-filter-names-not-localized Built-in saved-filter names render in English even when the UI locale is `zh` — the SavedFilterDropdown (and every other consumer) ships the English literal from the registry
Type: bug · Parent: Epic 6 (where the bug was DISCOVERED) · Surfaces: every UI that lists built-in saved filters — confirmed in the issues-list SavedFilterDropdown (/issues), referenced by the reports ReportScopeCombobox and the dashboard DataSourceField saved-filter picker · Status: open · Reported by: Yue.
When the UI locale is zh, the dropdown chrome around saved filters IS translated correctly (默认 / 我的筛选器 / 项目筛选器 / 查找筛选器…), but the actual filter ROW names remain English (My open issues / Reported by me / All issues / Open issues / Done issues / Created recently / Updated recently / Resolved recently). The screenshot Yue attached shows the issues-list dropdown with a 查找筛选器… placeholder + 默认 group header + English row names — evidence that the i18n thread reaches the chrome but stops at the rows. The "Built-in" / 内置 tag on the right column is localised, so the chrome / row split is visually jarring.
Repro. Sign in as zhuyue@motir.co / !QAZ1qaz, set the UI language to Chinese (or any non-English locale; messages/zh.json is the shipped second locale). Open the moooon / motir project → /issues, click the saved-filter dropdown trigger (the "Saved" / 已保存 button). Observe: the 默认 group header is in Chinese, the search placeholder is 查找筛选器…, but every row in the group is in English. Same shape in the Distribution / Created-vs-Resolved report-landing scope picker when a saved filter is chosen, and in the dashboard Add-widget modal's Data source picker (Saved filter mode).
Root cause (HIGH confidence — the source comment names it). lib/savedFilters/builtins.ts:33 documents the design intent verbatim:
/** English display name (the 6.2.3 UI threads i18n over the slug). */
The registry then hard-codes the name field as the English literal for each of the eight built-ins. The intended pattern was: server ships the slug (stable, locale-independent), and every UI consumer threads t(...) over the slug. That thread was never wired. Instead, the mapper at lib/mappers/savedFilterMappers.ts:39 reads:
return { id: builtinFilterId(def.slug), name: def.name, builtin: true };
…copying the English def.name straight onto the DTO name field. The UI then renders builtin.name directly — see app/(authed)/issues/_components/SavedFilterDropdown.tsx:480 (label={builtin.name}). The slug never reaches the client; the DTO carries the English text. So t('savedFilters.builtinNames.<slug>') is what the comment expected, but no such key exists in messages/{en,zh}.json and no consumer calls it.
Impact. Eight strings (the built-in names) leak English into every non-English locale, across at least three surfaces (issues-list dropdown, report scope, dashboard data source). It is the most visible i18n hole because the dropdown's chrome is fully localised — the row names stand out as the only untranslated text.
Fix shapes (decide at fix time — option 1 is the durable shape the comment asked for):
- Client-side i18n via the slug (recommended; matches the design comment). Add
slugtoBuiltinFilterSummaryDto(alongsidename, or in place of it). Add asavedFilters.builtinNamesblock tomessages/en.jsonandmessages/zh.jsonkeyed by slug (my-open-issues / reported-by-me / all-issues / open-issues / done-issues / created-recently / updated-recently / resolved-recently). Every UI consumer replacesbuiltin.namewitht(savedFilters.builtinNames.${builtin.slug}). The registry's Englishnamebecomes the canonical FALLBACK (used only by tools / tests that have notin scope), not the user-facing string. This is exactly what thebuiltins.ts:33comment described. - Server-side localisation in the mapper. Pass the request locale to
toBuiltinFilterSummaryDto; resolve the localized name via a server-side message table; ship the localized string inname. Works, but it (a) bakes locale into the DTO (a row read returns a different shape per locale), (b) still leaves the registry as the English fallback for tools, and (c) is the opposite shape from the existing client-side i18n (everything else in the app usesnext-intlon the client). Reject in favour of option 1. - Inline
tin each consumer over the slug. Like option 1 but without adding asavedFilters.builtinNamesnamespace — each consumer has its own mapping table. Reject: violates DRY and the three consumers will drift.
Recommended: option 1. Single message namespace, slug-keyed, one DTO change, one t(...) call shape repeated across consumers. The registry stays as the source of slugs + the English fallback.
Test gap that let it ship. Existing tests likely verify the dropdown opens / lists the built-ins / applies a chosen filter — but assert against the English name directly, so the assertion would pass against any locale. The fix MUST add a test that:
- Renders
SavedFilterDropdown(or any of the three consumers) under azhnext-intlNextIntlClientProviderwrapper. - Asserts the rendered row labels are the Chinese strings, not the English ones.
- Asserts ALL eight built-ins translate (no half-translated regression).
- Re-renders under
enand asserts the labels match the registrynameliterals (the en path stays green).
Acceptance criteria
- In the
zhlocale, every built-in filter row inSavedFilterDropdownrenders its Chinese label (8 strings: 我的待办 / 我报告的 / 全部事项 / 待办 / 已完成 / 最近创建 / 最近更新 / 最近已解决 — exact wording to be confirmed at fix time against the existing translation style inmessages/zh.json; the Chinese above is illustrative). - Same in every other consumer that lists built-in saved filters — the report scope picker (
ReportScopeCombobox) and the dashboard data-source picker (DataSourceField). Audit the call sites; each one must threadt(...)over the slug, not overbuiltin.name. - The
enlocale renders the SAME English strings it does today (no regression of the green path). - The
BuiltinFilterSummaryDtocarriesslug(the locale-independent identifier). Thenamefield MAY remain as the canonical English fallback for callers without atin scope (tools, CLI, server-side logs), or MAY be dropped entirely if no such caller exists — decide at fix time. - A render test asserts the Chinese labels under
zhand the English labels underen, for all eight built-ins, in at least one consumer surface (SavedFilterDropdownrecommended; the others share the same DTO +t(...)shape so one consumer's coverage is enough). - The
lib/savedFilters/builtins.ts:33comment is UPDATED to reflect the now-wired pattern ("English fallback; the UI threadst('savedFilters.builtinNames.<slug>')for the localised label") so the next reader of the registry sees the contract honoured rather than the broken promise. - AA contrast preserved; no service / DTO transport change beyond adding
slugtoBuiltinFilterSummaryDto; no route change.
Context refs
lib/savedFilters/builtins.ts:29-112— theBUILTIN_FILTERSregistry; the comment at line 33 documents the design intent (and names the gap). The eight slugs are the i18n keys.lib/mappers/savedFilterMappers.ts:39—toBuiltinFilterSummaryDto; wheredef.nameleaks into the DTO unmediated.lib/dto/savedFilters.ts—BuiltinFilterSummaryDtodefinition;slugaddition lands here.app/(authed)/issues/_components/SavedFilterDropdown.tsx:480— the issues-list dropdown that renderslabel={builtin.name}directly (the most visible consumer; the screenshot surface).app/(authed)/reports/_components/ReportScopeCombobox.tsx:54—label: f.namefor the report scope picker; second consumer.app/(authed)/dashboard/_components/DataSourceField.tsx— the dashboard data-source picker; third consumer (Saved filter mode).messages/en.json:1893-...+messages/zh.json:1985-...— thesavedFiltersi18n block; thesavedFilters.builtinNames.<slug>keys land here.bug-backlog-zh-sprint-translated-as-chongci(sibling i18n bug) — the sprint/冲刺 mistranslation precedent; same shape as this one (i18n string mistakenly literal-shipped through to azhsurface).motir-core/CLAUDE.md— i18n strings live inmessages/{en,zh}.json; service mappers carry locale-independent shapes (the DTO contract).
Refactor signal (rule of three watch). This is the SECOND i18n leak we have logged where a server-side English literal ships untranslated to a zh-localised UI surface — first was bug-backlog-zh-sprint-translated-as-chongci (the sprint label, fixed in PR #502), now this one (the eight built-in filter names). Both share the same underlying defect: a server-shipped human-readable string that should have been a locale-independent KEY (slug / id) for the client to thread t(...) over. If a third surface ships this way (e.g. workflow status labels, priority labels, or kind labels), the fix is no longer "thread t(...) per consumer" — it is a lint rule (or a typed LocalizedString DTO field) that forbids server-shipped user-facing English from crossing the boundary.