web/user: add language selection

closes #2041

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-01-01 15:03:27 +01:00
parent a6373ebb33
commit 0ef8edc9f1
9 changed files with 487 additions and 307 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,21 @@
import { CoreApi, SessionUser } from "@goauthentik/api";
import { i18n } from "@lingui/core";
import { DEFAULT_CONFIG } from "./Config";
let globalMePromise: Promise<SessionUser>;
export function me(): Promise<SessionUser> {
if (!globalMePromise) {
globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMeRetrieve().catch((ex) => {
globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMeRetrieve().then((user) => {
if (!user.user.settings || !("locale" in user.user.settings)) {
return user;
}
const locale = user.user.settings.locale;
if (locale && locale !== "") {
console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
i18n.activate(locale);
}
return user;
}).catch((ex) => {
const defaultUser: SessionUser = {
user: {
pk: -1,

View File

@ -1,3 +1,5 @@
import { UserSelf } from "@goauthentik/api";
import { me } from "../api/Users";
export enum UserDisplay {
@ -29,6 +31,7 @@ export interface UIConfig {
pagination: {
perPage: number;
};
locale: string;
}
export class DefaultUIConfig implements UIConfig {
@ -49,22 +52,25 @@ export class DefaultUIConfig implements UIConfig {
pagination = {
perPage: 20,
};
locale = "";
}
let globalUiConfig: Promise<UIConfig>;
export function getConfigForUser(user: UserSelf): UIConfig {
const settings = user.settings;
let config = new DefaultUIConfig();
if (!settings) {
return config;
}
config = Object.assign(new DefaultUIConfig(), settings);
return config;
}
export function uiConfig(): Promise<UIConfig> {
if (!globalUiConfig) {
globalUiConfig = me().then((user) => {
const settings = user.user.settings;
let config = new DefaultUIConfig();
if (!settings) {
return config;
}
if ("userInterface" in settings) {
config = Object.assign(new DefaultUIConfig(), settings.userInterface);
}
return config;
return getConfigForUser(user.user);
});
}
return globalUiConfig;

View File

@ -1,21 +1,51 @@
import { en, fr, tr } from "make-plural/plurals";
import { i18n } from "@lingui/core";
import { Messages, i18n } from "@lingui/core";
import { detect, fromNavigator, fromStorage, fromUrl } from "@lingui/detect-locale";
import { t } from "@lingui/macro";
import { messages as localeEN } from "../locales/en";
import { messages as localeFR_FR } from "../locales/fr_FR";
import { messages as localeDEBUG } from "../locales/pseudo-LOCALE";
import { messages as localeTR } from "../locales/tr";
i18n.loadLocaleData("en", { plurals: en });
i18n.loadLocaleData("debug", { plurals: en });
i18n.loadLocaleData("tr", { plurals: tr });
i18n.loadLocaleData("fr_FR", { plurals: fr });
i18n.load("en", localeEN);
i18n.load("tr", localeTR);
i18n.load("fr_FR", localeFR_FR);
i18n.load("debug", localeDEBUG);
export const LOCALES: {
code: string;
label: string;
// eslint-disable-next-line @typescript-eslint/ban-types
plurals: Function;
locale: Messages;
}[] = [
{
code: "en",
plurals: en,
label: t`English`,
locale: localeEN,
},
{
code: "debug",
plurals: en,
label: t`Debug`,
locale: localeDEBUG,
},
{
code: "fr_FR",
plurals: fr,
label: t`French`,
locale: localeFR_FR,
},
{
code: "tr",
plurals: tr,
label: t`Turkish`,
locale: localeTR,
},
];
LOCALES.forEach((locale) => {
i18n.loadLocaleData(locale.code, { plurals: locale.plurals });
i18n.load(locale.code, locale.locale);
});
const DEFAULT_FALLBACK = () => "en";

View File

@ -516,6 +516,10 @@ msgstr "Authorize URL"
msgid "Authorized application:"
msgstr "Authorized application:"
#: src/user/user-settings/details/UserDetailsForm.ts
msgid "Auto-detect (based on your browser)"
msgstr "Auto-detect (based on your browser)"
#: src/interfaces/UserInterface.ts
msgid "Avatar image"
msgstr "Avatar image"
@ -1317,6 +1321,10 @@ msgstr "Date Time"
msgid "Deactivate"
msgstr "Deactivate"
#: src/interfaces/locale.ts
msgid "Debug"
msgstr "Debug"
#: src/pages/flows/FlowForm.ts
msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
msgstr "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
@ -1715,6 +1723,10 @@ msgstr "Enabled"
msgid "Enabling this toggle will create a group named after the user, with the user as member."
msgstr "Enabling this toggle will create a group named after the user, with the user as member."
#: src/interfaces/locale.ts
msgid "English"
msgstr "English"
#: src/user/user-settings/mfa/MFADevicesPage.ts
msgid "Enroll"
msgstr "Enroll"
@ -2122,6 +2134,10 @@ msgstr "Forward auth (domain-level)"
msgid "Forward auth (single application)"
msgstr "Forward auth (single application)"
#: src/interfaces/locale.ts
msgid "French"
msgstr "French"
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
msgid "Friendly Name"
msgstr "Friendly Name"
@ -2742,6 +2758,10 @@ msgstr "Loading..."
msgid "Local"
msgstr "Local"
#: src/user/user-settings/details/UserDetailsForm.ts
msgid "Locale"
msgstr "Locale"
#: src/pages/stages/user_login/UserLoginStageForm.ts
msgid "Log the currently pending user in."
msgstr "Log the currently pending user in."
@ -5233,6 +5253,10 @@ msgstr "Transient"
msgid "Transports"
msgstr "Transports"
#: src/interfaces/locale.ts
msgid "Turkish"
msgstr "Turkish"
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
msgid "Twilio"
msgstr "Twilio"

View File

@ -520,6 +520,10 @@ msgstr "URL d'authorisation"
msgid "Authorized application:"
msgstr "Application autorisée :"
#: src/user/user-settings/details/UserDetailsForm.ts
msgid "Auto-detect (based on your browser)"
msgstr ""
#: src/interfaces/UserInterface.ts
msgid "Avatar image"
msgstr "Image d'avatar"
@ -1316,6 +1320,10 @@ msgstr "Date et heure"
msgid "Deactivate"
msgstr "Désactiver"
#: src/interfaces/locale.ts
msgid "Debug"
msgstr ""
#: src/pages/flows/FlowForm.ts
msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
msgstr "Détermine l'usage de ce flux. Par exemple, un flux d'authentification est la destination d'un visiteur d'authentik non authentifié."
@ -1702,6 +1710,10 @@ msgstr "Activé"
msgid "Enabling this toggle will create a group named after the user, with the user as member."
msgstr "Activer cette option va créer un groupe du même nom que l'utilisateur dont il sera membre."
#: src/interfaces/locale.ts
msgid "English"
msgstr ""
#: src/user/user-settings/mfa/MFADevicesPage.ts
msgid "Enroll"
msgstr ""
@ -2108,6 +2120,10 @@ msgstr ""
msgid "Forward auth (single application)"
msgstr "Transférer l'authentification (application unique)"
#: src/interfaces/locale.ts
msgid "French"
msgstr ""
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
msgid "Friendly Name"
msgstr "Nom amical"
@ -2722,6 +2738,10 @@ msgstr "Chargement en cours..."
msgid "Local"
msgstr "Local"
#: src/user/user-settings/details/UserDetailsForm.ts
msgid "Locale"
msgstr ""
#: src/pages/stages/user_login/UserLoginStageForm.ts
msgid "Log the currently pending user in."
msgstr "Ouvre la session de l'utilisateur courant."
@ -5175,6 +5195,10 @@ msgstr "Transitoire"
msgid "Transports"
msgstr "Transports"
#: src/interfaces/locale.ts
msgid "Turkish"
msgstr ""
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
msgid "Twilio"
msgstr ""

View File

@ -512,6 +512,10 @@ msgstr ""
msgid "Authorized application:"
msgstr ""
#: src/user/user-settings/details/UserDetailsForm.ts
msgid "Auto-detect (based on your browser)"
msgstr ""
#: src/interfaces/UserInterface.ts
msgid "Avatar image"
msgstr ""
@ -1311,6 +1315,10 @@ msgstr ""
msgid "Deactivate"
msgstr ""
#: src/interfaces/locale.ts
msgid "Debug"
msgstr ""
#: src/pages/flows/FlowForm.ts
msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
msgstr ""
@ -1707,6 +1715,10 @@ msgstr ""
msgid "Enabling this toggle will create a group named after the user, with the user as member."
msgstr ""
#: src/interfaces/locale.ts
msgid "English"
msgstr ""
#: src/user/user-settings/mfa/MFADevicesPage.ts
msgid "Enroll"
msgstr ""
@ -2114,6 +2126,10 @@ msgstr ""
msgid "Forward auth (single application)"
msgstr ""
#: src/interfaces/locale.ts
msgid "French"
msgstr ""
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
msgid "Friendly Name"
msgstr ""
@ -2732,6 +2748,10 @@ msgstr ""
msgid "Local"
msgstr ""
#: src/user/user-settings/details/UserDetailsForm.ts
msgid "Locale"
msgstr ""
#: src/pages/stages/user_login/UserLoginStageForm.ts
msgid "Log the currently pending user in."
msgstr ""
@ -5213,6 +5233,10 @@ msgstr ""
msgid "Transports"
msgstr ""
#: src/interfaces/locale.ts
msgid "Turkish"
msgstr ""
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
msgid "Twilio"
msgstr ""

View File

@ -514,6 +514,10 @@ msgstr "URL'yi yetkilendirme"
msgid "Authorized application:"
msgstr "Yetkili başvuru:"
#: src/user/user-settings/details/UserDetailsForm.ts
msgid "Auto-detect (based on your browser)"
msgstr ""
#: src/interfaces/UserInterface.ts
msgid "Avatar image"
msgstr "Avatar resmi"
@ -1304,6 +1308,10 @@ msgstr "Tarih Saati"
msgid "Deactivate"
msgstr "Devre dışı bırak"
#: src/interfaces/locale.ts
msgid "Debug"
msgstr ""
#: src/pages/flows/FlowForm.ts
msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
msgstr "Bu Akış'ın ne için kullanıldığına karar verir. Örneğin, kimliği doğrulanmamış bir kullanıcı authentik ziyaret ettiğinde kimlik doğrulama akışı yönlendirir."
@ -1682,6 +1690,10 @@ msgstr "Etkin"
msgid "Enabling this toggle will create a group named after the user, with the user as member."
msgstr "Bu geçiş özelliğini etkinleştirmek, kullanıcının adını taşıyan ve kullanıcının üye olduğu bir grup oluşturur."
#: src/interfaces/locale.ts
msgid "English"
msgstr ""
#: src/user/user-settings/mfa/MFADevicesPage.ts
msgid "Enroll"
msgstr "Kaydolun"
@ -2085,6 +2097,10 @@ msgstr "İleri kimlik doğrulama (alan düzeyi)"
msgid "Forward auth (single application)"
msgstr "İleri kimlik doğrulaması (tek uygulama)"
#: src/interfaces/locale.ts
msgid "French"
msgstr ""
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
msgid "Friendly Name"
msgstr "Dostça İsim"
@ -2696,6 +2712,10 @@ msgstr "Yükleniyor..."
msgid "Local"
msgstr "Yerel"
#: src/user/user-settings/details/UserDetailsForm.ts
msgid "Locale"
msgstr ""
#: src/pages/stages/user_login/UserLoginStageForm.ts
msgid "Log the currently pending user in."
msgstr "Şu anda bekleyen kullanıcıyı oturum açın."
@ -5132,6 +5152,10 @@ msgstr "Geçici"
msgid "Transports"
msgstr "Taşımacılık"
#: src/interfaces/locale.ts
msgid "Turkish"
msgstr ""
#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts
msgid "Twilio"
msgstr "Twilio"

View File

@ -1,3 +1,4 @@
import { i18n } from "@lingui/core";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
@ -9,19 +10,25 @@ import { CoreApi, UserSelf } from "@goauthentik/api";
import { DEFAULT_CONFIG, tenant } from "../../../api/Config";
import { me } from "../../../api/Users";
import { getConfigForUser, uiConfig } from "../../../common/config";
import "../../../elements/EmptyState";
import "../../../elements/forms/Form";
import "../../../elements/forms/FormElement";
import "../../../elements/forms/HorizontalFormElement";
import { ModelForm } from "../../../elements/forms/ModelForm";
import { LOCALES } from "../../../interfaces/locale";
@customElement("ak-user-details-form")
export class UserDetailsForm extends ModelForm<UserSelf, number> {
currentLocale?: string;
viewportCheck = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadInstance(pk: number): Promise<UserSelf> {
return me().then((user) => {
const config = getConfigForUser(user.user);
this.currentLocale = config.locale;
return user.user;
});
}
@ -31,6 +38,13 @@ export class UserDetailsForm extends ModelForm<UserSelf, number> {
}
send = (data: UserSelf): Promise<UserSelf> => {
const newConfig = getConfigForUser(data);
const newLocale = LOCALES.find((locale) => locale.code === newConfig.locale);
if (newLocale) {
i18n.activate(newLocale.code);
} else {
console.debug(`authentik/user: invalid locale: '${newConfig.locale}'`);
}
return new CoreApi(DEFAULT_CONFIG)
.coreUsersUpdateSelfUpdate({
userSelfRequest: data,
@ -44,61 +58,84 @@ export class UserDetailsForm extends ModelForm<UserSelf, number> {
if (!this.instance) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="username">
<input
type="text"
value="${ifDefined(this.instance?.username)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Name`} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${t`User's display name.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Email`} name="email">
<input
type="email"
value="${ifDefined(this.instance?.email)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
return html`${until(
uiConfig().then((config) => {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal
label=${t`Username`}
?required=${true}
name="username"
>
<input
type="text"
value="${ifDefined(this.instance?.username)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Name`} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${t`User's display name.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Email`} name="email">
<input
type="email"
value="${ifDefined(this.instance?.email)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Locale`} name="settings.locale">
<select class="pf-c-form-control">
<option value="" ?selected=${config.locale === ""}>
${t`Auto-detect (based on your browser)`}
</option>
${LOCALES.map((locale) => {
return html`<option
value=${locale.code}
?selected=${config.locale === locale.code}
>
${locale.label}
</option>`;
})}
</select>
</ak-form-element-horizontal>
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<button
@click=${(ev: Event) => {
return this.submit(ev);
}}
class="pf-c-button pf-m-primary"
>
${t`Save`}
</button>
${until(
tenant().then((tenant) => {
if (tenant.flowUnenrollment) {
return html`<a
class="pf-c-button pf-m-danger"
href="/if/flow/${tenant.flowUnenrollment}"
>
${t`Delete account`}
</a>`;
}
return html``;
}),
)}
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<button
@click=${(ev: Event) => {
return this.submit(ev);
}}
class="pf-c-button pf-m-primary"
>
${t`Save`}
</button>
${until(
tenant().then((tenant) => {
if (tenant.flowUnenrollment) {
return html`<a
class="pf-c-button pf-m-danger"
href="/if/flow/${tenant.flowUnenrollment}"
>
${t`Delete account`}
</a>`;
}
return html``;
}),
)}
</div>
</div>
</div>
</div>
</div>
</form>`;
</form>`;
}),
)}`;
}
}