Add multilingual glossary and phrases for i18n, including terms, attributes, and compatibility across da, de, en, no, and sv locales.

This commit is contained in:
Jeppe Bundgaard
2026-05-19 08:03:39 +02:00
parent 56078307da
commit d69c474655
723 changed files with 69649 additions and 559 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue -type f \\\\\\(-name *.ts -o -name *.js \\\\\\) -not -path */node_modules* -not -path */dist/* -not -path */.git/* -not -path */dev-dist/*)",
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue/src -type f \\\\\\(-name *.ts -o -name *.js -o -name *.vue \\\\\\))",
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue/app -type f \\\\\\(-name *.ts -o -name *.js -o -name *.java -o -name *.kt \\\\\\))",
"Bash(grep -r \"getAutomaticFlags\\\\|AutomaticFlags\\\\|automaticFlags\\\\|Automatic.*Flags\" /c/Users/2jepp/WebstormProjects/pleno-vue --include=*.ts --include=*.js --include=*.java --include=*.kt --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=dev-dist --exclude-dir=output --exclude-dir=.git)",
"Bash(xargs grep:*)"
]
}
}
View File
View File
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
3.0
View File
+9
View File
@@ -16,6 +16,15 @@
"preview:prod": "npm run build && npm run preview -- --host 127.0.0.1 --port 4173",
"text:fix-encoding": "node scripts/text-encoding.mjs fix",
"text:check-encoding": "node scripts/text-encoding.mjs check",
"i18n:v2:compile": "node scripts/i18n-v2-compile-source.mjs",
"i18n:v2:source-check": "node scripts/i18n-v2-compile-source.mjs --check",
"i18n:v2:check": "npm run i18n:v2:source-check && npm run i18n:v2:global-template-audit && npm run i18n:v2:template-dedupe-audit && npm run i18n:v2:word-audit",
"i18n:v2:word-audit": "node scripts/i18n-v2-word-dedupe.mjs",
"i18n:v2:word-dedupe": "node scripts/i18n-v2-word-dedupe.mjs --apply",
"i18n:v2:global-template-audit": "node scripts/i18n-v2-globalize-templates.mjs",
"i18n:v2:globalize-templates": "node scripts/i18n-v2-globalize-templates.mjs --apply",
"i18n:v2:template-dedupe-audit": "node scripts/i18n-v2-template-dedupe.mjs",
"i18n:v2:template-dedupe": "node scripts/i18n-v2-template-dedupe.mjs --apply",
"test:unit": "npm run text:check-encoding && npm run test:unit:fast && npm run test:unit:serial",
"test:unit:fast": "node scripts/run-vitest-unit-fast.mjs",
"test:unit:serial": "node scripts/run-vitest-unit-batches.mjs --from-list tests/unit/serial-tests.txt",
+185
View File
@@ -0,0 +1,185 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const sourceDirectory = path.join(projectRoot, "src", "i18n", "source");
const generatedDirectory = path.join(projectRoot, "src", "i18n", "generated");
const activeLocales = ["da", "en", "sv", "de", "no"];
const mode = process.argv.includes("--check") ? "check" : "write";
const linkTokenPattern = /@(?<modifier>\.[\p{L}]+)?:(?:\{'(?<literal>[^']+)'\}|(?<path>[\p{L}\p{N}_.-]+))/gu;
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
const stableJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
const writeJson = (filePath, value) => {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, stableJson(value), "utf8");
};
const listJsonFiles = (directory) => {
if (!fs.existsSync(directory)) {
return [];
}
const entries = fs.readdirSync(directory, { withFileTypes: true });
return entries
.flatMap((entry) => {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
return listJsonFiles(entryPath);
}
return entry.isFile() && entry.name.endsWith(".json") ? [entryPath] : [];
})
.sort((left, right) => left.localeCompare(right, "en", { sensitivity: "base" }));
};
const mergeMessages = (left = {}, right = {}) => {
const merged = { ...left };
for (const [key, value] of Object.entries(right ?? {})) {
if (isPlainObject(value) && isPlainObject(merged[key])) {
merged[key] = mergeMessages(merged[key], value);
} else {
merged[key] = value;
}
}
return merged;
};
const readJsonFragments = (directory) => {
let merged = {};
for (const filePath of listJsonFiles(directory)) {
merged = mergeMessages(merged, readJson(filePath));
}
return merged;
};
const mapSourcePathToRuntimePath = (keyPath) => {
if (keyPath.startsWith("terms.glossary.")) {
return `words.generated.${keyPath.slice("terms.glossary.".length)}`;
}
if (keyPath.startsWith("terms.")) {
return `words.${keyPath.slice("terms.".length)}`;
}
if (keyPath.startsWith("phrases.compat.")) {
return `templates.generated.compat.${keyPath.slice("phrases.compat.".length)}`;
}
if (keyPath.startsWith("phrases.")) {
return `templates.${keyPath.slice("phrases.".length)}`;
}
return keyPath;
};
const rewriteLinks = (value) => {
if (typeof value === "string") {
return value.replace(linkTokenPattern, (token, modifier = "", literalTarget, pathTarget) => {
const target = literalTarget ?? pathTarget;
const mappedTarget = mapSourcePathToRuntimePath(target);
const quoteTarget = literalTarget || /[^\p{L}\p{N}_.-]/u.test(mappedTarget);
return quoteTarget ? `@${modifier}:{'${mappedTarget}'}` : `@${modifier}:${mappedTarget}`;
});
}
if (Array.isArray(value)) {
return value.map((entry) => rewriteLinks(entry));
}
if (!isPlainObject(value)) {
return value;
}
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, rewriteLinks(entry)]));
};
const compileWords = (sourceTerms) => {
const { glossary, ...terms } = sourceTerms;
return {
grammar: terms.grammar ?? {},
actions: terms.actions ?? {},
entities: terms.entities ?? {},
statuses: terms.statuses ?? {},
domain: terms.domain ?? {},
services: terms.services ?? {},
units: terms.units ?? {},
replication: terms.replication ?? {},
generated: glossary ?? {},
};
};
const compileTemplates = (sourcePhrases) => {
const { compat, ...phrases } = sourcePhrases;
return {
...phrases,
generated: {
compat: compat ?? {},
},
};
};
const compileLocale = (locale) => {
const localeSourceDirectory = path.join(sourceDirectory, locale);
const sourceTerms = readJsonFragments(path.join(localeSourceDirectory, "terms"));
const sourcePhrases = readJsonFragments(path.join(localeSourceDirectory, "phrases"));
return rewriteLinks({
words: compileWords(sourceTerms),
templates: compileTemplates(sourcePhrases),
});
};
const compileGlobal = () => {
const shared = readJsonFragments(path.join(sourceDirectory, "global", "shared"));
const locales = {};
for (const locale of activeLocales) {
locales[locale] = readJsonFragments(path.join(sourceDirectory, "global", "locales", locale));
}
return rewriteLinks({
shared,
locales,
});
};
const compiledFiles = new Map([
[path.join(generatedDirectory, "global-v2.json"), compileGlobal()],
...activeLocales.map((locale) => [path.join(generatedDirectory, `${locale}-v2.json`), compileLocale(locale)]),
]);
if (mode === "check") {
const mismatches = [];
for (const [filePath, value] of compiledFiles.entries()) {
const expected = stableJson(value);
const actual = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, "") : "";
if (actual !== expected) {
mismatches.push(path.relative(projectRoot, filePath));
}
}
if (mismatches.length > 0) {
console.error("Generated i18n v2 runtime files are out of date:");
for (const mismatch of mismatches) {
console.error(` ${mismatch}`);
}
console.error("Run npm run i18n:v2:compile.");
process.exit(1);
}
console.log("Generated i18n v2 runtime files are up to date.");
} else {
for (const [filePath, value] of compiledFiles.entries()) {
writeJson(filePath, value);
}
console.log(`Compiled i18n v2 runtime files to ${path.relative(projectRoot, generatedDirectory)}.`);
}
+389
View File
@@ -0,0 +1,389 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const localesDirectory = path.join(projectRoot, "src", "i18n", "generated");
const activeLocales = ["da", "en", "sv", "de", "no"];
const mode = process.argv.includes("--apply") ? "apply" : "check";
const templatePrefix = "templates.generated.compat";
const linkTokenPattern = /@(?:\.[\p{L}]+)?:(?:\{'[^']+'\}|[\p{L}\p{N}_.-]+)/gu;
const exactLinkPattern = /^@(?<modifier>\.[\p{L}]+)?:(?:\{'(?<literal>[^']+)'\}|(?<path>[\p{L}\p{N}_.-]+))$/u;
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
const writeJson = (filePath, value) => {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
};
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
const flattenStringEntries = (value, prefix = "") => {
if (typeof value === "string") {
return [{ key: prefix, value }];
}
if (!isPlainObject(value)) {
return [];
}
return Object.entries(value).flatMap(([key, entry]) =>
flattenStringEntries(entry, prefix ? `${prefix}.${key}` : key)
);
};
const getValueAtPath = (value, keyPath) => {
let current = value;
for (const segment of keyPath.split(".")) {
if (!isPlainObject(current) || !(segment in current)) {
return undefined;
}
current = current[segment];
}
return current;
};
const ensureObjectAtPath = (value, keyPath) => {
let current = value;
for (const segment of keyPath.split(".")) {
if (!isPlainObject(current[segment])) {
current[segment] = {};
}
current = current[segment];
}
return current;
};
const setValueAtPath = (value, keyPath, nextValue) => {
const segments = keyPath.split(".");
const parent = ensureObjectAtPath(value, segments.slice(0, -1).join("."));
parent[segments.at(-1)] = nextValue;
};
const deleteValueAtPath = (value, keyPath) => {
const segments = keyPath.split(".");
const parents = [];
let current = value;
for (const segment of segments.slice(0, -1)) {
if (!isPlainObject(current) || !(segment in current)) {
return false;
}
parents.push([current, segment]);
current = current[segment];
}
if (!isPlainObject(current) || !(segments.at(-1) in current)) {
return false;
}
delete current[segments.at(-1)];
for (let index = parents.length - 1; index >= 0; index -= 1) {
const [parent, segment] = parents[index];
const child = parent[segment];
if (isPlainObject(child) && Object.keys(child).length === 0) {
delete parent[segment];
} else {
break;
}
}
return true;
};
const mergeMessages = (sharedMessages = {}, localeMessages = {}) => {
const mergedMessages = { ...sharedMessages };
for (const [key, value] of Object.entries(localeMessages ?? {})) {
if (isPlainObject(value) && isPlainObject(mergedMessages[key])) {
mergedMessages[key] = mergeMessages(mergedMessages[key], value);
} else {
mergedMessages[key] = value;
}
}
return mergedMessages;
};
const applyTextModifier = (value, modifier) => {
switch (modifier) {
case ".capitalize":
return `${value.charAt(0).toLocaleUpperCase()}${value.slice(1)}`;
case ".upper":
return value.toLocaleUpperCase();
case ".lower":
return value.toLocaleLowerCase();
default:
return value;
}
};
const resolveLinkedMessage = (messages, keyPath, seen = new Set()) => {
if (seen.has(keyPath)) {
return undefined;
}
seen.add(keyPath);
const value = getValueAtPath(messages, keyPath);
if (typeof value !== "string") {
return undefined;
}
const exactLink = value.trim().match(exactLinkPattern);
if (exactLink) {
const target = exactLink.groups.literal ?? exactLink.groups.path;
const resolved = resolveLinkedMessage(messages, target, seen);
return resolved === undefined ? undefined : applyTextModifier(resolved, exactLink.groups.modifier);
}
return value.replace(linkTokenPattern, (token) => {
const match = token.match(exactLinkPattern);
if (!match?.groups) {
return token;
}
const target = match.groups.literal ?? match.groups.path;
const resolved = resolveLinkedMessage(messages, target, new Set(seen));
return resolved === undefined ? token : applyTextModifier(resolved, match.groups.modifier);
});
};
const readRawLocales = () =>
Object.fromEntries(
activeLocales.map((locale) => [locale, readJson(path.join(localesDirectory, `${locale}-v2.json`))])
);
const buildMergedLocales = (globalMessages, rawLocales) =>
Object.fromEntries(
activeLocales.map((locale) => [
locale,
mergeMessages(
mergeMessages(globalMessages.shared ?? {}, globalMessages.locales?.[locale] ?? {}),
rawLocales[locale]
),
])
);
const isCatalogPhraseKey = (keyPath) => !keyPath.startsWith("words.") && !keyPath.startsWith("templates.");
const exactLinkTarget = (value) => {
const match = value.trim().match(exactLinkPattern);
return match?.groups?.literal ?? match?.groups?.path;
};
const templateKeyFor = (keyPath) => `${templatePrefix}.${keyPath}`;
const linkedTemplateFor = (keyPath) => `@:{'${templateKeyFor(keyPath)}'}`;
const sourceValueFor = (globalMessages, rawLocales, locale, keyPath) => {
const rawValue = getValueAtPath(rawLocales[locale], keyPath);
if (typeof rawValue === "string") {
return rawValue;
}
const localeValue = getValueAtPath(globalMessages.locales?.[locale] ?? {}, keyPath);
if (typeof localeValue === "string") {
return localeValue;
}
const sharedValue = getValueAtPath(globalMessages.shared ?? {}, keyPath);
return typeof sharedValue === "string" ? sharedValue : undefined;
};
const collectCandidateKeys = (globalMessages, rawLocales) => {
const keys = new Set();
for (const locale of activeLocales) {
for (const entry of flattenStringEntries(rawLocales[locale]).filter((entry) => isCatalogPhraseKey(entry.key))) {
keys.add(entry.key);
}
for (const entry of flattenStringEntries(globalMessages.locales?.[locale] ?? {}).filter((entry) =>
isCatalogPhraseKey(entry.key)
)) {
const target = exactLinkTarget(entry.value);
if (target !== templateKeyFor(entry.key)) {
keys.add(entry.key);
}
}
}
return [...keys].sort((left, right) => left.localeCompare(right, "en", { sensitivity: "base" }));
};
const reorderLocaleRoot = (localeMessages) => {
const { words, templates, ...rest } = localeMessages;
return { words, templates, ...rest };
};
const pruneLocaleMap = (locales) => {
const nextLocales = {};
for (const locale of activeLocales) {
nextLocales[locale] = isPlainObject(locales?.[locale]) ? locales[locale] : {};
}
return nextLocales;
};
const transformCatalogs = (originalGlobalMessages, originalRawLocales) => {
const globalMessages = cloneJson(originalGlobalMessages);
const rawLocales = cloneJson(originalRawLocales);
globalMessages.shared = globalMessages.shared ?? {};
globalMessages.locales = pruneLocaleMap(globalMessages.locales ?? {});
const originalMergedLocales = buildMergedLocales(originalGlobalMessages, originalRawLocales);
const candidateKeys = collectCandidateKeys(originalGlobalMessages, originalRawLocales);
let directSharedAliasCount = 0;
let generatedTemplateCount = 0;
let removedRawEntryCount = 0;
let removedLocaleEntryCount = 0;
for (const keyPath of candidateKeys) {
const valuesByLocale = Object.fromEntries(
activeLocales.map((locale) => {
const sourceValue = sourceValueFor(originalGlobalMessages, originalRawLocales, locale, keyPath);
if (typeof sourceValue === "string") {
return [locale, sourceValue];
}
const mergedValue = getValueAtPath(originalMergedLocales[locale], keyPath);
return [locale, typeof mergedValue === "string" ? mergedValue : undefined];
})
);
const values = activeLocales.map((locale) => valuesByLocale[locale]);
if (values.some((value) => typeof value !== "string")) {
continue;
}
const firstValue = values[0];
const allValuesIdentical = values.every((value) => value === firstValue);
const canUseDirectSharedAlias =
allValuesIdentical &&
!firstValue.includes("words.generated.") &&
!firstValue.includes(templatePrefix) &&
exactLinkTarget(firstValue) !== keyPath;
if (canUseDirectSharedAlias) {
setValueAtPath(globalMessages.shared, keyPath, firstValue);
directSharedAliasCount += 1;
} else {
setValueAtPath(globalMessages.shared, keyPath, linkedTemplateFor(keyPath));
for (const locale of activeLocales) {
setValueAtPath(rawLocales[locale], templateKeyFor(keyPath), valuesByLocale[locale]);
}
generatedTemplateCount += 1;
}
for (const locale of activeLocales) {
if (deleteValueAtPath(rawLocales[locale], keyPath)) {
removedRawEntryCount += 1;
}
if (deleteValueAtPath(globalMessages.locales[locale], keyPath)) {
removedLocaleEntryCount += 1;
}
}
}
for (const locale of activeLocales) {
rawLocales[locale] = reorderLocaleRoot(rawLocales[locale]);
globalMessages.locales[locale] = isPlainObject(globalMessages.locales[locale])
? globalMessages.locales[locale]
: {};
}
const nextMergedLocales = buildMergedLocales(globalMessages, rawLocales);
const mismatches = [];
for (const locale of activeLocales) {
for (const entry of flattenStringEntries(originalMergedLocales[locale])) {
const expected = resolveLinkedMessage(originalMergedLocales[locale], entry.key);
const actual = resolveLinkedMessage(nextMergedLocales[locale], entry.key);
if (actual !== expected) {
mismatches.push(
`${locale}.${entry.key}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
);
}
}
}
const remainingRawPhraseEntries = activeLocales.flatMap((locale) =>
flattenStringEntries(rawLocales[locale])
.filter((entry) => isCatalogPhraseKey(entry.key))
.map((entry) => `${locale}.${entry.key}`)
);
const remainingLocalePhraseEntries = activeLocales.flatMap((locale) =>
flattenStringEntries(globalMessages.locales?.[locale] ?? {})
.filter((entry) => isCatalogPhraseKey(entry.key))
.map((entry) => `${locale}.${entry.key}`)
);
return {
globalMessages,
rawLocales,
candidateCount: candidateKeys.length,
directSharedAliasCount,
generatedTemplateCount,
removedRawEntryCount,
removedLocaleEntryCount,
mismatches,
remainingRawPhraseEntries,
remainingLocalePhraseEntries,
};
};
const globalPath = path.join(localesDirectory, "global-v2.json");
const originalGlobalMessages = readJson(globalPath);
const originalRawLocales = readRawLocales();
const result = transformCatalogs(originalGlobalMessages, originalRawLocales);
console.log(
[
`candidate phrase keys: ${result.candidateCount}`,
`direct shared aliases: ${result.directSharedAliasCount}`,
`generated shared template aliases: ${result.generatedTemplateCount}`,
`removed raw locale entries: ${result.removedRawEntryCount}`,
`removed global locale entries: ${result.removedLocaleEntryCount}`,
].join("\n")
);
let hasErrors = false;
if (result.mismatches.length > 0) {
hasErrors = true;
console.error(`transformed output changed ${result.mismatches.length} strings`);
for (const mismatch of result.mismatches.slice(0, 30)) {
console.error(` ${mismatch}`);
}
}
if (result.remainingRawPhraseEntries.length > 0) {
hasErrors = true;
console.error(`raw locale phrase entries remain outside words/templates: ${result.remainingRawPhraseEntries.length}`);
for (const entry of result.remainingRawPhraseEntries.slice(0, 30)) {
console.error(` ${entry}`);
}
}
if (result.remainingLocalePhraseEntries.length > 0) {
hasErrors = true;
console.error(`global-v2 locale phrase entries remain: ${result.remainingLocalePhraseEntries.length}`);
for (const entry of result.remainingLocalePhraseEntries.slice(0, 30)) {
console.error(` ${entry}`);
}
}
if (hasErrors) {
process.exit(1);
}
if (mode === "apply") {
writeJson(globalPath, result.globalMessages);
for (const locale of activeLocales) {
writeJson(path.join(localesDirectory, `${locale}-v2.json`), result.rawLocales[locale]);
}
}
+341
View File
@@ -0,0 +1,341 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const localesDirectory = path.join(projectRoot, "src", "i18n", "generated");
const activeLocales = ["da", "en", "sv", "de", "no"];
const mode = process.argv.includes("--apply") ? "apply" : "check";
const templatePrefix = "templates.generated.compat";
const placeholderPattern = /\{[A-Za-z_][A-Za-z0-9_]*\}/g;
const exactLinkPattern = /^@(?:\.[\p{L}]+)?:(?:\{'[^']+'\}|[\p{L}\p{N}_.-]+)$/u;
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
const writeJson = (filePath, value) => {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
};
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
const flattenStringEntries = (value, prefix = "") => {
if (typeof value === "string") {
return [{ key: prefix, value }];
}
if (!isPlainObject(value)) {
return [];
}
return Object.entries(value).flatMap(([key, entry]) =>
flattenStringEntries(entry, prefix ? `${prefix}.${key}` : key)
);
};
const getValueAtPath = (value, keyPath) => {
let current = value;
for (const segment of keyPath.split(".")) {
if (!isPlainObject(current) || !(segment in current)) {
return undefined;
}
current = current[segment];
}
return current;
};
const ensureObjectAtPath = (value, keyPath) => {
let current = value;
if (!keyPath) {
return current;
}
for (const segment of keyPath.split(".")) {
if (!isPlainObject(current[segment])) {
current[segment] = {};
}
current = current[segment];
}
return current;
};
const setValueAtPath = (value, keyPath, nextValue) => {
const segments = keyPath.split(".");
const parent = ensureObjectAtPath(value, segments.slice(0, -1).join("."));
parent[segments.at(-1)] = nextValue;
};
const deleteValueAtPath = (value, keyPath) => {
const segments = keyPath.split(".");
const parents = [];
let current = value;
for (const segment of segments.slice(0, -1)) {
if (!isPlainObject(current) || !(segment in current)) {
return false;
}
parents.push([current, segment]);
current = current[segment];
}
if (!isPlainObject(current) || !(segments.at(-1) in current)) {
return false;
}
delete current[segments.at(-1)];
for (let index = parents.length - 1; index >= 0; index -= 1) {
const [parent, segment] = parents[index];
const child = parent[segment];
if (isPlainObject(child) && Object.keys(child).length === 0) {
delete parent[segment];
} else {
break;
}
}
return true;
};
const normalizePhrase = (value) =>
value
.normalize("NFKC")
.toLocaleLowerCase()
.replace(placeholderPattern, "{}")
.replace(/[\p{P}\p{S}]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
const linkToTemplate = (suffix) => `@:{'${templatePrefix}.${suffix}'}`;
const canonicalPriority = (suffix) => {
if (suffix.startsWith("common.")) return 0;
if (suffix.startsWith("global.")) return 1;
if (suffix.startsWith("nav.")) return 2;
if (suffix.startsWith("tables.common.")) return 3;
if (suffix.startsWith("objects.columns.")) return 4;
if (suffix.startsWith("auth.")) return 5;
if (suffix.startsWith("products.")) return 6;
return 10;
};
const chooseCanonicalSuffix = (suffixes) =>
[...suffixes].sort(
(left, right) =>
canonicalPriority(left) - canonicalPriority(right) ||
left.split(".").length - right.split(".").length ||
left.length - right.length ||
left.localeCompare(right, "en", { sensitivity: "base" })
)[0];
const createDisjointSet = (items) => {
const parent = new Map(items.map((item) => [item, item]));
const find = (item) => {
const currentParent = parent.get(item);
if (currentParent === item) {
return item;
}
const root = find(currentParent);
parent.set(item, root);
return root;
};
const union = (left, right) => {
const leftRoot = find(left);
const rightRoot = find(right);
if (leftRoot !== rightRoot) {
parent.set(rightRoot, leftRoot);
}
};
const groups = () => {
const grouped = new Map();
for (const item of items) {
const root = find(item);
const group = grouped.get(root) ?? [];
group.push(item);
grouped.set(root, group);
}
return [...grouped.values()];
};
return { union, groups };
};
const replaceTemplatePathReferences = (value, replacements) => {
if (typeof value === "string") {
let nextValue = value;
for (const [fromSuffix, toSuffix] of replacements.entries()) {
nextValue = nextValue.replaceAll(`${templatePrefix}.${fromSuffix}`, `${templatePrefix}.${toSuffix}`);
}
return nextValue;
}
if (Array.isArray(value)) {
return value.map((entry) => replaceTemplatePathReferences(entry, replacements));
}
if (!isPlainObject(value)) {
return value;
}
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, replaceTemplatePathReferences(entry, replacements)])
);
};
const readRawLocales = () =>
Object.fromEntries(
activeLocales.map((locale) => [locale, readJson(path.join(localesDirectory, `${locale}-v2.json`))])
);
const collectTemplateSuffixes = (rawLocales) => {
const suffixes = new Set();
for (const locale of activeLocales) {
for (const entry of flattenStringEntries(getValueAtPath(rawLocales[locale], templatePrefix) ?? {})) {
suffixes.add(entry.key);
}
}
return [...suffixes].sort((left, right) => left.localeCompare(right, "en", { sensitivity: "base" }));
};
const transformCatalogs = (originalGlobalMessages, originalRawLocales) => {
const rawLocales = cloneJson(originalRawLocales);
let globalMessages = cloneJson(originalGlobalMessages);
const suffixes = collectTemplateSuffixes(rawLocales);
const disjointSet = createDisjointSet(suffixes);
for (const locale of activeLocales) {
const groupedByLocalePhrase = new Map();
for (const suffix of suffixes) {
const value = getValueAtPath(rawLocales[locale], `${templatePrefix}.${suffix}`);
if (typeof value !== "string") {
continue;
}
const signature = normalizePhrase(value);
if (signature.length < 4) {
continue;
}
const group = groupedByLocalePhrase.get(signature) ?? [];
group.push(suffix);
groupedByLocalePhrase.set(signature, group);
}
for (const group of groupedByLocalePhrase.values()) {
if (group.length <= 1) {
continue;
}
const [first, ...rest] = group;
for (const suffix of rest) {
disjointSet.union(first, suffix);
}
}
}
const duplicateGroups = disjointSet.groups().filter((group) => group.length > 1);
const replacements = new Map();
for (const group of duplicateGroups) {
const canonicalSuffix = chooseCanonicalSuffix(group);
for (const suffix of group) {
if (suffix !== canonicalSuffix) {
replacements.set(suffix, canonicalSuffix);
}
}
}
for (const [fromSuffix, toSuffix] of [...replacements.entries()]) {
const seen = new Set([fromSuffix]);
let finalSuffix = toSuffix;
while (replacements.has(finalSuffix) && !seen.has(finalSuffix)) {
seen.add(finalSuffix);
finalSuffix = replacements.get(finalSuffix);
}
replacements.set(fromSuffix, finalSuffix);
}
if (replacements.size > 0) {
globalMessages = replaceTemplatePathReferences(globalMessages, replacements);
for (const locale of activeLocales) {
rawLocales[locale] = replaceTemplatePathReferences(rawLocales[locale], replacements);
}
}
let removedTemplateCount = 0;
let repointedAliasCount = 0;
for (const [fromSuffix, toSuffix] of replacements.entries()) {
setValueAtPath(globalMessages.shared, fromSuffix, linkToTemplate(toSuffix));
repointedAliasCount += 1;
for (const locale of activeLocales) {
if (deleteValueAtPath(rawLocales[locale], `${templatePrefix}.${fromSuffix}`)) {
removedTemplateCount += 1;
}
}
}
const remainingDuplicateGroups = [];
for (const locale of activeLocales) {
const nonLinkedTemplates = flattenStringEntries(getValueAtPath(rawLocales[locale], templatePrefix) ?? {}).filter(
(entry) => !exactLinkPattern.test(entry.value.trim())
);
const byPhrase = new Map();
for (const entry of nonLinkedTemplates) {
const signature = normalizePhrase(entry.value);
const group = byPhrase.get(signature) ?? [];
group.push(entry.key);
byPhrase.set(signature, group);
}
for (const [signature, group] of byPhrase.entries()) {
if (signature.length >= 4 && group.length > 1) {
remainingDuplicateGroups.push(`${locale}: ${signature} <- ${group.join(", ")}`);
}
}
}
return {
globalMessages,
rawLocales,
duplicateGroupCount: duplicateGroups.length,
replacementCount: replacements.size,
repointedAliasCount,
removedTemplateCount,
remainingDuplicateGroups,
};
};
const globalPath = path.join(localesDirectory, "global-v2.json");
const originalGlobalMessages = readJson(globalPath);
const originalRawLocales = readRawLocales();
const result = transformCatalogs(originalGlobalMessages, originalRawLocales);
console.log(
[
`duplicate generated template groups: ${result.duplicateGroupCount}`,
`template replacements: ${result.replacementCount}`,
`repointed shared aliases: ${result.repointedAliasCount}`,
`removed locale template entries: ${result.removedTemplateCount}`,
].join("\n")
);
if (result.remainingDuplicateGroups.length > 0) {
console.error(`near-duplicate generated templates remain: ${result.remainingDuplicateGroups.length}`);
for (const group of result.remainingDuplicateGroups.slice(0, 30)) {
console.error(` ${group}`);
}
process.exit(1);
}
if (mode === "apply") {
writeJson(globalPath, result.globalMessages);
for (const locale of activeLocales) {
writeJson(path.join(localesDirectory, `${locale}-v2.json`), result.rawLocales[locale]);
}
}
+390
View File
@@ -0,0 +1,390 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const localesDirectory = path.join(projectRoot, "src", "i18n", "generated");
const activeLocales = ["da", "en", "sv", "de", "no"];
const linkTokenPattern = /@(?:\.[\p{L}]+)?:(?:\{'[^']+'\}|[\p{L}\p{N}_.-]+)/gu;
const placeholderPattern = /\{[A-Za-z_][A-Za-z0-9_]*\}/g;
const wordTokenPattern = /[\p{L}\p{N}]+/gu;
const linkedMessagePattern = /^@(?<modifier>\.[\p{L}]+)?:(?:\{'(?<literal>[^']+)'\}|(?<path>[\p{L}\p{N}_.-]+))$/u;
const mode = process.argv.includes("--apply") ? "apply" : "check";
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
const writeJson = (filePath, value) => {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
};
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
const flattenStringEntries = (value, prefix = "") => {
if (typeof value === "string") {
return [{ key: prefix, value }];
}
if (!isPlainObject(value)) {
return [];
}
return Object.entries(value).flatMap(([key, entry]) =>
flattenStringEntries(entry, prefix ? `${prefix}.${key}` : key)
);
};
const getValueAtPath = (value, keyPath) => {
let current = value;
for (const segment of keyPath.split(".")) {
if (!isPlainObject(current) || !(segment in current)) {
return undefined;
}
current = current[segment];
}
return current;
};
const setValueAtPath = (value, keyPath, nextValue) => {
const segments = keyPath.split(".");
let current = value;
for (const segment of segments.slice(0, -1)) {
current = current[segment];
}
current[segments.at(-1)] = nextValue;
};
const compareStringValues = (left, right) => left.localeCompare(right, "en", { sensitivity: "base" });
const protectSegments = (value) => {
const protectedRanges = [];
for (const pattern of [linkTokenPattern, placeholderPattern]) {
pattern.lastIndex = 0;
for (const match of value.matchAll(pattern)) {
protectedRanges.push({ start: match.index ?? 0, end: (match.index ?? 0) + match[0].length });
}
}
protectedRanges.sort((a, b) => a.start - b.start || b.end - a.end);
const mergedRanges = [];
for (const range of protectedRanges) {
const previous = mergedRanges.at(-1);
if (previous && range.start <= previous.end) {
previous.end = Math.max(previous.end, range.end);
} else {
mergedRanges.push({ ...range });
}
}
const segments = [];
let offset = 0;
for (const range of mergedRanges) {
if (offset < range.start) {
segments.push({ protected: false, value: value.slice(offset, range.start) });
}
segments.push({ protected: true, value: value.slice(range.start, range.end) });
offset = range.end;
}
if (offset < value.length) {
segments.push({ protected: false, value: value.slice(offset) });
}
return segments;
};
const normalizeToken = (token) => token.normalize("NFKC").toLocaleLowerCase();
const isAuditedToken = (token) => /\p{L}/u.test(token);
const collectTokens = (value) => {
const tokens = [];
for (const segment of protectSegments(value)) {
if (segment.protected) {
continue;
}
wordTokenPattern.lastIndex = 0;
for (const match of segment.value.matchAll(wordTokenPattern)) {
const token = match[0];
if (isAuditedToken(token)) {
tokens.push(token);
}
}
}
return tokens;
};
const stripMarks = (value) => value.normalize("NFKD").replace(/\p{M}/gu, "");
const slugify = (token, usedKeys) => {
const base =
stripMarks(token)
.toLocaleLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, "_")
.replace(/^_+|_+$/g, "")
.replace(/_{2,}/g, "_") || "word";
const prefixed = /^\p{N}/u.test(base) ? `word_${base}` : base;
let key = prefixed;
let suffix = 2;
while (usedKeys.has(key)) {
key = `${prefixed}_${suffix}`;
suffix += 1;
}
usedKeys.add(key);
return key;
};
const isCapitalizedToken = (token) => {
const lower = token.toLocaleLowerCase();
return token === `${lower.charAt(0).toLocaleUpperCase()}${lower.slice(1)}` && token !== lower;
};
const isUpperToken = (token) => token.length > 1 && token === token.toLocaleUpperCase() && token !== token.toLocaleLowerCase();
const isLowerToken = (token) => token === token.toLocaleLowerCase() && token !== token.toLocaleUpperCase();
const linkedWord = (keyPath, modifier = "") => `@${modifier}:{'${keyPath}'}`;
const applyTextModifier = (value, modifier) => {
switch (modifier) {
case ".capitalize":
return `${value.charAt(0).toLocaleUpperCase()}${value.slice(1)}`;
case ".upper":
return value.toLocaleUpperCase();
case ".lower":
return value.toLocaleLowerCase();
default:
return value;
}
};
const modifierForToken = (token, word) => {
if (normalizeToken(token) !== normalizeToken(word.value)) {
return null;
}
const candidateModifiers = ["", ".upper", ".capitalize", ".lower"];
for (const modifier of candidateModifiers) {
if (applyTextModifier(word.value, modifier) === token) {
return modifier;
}
}
return null;
};
const replaceRepeatedTokens = (value, tokenToWord) =>
protectSegments(value)
.map((segment) => {
if (segment.protected) {
return segment.value;
}
return segment.value.replace(wordTokenPattern, (token) => {
const normalized = normalizeToken(token);
const word = tokenToWord.get(normalized);
if (!word || !isAuditedToken(token)) {
return token;
}
const exactVariant = word.variants?.get(token);
if (exactVariant) {
return linkedWord(exactVariant.path);
}
const modifier = modifierForToken(token, word);
if (modifier !== null) {
return linkedWord(word.path, modifier);
}
return token;
});
})
.join("");
const applyLinkedModifier = (value, modifier) => {
return applyTextModifier(value, modifier);
};
const resolveLinkedMessage = (messages, keyPath, seen = new Set()) => {
if (seen.has(keyPath)) {
return undefined;
}
seen.add(keyPath);
const value = getValueAtPath(messages, keyPath);
if (typeof value !== "string") {
return undefined;
}
const exactLink = value.trim().match(linkedMessagePattern);
if (exactLink) {
const resolved = resolveLinkedMessage(messages, exactLink.groups.literal ?? exactLink.groups.path, seen);
return resolved === undefined ? undefined : applyLinkedModifier(resolved, exactLink.groups.modifier);
}
return value.replace(linkTokenPattern, (token) => {
const match = token.match(linkedMessagePattern);
if (!match?.groups) {
return token;
}
const resolved = resolveLinkedMessage(messages, match.groups.literal ?? match.groups.path, new Set(seen));
if (resolved === undefined) {
return token;
}
return applyLinkedModifier(resolved, match.groups.modifier);
});
};
const reorderLocaleRoot = (localeMessages) => {
const { words, templates, ...rest } = localeMessages;
return { words, templates, ...rest };
};
const transformLocale = (locale, originalMessages) => {
const messages = cloneJson(originalMessages);
const entries = flattenStringEntries(messages).filter((entry) => !entry.key.startsWith("words."));
const tokenStats = new Map();
for (const entry of entries) {
for (const token of collectTokens(entry.value)) {
const normalized = normalizeToken(token);
const stat = tokenStats.get(normalized) ?? { count: 0, casings: new Map() };
stat.count += 1;
stat.casings.set(token, (stat.casings.get(token) ?? 0) + 1);
tokenStats.set(normalized, stat);
}
}
const repeatedTokens = [...tokenStats.entries()]
.filter(([, stat]) => stat.count > 1)
.sort(([left], [right]) => compareStringValues(left, right));
const existingWords = messages.words ?? {};
const generatedWords = {};
const tokenToWord = new Map();
const usedKeys = new Set(Object.keys(existingWords.generated ?? {}));
for (const [normalized, stat] of repeatedTokens) {
const existingWordEntry = flattenStringEntries(existingWords).find(
(entry) => normalizeToken(entry.value) === normalized
);
if (existingWordEntry) {
tokenToWord.set(normalized, { path: `words.${existingWordEntry.key}`, value: existingWordEntry.value });
continue;
}
const casings = [...stat.casings.entries()].sort(
([leftToken, leftCount], [rightToken, rightCount]) =>
rightCount - leftCount || compareStringValues(leftToken, rightToken)
);
const lowerCaseToken = casings.find(([token]) => token === normalized)?.[0];
const canonical = lowerCaseToken ?? casings[0][0];
const key = slugify(normalized, usedKeys);
generatedWords[key] = canonical;
const word = { path: `words.generated.${key}`, value: canonical, variants: new Map() };
for (const [casing] of casings) {
if (modifierForToken(casing, word) !== null) {
continue;
}
const variantKey = slugify(`${normalized}_${casing}`, usedKeys);
generatedWords[variantKey] = casing;
word.variants.set(casing, { path: `words.generated.${variantKey}`, value: casing });
}
tokenToWord.set(normalized, word);
}
messages.words = {
grammar: messages.words?.grammar ?? {},
actions: messages.words?.actions ?? {},
entities: messages.words?.entities ?? {},
statuses: messages.words?.statuses ?? {},
domain: messages.words?.domain ?? {},
services: messages.words?.services ?? {},
units: messages.words?.units ?? {},
...(messages.words ?? {}),
generated: {
...(messages.words?.generated ?? {}),
...Object.fromEntries(Object.entries(generatedWords).sort(([left], [right]) => compareStringValues(left, right))),
},
};
for (const entry of entries) {
const nextValue = replaceRepeatedTokens(entry.value, tokenToWord);
if (nextValue !== entry.value) {
setValueAtPath(messages, entry.key, nextValue);
}
}
const mismatches = [];
for (const entry of entries) {
const previousResolved = resolveLinkedMessage(originalMessages, entry.key);
const resolved = resolveLinkedMessage(messages, entry.key);
if (resolved !== previousResolved) {
mismatches.push(`${entry.key}: expected ${JSON.stringify(previousResolved)}, got ${JSON.stringify(resolved)}`);
}
}
return {
messages: reorderLocaleRoot(messages),
generatedWordCount: Object.keys(generatedWords).length,
repeatedTokenCount: repeatedTokens.length,
mismatches,
};
};
const countRepeatedTokensOutsideWords = (messages) => {
const counts = new Map();
for (const entry of flattenStringEntries(messages).filter((entry) => !entry.key.startsWith("words."))) {
for (const token of collectTokens(entry.value)) {
const normalized = normalizeToken(token);
counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
}
}
return [...counts.entries()].filter(([, count]) => count > 1);
};
let hasErrors = false;
for (const locale of activeLocales) {
const filePath = path.join(localesDirectory, `${locale}-v2.json`);
const originalMessages = readJson(filePath);
const result = transformLocale(locale, originalMessages);
const repeatedAfter = countRepeatedTokensOutsideWords(result.messages);
if (result.mismatches.length > 0) {
hasErrors = true;
console.error(`${locale}: transformed output changed ${result.mismatches.length} strings`);
for (const mismatch of result.mismatches.slice(0, 20)) {
console.error(` ${mismatch}`);
}
}
if (repeatedAfter.length > 0) {
hasErrors = true;
console.error(`${locale}: ${repeatedAfter.length} repeated token groups remain outside words`);
for (const [token, count] of repeatedAfter.slice(0, 20)) {
console.error(` ${token}: ${count}`);
}
}
console.log(
`${locale}: ${result.repeatedTokenCount} repeated token groups, ${result.generatedWordCount} new generated word entries`
);
if (mode === "apply" && !hasErrors) {
writeJson(filePath, result.messages);
}
}
if (hasErrors) {
process.exit(1);
}
+1 -1
View File
@@ -20,7 +20,7 @@ const redirect = (path) => {
<template>
<div>
<PageTitle :title="t('superuser.pages.departments.title')" :subtitle="t('superuser.pages.departments.subtitle')">
<PageTitle :title="t('common.departments')" :subtitle="t('superuser.pages.departments.subtitle')">
<template #buttons>
<button class="button is-dark" @click="showCreateDepartmentForm">
<span class="icon">
+1 -1
View File
@@ -11,7 +11,7 @@ const { t } = useI18n();
<template>
<div>
<PageTitle :title="t('superuser.pages.products.title')" :subtitle="t('superuser.pages.products.subtitle')">
<PageTitle :title="t('common.products')" :subtitle="t('superuser.pages.products.subtitle')">
<template #buttons>
<button class="button is-dark" @click="SessionUser.objects.products.showCreateObjectForm(loadList)">
<span class="icon">
@@ -186,12 +186,24 @@ const dropdownClass = computed(() => ({
const dropdownContentClass = computed(() => ({
"action-settings-wheel-dropdown-content--desktop-flyout": isDesktopFlyoutLayout.value,
}));
const dropdownMenuStyle = computed(() => {
if (!isFixedPosition.value) {
return {};
}
return {
position: "fixed",
zIndex: 10000,
left: "auto",
bottom: "auto",
paddingTop: 0,
paddingBottom: 0,
...fixedPositionStyles.value,
};
});
const dropdownContentStyle = computed(() => {
if (isFixedPosition.value) {
return {
position: "fixed",
zIndex: 10000,
...fixedPositionStyles.value,
maxHeight: dropdownMaxHeight.value ? `${dropdownMaxHeight.value}px` : "95vh",
overflowY: "auto",
overscrollBehavior: "contain",
@@ -477,7 +489,7 @@ const showSetCustomerPassword = (userId) => {
})
.catch((error) => {
Swal.fire({
title: t("admin.pos.settings_wheel.error"),
title: t("common.error"),
text: t("admin.pos.settings_wheel.error_changing_password"),
icon: "error",
});
@@ -682,7 +694,7 @@ const showCreateInvoicePeriodFlagForm = async (target) => {
},
showCancelButton: true,
confirmButtonText: t("admin.pos.settings_wheel.add_flag"),
cancelButtonText: t("global.cancel"),
cancelButtonText: t("common.cancel"),
});
if (!result.isConfirmed) {
@@ -777,7 +789,7 @@ const customerShortcutItems = computed(() => {
{
key: "customer-shortcut-vehicles",
icon: "fas fa-car",
label: t("admin.pos.settings_wheel.shortcut_vehicles"),
label: t("common.vehicles"),
clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/vehicles`, true),
},
];
@@ -1170,7 +1182,7 @@ const previewAttachment = async (attachment) => {
} catch (error) {
console.warn("Unable to preview attachment", attachment?.id, error);
Swal.fire({
title: t("admin.pos.settings_wheel.error"),
title: t("common.error"),
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
icon: "error",
});
@@ -1183,7 +1195,7 @@ const downloadAttachment = async (attachment) => {
downloadAttachmentFile(downloadLink, attachment);
} catch (error) {
Swal.fire({
title: t("admin.pos.settings_wheel.error"),
title: t("common.error"),
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
icon: "error",
});
@@ -1286,7 +1298,7 @@ const printAttachment = async (attachment) => {
} catch (error) {
console.warn("Unable to print attachment", attachment?.id, error);
Swal.fire({
title: t("admin.pos.settings_wheel.error"),
title: t("common.error"),
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
icon: "error",
});
@@ -1317,7 +1329,7 @@ const removeAttachment = async (attachment) => {
} catch (error) {
console.warn("Unable to delete attachment", attachment?.id, error);
Swal.fire({
title: t("admin.pos.settings_wheel.error"),
title: t("common.error"),
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
icon: "error",
});
@@ -1394,7 +1406,7 @@ const onShowImpersonationQRCode = (src, directLink) => {
showCancelButton: true,
focusConfirm: false,
confirmButtonText: t("admin.pos.settings_wheel.copy_link"),
cancelButtonText: t("admin.pos.settings_wheel.close"),
cancelButtonText: t("common.close"),
preConfirm: () => {
navigator.clipboard.writeText(directLink);
Swal.showValidationMessage(t("admin.pos.settings_wheel.link_copied"));
@@ -1449,7 +1461,7 @@ const isToggleMenuItem = (item) => item?.type === "toggle";
const downloadOrderAttachment = (attachment) =>
downloadAttachment(attachment).catch(() => {
Swal.fire({
title: t("admin.pos.settings_wheel.error"),
title: t("common.error"),
text: t("admin.pos.settings_wheel.error_downloading_attachment"),
icon: "error",
});
@@ -1497,7 +1509,7 @@ const showCompleteOrderBookingConfirmation = async () => {
showCancelButton: true,
confirmButtonText: t("admin.pos.settings_wheel.view_booking_new_tab"),
denyButtonText: t("tables.bookings.complete_wash_without_certificate"),
cancelButtonText: t("global.cancel"),
cancelButtonText: t("common.cancel"),
reverseButtons: true,
});
@@ -2167,6 +2179,38 @@ const setActiveDesktopFlyoutSection = (sectionKey) => {
}
};
const isOverflowClippingStyle = (style) =>
["overflow", "overflowX", "overflowY"].some((property) => {
const value = style[property];
return value && value !== "visible";
});
const isDropdownClippedByAncestor = (dropdownRect, triggerRect) => {
let el = dropdownRoot.value?.parentElement;
while (el && el !== document.body) {
const style = window.getComputedStyle(el);
if (isOverflowClippingStyle(style)) {
const ancestorRect = el.getBoundingClientRect();
const wouldClipCurrentDropdown =
dropdownRect.top < ancestorRect.top ||
dropdownRect.right > ancestorRect.right ||
dropdownRect.bottom > ancestorRect.bottom ||
dropdownRect.left < ancestorRect.left;
const wouldClipBelow = triggerRect.bottom + dropdownRect.height > ancestorRect.bottom;
const wouldClipAbove = triggerRect.top - dropdownRect.height < ancestorRect.top;
if (wouldClipCurrentDropdown || (wouldClipBelow && wouldClipAbove)) {
return true;
}
}
el = el.parentElement;
}
return false;
};
// Smart re-positioning of the flyout menu when either it's partially or fully hidden
const syncDesktopFlyoutPosition = () => {
if (typeof window === "undefined") {
@@ -2182,9 +2226,9 @@ const syncDesktopFlyoutPosition = () => {
const contentHeight = dropdownContentEl.scrollHeight;
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const contentRect = dropdownContentEl.getBoundingClientRect();
// Check if we need fixed position (parent too small)
let needsFixed = false;
let needsFixed = isDropdownClippedByAncestor(contentRect, triggerRect);
if (isDesktopFlyoutLayout.value) {
let el = dropdownRoot.value.parentElement;
while (el && el !== document.body && el !== null) {
@@ -2218,7 +2262,7 @@ const syncDesktopFlyoutPosition = () => {
const nextFixedStyles = {
top: `${top}px`,
right: `${viewportWidth - triggerRect.right}px`,
width: "fit-content",
width: "max-content",
};
if (
@@ -2309,7 +2353,7 @@ const syncDesktopFlyoutPosition = () => {
<span v-if="props.label.length > 0" class="ml-2">{{ props.label }}</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-menu" id="dropdown-menu" role="menu" :style="dropdownMenuStyle">
<div
ref="dropdownContent"
class="dropdown-content"
@@ -2460,7 +2504,7 @@ const syncDesktopFlyoutPosition = () => {
<span class="action-settings-wheel-attachment-action__icon">
<i class="fas fa-download" aria-hidden="true"></i>
</span>
<span>{{ t("global.download") }}</span>
<span>{{ t("common.download") }}</span>
</button>
<button
type="button"
@@ -28,10 +28,10 @@ if (departments.value.length === 0) {
<th>{{ $t('objects.columns.department') }}</th>
<th>{{ $t('tables.bookings.reg_tractor') }}</th>
<th>{{ $t('tables.bookings.reg_trailer') }}</th>
<th>{{ $t('objects.columns.reference') }}</th>
<th>{{ $t('common.reference') }}</th>
<th>{{ $t('objects.columns.notes') }}</th>
<th>{{ $t('objects.columns.date') }}</th>
<th>{{ $t('objects.columns.status') }}</th>
<th>{{ $t('common.date') }}</th>
<th>{{ $t('common.status') }}</th>
<th>{{ $t('objects.bookings.columns.contact_email') }}</th>
<th>{{ $t('objects.bookings.columns.wash_certificate_email') }}</th>
<th>{{ $t('tables.actions') }}</th>
@@ -53,7 +53,7 @@ if (departments.value.length === 0) {
<td>{{ booking.washCertificateEmail }}</td>
<td>
<div class="buttons">
<button class="button is-small is-dark" @click="redirectDepartmentOrderPage(booking.id, booking.department_id)">{{ $t('global.show') }}</button>
<button class="button is-small is-dark" @click="redirectDepartmentOrderPage(booking.id, booking.department_id)">{{ $t('common.show') }}</button>
</div>
</td>
</tr>
@@ -70,7 +70,7 @@ const hasUpdatePermission = () => {
:editFunction="su_object.showEditObjectFieldForm"
column="enabled"
:parse-function="(value) => {
return value ? t('global.yes') : t('global.no');
return value ? t('common.yes') : t('common.no');
}"
:permission-check-function="hasUpdatePermission"
/>
@@ -246,7 +246,7 @@ const getScanBadges = (scan) => {
if (!scan.seen_before) {
badges.push({
key: 'unknown',
text: t('admin.pos.status_unknown'),
text: t('common.new'),
className: 'pos-scan-badge--unknown',
});
}
@@ -560,7 +560,7 @@ setTimeout(() => {
<span class="icon">
<i class="fas fa-redo"></i>
</span>
<span>{{ $t('admin.pos.try_again') }}</span>
<span>{{ $t('common.retry') }}</span>
</button>
</div>
</article>
@@ -540,14 +540,14 @@ const deleteOrderItem = async (orderItemId) => {
<table v-if="false" class="table is-fullwidth pos-order-items__table">
<thead>
<tr>
<th>{{ $t("admin.pos.product") }}</th>
<th>{{ $t("common.product") }}</th>
<th style="width: 20%; text-align: right">{{ $t("admin.pos.price_dkk") }}</th>
<th
v-if="editPossible"
class="pos-order-items__actions-header"
:style="{ width: isOrderDetailVariant ? '18%' : '15%' }"
>
{{ isOrderDetailVariant ? $t("global.actions") : "" }}
{{ isOrderDetailVariant ? $t("common.actions") : "" }}
</th>
</tr>
</thead>
@@ -121,7 +121,7 @@ const createCustomerWishField = ({
const referenceField = createCustomerWishField({
key: "reference",
label: computed(() => t("pos.order.reference")),
label: computed(() => t("common.reference")),
source: () => props.reference,
saveValue: (value) => SessionUser.objects.orders.set.reference(props.order_id, value),
testIdBase: "pos-order-customer-wishes-reference",
@@ -186,7 +186,7 @@ watch(normalizedOrderId, async (nextOrderId, previousOrderId) => {
class="pos-inline-tag"
data-testid="pos-order-inline-created"
>
<span class="pos-inline-tag__label">{{ $t('pos.order.created') }}:</span>
<span class="pos-inline-tag__label">{{ $t('common.created') }}:</span>
<span class="pos-inline-tag__value">{{ resolvedCreatedAt }}</span>
</span>
</div>
@@ -107,7 +107,7 @@ const getActionsCellAttrs = () => ({
>
<b-table-column
field="product"
:label="$t('admin.pos.product')"
:label="$t('common.product')"
v-slot="tableProps"
>
<div class="pos-order-items__product-cell" :data-testid="`pos-order-item-name-${tableProps.row.orderItem.id}`">
@@ -192,7 +192,7 @@ const getActionsCellAttrs = () => ({
<b-table-column
v-if="showActions"
field="actions"
:label="$t('global.actions')"
:label="$t('common.actions')"
custom-key="actions"
header-class="pos-order-items__actions-header"
:th-attrs="getActionsHeaderAttrs"
@@ -116,12 +116,12 @@ const saveChanges = async () => {
>
<div class="pos-order-item-edit-modal__header">
<p class="title is-5 mb-1">{{ t('admin.pos.edit') }}</p>
<p class="is-size-7 has-text-grey mb-0">{{ t('admin.pos.product') }} #{{ orderItem.id }}</p>
<p class="is-size-7 has-text-grey mb-0">{{ t('common.product') }} #{{ orderItem.id }}</p>
</div>
<div class="columns is-multiline">
<div class="column is-12">
<label class="label" for="pos-order-item-edit-name">{{ t('admin.pos.product') }}</label>
<label class="label" for="pos-order-item-edit-name">{{ t('common.product') }}</label>
<input
id="pos-order-item-edit-name"
class="input"
@@ -133,7 +133,7 @@ const saveChanges = async () => {
</div>
<div class="column is-12-mobile is-6-tablet">
<label class="label" for="pos-order-item-edit-price">{{ t('global.price') }}</label>
<label class="label" for="pos-order-item-edit-price">{{ t('common.price') }}</label>
<input
id="pos-order-item-edit-price"
v-model="form.price"
@@ -147,7 +147,7 @@ const saveChanges = async () => {
</div>
<div class="column is-12-mobile is-6-tablet">
<label class="label" for="pos-order-item-edit-quantity">{{ t('global.quantity') }}</label>
<label class="label" for="pos-order-item-edit-quantity">{{ t('common.quantity') }}</label>
<input
id="pos-order-item-edit-quantity"
v-model="form.quantity"
@@ -173,7 +173,7 @@ const saveChanges = async () => {
</div>
<div class="column is-12">
<label class="label" for="pos-order-item-edit-reference">{{ t('objects.columns.reference') }}</label>
<label class="label" for="pos-order-item-edit-reference">{{ t('common.reference') }}</label>
<input
id="pos-order-item-edit-reference"
v-model="form.reference"
@@ -64,7 +64,7 @@ const getRowClass = (row) => {
>
<b-table-column
field="product"
:label="$t('admin.pos.product')"
:label="$t('common.product')"
v-slot="tableProps"
>
<div class="pos-order-items__product-cell" :data-testid="`pos-order-item-name-${tableProps.row.orderItem.id}`">
@@ -80,9 +80,9 @@ const getOrderItemBasePrice = (orderItem) => {
<tr>
<th>{{ $t('tables.products.name') }}</th>
<th>{{ $t('global.note') }}</th>
<th>{{ $t('objects.columns.reference') }}</th>
<th>{{ $t('global.quantity') }}</th>
<th style="text-align: right;">{{ $t('global.price') }}</th>
<th>{{ $t('common.reference') }}</th>
<th>{{ $t('common.quantity') }}</th>
<th style="text-align: right;">{{ $t('common.price') }}</th>
</tr>
</thead>
<tbody>
@@ -121,7 +121,7 @@ const getOrderItemBasePrice = (orderItem) => {
<tfoot>
<!-- Tax -->
<tr v-if="props.taxPercentage > 0">
<td colspan="4">{{ $t('global.tax') }} ({{ props.taxPercentage }}%)</td>
<td colspan="4">{{ $t('common.tax') }} ({{ props.taxPercentage }}%)</td>
<td style="text-align: right;" v-if="props.isLoading">
<!-- Skeleton-lines 1 row -->
<div class="skeleton-lines"><div style="width: 100%;"></div></div>
@@ -294,7 +294,7 @@ const uploadOrderAttachmentFile = async (file) => {
} catch (error) {
console.error("Error uploading order attachment:", error);
Swal.fire({
title: t("admin.pos.settings_wheel.error"),
title: t("common.error"),
text: t("admin.pos.attachments_upload_error"),
icon: "error",
});
@@ -857,7 +857,7 @@ const formatCashierName = (order) => {
@click="invoiceSelectedCollections()"
:disabled="selectedInvoiceCollections.length === 0 || isInvoiceQueueBusy"
>
{{ $t("global.invoice") }} {{ $t("global.selected_multiple") }} ({{
{{ $t("common.invoice") }} {{ $t("global.selected_multiple") }} ({{
selectedInvoiceCollections.length
}})
</button>
@@ -870,8 +870,8 @@ const formatCashierName = (order) => {
'is-dark': isInvoiceCollectionSelectedAll(),
}"
>
{{ isInvoiceCollectionSelectedAll() ? $t("global.unselect") : $t("global.select") }}
{{ $t("global.all").toLowerCase() }}
{{ isInvoiceCollectionSelectedAll() ? $t("global.unselect") : $t("common.select") }}
{{ $t("common.all").toLowerCase() }}
</button>
<!-- Expand / Collapse all invoice collections -->
<button
@@ -883,7 +883,7 @@ const formatCashierName = (order) => {
}"
>
{{ isAutoExpandAll() ? $t("global.collapse") : $t("global.expand") }}
{{ $t("global.all").toLowerCase() }}
{{ $t("common.all").toLowerCase() }}
</button>
</div>
</td>
@@ -267,10 +267,10 @@ const bookingSelectionObjects = computed(() => {
const contentSegments = [
`${t("admin.pos.order_booking_selector.customer_label")}: ${booking?.customer_name || t("admin.pos.not_found")}`,
`${t("admin.pos.order_booking_selector.plates_label")}: ${plateText || t("admin.pos.not_found")}`,
`${t("admin.pos.order_booking_selector.reference_label")}: ${
`${t("common.reference")}: ${
getOrderBookingReferenceValue(booking) || t("admin.pos.not_found")
}`,
`${t("admin.pos.order_booking_selector.services_label")}: ${
`${t("common.services")}: ${
getOrderBookingServiceText(booking) || t("admin.pos.not_found")
}`,
];
@@ -284,7 +284,7 @@ const bookingSelectionObjects = computed(() => {
content: contentSegments.join(" • "),
buttons: [
{
label: t("admin.pos.order_booking_selector.use_booking"),
label: t("common.select"),
action: () => handleBookingSelection(booking),
color: "primary",
testId: `pos-desktop-order-booking-use-${booking.id}`,
@@ -297,7 +297,7 @@ const bookingSelectionObjects = computed(() => {
const duplicateDetailsObjects = computed(() => {
return duplicateOrders.value.map((order) => ({
id: Number(order.id),
label: `${t("admin.pos.order")} #${order.id} - ${formatDuplicateOrderDate(order.created_at)}`,
label: `${t("common.order")} #${order.id} - ${formatDuplicateOrderDate(order.created_at)}`,
content: getDuplicateOrderContent(order),
buttons: [
{
@@ -126,7 +126,7 @@ const closeModal = () => {
data-testid="pos-desktop-customer-conflict-cancel"
@click="emit('cancel')"
>
{{ t("admin.pos.customer_conflict.cancel") }}
{{ t("common.cancel") }}
</button>
<button
class="button is-primary is-light"
@@ -128,7 +128,7 @@ const lastWashMetadata = computed(() => {
return [
{
key: "created",
label: t("pos.order.created"),
label: t("common.created"),
value: formatDateTime(currentOrder.created_at),
},
{
@@ -394,7 +394,7 @@ const closeModal = () => {
class="tag pos-desktop-order-booking-card__today-tag"
:data-testid="`pos-desktop-order-booking-today-${booking.id}`"
>
{{ t("global.time.today") }}
{{ t("common.today") }}
</span>
<span v-if="didBookingDetailFail(booking)" class="tag is-light pos-desktop-order-booking-card__tag">
{{ t("admin.pos.not_found") }}
@@ -431,7 +431,7 @@ const closeModal = () => {
<dd>{{ getBookingPlates(booking) || t("admin.pos.not_found") }}</dd>
</div>
<div class="pos-desktop-order-booking-card__meta-item">
<dt>{{ t("admin.pos.order_booking_selector.reference_label") }}</dt>
<dt>{{ t("common.reference") }}</dt>
<dd>{{ getBookingReference(booking) || "—" }}</dd>
</div>
<div v-if="getBookingPo(booking)" class="pos-desktop-order-booking-card__meta-item">
@@ -455,7 +455,7 @@ const closeModal = () => {
>
<div class="pos-desktop-order-booking-card__section-header">
<p class="pos-desktop-order-booking-card__section-title">
{{ t("admin.pos.order_booking_selector.services_label") }}
{{ t("common.services") }}
</p>
<div
v-if="!isBookingLoading(booking) && getBookingItemPreviewTotal(booking) !== null"
@@ -476,8 +476,8 @@ const closeModal = () => {
class="pos-desktop-order-booking-card__item-preview"
>
<div class="pos-desktop-order-booking-card__item-preview-head">
<span>{{ t("global.quantity") }}</span>
<span>{{ t("admin.pos.order_booking_selector.services_label") }}</span>
<span>{{ t("common.quantity") }}</span>
<span>{{ t("common.services") }}</span>
<span>{{ t("admin.pos.price_dkk") }}</span>
</div>
<div
@@ -529,7 +529,7 @@ const closeModal = () => {
:data-testid="`pos-desktop-order-booking-use-${booking.id}`"
@click="onSelect(booking)"
>
{{ t("admin.pos.order_booking_selector.use_booking") }}
{{ t("common.select") }}
</button>
</div>
</article>
@@ -326,7 +326,7 @@ const onClickAttachWashCertificate = () => {
</template>
<template #footer>
<div class="card-footer-item">
{{ t('admin.pos.created') }}: {{ new Date(attachment.created_at).toLocaleString() }}
{{ t('common.created') }}: {{ new Date(attachment.created_at).toLocaleString() }}
</div>
<a class="card-footer-item" v-if="!attachment.deleted_at" @click="onClickDelete(attachment.id)">
<!-- Delete button or action can be placed here -->
@@ -293,7 +293,7 @@ const isAttachmentsVisible = () => {
</div>
<div class="column is-narrow pl-0">
<p class="is-size-7 has-text-weight-semibold">
{{ t("admin.pos.reference") }}
{{ t("common.reference") }}
</p>
</div>
</div>
@@ -132,7 +132,7 @@ const promptForReference = async (initialValue = "") => {
title: t("admin.pos.reference_required_title"),
text: t("admin.pos.reference_required_text"),
input: "text",
inputLabel: t("admin.pos.reference"),
inputLabel: t("common.reference"),
inputValue: initialValue,
showCancelButton: true,
confirmButtonText: t("admin.pos.confirm"),
@@ -339,7 +339,7 @@ const isBookingScheduledForToday = (booking: any) => {
class="tag booking-option__today-tag"
:data-testid="`pos-mobile-order-booking-today-${booking.id}`"
>
{{ t("global.time.today") }}
{{ t("common.today") }}
</span>
<span v-if="didBookingDetailFail(booking)" class="tag is-light booking-option__tag">
{{ t("admin.pos.not_found") }}
@@ -373,7 +373,7 @@ const isBookingScheduledForToday = (booking: any) => {
<dd>{{ getBookingPlates(booking) || t("admin.pos.not_found") }}</dd>
</div>
<div class="booking-option__meta-item">
<dt>{{ t("admin.pos.order_booking_selector.reference_label") }}</dt>
<dt>{{ t("common.reference") }}</dt>
<dd>{{ getBookingReference(booking) || "-" }}</dd>
</div>
<div v-if="getBookingPo(booking)" class="booking-option__meta-item">
@@ -392,7 +392,7 @@ const isBookingScheduledForToday = (booking: any) => {
:data-testid="`pos-mobile-order-booking-services-${booking.id}`"
>
<div class="booking-option__section-header">
<p class="booking-option__section-title">{{ t("admin.pos.order_booking_selector.services_label") }}</p>
<p class="booking-option__section-title">{{ t("common.services") }}</p>
<div
v-if="!isBookingLoading(booking) && getBookingItemPreviewTotal(booking) !== null"
class="booking-option__section-total"
@@ -409,8 +409,8 @@ const isBookingScheduledForToday = (booking: any) => {
</div>
<div v-else-if="getBookingItemPreviewRows(booking).length > 0" class="booking-option__item-preview">
<div class="booking-option__item-preview-head">
<span>{{ t("global.quantity") }}</span>
<span>{{ t("admin.pos.order_booking_selector.services_label") }}</span>
<span>{{ t("common.quantity") }}</span>
<span>{{ t("common.services") }}</span>
<span>{{ t("admin.pos.price_dkk") }}</span>
</div>
<div
@@ -456,7 +456,7 @@ const isBookingScheduledForToday = (booking: any) => {
:data-testid="`pos-mobile-order-booking-use-${booking.id}`"
@click="onSelect(booking)"
>
{{ t("admin.pos.order_booking_selector.use_booking") }}
{{ t("common.select") }}
</button>
</div>
</article>
@@ -133,7 +133,7 @@ const defaultActionButtons = computed<{ [key: string]: PosActionButton }>(() =>
return {
/** General purpose action buttons (used by PosAddons)*/
confirm: {
label: t("admin.pos.action_buttons.confirm"),
label: t("common.confirm"),
description: t("admin.pos.action_buttons.confirm_desc"),
onClick: () => {
clearPopup(); // Close the popup
@@ -141,7 +141,7 @@ const defaultActionButtons = computed<{ [key: string]: PosActionButton }>(() =>
color: "primary",
},
cancel: {
label: t("admin.pos.action_buttons.cancel"),
label: t("common.cancel"),
description: t("admin.pos.action_buttons.cancel_desc"),
onClick: () => {
clearPopup(); // Close the popup
@@ -149,7 +149,7 @@ const defaultActionButtons = computed<{ [key: string]: PosActionButton }>(() =>
color: "light",
},
close: {
label: t("admin.pos.action_buttons.close"),
label: t("common.close"),
description: t("admin.pos.action_buttons.close_desc"),
onClick: () => {
clearPopup(); // Close the popup
@@ -183,7 +183,7 @@ const handleMouseWheel = (e: WheelEvent) => {
<span class="icon is-small">
<i class="fas fa-times"></i>
</span>
<small>{{ t("global.close") }}</small>
<small>{{ t("common.close") }}</small>
</button>
</div>
</div>
@@ -286,7 +286,7 @@ const tableHeaders = ref([
},
{
name: 'reference',
title: t('objects.columns.reference'),
title: t('common.reference'),
sortable: true,
},
{
@@ -296,7 +296,7 @@ const tableHeaders = ref([
},
{
name: 'created_at',
title: t('objects.columns.date'),
title: t('common.date'),
sortable: true,
},
{
@@ -50,7 +50,7 @@ const handleFileUpload = async (event) => {
await fetchAttachments();
Swal.fire({
icon: "success",
title: t('modals.success'),
title: t('common.success'),
text: t('modals.file_uploaded'),
timer: 1500,
showConfirmButton: false,
@@ -59,7 +59,7 @@ const handleFileUpload = async (event) => {
console.error("Error uploading attachment:", error);
Swal.fire({
icon: "error",
title: t('modals.error'),
title: t('common.error'),
text: t('modals.upload_error'),
});
} finally {
@@ -96,7 +96,7 @@ const deleteAttachment = async (attachmentId) => {
console.error("Error deleting attachment:", error);
Swal.fire({
icon: "error",
title: t('modals.error'),
title: t('common.error'),
text: t('modals.delete_error'),
});
}
@@ -117,7 +117,7 @@ const downloadAttachment = async (attachmentId) => {
console.error("Error downloading attachment:", error);
Swal.fire({
icon: "error",
title: t('modals.error'),
title: t('common.error'),
text: t('modals.download_error'),
});
}
@@ -145,7 +145,7 @@ onMounted(() => {
<thead>
<tr>
<th>{{ t('modals.filename') }}</th>
<th class="has-text-right">{{ t('modals.actions') }}</th>
<th class="has-text-right">{{ t('common.actions') }}</th>
</tr>
</thead>
<tbody>
@@ -190,7 +190,7 @@ const getScopeLabel = (object) => {
<td>{{ object.order_priority }}</td>
<td>{{ object.task }}</td>
<td>{{ object.description }}</td>
<td>{{ object.condition_id ? SessionUser.objects.self_serve_conditions.functions.getConditionName(object.condition_id) : $t('global.all') }}</td>
<td>{{ object.condition_id ? SessionUser.objects.self_serve_conditions.functions.getConditionName(object.condition_id) : $t('common.all') }}</td>
<td>{{ getScopeLabel(object) }}</td>
<td>{{ SessionUser.objects.self_serve_tasks.columns.services.parse(object.services) }}</td>
<td>{{ SessionUser.objects.self_serve_tasks.columns.buttons.parse(object.buttons) }}</td>
@@ -54,7 +54,7 @@ const props = defineProps({
:loadList="loadList"
column="value"
:edit-function="SessionUser.objects.self_serve_vehicle_conditions.showEditObjectFieldForm"
:parse-function="(val) => val ? t('global.yes') : t('global.no')"
:parse-function="(val) => val ? t('common.yes') : t('common.no')"
:permission-check-function="SessionUser.canAccessAdmin"
/>
<EditableTableColumn
@@ -34,10 +34,10 @@ if (departments.value.length === 0) {
<th>{{ $t('objects.columns.department') }}</th>
<th>{{ $t('tables.bookings.reg_tractor') }}</th>
<th>{{ $t('tables.bookings.reg_trailer') }}</th>
<th>{{ $t('objects.columns.reference') }}</th>
<th>{{ $t('common.reference') }}</th>
<th>{{ $t('objects.columns.notes') }}</th>
<th>{{ $t('objects.columns.date') }}</th>
<th>{{ $t('objects.columns.status') }}</th>
<th>{{ $t('common.date') }}</th>
<th>{{ $t('common.status') }}</th>
<th>{{ $t('objects.bookings.columns.contact_email') }}</th>
<th>{{ $t('objects.bookings.columns.wash_certificate_email') }}</th>
<th>{{ $t('tables.actions') }}</th>
@@ -59,7 +59,7 @@ if (departments.value.length === 0) {
<td>{{ booking.washCertificateEmail }}</td>
<td>
<div class="buttons">
<button class="button is-small is-dark" @click="redirectDepartmentOrderPage(booking.id, booking.department_id)">{{ $t('global.show') }}</button>
<button class="button is-small is-dark" @click="redirectDepartmentOrderPage(booking.id, booking.department_id)">{{ $t('common.show') }}</button>
</div>
</td>
</tr>
@@ -598,7 +598,7 @@ onBeforeUnmount(() => {
data-testid="draft-order-assign-cancel"
@click="closeModal"
>
{{ t("global.cancel") }}
{{ t("common.cancel") }}
</button>
<button
class="button is-link"
@@ -107,7 +107,7 @@ loadUser();
<div class="modal-background" @click="closeModal"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{getUserKey('customer_name', t('modals.unknown_customer'), t('modals.loading'))}}</p>
<p class="modal-card-title">{{getUserKey('customer_name', t('modals.unknown_customer'), t('common.loading'))}}</p>
<button class="delete" aria-label="close" @click="closeModal"></button>
</header>
<section class="modal-card-body">
@@ -150,7 +150,7 @@ loadUser();
<p>{{ t('modals.fetching_data') }}</p>
<p>{{ t('modals.please_wait') }}</p>
</div>
<progress class="progress is-small is-dark" max="100">{{ t('modals.loading') }}</progress>
<progress class="progress is-small is-dark" max="100">{{ t('common.loading') }}</progress>
</template>
</section>
<footer class="modal-card-foot">
@@ -93,7 +93,7 @@ if (props.autoLoad) {
data-testid="superuser-complaints-department-filter"
@change="onDepartmentFilterChange($event.target.value)"
>
<option value="*">{{ t('global.all') }}</option>
<option value="*">{{ t('common.all') }}</option>
<option
v-for="department in departments"
:key="department.id"
@@ -64,7 +64,7 @@ if (props.autoLoad) {
<template #paginationColumns>
<div class="column is-narrow my-3 department-status-filter">
<label class="label is-small" for="department-archive-filter">{{
t("objects.departments.filters.status")
t("common.status")
}}</label>
<div class="control">
<div class="select">
@@ -241,7 +241,7 @@ const doesEndpointMatch = (matcher) => {
<div class="control">
<div class="select">
<select @change="setFilter('booked_invoice_id', $event.target.value, true);">
<option value="*" :selected="!props.applyDefaultFilters">{{ t('pagination.all') }}</option>
<option value="*" :selected="!props.applyDefaultFilters">{{ t('common.all') }}</option>
<option value="not null">{{ t('pagination.booked') }}</option>
<option value="is_null" :selected="props.applyDefaultFilters">{{ t('pagination.not_booked') }}</option>
</select>
@@ -254,9 +254,9 @@ const doesEndpointMatch = (matcher) => {
<div class="control">
<div class="select">
<select @change="setFilter('error_message', $event.target.value, true);">
<option value="*" selected>{{ t('pagination.all') }}</option>
<option value="not null">{{ t('pagination.yes') }}</option>
<option value="is_null">{{ t('pagination.no') }}</option>
<option value="*" selected>{{ t('common.all') }}</option>
<option value="not null">{{ t('common.yes') }}</option>
<option value="is_null">{{ t('common.no') }}</option>
</select>
</div>
</div>
@@ -267,7 +267,7 @@ const doesEndpointMatch = (matcher) => {
<div class="control">
<div class="select">
<select @change="setFilter('processor', $event.target.value, true);">
<option value="*" selected>{{ t('pagination.all') }}</option>
<option value="*" selected>{{ t('common.all') }}</option>
<option value="1">E-conomic</option>
<option value="2">Stripe</option>
<option value="3">{{ t('pagination.other_no_tracking') }}</option>
@@ -282,7 +282,7 @@ const doesEndpointMatch = (matcher) => {
<div class="control">
<div class="select">
<select @change="setFilter('completed_at', $event.target.value, true);">
<option value="*" selected>{{ t('pagination.all') }}</option>
<option value="*" selected>{{ t('common.all') }}</option>
<option value="not null">{{ t('pagination.completed') }}</option>
<option :value="'is_null'">{{ t('pagination.not_completed') }}</option>
</select>
@@ -295,7 +295,7 @@ const doesEndpointMatch = (matcher) => {
<div class="control">
<div class="select">
<select @change="setFilter('customer_id-has_attribute', $event.target.value, true);">
<option value="*" selected>{{ t('pagination.all') }}</option>
<option value="*" selected>{{ t('common.all') }}</option>
<option value="invoiceAllOrdersIndividually">{{ t('pagination.invoice_per_order') }}</option>
<option value="!invoiceAllOrdersIndividually">{{ t('pagination.invoice_per_month') }}</option>
</select>
@@ -308,7 +308,7 @@ const doesEndpointMatch = (matcher) => {
<div class="control">
<div class="select">
<select @change="setFilter('customer_id-has_key-OtherSpecialArrangement', $event.target.value, true);">
<option value="*" :selected="!props.applyDefaultFilters">{{ t('pagination.all') }}</option>
<option value="*" :selected="!props.applyDefaultFilters">{{ t('common.all') }}</option>
<option value="OtherSpecialArrangement">{{ t('pagination.has_special_agreement') }}</option>
<option value="!OtherSpecialArrangement" :selected="props.applyDefaultFilters">{{ t('pagination.no_special_agreement') }}</option>
</select>
@@ -321,7 +321,7 @@ const doesEndpointMatch = (matcher) => {
<div class="control">
<div class="select">
<select @change="setFilter('customer_id-has_key-OtherVaskeabonnement', $event.target.value, true);">
<option value="*" :selected="!props.applyDefaultFilters">{{ t('pagination.all') }}</option>
<option value="*" :selected="!props.applyDefaultFilters">{{ t('common.all') }}</option>
<option value="OtherVaskeabonnement">{{ t('pagination.has_wash_subscription') }}</option>
<option value="!OtherVaskeabonnement" :selected="props.applyDefaultFilters">{{ t('pagination.no_wash_subscription') }}</option>
</select>
@@ -116,7 +116,7 @@ if (props.autoLoad) {
</div>
<!-- Filter by status -->
<div class="column is-narrow my-3">
<label class="label">{{ t('pagination.status') }}</label>
<label class="label">{{ t('common.status') }}</label>
<div class="control">
<div class="select">
<select @change="setShowClosedInvoices($event.target.value === 'true')" v-model="showClosedInvoices">
@@ -108,11 +108,11 @@ const showNewOrderBookingsPortal = () => {
<template #paginationColumns>
<!-- Filter by status -->
<div class="column is-narrow my-3">
<label class="label">{{ t('pagination.status') }}</label>
<label class="label">{{ t('common.status') }}</label>
<div class="control">
<div class="select">
<select @change="setFilter('status', $event.target.value)" v-model="selectedStatus">
<option value="*">{{ t('pagination.all') }}</option>
<option value="*">{{ t('common.all') }}</option>
<option value="pending">{{ t('pagination.pending') }}</option>
<option value="completed">{{ t('pagination.completed') }}</option>
<option value="cancelled">{{ t('pagination.cancelled') }}</option>
@@ -126,9 +126,9 @@ const showNewOrderBookingsPortal = () => {
<div class="control">
<div class="select">
<select @change="setFilter('pickup_bool', $event.target.value)">
<option value="*">{{ t('pagination.all') }}</option>
<option value="1">{{ t('pagination.yes') }}</option>
<option value="0">{{ t('pagination.no') }}</option>
<option value="*">{{ t('common.all') }}</option>
<option value="1">{{ t('common.yes') }}</option>
<option value="0">{{ t('common.no') }}</option>
</select>
</div>
</div>
@@ -139,7 +139,7 @@ const showNewOrderBookingsPortal = () => {
<div class="control">
<div class="select">
<select @change="setFilter('department', $event.target.value)">
<option value="*">{{ t('pagination.all') }}</option>
<option value="*">{{ t('common.all') }}</option>
<option v-for="department in departments" :key="department.id" :value="department.id">{{ department.name }}</option>
</select>
</div>
@@ -148,7 +148,7 @@ const showNewOrderBookingsPortal = () => {
<div class="column is-auto-fill my-3" v-if="!isSmall"/>
<!-- Only show the bookings for today (Switch, if the route is /user) -->
<div class="column is-narrow my-3" v-if="router.currentRoute.value.path.startsWith('/user')" :class="{ 'has-text-right': !isSmall }">
<label class="label">{{ isSmall ? t('pagination.today') : t('pagination.show_only_today') }}</label>
<label class="label">{{ isSmall ? t('common.today') : t('pagination.show_only_today') }}</label>
<div class="control">
<div class="field">
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? new Date().toISOString().split('T')[0] : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
@@ -116,11 +116,11 @@ onMounted(() => {
<!-- Status filter -->
<div class="column is-narrow">
<div class="field mb-0">
<label class="label is-small">{{ t('pagination.status') }}</label>
<label class="label is-small">{{ t('common.status') }}</label>
<div class="control">
<div class="select">
<select :value="orderIdFilter" @change="onOrderIdFilterChange">
<option value="*">{{ t('pagination.all') }}</option>
<option value="*">{{ t('common.all') }}</option>
<option value="is null">{{ t('pagination.not_completed') }}</option>
<option value="not null">{{ t('pagination.completed') }}</option>
</select>
@@ -135,7 +135,7 @@ onMounted(() => {
<div class="control">
<div class="select">
<select :value="departmentFilter" @change="onDepartmentFilterChange">
<option value="*">{{ t('pagination.all') }}</option>
<option value="*">{{ t('common.all') }}</option>
<option v-for="department in departments" :key="department.id" :value="department.id">{{ department.name }}</option>
</select>
</div>
@@ -149,7 +149,7 @@ onMounted(() => {
<div class="control">
<div class="select">
<select @change="showLegacyOrderBookingsPortal(); resetVersionToNew()" v-model="versionSelector">
<option value="new">{{ t('pagination.new') }}</option>
<option value="new">{{ t('common.new') }}</option>
<option value="legacy">{{ t('pagination.old') }}</option>
</select>
</div>
@@ -163,8 +163,8 @@ onMounted(() => {
<div class="control">
<div class="select">
<select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange">
<option value="*">{{ t('pagination.all') }}</option>
<option :value="new Date().toISOString().split('T')[0]">{{ t('pagination.yes') }}</option>
<option value="*">{{ t('common.all') }}</option>
<option :value="new Date().toISOString().split('T')[0]">{{ t('common.yes') }}</option>
</select>
</div>
</div>
@@ -61,7 +61,7 @@ const emitRegistration = (value: unknown) => {
<template>
<div data-testid="self-serve-vehicle-step">
<h1 class="title has-text-centered">{{ $t("self_wash.vehicle") }}</h1>
<h1 class="title has-text-centered">{{ $t("common.vehicle") }}</h1>
<b-field v-if="showCustomerNumberInput" :label="$t('self_wash.customer_number')">
<b-input
:model-value="customerNumber || ''"
@@ -47,6 +47,7 @@ const moduleConfigPaths = {
shelly: "/superuser/configuration/shelly",
selfserve: "/superuser/configuration/selfserve",
bird: "/superuser/configuration/bird",
coolify: "/superuser/configuration/coolify",
};
const snapshot = computed(() => SuperUserSystemStatusObject.snapshot.value);
@@ -494,10 +495,31 @@ function formatReplicationLabel(replication) {
const percent = Number(replication.average_percent ?? replication.min_percent ?? 0);
const status = statusLabel(replication.status || "unknown");
return t("system_status.labels.replication_percent", {
const label = t("system_status.labels.replication_percent", {
percent: Number.isFinite(percent) ? percent.toFixed(1) : "0.0",
status,
});
const coolify = coolifyAvailabilityForReplication(replication);
return coolify ? `${label} - Coolify ${coolify}` : label;
}
function coolifyAvailabilityForReplication(replication) {
const hosts = [
...(Array.isArray(replication.replicas) ? replication.replicas : []),
...(Array.isArray(replication.hosts) ? replication.hosts : []),
replication.primary,
].filter(Boolean);
const host = hosts.find((entry) => entry?.coolify?.availability_state || entry?.coolify?.deployment_status);
if (!host) {
return "";
}
const availability = String(host.coolify?.availability_state || host.coolify?.deployment_status || "").trim();
if (availability === "") {
return "";
}
return availability.replace(/_/g, " ");
}
function formatBytes(value) {
@@ -72,7 +72,7 @@ const parseClosedAt = (value) => {
<ActionSettingsWheelItem
icon="fas fa-eye"
@click="redirect('/superuser/invoices/' + object.id)"
:label="t('global.show')"
:label="t('common.show')"
/>
</template>
</ActionSettingsWheelButton>
@@ -272,7 +272,7 @@ const getCustomerUserId = (object) => {
<ActionSettingsWheelItem
icon="fas fa-eye"
@click="redirect('/superuser/invoices/' + object.id)"
:label="t('global.show')"
:label="t('common.show')"
/>
</template>
</ActionSettingsWheelButton>
@@ -203,7 +203,7 @@ const isOrderContentVisible = (order) => {
class="button is-small"
@click="toggleAllOrders()"
>
{{ isAnyOrderSelected() ? $t('global.unselect') + ' ' + $t('global.all').toLowerCase() : $t('global.select') + ' ' + $t('global.all').toLowerCase() }}
{{ isAnyOrderSelected() ? $t('global.unselect') + ' ' + $t('common.all').toLowerCase() : $t('common.select') + ' ' + $t('common.all').toLowerCase() }}
</button>
<!-- Invoice -->
<button
@@ -122,7 +122,7 @@ getDepartments();
<ActionSettingsWheelItem
icon="fas fa-eye"
@click="redirect('/superuser/invoices/' + object.id)"
:label="t('global.show')"
:label="t('common.show')"
/>
</template>
</ActionSettingsWheelButton>
@@ -151,8 +151,8 @@ watch(() => props.orderId, (newValue, oldValue) => {
<tr>
<th>{{ $t('tables.products.name') }}</th>
<th v-if="props.displayNotes">{{ $t('objects.columns.notes') }}</th>
<th v-if="props.displayReference">{{ $t('objects.columns.reference') }}</th>
<th>{{ $t('global.quantity') }}</th>
<th v-if="props.displayReference">{{ $t('common.reference') }}</th>
<th>{{ $t('common.quantity') }}</th>
<th v-if="props.displayPrice">{{ $t('tables.products.price') }}</th>
</tr>
</thead>
@@ -27,10 +27,10 @@ const formatDateTime = (dateString) => {
<th>{{ $t('objects.columns.id') }}</th>
<th>{{ $t('objects.subuser_grants.columns.subuser') }}</th>
<!-- <th>{{ $t('objects.subuser_grants.columns.billing_customer_number') }}</th> -->
<th>{{ $t('objects.subuser_grants.columns.enabled') }}</th>
<th>{{ $t('common.enabled') }}</th>
<th>{{ $t('objects.subuser_grants.columns.note') }}</th>
<!-- <th>{{ $t('objects.subuser_grants.columns.permissions') }}</th> -->
<th>{{ $t('objects.subuser_grants.columns.created_at') }}</th>
<th>{{ $t('common.created') }}</th>
<th>{{ $t('objects.subuser_grants.columns.updated_at') }}</th>
<th class="has-text-right"></th>
</tr>
@@ -60,7 +60,7 @@ const formatDateTime = (dateString) => {
:object="grant"
:loadList="loadList"
column="enabled"
:parse-function="(value) => value ? t('global.yes') : t('global.no')"
:parse-function="(value) => value ? t('common.yes') : t('common.no')"
:edit-function="SessionUser.objects.subuser_grants.showEditObjectFieldForm"
/>
<EditableTableColumn
@@ -22,7 +22,7 @@ const redirect = (path) => {
<tr>
<th>{{ $t('objects.columns.id') }}</th>
<th>{{ $t('tables.categories.name') }}</th>
<th>{{ $t('tables.categories.description') }}</th>
<th>{{ $t('common.description') }}</th>
</tr>
</thead>
<tbody>
@@ -90,7 +90,7 @@ const redirect = (path) => {
<ActionSettingsWheelItem
icon="fas fa-eye"
@click="redirect('/admin/' + object.department_id + '/modules/pos/orders/' + object.id)"
:label="t('global.show')"
:label="t('common.show')"
/>
</template>
</ActionSettingsWheelButton>
@@ -70,7 +70,7 @@ const onSelect = (id) => {
<th class="is-narrow">{{ SessionUser.objects.collectedOrderInvoices.columns.customer_number.label }}</th>
<th class="is-narrow">{{ SessionUser.objects.collectedOrderInvoices.columns.processor.label }}</th>
<th class="is-narrow">{{ SessionUser.objects.orders.meta.title }}</th>
<th>{{ $t('objects.columns.status') }}</th>
<th>{{ $t('common.status') }}</th>
<th>{{ SessionUser.objects.collectedOrderInvoices.columns.created_at.label }}</th>
<th v-if="props.showTotal">{{ $t('tables.orders.total') }}</th>
<th><!-- Actions / Selector --></th>
@@ -130,7 +130,7 @@ const onSelect = (id) => {
<ActionSettingsWheelItem
icon="fas fa-eye"
@click="redirect('/superuser/invoices/' + object.id)"
:label="t('global.show')"
:label="t('common.show')"
/>
</template>
</ActionSettingsWheelButton>
@@ -138,7 +138,7 @@ const onSelect = (id) => {
<!-- Selector -->
<td v-if="props.isSelector">
<button class="button is-small is-dark" @click="onSelect(object.id)">
{{ $t('global.select') }}
{{ $t('common.select') }}
</button>
</td>
</tr>
@@ -59,12 +59,12 @@ const formatCategory = (value) => (
<thead>
<tr>
<th>{{ $t('objects.columns.id') }}</th>
<th>{{ $t('global.time.date') }}</th>
<th>{{ $t('common.date') }}</th>
<th>{{ $t('superuser.pages.complaints.department_column') }}</th>
<th>{{ $t('global.customer') }}</th>
<th>Vaskedato</th>
<th>Kategori</th>
<th>{{ $t('objects.columns.description') }}</th>
<th>{{ $t('common.description') }}</th>
<th>{{ $t('superuser.pages.complaints.created_by_column') }}</th>
<th class="has-text-right">{{ $t('tables.actions') }}</th>
</tr>
@@ -55,7 +55,7 @@ const parseBalance = (user) => {
<th>{{ $t('objects.columns.id') }}</th>
<th>{{ $t('objects.columns.customer_number') }}</th>
<th class="is-narrow">{{ $t('global.customer_name') }}</th>
<th class="is-narrow">{{ $t('global.email') }}</th>
<th class="is-narrow">{{ $t('common.email') }}</th>
<th>{{ $t('objects.invoices.columns.due_date') }}</th>
<th class="has-text-right">{{ $t('tables.actions') }}</th>
</tr>
@@ -23,7 +23,7 @@ const redirect = (path) => {
<tr>
<th>{{ $t('objects.columns.id') }}</th>
<th>{{ $t('tables.categories.name') }}</th>
<th>{{ $t('tables.categories.description') }}</th>
<th>{{ $t('common.description') }}</th>
<th>{{ $t('tables.categories.products_count') }}</th>
<th class="has-text-right">{{ $t('tables.actions') }}</th>
</tr>
@@ -72,7 +72,7 @@ const redirect = (path) => {
<span class="icon">
<i class="fas fa-trash"></i>
</span>
<span class="ml-1">{{ $t('global.remove') }}</span>
<span class="ml-1">{{ $t('common.remove') }}</span>
</a>
</div>
</div>
@@ -80,7 +80,7 @@ const getStatusInfo = (lane) => {
) {
return {
iconClass: "fas fa-check-circle has-text-success",
label: t("global.active"),
label: t("common.active"),
};
}
@@ -90,7 +90,7 @@ const getStatusInfo = (lane) => {
) {
return {
iconClass: "fas fa-times-circle has-text-danger",
label: t("global.inactive"),
label: t("common.inactive"),
};
}
@@ -134,7 +134,7 @@ const getMachineTypeDisplayValue = (machineTypeId) => {
<th>{{ SessionUser.objects.department_lanes.columns.name.label }}</th>
<th>{{ SessionUser.objects.department_lanes.columns.department.label }}</th>
<template v-if="advancedView">
<th>{{ $t("objects.columns.status") }}</th>
<th>{{ $t("common.status") }}</th>
<th class="is-narrow">{{ SessionUser.objects.department_lanes.columns.id.label }}</th>
<th>{{ SessionUser.objects.department_lanes.columns.machine_type_id.label }}</th>
</template>
@@ -149,7 +149,7 @@ const getMachineTypeDisplayValue = (machineTypeId) => {
type="button"
class="button is-small is-light department-lanes-table__details-toggle"
:data-testid="`department-lanes-row-toggle-${object.id}`"
:aria-label="isExpanded(object.id) ? $t('global.hide') : $t('global.details')"
:aria-label="isExpanded(object.id) ? $t('common.hide') : $t('global.details')"
:aria-expanded="isExpanded(object.id) ? 'true' : 'false'"
@click="toggleExpanded(object.id)"
>
@@ -29,7 +29,7 @@ const productOptionsCache = ref(null);
const productCache = ref(null);
const mainTableColumnCount = 9;
const getBooleanLabel = (value) => (value ? t("global.yes") : t("global.no"));
const getBooleanLabel = (value) => (value ? t("common.yes") : t("common.no"));
const isExpanded = (productId) => expandedProductIds.value.includes(productId);
@@ -140,7 +140,7 @@ const getEconomicProductLabel = (value) => {
type="button"
class="button is-small is-light product-table__details-toggle"
:data-testid="`products-table-row-toggle-${product.id}`"
:aria-label="isExpanded(product.id) ? $t('global.hide') : $t('global.details')"
:aria-label="isExpanded(product.id) ? $t('common.hide') : $t('global.details')"
:aria-expanded="isExpanded(product.id) ? 'true' : 'false'"
@click="toggleExpanded(product.id)"
>
@@ -24,7 +24,7 @@ const redirect = (path) => {
<tr>
<th>{{ $t('objects.columns.id') }}</th>
<th>{{ $t('tables.roles.name') }}</th>
<th>{{ $t('tables.products.description') }}</th>
<th>{{ $t('common.description') }}</th>
<th>{{ $t('tables.roles.permissions') }}</th>
<th class="has-text-right">{{ $t('tables.actions') }}</th>
</tr>
@@ -53,7 +53,7 @@ const redirect = (path) => {
<p class="is-size-7">{{ t('user_navigation.back_to_own_user') }}</p>
</a>
<a href="#" class="dropdown-item" @click="SessionUser.functions.showConfirmLogoutDialog()"> <!-- Log out the user -->
<p class="is-size-7 has-text-danger">{{ t('user_navigation.logout') }}</p>
<p class="is-size-7 has-text-danger">{{ t('common.logout') }}</p>
</a>
</div>
</div>
@@ -112,8 +112,8 @@ const showCompleteWashWithoutWashCertificate = (object) => {
icon: 'warning',
input: 'text',
showCancelButton: true,
confirmButtonText: t('global.yes'),
cancelButtonText: t('global.no'),
confirmButtonText: t('common.yes'),
cancelButtonText: t('common.no'),
allowOutsideClick: false,
allowEscapeKey: false,
allowEnterKey: false,
@@ -168,8 +168,8 @@ const showCreateOrderFromBooking = (object) => {
html: t('tables.bookings.create_order_from_booking_confirm'),
icon: 'warning',
showCancelButton: true,
confirmButtonText: t('global.yes'),
cancelButtonText: t('global.no'),
confirmButtonText: t('common.yes'),
cancelButtonText: t('common.no'),
allowOutsideClick: false,
allowEscapeKey: false,
allowEnterKey: false
@@ -338,13 +338,13 @@ const canUserEditObject = (object) => {
<th>{{ $t('objects.columns.department') }}</th>
<th>{{ $t('tables.bookings.customer_name') }}</th>
<th>{{ $t('objects.columns.customer_number') }}</th>
<th>{{ $t('objects.columns.date') }}</th>
<th>{{ $t('common.date') }}</th>
<th>{{ $t('tables.bookings.reg_tractor') }}</th>
<th>{{ $t('tables.bookings.reg_trailer') }}</th>
<th>{{ $t('objects.columns.reference') }}</th>
<th>{{ $t('common.reference') }}</th>
<th>{{ $t('objects.columns.notes') }}</th>
<th>{{ $t('tables.bookings.pickup_required') }}</th>
<th>{{ $t('objects.columns.created') }}</th>
<th>{{ $t('common.created') }}</th>
<th>{{ $t('tables.bookings.wash_certificate_type') }}</th>
<th>{{ $t('tables.actions') }}</th>
</tr>
@@ -386,14 +386,14 @@ const canUserEditObject = (object) => {
:permission-check-function="() => canUserEditObject(object)"
/>
<td>{{ object.notes }}</td>
<td>{{ object.pickup_bool == 1 ? $t('global.yes') : $t('global.no') }}</td>
<td>{{ object.pickup_bool == 1 ? $t('common.yes') : $t('common.no') }}</td>
<td>{{ object.created_at }}</td>
<td>
<div class="buttons is-narrow">
<!-- Show the type of wash -->
<button class="button is-small is-text"
@click="Swal.fire(
t('tables.bookings.services') + ': ' + getWashServicesString(object),
t('common.services') + ': ' + getWashServicesString(object),
'',
'info'
)">
@@ -483,13 +483,13 @@ const canUserEditObject = (object) => {
</div>
<p><strong>{{ $t('objects.columns.id') }}:</strong> {{ object.id }}</p>
<p><strong>{{ $t('objects.columns.department') }}:</strong> {{ getDepartmentName(object.department) }}</p>
<p><strong>{{ $t('objects.columns.date') }}:</strong> {{ object.date }}</p>
<p><strong>{{ $t('common.date') }}:</strong> {{ object.date }}</p>
<p><strong>{{ $t('tables.bookings.reg_tractor') }}:</strong> {{ object.regNrTraekker }}</p>
<p><strong>{{ $t('tables.bookings.reg_trailer') }}:</strong> {{ object.regNrTrailer }}</p>
<p><strong>{{ $t('objects.columns.reference') }}:</strong> {{ object.reference_number }}</p>
<p><strong>{{ $t('common.reference') }}:</strong> {{ object.reference_number }}</p>
<p><strong>{{ $t('objects.columns.notes') }}:</strong> {{ object.notes }}</p>
<p><strong>{{ $t('tables.bookings.pickup_required') }}</strong> {{ object.pickup_bool == 1 ? $t('global.yes') : $t('global.no') }}</p>
<p><strong>{{ $t('objects.columns.created') }}:</strong> {{ object.created_at }}</p>
<p><strong>{{ $t('tables.bookings.pickup_required') }}</strong> {{ object.pickup_bool == 1 ? $t('common.yes') : $t('common.no') }}</p>
<p><strong>{{ $t('common.created') }}:</strong> {{ object.created_at }}</p>
<p><strong>{{ $t('tables.bookings.wash_certificate_type') }}:</strong>
<button class="button is-small is-text"
@click="Swal.fire(
@@ -64,7 +64,7 @@ const getObjectStatus = (object) => {
status = t('tables.invoices.unpaid');
}
if (object.economic_invoice_booked_id || object.economic_invoice_booked_id === 0) {
status = t('global.invoice');
status = t('common.invoice');
}
if (isInvoiceCancelled(object)) {
return t('global.cancelled');
@@ -38,7 +38,7 @@ window.addEventListener('resize', () => {
<th>{{ $t('objects.bookings.columns.reg_1') }}</th>
<th>{{ $t('objects.orders.columns.reg_2') }}</th>
<th>{{ $t('objects.orders.columns.reg_3') }}</th>
<th>{{ $t('objects.columns.date') }}</th>
<th>{{ $t('common.date') }}</th>
<th>{{ $t('tables.actions') }}</th>
</tr>
</thead>
@@ -52,7 +52,7 @@ window.addEventListener('resize', () => {
<td>{{ object.created_at }}</td>
<td>
<div class="buttons">
<button class="button is-small is-dark" @click="redirectUserObjectPage(object.id)">{{ $t('global.show') }}</button>
<button class="button is-small is-dark" @click="redirectUserObjectPage(object.id)">{{ $t('common.show') }}</button>
</div>
</td>
</tr>
@@ -73,7 +73,7 @@ window.addEventListener('resize', () => {
<span class="icon">
<i class="fas fa-eye"></i>
</span>
<span>{{ $t('global.show') }}</span>
<span>{{ $t('common.show') }}</span>
</button>
</div>
<div class="content">
@@ -82,7 +82,7 @@ window.addEventListener('resize', () => {
<p><strong>{{ $t('objects.bookings.columns.reg_1') }}:</strong> {{ object.reg_1 }}</p>
<p><strong>{{ $t('objects.orders.columns.reg_2') }}:</strong> {{ object.reg_2 }}</p>
<p><strong>{{ $t('objects.orders.columns.reg_3') }}:</strong> {{ object.reg_3 }}</p>
<p><strong>{{ $t('objects.columns.date') }}:</strong> {{ object.created_at }}</p>
<p><strong>{{ $t('common.date') }}:</strong> {{ object.created_at }}</p>
</div>
</div>
</div>
@@ -17,7 +17,7 @@ const markAsRead = () => {
props.reload();
}).catch((error) => {
Swal.fire(
t('user_notifications.error_title'),
t('common.error'),
t('user_notifications.mark_as_read_error'),
'error'
)
@@ -144,7 +144,7 @@ const getProductOptionsLabel = (vehicle) => {
<th>{{ $t('vehicles.type') }}</th>
<th>{{ $t('objects.vehicles.columns.wash_subscription') }}</th>
<th v-if="!props.compact">{{ SessionUser.objects.product_options.meta.title }}</th>
<th v-if="!props.compact">{{ $t('objects.columns.reference') }}</th>
<th v-if="!props.compact">{{ $t('common.reference') }}</th>
<th v-if="!props.compact"></th>
</tr>
</thead>
@@ -188,7 +188,7 @@ const getProductOptionsLabel = (vehicle) => {
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
column="wash_subscription"
:parse-function="(value) => {
return value ? t('global.yes') : t('global.no');
return value ? t('common.yes') : t('common.no');
}"
/>
<!-- Product Options, if the wash subscription is set to true -->
@@ -125,7 +125,7 @@ const loginWithPasskey = async () => {
@click="loginMethod = 'phone'"
id="subuser_login_method_phone_button"
>
{{ t('auth.phone') }}
{{ t('common.phone') }}
</button>
<button
type="button"
@@ -29,18 +29,18 @@ const getReceiptLabels = () => ({
title: SessionUser.objects.global.language.payment.receipt,
transactionInformation: "Transaktions information",
customerInformation: "Kunde information",
vehicles: t('pos.order.vehicles'),
vehicles: t('common.vehicles'),
transactionId: "Transaktions ID",
invoiceId: "Faktura ID",
created: t('pos.order.created'),
created: t('common.created'),
department: t('pos.order.department_id'),
customer: t('pos.order.customer'),
name: t('tables.products.name'),
note: t('global.note'),
reference: t('objects.columns.reference'),
quantity: t('global.quantity'),
price: t('global.price'),
tax: t('global.tax'),
reference: t('common.reference'),
quantity: t('common.quantity'),
price: t('common.price'),
tax: t('common.tax'),
total: t('tables.orders.total'),
exclTax: t('global.excl_tax'),
inclTax: t('global.incl_tax'),
@@ -8,6 +8,11 @@ import {
clearMissingPermissions,
requestQueueState,
} from "@/services/requestQueue.js";
import {
I18N_CATALOG_VERSIONS,
activeI18nCatalogVersion,
setI18nCatalogVersion,
} from "@/i18n";
const METHOD_ICON_CLASS = Object.freeze({
GET: "fa-download",
@@ -67,6 +72,12 @@ const appCommitHash = computed(() => String(import.meta.env.VITE_COMMIT_HASH ||
const appVersion = computed(() => String(import.meta.env.VITE_APP_VERSION || "0.0.0"));
const permissionGrantStatusByKey = ref({});
const requestInsightHistory = ref({});
const i18nCatalogVersionLabel = computed(() => activeI18nCatalogVersion.value.toUpperCase());
const nextI18nCatalogVersion = computed(() => {
const currentIndex = I18N_CATALOG_VERSIONS.indexOf(activeI18nCatalogVersion.value);
const nextIndex = currentIndex >= 0 ? (currentIndex + 1) % I18N_CATALOG_VERSIONS.length : 0;
return I18N_CATALOG_VERSIONS[nextIndex];
});
const currentUrlHost = computed(() => {
if (typeof window === "undefined" || !window.location) {
return "unknown";
@@ -358,6 +369,10 @@ const handleClearMissingPermissions = () => {
clearMissingPermissions();
};
const handleToggleI18nCatalogVersion = () => {
setI18nCatalogVersion(nextI18nCatalogVersion.value);
};
const getPermissionGrantStatus = (permission) =>
permissionGrantStatusByKey.value[String(permission || "")] || "idle";
@@ -649,6 +664,17 @@ onBeforeUnmount(() => {
<span class="request-queue-progress__meta-label">Version time</span>
<span class="request-queue-progress__meta-value">{{ appBuildTime }}</span>
</li>
<li class="request-queue-progress__meta-item" data-testid="request-queue-i18n-catalog-row">
<span class="request-queue-progress__meta-label">i18n catalog</span>
<button
class="request-queue-progress__catalog-switch"
data-testid="request-queue-i18n-catalog-switch"
type="button"
@click="handleToggleI18nCatalogVersion"
>
{{ i18nCatalogVersionLabel }}
</button>
</li>
<li class="request-queue-progress__meta-item">
<span class="request-queue-progress__meta-label">Outgoing requests</span>
<span class="request-queue-progress__meta-value">{{ networkTotals.outgoingRequests }}</span>
@@ -1141,6 +1167,22 @@ onBeforeUnmount(() => {
white-space: nowrap;
}
.request-queue-progress__catalog-switch {
border: 1px solid rgba(126, 249, 227, 0.5);
border-radius: 999px;
background: rgba(126, 249, 227, 0.12);
color: #ffffff;
cursor: pointer;
font-size: 11px;
font-weight: 700;
line-height: 1;
padding: 4px 9px;
}
.request-queue-progress__catalog-switch:hover {
background: rgba(126, 249, 227, 0.22);
}
.request-queue-progress__bottom-requests {
margin-top: 8px;
padding-top: 8px;
@@ -134,6 +134,13 @@ const menu_items = ref([
children: [],
hidden: false
},
{
label: SessionUser.superUser.modules.coolify.meta.title,
value: SessionUser.superUser.modules.coolify.meta.config_endpoint,
icon: SessionUser.superUser.modules.coolify.meta.icon,
children: [],
hidden: false
},
{
label: SessionUser.superUser.modules.motorapi.meta.title,
value: SessionUser.superUser.modules.motorapi.meta.config_endpoint,
@@ -76,14 +76,14 @@ const items = computed<NavigationItemProps[]>(() => [
* }
*/
{
label: t('nav.settings'),
label: t('common.settings'),
to: `/user/profile`,
type: 'link',
permissions: ['user'],
hidden: !window.location.pathname.startsWith('/user')
},
{
label: t('nav.logout'),
label: t('common.logout'),
to: `/logout`,
type: 'link',
hidden: !SessionUser.authenticated.value
@@ -289,7 +289,7 @@ setInterval(() => {
const items = computed<NavigationItemProps[]>(() => [
// Invoicing
{ label: t('superuser.nav.invoicing'), type: 'category', children: [
{ label: t('superuser.nav.invoices'), to: '/superuser/invoices' },
{ label: t('common.invoices'), to: '/superuser/invoices' },
{ label: t('superuser.nav.orders'), to: '/superuser/orders' },
{
label: t('nav.bookings'),
@@ -304,20 +304,20 @@ const items = computed<NavigationItemProps[]>(() => [
},
]},
// Products
{ label: t('superuser.nav.products'), type: 'category', children: [
{ label: t('superuser.nav.products'), to: '/superuser/products' },
{ label: t('common.products'), type: 'category', children: [
{ label: t('common.products'), to: '/superuser/products' },
{ label: t('superuser.nav.categories'), to: '/superuser/categories' },
]},
// Users
{ label: t('superuser.nav.users'), type: 'category', children: [
{ label: t('common.users'), type: 'category', children: [
{ label: t('superuser.nav.employees'), to: '/superuser/users' },
{ label: t('superuser.nav.customers'), to: '/superuser/customers' },
{ label: t('superuser.nav.complaints'), to: '/superuser/complaints' },
{ label: t('superuser.nav.roles'), to: '/superuser/roles' },
]},
// Departments
{ label: t('superuser.nav.departments'), type: 'category', children: [
{ label: t('superuser.nav.departments'), to: '/superuser/departments' },
{ label: t('common.departments'), type: 'category', children: [
{ label: t('common.departments'), to: '/superuser/departments' },
{ label: t('superuser.nav.department_lanes'), to: '/superuser/department/lanes'},
{ label: t('superuser.nav.department_gates'), to: '/superuser/department/gates'},
{ label: t('superuser.nav.department_relays'), to: '/superuser/department/relays'},
@@ -333,6 +333,8 @@ const items = computed<NavigationItemProps[]>(() => [
{ label: firstToUpperCase(SessionUser.superUser.modules.reCAPTCHA.meta.labels.multiple), to: '/superuser/configuration/recaptcha'},
{ label: firstToUpperCase(SessionUser.superUser.modules.email.meta.labels.multiple), to: '/superuser/configuration/email'},
{ label: firstToUpperCase(SessionUser.superUser.modules.backups.meta.labels.multiple), to: '/superuser/configuration/backup'},
{ label: firstToUpperCase(SessionUser.superUser.modules.failover.meta.labels.multiple), to: '/superuser/configuration/failover'},
{ label: firstToUpperCase(SessionUser.superUser.modules.coolify.meta.labels.multiple), to: '/superuser/configuration/coolify'},
{ label: firstToUpperCase(SessionUser.superUser.modules.motorapi.meta.labels.multiple), to: '/superuser/configuration/motorapi'},
{ label: firstToUpperCase(SessionUser.superUser.modules.stripe.meta.labels.multiple), to: '/superuser/configuration/stripe'},
{ label: firstToUpperCase(SessionUser.superUser.modules.fxratesapi.meta.labels.multiple), to: '/superuser/configuration/fxratesapi'},
@@ -353,7 +355,7 @@ const items = computed<NavigationItemProps[]>(() => [
// Other
{ label: t('superuser.nav.other'), type: 'category', children: [
//{ label: 'Statistics', to: '/superuser/statistics' },
{ label: t('superuser.nav.vehicles'), to: '/superuser/vehicles' },
{ label: t('common.vehicles'), to: '/superuser/vehicles' },
]},
]);
@@ -54,7 +54,7 @@ const items = computed<NavigationItemProps[]>(() => [
permissions: ['user', 'BOOKINGS_LIST', 'ORDERS_LIST', 'VEHICLES_LIST', 'SELFSERVE_LIST', 'SUBUSERS_LIST'],
},
{
label: t('nav.vehicles'),
label: t('common.vehicles'),
to: `/user/vehicles`,
type: 'link',
permissions: ['user', 'VEHICLES_LIST'],
@@ -72,7 +72,7 @@ const items = computed<NavigationItemProps[]>(() => [
permissions: ['user', 'ORDERS_LIST'],
},
{
label: t('nav.invoices'),
label: t('common.invoices'),
to: `/user/invoices`,
type: 'link',
permissions: ['user'],
@@ -66,7 +66,7 @@ export const Bookings = {
},
},
reference_number: {
label: t('objects.columns.reference'),
label: t('common.reference'),
type: "string",
sortable: true,
creation: {
@@ -98,7 +98,7 @@ export const Bookings = {
},
},
date: {
label: t('objects.columns.date'),
label: t('common.date'),
type: "string",
sortable: true,
creation: {
@@ -156,7 +156,7 @@ export const Bookings = {
},
},
status: {
label: t('objects.columns.status'),
label: t('common.status'),
type: "select",
sortable: true,
creation: {
@@ -187,7 +187,7 @@ export const Bookings = {
},
},
created_at: {
label: t('objects.columns.created'),
label: t('common.created'),
type: "string",
sortable: true,
creation: {
@@ -57,7 +57,7 @@ export const Categories = {
}
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "string",
sortable: true,
creation: {
@@ -65,7 +65,7 @@ export const Categories = {
}
},
description: {
label: t('objects.columns.description'),
label: t('common.description'),
type: "string",
sortable: true,
creation: {
@@ -265,7 +265,7 @@ export const CollectedOrderInvoices = {
}
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "string",
sortable: true,
creation: {
@@ -313,7 +313,7 @@ export const CollectedOrderInvoices = {
}
},
created_at: {
label: t('objects.columns.created'),
label: t('common.created'),
type: "date",
sortable: true,
creation: {
@@ -502,8 +502,8 @@ export const DepartmentDailyReportComplaints = {
title: t("superuser.pages.complaints.edit_title"),
html: buildEditComplaintForm(complaint, departments),
showCancelButton: true,
confirmButtonText: t("global.save"),
cancelButtonText: t("global.cancel"),
confirmButtonText: t("common.save"),
cancelButtonText: t("common.cancel"),
focusConfirm: false,
didOpen: () => {
cleanupCustomerLookup = setupComplaintCustomerLookupField(customerLookupState);
@@ -575,7 +575,7 @@ export const DepartmentDailyReportComplaints = {
text: t("superuser.pages.complaints.delete_message"),
showCancelButton: true,
confirmButtonText: t("global.confirm_delete"),
cancelButtonText: t("global.cancel"),
cancelButtonText: t("common.cancel"),
showLoaderOnConfirm: true,
preConfirm: async () => {
try {
@@ -63,7 +63,7 @@ const t = (key) => i18n.global.t(key);
},
},
created_at: {
label: t('objects.columns.created'),
label: t('common.created'),
type: "date",
sortable: true,
creation: {
@@ -689,7 +689,7 @@ export const DepartmentGates = {
options: async () => SessionUser.objects.departments.get.all()
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: 'string',
sortable: true,
creation: { required: true }
@@ -58,7 +58,7 @@
}
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "string",
sortable: true,
creation: {
@@ -88,7 +88,7 @@ export const DepartmentNotificationSms = {
}
},
enabled: {
get label() { return t('objects.department_notification_sms.columns.enabled'); },
get label() { return t('common.enabled'); },
type: "boolean",
sortable: true,
creation: {
@@ -56,7 +56,7 @@ export const DepartmentRelays = {
creation: { required: true }
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: 'string',
sortable: true,
creation: { required: true }
@@ -37,7 +37,7 @@ export const getDepartmentName = async (id) => {
export const Departments = {
get meta() {
return {
title: t("objects.departments.title"),
title: t("common.departments"),
icon: "fas fa-list",
description: t("objects.departments.description"),
endpoint: "/departments",
@@ -58,7 +58,7 @@ export const Departments = {
},
},
name: {
label: t("objects.columns.name"),
label: t("common.name"),
type: "string",
sortable: true,
creation: {
@@ -66,7 +66,7 @@ export const Departments = {
},
},
description: {
label: t("objects.columns.description"),
label: t("common.description"),
type: "string",
sortable: true,
creation: {
@@ -58,17 +58,17 @@ export const ObjectsGlobal = {
get register() { return t('global.register'); },
get total_price_amount() { return t('global.total_price_amount'); },
get change_customer() { return t('global.change_customer'); },
get settings() { return t('global.settings'); },
get settings() { return t('common.settings'); },
get view_order() { return t('global.view_order'); },
get collection() { return t('global.collection'); },
get generated() { return t('global.generated'); },
get login_with_qr_code() { return t('global.login_with_qr_code'); },
get logout() { return t('global.logout'); },
get logout() { return t('common.logout'); },
get wash_multiple() { return t('global.wash_multiple'); },
get homepage() { return t('global.homepage'); },
get error_loading_transaction() { return t('global.error_loading_transaction'); },
get requires_action() { return t('global.requires_action'); },
get unknown_error() { return t('global.unknown_error'); },
get unknown_error() { return t('common.unknown_error'); },
get expand() { return t('global.expand'); },
get no_transactions_yet() { return t('global.no_transactions_yet'); },
get add_other_product() { return t('global.add_other_product'); },
@@ -78,7 +78,7 @@ export const ObjectsGlobal = {
get confirmation_needed() { return t('global.confirmation_needed'); },
get showing_of_separator() { return t('global.showing_of_separator'); },
get scanning() { return t('global.scanning'); },
get next() { return t('global.next'); },
get next() { return t('common.next'); },
get send_as_is() { return t('global.send_as_is'); },
get ignore_customer_arrangements() { return t('global.ignore_customer_arrangements'); },
get recommended() { return t('global.recommended'); },
@@ -103,7 +103,7 @@ export const ObjectsGlobal = {
get complete() { return t('global.complete'); },
get lane() { return t('global.lane'); },
get invoice_per_order() { return t('global.invoice_per_order'); },
get price() { return t('global.price'); },
get price() { return t('common.price'); },
get collapse() { return t('global.collapse'); },
get edit() { return t('global.edit'); },
get home() { return t('global.home'); },
@@ -121,13 +121,13 @@ export const ObjectsGlobal = {
get recent() { return t('global.recent'); },
get possible_duplicates() { return t('global.possible_duplicates'); },
get successful_syncs() { return t('global.successful_syncs'); },
get save() { return t('global.save'); },
get save() { return t('common.save'); },
get mark_as_not_duplicate() { return t('global.mark_as_not_duplicate'); },
get my() { return t('global.my'); },
get attach_to_order() { return t('global.attach_to_order'); },
get synchronized() { return t('global.synchronized'); },
get warnings() { return t('global.warnings'); },
get warning() { return t('global.warning'); },
get warning() { return t('common.warning'); },
get tank_cleaning() { return t('global.tank_cleaning'); },
get none() { return t('global.none'); },
get default() { return t('global.default'); },
@@ -136,7 +136,7 @@ export const ObjectsGlobal = {
get optional() { return t('global.optional'); },
get rows() { return t('global.rows'); },
get auto_start_on_lpr() { return t('global.auto_start_on_lpr'); },
get close() { return t('global.close'); },
get close() { return t('common.close'); },
get no_slots_available() { return t('global.no_slots_available'); },
get past() { return t('global.past'); },
get closed() { return t('global.closed'); },
@@ -165,23 +165,23 @@ export const ObjectsGlobal = {
get invalid() { return t('global.invalid'); },
get last_wash() { return t('global.last_wash'); },
get have_not() { return t('global.have_not'); },
get clear() { return t('global.clear'); },
get clear() { return t('common.clear'); },
get have() { return t('global.have'); },
get hide() { return t('global.hide'); },
get cancel() { return t('global.cancel'); },
get hide() { return t('common.hide'); },
get cancel() { return t('common.cancel'); },
get previous() { return t('global.previous'); },
get unselect() { return t('global.unselect'); },
get select() { return t('global.select'); },
get select() { return t('common.select'); },
get month() { return t('global.month'); },
get all() { return t('global.all'); },
get all() { return t('common.all'); },
get copy() { return t('global.copy'); },
get yes() { return t('global.yes'); },
get no() { return t('global.no'); },
get email() { return t('global.email'); },
get yes() { return t('common.yes'); },
get no() { return t('common.no'); },
get email() { return t('common.email'); },
get subscriptions() { return t('global.subscriptions'); },
get invoice_already_created() { return t('global.invoice_already_created'); },
get active() { return t('global.active'); },
get inactive() { return t('global.inactive'); },
get active() { return t('common.active'); },
get inactive() { return t('common.inactive'); },
get password() { return t('global.password'); },
get customer() { return t('global.customer'); },
get other_period() { return t('global.other_period'); },
@@ -191,13 +191,13 @@ export const ObjectsGlobal = {
get confirm_delete() { return t('global.confirm_delete'); },
get clear_all() { return t('global.clear_all'); },
get customer_notes() { return t('global.customer_notes'); },
get done() { return t('global.done'); },
get done() { return t('common.done'); },
get stop() { return t('global.stop'); },
get confirm_delete_message() { return t('global.confirm_delete_message'); },
get actions() { return t('global.actions'); },
get actions() { return t('common.actions'); },
get update() { return t('global.update'); },
get loading() { return t('global.loading'); },
get error() { return t('global.error'); },
get error() { return t('common.error'); },
get completed() { return t('global.completed'); },
get not_completed() { return t('global.not_completed'); },
get nothing_to_do_all_set() { return t('global.nothing_to_do_all_set'); },
@@ -207,22 +207,22 @@ export const ObjectsGlobal = {
get reset() { return t('global.reset'); },
get customer_name() { return t('global.customer_name'); },
get total() { return t('global.total'); },
get status() { return t('global.status'); },
get status() { return t('common.status'); },
get other() { return t('global.other'); },
get remove() { return t('global.remove'); },
get remove() { return t('common.remove'); },
get showing() { return t('global.showing'); },
get add() { return t('global.add'); },
get reload() { return t('global.reload'); },
get time() {
return {
get at_hour() { return t('global.time.at_hour'); },
get today() { return t('global.time.today'); },
get today() { return t('common.today'); },
get yesterday() { return t('global.time.yesterday'); },
get this_month() { return t('global.time.this_month'); },
get last_month() { return t('global.time.last_month'); },
get this_year() { return t('global.time.this_year'); },
get all() { return t('global.time.all'); },
get date() { return t('global.time.date'); },
get date() { return t('common.date'); },
};
},
get error_messages() {
@@ -237,9 +237,9 @@ export const ObjectsGlobal = {
get pending() { return t('global.pending'); },
get cancelled() { return t('global.cancelled'); },
get show_content() { return t('global.show_content'); },
get show() { return t('global.show'); },
get show() { return t('common.show'); },
get hide_content() { return t('global.hide_content'); },
get quantity() { return t('global.quantity'); },
get quantity() { return t('common.quantity'); },
get manage() { return t('global.manage'); },
get customer_number() { return t('global.customer_number'); },
get payment() {
@@ -264,19 +264,19 @@ export const ObjectsGlobal = {
},
get payment_recieved() { return t('global.payment.payment_recieved'); },
get amount() { return t('global.payment.amount'); },
get date() { return t('global.payment.date'); },
get reference() { return t('global.payment.reference'); },
get date() { return t('common.date'); },
get reference() { return t('common.reference'); },
get transaction_id() { return t('global.payment.transaction_id'); },
get payment_intent() { return t('global.payment.payment_intent'); },
get receipt() { return t('global.payment.receipt'); },
get receipt_url() { return t('global.payment.receipt_url'); },
get discount() { return t('global.payment.discount'); },
get discount() { return t('common.discount'); },
};
},
get invoice() { return t('global.invoice'); },
get invoice() { return t('common.invoice'); },
get text() {
return {
get today() { return t('global.text.today'); },
get today() { return t('common.today'); },
get yesterday() { return t('global.text.yesterday'); },
get this_week() { return t('global.text.this_week'); },
get last_7_days() { return t('global.text.last_7_days'); },
@@ -287,7 +287,7 @@ export const ObjectsGlobal = {
get same_week_last_year() { return t('global.text.same_week_last_year'); },
get same_month_last_year() { return t('global.text.same_month_last_year'); },
get all() { return t('global.text.all'); },
get year() { return t('global.text.year'); },
get year() { return t('common.year'); },
get month() { return t('global.text.month'); },
get search_in() { return t('global.text.search_in'); },
};
@@ -440,7 +440,7 @@ const assignDraftOrderCustomer = async ({
}
},
reference: {
label: t('objects.columns.reference'),
label: t('common.reference'),
type: "string",
sortable: true,
creation: {
@@ -480,7 +480,7 @@ const assignDraftOrderCustomer = async ({
}
},
invoice_collection_id: {
label: t('objects.columns.invoice'),
label: t('common.invoice'),
type: "number",
sortable: true,
creation: {
@@ -528,7 +528,7 @@ const assignDraftOrderCustomer = async ({
}
},
created_at: {
label: t('objects.columns.date'),
label: t('common.date'),
type: "datetime",
sortable: true,
creation: {
@@ -35,7 +35,7 @@ const t = (key) => i18n.global.t(key);
}
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "text",
sortable: true,
creation: {
@@ -43,7 +43,7 @@ const t = (key) => i18n.global.t(key);
}
},
description: {
label: t('objects.columns.description'),
label: t('common.description'),
type: "text",
sortable: true,
creation: {
@@ -83,7 +83,7 @@ export const getProductOptions = async (id) => {
}
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "string",
sortable: true,
creation: {
@@ -37,7 +37,7 @@ export const getProductName = (id, fallback = null) => {
export const Products = {
get meta() {
return {
title: t('objects.products.title'),
title: t('common.products'),
icon: "fas fa-list",
description: t('objects.products.description'),
endpoint: "/products",
@@ -58,7 +58,7 @@ export const Products = {
}
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "string",
sortable: true,
creation: {
@@ -66,7 +66,7 @@ export const Products = {
}
},
description: {
label: t('objects.columns.description'),
label: t('common.description'),
type: "string",
sortable: true,
creation: {
@@ -74,7 +74,7 @@ export const Products = {
}
},
price: {
label: t('objects.columns.price'),
label: t('common.price'),
type: "number",
sortable: true,
creation: {
@@ -57,7 +57,7 @@ export const Roles = {
}
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "string",
sortable: true,
creation: {
@@ -65,7 +65,7 @@ export const Roles = {
}
},
description: {
label: t('objects.columns.description'),
label: t('common.description'),
type: "string",
sortable: true,
creation: {
@@ -13,7 +13,7 @@ const t = (key) => i18n.global.t(key);
export const Vehicles = {
get meta() {
return {
title: t('objects.vehicles.title'),
title: t('common.vehicles'),
icon: "fas fa-list",
description: t('objects.vehicles.description'),
endpoint: "/vehicles",
@@ -108,7 +108,7 @@ export const Vehicles = {
}
},
reference: {
label: t('objects.columns.reference'),
label: t('common.reference'),
type: "string",
sortable: true,
creation: {
@@ -71,7 +71,7 @@ export const EconomicDepartments = {
get columns() {
return {
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "string",
sortable: true,
creation: {
@@ -57,7 +57,7 @@ export const EconomicProducts = {
}
},
name: {
label: t('objects.columns.name'),
label: t('common.name'),
type: "string",
sortable: true,
creation: {
@@ -0,0 +1,13 @@
<script>
import { getCoolifyConfig, setCoolifyConfig } from "@/services/superuserCoolify.js";
export const Config = {
get: (variable) => getCoolifyConfig(variable),
get_all: () => getCoolifyConfig(),
set: (payload) => setCoolifyConfig(payload),
enabled: {
get: () => getCoolifyConfig(),
set: (value) => setCoolifyConfig({ variable: "enabled", value }),
},
};
</script>
@@ -0,0 +1,20 @@
<script>
import { Config } from "@/components/session/token/superUser/modules/coolify/Config.vue";
export const Coolify = {
meta: {
title: "Coolify",
description: "Managed replicated infrastructure",
endpoint: "/superuser/coolify",
config_endpoint: "/configuration/coolify",
labels: {
single: "Coolify",
multiple: "Coolify",
},
icon: "fas fa-server",
},
get config() {
return Config;
},
};
</script>

Some files were not shown because too many files have changed in this diff Show More