diff --git a/web/package-lock.json b/web/package-lock.json
index ce503219f..8be577908 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -18,6 +18,7 @@
"@formatjs/intl-listformat": "^7.4.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"@goauthentik/api": "^2023.6.0-1688736781",
+ "@lit-labs/context": "^0.3.3",
"@lit/localize": "^0.11.4",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.57.0",
@@ -2531,54 +2532,6 @@
"react": ">=16.8.0"
}
},
- "node_modules/@esbuild/android-arm": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
- "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
- "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
- "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@esbuild/darwin-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
@@ -2595,294 +2548,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
- "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
- "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
- "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
- "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
- "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
- "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
- "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
- "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
- "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
- "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
- "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
- "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
- "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
- "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
- "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
- "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
- "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
- "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.2.0",
"dev": true,
@@ -3164,22 +2829,22 @@
}
},
"node_modules/@jest/transform": {
- "version": "29.6.0",
- "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.0.tgz",
- "integrity": "sha512-bhP/KxPo3e322FJ0nKAcb6WVK76ZYyQd1lWygJzoSqP8SYMSLdxHqP4wnPTI4WvbB8PKPDV30y5y7Tya4RHOBA==",
+ "version": "29.6.1",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.1.tgz",
+ "integrity": "sha512-URnTneIU3ZjRSaf906cvf6Hpox3hIeJXRnz3VDSw5/X93gR8ycdfSIEy19FlVx8NFmpN7fe3Gb1xF+NjXaQLWg==",
"dev": true,
"dependencies": {
"@babel/core": "^7.11.6",
- "@jest/types": "^29.6.0",
+ "@jest/types": "^29.6.1",
"@jridgewell/trace-mapping": "^0.3.18",
"babel-plugin-istanbul": "^6.1.1",
"chalk": "^4.0.0",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.9",
- "jest-haste-map": "^29.6.0",
+ "jest-haste-map": "^29.6.1",
"jest-regex-util": "^29.4.3",
- "jest-util": "^29.6.0",
+ "jest-util": "^29.6.1",
"micromatch": "^4.0.4",
"pirates": "^4.0.4",
"slash": "^3.0.0",
@@ -3266,9 +2931,9 @@
}
},
"node_modules/@jest/types": {
- "version": "29.6.0",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.0.tgz",
- "integrity": "sha512-8XCgL9JhqbJTFnMRjEAO+TuW251+MoMd5BSzLiE3vvzpQ8RlBxy8NoyNkDhs3K3OL3HeVinlOl9or5p7GTeOLg==",
+ "version": "29.6.1",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz",
+ "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==",
"dev": true,
"dependencies": {
"@jest/schemas": "^29.6.0",
@@ -3498,6 +3163,15 @@
"@lezer/lr": "^1.0.0"
}
},
+ "node_modules/@lit-labs/context": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@lit-labs/context/-/context-0.3.3.tgz",
+ "integrity": "sha512-5pWPLiXJnx8fZREF4w7RXBwJOxqRBJ57tujo7k23s0ZDfnSltomvYGW4kTOurXfyzDR0OLBBkv9xsWGDhauqew==",
+ "dependencies": {
+ "@lit/reactive-element": "^1.5.0",
+ "lit": "^2.7.0"
+ }
+ },
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.1.0",
"license": "BSD-3-Clause"
@@ -9703,9 +9377,9 @@
}
},
"node_modules/acorn": {
- "version": "8.9.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
- "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -11087,12 +10761,12 @@
}
},
"node_modules/core-js-compat": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz",
- "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==",
+ "version": "3.31.1",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.1.tgz",
+ "integrity": "sha512-wIDWd2s5/5aJSdpOJHfSibxNODxoGoWOBHt8JSPB41NOE94M7kuTPZCYLOlTtuoXTsBPKobpJ6T+y0SSy5L9SA==",
"dev": true,
"dependencies": {
- "browserslist": "^4.21.5"
+ "browserslist": "^4.21.9"
},
"funding": {
"type": "opencollective",
@@ -11992,9 +11666,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.450",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.450.tgz",
- "integrity": "sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==",
+ "version": "1.4.451",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.451.tgz",
+ "integrity": "sha512-YYbXHIBxAHe3KWvGOJOuWa6f3tgow44rBW+QAuwVp2DvGqNZeE//K2MowNdWS7XE8li5cgQDrX1LdBr41LufkA==",
"dev": true
},
"node_modules/elkjs": {
@@ -12943,8 +12617,9 @@
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
@@ -14755,20 +14430,20 @@
"license": "MIT"
},
"node_modules/jest-haste-map": {
- "version": "29.6.0",
- "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.0.tgz",
- "integrity": "sha512-dY1DKufptj7hcJSuhpqlYPGcnN3XjlOy/g0jinpRTMsbb40ivZHiuIPzeminOZkrek8C+oDxC54ILGO3vMLojg==",
+ "version": "29.6.1",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.1.tgz",
+ "integrity": "sha512-0m7f9PZXxOCk1gRACiVgX85knUKPKLPg4oRCjLoqIm9brTHXaorMA0JpmtmVkQiT8nmXyIVoZd/nnH1cfC33ig==",
"dev": true,
"dependencies": {
- "@jest/types": "^29.6.0",
+ "@jest/types": "^29.6.1",
"@types/graceful-fs": "^4.1.3",
"@types/node": "*",
"anymatch": "^3.0.3",
"fb-watchman": "^2.0.0",
"graceful-fs": "^4.2.9",
"jest-regex-util": "^29.4.3",
- "jest-util": "^29.6.0",
- "jest-worker": "^29.6.0",
+ "jest-util": "^29.6.1",
+ "jest-worker": "^29.6.1",
"micromatch": "^4.0.4",
"walker": "^1.0.8"
},
@@ -14789,13 +14464,13 @@
}
},
"node_modules/jest-haste-map/node_modules/jest-worker": {
- "version": "29.6.0",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.0.tgz",
- "integrity": "sha512-oiQHH1SnKmZIwwPnpOrXTq4kHBk3lKGY/07DpnH0sAu+x7J8rXlbLDROZsU6vy9GwB0hPiZeZpu6YlJ48QoKcA==",
+ "version": "29.6.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.1.tgz",
+ "integrity": "sha512-U+Wrbca7S8ZAxAe9L6nb6g8kPdia5hj32Puu5iOqBCMTMWFHXuK6dOV2IFrpedbTV8fjMFLdWNttQTBL6u2MRA==",
"dev": true,
"dependencies": {
"@types/node": "*",
- "jest-util": "^29.6.0",
+ "jest-util": "^29.6.1",
"merge-stream": "^2.0.0",
"supports-color": "^8.0.0"
},
@@ -14828,12 +14503,12 @@
}
},
"node_modules/jest-util": {
- "version": "29.6.0",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.0.tgz",
- "integrity": "sha512-S0USx9YwcvEm4pQ5suisVm/RVxBmi0GFR7ocJhIeaCuW5AXnAnffXbaVKvIFodyZNOc9ygzVtTxmBf40HsHXaA==",
+ "version": "29.6.1",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.1.tgz",
+ "integrity": "sha512-NRFCcjc+/uO3ijUVyNOQJluf8PtGCe/W6cix36+M3cTFgiYqFOOW5MgN4JOOcvbUhcKTYVd1CvHz/LWi8d16Mg==",
"dev": true,
"dependencies": {
- "@jest/types": "^29.6.0",
+ "@jest/types": "^29.6.1",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
@@ -15503,8 +15178,9 @@
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "dev": true
},
"node_modules/lodash.deburr": {
"version": "4.1.0",
@@ -16297,8 +15973,9 @@
},
"node_modules/mimic-fn": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -16680,8 +16357,9 @@
},
"node_modules/node-releases": {
"version": "2.0.12",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz",
+ "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==",
+ "dev": true
},
"node_modules/non-layered-tidy-tree-layout": {
"version": "2.0.2",
@@ -17023,8 +16701,9 @@
},
"node_modules/onetime": {
"version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"mimic-fn": "^2.1.0"
},
@@ -17431,9 +17110,9 @@
}
},
"node_modules/pirates": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
- "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
"dev": true,
"engines": {
"node": ">= 6"
@@ -20586,6 +20265,8 @@
},
"node_modules/update-browserslist-db": {
"version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+ "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
"dev": true,
"funding": [
{
@@ -20601,7 +20282,6 @@
"url": "https://github.com/sponsors/ai"
}
],
- "license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
diff --git a/web/package.json b/web/package.json
index c977418d4..0715a816e 100644
--- a/web/package.json
+++ b/web/package.json
@@ -16,6 +16,7 @@
"watch": "run-s build-locales rollup:watch",
"lint": "eslint . --max-warnings 0 --fix",
"lit-analyse": "lit-analyzer src",
+ "precommit": "run-s tsc lit-analyse lint prettier",
"prettier-check": "prettier --check .",
"prettier": "prettier --write .",
"tsc:execute": "tsc --noEmit -p .",
@@ -33,6 +34,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.4.0",
"@fortawesome/fontawesome-free": "^6.4.0",
+ "@lit-labs/context": "^0.3.3",
"@goauthentik/api": "^2023.6.0-1688736781",
"@lit/localize": "^0.11.4",
"@patternfly/patternfly": "^4.224.2",
diff --git a/web/src/admin/AdminInterface.ts b/web/src/admin/AdminInterface.ts
index d467ad08a..d41af2692 100644
--- a/web/src/admin/AdminInterface.ts
+++ b/web/src/admin/AdminInterface.ts
@@ -7,10 +7,10 @@ import {
VERSION,
} from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry";
-import { autoDetectLanguage } from "@goauthentik/common/ui/locale";
import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base";
+import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/notifications/APIDrawer";
@@ -32,8 +32,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { AdminApi, CoreApi, SessionUser, UiThemeEnum, Version } from "@goauthentik/api";
-autoDetectLanguage();
-
@customElement("ak-interface-admin")
export class AdminInterface extends Interface {
@property({ type: Boolean })
@@ -114,54 +112,56 @@ export class AdminInterface extends Interface {
}
render(): TemplateResult {
- return html`
-
-
-
+
`;
}
renderSidebarItems(): TemplateResult {
diff --git a/web/src/common/api/config.ts b/web/src/common/api/config.ts
index 55a42c154..4e120a38e 100644
--- a/web/src/common/api/config.ts
+++ b/web/src/common/api/config.ts
@@ -3,9 +3,9 @@ import {
EventMiddleware,
LoggingMiddleware,
} from "@goauthentik/common/api/middleware";
-import { EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
+import { EVENT_LOCALE_REQUEST, EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
-import { activateLocale } from "@goauthentik/common/ui/locale";
+import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { Config, Configuration, CoreApi, CurrentTenant, RootApi } from "@goauthentik/api";
@@ -39,7 +39,7 @@ export function tenantSetLocale(tenant: CurrentTenant) {
return;
}
console.debug("authentik/locale: setting locale from tenant default");
- activateLocale(tenant.defaultLocale);
+ window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale: tenant.defaultLocale }));
}
let globalTenantPromise: Promise
| undefined = Promise.resolve(globalAK().tenant);
diff --git a/web/src/common/constants.ts b/web/src/common/constants.ts
index a0c4be949..20089142b 100644
--- a/web/src/common/constants.ts
+++ b/web/src/common/constants.ts
@@ -15,6 +15,7 @@ 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";
diff --git a/web/src/common/ui/locale/activateLocale.ts b/web/src/common/ui/locale/activateLocale.ts
deleted file mode 100644
index 5a9af5774..000000000
--- a/web/src/common/ui/locale/activateLocale.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { getLocale, setLocale } from "./configureLocale";
-import { getBestMatchLocale, localeFromUrl } from "./helpers";
-
-export function activateLocale(code: string) {
- const urlLocale = localeFromUrl("locale");
- if (urlLocale !== null && urlLocale !== "") {
- code = urlLocale;
- }
-
- const locale = getBestMatchLocale(code);
- if (!locale) {
- console.warn(`authentik/locale: failed to find locale for code ${code}`);
- return;
- }
-
- locale.locale().then(() => {
- console.debug(`authentik/locale: Loaded locale '${code}'`);
- if (getLocale() === code) {
- return;
- }
- console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`);
- setLocale(locale.code);
- });
-}
-
-export default activateLocale;
diff --git a/web/src/common/ui/locale/autodetectLanguage.ts b/web/src/common/ui/locale/autodetectLanguage.ts
deleted file mode 100644
index 302c7470d..000000000
--- a/web/src/common/ui/locale/autodetectLanguage.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { globalAK } from "@goauthentik/common/global";
-
-import { activateLocale } from "./activateLocale";
-import { setLocale } from "./configureLocale";
-import { DEFAULT_FALLBACK } from "./definitions";
-import { findSupportedLocale, localeFromUrl } from "./helpers";
-
-const isLocaleCandidate = (v: unknown): v is string => typeof v === "string" && v !== "";
-
-export function autoDetectLanguage(defaultLanguage = "en") {
- // Always load en locale at the start so we have something and don't error
- setLocale(defaultLanguage);
-
- // Get all locales we can, in order
- // - Global authentik settings (contains user settings)
- // - URL parameter
- // - Navigator
- // - Fallback (en)
-
- const localeCandidates: string[] = [
- globalAK()?.locale,
- localeFromUrl("locale"),
- window.navigator.language,
- DEFAULT_FALLBACK,
- ].filter(isLocaleCandidate);
-
- const firstSupportedLocale = findSupportedLocale(localeCandidates);
-
- if (!firstSupportedLocale) {
- console.debug(`authentik/locale: No locale for '${localeCandidates}', falling back to en`);
- activateLocale(defaultLanguage);
- return;
- }
-
- activateLocale(firstSupportedLocale.code);
-}
-
-export default autoDetectLanguage;
diff --git a/web/src/common/ui/locale/configureLocale.ts b/web/src/common/ui/locale/configureLocale.ts
deleted file mode 100644
index cd2e86e98..000000000
--- a/web/src/common/ui/locale/configureLocale.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { configureLocalization } from "@lit/localize";
-
-import { sourceLocale, targetLocales } from "../../../locale-codes";
-import { getBestMatchLocale } from "./helpers";
-
-export 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();
- },
-});
diff --git a/web/src/common/ui/locale/helpers.ts b/web/src/common/ui/locale/helpers.ts
deleted file mode 100644
index 89fd22cf3..000000000
--- a/web/src/common/ui/locale/helpers.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { LOCALES as RAW_LOCALES, enLocale } from "./definitions";
-import { AkLocale } from "./types";
-
-// 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,
-);
-
-// First attempt a precise match, then see if there's a precise match on the requested locale's
-// prefix, then find the *first* locale for which that locale's prefix matches the requested prefix.
-
-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
-// 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 localeFromUrl(param = "locale") {
- const url = new URL(window.location.href);
- return url.searchParams.get(param) || "";
-}
diff --git a/web/src/common/ui/locale/index.ts b/web/src/common/ui/locale/index.ts
deleted file mode 100644
index 30123df9e..000000000
--- a/web/src/common/ui/locale/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import activateLocale from "./activateLocale";
-import autoDetectLanguage from "./autodetectLanguage";
-import { getLocale, setLocale } from "./configureLocale";
-import { LOCALES } from "./helpers";
-
-export { LOCALES, getLocale, setLocale, activateLocale, autoDetectLanguage };
diff --git a/web/src/common/users.ts b/web/src/common/users.ts
index 994a09409..02e269895 100644
--- a/web/src/common/users.ts
+++ b/web/src/common/users.ts
@@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
-import { activateLocale } from "@goauthentik/common/ui/locale";
+import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
+import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api";
@@ -23,7 +24,7 @@ export function me(): Promise {
console.debug(
`authentik/locale: Activating user's configured locale '${locale}'`,
);
- activateLocale(locale);
+ window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale }));
}
return user;
})
diff --git a/web/src/elements/Base.ts b/web/src/elements/Base.ts
index f0e0b6108..3d38fc3ad 100644
--- a/web/src/elements/Base.ts
+++ b/web/src/elements/Base.ts
@@ -1,8 +1,9 @@
import { config, tenant } from "@goauthentik/common/api/config";
-import { EVENT_LOCALE_CHANGE, EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
+import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { adaptCSS } from "@goauthentik/common/utils";
+import { localized } from "@lit/localize";
import { LitElement } from "lit";
import { state } from "lit/decorators.js";
@@ -45,6 +46,7 @@ export interface AdoptedStyleSheetsElement {
const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
+@localized()
export class AKElement extends LitElement {
_mediaMatcher?: MediaQueryList;
_mediaMatcherHandler?: (ev?: MediaQueryListEvent) => void;
@@ -53,14 +55,9 @@ export class AKElement extends LitElement {
get activeTheme(): UiThemeEnum | undefined {
return this._activeTheme;
}
- private _handleLocaleChange: () => void;
constructor() {
super();
- this._handleLocaleChange = (() => {
- this.requestUpdate();
- }).bind(this);
- window.addEventListener(EVENT_LOCALE_CHANGE, this._handleLocaleChange);
}
protected createRenderRoot(): ShadowRoot | Element {
@@ -162,11 +159,6 @@ export class AKElement extends LitElement {
this._activeTheme = theme;
this.requestUpdate();
}
-
- disconnectedCallback() {
- super.disconnectedCallback();
- window.removeEventListener(EVENT_LOCALE_CHANGE, this._handleLocaleChange);
- }
}
export class Interface extends AKElement {
diff --git a/web/src/elements/ak-locale-context/ak-locale-context.stories.ts b/web/src/elements/ak-locale-context/ak-locale-context.stories.ts
new file mode 100644
index 000000000..8eaa91769
--- /dev/null
+++ b/web/src/elements/ak-locale-context/ak-locale-context.stories.ts
@@ -0,0 +1,51 @@
+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";
+
+export default {
+ title: "Elements / Shell / Locale Context",
+};
+
+@localized()
+@customElement("ak-locale-demo-component")
+export class AKLocaleDemoComponent extends LitElement {
+ render() {
+ return html`${msg("Everything is ok.")}`;
+ }
+}
+
+@localized()
+@customElement("ak-locale-sensitive-demo-component")
+export class AKLocaleSensitiveDemoComponent extends LitElement {
+ render() {
+ return html`${msg("Everything is ok.")}
`;
+ }
+}
+
+export const InFrench = () =>
+ html`
+
Everything is not ok.
+
`;
+
+export const SwitchingBackAndForth = () => {
+ let lang = "en";
+ window.setInterval(() => {
+ lang = lang === "en" ? "fr_FR" : "en";
+ window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale: lang }));
+ }, 1000);
+
+ return html``;
+};
diff --git a/web/src/elements/ak-locale-context/ak-locale-context.ts b/web/src/elements/ak-locale-context/ak-locale-context.ts
new file mode 100644
index 000000000..304ddacac
--- /dev/null
+++ b/web/src/elements/ak-locale-context/ak-locale-context.ts
@@ -0,0 +1,115 @@
+import { EVENT_LOCALE_CHANGE } from "@goauthentik/common/constants";
+import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
+import { customEvent, isCustomEvent } from "@goauthentik/elements/utils/customEvents";
+
+import { provide } from "@lit-labs/context";
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+
+import { initializeLocalization } from "./configureLocale";
+import type { LocaleGetter, LocaleSetter } from "./configureLocale";
+import locale from "./context";
+import {
+ DEFAULT_LOCALE,
+ autoDetectLanguage,
+ getBestMatchLocale,
+ localeCodeFromUrl,
+} from "./helpers";
+
+/**
+ * A component to manage your locale settings.
+ *
+ * ## Details
+ *
+ * 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
+ * appropriately. If that works, it sends off an event saying so.
+ *
+ * @element ak-locale-context
+ * @slot - The content which consumes this context
+ * @fires ak-locale-change - When a valid locale has been swapped in
+ */
+@customElement("ak-locale-context")
+export class LocaleContext extends LitElement {
+ /// @attribute The text representation of the current locale */
+ @provide({ context: locale })
+ @property({ attribute: true, type: String })
+ locale = DEFAULT_LOCALE;
+
+ /// @attribute The URL parameter to look for (if any)
+ @property({ attribute: true, type: String })
+ param = "locale";
+
+ getLocale: LocaleGetter;
+
+ setLocale: LocaleSetter;
+
+ constructor(code = DEFAULT_LOCALE) {
+ super();
+ this.notifyApplication = this.notifyApplication.bind(this);
+ this.updateLocaleHandler = this.updateLocaleHandler.bind(this);
+ try {
+ const [getLocale, setLocale] = initializeLocalization();
+ this.getLocale = getLocale;
+ this.setLocale = setLocale;
+ this.setLocale(code).then(() => {
+ window.setTimeout(this.notifyApplication, 0);
+ });
+ } catch (e) {
+ throw new Error(`Developer error: Must have only one locale context per session: ${e}`);
+ }
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ const localeRequest = autoDetectLanguage(this.locale);
+ this.updateLocale(localeRequest);
+ window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler);
+ }
+
+ disconnectedCallback() {
+ window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler);
+ super.disconnectedCallback();
+ }
+
+ updateLocaleHandler(ev: Event) {
+ if (!isCustomEvent(ev)) {
+ console.warn(`Received a non-custom event at EVENT_LOCALE_REQUEST: ${ev}`);
+ return;
+ }
+ console.log("Locale update request received.");
+ this.updateLocale(ev.detail.locale);
+ }
+
+ updateLocale(code: string) {
+ const urlCode = localeCodeFromUrl(this.param);
+ const requestedLocale = urlCode ? urlCode : code;
+ const locale = getBestMatchLocale(requestedLocale);
+ if (!locale) {
+ console.warn(`authentik/locale: failed to find locale for code ${code}`);
+ return;
+ }
+ locale.locale().then(() => {
+ console.debug(`authentik/locale: Loaded locale '${code}'`);
+ if (this.getLocale() === code) {
+ return;
+ }
+ console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`);
+ this.setLocale(locale.code).then(() => {
+ window.setTimeout(this.notifyApplication, 0);
+ });
+ });
+ }
+
+ 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));
+ }
+
+ render() {
+ return html``;
+ }
+}
+
+export default LocaleContext;
diff --git a/web/src/elements/ak-locale-context/configureLocale.ts b/web/src/elements/ak-locale-context/configureLocale.ts
new file mode 100644
index 000000000..21f244668
--- /dev/null
+++ b/web/src/elements/ak-locale-context/configureLocale.ts
@@ -0,0 +1,40 @@
+import { sourceLocale, targetLocales } from "@goauthentik/app/locale-codes";
+
+import { configureLocalization } from "@lit/localize";
+
+import { getBestMatchLocale } from "./helpers";
+
+type LocaleGetter = ReturnType["getLocale"];
+type LocaleSetter = ReturnType["setLocale"];
+
+// 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.
+
+let getLocale: LocaleGetter | undefined = undefined;
+let setLocale: LocaleSetter | undefined = undefined;
+
+export function initializeLocalization(): [LocaleGetter, LocaleSetter] {
+ if (getLocale && setLocale) {
+ return [getLocale, setLocale];
+ }
+
+ ({ 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();
+ },
+ }));
+
+ return [getLocale, setLocale];
+}
+
+export default initializeLocalization;
+export type { LocaleGetter, LocaleSetter };
diff --git a/web/src/elements/ak-locale-context/context.ts b/web/src/elements/ak-locale-context/context.ts
new file mode 100644
index 000000000..9c9903bb3
--- /dev/null
+++ b/web/src/elements/ak-locale-context/context.ts
@@ -0,0 +1,4 @@
+import { createContext } from "@lit-labs/context";
+
+export const localeContext = createContext("locale");
+export default localeContext;
diff --git a/web/src/common/ui/locale/definitions.ts b/web/src/elements/ak-locale-context/definitions.ts
similarity index 100%
rename from web/src/common/ui/locale/definitions.ts
rename to web/src/elements/ak-locale-context/definitions.ts
diff --git a/web/src/elements/ak-locale-context/helpers.ts b/web/src/elements/ak-locale-context/helpers.ts
new file mode 100644
index 000000000..51ef76509
--- /dev/null
+++ b/web/src/elements/ak-locale-context/helpers.ts
@@ -0,0 +1,69 @@
+import { globalAK } from "@goauthentik/common/global";
+
+import { LOCALES as RAW_LOCALES, enLocale } from "./definitions";
+import { AkLocale } from "./types";
+
+export const DEFAULT_LOCALE = "en";
+
+export const EVENT_REQUEST_LOCALE = "ak-request-locale";
+
+const TOMBSTONE = "⛼⛼tombstone⛼⛼";
+
+// 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));
+}
+
+// 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 localeCodeFromUrl(param = "locale") {
+ const url = new URL(window.location.href);
+ return url.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)
+
+const isLocaleCandidate = (v: unknown): v is string =>
+ typeof v === "string" && v !== "" && v !== TOMBSTONE;
+
+export function autoDetectLanguage(requestedCode?: string): string {
+ const localeCandidates: string[] = [
+ globalAK()?.locale ?? TOMBSTONE,
+ localeCodeFromUrl("locale"),
+ requestedCode ?? TOMBSTONE,
+ window.navigator?.language ?? TOMBSTONE,
+ DEFAULT_LOCALE,
+ ].filter(isLocaleCandidate);
+
+ const firstSupportedLocale = findSupportedLocale(localeCandidates);
+
+ if (!firstSupportedLocale) {
+ console.debug(
+ `authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`,
+ );
+ return DEFAULT_LOCALE;
+ }
+
+ return firstSupportedLocale.code;
+}
+
+export default autoDetectLanguage;
diff --git a/web/src/elements/ak-locale-context/index.ts b/web/src/elements/ak-locale-context/index.ts
new file mode 100644
index 000000000..1a264c6b3
--- /dev/null
+++ b/web/src/elements/ak-locale-context/index.ts
@@ -0,0 +1,4 @@
+import LocaleContext from "./ak-locale-context";
+
+export { LocaleContext };
+export default LocaleContext;
diff --git a/web/src/elements/ak-locale-context/types.ts b/web/src/elements/ak-locale-context/types.ts
new file mode 100644
index 000000000..55147d247
--- /dev/null
+++ b/web/src/elements/ak-locale-context/types.ts
@@ -0,0 +1,10 @@
+import type { LocaleModule } from "@lit/localize";
+
+export type LocaleRow = [string, RegExp, () => string, () => Promise];
+
+export type AkLocale = {
+ code: string;
+ match: RegExp;
+ label: () => string;
+ locale: () => Promise;
+};
diff --git a/web/src/elements/utils/customEvents.ts b/web/src/elements/utils/customEvents.ts
new file mode 100644
index 000000000..f0dab467a
--- /dev/null
+++ b/web/src/elements/utils/customEvents.ts
@@ -0,0 +1,13 @@
+export const customEvent = (name: string, details = {}) =>
+ new CustomEvent(name as string, {
+ composed: true,
+ bubbles: true,
+ detail: details,
+ });
+
+// "Unknown" seems to violate some obscure Typescript rule and doesn't work here, although it
+// should.
+//
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const isCustomEvent = (v: any): v is CustomEvent =>
+ v instanceof CustomEvent && "detail" in v;
diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts
index 49348b8e5..21121eac0 100644
--- a/web/src/flow/FlowExecutor.ts
+++ b/web/src/flow/FlowExecutor.ts
@@ -10,6 +10,7 @@ import { first } from "@goauthentik/common/utils";
import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/LoadingOverlay";
+import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/flow/stages/FlowErrorStage";
import "@goauthentik/flow/stages/RedirectStage";
import { StageHost } from "@goauthentik/flow/stages/base";
@@ -487,7 +488,8 @@ export class FlowExecutor extends Interface implements StageHost {
}
render(): TemplateResult {
- return html`${this.renderBackgroundOverlay()}
+ return html`
+ ${this.renderBackgroundOverlay()}
@@ -541,6 +543,7 @@ export class FlowExecutor extends Interface implements StageHost {
${until(this.renderInspector())}
-
`;
+
+ `;
}
}
diff --git a/web/src/flow/FlowInterface.ts b/web/src/flow/FlowInterface.ts
index c46396e26..f645023c0 100644
--- a/web/src/flow/FlowInterface.ts
+++ b/web/src/flow/FlowInterface.ts
@@ -1,4 +1,3 @@
-import { autoDetectLanguage } from "@goauthentik/common/ui/locale";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/flow/FlowExecutor";
// Statically import some stages to speed up load speed
@@ -14,5 +13,3 @@ import "@goauthentik/flow/stages/identification/IdentificationStage";
import "@goauthentik/flow/stages/password/PasswordStage";
// end of stage import
-
-autoDetectLanguage();
diff --git a/web/src/flow/stages/prompt/PromptStage.ts b/web/src/flow/stages/prompt/PromptStage.ts
index 1d074b2e0..1015737ed 100644
--- a/web/src/flow/stages/prompt/PromptStage.ts
+++ b/web/src/flow/stages/prompt/PromptStage.ts
@@ -1,7 +1,7 @@
-import { LOCALES } from "@goauthentik/common/ui/locale";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Divider";
import "@goauthentik/elements/EmptyState";
+import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions";
import "@goauthentik/elements/forms/FormElement";
import { BaseStage } from "@goauthentik/flow/stages/base";
diff --git a/web/src/standalone/api-browser/index.ts b/web/src/standalone/api-browser/index.ts
index 78c8d4c7c..b0c5849ed 100644
--- a/web/src/standalone/api-browser/index.ts
+++ b/web/src/standalone/api-browser/index.ts
@@ -1,9 +1,9 @@
import { CSRFHeaderName } from "@goauthentik/common/api/middleware";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
-import { autoDetectLanguage } from "@goauthentik/common/ui/locale";
import { first, getCookie } from "@goauthentik/common/utils";
import { Interface } from "@goauthentik/elements/Base";
+import "@goauthentik/elements/ak-locale-context";
import { DefaultTenant } from "@goauthentik/elements/sidebar/SidebarBrand";
import "rapidoc";
@@ -13,8 +13,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { UiThemeEnum } from "@goauthentik/api";
-autoDetectLanguage();
-
@customElement("ak-api-browser")
export class APIBrowser extends Interface {
@property()
@@ -66,45 +64,50 @@ export class APIBrowser extends Interface {
render(): TemplateResult {
return html`
-
,
- ) => {
- e.detail.request.headers.append(CSRFHeaderName, getCookie("authentik_csrf"));
- }}
- >
-
-
-
-
+
+ ,
+ ) => {
+ e.detail.request.headers.append(
+ CSRFHeaderName,
+ getCookie("authentik_csrf"),
+ );
+ }}
+ >
+
+
+
+
+
`;
}
}
diff --git a/web/src/standalone/loading/index.ts b/web/src/standalone/loading/index.ts
index 9832cf970..907a05140 100644
--- a/web/src/standalone/loading/index.ts
+++ b/web/src/standalone/loading/index.ts
@@ -1,5 +1,4 @@
import { globalAK } from "@goauthentik/common/global";
-import { autoDetectLanguage } from "@goauthentik/common/ui/locale";
import { Interface } from "@goauthentik/elements/Base";
import { msg } from "@lit/localize";
@@ -13,8 +12,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { UiThemeEnum } from "@goauthentik/api";
-autoDetectLanguage();
-
@customElement("ak-loading")
export class Loading extends Interface {
static get styles(): CSSResult[] {
diff --git a/web/src/user/UserInterface.ts b/web/src/user/UserInterface.ts
index c4cb19891..acd72e75f 100644
--- a/web/src/user/UserInterface.ts
+++ b/web/src/user/UserInterface.ts
@@ -6,12 +6,11 @@ import {
} from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry";
import { UserDisplay } from "@goauthentik/common/ui/config";
-import { autoDetectLanguage } from "@goauthentik/common/ui/locale";
import { me } from "@goauthentik/common/users";
import { first } from "@goauthentik/common/utils";
import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base";
-import "@goauthentik/elements/buttons/ActionButton";
+import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/notifications/APIDrawer";
import "@goauthentik/elements/notifications/NotificationDrawer";
@@ -36,9 +35,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
-import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api";
-
-autoDetectLanguage();
+import { EventsApi, SessionUser } from "@goauthentik/api";
@customElement("ak-interface-user")
export class UserInterface extends Interface {
@@ -150,157 +147,168 @@ export class UserInterface extends Interface {
default:
userDisplay = this.me.user.username;
}
- return html`