fix(ui): harden menus for all locales

- LanguageSelector was still rendering the new SVG flag paths as text; use
  <Flag> there too and give every flag a fixed 24px slot (SVG/emoji align).
- Language dropdown w-48 -> w-72 with truncate + hover titles, so
  'Português (Brasil)' / 'Português (Portugal)' no longer clip.
- Settings language grid 1 column on phones / 2 on sm+.
- Header More menu w-48 -> w-64; header nav keeps its scroll, bottom nav keeps
  truncation.
- :lang(hi/th/ja/ko/zh) disables uppercase/tracking-wide so Devanagari, Thai
  and CJK menu labels are not letter-spaced.
- tsc, 101 tests, build and full-route smoke green.
This commit is contained in:
SkateSpots 2026-10-26 21:44:16 +07:00
parent 828684cf15
commit d53b8cd0cc
4 changed files with 52 additions and 18 deletions

View file

@ -2727,6 +2727,13 @@ similar are internal endpoints/hook names, not credits — leave them.)
`@fontsource/noto-sans-devanagari` (400/500/700) is **lazy-loaded** for `hi`
via `lib/localeFonts.ts` (same pattern as CJK) and the CSS stack includes
`'Noto Sans Devanagari'` before `system-ui`.
- **Menu hardening across all locales:** the language dropdown is `w-72` with
`truncate` + a fixed 24px flag slot (SVG and emoji align); the settings
language grid is 1 column on phones / 2 on `sm+`; the header **More** menu is
`w-64`; the header nav scrolls (`overflow-x-auto scrollbar-hide`) so long
labels never push the logo/controls; the bottom nav truncates its labels.
`:lang(hi/th/ja/ko/zh)` removes `uppercase`/`tracking-wide`, because
Devanagari/Thai/CJK labels look broken when letter-spaced.
- Translation sources were split by namespace into
`/tmp/opencode/i18n-src/batch1..7.json` (regenerate from `en.ts` if
re-translating). When adding new UI strings, add them to `en`/`es` **and all

View file

@ -79,7 +79,11 @@ function Flag({ flag, className }: { flag: string; className?: string }) {
/>
);
}
return <span className={cn('text-lg leading-none', className)}>{flag}</span>;
return (
<span className={cn('inline-flex items-center justify-center text-lg leading-none', className)}>
{flag}
</span>
);
}
/**
@ -124,11 +128,11 @@ export function LanguageToggle({ compact = false, inline = false, className, var
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
)}
>
<Flag flag={lang.flag} />
<div className="flex flex-col">
<span className="font-medium">{lang.nativeName}</span>
<span className="flex h-5 w-6 shrink-0 items-center justify-center"><Flag flag={lang.flag} /></span>
<div className="flex min-w-0 flex-col">
<span className="truncate font-medium" title={lang.nativeName}>{lang.nativeName}</span>
{lang.nativeName !== lang.name && (
<span className="text-xs opacity-70">{lang.name}</span>
<span className="truncate text-xs opacity-70" title={lang.name}>{lang.name}</span>
)}
</div>
{language === lang.code && <span className="ml-auto text-primary">✓</span>}
@ -166,7 +170,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuContent align="end" className="w-72">
<div
className="sticky top-0 z-10 -mx-1 -mt-1 mb-1 border-b bg-popover p-1"
onKeyDown={(e) => e.stopPropagation()}
@ -189,15 +193,15 @@ export function LanguageToggle({ compact = false, inline = false, className, var
language === lang.code && 'bg-primary/10 text-primary'
)}
>
<Flag flag={lang.flag} />
<div className="flex flex-col">
<span className="font-medium">{lang.nativeName}</span>
<span className="flex h-5 w-6 shrink-0 items-center justify-center"><Flag flag={lang.flag} /></span>
<div className="flex min-w-0 flex-col">
<span className="truncate font-medium" title={lang.nativeName}>{lang.nativeName}</span>
{lang.nativeName !== lang.name && (
<span className="text-xs text-muted-foreground">{lang.name}</span>
<span className="truncate text-xs text-muted-foreground" title={lang.name}>{lang.name}</span>
)}
</div>
{language === lang.code && (
<span className="ml-auto text-primary">✓</span>
<span className="ml-auto shrink-0 text-primary">✓</span>
)}
</DropdownMenuItem>
))}
@ -220,23 +224,25 @@ export function LanguageSelector() {
return (
<div className="space-y-3">
<label className="text-sm font-medium">{t('settings.selectLanguage')}</label>
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => setLanguage(lang.code)}
className={cn(
'flex items-center gap-3 p-4 rounded-xl border-2 transition-all',
'flex items-center gap-3 p-4 rounded-xl border-2 text-left transition-all',
language === lang.code
? 'border-primary bg-primary/10 shadow-lg shadow-primary/20'
: 'border-border hover:border-primary/50 hover:bg-muted/50'
)}
>
<span className="text-2xl">{lang.flag}</span>
<div className="text-left">
<div className="font-bold">{lang.nativeName}</div>
<span className="flex h-7 w-9 shrink-0 items-center justify-center">
<Flag flag={lang.flag} className="h-6 w-9" />
</span>
<div className="min-w-0 text-left">
<div className="truncate font-bold" title={lang.nativeName}>{lang.nativeName}</div>
{lang.nativeName !== lang.name && (
<div className="text-xs text-muted-foreground">{lang.name}</div>
<div className="truncate text-xs text-muted-foreground" title={lang.name}>{lang.name}</div>
)}
</div>
</button>

View file

@ -623,7 +623,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
{t('nav.more')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuContent align="end" className="w-64">
{/* Catalog items the user hasn't pinned to the top bar.
Auth-only items (stash/collections/contacts/wallet/messages)
only show when logged in. */}

View file

@ -914,3 +914,24 @@ select {
.ss-sheet-table td.ss-cell-name > input { max-width: 100%; }
.ss-sheet-table td.ss-cell-actions { justify-content: flex-end; padding-top: 0.35rem; }
}
/* ── Caseless scripts ─────────────────────────────────────────────────────
The nav/menu labels use `uppercase tracking-wide` for the Latin UI. Those
classes are no-ops or actively wrong for Devanagari, Thai and CJK: letter
spacing separates conjuncts and uppercase does nothing. Keep the labels
natural when the document language is one of those. */
:lang(hi) .uppercase,
:lang(th) .uppercase,
:lang(ja) .uppercase,
:lang(ko) .uppercase,
:lang(zh) .uppercase {
text-transform: none;
}
:lang(hi) .tracking-wide,
:lang(th) .tracking-wide,
:lang(ja) .tracking-wide,
:lang(ko) .tracking-wide,
:lang(zh) .tracking-wide {
letter-spacing: 0;
}