Compare commits
1 Commits
server-con
...
safari-loc
Author | SHA1 | Date | |
---|---|---|---|
9deed34479 |
@ -4,8 +4,12 @@ import {
|
|||||||
EventMiddleware,
|
EventMiddleware,
|
||||||
LoggingMiddleware,
|
LoggingMiddleware,
|
||||||
} from "@goauthentik/common/api/middleware";
|
} from "@goauthentik/common/api/middleware";
|
||||||
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
|
import { VERSION } from "@goauthentik/common/constants";
|
||||||
import { globalAK } from "@goauthentik/common/global";
|
import { globalAK } from "@goauthentik/common/global";
|
||||||
|
import {
|
||||||
|
EVENT_LOCALE_REQUEST,
|
||||||
|
LocaleContextEventDetail,
|
||||||
|
} from "@goauthentik/elements/ak-locale-context/events.js";
|
||||||
|
|
||||||
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
|
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
|
||||||
|
|
||||||
@ -44,7 +48,7 @@ export function brandSetLocale(brand: CurrentBrand) {
|
|||||||
}
|
}
|
||||||
console.debug("authentik/locale: setting locale from brand default");
|
console.debug("authentik/locale: setting locale from brand default");
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
|
||||||
composed: true,
|
composed: true,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: { locale: brand.defaultLocale },
|
detail: { locale: brand.defaultLocale },
|
||||||
|
@ -14,8 +14,6 @@ export const EVENT_FLOW_INSPECTOR_TOGGLE = "ak-flow-inspector-toggle";
|
|||||||
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
|
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
|
||||||
export const EVENT_WS_MESSAGE = "ak-ws-message";
|
export const EVENT_WS_MESSAGE = "ak-ws-message";
|
||||||
export const EVENT_FLOW_ADVANCE = "ak-flow-advance";
|
export const EVENT_FLOW_ADVANCE = "ak-flow-advance";
|
||||||
export const EVENT_LOCALE_CHANGE = "ak-locale-change";
|
|
||||||
export const EVENT_LOCALE_REQUEST = "ak-locale-request";
|
|
||||||
export const EVENT_REQUEST_POST = "ak-request-post";
|
export const EVENT_REQUEST_POST = "ak-request-post";
|
||||||
export const EVENT_MESSAGE = "ak-message";
|
export const EVENT_MESSAGE = "ak-message";
|
||||||
export const EVENT_THEME_CHANGE = "ak-theme-change";
|
export const EVENT_THEME_CHANGE = "ak-theme-change";
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
|
||||||
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
|
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
|
||||||
|
import {
|
||||||
|
EVENT_LOCALE_REQUEST,
|
||||||
|
LocaleContextEventDetail,
|
||||||
|
} from "@goauthentik/elements/ak-locale-context/events.js";
|
||||||
|
|
||||||
import { CoreApi, SessionUser } from "@goauthentik/api";
|
import { CoreApi, SessionUser } from "@goauthentik/api";
|
||||||
|
|
||||||
@ -57,7 +60,7 @@ export async function me(): Promise<SessionUser> {
|
|||||||
console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
|
console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
|
||||||
composed: true,
|
composed: true,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: { locale },
|
detail: { locale },
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
|
||||||
import { customEvent } from "@goauthentik/elements/utils/customEvents";
|
|
||||||
|
|
||||||
import { localized, msg } from "@lit/localize";
|
import { localized, msg } from "@lit/localize";
|
||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
|
|
||||||
import "./ak-locale-context";
|
import "./ak-locale-context";
|
||||||
|
import { EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Elements / Shell / Locale Context",
|
title: "Elements / Shell / Locale Context",
|
||||||
@ -37,10 +35,18 @@ export const InFrench = () =>
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export const SwitchingBackAndForth = () => {
|
export const SwitchingBackAndForth = () => {
|
||||||
let lang = "en";
|
let languageCode = "en";
|
||||||
|
|
||||||
window.setInterval(() => {
|
window.setInterval(() => {
|
||||||
lang = lang === "en" ? "fr" : "en";
|
languageCode = languageCode === "en" ? "fr" : "en";
|
||||||
window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale: lang }));
|
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
|
||||||
|
composed: true,
|
||||||
|
bubbles: true,
|
||||||
|
detail: { locale: languageCode },
|
||||||
|
}),
|
||||||
|
);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return html`<div style="background: #fff; padding: 4em">
|
return html`<div style="background: #fff; padding: 4em">
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { customEvent } from "@goauthentik/elements/utils/customEvents";
|
|
||||||
|
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import { WithBrandConfig } from "../Interface/brandProvider";
|
import { WithBrandConfig } from "../Interface/brandProvider";
|
||||||
import { initializeLocalization } from "./configureLocale";
|
import { initializeLocalization } from "./configureLocale.js";
|
||||||
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
import type { GetLocale, SetLocale } from "./configureLocale.js";
|
||||||
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
|
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js";
|
||||||
|
import { DEFAULT_LOCALE, autoDetectLanguage, findLocaleDefinition } from "./helpers.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component to manage your locale settings.
|
* A component to manage your locale settings.
|
||||||
*
|
*
|
||||||
* ## Details
|
* @remarks
|
||||||
*
|
*
|
||||||
* This component exists to take a locale setting from several different places, find the
|
* This component exists to take a locale setting from several different places, find the
|
||||||
* appropriate locale file in our catalog of locales, and set the lit-localization context
|
* appropriate locale file in our catalog of locales, and set the lit-localization context
|
||||||
@ -25,70 +24,98 @@ import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helper
|
|||||||
*/
|
*/
|
||||||
@customElement("ak-locale-context")
|
@customElement("ak-locale-context")
|
||||||
export class LocaleContext extends WithBrandConfig(AKElement) {
|
export class LocaleContext extends WithBrandConfig(AKElement) {
|
||||||
/// @attribute The text representation of the current locale */
|
protected static singleton: LocaleContext | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text representation of the current locale
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ attribute: true, type: String })
|
@property({ attribute: true, type: String })
|
||||||
locale = DEFAULT_LOCALE;
|
public locale = DEFAULT_LOCALE;
|
||||||
|
|
||||||
/// @attribute The URL parameter to look for (if any)
|
/**
|
||||||
|
* The URL parameter to look for (if any)
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
@property({ attribute: true, type: String })
|
@property({ attribute: true, type: String })
|
||||||
param = "locale";
|
public param = "locale";
|
||||||
|
|
||||||
getLocale: LocaleGetter;
|
protected readonly getLocale: GetLocale;
|
||||||
|
protected readonly setLocale: SetLocale;
|
||||||
setLocale: LocaleSetter;
|
|
||||||
|
|
||||||
constructor(code = DEFAULT_LOCALE) {
|
constructor(code = DEFAULT_LOCALE) {
|
||||||
super();
|
super();
|
||||||
this.notifyApplication = this.notifyApplication.bind(this);
|
|
||||||
this.updateLocaleHandler = this.updateLocaleHandler.bind(this);
|
if (LocaleContext.singleton) {
|
||||||
try {
|
throw new Error(`Developer error: Must have only one locale context per session`);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocaleContext.singleton = this;
|
||||||
|
|
||||||
const [getLocale, setLocale] = initializeLocalization();
|
const [getLocale, setLocale] = initializeLocalization();
|
||||||
|
|
||||||
this.getLocale = getLocale;
|
this.getLocale = getLocale;
|
||||||
this.setLocale = setLocale;
|
this.setLocale = setLocale;
|
||||||
this.setLocale(code).then(() => {
|
|
||||||
window.setTimeout(this.notifyApplication, 0);
|
this.setLocale(code).then(this.#notifyApplication);
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Developer error: Must have only one locale context per session: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
this.#updateLocale();
|
||||||
this.updateLocale();
|
|
||||||
window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener);
|
window.addEventListener(EVENT_LOCALE_REQUEST, this.#localeUpdateListener as EventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener);
|
LocaleContext.singleton = null;
|
||||||
|
|
||||||
|
window.removeEventListener(
|
||||||
|
EVENT_LOCALE_REQUEST,
|
||||||
|
this.#localeUpdateListener as EventListener,
|
||||||
|
);
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocaleHandler(ev: CustomEvent<{ locale: string }>) {
|
#localeUpdateListener = (ev: CustomEvent<LocaleContextEventDetail>) => {
|
||||||
console.debug("authentik/locale: Locale update request received.");
|
console.debug("authentik/locale: Locale update request received.");
|
||||||
this.updateLocale(ev.detail.locale);
|
this.#updateLocale(ev.detail.locale);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
#updateLocale(requestedLanguageCode?: string) {
|
||||||
|
const localeRequest = autoDetectLanguage(requestedLanguageCode, this.brand?.defaultLocale);
|
||||||
|
|
||||||
|
const locale = findLocaleDefinition(localeRequest);
|
||||||
|
|
||||||
updateLocale(requestedLocale: string | undefined = undefined) {
|
|
||||||
const localeRequest = autoDetectLanguage(requestedLocale, this.brand?.defaultLocale);
|
|
||||||
const locale = getBestMatchLocale(localeRequest);
|
|
||||||
if (!locale) {
|
if (!locale) {
|
||||||
console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
|
console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
locale.locale().then(() => {
|
|
||||||
console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`);
|
return locale.fetch().then(() => {
|
||||||
this.setLocale(locale.code).then(() => {
|
console.debug(
|
||||||
window.setTimeout(this.notifyApplication, 0);
|
`authentik/locale: Setting Locale to ${locale.formatLabel()} (${locale.languageCode})`,
|
||||||
});
|
);
|
||||||
|
|
||||||
|
this.setLocale(locale.languageCode).then(this.#notifyApplication);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyApplication() {
|
#notifyFrameID = -1;
|
||||||
// You will almost never have cause to catch this event. Lit's own `@localized()` decorator
|
|
||||||
// works just fine for almost every use case.
|
#notifyApplication = () => {
|
||||||
this.dispatchEvent(customEvent(EVENT_LOCALE_CHANGE));
|
cancelAnimationFrame(this.#notifyFrameID);
|
||||||
}
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// You will almost never have cause to catch this event.
|
||||||
|
// Lit's own `@localized()` decorator works just fine for almost every use case.
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_LOCALE_CHANGE, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`<slot></slot>`;
|
return html`<slot></slot>`;
|
||||||
|
@ -1,39 +1,44 @@
|
|||||||
import { configureLocalization } from "@lit/localize";
|
import { configureLocalization } from "@lit/localize";
|
||||||
|
|
||||||
import { sourceLocale, targetLocales } from "../../locale-codes";
|
import { sourceLocale, targetLocales } from "../../locale-codes.js";
|
||||||
import { getBestMatchLocale } from "./helpers";
|
import { findLocaleDefinition } from "./helpers.js";
|
||||||
|
|
||||||
type LocaleGetter = ReturnType<typeof configureLocalization>["getLocale"];
|
export type ConfigureLocalizationResult = ReturnType<typeof configureLocalization>;
|
||||||
type LocaleSetter = ReturnType<typeof configureLocalization>["setLocale"];
|
|
||||||
|
|
||||||
// Internal use only.
|
export type GetLocale = ConfigureLocalizationResult["getLocale"];
|
||||||
//
|
export type SetLocale = ConfigureLocalizationResult["setLocale"];
|
||||||
// This is where the lit-localization module is initialized with our loader, which associates our
|
|
||||||
// collection of locales with its getter and setter functions.
|
|
||||||
|
|
||||||
let getLocale: LocaleGetter | undefined = undefined;
|
export type LocaleState = [GetLocale, SetLocale];
|
||||||
let setLocale: LocaleSetter | undefined = undefined;
|
|
||||||
|
|
||||||
export function initializeLocalization(): [LocaleGetter, LocaleSetter] {
|
let cachedLocaleState: LocaleState | undefined = undefined;
|
||||||
if (getLocale && setLocale) {
|
|
||||||
return [getLocale, setLocale];
|
|
||||||
}
|
|
||||||
|
|
||||||
({ getLocale, setLocale } = configureLocalization({
|
/**
|
||||||
|
* This is where the lit-localization module is initialized with our loader,
|
||||||
|
* which associates our collection of locales with its getter and setter functions.
|
||||||
|
*
|
||||||
|
* @returns A tuple of getter and setter functions.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function initializeLocalization(): LocaleState {
|
||||||
|
if (cachedLocaleState) return cachedLocaleState;
|
||||||
|
|
||||||
|
const { getLocale, setLocale } = configureLocalization({
|
||||||
sourceLocale,
|
sourceLocale,
|
||||||
targetLocales,
|
targetLocales,
|
||||||
loadLocale: async (locale: string) => {
|
loadLocale: (languageCode) => {
|
||||||
const localeDef = getBestMatchLocale(locale);
|
const localeDef = findLocaleDefinition(languageCode);
|
||||||
if (!localeDef) {
|
|
||||||
console.warn(`Unrecognized locale: ${localeDef}`);
|
|
||||||
return Promise.reject("");
|
|
||||||
}
|
|
||||||
return localeDef.locale();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [getLocale, setLocale];
|
if (!localeDef) {
|
||||||
|
throw new Error(`Unrecognized locale: ${localeDef}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return localeDef.fetch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cachedLocaleState = [getLocale, setLocale];
|
||||||
|
|
||||||
|
return cachedLocaleState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default initializeLocalization;
|
export default initializeLocalization;
|
||||||
export type { LocaleGetter, LocaleSetter };
|
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import * as _enLocale from "@goauthentik/locales/en";
|
import * as EnglishLocaleModule from "@goauthentik/locales/en";
|
||||||
|
|
||||||
import type { LocaleModule } from "@lit/localize";
|
import type { LocaleModule } from "@lit/localize";
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
|
|
||||||
import { AkLocale, LocaleRow } from "./types";
|
import { AKLocaleDefinition, LocaleRow } from "./types.js";
|
||||||
|
|
||||||
export const DEFAULT_FALLBACK = "en";
|
/**
|
||||||
|
* The default ISO 639-1 language code.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_LANGUAGE_CODE = "en";
|
||||||
|
|
||||||
const enLocale: LocaleModule = _enLocale;
|
/**
|
||||||
|
* The default English locale module.
|
||||||
export { enLocale };
|
*/
|
||||||
|
export const DefaultLocaleModule: LocaleModule = EnglishLocaleModule;
|
||||||
|
|
||||||
// NOTE: This table cannot be made any shorter, despite all the repetition of syntax. Bundlers look
|
// NOTE: This table cannot be made any shorter, despite all the repetition of syntax. Bundlers look
|
||||||
// for the `await import` string as a *string target* for doing alias substitution, so putting
|
// for the `await import` string as a *string target* for doing alias substitution, so putting
|
||||||
@ -35,34 +39,44 @@ export { enLocale };
|
|||||||
// - Text Label
|
// - Text Label
|
||||||
// - Locale loader.
|
// - Locale loader.
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const debug: LocaleRow = [
|
const debug: LocaleRow = [
|
||||||
"pseudo-LOCALE", /^pseudo/i, () => msg("Pseudolocale (for testing)"), async () => await import("@goauthentik/locales/pseudo-LOCALE"),
|
"pseudo-LOCALE",
|
||||||
|
/^pseudo/i,
|
||||||
|
() => msg("Pseudolocale (for testing)"),
|
||||||
|
() => import("@goauthentik/locales/pseudo-LOCALE"),
|
||||||
];
|
];
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const LOCALE_TABLE: LocaleRow[] = [
|
const LOCALE_TABLE: readonly LocaleRow[] = [
|
||||||
["de", /^de([_-]|$)/i, () => msg("German"), async () => await import("@goauthentik/locales/de")],
|
// English loaded when the application is first instantiated.
|
||||||
["en", /^en([_-]|$)/i, () => msg("English"), async () => await import("@goauthentik/locales/en")],
|
["en", /^en([_-]|$)/i, () => msg("English"), () => Promise.resolve(DefaultLocaleModule)],
|
||||||
["es", /^es([_-]|$)/i, () => msg("Spanish"), async () => await import("@goauthentik/locales/es")],
|
["de", /^de([_-]|$)/i, () => msg("German"), () => import("@goauthentik/locales/de")],
|
||||||
["fr", /^fr([_-]|$)/i, () => msg("French"), async () => await import("@goauthentik/locales/fr")],
|
["es", /^es([_-]|$)/i, () => msg("Spanish"), () => import("@goauthentik/locales/es")],
|
||||||
["it", /^it([_-]|$)/i, () => msg("Italian"), async () => await import("@goauthentik/locales/it")],
|
["fr", /^fr([_-]|$)/i, () => msg("French"), () => import("@goauthentik/locales/fr")],
|
||||||
["ko", /^ko([_-]|$)/i, () => msg("Korean"), async () => await import("@goauthentik/locales/ko")],
|
["it", /^it([_-]|$)/i, () => msg("Italian"), () => import("@goauthentik/locales/it")],
|
||||||
["nl", /^nl([_-]|$)/i, () => msg("Dutch"), async () => await import("@goauthentik/locales/nl")],
|
["ko", /^ko([_-]|$)/i, () => msg("Korean"), () => import("@goauthentik/locales/ko")],
|
||||||
["pl", /^pl([_-]|$)/i, () => msg("Polish"), async () => await import("@goauthentik/locales/pl")],
|
["nl", /^nl([_-]|$)/i, () => msg("Dutch"), () => import("@goauthentik/locales/nl")],
|
||||||
["ru", /^ru([_-]|$)/i, () => msg("Russian"), async () => await import("@goauthentik/locales/ru")],
|
["pl", /^pl([_-]|$)/i, () => msg("Polish"), () => import("@goauthentik/locales/pl")],
|
||||||
["tr", /^tr([_-]|$)/i, () => msg("Turkish"), async () => await import("@goauthentik/locales/tr")],
|
["ru", /^ru([_-]|$)/i, () => msg("Russian"), () => import("@goauthentik/locales/ru")],
|
||||||
["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), async () => await import("@goauthentik/locales/zh_TW")],
|
["tr", /^tr([_-]|$)/i, () => msg("Turkish"), () => import("@goauthentik/locales/tr")],
|
||||||
["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), async () => await import("@goauthentik/locales/zh-Hans")],
|
["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), () => import("@goauthentik/locales/zh_TW")],
|
||||||
["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), async () => await import("@goauthentik/locales/zh-Hant")],
|
["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), () => import("@goauthentik/locales/zh-Hans")],
|
||||||
debug
|
["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), () => import("@goauthentik/locales/zh-Hant")],
|
||||||
|
debug,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const LOCALES: AkLocale[] = LOCALE_TABLE.map(([code, match, label, locale]) => ({
|
/**
|
||||||
code,
|
* Available locales, identified by their ISO 639-1 language code.
|
||||||
match,
|
*/
|
||||||
label,
|
export const AKLocalDefinitions: readonly AKLocaleDefinition[] = LOCALE_TABLE.map(
|
||||||
locale,
|
([languageCode, pattern, formatLabel, fetch]) => {
|
||||||
}));
|
return {
|
||||||
|
languageCode,
|
||||||
|
pattern,
|
||||||
|
formatLabel,
|
||||||
|
fetch,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default LOCALES;
|
export default AKLocalDefinitions;
|
||||||
|
6
web/src/elements/ak-locale-context/events.ts
Normal file
6
web/src/elements/ak-locale-context/events.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const EVENT_LOCALE_REQUEST = "ak-locale-request";
|
||||||
|
export const EVENT_LOCALE_CHANGE = "ak-locale-change";
|
||||||
|
|
||||||
|
export interface LocaleContextEventDetail {
|
||||||
|
locale: string;
|
||||||
|
}
|
@ -1,59 +1,80 @@
|
|||||||
import { globalAK } from "@goauthentik/common/global";
|
import { globalAK } from "@goauthentik/common/global";
|
||||||
|
|
||||||
import { LOCALES as RAW_LOCALES, enLocale } from "./definitions";
|
import { AKLocalDefinitions } from "./definitions.js";
|
||||||
import { AkLocale } from "./types";
|
import { AKLocaleDefinition } from "./types.js";
|
||||||
|
|
||||||
export const DEFAULT_LOCALE = "en";
|
export const DEFAULT_LOCALE = "en";
|
||||||
|
|
||||||
export const EVENT_REQUEST_LOCALE = "ak-request-locale";
|
export const EVENT_REQUEST_LOCALE = "ak-request-locale";
|
||||||
|
|
||||||
const TOMBSTONE = "⛼⛼tombstone⛼⛼";
|
/**
|
||||||
|
* Find the locale definition for a given language code.
|
||||||
|
*/
|
||||||
|
export function findLocaleDefinition(languageCode: string): AKLocaleDefinition | null {
|
||||||
|
for (const locale of AKLocalDefinitions) {
|
||||||
|
if (locale.pattern.test(languageCode)) {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: This is the definition of the LOCALES table that most of the code uses. The 'definitions'
|
return null;
|
||||||
// file is relatively pure, but here we establish that we want the English locale to loaded when an
|
|
||||||
// application is first instantiated.
|
|
||||||
|
|
||||||
export const LOCALES = RAW_LOCALES.map((locale) =>
|
|
||||||
locale.code === "en" ? { ...locale, locale: async () => enLocale } : locale,
|
|
||||||
);
|
|
||||||
|
|
||||||
export function getBestMatchLocale(locale: string): AkLocale | undefined {
|
|
||||||
return LOCALES.find((l) => l.match.test(locale));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This looks weird, but it's sensible: we have several candidates, and we want to find the first
|
// This looks weird, but it's sensible: we have several candidates, and we want to find the first
|
||||||
// one that has a supported locale. Then, from *that*, we have to extract that first supported
|
// one that has a supported locale. Then, from *that*, we have to extract that first supported
|
||||||
// locale.
|
// locale.
|
||||||
|
|
||||||
export function findSupportedLocale(candidates: string[]) {
|
export function findSupportedLocale(candidates: string[]): AKLocaleDefinition | null {
|
||||||
const candidate = candidates.find((candidate: string) => getBestMatchLocale(candidate));
|
for (const candidate of candidates) {
|
||||||
return candidate ? getBestMatchLocale(candidate) : undefined;
|
const locale = findLocaleDefinition(candidate);
|
||||||
|
|
||||||
|
if (locale) return locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function localeCodeFromUrl(param = "locale") {
|
return null;
|
||||||
const url = new URL(window.location.href);
|
|
||||||
return url.searchParams.get(param) || "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all locales we can, in order
|
export function localeCodeFromURL(param = "locale") {
|
||||||
// - Global authentik settings (contains user settings)
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
// - URL parameter
|
|
||||||
// - A requested code passed in, if any
|
|
||||||
// - Navigator
|
|
||||||
// - Fallback (en)
|
|
||||||
|
|
||||||
const isLocaleCandidate = (v: unknown): v is string =>
|
return searchParams.get(param);
|
||||||
typeof v === "string" && v !== "" && v !== TOMBSTONE;
|
}
|
||||||
|
|
||||||
export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string {
|
function isLocaleCodeCandidate(input: unknown): input is string {
|
||||||
const localeCandidates: string[] = [
|
if (typeof input !== "string") return false;
|
||||||
localeCodeFromUrl("locale"),
|
|
||||||
userReq,
|
return !!input;
|
||||||
window.navigator?.language ?? TOMBSTONE,
|
}
|
||||||
brandReq,
|
|
||||||
globalAK()?.locale ?? TOMBSTONE,
|
/**
|
||||||
DEFAULT_LOCALE,
|
* Auto-detect the most appropriate locale.
|
||||||
].filter(isLocaleCandidate);
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* The order of precedence is:
|
||||||
|
*
|
||||||
|
* 1. URL parameter `locale`.
|
||||||
|
* 2. User's preferred locale, if any.
|
||||||
|
* 3. Browser's preferred locale, if any.
|
||||||
|
* 4. Brand's preferred locale, if any.
|
||||||
|
* 5. Default locale.
|
||||||
|
*
|
||||||
|
* @param requestedLanguageCode - The user's preferred locale, if any.
|
||||||
|
* @param brandLanguageCode - The brand's preferred locale, if any.
|
||||||
|
*
|
||||||
|
* @returns The most appropriate locale.
|
||||||
|
*/
|
||||||
|
export function autoDetectLanguage(
|
||||||
|
requestedLanguageCode?: string,
|
||||||
|
brandLanguageCode?: string,
|
||||||
|
): string {
|
||||||
|
const localeCandidates = [
|
||||||
|
localeCodeFromURL("locale"),
|
||||||
|
requestedLanguageCode,
|
||||||
|
window.navigator?.language,
|
||||||
|
brandLanguageCode,
|
||||||
|
globalAK()?.locale,
|
||||||
|
].filter(isLocaleCodeCandidate);
|
||||||
|
|
||||||
const firstSupportedLocale = findSupportedLocale(localeCandidates);
|
const firstSupportedLocale = findSupportedLocale(localeCandidates);
|
||||||
|
|
||||||
@ -61,10 +82,11 @@ export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): s
|
|||||||
console.debug(
|
console.debug(
|
||||||
`authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`,
|
`authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return DEFAULT_LOCALE;
|
return DEFAULT_LOCALE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return firstSupportedLocale.code;
|
return firstSupportedLocale.languageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default autoDetectLanguage;
|
export default autoDetectLanguage;
|
||||||
|
@ -1,10 +1,21 @@
|
|||||||
import type { LocaleModule } from "@lit/localize";
|
import type { LocaleModule } from "@lit/localize";
|
||||||
|
|
||||||
export type LocaleRow = [string, RegExp, () => string, () => Promise<LocaleModule>];
|
/**
|
||||||
|
* - ISO 639-1 code for the locale.
|
||||||
|
* - Pattern to match the user-supplied locale.
|
||||||
|
* - Human-readable label for the locale.
|
||||||
|
* - Locale loader.
|
||||||
|
*/
|
||||||
|
export type LocaleRow = [
|
||||||
|
languageCode: string,
|
||||||
|
pattern: RegExp,
|
||||||
|
formatLabel: () => string,
|
||||||
|
fetch: () => Promise<LocaleModule>,
|
||||||
|
];
|
||||||
|
|
||||||
export type AkLocale = {
|
export interface AKLocaleDefinition {
|
||||||
code: string;
|
languageCode: string;
|
||||||
match: RegExp;
|
pattern: RegExp;
|
||||||
label: () => string;
|
formatLabel(): string;
|
||||||
locale: () => Promise<LocaleModule>;
|
fetch(): Promise<LocaleModule>;
|
||||||
};
|
}
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
CapabilitiesEnum,
|
CapabilitiesEnum,
|
||||||
WithCapabilitiesConfig,
|
WithCapabilitiesConfig,
|
||||||
} from "@goauthentik/elements/Interface/capabilitiesProvider";
|
} from "@goauthentik/elements/Interface/capabilitiesProvider";
|
||||||
import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions";
|
import { AKLocalDefinitions } from "@goauthentik/elements/ak-locale-context/definitions";
|
||||||
import "@goauthentik/elements/forms/FormElement";
|
import "@goauthentik/elements/forms/FormElement";
|
||||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||||
|
|
||||||
@ -199,15 +199,15 @@ ${prompt.initialValue}</textarea
|
|||||||
})}`;
|
})}`;
|
||||||
case PromptTypeEnum.AkLocale: {
|
case PromptTypeEnum.AkLocale: {
|
||||||
const locales = this.can(CapabilitiesEnum.CanDebug)
|
const locales = this.can(CapabilitiesEnum.CanDebug)
|
||||||
? LOCALES
|
? AKLocalDefinitions
|
||||||
: LOCALES.filter((locale) => locale.code !== "debug");
|
: AKLocalDefinitions.filter((locale) => locale.languageCode !== "debug");
|
||||||
const options = locales.map(
|
const options = locales.map(
|
||||||
(locale) =>
|
(locale) =>
|
||||||
html`<option
|
html`<option
|
||||||
value=${locale.code}
|
value=${locale.languageCode}
|
||||||
?selected=${locale.code === prompt.initialValue}
|
?selected=${locale.languageCode === prompt.initialValue}
|
||||||
>
|
>
|
||||||
${locale.code.toUpperCase()} - ${locale.label()}
|
${locale.languageCode.toUpperCase()} - ${locale.formatLabel()}
|
||||||
</option> `,
|
</option> `,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user