Compare commits
	
		
			1 Commits
		
	
	
		
			website/do
			...
			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`);
 | 
				
			||||||
            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}`);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        LocaleContext.singleton = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const [getLocale, setLocale] = initializeLocalization();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.getLocale = getLocale;
 | 
				
			||||||
 | 
					        this.setLocale = setLocale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.setLocale(code).then(this.#notifyApplication);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function localeCodeFromUrl(param = "locale") {
 | 
					export function localeCodeFromURL(param = "locale") {
 | 
				
			||||||
    const url = new URL(window.location.href);
 | 
					    const searchParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
    return url.searchParams.get(param) || "";
 | 
					
 | 
				
			||||||
 | 
					    return searchParams.get(param);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get all locales we can, in order
 | 
					function isLocaleCodeCandidate(input: unknown): input is string {
 | 
				
			||||||
// - Global authentik settings (contains user settings)
 | 
					    if (typeof input !== "string") return false;
 | 
				
			||||||
// - URL parameter
 | 
					 | 
				
			||||||
// - A requested code passed in, if any
 | 
					 | 
				
			||||||
// - Navigator
 | 
					 | 
				
			||||||
// - Fallback (en)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isLocaleCandidate = (v: unknown): v is string =>
 | 
					    return !!input;
 | 
				
			||||||
    typeof v === "string" && v !== "" && v !== TOMBSTONE;
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string {
 | 
					/**
 | 
				
			||||||
    const localeCandidates: string[] = [
 | 
					 * Auto-detect the most appropriate locale.
 | 
				
			||||||
        localeCodeFromUrl("locale"),
 | 
					 *
 | 
				
			||||||
        userReq,
 | 
					 * @remarks
 | 
				
			||||||
        window.navigator?.language ?? TOMBSTONE,
 | 
					 *
 | 
				
			||||||
        brandReq,
 | 
					 * The order of precedence is:
 | 
				
			||||||
        globalAK()?.locale ?? TOMBSTONE,
 | 
					 *
 | 
				
			||||||
        DEFAULT_LOCALE,
 | 
					 * 1. URL parameter `locale`.
 | 
				
			||||||
    ].filter(isLocaleCandidate);
 | 
					 * 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