feat(user.prefered_locale): save user's locale + invited have same locale as inviters

- OIDC JIT define the locale only at user creation, user can so change his preference later
    - invited users will inherit inviter's locale
    - email will use prefered_locale
    - login to a new browser will use prefered_locale
This commit is contained in:
Edouard Vanbelle
2026-06-03 14:26:45 +02:00
parent 854f1d3a07
commit 7db27af7a6
14 changed files with 342 additions and 26 deletions
+35
View File
@@ -5,6 +5,8 @@
* It loads translations from the server and provides functions to translate keys.
*/
import { getCsrfHeaders } from './csrf.js';
// Supported locales (languages that have locale files on the server)
// Keep in sync with AVAILABLE_LOCALES in core/languageSelector.js
const supportedLocales = ['en', 'es', 'zh', 'zh-TW', 'fa', 'fr', 'de', 'pt', 'nl', 'it', 'hi', 'ar', 'ru', 'ja', 'ko', 'pl'];
@@ -141,6 +143,14 @@ async function setLocale(locale) {
// Save locale preference
localStorage.setItem('oxicloud-locale', locale);
// PR C: also persist server-side via PATCH /api/auth/me/profile
// so the same choice is honoured by transactional emails and
// survives across devices. Fire-and-forget — anonymous callers
// (login page, magic-link landing) will 401 and that's fine; a
// network blip just leaves the row at its previous value, which
// localStorage already reflects on this device.
_persistLocaleToServer(locale);
// Trigger an event for components to update
window.dispatchEvent(new CustomEvent('localeChanged', { detail: { locale } }));
@@ -150,6 +160,31 @@ async function setLocale(locale) {
return true;
}
/**
* Fire-and-forget POST of the new locale to the server. Called from
* `setLocale`; failures are logged but never block the UI flip.
*
* The server side rejects requests from anonymous callers (no session
* cookie) with 401 — that's expected on the login / magic-link pages
* where i18n.js runs before the user is authenticated, so we treat any
* non-2xx as "skip, the next save will reconcile".
*
* @param {string} locale
*/
function _persistLocaleToServer(locale) {
fetch('/api/auth/me/profile', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
...getCsrfHeaders()
},
credentials: 'same-origin',
body: JSON.stringify({ preferred_locale: locale })
}).catch((err) => {
console.debug('locale: server persistence skipped:', err?.message ?? err);
});
}
/**
* Initialize the i18n system
* @returns {Promise<void>}
+1
View File
@@ -165,6 +165,7 @@
* @property {string} [given_name] First/given name; set at OIDC JIT or via PATCH /api/auth/me/profile (PR 24)
* @property {string} [family_name] Last/family name; set at OIDC JIT or via PATCH /api/auth/me/profile (PR 24)
* @property {string} [email_verified_at] ISO 8601 timestamp of the first proof-of-email-control (PR 23). Omitted when unverified.
* @property {string} [preferred_locale] User-chosen locale code (e.g. `"fr"`, `"zh-TW"`); omitted when unset. Round-trips via PATCH /api/auth/me/profile.
*/
/**