Compare commits
	
		
			1 Commits
		
	
	
		
			import-org
			...
			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 { | ||||
|  | ||||
|         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(() => { | ||||
|                 window.setTimeout(this.notifyApplication, 0); | ||||
|             }); | ||||
|         } catch (e) { | ||||
|             throw new Error(`Developer error: Must have only one locale context per session: ${e}`); | ||||
|         } | ||||
|  | ||||
|         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
	