feat(i18n): localize plugin catalog names and shell preset desc

Catalog entry names (15 tab + 4 viewer) and the dsh-desktop shell preset
desc were raw hardcoded zh strings rendered straight into the add-plugin
modals and the title-bar scheme dropdown. Mirror the existing desc
pattern exactly: PluginEntry.name / ShellPreset.desc widen to the
i18n-friendly string-or-() =>-string union, the catalogs carry lazy
t('pluginXxxName') / t('presetDshDesktopDesc') lookups, and the
add-plugin modal (render, search filter, aria-labels) plus the scheme
SelectMenu resolve them like every other copy. The plain-string arm of
the union stays as the raw-name fallback, and t() itself never yields
empty text (missing keys render the key), so a dict gap can never blank
a name.

20 new dictionary keys added to zh/en (locales.ts), ja (locales-ja.ts),
and all 18 third-language dictionaries — tests/locales.spec.ts enforces
key-set equality against zh for every shipped dict (precedent: 9cc642b),
so the 'leave other locales-* untouched' shortcut would fail the hard
parity gate; real translations are provided instead.

Spec follow-ups (behavior change: names/descs now render through t()):
- plugin-list.spec.ts: name non-empty assertion resolves the union.
- add-plugin-modal.spec.tsx: sentinel/office name assertions resolve
  from the catalog (en locale in tests) instead of pinning zh literals.
- shell-presets.spec.ts: desc non-empty assertion resolves the union.
This commit is contained in:
Menghuan1918
2026-09-03 19:59:00 +08:00
parent 9e1a034527
commit fa301da29e
29 changed files with 510 additions and 57 deletions
+14 -8
View File
@@ -56,8 +56,9 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
const needle = query.trim().toLowerCase()
const matches = (entry: PluginEntry): boolean => {
if (needle === '') return true
const name = typeof entry.name === 'function' ? entry.name() : entry.name
const description = typeof entry.description === 'function' ? entry.description() : entry.description
return entry.name.toLowerCase().includes(needle)
return name.toLowerCase().includes(needle)
|| entry.id.toLowerCase().includes(needle)
|| description.toLowerCase().includes(needle)
}
@@ -93,8 +94,12 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
window.open(entry.url, '_blank', 'noopener')
}
/** One catalog row (extracted so the group render stays flat). */
const renderEntry = (entry: PluginEntry): ReactNode => (
/** One catalog row (extracted so the group render stays flat). The name
* resolves like the description (string or () => string) so it follows
* the active locale; a plain-string entry keeps its raw name. */
const renderEntry = (entry: PluginEntry): ReactNode => {
const name = typeof entry.name === 'function' ? entry.name() : entry.name
return (
<div key={entry.id} className={css.pluginEntry}>
<div className={css.pluginEntryHead}>
{/* The name is a BUTTON on the same window.open path as the
@@ -104,16 +109,16 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
<button
type="button"
className={css.pluginName}
aria-label={`${t('openPlugin')}: ${entry.name}`}
aria-label={`${t('openPlugin')}: ${name}`}
onClick={() => { jump(entry) }}
>
{entry.name}
{name}
</button>
<span className={css.pluginEntryActions}>
<button
type="button"
className={css.pluginJumpBtn}
aria-label={`${t('openPlugin')}: ${entry.name}`}
aria-label={`${t('openPlugin')}: ${name}`}
onClick={() => { jump(entry) }}
>
{t('openPlugin')}
@@ -121,7 +126,7 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
<button
type="button"
className={css.pluginCopyBtn}
aria-label={`${t('copyInstall')}: ${entry.name}`}
aria-label={`${t('copyInstall')}: ${name}`}
onClick={() => { copy(entry) }}
>
{copiedId === entry.id ? t('copied') : t('copy')}
@@ -133,7 +138,8 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
</div>
<code className={css.pluginInstall}>{entry.install}</code>
</div>
)
)
}
return (
<div className={css.pluginList}>