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:
parent
828684cf15
commit
d53b8cd0cc
4 changed files with 52 additions and 18 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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. */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue