2.5.22 Bug — `/issues` filter popover renders untranslated English strings (FILTER / KIND / STATUS / "Find by ID or title…" / "Clear filters" / kind + status labels) (finding #61)
Estimate: 12m · Depends on: 2.5.4
Regression introduced by 2.5.4 (logged as finding #61). The /issues toolbar is translated — the [Filter] trigger renders t('issueViews.filter'), the view switcher renders t('issueViews.viewTree') / viewList, and the 新建工作项 button renders t('issueViews.newIssue') — but the popover the [Filter] button opens is rendered with hardcoded English string literals. With the locale set to zh, the popover shows the toolbar in Chinese (新建工作项 · 树形 · Filter correctly localized at the trigger) but the popover body is English: FILTER, × Clear filters, Find by ID or title…, KIND, Epic / Story / Task / Bug / Sub-task, STATUS, To Do / Blocked / In Progress / In Review / Done (see the attached screenshot for the user-visible state).
Root cause — collapsed i18n axis. app/(authed)/issues/_components/IssueFilterBar.tsx (shipped by 2.5.4) was authored before the surrounding surface was threaded through next-intl. The file has no useTranslations('issueViews') import; every label in the popover is a JSX literal. Adjacent surfaces in the same folder DO use next-intl — app/(authed)/issues/page.tsx calls getTranslations('issueViews'), [key]/edit/_components/EditIssueForm.tsx calls useTranslations('issueViews' | 'common' | 'errors')`, etc. — so the catalog scaffold and the consumption pattern are already in place, and the only outlier on this page is the filter popover.
Fix (mechanical, no new pattern). Thread IssueFilterBar through useTranslations('issueViews') and replace every English literal with a key, REUSING already-shipped catalog keys wherever they exist (do NOT add a duplicate). Concretely:
- Section + control labels — add to the
issueViewsnamespace inmessages/en.jsonANDmessages/zh.json(and any other locale catalogs present):filterPopoverHeading(FILTER/筛选),filterClearAll(Clear filters/清除筛选),filterFindPlaceholder(Find by ID or title…/按 ID 或标题查找…),filterKindHeading(KIND/类型),filterStatusHeading(STATUS/状态),filterAssigneeHeading(ASSIGNEE/负责人),filterUnassigned(Unassigned/未分配). Match the catalog's existing all-caps-for-section-label convention (or the locale's natural shape — Chinese is already non-caps). - Kind labels (Epic · Story · Task · Bug · Sub-task) — REUSE the existing catalog:
messages/zh.jsonalready hasissueTypes.{epic,story,task,bug,subtask}(篇章·故事·任务·缺陷·子任务). Replace any direct read ofISSUE_TYPE_META[*].label(or other hardcoded display string) in the popover witht('issueTypes.' + kind)(or the existing namespace's helper). DO NOT add a second copy of these labels. - Status labels (To Do · Blocked · In Progress · In Review · Done) — these are the workflow status names rendered for each project status row. The catalog already carries the default-status labels (
defaultStatus.toDo/blocked/inProgress/inReview/done=待办·受阻·进行中·审核中·已完成); statuses themselves come fromWorkflowStatusDto.label(per-project, user-editable). For default statuses, rendert('defaultStatus.<key>')via the same lookup the StatusPicker uses (e.g.STATUS_LABEL_FALLBACK[s.key]if one exists); for custom statuses, render the storeds.labelverbatim (user content, not translatable — same rule the rest of the app already follows for custom statuses, see the StatusPicker pattern). - Assignee names + the "Unassigned" bucket — names are user content (no translation); the
Unassignedbucket label MUST go throught('issueViews.filterUnassigned').
Scope guard. IssueFilterBar.tsx is the ONLY file with hardcoded strings on this surface (verified by grep -n against the popover's exact literals). Do NOT introduce a generic "translate every issues string" sweep — that's out of scope and would collide with surfaces still in flight. Do NOT alter the filter behaviour, the URL serialization, the filterRef/optimistic-state machinery from 2.5.17, or the popover layout — this is a pure string-threading change.
Acceptance criteria
- With
locale=zh, the/issuesfilter popover renders every label in Chinese: section headings (筛选·类型·状态·负责人), the清除筛选action, the按 ID 或标题查找…placeholder, every Kind row (篇章/故事/任务/缺陷/子任务), every default-status row (待办/受阻/进行中/审核中/已完成), and the未分配bucket; custom (user-named) statuses still render their storedlabelverbatim. - With
locale=en, the popover renders identically to today's English (no copy regression). IssueFilterBar.tsximportsuseTranslationsfromnext-intland contains NO English literal string in JSX or attribute values for these surfaces;grep -nE "(Clear filters|Find by ID|KIND|STATUS|FILTER)" app/\(authed\)/issues/_components/IssueFilterBar.tsxreturns zero lines (the literals live only inmessages/*.json).- Kind labels read from the existing
issueTypes.*keys; default-status labels read from the existingdefaultStatus.*keys — no duplicated catalog entries. - Behaviour is unchanged: the URL serialization, multi-select check marks, the
filterRef/optimistic state from 2.5.17, the active-count badge, theClear filtersdisabled state, and the focus-trap /Escclose behaviour all work as before. The 2.5.4 + 2.5.17 regression tests still pass. - New regression test in
tests/components/issue-filter-bar.test.tsx: render the popover under bothenandzhNextIntlClientProviderwrappers and assert the section headings +Clear filtersaction render the locale's strings (not the English literal) — fails onmain(catches a future regression that re-introduces hardcoded English). - tsc / eslint / prettier clean;
next buildcompiles;pnpm test+ the existingtests/e2e/issue-list-flow.spec.tspass (the E2E grabs the EnglishClear filtersbutton by name and must continue to work under defaulten).
Context refs
- Finding #61 in
motir-meta/prodect_plan/PRODECT_FINDINGS.md(full root-cause + screenshot of the broken popover) app/(authed)/issues/_components/IssueFilterBar.tsx— the file to thread; ~L256 (Clear filters) · ~L271 (Find by ID or title…placeholder) · the section headings + Kind / Status / AssigneeOptionRowlabelsmessages/en.json+messages/zh.jsonissueViewsnamespace — where the new keys land; existingissueTypes.{epic,story,task,bug,subtask}+defaultStatus.{toDo,blocked,inProgress,inReview,done}to reuse (do NOT duplicate)- The
next-intlconsumption pattern already used on this surface:app/(authed)/issues/page.tsx(getTranslations) +app/(authed)/issues/[key]/edit/_components/EditIssueForm.tsx(useTranslations) — mirror the client-component variant components/issues/StatusPicker.tsx(or equivalent) — the precedent for "translate default status keys, render custom statuslabelverbatim" already in use elsewheretests/components/issue-filter-bar.test.tsx— where the locale-rendering regression test lands;tests/e2e/issue-list-flow.spec.tsL206 (Clear filtersbutton name lookup — must still pass under defaulten)