mirror of
https://github.com/temetro/temetro.git
synced 2026-08-10 10:37:43 +00:00
d237504af9
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>
67 lines
2.0 KiB
TypeScript
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>
|
|
);
|
|
}
|