From d237504af9725fe47ff382f261b63a640b72163a Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Thu, 2 Jul 2026 23:03:13 +0300 Subject: [PATCH] frontend: add Somali, Arabic (RTL) & German languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three UI locales (so/ar/de) with full ~1,660-key translations alongside en/fr, selectable in Settings → Profile. Arabic gets full right-to-left support: - config.ts registers the locales and exports a `dirFor` helper; an inline script in layout.tsx sets before first paint (no RTL flash), and i18n-provider keeps them in sync on language change. - ~160 physical direction utilities converted to logical (ms/me/ps/pe/ start/end/text-start/text-end); directional chevrons/arrows get rtl:rotate-180; chat-bubble align variants fixed to logical. - IBM Plex Sans Arabic appended to the sans/heading font stacks for per-character Arabic fallback. - Language persists to the backend user_settings and re-applies on sign-in so it roams across devices (localStorage stays the offline source of truth). - New scripts/check-locales.mjs (npm run check-locales) enforces key/placeholder parity and Arabic CLDR plural completeness. Bump to 0.3.0. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 19 + backend/package.json | 2 +- frontend/app/globals.css | 6 +- frontend/app/layout.tsx | 34 +- .../components/activity/activity-view.tsx | 4 +- frontend/components/analysis/trend-card.tsx | 2 +- .../appointments/appointments-view.tsx | 4 +- .../appointments/calendar-dialog.tsx | 6 +- frontend/components/auth/app-auth-guard.tsx | 11 + .../components/chat/action-preview-card.tsx | 2 +- frontend/components/chat/ai-setup-notice.tsx | 2 +- .../chat/batch-action-preview-card.tsx | 2 +- .../components/chat/chat-history-panel.tsx | 6 +- frontend/components/chat/chat-input.tsx | 4 +- frontend/components/chat/chat-panel.tsx | 2 +- .../components/chat/import-preview-card.tsx | 4 +- .../components/chat/inventory-list-card.tsx | 2 +- frontend/components/chat/patient-cards.tsx | 6 +- .../components/chat/patient-form-dialog.tsx | 2 +- frontend/components/chat/record-list-card.tsx | 6 +- frontend/components/i18n-provider.tsx | 19 +- .../invoices/invoice-detail-sheet.tsx | 4 +- .../components/invoices/invoices-view.tsx | 6 +- .../components/lab/lab-integration-card.tsx | 8 +- frontend/components/lab/lab-view.tsx | 10 +- frontend/components/login-form.tsx | 2 +- frontend/components/meetings/meeting-room.tsx | 4 +- .../components/meetings/meetings-view.tsx | 6 +- .../meetings/schedule-meeting-dialog.tsx | 2 +- .../messages/appointment-detail-dialog.tsx | 2 +- .../components/messages/messages-view.tsx | 22 +- frontend/components/notes/notes-view.tsx | 2 +- .../components/patients/patient-detail.tsx | 4 +- .../components/patients/patient-files.tsx | 2 +- .../components/patients/patients-view.tsx | 6 +- .../pharmacy/inventory-detail-dialog.tsx | 4 +- .../components/pharmacy/inventory-view.tsx | 6 +- .../components/pharmacy/pharmacy-view.tsx | 4 +- frontend/components/portal/portal-kiosk.tsx | 8 +- .../prescriptions/add-prescription-dialog.tsx | 8 +- .../prescription-detail-sheet.tsx | 2 +- .../prescriptions/prescriptions-view.tsx | 4 +- .../settings/employee-detail-dialog.tsx | 2 +- .../components/settings/settings-billing.tsx | 2 +- .../settings/settings-care-team.tsx | 4 +- .../settings/settings-integrations.tsx | 2 +- .../components/settings/settings-parts.tsx | 2 +- .../settings/settings-preferences.tsx | 6 +- .../sidebar-02/mobile-sidebar-trigger.tsx | 2 +- frontend/components/sidebar-02/nav-main.tsx | 2 +- .../sidebar-02/nav-notifications.tsx | 2 +- frontend/components/sidebar-02/nav-user.tsx | 6 +- frontend/components/ui/accordion.tsx | 2 +- frontend/components/ui/bubble.tsx | 4 +- frontend/components/ui/list-pagination.tsx | 4 +- frontend/components/ui/menu.tsx | 2 +- frontend/components/ui/pagination.tsx | 4 +- frontend/components/ui/select.tsx | 2 +- frontend/components/ui/sidebar.tsx | 2 +- frontend/components/update-banner.tsx | 4 +- frontend/lib/i18n/config.ts | 18 +- frontend/lib/i18n/locales/ar/translation.json | 2046 +++++++++++++++++ frontend/lib/i18n/locales/de/translation.json | 2026 ++++++++++++++++ frontend/lib/i18n/locales/en/translation.json | 3 + frontend/lib/i18n/locales/fr/translation.json | 3 + frontend/lib/i18n/locales/so/translation.json | 2026 ++++++++++++++++ frontend/lib/language.ts | 43 + frontend/package.json | 5 +- frontend/scripts/check-locales.mjs | 119 + package.json | 2 +- 70 files changed, 6483 insertions(+), 123 deletions(-) create mode 100644 frontend/lib/i18n/locales/ar/translation.json create mode 100644 frontend/lib/i18n/locales/de/translation.json create mode 100644 frontend/lib/i18n/locales/so/translation.json create mode 100644 frontend/lib/language.ts create mode 100644 frontend/scripts/check-locales.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4165ae9..71bdeb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ for how releases are cut and published. ## [Unreleased] +## [0.3.0] — 2026-07-02 + +### Added +- **Three new interface languages** — Somali (`so`), Arabic (`ar`) and German + (`de`) join English and French, selectable in Settings → Profile → Language. + Each locale carries a full translation of the ~1,660 UI strings, with native + names shown in the selector (Soomaali, العربية, Deutsch). +- **Right-to-left (RTL) support** — selecting Arabic sets `dir="rtl"` on the + document (applied before first paint via an inline script, so no flash), flips + physical spacing/alignment to logical CSS utilities, mirrors directional icons, + and loads an Arabic-capable typeface (IBM Plex Sans Arabic) appended to the + font stack for per-character fallback. +- **Language roams across devices** — the chosen language is persisted to the + per-user `user_settings` preferences and re-applied on sign-in, with + localStorage remaining the offline source of truth. +- **`frontend/scripts/check-locales.mjs`** (+ `npm run check-locales`) — a parity + check that fails on missing/extra keys or `{{placeholder}}` mismatches across + locales and warns when Arabic count-keys lack the full CLDR plural forms. + ## [0.2.5] — 2026-07-01 ### Fixed diff --git a/backend/package.json b/backend/package.json index 6f5af17..019ade5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "temetro-backend", - "version": "0.2.5", + "version": "0.3.0", "private": true, "type": "module", "description": "temetro backend — Express + Postgres API with Better Auth (email/password, organizations) and org-scoped patient records.", diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 4cecfdd..078ad95 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -7,9 +7,11 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-sans); + /* Append the Arabic face so Arabic codepoints fall through per-character even + in LTR locales; Latin text still renders in Inter. See app/layout.tsx. */ + --font-sans: var(--font-sans), var(--font-arabic); --font-mono: var(--font-mono); - --font-heading: var(--font-heading); + --font-heading: var(--font-heading), var(--font-arabic); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 6b533a1..7e3e0e8 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { Geist_Mono, Inter } from "next/font/google"; +import { Geist_Mono, IBM_Plex_Sans_Arabic, Inter } from "next/font/google"; import "./globals.css"; import { cn } from "@/lib/utils"; import { ThemeProvider } from "@/components/theme-provider"; @@ -10,6 +10,27 @@ import { ToastProvider } from "@/components/ui/toast"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); const interHeading = Inter({ subsets: ["latin"], variable: "--font-heading" }); const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono" }); +// Arabic-capable fallback: Inter has no Arabic glyphs, so we append this to the +// sans/heading stacks (see globals.css) for per-character fallback in every +// locale, and rely on it fully when dir="rtl". Not a variable font — pin weights. +const plexArabic = IBM_Plex_Sans_Arabic({ + subsets: ["arabic"], + weight: ["400", "500", "600", "700"], + variable: "--font-arabic", +}); + +// Runs before first paint: mirror lib/i18n/config.ts `dirFor` so an Arabic user +// gets dir="rtl" immediately instead of a flash of LTR. Detection order matches +// i18next-browser-languagedetector (localStorage key "i18nextLng", then the +// browser language). suppressHydrationWarning on ignores the attr diff. +const setInitialDir = ` +(function(){try{ + var l=localStorage.getItem("i18nextLng")||navigator.language||"en"; + var e=document.documentElement; + e.lang=l; + e.dir=l.indexOf("ar")===0?"rtl":"ltr"; +}catch(_){}})(); +`; export const metadata: Metadata = { title: "temetro — AI assistant for clinicians", @@ -33,17 +54,22 @@ export default function RootLayout({ inter.variable, interHeading.variable, geistMono.variable, + plexArabic.variable, "font-sans" )} > {/* suppressHydrationWarning: next-themes sets the theme class on - before hydration, and browser extensions (e.g. ColorZilla's - cz-shortcut-listen) mutate . Only ignores attribute diffs on - those elements, not their children. */} + before hydration, the dir script below sets lang/dir, and browser + extensions (e.g. ColorZilla's cz-shortcut-listen) mutate . Only + ignores attribute diffs on those elements, not their children. */} +