Skip to content

Two call sites build a search placeholder as t(key) + '...' — the ellipsis is concatenated in code, so it stays ASCII in all ten locales and cannot be translated away #4375

Closed

Description

@yinlianghui

Found while executing #3878 (ellipsis convergence on U+2026), on branch claude/issue-3878-i18n-copy-trio. Out of that card's scope — the dispatch scoped ListView.tsx to the LIST_DEFAULT_TRANSLATIONS duplicate-copy case, and that case does not apply here (see below) — so recorded separately.

The two lines

packages/plugin-list/src/ListView.tsx:2829
    placeholder={t('list.search') + '...'}

packages/fields/src/widgets/LookupField.tsx:1085
    placeholder={t('common.search') + '...'}

In both, the translated value is the bare noun (list.search / common.search — 'Search' in en, '搜索' in zh, 'Pesquisar' in pt, …) and the ellipsis is a literal appended in code. Two consequences:

  1. It stays ASCII in every locale. [finding] en 包 32 个值用 ASCII "..." 结尾、113 个用 U+2026 "…" —— 同一屏上省略号两种字形,且九包各自跟着抄 #3878 converged all ten packs on the typographic ellipsis … (U+2026), per the consistency pass on objectstack#6015. Neither placeholder is a pack value, so both were untouched: the list search popover and the lookup picker render 搜索... / Pesquisar... / Search... with three ASCII full stops on screens where every other placeholder now ends in ….
  2. No locale can opt out of the ellipsis at all. Whether a trailing ellipsis belongs on a placeholder is a per-language typographic call, and this spelling takes it away from the packs entirely. ar gets a left-to-right ASCII run appended to a right-to-left string, which is the sharpest version of the problem.

How the second one was found — it costs a lap if you don't know it is there

LookupField.tsx:1085 was found the hard way, and that is worth recording. packages/fields/src/widgets/useFieldTranslation.ts carries 'table.search': 'Search...' in its no-provider defaults map, which #3878 converged to Search… along with the pack. Two tests in packages/fields/src/complex-widgets.test.tsx assert getByPlaceholderText('Search...') against a rendered LookupField, and it is entirely natural to read those as pinning that map's value — so they get "fixed" to Search… and go red, because the placeholder they actually pin comes from the concatenation on line 1085 and never moved.

So the two spellings are not interchangeable evidence: a Search... in a fields test may be pinning the defaults map (moves with the pack) or this concatenation (does not). #3878's branch carries a comment at that assertion naming line 1085 and this issue, so the next reader does not repeat the lap.

Why the #4294 duplicate-copy check did not catch either

LIST_DEFAULT_TRANSLATIONS in ListView.tsx is clean: it carries 'list.loading': 'Loading records…' and 'list.refreshing': 'Refreshing…', both already U+2026, and it holds no copy of any of the 35 keys #3878 converged. The defect here is a different shape — not a duplicated value, but a value assembled at the call site.

It is also invisible to every i18n gate for that reason. scripts/check-i18n-call-site-keys.mjs reads the t() call and its options object; a + '...' outside the call is not an inline defaultValue, so there is nothing for its byte-exact rule to compare. all-locales-key-parity and check-i18n-en-drift.mjs never see it either — the assembled string does not exist in any pack. #3878's new pin (packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts) scans pack values only and says so in its header.

Fix directions

A — give each placeholder its own pack key. table.search already means exactly this and is now 'Search…' in en, so both call sites can read it (or a new list.searchPlaceholder / lookup.searchPlaceholder if the two want to diverge later). Each pack then decides its own ellipsis, and the values come under the #3878 pin like everything else. Zero new keys if table.search is reused.

B — change the two literals to '…'. Two characters, fixes the glyph, leaves the concatenation and therefore leaves the ellipsis untranslatable and the RTL case unaddressed.

A is contract-first and is the one I would take; B is a band-aid on the visible half. Recording both rather than picking, since A may want a key-set decision.

Whichever lands must also move the two complex-widgets.test.tsx assertions (and any ListView equivalent) off Search... — they are pinned to the current, concatenated truth.

Anchor

Verified on origin/main at 2fea4d2fa. Related: #3878 (the convergence, and the … the rest of the screen now uses), #4294 (the duplicated-copy trap this is adjacent to but not an instance of).


Generated by Claude Code

Activity

  1. changed the title [-]ListView builds the list-search placeholder as `t('list.search') + '...'` — the ellipsis is concatenated in code, so it stays ASCII in all ten locales and cannot be translated away[/-] [+]Two call sites build a search placeholder as `t(key) + '...'` — the ellipsis is concatenated in code, so it stays ASCII in all ten locales and cannot be translated away[/+] on Aug 11, 2026
  2. self-assigned this
    on Aug 11, 2026
  3. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    CollaboratorAuthor

    CLAIM (pair) — PM seat session_017Qqyix2QcnpUC9XeYVDzx3, dispatching one dev agent for #4375 + #4376 (adjacent post-#3878 stragglers, shared locale-pack surface — one PR avoids two branches racing on the packs), branch claude/issue-4375-4376-i18n-stragglers, one PR with Fixes #4375 + Fixes #4376.

    Rulings riding on the dispatch (delegated authority, veto window open):

    #4377 (designer's own en/zh table) is NOT in this batch — its file is in app-shell, held by #4040-T5; it pools until that lands.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions