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:
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
3.0
|
||||
@@ -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",
|
||||
|
||||
@@ -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)}.`);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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 dropdownContentStyle = computed(() => {
|
||||
if (isFixedPosition.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 {
|
||||
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>
|
||||
|
||||
+1
-1
@@ -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: [
|
||||
{
|
||||
|
||||
+1
-1
@@ -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),
|
||||
},
|
||||
{
|
||||
|
||||
+6
-6
@@ -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>
|
||||
|
||||
+1
-1
@@ -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 -->
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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"),
|
||||
|
||||
+6
-6
@@ -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>
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+9
-9
@@ -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>
|
||||
|
||||
+1
-1
@@ -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: {
|
||||
|
||||
+3
-3
@@ -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
Reference in New Issue
Block a user