Files
temetro/frontend/components/update-banner.tsx
T
Khalid Abdi d237504af9 frontend: add Somali, Arabic (RTL) & German languages
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
  <head> script in layout.tsx sets <html dir/lang> 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 <noreply@anthropic.com>
2026-07-02 23:03:13 +03:00

67 lines
2.0 KiB
TypeScript

"use client";
import Link from "next/link";
import { X } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getVersionInfo } from "@/lib/version";
const DISMISS_KEY = "temetro:update-dismissed";
// A small, dismissible notice shown when a newer temetro release exists. Updating
// is optional — dismissing remembers the version so we don't nag again until the
// next release. Errors are swallowed (offline / private deployments).
export function UpdateBanner() {
const { t } = useTranslation();
const [latest, setLatest] = useState<string | null>(null);
useEffect(() => {
let active = true;
getVersionInfo()
.then((info) => {
if (!active || !info.updateAvailable || !info.latest) return;
if (localStorage.getItem(DISMISS_KEY) === info.latest) return;
setLatest(info.latest);
})
.catch(() => {
/* offline or no update server — stay silent */
});
return () => {
active = false;
};
}, []);
if (!latest) return null;
const dismiss = () => {
localStorage.setItem(DISMISS_KEY, latest);
setLatest(null);
};
return (
<div className="fixed end-4 bottom-4 z-50 flex max-w-sm items-start gap-3 rounded-2xl border border-border bg-card px-4 py-3 shadow-lg">
<div className="space-y-1.5">
<p className="text-sm text-foreground">
{t("settings.version.banner", { version: latest })}
</p>
<Link
className="text-sm font-medium text-primary underline-offset-4 hover:underline"
href="/settings?tab=version"
onClick={dismiss}
>
{t("settings.version.bannerUpdate")}
</Link>
</div>
<button
aria-label={t("settings.version.bannerDismiss")}
className="-me-1 shrink-0 rounded-lg p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
onClick={dismiss}
type="button"
>
<X className="size-4" />
</button>
</div>
);
}