web: replace rollup with esbuild (#8699)
* Holding for a moment... * web: replace rollup with esbuild This commit replaces rollup with esbuild. The biggest fix was to alter the way CSS is imported into our system; esbuild delivers it to the browser as text, rather than as a bundle with metadata that, frankly, we never use. ESBuild will bundle the CSS for us just fine, and interpreting those strings *as* CSS turned out to be a small hurdle. Code has been added to AKElement and Interface to ensure that all CSS referenced by an element has been converted to a Browser CSSStyleSheet before being presented to the browser. A similar fix has been provided for the markdown imports. The biggest headache there was that the re-arrangement of our documentation broke Jen's existing parser for fixing relative links. I've provided a corresponding hack that provides the necessary detail, but since the Markdown is being presented to the browser as text, we have to provide a hint in the markdown component for where any relative links should go, and we're importing and processing the markdown at runtime. This doesn't seem to be a big performance hit. The entire build process is driven by the new build script, `build.mjs`, which starts the esbuild process as a service connected to the build script and then runs the commands sent to it as fast as possible. The biggest "hack" in it is actually the replacement for rollup's `rollup-copy-plugin`, which is clever enough I'm surprised it doesn't exist as a standalone file-copy package in its own right. I've also used a filesystem watch library to encode a "watcher" mechanism into the build script. `node build.mjs --watch` will work on MacOS; I haven't tested it elsewhere, at least not yet. `node build.mjs --proxy` does what the old rollup.proxy.js script did. The savings are substantial. It takes less than two seconds to build the whole UI, a huge savings off the older ~45-50 seconds I routinely saw on my old Mac. It's also about 9% smaller. The trade-offs appear to be small: processing the CSS as StyleSheets, and the Markdown as HTML, at run-time is a small performance hit, but I didn't notice it in amongst everything else the UI does as it starts up. Manual chunking is gone; esbuild's support for that is quite difficult to get right compared to Rollup's, although there's been a bit of yelling at ESbuild over it. Codemirror is built into its own chunk; it's just not _named_ distinctly anymore. The one thing I haven't been able to test yet is whether or not the polyfills and runtim shims work as expected on older browsers. * web: continue with performance and build fixes This commit introduces a couple of fixes enabled by esbuild and other features. 1. build-locales `build-locales` is a new NodeJS script in the `./scripts` folder that does pretty much what it says in the name: it translates Xliff files into `.ts` files. It has two DevExp advantages over the old build system. First, it will check the build times of the xlf files and their ts equivalents, and will only run the actual build-locales command if the XLF files are newer than their TS equivalents. Second, it captures the stderr output from the build-locales command and summarizes it. Instead of the thousands of lines of "this string has no translation equivalent," now it just reports the number of missed translations per locale. 2. check-spelling This is a simple wrapper around the `codespell` command, mostly just to reduce the visual clutter of `package.json`, but also to permit it to run just about anywhere without needed hard-coded paths to the dictionaries, using a fairly classic trick with git. 3. pseudolocalize and import-maps These scripts were in TypeScript, but for our purposes I've saved their constructed equivalents instead. This saves on visual clutter in the `package.json` script, and reduced the time they have to run during full builds. They're small enough I feel confident they won't need too much looking over. Also, two lint bugs in Markdown.ts have been fixed. * Removed a few lines that weren't in use. * build-locales was sufficiently complex it needed some comments. * web: formalize that horrible unixy git status checker into a proper function. * Added types for , the Markdown processor for in-line documentation. * re-add dependencies required for storybook Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix optional deps Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix relative links for docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only build once on startup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * prevent crash when build fails in watch mode, improve console output Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
2
web/.gitignore
vendored
2
web/.gitignore
vendored
@ -109,5 +109,3 @@ temp/
|
||||
# End of https://www.gitignore.io/api/node
|
||||
api/**
|
||||
storybook-static/
|
||||
scripts/*.mjs
|
||||
scripts/*.js
|
||||
|
147
web/build.mjs
Normal file
147
web/build.mjs
Normal file
@ -0,0 +1,147 @@
|
||||
import * as chokidar from "chokidar";
|
||||
import esbuild from "esbuild";
|
||||
import fs from "fs";
|
||||
import { globSync } from "glob";
|
||||
import path from "path";
|
||||
import { cwd } from "process";
|
||||
import process from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const isProdBuild = process.env.NODE_ENV === "production";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const apiBasePath = process.env.AK_API_BASE_PATH || "";
|
||||
|
||||
const definitions = {
|
||||
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
||||
"process.env.CWD": JSON.stringify(cwd()),
|
||||
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
|
||||
};
|
||||
|
||||
// All is magic is just to make sure the assets are copied into the right places. This is a very stripped down version
|
||||
// of what the rollup-copy-plugin does, without any of the features we don't use, and using globSync instead of globby
|
||||
// since we already had globSync lying around thanks to Typescript. If there's a third argument in an array entry, it's
|
||||
// used to replace the internal path before concatenating it all together as the destination target.
|
||||
|
||||
const otherFiles = [
|
||||
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
|
||||
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
|
||||
["src/custom.css", "."],
|
||||
["src/common/styles/**", "."],
|
||||
["src/assets/images/**", "./assets/images"],
|
||||
["./icons/*", "./assets/icons"],
|
||||
];
|
||||
|
||||
const isFile = (filePath) => fs.statSync(filePath).isFile();
|
||||
function nameCopyTarget(src, dest, strip) {
|
||||
const target = path.join(dest, strip ? src.replace(strip, "") : path.parse(src).base);
|
||||
return [src, target];
|
||||
}
|
||||
|
||||
for (const [source, rawdest, strip] of otherFiles) {
|
||||
const matchedPaths = globSync(source);
|
||||
const dest = path.join("dist", rawdest);
|
||||
const copyTargets = matchedPaths.map((path) => nameCopyTarget(path, dest, strip));
|
||||
for (const [src, dest] of copyTargets) {
|
||||
if (isFile(src)) {
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This starts the definitions used for esbuild: Our targets, our arguments, the function for running a build, and three
|
||||
// options for building: watching, building, and building the proxy.
|
||||
|
||||
const interfaces = [
|
||||
["polyfill/poly.ts", "."],
|
||||
["standalone/loading/index.ts", "standalone/loading"],
|
||||
["flow/FlowInterface.ts", "flow"],
|
||||
["user/UserInterface.ts", "user"],
|
||||
["enterprise/rac/index.ts", "enterprise/rac"],
|
||||
["standalone/api-browser/index.ts", "standalone/api-browser"],
|
||||
["admin/AdminInterface/AdminInterface.ts", "admin"],
|
||||
];
|
||||
|
||||
const baseArgs = {
|
||||
bundle: true,
|
||||
write: true,
|
||||
sourcemap: !isProdBuild,
|
||||
minify: isProdBuild,
|
||||
splitting: true,
|
||||
treeShaking: true,
|
||||
external: ["*.woff", "*.woff2"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
loader: { ".css": "text", ".md": "text" },
|
||||
define: definitions,
|
||||
format: "esm",
|
||||
};
|
||||
|
||||
function buildAuthentik(interfaces) {
|
||||
const start = Date.now();
|
||||
console.clear();
|
||||
for (const [source, dest] of interfaces) {
|
||||
const DIST = path.join(__dirname, "./dist", dest);
|
||||
console.log(`[${new Date(start).toISOString()}] Starting build for target ${source}`);
|
||||
try {
|
||||
esbuild.buildSync({
|
||||
...baseArgs,
|
||||
entryPoints: [`./src/${source}`],
|
||||
outdir: DIST,
|
||||
});
|
||||
} catch (exc) {
|
||||
console.error(
|
||||
`[${new Date(Date.now()).toISOString()}] Failed to build ${source}: ${exc}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const end = Date.now();
|
||||
console.log(
|
||||
`[${new Date(end).toISOString()}] Finished build for target ${source} in ${Date.now() - start}ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let timeoutId = null;
|
||||
function debouncedBuild() {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
buildAuthentik(interfaces);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === "--help")) {
|
||||
console.log(`Build the authentikUI
|
||||
|
||||
options:
|
||||
-w, --watch: Build all ${interfaces.length} interfaces
|
||||
-p, --proxy: Build only the polyfills and the loading application
|
||||
-h, --help: This help message
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] === "--watch")) {
|
||||
console.log("Watching ./src for changes");
|
||||
chokidar.watch("./src").on("all", (event, path) => {
|
||||
if (!["add", "change", "unlink"].includes(event)) {
|
||||
return;
|
||||
}
|
||||
if (!/(\.css|\.ts|\.js)$/.test(path)) {
|
||||
return;
|
||||
}
|
||||
debouncedBuild();
|
||||
});
|
||||
} else if (process.argv.length > 2 && (process.argv[2] === "-p" || process.argv[2] === "--proxy")) {
|
||||
// There's no watch-for-proxy, sorry.
|
||||
buildAuthentik(interfaces.slice(0, 2));
|
||||
process.exit(0);
|
||||
} else {
|
||||
// And the fallback: just build it.
|
||||
buildAuthentik(interfaces);
|
||||
}
|
6296
web/package-lock.json
generated
6296
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,33 +5,29 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"extract-locales": "lit-localize extract",
|
||||
"build-locales": "run-s build-locales:build",
|
||||
"build-locales": "node scripts/build-locales.mjs",
|
||||
"build-locales:build": "lit-localize build",
|
||||
"build-locales:repair": "prettier --write ./src/locale-codes.ts",
|
||||
"rollup:build": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c ./rollup.config.mjs",
|
||||
"rollup:build-proxy": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c ./rollup.proxy.mjs",
|
||||
"rollup:watch": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c -w",
|
||||
"build": "run-s build-locales rollup:build",
|
||||
"build-proxy": "run-s build-locales rollup:build-proxy",
|
||||
"watch": "run-s build-locales rollup:watch",
|
||||
"esbuild:build": "node build.mjs",
|
||||
"esbuild:build-proxy": "node build.mjs --proxy",
|
||||
"esbuild:watch": "node build.mjs --watch",
|
||||
"build": "run-s build-locales esbuild:build",
|
||||
"build-proxy": "run-s build-locales esbuild:build-proxy",
|
||||
"watch": "run-s build-locales esbuild:watch",
|
||||
"lint": "eslint . --max-warnings 0 --fix",
|
||||
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[M?][M?]' | cut -c8- | grep -E '\\.(ts|js|tsx|jsx)$') ",
|
||||
"lint:spelling": "codespell -D - -D ../.github/codespell-dictionary.txt -I ../.github/codespell-words.txt -S './src/locales/**' ./src -s",
|
||||
"lint:precommit": "node scripts/eslint-precommit.mjs",
|
||||
"lint:spelling": "node scripts/check-spelling.mjs",
|
||||
"lit-analyse": "lit-analyzer src",
|
||||
"precommit": "run-s tsc lit-analyse lint:precommit lint:spelling prettier",
|
||||
"precommit": "npm-run-all --parallel tsc lit-analyse lint:spelling --sequential lint:precommit prettier",
|
||||
"prequick": "run-s tsc:execute lit-analyse lint:precommit lint:spelling",
|
||||
"prettier-check": "prettier --check .",
|
||||
"prettier": "prettier --write .",
|
||||
"pseudolocalize:build-extract-script": "cd scripts && tsc --esModuleInterop --module es2020 --moduleResolution 'node' pseudolocalize.ts && mv pseudolocalize.js pseudolocalize.mjs",
|
||||
"pseudolocalize:extract": "node scripts/pseudolocalize.mjs",
|
||||
"pseudolocalize": "run-s pseudolocalize:build-extract-script pseudolocalize:extract",
|
||||
"pseudolocalize": "node scripts/pseudolocalize.mjs",
|
||||
"tsc:execute": "tsc --noEmit -p .",
|
||||
"tsc": "run-s build-locales tsc:execute",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "cross-env NODE_OPTIONS='--max_old_space_size=8192' storybook build",
|
||||
"storybook:build-import-map": "run-s storybook:build-import-map-script storybook:run-import-map-script",
|
||||
"storybook:build-import-map-script": "cd scripts && tsc --esModuleInterop --module es2020 --target es2020 --moduleResolution 'node' build-storybook-import-maps.ts && mv build-storybook-import-maps.js build-storybook-import-maps.mjs",
|
||||
"storybook:run-import-map-script": "node scripts/build-storybook-import-maps.mjs"
|
||||
"storybook:build-import-map": "node scripts/build-storybook-import-maps.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.8",
|
||||
@ -61,8 +57,10 @@
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^2.8.0",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^10.9.0",
|
||||
"rapidoc": "^9.3.4",
|
||||
"showdown": "^2.1.0",
|
||||
"style-mod": "^4.1.2",
|
||||
"ts-pattern": "^5.0.6",
|
||||
"webcomponent-qr-code": "^1.2.0",
|
||||
@ -78,16 +76,9 @@
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@hcaptcha/types": "^1.0.3",
|
||||
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
|
||||
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
|
||||
"@lit/localize-tools": "^0.7.2",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@spotlightjs/spotlight": "^1.2.13",
|
||||
"@spotlightjs/spotlight": "^1.2.12",
|
||||
"@storybook/addon-essentials": "^7.6.17",
|
||||
"@storybook/addon-links": "^7.6.17",
|
||||
"@storybook/api": "^7.6.17",
|
||||
@ -100,11 +91,17 @@
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/grecaptcha": "^3.0.8",
|
||||
"@types/guacamole-common-js": "1.5.2",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||
"@typescript-eslint/parser": "^7.1.1",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"rollup-plugin-modify": "^3.0.0",
|
||||
"rollup-plugin-postcss-lit": "^2.1.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-tsconfig-paths": "^1.0.3",
|
||||
"chokidar": "^3.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.20.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.8",
|
||||
@ -112,17 +109,13 @@
|
||||
"eslint-plugin-sonarjs": "^0.24.0",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^10.3.10",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.2.5",
|
||||
"pseudolocale": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"rollup": "^4.12.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"rollup-plugin-cssimport": "^1.0.3",
|
||||
"rollup-plugin-modify": "^3.0.0",
|
||||
"rollup-plugin-postcss-lit": "^2.1.0",
|
||||
"storybook": "^7.6.17",
|
||||
"storybook-addon-mock": "^4.3.0",
|
||||
"ts-lit-plugin": "^2.0.2",
|
||||
@ -134,7 +127,10 @@
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.20.1",
|
||||
"@esbuild/linux-amd64": "^0.18.11",
|
||||
"@esbuild/linux-arm64": "^0.20.1"
|
||||
"@esbuild/linux-arm64": "^0.20.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.12.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.12.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
@ -1,192 +0,0 @@
|
||||
import markdown from "@jackfranklin/rollup-plugin-markdown";
|
||||
import babel from "@rollup/plugin-babel";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||
import replace from "@rollup/plugin-replace";
|
||||
import terser from "@rollup/plugin-terser";
|
||||
import { cwd } from "process";
|
||||
import copy from "rollup-plugin-copy";
|
||||
import cssimport from "rollup-plugin-cssimport";
|
||||
|
||||
// https://github.com/d3/d3-interpolate/issues/58
|
||||
const IGNORED_WARNINGS = /Circular dependency(.*d3-[interpolate|selection])|(.*@lit\/localize.*)/;
|
||||
|
||||
const extensions = [".js", ".jsx", ".ts", ".tsx"];
|
||||
|
||||
export const resources = [
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/patternfly.min.css",
|
||||
dest: "dist/",
|
||||
},
|
||||
{ src: "src/common/styles/*", dest: "dist/" },
|
||||
{ src: "src/custom.css", dest: "dist/" },
|
||||
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/assets/*",
|
||||
dest: "dist/assets/",
|
||||
},
|
||||
{ src: "src/assets/*", dest: "dist/assets" },
|
||||
{ src: "./icons/*", dest: "dist/assets/icons" },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
export const isProdBuild = process.env.NODE_ENV === "production";
|
||||
// eslint-disable-next-line no-undef
|
||||
export const apiBasePath = process.env.AK_API_BASE_PATH || "";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function manualChunks(id) {
|
||||
if (id.endsWith(".md")) {
|
||||
return "docs";
|
||||
}
|
||||
if (id.includes("@goauthentik/api")) {
|
||||
return "api";
|
||||
}
|
||||
if (id.includes("locales")) {
|
||||
const parts = id.split("/");
|
||||
const file = parts[parts.length - 1];
|
||||
return "locale-" + file.replace(".ts", "");
|
||||
}
|
||||
if (id.includes("node_modules")) {
|
||||
if (id.includes("codemirror")) {
|
||||
return "vendor-cm";
|
||||
}
|
||||
return "vendor";
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultOptions = {
|
||||
plugins: [
|
||||
cssimport(),
|
||||
markdown(),
|
||||
nodeResolve({ extensions, browser: true }),
|
||||
commonjs(),
|
||||
babel({
|
||||
extensions,
|
||||
babelHelpers: "runtime",
|
||||
include: ["src/**/*"],
|
||||
}),
|
||||
replace({
|
||||
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
||||
"process.env.CWD": JSON.stringify(cwd()),
|
||||
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
|
||||
"preventAssignment": true,
|
||||
}),
|
||||
isProdBuild && terser(),
|
||||
].filter((p) => p),
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
preserveEntrySignatures: "strict",
|
||||
cache: true,
|
||||
context: "window",
|
||||
onwarn: function (warning, warn) {
|
||||
if (IGNORED_WARNINGS.test(warning)) {
|
||||
return;
|
||||
}
|
||||
if (warning.code === "UNRESOLVED_IMPORT") {
|
||||
throw Object.assign(new Error(), warning);
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
};
|
||||
|
||||
// Polyfills (imported first)
|
||||
export const POLY = {
|
||||
input: "./src/polyfill/poly.ts",
|
||||
output: [
|
||||
{
|
||||
format: "iife",
|
||||
file: "dist/poly.js",
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
cache: true,
|
||||
plugins: [
|
||||
cssimport(),
|
||||
nodeResolve({ browser: true }),
|
||||
commonjs(),
|
||||
isProdBuild && terser(),
|
||||
copy({
|
||||
targets: [...resources],
|
||||
copyOnce: false,
|
||||
}),
|
||||
].filter((p) => p),
|
||||
};
|
||||
|
||||
export const standalone = ["api-browser", "loading"].map((input) => {
|
||||
return {
|
||||
input: `./src/standalone/${input}`,
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
dir: `dist/standalone/${input}`,
|
||||
sourcemap: true,
|
||||
manualChunks: manualChunks,
|
||||
},
|
||||
],
|
||||
...defaultOptions,
|
||||
};
|
||||
});
|
||||
|
||||
export const enterprise = ["rac"].map((input) => {
|
||||
return {
|
||||
input: `./src/enterprise/${input}`,
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
dir: `dist/enterprise/${input}`,
|
||||
sourcemap: true,
|
||||
manualChunks: manualChunks,
|
||||
},
|
||||
],
|
||||
...defaultOptions,
|
||||
};
|
||||
});
|
||||
|
||||
export default [
|
||||
POLY,
|
||||
// Standalone
|
||||
...standalone,
|
||||
// Flow interface
|
||||
{
|
||||
input: "./src/flow/FlowInterface.ts",
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
dir: "dist/flow",
|
||||
sourcemap: true,
|
||||
manualChunks: manualChunks,
|
||||
},
|
||||
],
|
||||
...defaultOptions,
|
||||
},
|
||||
// Admin interface
|
||||
{
|
||||
input: "./src/admin/AdminInterface/AdminInterface.ts",
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
dir: "dist/admin",
|
||||
sourcemap: true,
|
||||
manualChunks: manualChunks,
|
||||
},
|
||||
],
|
||||
...defaultOptions,
|
||||
},
|
||||
// User interface
|
||||
{
|
||||
input: "./src/user/UserInterface.ts",
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
dir: "dist/user",
|
||||
sourcemap: true,
|
||||
manualChunks: manualChunks,
|
||||
},
|
||||
],
|
||||
...defaultOptions,
|
||||
},
|
||||
// Enterprise
|
||||
...enterprise,
|
||||
];
|
@ -1,3 +0,0 @@
|
||||
import { POLY, standalone } from "./rollup.config.mjs";
|
||||
|
||||
export default [POLY, ...standalone];
|
67
web/scripts/build-locales.mjs
Normal file
67
web/scripts/build-locales.mjs
Normal file
@ -0,0 +1,67 @@
|
||||
import { spawnSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
|
||||
const localizeRules = JSON.parse(fs.readFileSync("./lit-localize.json", "utf-8"));
|
||||
|
||||
function compareXlfAndSrc(loc) {
|
||||
const xlf = path.join("./xliff", `${loc}.xlf`);
|
||||
const src = path.join("./src/locales", `${loc}.ts`);
|
||||
|
||||
// Returns false if: the expected XLF file doesn't exist, The expected
|
||||
// generated file doesn't exist, or the XLF file is newer (has a higher date)
|
||||
// than the generated file. The missing XLF file is important enough it
|
||||
// generates a unique error message and halts the build.
|
||||
|
||||
try {
|
||||
var xlfStat = fs.statSync(xlf);
|
||||
} catch (_error) {
|
||||
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
var srcStat = fs.statSync(src);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the xlf is newer (greater) than src, it's out of date.
|
||||
if (xlfStat.mtimeMs > srcStat.mtimeMs) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// For all the expected files, find out if any aren't up-to-date.
|
||||
|
||||
const upToDate = localizeRules.targetLocales.reduce(
|
||||
(acc, loc) => acc && compareXlfAndSrc(loc),
|
||||
true,
|
||||
);
|
||||
|
||||
if (!upToDate) {
|
||||
const status = spawnSync("npm", ["run", "build-locales:build"], { encoding: "utf8" });
|
||||
|
||||
// Count all the missing message warnings
|
||||
const counts = status.stderr.split("\n").reduce((acc, line) => {
|
||||
const match = /^([\w-]+) message/.exec(line);
|
||||
if (!match) {
|
||||
return acc;
|
||||
}
|
||||
acc.set(match[1], (acc.get(match[1]) || 0) + 1);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const locales = Array.from(counts.keys());
|
||||
locales.sort();
|
||||
|
||||
const report = locales
|
||||
.map((locale) => `Locale '${locale}' has ${counts.get(locale)} missing translations`)
|
||||
.join("\n");
|
||||
|
||||
console.log(`Translation tables rebuilt.\n${report}\n`);
|
||||
}
|
||||
|
||||
console.log("Locale ./src is up-to-date");
|
@ -5,13 +5,12 @@ import { fileURLToPath } from "url";
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function* walkFilesystem(dir: string): Generator<string, undefined, any> {
|
||||
function* walkFilesystem(dir) {
|
||||
const openeddir = fs.opendirSync(dir);
|
||||
if (!openeddir) {
|
||||
return;
|
||||
}
|
||||
|
||||
let d: fs.Dirent | null;
|
||||
let d;
|
||||
while ((d = openeddir?.readSync())) {
|
||||
if (!d) {
|
||||
break;
|
||||
@ -24,13 +23,14 @@ function* walkFilesystem(dir: string): Generator<string, undefined, any> {
|
||||
}
|
||||
|
||||
const import_re = /^(import \w+ from .*\.css)";/;
|
||||
function extractImportLinesFromFile(path: string) {
|
||||
|
||||
function extractImportLinesFromFile(path) {
|
||||
const source = fs.readFileSync(path, { encoding: "utf8", flag: "r" });
|
||||
const lines = source?.split("\n") ?? [];
|
||||
return lines.filter((l) => import_re.test(l));
|
||||
}
|
||||
|
||||
function createOneImportLine(line: string) {
|
||||
function createOneImportLine(line) {
|
||||
const importMatch = import_re.exec(line);
|
||||
if (!importMatch) {
|
||||
throw new Error("How did an unmatchable line get here?");
|
||||
@ -43,15 +43,16 @@ function createOneImportLine(line: string) {
|
||||
}
|
||||
|
||||
const isSourceFile = /\.ts$/;
|
||||
|
||||
function getTheSourceFiles() {
|
||||
return Array.from(walkFilesystem(path.join(__dirname, "..", "src"))).filter((path) =>
|
||||
isSourceFile.test(path),
|
||||
);
|
||||
}
|
||||
|
||||
function getTheImportLines(importPaths: string[]) {
|
||||
const importLines: string[] = importPaths.reduce(
|
||||
(acc: string[], path) => [...acc, extractImportLinesFromFile(path)].flat(),
|
||||
function getTheImportLines(importPaths) {
|
||||
const importLines = importPaths.reduce(
|
||||
(acc, path) => [...acc, extractImportLinesFromFile(path)].flat(),
|
||||
[],
|
||||
);
|
||||
const uniqueImportLines = new Set(importLines);
|
15
web/scripts/check-spelling.mjs
Normal file
15
web/scripts/check-spelling.mjs
Normal file
@ -0,0 +1,15 @@
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
|
||||
const projectRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).replace(
|
||||
"\n",
|
||||
"",
|
||||
);
|
||||
const cmd = [
|
||||
"codespell -D -",
|
||||
`-D ${path.join(projectRoot, ".github/codespell-dictionary.txt")}`,
|
||||
`-I ${path.join(projectRoot, ".github/codespell-words.txt")}`,
|
||||
"-S './src/locales/**' ./src -s",
|
||||
].join(" ");
|
||||
|
||||
console.log(execSync(cmd, { encoding: "utf8" }));
|
76
web/scripts/eslint-precommit.mjs
Normal file
76
web/scripts/eslint-precommit.mjs
Normal file
@ -0,0 +1,76 @@
|
||||
import { execFileSync } from "child_process";
|
||||
import { ESLint } from "eslint";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
|
||||
// Code assumes this script is in the './web/scripts' folder.
|
||||
const projectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||
encoding: "utf8",
|
||||
}).replace("\n", "");
|
||||
process.chdir(path.join(projectRoot, "./web"));
|
||||
|
||||
const eslintConfig = {
|
||||
overrideConfig: {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:lit/recommended",
|
||||
"plugin:custom-elements/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
"plugin:sonarjs/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint", "lit", "custom-elements", "sonarjs"],
|
||||
rules: {
|
||||
"indent": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "double", { avoidEscape: true }],
|
||||
"semi": ["error", "always"],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"sonarjs/cognitive-complexity": ["error", 9],
|
||||
"sonarjs/no-duplicate-string": "off",
|
||||
"sonarjs/no-nested-template-literals": "off",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const porcelainV1 = /^(..)\s+(.*$)/;
|
||||
const gitStatus = execFileSync("git", ["status", "--porcelain", "."], { encoding: "utf8" });
|
||||
|
||||
const statuses = gitStatus.split("\n").reduce((acc, line) => {
|
||||
const match = porcelainV1.exec(line.replace("\n"));
|
||||
if (!match) {
|
||||
return acc;
|
||||
}
|
||||
const [status, path] = Array.from(match).slice(1, 3);
|
||||
return [...acc, [status, path.split("\x00")[0]]];
|
||||
}, []);
|
||||
|
||||
const isModified = /^(M|\?|\s)(M|\?|\s)/;
|
||||
const modified = (s) => isModified.test(s);
|
||||
|
||||
const isCheckable = /\.(ts|js|mjs)$/;
|
||||
const checkable = (s) => isCheckable.test(s);
|
||||
|
||||
const updated = statuses.reduce(
|
||||
(acc, [status, filename]) =>
|
||||
modified(status) && checkable(filename) ? [...acc, path.join(projectRoot, filename)] : acc,
|
||||
[],
|
||||
);
|
||||
|
||||
const eslint = new ESLint(eslintConfig);
|
||||
const results = await eslint.lintFiles(updated);
|
||||
const formatter = await eslint.loadFormatter("stylish");
|
||||
const resultText = formatter.format(results);
|
||||
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
|
||||
|
||||
console.log(resultText);
|
||||
process.exit(errors > 1 ? 1 : 0);
|
@ -4,16 +4,12 @@ import pseudolocale from "pseudolocale";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js";
|
||||
import type { Message, ProgramMessage } from "@lit/localize-tools/lib/messages.d.ts";
|
||||
import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js";
|
||||
import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js";
|
||||
import type { Config } from "@lit/localize-tools/lib/types/config.d.ts";
|
||||
import type { Locale } from "@lit/localize-tools/lib/types/locale.d.ts";
|
||||
import type { TransformOutputConfig } from "@lit/localize-tools/lib/types/modes.d.ts";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const pseudoLocale: Locale = "pseudo-LOCALE" as Locale;
|
||||
const targetLocales: Locale[] = [pseudoLocale];
|
||||
const pseudoLocale = "pseudo-LOCALE";
|
||||
const targetLocales = [pseudoLocale];
|
||||
const baseConfig = JSON.parse(readFileSync(path.join(__dirname, "../lit-localize.json"), "utf-8"));
|
||||
|
||||
// Need to make some internal specifications to satisfy the transformer. It doesn't actually matter
|
||||
@ -21,27 +17,28 @@ const baseConfig = JSON.parse(readFileSync(path.join(__dirname, "../lit-localize
|
||||
// is in their common parent class, but I had to pick one. Everything else here is just pure
|
||||
// exploitation of the lit/localize-tools internals.
|
||||
|
||||
const config: Config = {
|
||||
const config = {
|
||||
...baseConfig,
|
||||
baseDir: path.join(__dirname, ".."),
|
||||
targetLocales,
|
||||
output: {
|
||||
...baseConfig,
|
||||
...baseConfig.output,
|
||||
mode: "transform",
|
||||
},
|
||||
resolve: (path: string) => path,
|
||||
} as Config;
|
||||
resolve: (path) => path,
|
||||
};
|
||||
|
||||
const pseudoMessagify = (message: ProgramMessage) => ({
|
||||
const pseudoMessagify = (message) => ({
|
||||
name: message.name,
|
||||
contents: message.contents.map((content) =>
|
||||
typeof content === "string" ? pseudolocale(content, { prepend: "", append: "" }) : content,
|
||||
),
|
||||
});
|
||||
|
||||
const localizer = new TransformLitLocalizer(config as Config & { output: TransformOutputConfig });
|
||||
const { messages } = localizer.extractSourceMessages();
|
||||
const localizer = new TransformLitLocalizer(config);
|
||||
const messages = localizer.extractSourceMessages().messages;
|
||||
const translations = messages.map(pseudoMessagify);
|
||||
const sorted = sortProgramMessages([...messages]);
|
||||
const formatter = makeFormatter(config);
|
||||
formatter.writeOutput(sorted, new Map<Locale, Message[]>([[pseudoLocale, translations]]));
|
||||
|
||||
formatter.writeOutput(sorted, new Map([[pseudoLocale, translations]]));
|
@ -93,12 +93,10 @@ export class ApplicationListPage extends TablePage<Application> {
|
||||
}
|
||||
|
||||
renderSidebarAfter(): TemplateResult {
|
||||
// Rendering the wizard with .open here, as if we set the attribute in
|
||||
// renderObjectCreate() it'll open two wizards, since that function gets called twice
|
||||
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown .md=${MDApplication}></ak-markdown>
|
||||
<ak-markdown .md=${MDApplication} meta="applications/index.md"></ak-markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -364,6 +364,8 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
},
|
||||
]}
|
||||
.md=${MDProviderOAuth2}
|
||||
meta="providers/oauth2/index.md"
|
||||
;
|
||||
></ak-markdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -121,30 +121,37 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
{
|
||||
label: msg("Nginx (Ingress)"),
|
||||
md: MDNginxIngress,
|
||||
meta: "providers/proxy/_nginx_ingress.md",
|
||||
},
|
||||
{
|
||||
label: msg("Nginx (Proxy Manager)"),
|
||||
md: MDNginxPM,
|
||||
meta: "providers/proxy/_nginx_proxy_manager.md",
|
||||
},
|
||||
{
|
||||
label: msg("Nginx (standalone)"),
|
||||
md: MDNginxStandalone,
|
||||
meta: "providers/proxy/_nginx_standalone.md",
|
||||
},
|
||||
{
|
||||
label: msg("Traefik (Ingress)"),
|
||||
md: MDTraefikIngress,
|
||||
meta: "providers/proxy/_traefik_ingress.md",
|
||||
},
|
||||
{
|
||||
label: msg("Traefik (Compose)"),
|
||||
md: MDTraefikCompose,
|
||||
meta: "providers/proxy/_traefik_compose.md",
|
||||
},
|
||||
{
|
||||
label: msg("Traefik (Standalone)"),
|
||||
md: MDTraefikStandalone,
|
||||
meta: "providers/proxy/_traefik_standalone.md",
|
||||
},
|
||||
{
|
||||
label: msg("Caddy (Standalone)"),
|
||||
md: MDCaddyStandalone,
|
||||
meta: "providers/proxy/_caddy_standalone.md",
|
||||
},
|
||||
];
|
||||
const replacers: Replacer[] = [
|
||||
@ -182,7 +189,11 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
data-tab-title="${server.label}"
|
||||
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
||||
>
|
||||
<ak-markdown .replacers=${replacers} .md=${server.md}></ak-markdown>
|
||||
<ak-markdown
|
||||
.replacers=${replacers}
|
||||
.md=${server.md}
|
||||
meta=${server.meta}
|
||||
></ak-markdown>
|
||||
</section>`;
|
||||
})}</ak-tabs
|
||||
>`;
|
||||
@ -248,7 +259,10 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown .md=${MDHeaderAuthentication}></ak-markdown>
|
||||
<ak-markdown
|
||||
.md=${MDHeaderAuthentication}
|
||||
meta="proxy/header_authentication.md"
|
||||
></ak-markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -249,7 +249,10 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-5-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown .md=${MDSCIMProvider}></ak-markdown>
|
||||
<ak-markdown
|
||||
.md=${MDSCIMProvider}
|
||||
meta="providers/scim/index.md"
|
||||
></ak-markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -4,7 +4,7 @@ import { adaptCSS } from "@goauthentik/common/utils";
|
||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
||||
|
||||
import { localized } from "@lit/localize";
|
||||
import { LitElement } from "lit";
|
||||
import { LitElement, ReactiveElement } from "lit";
|
||||
|
||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||
import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
|
||||
@ -60,6 +60,7 @@ export class AKElement extends LitElement {
|
||||
}
|
||||
|
||||
protected createRenderRoot(): ShadowRoot | Element {
|
||||
this.fixElementStyles();
|
||||
const root = super.createRenderRoot() as ShadowRoot;
|
||||
let styleRoot: AdoptedStyleSheetsElement = root;
|
||||
if ("ShadyDOM" in window) {
|
||||
@ -78,6 +79,13 @@ export class AKElement extends LitElement {
|
||||
return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
|
||||
}
|
||||
|
||||
fixElementStyles() {
|
||||
// Ensure all style sheets being passed are really style sheets.
|
||||
(this.constructor as typeof ReactiveElement).elementStyles = (
|
||||
this.constructor as typeof ReactiveElement
|
||||
).elementStyles.map(ensureCSSStyleSheet);
|
||||
}
|
||||
|
||||
async _initTheme(root: AdoptedStyleSheetsElement): Promise<void> {
|
||||
// Early activate theme based on media query to prevent light flash
|
||||
// when dark is preferred
|
||||
|
@ -2,8 +2,10 @@ import { docLink } from "@goauthentik/common/global";
|
||||
import "@goauthentik/elements/Alert";
|
||||
import { Level } from "@goauthentik/elements/Alert";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { matter } from "md-front-matter";
|
||||
import * as showdown from "showdown";
|
||||
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { CSSResult, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
@ -11,22 +13,28 @@ import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
|
||||
export interface MarkdownDocument {
|
||||
html: string;
|
||||
metadata: { [key: string]: string };
|
||||
filename: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type Replacer = (input: string, md: MarkdownDocument) => string;
|
||||
|
||||
const isRelativeLink = /href="(\.[^"]*)"/gm;
|
||||
const isFile = /[^/]+\.md/;
|
||||
|
||||
@customElement("ak-markdown")
|
||||
export class Markdown extends AKElement {
|
||||
@property({ attribute: false })
|
||||
md?: MarkdownDocument;
|
||||
@property()
|
||||
md: string = "";
|
||||
|
||||
@property()
|
||||
meta: string = "";
|
||||
|
||||
@property({ attribute: false })
|
||||
replacers: Replacer[] = [];
|
||||
|
||||
docHtml = "";
|
||||
docTitle = "";
|
||||
|
||||
defaultReplacers: Replacer[] = [
|
||||
this.replaceAdmonitions,
|
||||
this.replaceList,
|
||||
@ -45,6 +53,8 @@ export class Markdown extends AKElement {
|
||||
];
|
||||
}
|
||||
|
||||
converter = new showdown.Converter({ metadata: true });
|
||||
|
||||
replaceAdmonitions(input: string): string {
|
||||
const admonitionStart = /:::(\w+)<br\s\/>/gm;
|
||||
const admonitionEnd = /:::/gm;
|
||||
@ -62,31 +72,36 @@ export class Markdown extends AKElement {
|
||||
}
|
||||
|
||||
replaceRelativeLinks(input: string, md: MarkdownDocument): string {
|
||||
const relativeLink = /href=".(.*)"/gm;
|
||||
const cwd = process.env.CWD as string;
|
||||
// cwd will point to $root/web, but the docs are in $root/website/docs
|
||||
let relPath = md.path.replace(cwd + "site", "");
|
||||
if (md.filename === "index.md") {
|
||||
relPath = relPath.replace("index.md", "");
|
||||
}
|
||||
const baseURL = docLink("");
|
||||
const fullURL = `${baseURL}${relPath}.$1`;
|
||||
return input.replace(relativeLink, `href="${fullURL}" target="_blank"`);
|
||||
const baseName = md.path.replace(isFile, "");
|
||||
const baseUrl = docLink("");
|
||||
const result = input.replace(isRelativeLink, (match, path) => {
|
||||
const pathName = path.replace(".md", "");
|
||||
const link = `docs/${baseName}${pathName}`;
|
||||
const url = new URL(link, baseUrl).toString();
|
||||
return `href="${url}" _target="blank"`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.md) {
|
||||
return html``;
|
||||
willUpdate(properties: PropertyValues<this>) {
|
||||
if (properties.has("md") || properties.has("meta")) {
|
||||
const parsedContent = matter(this.md);
|
||||
const parsedHTML = this.converter.makeHtml(parsedContent.content);
|
||||
const replacers = [...this.defaultReplacers, ...this.replacers];
|
||||
this.docTitle = parsedContent.data["title"] ?? "";
|
||||
this.docHtml = replacers.reduce(
|
||||
(html, replacer) => replacer(html, { path: this.meta }),
|
||||
parsedHTML,
|
||||
);
|
||||
}
|
||||
let finalHTML = this.md.html;
|
||||
const replacers = [...this.defaultReplacers, ...this.replacers];
|
||||
replacers.forEach((r) => {
|
||||
if (!this.md) {
|
||||
return;
|
||||
}
|
||||
finalHTML = r(finalHTML, this.md);
|
||||
});
|
||||
return html`${this.md?.metadata.title ? html`<h2>${this.md.metadata.title}</h2>` : html``}
|
||||
${unsafeHTML(finalHTML)}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.md) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${this.docTitle ? html`<h2>${this.docTitle}</h2>` : nothing}
|
||||
${unsafeHTML(this.docHtml)}`;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { CSSResult } from "lit";
|
||||
import { CSSResult, unsafeCSS } from "lit";
|
||||
|
||||
export const ensureCSSStyleSheet = (css: CSSStyleSheet | CSSResult): CSSStyleSheet =>
|
||||
css instanceof CSSResult ? css.styleSheet! : css;
|
||||
export const ensureCSSStyleSheet = (css: string | CSSStyleSheet | CSSResult): CSSStyleSheet =>
|
||||
typeof css === "string"
|
||||
? (unsafeCSS(css).styleSheet as CSSStyleSheet)
|
||||
: css instanceof CSSResult
|
||||
? (css.styleSheet as CSSStyleSheet)
|
||||
: css;
|
||||
|
@ -7857,407 +7857,549 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7513372fe60f6387">
|
||||
<source>Event volume</source>
|
||||
<target>Ēvēńţ vōĺũmē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s047a5f0211fedc72">
|
||||
<source>Require Outpost (flow can only be executed from an outpost).</source>
|
||||
<target>Ŕēǫũĩŕē Ōũţƥōśţ (ƒĺōŵ ćàń ōńĺŷ ƀē ēxēćũţēď ƒŕōm àń ōũţƥōśţ).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3271da6c18c25b18">
|
||||
<source>Connection settings.</source>
|
||||
<target>Ćōńńēćţĩōń śēţţĩńĝś.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f4ca2148183d692">
|
||||
<source>Successfully updated endpoint.</source>
|
||||
<target>Śũććēśśƒũĺĺŷ ũƥďàţēď ēńďƥōĩńţ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5adee855dbe191d9">
|
||||
<source>Successfully created endpoint.</source>
|
||||
<target>Śũććēśśƒũĺĺŷ ćŕēàţēď ēńďƥōĩńţ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s61e136c0658e27d5">
|
||||
<source>Protocol</source>
|
||||
<target>Ƥŕōţōćōĺ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa062b019ff0c8809">
|
||||
<source>RDP</source>
|
||||
<target>ŔĎƤ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s97f9bf19fa5b57d1">
|
||||
<source>SSH</source>
|
||||
<target>ŚŚĤ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7c100119e9ffcc32">
|
||||
<source>VNC</source>
|
||||
<target>VŃĆ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b05f9d8801fc14f">
|
||||
<source>Host</source>
|
||||
<target>Ĥōśţ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb474f652a2c2fc76">
|
||||
<source>Hostname/IP to connect to.</source>
|
||||
<target>Ĥōśţńàmē/ĨƤ ţō ćōńńēćţ ţō.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8276649077e8715c">
|
||||
<source>Endpoint(s)</source>
|
||||
<target>Ēńďƥōĩńţ(ś)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1dabfe0fe8a75ad">
|
||||
<source>Update Endpoint</source>
|
||||
<target>Ũƥďàţē Ēńďƥōĩńţ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s008496c7716b9812">
|
||||
<source>These bindings control which users will have access to this endpoint. Users must also have access to the application.</source>
|
||||
<target>Ţĥēśē ƀĩńďĩńĝś ćōńţŕōĺ ŵĥĩćĥ ũśēŕś ŵĩĺĺ ĥàvē àććēśś ţō ţĥĩś ēńďƥōĩńţ. Ũśēŕś mũśţ àĺśō ĥàvē àććēśś ţō ţĥē àƥƥĺĩćàţĩōń.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s38e7cd1a24e70faa">
|
||||
<source>Create Endpoint</source>
|
||||
<target>Ćŕēàţē Ēńďƥōĩńţ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4770c10e5b1c028c">
|
||||
<source>RAC is in preview.</source>
|
||||
<target>ŔÀĆ ĩś ĩń ƥŕēvĩēŵ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s168565f5ac74a89f">
|
||||
<source>Update RAC Provider</source>
|
||||
<target>Ũƥďàţē ŔÀĆ Ƥŕōvĩďēŕ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8465a2caa2d9ea5d">
|
||||
<source>Endpoints</source>
|
||||
<target>Ēńďƥōĩńţś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9857d883d8eb98fc">
|
||||
<source>General settings</source>
|
||||
<target>Ĝēńēŕàĺ śēţţĩńĝś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd2066881798a1b96">
|
||||
<source>RDP settings</source>
|
||||
<target>ŔĎƤ śēţţĩńĝś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb864dc36a463a155">
|
||||
<source>Ignore server certificate</source>
|
||||
<target>Ĩĝńōŕē śēŕvēŕ ćēŕţĩƒĩćàţē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s20366a8d1eaaca54">
|
||||
<source>Enable wallpaper</source>
|
||||
<target>Ēńàƀĺē ŵàĺĺƥàƥēŕ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1e44c5350ef7598c">
|
||||
<source>Enable font-smoothing</source>
|
||||
<target>Ēńàƀĺē ƒōńţ-śmōōţĥĩńĝ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s04ff5d6ae711e6d6">
|
||||
<source>Enable full window dragging</source>
|
||||
<target>Ēńàƀĺē ƒũĺĺ ŵĩńďōŵ ďŕàĝĝĩńĝ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s663ccbfdf27e8dd0">
|
||||
<source>Network binding</source>
|
||||
<target>Ńēţŵōŕķ ƀĩńďĩńĝ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb108a06693c67753">
|
||||
<source>No binding</source>
|
||||
<target>Ńō ƀĩńďĩńĝ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5aab90c74f1233b8">
|
||||
<source>Bind ASN</source>
|
||||
<target>ßĩńď ÀŚŃ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s488303b048afe83b">
|
||||
<source>Bind ASN and Network</source>
|
||||
<target>ßĩńď ÀŚŃ àńď Ńēţŵōŕķ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3268dcfe0c8234dc">
|
||||
<source>Bind ASN, Network and IP</source>
|
||||
<target>ßĩńď ÀŚŃ, Ńēţŵōŕķ àńď ĨƤ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s226381aca231644f">
|
||||
<source>Configure if sessions created by this stage should be bound to the Networks they were created in.</source>
|
||||
<target>Ćōńƒĩĝũŕē ĩƒ śēśśĩōńś ćŕēàţēď ƀŷ ţĥĩś śţàĝē śĥōũĺď ƀē ƀōũńď ţō ţĥē Ńēţŵōŕķś ţĥēŷ ŵēŕē ćŕēàţēď ĩń.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2555a1f20f3fd93e">
|
||||
<source>GeoIP binding</source>
|
||||
<target>ĜēōĨƤ ƀĩńďĩńĝ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d63c78f93c9a92e">
|
||||
<source>Bind Continent</source>
|
||||
<target>ßĩńď Ćōńţĩńēńţ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s395d5863b3a259b5">
|
||||
<source>Bind Continent and Country</source>
|
||||
<target>ßĩńď Ćōńţĩńēńţ àńď Ćōũńţŕŷ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s625ea0c32b4b136c">
|
||||
<source>Bind Continent, Country and City</source>
|
||||
<target>ßĩńď Ćōńţĩńēńţ, Ćōũńţŕŷ àńď Ćĩţŷ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bc7a1a88961be90">
|
||||
<source>Configure if sessions created by this stage should be bound to their GeoIP-based location</source>
|
||||
<target>Ćōńƒĩĝũŕē ĩƒ śēśśĩōńś ćŕēàţēď ƀŷ ţĥĩś śţàĝē śĥōũĺď ƀē ƀōũńď ţō ţĥēĩŕ ĜēōĨƤ-ƀàśēď ĺōćàţĩōń</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa06cd519ff151b6d">
|
||||
<source>RAC</source>
|
||||
<target>ŔÀĆ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s28b99b59541f54ca">
|
||||
<source>Connection failed after <x id="0" equiv-text="${this.connectionAttempt}"/> attempts.</source>
|
||||
<target>Ćōńńēćţĩōń ƒàĩĺēď àƒţēŕ <x id="0" equiv-text="${this.connectionAttempt}"/> àţţēmƥţś.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7c7d956418e1c8c8">
|
||||
<source>Re-connecting in <x id="0" equiv-text="${Math.max(1, delay / 1000)}"/> second(s).</source>
|
||||
<target>Ŕē-ćōńńēćţĩńĝ ĩń <x id="0" equiv-text="${Math.max(1, delay / 1000)}"/> śēćōńď(ś).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc003381f593d943">
|
||||
<source>Connecting...</source>
|
||||
<target>Ćōńńēćţĩńĝ...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31aa94a0b3c7edb2">
|
||||
<source>Select endpoint to connect to</source>
|
||||
<target>Śēĺēćţ ēńďƥōĩńţ ţō ćōńńēćţ ţō</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa2ea0fcd3ffa80e0">
|
||||
<source>Connection expiry</source>
|
||||
<target>Ćōńńēćţĩōń ēxƥĩŕŷ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6dd297c217729828">
|
||||
<source>Determines how long a session lasts before being disconnected and requiring re-authorization.</source>
|
||||
<target>Ďēţēŕmĩńēś ĥōŵ ĺōńĝ à śēśśĩōń ĺàśţś ƀēƒōŕē ƀēĩńĝ ďĩśćōńńēćţēď àńď ŕēǫũĩŕĩńĝ ŕē-àũţĥōŕĩźàţĩōń.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31f1afc1bfe1cb3a">
|
||||
<source>Learn more</source>
|
||||
<target>Ĺēàŕń mōŕē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc39f6abf0daedb0f">
|
||||
<source>Maximum concurrent connections</source>
|
||||
<target>Màxĩmũm ćōńćũŕŕēńţ ćōńńēćţĩōńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s62418cbcd2a25498">
|
||||
<source>Maximum concurrent allowed connections to this endpoint. Can be set to -1 to disable the limit.</source>
|
||||
<target>Màxĩmũm ćōńćũŕŕēńţ àĺĺōŵēď ćōńńēćţĩōńś ţō ţĥĩś ēńďƥōĩńţ. Ćàń ƀē śēţ ţō -1 ţō ďĩśàƀĺē ţĥē ĺĩmĩţ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s94d61907ee22a8c1">
|
||||
<source>Korean</source>
|
||||
<target>Ķōŕēàń</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s95d56e58f816d211">
|
||||
<source>Dutch</source>
|
||||
<target>Ďũţćĥ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16a15af46bc9aeef">
|
||||
<source>Failed to fetch objects: <x id="0" equiv-text="${this.error.detail}"/></source>
|
||||
<target>Ƒàĩĺēď ţō ƒēţćĥ ōƀĴēćţś: <x id="0" equiv-text="${this.error.detail}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s744401846fea6e76">
|
||||
<source>Brand</source>
|
||||
<target>ßŕàńď</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sab21e1f62676b56c">
|
||||
<source>Successfully updated brand.</source>
|
||||
<target>Śũććēśśƒũĺĺŷ ũƥďàţēď ƀŕàńď.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa43e43fd3a23e22d">
|
||||
<source>Successfully created brand.</source>
|
||||
<target>Śũććēśśƒũĺĺŷ ćŕēàţēď ƀŕàńď.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s41b3f9b4c98aabd9">
|
||||
<source>Use this brand for each domain that doesn't have a dedicated brand.</source>
|
||||
<target>Ũśē ţĥĩś ƀŕàńď ƒōŕ ēàćĥ ďōmàĩń ţĥàţ ďōēśń'ţ ĥàvē à ďēďĩćàţēď ƀŕàńď.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s17260b71484b307f">
|
||||
<source>Set custom attributes using YAML or JSON. Any attributes set here will be inherited by users, if the request is handled by this brand.</source>
|
||||
<target>Śēţ ćũśţōm àţţŕĩƀũţēś ũśĩńĝ ŶÀMĹ ōŕ ĵŚŌŃ. Àńŷ àţţŕĩƀũţēś śēţ ĥēŕē ŵĩĺĺ ƀē ĩńĥēŕĩţēď ƀŷ ũśēŕś, ĩƒ ţĥē ŕēǫũēśţ ĩś ĥàńďĺēď ƀŷ ţĥĩś ƀŕàńď.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s79fc990a2b58f27f">
|
||||
<source>Brands</source>
|
||||
<target>ßŕàńďś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s02774bc46a167346">
|
||||
<source>Brand(s)</source>
|
||||
<target>ßŕàńď(ś)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s801bf3d03f4a3ff1">
|
||||
<source>Update Brand</source>
|
||||
<target>Ũƥďàţē ßŕàńď</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5c3efec5330e0000">
|
||||
<source>Create Brand</source>
|
||||
<target>Ćŕēàţē ßŕàńď</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa9d13ce9e83aac17">
|
||||
<source>To let a user directly reset a their password, configure a recovery flow on the currently active brand.</source>
|
||||
<target>Ţō ĺēţ à ũśēŕ ďĩŕēćţĺŷ ŕēśēţ à ţĥēĩŕ ƥàśśŵōŕď, ćōńƒĩĝũŕē à ŕēćōvēŕŷ ƒĺōŵ ōń ţĥē ćũŕŕēńţĺŷ àćţĩvē ƀŕàńď.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6709b81e1ed4e39f">
|
||||
<source>The current brand must have a recovery flow configured to use a recovery link</source>
|
||||
<target>Ţĥē ćũŕŕēńţ ƀŕàńď mũśţ ĥàvē à ŕēćōvēŕŷ ƒĺōŵ ćōńƒĩĝũŕēď ţō ũśē à ŕēćōvēŕŷ ĺĩńķ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s634e2fd82c397576">
|
||||
<source>Successfully updated settings.</source>
|
||||
<target>Śũććēśśƒũĺĺŷ ũƥďàţēď śēţţĩńĝś.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb8e4edaea6f1d935">
|
||||
<source>Avatars</source>
|
||||
<target>Àvàţàŕś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s945856050217c828">
|
||||
<source>Configure how authentik should show avatars for users. The following values can be set:</source>
|
||||
<target>Ćōńƒĩĝũŕē ĥōŵ àũţĥēńţĩķ śĥōũĺď śĥōŵ àvàţàŕś ƒōŕ ũśēŕś. Ţĥē ƒōĺĺōŵĩńĝ vàĺũēś ćàń ƀē śēţ:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf4ef4c8ce713f775">
|
||||
<source>Disables per-user avatars and just shows a 1x1 pixel transparent picture</source>
|
||||
<target>Ďĩśàƀĺēś ƥēŕ-ũśēŕ àvàţàŕś àńď Ĵũśţ śĥōŵś à 1x1 ƥĩxēĺ ţŕàńśƥàŕēńţ ƥĩćţũŕē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5446842a7e4a963b">
|
||||
<source>Uses gravatar with the user's email address</source>
|
||||
<target>Ũśēś ĝŕàvàţàŕ ŵĩţĥ ţĥē ũśēŕ'ś ēmàĩĺ àďďŕēśś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s35363b9e1cc2abd3">
|
||||
<source>Generated avatars based on the user's name</source>
|
||||
<target>Ĝēńēŕàţēď àvàţàŕś ƀàśēď ōń ţĥē ũśēŕ'ś ńàmē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s48110ca292cad513">
|
||||
<source>Any URL: If you want to use images hosted on another server, you can set any URL. Additionally, these placeholders can be used:</source>
|
||||
<target>Àńŷ ŨŔĹ: Ĩƒ ŷōũ ŵàńţ ţō ũśē ĩmàĝēś ĥōśţēď ōń àńōţĥēŕ śēŕvēŕ, ŷōũ ćàń śēţ àńŷ ŨŔĹ. Àďďĩţĩōńàĺĺŷ, ţĥēśē ƥĺàćēĥōĺďēŕś ćàń ƀē ũśēď:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe1dfda044bdc93b">
|
||||
<source>The user's username</source>
|
||||
<target>Ţĥē ũśēŕ'ś ũśēŕńàmē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s653f257c9c2d4dc5">
|
||||
<source>The email address, md5 hashed</source>
|
||||
<target>Ţĥē ēmàĩĺ àďďŕēśś, mď5 ĥàśĥēď</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9c9183cd80916b4f">
|
||||
<source>The user's UPN, if set (otherwise an empty string)</source>
|
||||
<target>Ţĥē ũśēŕ'ś ŨƤŃ, ĩƒ śēţ (ōţĥēŕŵĩśē àń ēmƥţŷ śţŕĩńĝ)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="h4963ed14d7e239a9">
|
||||
<source>An attribute path like
|
||||
<x id="0" equiv-text="<code>"/>attributes.something.avatar<x id="1" equiv-text="</code>"/>, which can be used in
|
||||
combination with the file field to allow users to upload custom
|
||||
avatars for themselves.</source>
|
||||
<target>Àń àţţŕĩƀũţē ƥàţĥ ĺĩķē
|
||||
<x id="0" equiv-text="<code>"/>àţţŕĩƀũţēś.śōmēţĥĩńĝ.àvàţàŕ<x id="1" equiv-text="</code>"/>, ŵĥĩćĥ ćàń ƀē ũśēď ĩń
|
||||
ćōmƀĩńàţĩōń ŵĩţĥ ţĥē ƒĩĺē ƒĩēĺď ţō àĺĺōŵ ũśēŕś ţō ũƥĺōàď ćũśţōm
|
||||
àvàţàŕś ƒōŕ ţĥēmśēĺvēś.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4c80c34a67a6f1c9">
|
||||
<source>Multiple values can be set, comma-separated, and authentik will fallback to the next mode when no avatar could be found.</source>
|
||||
<target>Mũĺţĩƥĺē vàĺũēś ćàń ƀē śēţ, ćōmmà-śēƥàŕàţēď, àńď àũţĥēńţĩķ ŵĩĺĺ ƒàĺĺƀàćķ ţō ţĥē ńēxţ mōďē ŵĥēń ńō àvàţàŕ ćōũĺď ƀē ƒōũńď.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="h2fafcc3ebafea2f8">
|
||||
<source>For example, setting this to <x id="0" equiv-text="<code>"/>gravatar,initials<x id="1" equiv-text="</code>"/> will
|
||||
attempt to get an avatar from Gravatar, and if the user has not
|
||||
configured on there, it will fallback to a generated avatar.</source>
|
||||
<target>Ƒōŕ ēxàmƥĺē, śēţţĩńĝ ţĥĩś ţō <x id="0" equiv-text="<code>"/>ĝŕàvàţàŕ,ĩńĩţĩàĺś<x id="1" equiv-text="</code>"/> ŵĩĺĺ
|
||||
àţţēmƥţ ţō ĝēţ àń àvàţàŕ ƒŕōm Ĝŕàvàţàŕ, àńď ĩƒ ţĥē ũśēŕ ĥàś ńōţ
|
||||
ćōńƒĩĝũŕēď ōń ţĥēŕē, ĩţ ŵĩĺĺ ƒàĺĺƀàćķ ţō à ĝēńēŕàţēď àvàţàŕ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5faec5eb5faf62ac">
|
||||
<source>Allow users to change name</source>
|
||||
<target>Àĺĺōŵ ũśēŕś ţō ćĥàńĝē ńàmē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s078ffec0257621c0">
|
||||
<source>Enable the ability for users to change their name.</source>
|
||||
<target>Ēńàƀĺē ţĥē àƀĩĺĩţŷ ƒōŕ ũśēŕś ţō ćĥàńĝē ţĥēĩŕ ńàmē.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s456d88f3679190fd">
|
||||
<source>Allow users to change email</source>
|
||||
<target>Àĺĺōŵ ũśēŕś ţō ćĥàńĝē ēmàĩĺ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5fc6c14d106f40d3">
|
||||
<source>Enable the ability for users to change their email.</source>
|
||||
<target>Ēńàƀĺē ţĥē àƀĩĺĩţŷ ƒōŕ ũśēŕś ţō ćĥàńĝē ţĥēĩŕ ēmàĩĺ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s628e414bb2367057">
|
||||
<source>Allow users to change username</source>
|
||||
<target>Àĺĺōŵ ũśēŕś ţō ćĥàńĝē ũśēŕńàmē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d816a95ca43a99d">
|
||||
<source>Enable the ability for users to change their username.</source>
|
||||
<target>Ēńàƀĺē ţĥē àƀĩĺĩţŷ ƒōŕ ũśēŕś ţō ćĥàńĝē ţĥēĩŕ ũśēŕńàmē.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b52b60ed5e2bc7">
|
||||
<source>Footer links</source>
|
||||
<target>Ƒōōţēŕ ĺĩńķś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s166b59f3cc5d8ec3">
|
||||
<source>GDPR compliance</source>
|
||||
<target>ĜĎƤŔ ćōmƥĺĩàńćē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb8b23770f899e5bb">
|
||||
<source>When enabled, all the events caused by a user will be deleted upon the user's deletion.</source>
|
||||
<target>Ŵĥēń ēńàƀĺēď, àĺĺ ţĥē ēvēńţś ćàũśēď ƀŷ à ũśēŕ ŵĩĺĺ ƀē ďēĺēţēď ũƥōń ţĥē ũśēŕ'ś ďēĺēţĩōń.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s29501761df0fe837">
|
||||
<source>Impersonation</source>
|
||||
<target>Ĩmƥēŕśōńàţĩōń</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8f503553d8432487">
|
||||
<source>Globally enable/disable impersonation.</source>
|
||||
<target>Ĝĺōƀàĺĺŷ ēńàƀĺē/ďĩśàƀĺē ĩmƥēŕśōńàţĩōń.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="see1eb81c1f734079">
|
||||
<source>System settings</source>
|
||||
<target>Śŷśţēm śēţţĩńĝś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s47fb5504f693775b">
|
||||
<source>Changes made:</source>
|
||||
<target>Ćĥàńĝēś màďē:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s506b6a19d12f414c">
|
||||
<source>Key</source>
|
||||
<target>Ķēŷ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4fc9dc73245eab09">
|
||||
<source>Previous value</source>
|
||||
<target>Ƥŕēvĩōũś vàĺũē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="scb3b0671d7b7f640">
|
||||
<source>New value</source>
|
||||
<target>Ńēŵ vàĺũē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4a642406b0745917">
|
||||
<source>Raw event info</source>
|
||||
<target>Ŕàŵ ēvēńţ ĩńƒō</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa65e7bc7ddd3484d">
|
||||
<source>Anonymous user</source>
|
||||
<target>Àńōńŷmōũś ũśēŕ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0db494ff61b48438">
|
||||
<source>Add All Available</source>
|
||||
<target>Àďď Àĺĺ Àvàĩĺàƀĺē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f68faa5b84ee9fd">
|
||||
<source>Remove All Available</source>
|
||||
<target>Ŕēmōvē Àĺĺ Àvàĩĺàƀĺē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0ba85594344a5c02">
|
||||
<source>Remove All</source>
|
||||
<target>Ŕēmōvē Àĺĺ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1efa17e3407e0f56">
|
||||
<source>Available options</source>
|
||||
<target>Àvàĩĺàƀĺē ōƥţĩōńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s886db4d476f66522">
|
||||
<source>Selected options</source>
|
||||
<target>Śēĺēćţēď ōƥţĩōńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd429eba21dd59f44">
|
||||
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
|
||||
<target><x id="0" equiv-text="${availableCount}"/> ĩţēm(ś) màŕķēď ţō àďď.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4abfe4e0d0665fd1">
|
||||
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
|
||||
<target><x id="0" equiv-text="${selectedTotal}"/> ĩţēm(ś) śēĺēćţēď.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s56b3d54118c34b2f">
|
||||
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
|
||||
<target><x id="0" equiv-text="${selectedCount}"/> ĩţēm(ś) màŕķēď ţō ŕēmōvē.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="scb76a63c68b0d81f">
|
||||
<source>Available Applications</source>
|
||||
<target>Àvàĩĺàƀĺē Àƥƥĺĩćàţĩōńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd176021da2ea0fe3">
|
||||
<source>Selected Applications</source>
|
||||
<target>Śēĺēćţēď Àƥƥĺĩćàţĩōńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s862505f29064fc72">
|
||||
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
|
||||
<target>Ţĥĩś ōƥţĩōń ćōńƒĩĝũŕēś ţĥē ƒōōţēŕ ĺĩńķś ōń ţĥē ƒĺōŵ ēxēćũţōŕ ƥàĝēś. Ĩţ mũśţ ƀē à vàĺĩď ŶÀMĹ ōŕ ĵŚŌŃ ĺĩśţ àńď ćàń ƀē ũśēď àś ƒōĺĺōŵś:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb2275335377069aa">
|
||||
<source>This feature requires an enterprise license.</source>
|
||||
<target>Ţĥĩś ƒēàţũŕē ŕēǫũĩŕēś àń ēńţēŕƥŕĩśē ĺĩćēńśē.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d3f81dc4bcacbda">
|
||||
<source>Last used</source>
|
||||
<target>Ĺàśţ ũśēď</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s96b2703420bfbd0c">
|
||||
<source>OAuth Access Tokens</source>
|
||||
<target>ŌÀũţĥ Àććēśś Ţōķēńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd99e668c54f78f6e">
|
||||
<source>Credentials / Tokens</source>
|
||||
<target>Ćŕēďēńţĩàĺś / Ţōķēńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="saada50e84c7c646d">
|
||||
<source>Permissions set on users which affect this object.</source>
|
||||
<target>Ƥēŕmĩśśĩōńś śēţ ōń ũśēŕś ŵĥĩćĥ àƒƒēćţ ţĥĩś ōƀĴēćţ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s37d1b74df205b528">
|
||||
<source>Permissions set on roles which affect this object.</source>
|
||||
<target>Ƥēŕmĩśśĩōńś śēţ ōń ŕōĺēś ŵĥĩćĥ àƒƒēćţ ţĥĩś ōƀĴēćţ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc443bfff8a5dd106">
|
||||
<source>Permissions assigned to this user which affect all object instances of a given type.</source>
|
||||
<target>Ƥēŕmĩśśĩōńś àśśĩĝńēď ţō ţĥĩś ũśēŕ ŵĥĩćĥ àƒƒēćţ àĺĺ ōƀĴēćţ ĩńśţàńćēś ōƒ à ĝĩvēń ţŷƥē.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3eba09a350b7d6c9">
|
||||
<source>Permissions assigned to this user affecting specific object instances.</source>
|
||||
<target>Ƥēŕmĩśśĩōńś àśśĩĝńēď ţō ţĥĩś ũśēŕ àƒƒēćţĩńĝ śƥēćĩƒĩć ōƀĴēćţ ĩńśţàńćēś.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s17032e57ba222d2f">
|
||||
<source>Permissions assigned to this role which affect all object instances of a given type.</source>
|
||||
<target>Ƥēŕmĩśśĩōńś àśśĩĝńēď ţō ţĥĩś ŕōĺē ŵĥĩćĥ àƒƒēćţ àĺĺ ōƀĴēćţ ĩńśţàńćēś ōƒ à ĝĩvēń ţŷƥē.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb9b51124d1b3dca0">
|
||||
<source>JWT payload</source>
|
||||
<target>ĵŴŢ ƥàŷĺōàď</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbd065743a0c599e3">
|
||||
<source>Preview for user</source>
|
||||
<target>Ƥŕēvĩēŵ ƒōŕ ũśēŕ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se16b4d412ad1aba9">
|
||||
<source>Brand name</source>
|
||||
<target>ßŕàńď ńàmē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb7e68dcad68a638c">
|
||||
<source>Remote Access Provider</source>
|
||||
<target>Ŕēmōţē Àććēśś Ƥŕōvĩďēŕ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfb1980a471b7dfb6">
|
||||
<source>Remotely access computers/servers via RDP/SSH/VNC</source>
|
||||
<target>Ŕēmōţēĺŷ àććēśś ćōmƥũţēŕś/śēŕvēŕś vĩà ŔĎƤ/ŚŚĤ/VŃĆ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9ffdea131dddb3c5">
|
||||
<source>Configure Remote Access Provider Provider</source>
|
||||
<target>Ćōńƒĩĝũŕē Ŕēmōţē Àććēśś Ƥŕōvĩďēŕ Ƥŕōvĩďēŕ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s56bc67239b9255a2">
|
||||
<source>Delete authorization on disconnect</source>
|
||||
<target>Ďēĺēţē àũţĥōŕĩźàţĩōń ōń ďĩśćōńńēćţ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3945449bd524d8a5">
|
||||
<source>When enabled, connection authorizations will be deleted when a client disconnects. This will force clients with flaky internet connections to re-authorize the endpoint.</source>
|
||||
<target>Ŵĥēń ēńàƀĺēď, ćōńńēćţĩōń àũţĥōŕĩźàţĩōńś ŵĩĺĺ ƀē ďēĺēţēď ŵĥēń à ćĺĩēńţ ďĩśćōńńēćţś. Ţĥĩś ŵĩĺĺ ƒōŕćē ćĺĩēńţś ŵĩţĥ ƒĺàķŷ ĩńţēŕńēţ ćōńńēćţĩōńś ţō ŕē-àũţĥōŕĩźē ţĥē ēńďƥōĩńţ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc93712b5fbdc6810">
|
||||
<source>Connection Token(s)</source>
|
||||
<target>Ćōńńēćţĩōń Ţōķēń(ś)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s526a525cdc79bbdc">
|
||||
<source>Endpoint</source>
|
||||
<target>Ēńďƥōĩńţ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5c18f85cda6e89be">
|
||||
<source>Connections</source>
|
||||
<target>Ćōńńēćţĩōńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s269a190f0086d892">
|
||||
<source>Unconfigured</source>
|
||||
<target>Ũńćōńƒĩĝũŕēď</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9e24bec94ec23dc9">
|
||||
<source>This option will not be changed by this mapping.</source>
|
||||
<target>Ţĥĩś ōƥţĩōń ŵĩĺĺ ńōţ ƀē ćĥàńĝēď ƀŷ ţĥĩś màƥƥĩńĝ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sac428446e9e41f54">
|
||||
<source>RAC Connections</source>
|
||||
<target>ŔÀĆ Ćōńńēćţĩōńś</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s39002897db60bb28">
|
||||
<source>Sending Duo push notification...</source>
|
||||
<target>Śēńďĩńĝ Ďũō ƥũśĥ ńōţĩƒĩćàţĩōń...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd2b8c1caa0340ed6">
|
||||
<source>Failed to authenticate</source>
|
||||
<target>Ƒàĩĺēď ţō àũţĥēńţĩćàţē</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sffef1a8596bc58bb">
|
||||
<source>Authenticating...</source>
|
||||
<target>Àũţĥēńţĩćàţĩńĝ...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd12f594a3184c056">
|
||||
<source>Customization</source>
|
||||
<target>Ćũśţōmĩźàţĩōń</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s363abde8a254ea5f">
|
||||
<source>Authentication failed. Please try again.</source>
|
||||
<target>Àũţĥēńţĩćàţĩōń ƒàĩĺēď. Ƥĺēàśē ţŕŷ àĝàĩń.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb0821a9e92cac5eb">
|
||||
<source>Failed to register. Please try again.</source>
|
||||
<target>Ƒàĩĺēď ţō ŕēĝĩśţēŕ. Ƥĺēàśē ţŕŷ àĝàĩń.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s238784fc1dc672ae">
|
||||
<source>Registering...</source>
|
||||
<target>Ŕēĝĩśţēŕĩńĝ...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s009bd1c98a9f5de2">
|
||||
<source>Failed to register</source>
|
||||
<target>Ƒàĩĺēď ţō ŕēĝĩśţēŕ</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb166ce92e8e807d6">
|
||||
<source>Retry registration</source>
|
||||
<target>Ŕēţŕŷ ŕēĝĩśţŕàţĩōń</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6ecfc18dbfeedd76">
|
||||
<source>Select one of the options below to continue.</source>
|
||||
<target>Śēĺēćţ ōńē ōƒ ţĥē ōƥţĩōńś ƀēĺōŵ ţō ćōńţĩńũē.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6ecfc18dbfeedd76">
|
||||
<source>Select one of the options below to continue.</source>
|
||||
|
Reference in New Issue
Block a user