Compare commits
	
		
			1 Commits
		
	
	
		
			import-org
			...
			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
	