Skip to content

Commit 8a1a856

Browse files
committed
fix(web): RTL prose mono flow, structural catalog guard, detection hardening
1 parent fb764b3 commit 8a1a856

4 files changed

Lines changed: 55 additions & 4 deletions

File tree

clients/web/src/components/Header.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Route } from '../hooks/useRoute';
22
import type { ThemeMode } from '../hooks/useTheme';
33
import { useI18n } from '../hooks/useLocale';
4-
import { LOCALE_NAMES, SUPPORTED_LOCALES } from '../i18n';
5-
import type { Locale, MessageKey } from '../i18n';
4+
import { isLocale, LOCALE_NAMES, SUPPORTED_LOCALES } from '../i18n';
5+
import type { MessageKey } from '../i18n';
66
import { PoweredByFlux } from './PoweredByFlux';
77

88
interface HeaderProps {
@@ -82,7 +82,9 @@ export function Header({ route, onNavigate, themeMode, onToggleTheme }: HeaderPr
8282
<select
8383
className="lang-select"
8484
value={locale}
85-
onChange={(e) => setLocale(e.target.value as Locale)}
85+
onChange={(e) => {
86+
if (isLocale(e.target.value)) setLocale(e.target.value);
87+
}}
8688
aria-label={t('header_language_label')}
8789
>
8890
{SUPPORTED_LOCALES.map((l) => (

clients/web/src/hooks/useLocale.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ function initialLocaleFor(): Locale {
2424
} catch {
2525
// Ignore storage failures (private mode).
2626
}
27-
return detectLocale(navigator.languages ?? [navigator.language]);
27+
return detectLocale(
28+
(navigator.languages ?? [navigator.language]).filter((l): l is string => typeof l === 'string'),
29+
);
2830
}
2931

3032
/**

clients/web/src/i18n/catalogs.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,38 @@ describe('no untranslated stubs', () => {
8383
});
8484
}
8585
});
86+
87+
/** Multiset of tag names (e.g. <b>, <em>) referenced across a message's text. */
88+
function tagCounts(msg: Message): Record<string, number> {
89+
const text = typeof msg === 'string' ? msg : Object.values(msg).join(' ');
90+
const counts: Record<string, number> = {};
91+
for (const m of text.matchAll(/<(\w+)[\s/>]/g)) {
92+
counts[m[1]!] = (counts[m[1]!] ?? 0) + 1;
93+
}
94+
return counts;
95+
}
96+
97+
/** Set of {placeholder} names referenced across a message's text. */
98+
function paramNames(msg: Message): Set<string> {
99+
const text = typeof msg === 'string' ? msg : Object.values(msg).join(' ');
100+
const names = new Set<string>();
101+
for (const m of text.matchAll(/\{(\w+)\}/g)) {
102+
names.add(m[1]!);
103+
}
104+
return names;
105+
}
106+
107+
describe('structural parity with en', () => {
108+
for (const locale of SUPPORTED_LOCALES) {
109+
if (locale === 'en') continue;
110+
it(`${locale}: tags and params match en per key`, () => {
111+
const catalog = ALL_CATALOGS[locale];
112+
for (const key of Object.keys(en) as (keyof typeof en)[]) {
113+
expect(tagCounts(catalog[key]), `${locale}.${key} tag mismatch`).toEqual(tagCounts(en[key]));
114+
expect([...paramNames(catalog[key])].sort(), `${locale}.${key} param mismatch`).toEqual(
115+
[...paramNames(en[key])].sort(),
116+
);
117+
}
118+
});
119+
}
120+
});

clients/web/src/styles.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,18 @@ footer .wrap {
11421142
direction: ltr;
11431143
unicode-bidi: isolate;
11441144
}
1145+
/* …but these carry translated prose that merely uses mono typography —
1146+
let them flow RTL; plaintext keeps embedded LTR fragments (MTU 1420,
1147+
live/bundled) legible. */
1148+
[dir='rtl'] footer .mono,
1149+
[dir='rtl'] .qr-cap.mono,
1150+
[dir='rtl'] .mh-style.mono,
1151+
[dir='rtl'] .mh-conf-tag.mono,
1152+
[dir='rtl'] .src-pill,
1153+
[dir='rtl'] .mh-route.mono {
1154+
direction: rtl;
1155+
unicode-bidi: plaintext;
1156+
}
11451157
/* Decorative arrows point "forward"; mirror them when the flow mirrors. */
11461158
[dir='rtl'] .mh-arrow,
11471159
[dir='rtl'] .mh-route-arrow {

0 commit comments

Comments
 (0)