Parity.
This commit is contained in:
@ -1,14 +1,13 @@
|
||||
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
|
||||
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||
import { isSlug } from "@goauthentik/common/utils.js";
|
||||
import { isSlug, isURLInput } from "@goauthentik/common/utils.js";
|
||||
import { camelToSnake } from "@goauthentik/common/utils.js";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-slug-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
@ -21,13 +20,25 @@ import { type ApplicationRequest } from "@goauthentik/api";
|
||||
|
||||
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
|
||||
|
||||
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
|
||||
/**
|
||||
* Plucks the specified keys from an object, trimming their values if they are strings.
|
||||
*
|
||||
* @template T - The type of the input object.
|
||||
* @template K - The keys to be plucked from the input object.
|
||||
*
|
||||
* @param {T} input - The input object.
|
||||
* @param {Array<K>} keys - The keys to be plucked from the input object.
|
||||
*/
|
||||
function trimMany<T extends object, K extends keyof T>(input: T, keys: Array<K>): Pick<T, K> {
|
||||
const result: Partial<T> = {};
|
||||
|
||||
const trimMany = (o: KeyUnknown, vs: string[]) =>
|
||||
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
|
||||
for (const key of keys) {
|
||||
const value = input[key];
|
||||
result[key] = (typeof value === "string" ? value.trim() : value) as T[K];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isStr = (v: any): v is string => typeof v === "string";
|
||||
return result as Pick<T, K>;
|
||||
}
|
||||
|
||||
@customElement("ak-application-wizard-application-step")
|
||||
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
@ -54,27 +65,34 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
}
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
|
||||
return [
|
||||
// ---
|
||||
{ kind: "next", destination: "provider-choice" },
|
||||
{ kind: "cancel" },
|
||||
];
|
||||
}
|
||||
|
||||
get valid() {
|
||||
this.errors = new Map();
|
||||
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
|
||||
|
||||
if (values.name === "") {
|
||||
const trimmed = trimMany((this.formValues || {}) as Partial<ApplicationRequest>, [
|
||||
"name",
|
||||
"slug",
|
||||
"metaLaunchUrl",
|
||||
]);
|
||||
|
||||
if (!trimmed.name) {
|
||||
this.errors.set("name", msg("An application name is required"));
|
||||
}
|
||||
if (
|
||||
!(
|
||||
isStr(values.metaLaunchUrl) &&
|
||||
(values.metaLaunchUrl === "" || URL.canParse(values.metaLaunchUrl))
|
||||
)
|
||||
) {
|
||||
|
||||
if (!isURLInput(trimmed.metaLaunchUrl)) {
|
||||
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
|
||||
}
|
||||
if (!(isStr(values.slug) && values.slug !== "" && isSlug(values.slug))) {
|
||||
|
||||
if (!isSlug(trimmed.slug)) {
|
||||
this.errors.set("slug", msg("Not a valid slug"));
|
||||
}
|
||||
|
||||
return this.errors.size === 0;
|
||||
}
|
||||
|
||||
@ -82,27 +100,39 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
if (button.kind === "next") {
|
||||
if (!this.valid) {
|
||||
this.handleEnabling({
|
||||
disabled: ["provider-choice", "provider", "bindings", "submit"],
|
||||
disabled: [
|
||||
// ---
|
||||
"provider-choice",
|
||||
"provider",
|
||||
"bindings",
|
||||
"submit",
|
||||
],
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
|
||||
|
||||
let payload: ApplicationWizardStateUpdate = {
|
||||
app: this.formValues,
|
||||
errors: this.removeErrors("app"),
|
||||
};
|
||||
|
||||
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
|
||||
payload = {
|
||||
...payload,
|
||||
provider: { name: `Provider for ${app.name}` },
|
||||
};
|
||||
}
|
||||
|
||||
this.handleUpdate(payload, button.destination, {
|
||||
enable: "provider-choice",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
super.handleButton(button);
|
||||
}
|
||||
|
||||
@ -181,6 +211,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
if (!(this.wizard.app && this.wizard.errors)) {
|
||||
throw new Error("Application Step received uninitialized wizard context.");
|
||||
}
|
||||
|
||||
return this.renderForm(
|
||||
this.wizard.app as ApplicationRequest,
|
||||
this.wizard.errors?.app ?? {},
|
||||
|
||||
@ -15,7 +15,9 @@ export const bindModeOptions = [
|
||||
{
|
||||
label: msg("Direct binding"),
|
||||
value: LDAPAPIAccessMode.Direct,
|
||||
description: html`${msg("Always execute the configured bind flow to authenticate the user")}`,
|
||||
description: html`${msg(
|
||||
"Always execute the configured bind flow to authenticate the user",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
@ -31,7 +33,9 @@ export const searchModeOptions = [
|
||||
{
|
||||
label: msg("Direct querying"),
|
||||
value: LDAPAPIAccessMode.Direct,
|
||||
description: html`${msg("Always returns the latest data, but slower than cached querying")}`,
|
||||
description: html`${msg(
|
||||
"Always returns the latest data, but slower than cached querying",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2025.2.2";
|
||||
export const VERSION = "2025.2.3";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
||||
@ -25,10 +25,35 @@ export function convertToSlug(text: string): string {
|
||||
.replace(/[^\w-]+/g, "");
|
||||
}
|
||||
|
||||
export function isSlug(text: string): boolean {
|
||||
const lowered = text.toLowerCase();
|
||||
const forbidden = /([^\w-]|\s)/.test(lowered);
|
||||
return lowered === text && !forbidden;
|
||||
/**
|
||||
* Type guard to check if a given string is a valid URL slug, i.e.
|
||||
* only containing alphanumeric characters, dashes, and underscores.
|
||||
*/
|
||||
export function isSlug(input: unknown): input is string {
|
||||
if (typeof input !== "string") return false;
|
||||
if (!input) return false;
|
||||
|
||||
const lowered = input.toLowerCase();
|
||||
if (input !== lowered) return false;
|
||||
|
||||
return /([^\w-]|\s)/.test(lowered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a given input is parsable as a URL.
|
||||
*
|
||||
* ```js
|
||||
* isURLInput("https://example.com") // true
|
||||
* isURLInput("invalid-url") // false
|
||||
* isURLInput(new URL("https://example.com")) // true
|
||||
* ```
|
||||
*/
|
||||
export function isURLInput(input: unknown): input is string | URL {
|
||||
if (typeof input !== "string" && !(input instanceof URL)) return false;
|
||||
|
||||
if (!input) return false;
|
||||
|
||||
return URL.canParse(input);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -11,6 +11,7 @@ import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@ -34,6 +35,9 @@ export class SyncStatusTable extends Table<SystemTask> {
|
||||
}
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<SystemTask>> {
|
||||
if (this.tasks.length === 1) {
|
||||
this.expandedElements = this.tasks;
|
||||
}
|
||||
return {
|
||||
pagination: {
|
||||
next: 0,
|
||||
@ -104,7 +108,7 @@ export class SyncStatusCard extends AKElement {
|
||||
triggerSync!: () => Promise<unknown>;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFCard, PFTable];
|
||||
return [PFBase, PFButton, PFCard, PFTable];
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
@ -133,7 +137,20 @@ export class SyncStatusCard extends AKElement {
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">${msg("Sync status")}</div>
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__actions">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.fetch();
|
||||
}}
|
||||
>
|
||||
<i class="fa fa-sync"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-card__title">${msg("Sync status")}</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-action-button
|
||||
|
||||
@ -187,7 +187,11 @@ export class Wizard extends ModalButton {
|
||||
/**
|
||||
* Reset the wizard to it's initial state.
|
||||
*/
|
||||
reset = () => {
|
||||
reset = (ev?: Event) => {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
this.open = false;
|
||||
|
||||
this.querySelectorAll("[data-wizardmanaged=true]").forEach((el) => {
|
||||
@ -332,9 +336,7 @@ export class Wizard extends ModalButton {
|
||||
<button
|
||||
class="pf-c-button pf-m-link"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.reset();
|
||||
}}
|
||||
@click=${this.reset}
|
||||
>
|
||||
${msg("Cancel")}
|
||||
</button>
|
||||
|
||||
@ -72,7 +72,9 @@ export class BaseStage<
|
||||
}
|
||||
return this.host?.submit(object as unknown as Tout).then((successful) => {
|
||||
if (successful) {
|
||||
this.cleanup();
|
||||
this.onSubmitSuccess();
|
||||
} else {
|
||||
this.onSubmitFailure();
|
||||
}
|
||||
return successful;
|
||||
});
|
||||
@ -80,13 +82,13 @@ export class BaseStage<
|
||||
|
||||
renderNonFieldErrors() {
|
||||
const errors = this.challenge?.responseErrors || {};
|
||||
if (!("non_field_errors" in errors)) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!("non_field_errors" in errors)) return nothing;
|
||||
|
||||
const nonFieldErrors = errors.non_field_errors;
|
||||
if (!nonFieldErrors) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!nonFieldErrors) return nothing;
|
||||
|
||||
return html`<div class="pf-c-form__alert">
|
||||
${nonFieldErrors.map((err) => {
|
||||
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
||||
@ -124,7 +126,12 @@ export class BaseStage<
|
||||
`;
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
onSubmitSuccess(): void {
|
||||
// Method that can be overridden by stages
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmitFailure(): void {
|
||||
// Method that can be overridden by stages
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
/// <reference types="@hcaptcha/types"/>
|
||||
///<reference types="@hcaptcha/types"/>
|
||||
import { renderStatic } from "@goauthentik/common/purify";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import { akEmptyState } from "@goauthentik/elements/EmptyState";
|
||||
@ -9,7 +9,7 @@ import { randomId } from "@goauthentik/elements/utils/randomId";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import { P, match } from "ts-pattern";
|
||||
import type { TurnstileObject } from "turnstile-types";
|
||||
import type * as _ from "turnstile-types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
@ -24,10 +24,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
|
||||
|
||||
interface TurnstileWindow extends Window {
|
||||
turnstile: TurnstileObject;
|
||||
}
|
||||
|
||||
type TokenHandler = (token: string) => void;
|
||||
|
||||
type Dims = { height: number };
|
||||
@ -52,6 +48,8 @@ type CaptchaHandler = {
|
||||
name: string;
|
||||
interactive: () => Promise<unknown>;
|
||||
execute: () => Promise<unknown>;
|
||||
refreshInteractive: () => Promise<unknown>;
|
||||
refresh: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
// A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces
|
||||
@ -119,6 +117,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
this.host.submit({ component: "ak-stage-captcha", token });
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
refreshedAt = new Date();
|
||||
|
||||
@state()
|
||||
activeHandler?: CaptchaHandler = undefined;
|
||||
|
||||
@state()
|
||||
error?: string;
|
||||
|
||||
@ -127,16 +131,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
name: "grecaptcha",
|
||||
interactive: this.renderGReCaptchaFrame,
|
||||
execute: this.executeGReCaptcha,
|
||||
refreshInteractive: this.refreshGReCaptchaFrame,
|
||||
refresh: this.refreshGReCaptcha,
|
||||
},
|
||||
{
|
||||
name: "hcaptcha",
|
||||
interactive: this.renderHCaptchaFrame,
|
||||
execute: this.executeHCaptcha,
|
||||
refreshInteractive: this.refreshHCaptchaFrame,
|
||||
refresh: this.refreshHCaptcha,
|
||||
},
|
||||
{
|
||||
name: "turnstile",
|
||||
interactive: this.renderTurnstileFrame,
|
||||
execute: this.executeTurnstile,
|
||||
refreshInteractive: this.refreshTurnstileFrame,
|
||||
refresh: this.refreshTurnstile,
|
||||
},
|
||||
];
|
||||
|
||||
@ -205,7 +215,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
console.debug(`authentik/stages/captcha: Unknown message: ${message}`);
|
||||
},
|
||||
)
|
||||
.otherwise(() => null);
|
||||
.otherwise(() => {});
|
||||
}
|
||||
|
||||
async renderGReCaptchaFrame() {
|
||||
@ -230,6 +240,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
});
|
||||
}
|
||||
|
||||
async refreshGReCaptchaFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.grecaptcha.reset();
|
||||
}
|
||||
|
||||
async refreshGReCaptcha() {
|
||||
window.grecaptcha.reset();
|
||||
window.grecaptcha.execute();
|
||||
}
|
||||
|
||||
async renderHCaptchaFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
@ -251,6 +270,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
);
|
||||
}
|
||||
|
||||
async refreshHCaptchaFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.hcaptcha.reset();
|
||||
}
|
||||
|
||||
async refreshHCaptcha() {
|
||||
window.hcaptcha.reset();
|
||||
window.hcaptcha.execute();
|
||||
}
|
||||
|
||||
async renderTurnstileFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
@ -262,13 +290,18 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
}
|
||||
|
||||
async executeTurnstile() {
|
||||
return (window as unknown as TurnstileWindow).turnstile.render(
|
||||
this.captchaDocumentContainer,
|
||||
{
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
},
|
||||
);
|
||||
return window.turnstile.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
});
|
||||
}
|
||||
|
||||
async refreshTurnstileFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.turnstile.reset();
|
||||
}
|
||||
|
||||
async refreshTurnstile() {
|
||||
window.turnstile.reset();
|
||||
}
|
||||
|
||||
async renderFrame(captchaElement: TemplateResult) {
|
||||
@ -336,16 +369,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name));
|
||||
let lastError = undefined;
|
||||
let found = false;
|
||||
for (const { name, interactive, execute } of handlers) {
|
||||
console.debug(`authentik/stages/captcha: trying handler ${name}`);
|
||||
for (const handler of handlers) {
|
||||
console.debug(`authentik/stages/captcha: trying handler ${handler.name}`);
|
||||
try {
|
||||
const runner = this.challenge.interactive ? interactive : execute;
|
||||
const runner = this.challenge.interactive
|
||||
? handler.interactive
|
||||
: handler.execute;
|
||||
await runner.apply(this);
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: handler succeeded`);
|
||||
found = true;
|
||||
this.activeHandler = handler;
|
||||
break;
|
||||
} catch (exc) {
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler failed`);
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: handler failed`);
|
||||
console.debug(exc);
|
||||
lastError = exc;
|
||||
}
|
||||
@ -370,6 +406,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
document.body.appendChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("refreshedAt") || !this.challenge) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug("authentik/stages/captcha: refresh triggered");
|
||||
if (this.challenge.interactive) {
|
||||
this.activeHandler?.refreshInteractive.apply(this);
|
||||
} else {
|
||||
this.activeHandler?.refresh.apply(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@ -49,6 +49,8 @@ export class IdentificationStage extends BaseStage<
|
||||
|
||||
@state()
|
||||
captchaToken = "";
|
||||
@state()
|
||||
captchaRefreshedAt = new Date();
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
@ -136,6 +138,7 @@ export class IdentificationStage extends BaseStage<
|
||||
if (ev.key === "Enter") {
|
||||
this.submitForm(ev);
|
||||
}
|
||||
|
||||
const el = ev.target as HTMLInputElement;
|
||||
// Because the password field is not actually on this page,
|
||||
// and we want to 'prefill' the password for the user,
|
||||
@ -179,12 +182,16 @@ export class IdentificationStage extends BaseStage<
|
||||
this.form.appendChild(totp);
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
onSubmitSuccess(): void {
|
||||
if (this.form) {
|
||||
this.form.remove();
|
||||
}
|
||||
}
|
||||
|
||||
onSubmitFailure(): void {
|
||||
this.captchaRefreshedAt = new Date();
|
||||
}
|
||||
|
||||
renderSource(source: LoginSource): TemplateResult {
|
||||
const icon = renderSourceIcon(source.name, source.iconUrl);
|
||||
return html`<li class="pf-c-login__main-footer-links-item">
|
||||
@ -287,6 +294,7 @@ export class IdentificationStage extends BaseStage<
|
||||
.onTokenChange=${(token: string) => {
|
||||
this.captchaToken = token;
|
||||
}}
|
||||
.refreshedAt=${this.captchaRefreshedAt}
|
||||
embedded
|
||||
></ak-stage-captcha>
|
||||
`
|
||||
|
||||
@ -11,41 +11,41 @@ export const sourceLocale = `en`;
|
||||
* lexicographically.
|
||||
*/
|
||||
export const targetLocales = [
|
||||
`de`,
|
||||
`en`,
|
||||
`es`,
|
||||
`fr`,
|
||||
`it`,
|
||||
`ko`,
|
||||
`nl`,
|
||||
`pl`,
|
||||
`pseudo-LOCALE`,
|
||||
`ru`,
|
||||
`tr`,
|
||||
`zh_TW`,
|
||||
`zh-CN`,
|
||||
`zh-Hans`,
|
||||
`zh-Hant`,
|
||||
`de`,
|
||||
`en`,
|
||||
`es`,
|
||||
`fr`,
|
||||
`it`,
|
||||
`ko`,
|
||||
`nl`,
|
||||
`pl`,
|
||||
`pseudo-LOCALE`,
|
||||
`ru`,
|
||||
`tr`,
|
||||
`zh_TW`,
|
||||
`zh-CN`,
|
||||
`zh-Hans`,
|
||||
`zh-Hant`,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* All valid project locale codes. Sorted lexicographically.
|
||||
*/
|
||||
export const allLocales = [
|
||||
`de`,
|
||||
`en`,
|
||||
`en`,
|
||||
`es`,
|
||||
`fr`,
|
||||
`it`,
|
||||
`ko`,
|
||||
`nl`,
|
||||
`pl`,
|
||||
`pseudo-LOCALE`,
|
||||
`ru`,
|
||||
`tr`,
|
||||
`zh_TW`,
|
||||
`zh-CN`,
|
||||
`zh-Hans`,
|
||||
`zh-Hant`,
|
||||
`de`,
|
||||
`en`,
|
||||
`en`,
|
||||
`es`,
|
||||
`fr`,
|
||||
`it`,
|
||||
`ko`,
|
||||
`nl`,
|
||||
`pl`,
|
||||
`pseudo-LOCALE`,
|
||||
`ru`,
|
||||
`tr`,
|
||||
`zh_TW`,
|
||||
`zh-CN`,
|
||||
`zh-Hans`,
|
||||
`zh-Hant`,
|
||||
] as const;
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import "@formatjs/intl-listformat/locale-data/en";
|
||||
import "@formatjs/intl-listformat/polyfill";
|
||||
import "@webcomponents/webcomponentsjs";
|
||||
import "construct-style-sheets-polyfill";
|
||||
|
||||
import "@webcomponents/webcomponentsjs";
|
||||
import "lit/polyfill-support.js";
|
||||
import "core-js/actual";
|
||||
|
||||
import "lit/polyfill-support.js";
|
||||
import "@formatjs/intl-listformat/polyfill";
|
||||
import "@formatjs/intl-listformat/locale-data/en";
|
||||
|
||||
Reference in New Issue
Block a user