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:
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { getCsrfHeaders } from '../core/csrf.js';
|
||||
import { i18n } from '../core/i18n.js';
|
||||
import { updateStorageUsageDisplay } from './main.js';
|
||||
import { app } from './state.js';
|
||||
import { ui } from './ui.js';
|
||||
@@ -12,6 +13,37 @@ import { updateUserMenuData } from './userMenu.js';
|
||||
* @import {User} from '../core/types.js'
|
||||
*/
|
||||
|
||||
/**
|
||||
* Apply the server's `preferred_locale` to this browser if it differs
|
||||
* from the currently-active one.
|
||||
*
|
||||
* The page initially renders in whichever locale `i18n.initI18n()`
|
||||
* picked from localStorage / Accept-Language. After `/api/auth/me`
|
||||
* returns we know the user's persisted choice; if this is a fresh
|
||||
* browser (no `oxicloud-locale` in localStorage) or the local copy
|
||||
* drifted (user changed their preference elsewhere), switching here
|
||||
* is what makes "sign in on phone, see UI in the language I picked on
|
||||
* my laptop" work.
|
||||
*
|
||||
* Safeguards:
|
||||
* - `null` / `undefined` server value means "no preference stored" →
|
||||
* leave the browser-picked locale alone.
|
||||
* - When the server value matches the active locale we skip
|
||||
* `setLocale` entirely to avoid a no-op `translatePage()` flash.
|
||||
* - `setLocale` itself writes the new value back via PATCH; that's
|
||||
* benign here (server already agrees) and avoids special-casing
|
||||
* the call site.
|
||||
*
|
||||
* @param {string|undefined|null} serverLocale
|
||||
*/
|
||||
function _syncPreferredLocale(serverLocale) {
|
||||
if (!serverLocale) return;
|
||||
if (i18n.getCurrentLocale && i18n.getCurrentLocale() === serverLocale) return;
|
||||
i18n.setLocale(serverLocale).catch((err) => {
|
||||
console.debug('locale: sync from server failed:', err?.message ?? err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<User | null>}
|
||||
@@ -40,6 +72,11 @@ async function refreshUserData() {
|
||||
|
||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
|
||||
app.isExternalUser = !!userData.is_external;
|
||||
// PR C: sync the server-stored preferred_locale to this device.
|
||||
// Triggered on every `/api/auth/me` fetch, but `_syncPreferredLocale`
|
||||
// short-circuits when the active locale already matches so we
|
||||
// don't trigger an unnecessary translatePage() pass.
|
||||
_syncPreferredLocale(userData.preferred_locale);
|
||||
updateStorageUsageDisplay(userData);
|
||||
return userData;
|
||||
} catch (error) {
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user