ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 50s
ci / ts / ts (packages + web) (push) Successful in 43s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 27s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m22s
- core: AuthSession keeps the access token in memory, refreshes it once for all parallel requests (single-flight) and retries them; refresh races between tabs are retried, reuse signs out; ApiError carries problem+json codes and field errors - web: TanStack Router with guards — guests go to /login, the bootstrap admin to /setup/admin, temporary passwords to /setup/password, admin pages by role - login, setup screens, Classic shell (top bar, role-based sidebar, empty player), settings: name, theme, language, password, devices - users admin: search, role filter, create with a one-time temporary password, reset, block, role change, delete as the policy allows; audit log for admins - ru/en strings for auth, admin and error codes; language and theme are saved in the profile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 lines
1.2 KiB
TypeScript
34 lines
1.2 KiB
TypeScript
import { unwrap } from '@pipu/core';
|
|
import { SUPPORTED_LOCALES, type Locale } from '@pipu/i18n';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { useAuthState, useSession } from '../app/session';
|
|
import { Segmented } from '../shared/ui/ui';
|
|
|
|
/** Переключатель языка. Для вошедшего пользователя язык сохраняется в профиле (PATCH /me). */
|
|
export function LanguageSwitch() {
|
|
const { t, i18n } = useTranslation();
|
|
const session = useSession();
|
|
const auth = useAuthState();
|
|
|
|
const change = async (l: Locale) => {
|
|
document.documentElement.lang = l;
|
|
await i18n.changeLanguage(l);
|
|
if (auth.status !== 'signed_in') return;
|
|
try {
|
|
session.setUser(unwrap(await session.client.PATCH('/api/v1/me', { body: { locale: l } })));
|
|
} catch {
|
|
// Язык уже переключён локально; сохранится при следующей смене.
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Segmented
|
|
label={t('language.label')}
|
|
value={(SUPPORTED_LOCALES as readonly string[]).includes(i18n.language) ? (i18n.language as Locale) : 'en'}
|
|
options={SUPPORTED_LOCALES.map((l) => ({ value: l, label: l.toUpperCase() }))}
|
|
onChange={(l) => void change(l)}
|
|
/>
|
|
);
|
|
}
|