Compare commits
	
		
			1 Commits
		
	
	
		
			version-20
			...
			safari-loc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9deed34479 | 
@ -4,8 +4,12 @@ import {
 | 
			
		||||
    EventMiddleware,
 | 
			
		||||
    LoggingMiddleware,
 | 
			
		||||
} 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 {
 | 
			
		||||
    EVENT_LOCALE_REQUEST,
 | 
			
		||||
    LocaleContextEventDetail,
 | 
			
		||||
} from "@goauthentik/elements/ak-locale-context/events.js";
 | 
			
		||||
 | 
			
		||||
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");
 | 
			
		||||
    window.dispatchEvent(
 | 
			
		||||
        new CustomEvent(EVENT_LOCALE_REQUEST, {
 | 
			
		||||
        new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
 | 
			
		||||
            composed: true,
 | 
			
		||||
            bubbles: true,
 | 
			
		||||
            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_WS_MESSAGE = "ak-ws-message";
 | 
			
		||||
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_MESSAGE = "ak-message";
 | 
			
		||||
export const EVENT_THEME_CHANGE = "ak-theme-change";
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,9 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
@ -57,7 +60,7 @@ export async function me(): Promise<SessionUser> {
 | 
			
		||||
                console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
 | 
			
		||||
 | 
			
		||||
                window.dispatchEvent(
 | 
			
		||||
                    new CustomEvent(EVENT_LOCALE_REQUEST, {
 | 
			
		||||
                    new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
 | 
			
		||||
                        composed: true,
 | 
			
		||||
                        bubbles: true,
 | 
			
		||||
                        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 { LitElement, html } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import "./ak-locale-context";
 | 
			
		||||
import { EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    title: "Elements / Shell / Locale Context",
 | 
			
		||||
@ -37,10 +35,18 @@ export const InFrench = () =>
 | 
			
		||||
    </div>`;
 | 
			
		||||
 | 
			
		||||
export const SwitchingBackAndForth = () => {
 | 
			
		||||
    let lang = "en";
 | 
			
		||||
    let languageCode = "en";
 | 
			
		||||
 | 
			
		||||
    window.setInterval(() => {
 | 
			
		||||
        lang = lang === "en" ? "fr" : "en";
 | 
			
		||||
        window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale: lang }));
 | 
			
		||||
        languageCode = languageCode === "en" ? "fr" : "en";
 | 
			
		||||
 | 
			
		||||
        window.dispatchEvent(
 | 
			
		||||
            new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
 | 
			
		||||
                composed: true,
 | 
			
		||||
                bubbles: true,
 | 
			
		||||
                detail: { locale: languageCode },
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
    }, 1000);
 | 
			
		||||
 | 
			
		||||
    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 { customEvent } from "@goauthentik/elements/utils/customEvents";
 | 
			
		||||
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { WithBrandConfig } from "../Interface/brandProvider";
 | 
			
		||||
import { initializeLocalization } from "./configureLocale";
 | 
			
		||||
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
 | 
			
		||||
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
 | 
			
		||||
import { initializeLocalization } from "./configureLocale.js";
 | 
			
		||||
import type { GetLocale, SetLocale } from "./configureLocale.js";
 | 
			
		||||
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.
 | 
			
		||||
 *
 | 
			
		||||
 * ## Details
 | 
			
		||||
 * @remarks
 | 
			
		||||
 *
 | 
			
		||||
 * 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
 | 
			
		||||
@ -25,70 +24,98 @@ import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helper
 | 
			
		||||
 */
 | 
			
		||||
@customElement("ak-locale-context")
 | 
			
		||||
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 })
 | 
			
		||||
    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 })
 | 
			
		||||
    param = "locale";
 | 
			
		||||
    public param = "locale";
 | 
			
		||||
 | 
			
		||||
    getLocale: LocaleGetter;
 | 
			
		||||
 | 
			
		||||
    setLocale: LocaleSetter;
 | 
			
		||||
    protected readonly getLocale: GetLocale;
 | 
			
		||||
    protected readonly setLocale: SetLocale;
 | 
			
		||||
 | 
			
		||||
    constructor(code = DEFAULT_LOCALE) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.notifyApplication = this.notifyApplication.bind(this);
 | 
			
		||||
        this.updateLocaleHandler = this.updateLocaleHandler.bind(this);
 | 
			
		||||
        try {
 | 
			
		||||
            const [getLocale, setLocale] = initializeLocalization();
 | 
			
		||||
            this.getLocale = getLocale;
 | 
			
		||||
            this.setLocale = setLocale;
 | 
			
		||||
            this.setLocale(code).then(() => {
 | 
			
		||||
                window.setTimeout(this.notifyApplication, 0);
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            throw new Error(`Developer error: Must have only one locale context per session: ${e}`);
 | 
			
		||||
 | 
			
		||||
        if (LocaleContext.singleton) {
 | 
			
		||||
            throw new Error(`Developer error: Must have only one locale context per session`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LocaleContext.singleton = this;
 | 
			
		||||
 | 
			
		||||
        const [getLocale, setLocale] = initializeLocalization();
 | 
			
		||||
 | 
			
		||||
        this.getLocale = getLocale;
 | 
			
		||||
        this.setLocale = setLocale;
 | 
			
		||||
 | 
			
		||||
        this.setLocale(code).then(this.#notifyApplication);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        this.updateLocale();
 | 
			
		||||
        window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener);
 | 
			
		||||
        this.#updateLocale();
 | 
			
		||||
 | 
			
		||||
        window.addEventListener(EVENT_LOCALE_REQUEST, this.#localeUpdateListener as EventListener);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
        window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener);
 | 
			
		||||
        LocaleContext.singleton = null;
 | 
			
		||||
 | 
			
		||||
        window.removeEventListener(
 | 
			
		||||
            EVENT_LOCALE_REQUEST,
 | 
			
		||||
            this.#localeUpdateListener as EventListener,
 | 
			
		||||
        );
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateLocaleHandler(ev: CustomEvent<{ locale: string }>) {
 | 
			
		||||
    #localeUpdateListener = (ev: CustomEvent<LocaleContextEventDetail>) => {
 | 
			
		||||
        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) {
 | 
			
		||||
            console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        locale.locale().then(() => {
 | 
			
		||||
            console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`);
 | 
			
		||||
            this.setLocale(locale.code).then(() => {
 | 
			
		||||
                window.setTimeout(this.notifyApplication, 0);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        return locale.fetch().then(() => {
 | 
			
		||||
            console.debug(
 | 
			
		||||
                `authentik/locale: Setting Locale to ${locale.formatLabel()} (${locale.languageCode})`,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            this.setLocale(locale.languageCode).then(this.#notifyApplication);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    notifyApplication() {
 | 
			
		||||
        // 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(customEvent(EVENT_LOCALE_CHANGE));
 | 
			
		||||
    }
 | 
			
		||||
    #notifyFrameID = -1;
 | 
			
		||||
 | 
			
		||||
    #notifyApplication = () => {
 | 
			
		||||
        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() {
 | 
			
		||||
        return html`<slot></slot>`;
 | 
			
		||||
 | 
			
		||||
@ -1,39 +1,44 @@
 | 
			
		||||
import { configureLocalization } from "@lit/localize";
 | 
			
		||||
 | 
			
		||||
import { sourceLocale, targetLocales } from "../../locale-codes";
 | 
			
		||||
import { getBestMatchLocale } from "./helpers";
 | 
			
		||||
import { sourceLocale, targetLocales } from "../../locale-codes.js";
 | 
			
		||||
import { findLocaleDefinition } from "./helpers.js";
 | 
			
		||||
 | 
			
		||||
type LocaleGetter = ReturnType<typeof configureLocalization>["getLocale"];
 | 
			
		||||
type LocaleSetter = ReturnType<typeof configureLocalization>["setLocale"];
 | 
			
		||||
export type ConfigureLocalizationResult = ReturnType<typeof configureLocalization>;
 | 
			
		||||
 | 
			
		||||
// Internal use only.
 | 
			
		||||
//
 | 
			
		||||
// This is where the lit-localization module is initialized with our loader, which associates our
 | 
			
		||||
// collection of locales with its getter and setter functions.
 | 
			
		||||
export type GetLocale = ConfigureLocalizationResult["getLocale"];
 | 
			
		||||
export type SetLocale = ConfigureLocalizationResult["setLocale"];
 | 
			
		||||
 | 
			
		||||
let getLocale: LocaleGetter | undefined = undefined;
 | 
			
		||||
let setLocale: LocaleSetter | undefined = undefined;
 | 
			
		||||
export type LocaleState = [GetLocale, SetLocale];
 | 
			
		||||
 | 
			
		||||
export function initializeLocalization(): [LocaleGetter, LocaleSetter] {
 | 
			
		||||
    if (getLocale && setLocale) {
 | 
			
		||||
        return [getLocale, setLocale];
 | 
			
		||||
    }
 | 
			
		||||
let cachedLocaleState: LocaleState | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
    ({ 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,
 | 
			
		||||
        targetLocales,
 | 
			
		||||
        loadLocale: async (locale: string) => {
 | 
			
		||||
            const localeDef = getBestMatchLocale(locale);
 | 
			
		||||
            if (!localeDef) {
 | 
			
		||||
                console.warn(`Unrecognized locale: ${localeDef}`);
 | 
			
		||||
                return Promise.reject("");
 | 
			
		||||
            }
 | 
			
		||||
            return localeDef.locale();
 | 
			
		||||
        },
 | 
			
		||||
    }));
 | 
			
		||||
        loadLocale: (languageCode) => {
 | 
			
		||||
            const localeDef = findLocaleDefinition(languageCode);
 | 
			
		||||
 | 
			
		||||
    return [getLocale, setLocale];
 | 
			
		||||
            if (!localeDef) {
 | 
			
		||||
                throw new Error(`Unrecognized locale: ${localeDef}`);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return localeDef.fetch();
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    cachedLocaleState = [getLocale, setLocale];
 | 
			
		||||
 | 
			
		||||
    return cachedLocaleState;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 { 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;
 | 
			
		||||
 | 
			
		||||
export { enLocale };
 | 
			
		||||
/**
 | 
			
		||||
 * The default English locale module.
 | 
			
		||||
 */
 | 
			
		||||
export const DefaultLocaleModule: LocaleModule = EnglishLocaleModule;
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
@ -35,34 +39,44 @@ export { enLocale };
 | 
			
		||||
// - Text Label
 | 
			
		||||
// - Locale loader.
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
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
 | 
			
		||||
const LOCALE_TABLE: LocaleRow[] = [
 | 
			
		||||
    ["de",      /^de([_-]|$)/i,      () => msg("German"),                async () => await import("@goauthentik/locales/de")],
 | 
			
		||||
    ["en",      /^en([_-]|$)/i,      () => msg("English"),               async () => await import("@goauthentik/locales/en")],
 | 
			
		||||
    ["es",      /^es([_-]|$)/i,      () => msg("Spanish"),               async () => await import("@goauthentik/locales/es")],
 | 
			
		||||
    ["fr",      /^fr([_-]|$)/i,      () => msg("French"),                async () => await import("@goauthentik/locales/fr")],
 | 
			
		||||
    ["it",      /^it([_-]|$)/i,      () => msg("Italian"),               async () => await import("@goauthentik/locales/it")],
 | 
			
		||||
    ["ko",      /^ko([_-]|$)/i,      () => msg("Korean"),                async () => await import("@goauthentik/locales/ko")],
 | 
			
		||||
    ["nl",      /^nl([_-]|$)/i,      () => msg("Dutch"),                 async () => await import("@goauthentik/locales/nl")],
 | 
			
		||||
    ["pl",      /^pl([_-]|$)/i,      () => msg("Polish"),                async () => await import("@goauthentik/locales/pl")],
 | 
			
		||||
    ["ru",      /^ru([_-]|$)/i,      () => msg("Russian"),               async () => await import("@goauthentik/locales/ru")],
 | 
			
		||||
    ["tr",      /^tr([_-]|$)/i,      () => msg("Turkish"),               async () => await import("@goauthentik/locales/tr")],
 | 
			
		||||
    ["zh_TW",   /^zh[_-]TW$/i,       () => msg("Taiwanese Mandarin"),    async () => await import("@goauthentik/locales/zh_TW")],
 | 
			
		||||
    ["zh-Hans", /^zh(\b|_)/i,        () => msg("Chinese (simplified)"),  async () => await import("@goauthentik/locales/zh-Hans")],
 | 
			
		||||
    ["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), async () => await import("@goauthentik/locales/zh-Hant")],
 | 
			
		||||
    debug
 | 
			
		||||
const LOCALE_TABLE: readonly LocaleRow[] = [
 | 
			
		||||
    // English loaded when the application is first instantiated.
 | 
			
		||||
    ["en", /^en([_-]|$)/i,   () => msg("English"), () => Promise.resolve(DefaultLocaleModule)],
 | 
			
		||||
    ["de", /^de([_-]|$)/i,   () => msg("German"),  () => import("@goauthentik/locales/de")],
 | 
			
		||||
    ["es", /^es([_-]|$)/i,   () => msg("Spanish"), () => import("@goauthentik/locales/es")],
 | 
			
		||||
    ["fr", /^fr([_-]|$)/i,   () => msg("French"),  () => import("@goauthentik/locales/fr")],
 | 
			
		||||
    ["it", /^it([_-]|$)/i,   () => msg("Italian"), () => import("@goauthentik/locales/it")],
 | 
			
		||||
    ["ko", /^ko([_-]|$)/i,   () => msg("Korean"),  () => import("@goauthentik/locales/ko")],
 | 
			
		||||
    ["nl", /^nl([_-]|$)/i,   () => msg("Dutch"),   () => import("@goauthentik/locales/nl")],
 | 
			
		||||
    ["pl", /^pl([_-]|$)/i,   () => msg("Polish"),  () => import("@goauthentik/locales/pl")],
 | 
			
		||||
    ["ru", /^ru([_-]|$)/i,   () => msg("Russian"), () => import("@goauthentik/locales/ru")],
 | 
			
		||||
    ["tr", /^tr([_-]|$)/i,   () => msg("Turkish"), () => import("@goauthentik/locales/tr")],
 | 
			
		||||
    ["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), () => import("@goauthentik/locales/zh_TW")],
 | 
			
		||||
    ["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), () => import("@goauthentik/locales/zh-Hans")],
 | 
			
		||||
    ["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,
 | 
			
		||||
    match,
 | 
			
		||||
    label,
 | 
			
		||||
    locale,
 | 
			
		||||
}));
 | 
			
		||||
/**
 | 
			
		||||
 * Available locales, identified by their ISO 639-1 language code.
 | 
			
		||||
 */
 | 
			
		||||
export const AKLocalDefinitions: readonly AKLocaleDefinition[] = LOCALE_TABLE.map(
 | 
			
		||||
    ([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 { LOCALES as RAW_LOCALES, enLocale } from "./definitions";
 | 
			
		||||
import { AkLocale } from "./types";
 | 
			
		||||
import { AKLocalDefinitions } from "./definitions.js";
 | 
			
		||||
import { AKLocaleDefinition } from "./types.js";
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_LOCALE = "en";
 | 
			
		||||
 | 
			
		||||
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'
 | 
			
		||||
// 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));
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
// locale.
 | 
			
		||||
 | 
			
		||||
export function findSupportedLocale(candidates: string[]) {
 | 
			
		||||
    const candidate = candidates.find((candidate: string) => getBestMatchLocale(candidate));
 | 
			
		||||
    return candidate ? getBestMatchLocale(candidate) : undefined;
 | 
			
		||||
export function findSupportedLocale(candidates: string[]): AKLocaleDefinition | null {
 | 
			
		||||
    for (const candidate of candidates) {
 | 
			
		||||
        const locale = findLocaleDefinition(candidate);
 | 
			
		||||
 | 
			
		||||
        if (locale) return locale;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function localeCodeFromUrl(param = "locale") {
 | 
			
		||||
    const url = new URL(window.location.href);
 | 
			
		||||
    return url.searchParams.get(param) || "";
 | 
			
		||||
export function localeCodeFromURL(param = "locale") {
 | 
			
		||||
    const searchParams = new URLSearchParams(window.location.search);
 | 
			
		||||
 | 
			
		||||
    return searchParams.get(param);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get all locales we can, in order
 | 
			
		||||
// - Global authentik settings (contains user settings)
 | 
			
		||||
// - URL parameter
 | 
			
		||||
// - A requested code passed in, if any
 | 
			
		||||
// - Navigator
 | 
			
		||||
// - Fallback (en)
 | 
			
		||||
function isLocaleCodeCandidate(input: unknown): input is string {
 | 
			
		||||
    if (typeof input !== "string") return false;
 | 
			
		||||
 | 
			
		||||
const isLocaleCandidate = (v: unknown): v is string =>
 | 
			
		||||
    typeof v === "string" && v !== "" && v !== TOMBSTONE;
 | 
			
		||||
    return !!input;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string {
 | 
			
		||||
    const localeCandidates: string[] = [
 | 
			
		||||
        localeCodeFromUrl("locale"),
 | 
			
		||||
        userReq,
 | 
			
		||||
        window.navigator?.language ?? TOMBSTONE,
 | 
			
		||||
        brandReq,
 | 
			
		||||
        globalAK()?.locale ?? TOMBSTONE,
 | 
			
		||||
        DEFAULT_LOCALE,
 | 
			
		||||
    ].filter(isLocaleCandidate);
 | 
			
		||||
/**
 | 
			
		||||
 * Auto-detect the most appropriate locale.
 | 
			
		||||
 *
 | 
			
		||||
 * @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);
 | 
			
		||||
 | 
			
		||||
@ -61,10 +82,11 @@ export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): s
 | 
			
		||||
        console.debug(
 | 
			
		||||
            `authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return DEFAULT_LOCALE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return firstSupportedLocale.code;
 | 
			
		||||
    return firstSupportedLocale.languageCode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default autoDetectLanguage;
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,21 @@
 | 
			
		||||
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 = {
 | 
			
		||||
    code: string;
 | 
			
		||||
    match: RegExp;
 | 
			
		||||
    label: () => string;
 | 
			
		||||
    locale: () => Promise<LocaleModule>;
 | 
			
		||||
};
 | 
			
		||||
export interface AKLocaleDefinition {
 | 
			
		||||
    languageCode: string;
 | 
			
		||||
    pattern: RegExp;
 | 
			
		||||
    formatLabel(): string;
 | 
			
		||||
    fetch(): Promise<LocaleModule>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import {
 | 
			
		||||
    CapabilitiesEnum,
 | 
			
		||||
    WithCapabilitiesConfig,
 | 
			
		||||
} 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 { BaseStage } from "@goauthentik/flow/stages/base";
 | 
			
		||||
 | 
			
		||||
@ -199,15 +199,15 @@ ${prompt.initialValue}</textarea
 | 
			
		||||
                })}`;
 | 
			
		||||
            case PromptTypeEnum.AkLocale: {
 | 
			
		||||
                const locales = this.can(CapabilitiesEnum.CanDebug)
 | 
			
		||||
                    ? LOCALES
 | 
			
		||||
                    : LOCALES.filter((locale) => locale.code !== "debug");
 | 
			
		||||
                    ? AKLocalDefinitions
 | 
			
		||||
                    : AKLocalDefinitions.filter((locale) => locale.languageCode !== "debug");
 | 
			
		||||
                const options = locales.map(
 | 
			
		||||
                    (locale) =>
 | 
			
		||||
                        html`<option
 | 
			
		||||
                            value=${locale.code}
 | 
			
		||||
                            ?selected=${locale.code === prompt.initialValue}
 | 
			
		||||
                            value=${locale.languageCode}
 | 
			
		||||
                            ?selected=${locale.languageCode === prompt.initialValue}
 | 
			
		||||
                        >
 | 
			
		||||
                            ${locale.code.toUpperCase()} - ${locale.label()}
 | 
			
		||||
                            ${locale.languageCode.toUpperCase()} - ${locale.formatLabel()}
 | 
			
		||||
                        </option> `,
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user