feat(i18n): make language a user profile attribute (register/profile/login)
All checks were successful
Build and Push / build (push) Successful in 1m47s
All checks were successful
Build and Push / build (push) Successful in 1m47s
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface Props {
|
||||
@@ -10,6 +10,8 @@ interface Props {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
/** Current ZITADEL preferredLanguage; "" if never set. */
|
||||
language: string;
|
||||
};
|
||||
/**
|
||||
* Personal-account flag. Drives a small hint about how the ZITADEL
|
||||
@@ -43,10 +45,15 @@ interface Props {
|
||||
*/
|
||||
export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) {
|
||||
const t = useTranslations("settingsProfile");
|
||||
const locale = useLocale();
|
||||
const { update } = useSession();
|
||||
const [form, setForm] = useState({
|
||||
firstName: initial.firstName,
|
||||
lastName: initial.lastName,
|
||||
// Fall back to the current UI locale when the profile has no stored
|
||||
// preference yet (older accounts), so the selector shows something
|
||||
// sensible rather than blank.
|
||||
language: initial.language || locale,
|
||||
});
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -67,6 +74,7 @@ export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) {
|
||||
body: JSON.stringify({
|
||||
firstName: form.firstName.trim(),
|
||||
lastName: form.lastName.trim(),
|
||||
language: form.language,
|
||||
}),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
@@ -79,15 +87,15 @@ export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) {
|
||||
// to session.user.name. No re-login needed.
|
||||
await update({ name: data.displayName });
|
||||
setSavedFlash(true);
|
||||
// Force a full reload so EVERY server-rendered component picks
|
||||
// up the new session cookie immediately — router.refresh() only
|
||||
// re-runs the current route's server components, leaving the
|
||||
// nav-shell (rendered higher in the tree) and other cached
|
||||
// segments showing the old name until the user navigates.
|
||||
// The 800ms delay lets the "Saved" flash render briefly before
|
||||
// the page reloads, so the user gets visible feedback.
|
||||
// If the language changed, land the user on the new locale (a
|
||||
// full navigation so every server-rendered surface re-renders in
|
||||
// the new language). Otherwise just reload so the new name
|
||||
// propagates. The 800ms delay lets the "Saved" flash show first.
|
||||
const localeChanged = form.language && form.language !== locale;
|
||||
const target = localeChanged ? localePath(form.language) : null;
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
if (target) window.location.assign(target);
|
||||
else window.location.reload();
|
||||
}, 800);
|
||||
} catch (e: any) {
|
||||
setError(e?.message ?? String(e));
|
||||
@@ -132,6 +140,20 @@ export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) {
|
||||
className="w-full px-3 py-2 rounded-md bg-surface-2 border border-border text-sm text-text-muted cursor-not-allowed"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("languageLabel")} hint={t("languageHint")}>
|
||||
<select
|
||||
value={form.language}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, language: e.target.value }))
|
||||
}
|
||||
className="w-full px-3 py-2 rounded-md bg-surface-2 border border-border focus:border-accent focus:outline-none text-sm"
|
||||
>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
</select>
|
||||
</Field>
|
||||
{/* Personal vs company hint. Personals get the
|
||||
"this won't change your invoice name" warning since their
|
||||
ZITADEL name and their invoice identity are intentionally
|
||||
@@ -163,6 +185,15 @@ export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
// Build the as-needed-prefixed path for a target locale from the
|
||||
// current URL (default locale `de` is unprefixed). Client-only — uses
|
||||
// window; called from the save handler.
|
||||
function localePath(lang: string): string {
|
||||
const p =
|
||||
window.location.pathname.replace(/^\/(de|fr|it|en)(?=\/|$)/, "") || "/";
|
||||
return lang === "de" ? p : `/${lang}${p === "/" ? "" : p}`;
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
required,
|
||||
|
||||
Reference in New Issue
Block a user