diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..70e93d6f --- /dev/null +++ b/.claude/settings.local.json @@ -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:*)" + ] + } +} diff --git a/.junie/memory/errors.md b/.junie/memory/errors.md new file mode 100644 index 00000000..e69de29b diff --git a/.junie/memory/feedback.md b/.junie/memory/feedback.md new file mode 100644 index 00000000..e69de29b diff --git a/.junie/memory/language.json b/.junie/memory/language.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/.junie/memory/language.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.junie/memory/memory.version b/.junie/memory/memory.version new file mode 100644 index 00000000..f398a206 --- /dev/null +++ b/.junie/memory/memory.version @@ -0,0 +1 @@ +3.0 \ No newline at end of file diff --git a/.junie/memory/tasks.md b/.junie/memory/tasks.md new file mode 100644 index 00000000..e69de29b diff --git a/package.json b/package.json index 806d4532..d2a4dcab 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/i18n-v2-compile-source.mjs b/scripts/i18n-v2-compile-source.mjs new file mode 100644 index 00000000..291ef949 --- /dev/null +++ b/scripts/i18n-v2-compile-source.mjs @@ -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 = /@(?\.[\p{L}]+)?:(?:\{'(?[^']+)'\}|(?[\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)}.`); +} diff --git a/scripts/i18n-v2-globalize-templates.mjs b/scripts/i18n-v2-globalize-templates.mjs new file mode 100644 index 00000000..a611357e --- /dev/null +++ b/scripts/i18n-v2-globalize-templates.mjs @@ -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 = /^@(?\.[\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; + 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]); + } +} diff --git a/scripts/i18n-v2-template-dedupe.mjs b/scripts/i18n-v2-template-dedupe.mjs new file mode 100644 index 00000000..a74d10a7 --- /dev/null +++ b/scripts/i18n-v2-template-dedupe.mjs @@ -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]); + } +} diff --git a/scripts/i18n-v2-word-dedupe.mjs b/scripts/i18n-v2-word-dedupe.mjs new file mode 100644 index 00000000..0a6f02b6 --- /dev/null +++ b/scripts/i18n-v2-word-dedupe.mjs @@ -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 = /^@(?\.[\p{L}]+)?:(?:\{'(?[^']+)'\}|(?[\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); +} diff --git a/src/components/displays/Departments.vue b/src/components/displays/Departments.vue index dc949564..b05985fb 100644 --- a/src/components/displays/Departments.vue +++ b/src/components/displays/Departments.vue @@ -20,7 +20,7 @@ const redirect = (path) => { @@ -138,7 +138,7 @@ const onSelect = (id) => { diff --git a/src/components/displays/superuser/tables/customerComplaintsTable.vue b/src/components/displays/superuser/tables/customerComplaintsTable.vue index cda7364c..1478632d 100644 --- a/src/components/displays/superuser/tables/customerComplaintsTable.vue +++ b/src/components/displays/superuser/tables/customerComplaintsTable.vue @@ -59,12 +59,12 @@ const formatCategory = (value) => ( {{ $t('objects.columns.id') }} - {{ $t('global.time.date') }} + {{ $t('common.date') }} {{ $t('superuser.pages.complaints.department_column') }} {{ $t('global.customer') }} Vaskedato Kategori - {{ $t('objects.columns.description') }} + {{ $t('common.description') }} {{ $t('superuser.pages.complaints.created_by_column') }} {{ $t('tables.actions') }} diff --git a/src/components/displays/superuser/tables/customersTable.vue b/src/components/displays/superuser/tables/customersTable.vue index 9c055a09..0f23e504 100644 --- a/src/components/displays/superuser/tables/customersTable.vue +++ b/src/components/displays/superuser/tables/customersTable.vue @@ -55,7 +55,7 @@ const parseBalance = (user) => { {{ $t('objects.columns.id') }} {{ $t('objects.columns.customer_number') }} {{ $t('global.customer_name') }} - {{ $t('global.email') }} + {{ $t('common.email') }} {{ $t('objects.invoices.columns.due_date') }} {{ $t('tables.actions') }} diff --git a/src/components/displays/superuser/tables/departmentCategoriesTable.vue b/src/components/displays/superuser/tables/departmentCategoriesTable.vue index bb76471e..c9b21448 100644 --- a/src/components/displays/superuser/tables/departmentCategoriesTable.vue +++ b/src/components/displays/superuser/tables/departmentCategoriesTable.vue @@ -23,7 +23,7 @@ const redirect = (path) => { {{ $t('objects.columns.id') }} {{ $t('tables.categories.name') }} - {{ $t('tables.categories.description') }} + {{ $t('common.description') }} {{ $t('tables.categories.products_count') }} {{ $t('tables.actions') }} @@ -72,7 +72,7 @@ const redirect = (path) => { - {{ $t('global.remove') }} + {{ $t('common.remove') }} diff --git a/src/components/displays/superuser/tables/departmentLanesTable.vue b/src/components/displays/superuser/tables/departmentLanesTable.vue index 7227c07c..e6875d8d 100644 --- a/src/components/displays/superuser/tables/departmentLanesTable.vue +++ b/src/components/displays/superuser/tables/departmentLanesTable.vue @@ -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) => { {{ SessionUser.objects.department_lanes.columns.name.label }} {{ SessionUser.objects.department_lanes.columns.department.label }} @@ -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)" > diff --git a/src/components/displays/superuser/tables/productsTable.vue b/src/components/displays/superuser/tables/productsTable.vue index dd52a608..6abd71e7 100644 --- a/src/components/displays/superuser/tables/productsTable.vue +++ b/src/components/displays/superuser/tables/productsTable.vue @@ -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)" > diff --git a/src/components/displays/superuser/tables/rolesTable.vue b/src/components/displays/superuser/tables/rolesTable.vue index 381cd7a5..237a4394 100644 --- a/src/components/displays/superuser/tables/rolesTable.vue +++ b/src/components/displays/superuser/tables/rolesTable.vue @@ -24,7 +24,7 @@ const redirect = (path) => { {{ $t('objects.columns.id') }} {{ $t('tables.roles.name') }} - {{ $t('tables.products.description') }} + {{ $t('common.description') }} {{ $t('tables.roles.permissions') }} {{ $t('tables.actions') }} diff --git a/src/components/displays/user/UserNavigationTop.vue b/src/components/displays/user/UserNavigationTop.vue index 3dc5a632..0cc7d8c5 100644 --- a/src/components/displays/user/UserNavigationTop.vue +++ b/src/components/displays/user/UserNavigationTop.vue @@ -53,7 +53,7 @@ const redirect = (path) => {

{{ t('user_navigation.back_to_own_user') }}

-

{{ t('user_navigation.logout') }}

+

{{ t('common.logout') }}

diff --git a/src/components/displays/user/bookings/bookingsTable.vue b/src/components/displays/user/bookings/bookingsTable.vue index 9a4dcede..f8accbdf 100644 --- a/src/components/displays/user/bookings/bookingsTable.vue +++ b/src/components/displays/user/bookings/bookingsTable.vue @@ -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) => { {{ $t('objects.columns.department') }} {{ $t('tables.bookings.customer_name') }} {{ $t('objects.columns.customer_number') }} - {{ $t('objects.columns.date') }} + {{ $t('common.date') }} {{ $t('tables.bookings.reg_tractor') }} {{ $t('tables.bookings.reg_trailer') }} - {{ $t('objects.columns.reference') }} + {{ $t('common.reference') }} {{ $t('objects.columns.notes') }} {{ $t('tables.bookings.pickup_required') }} - {{ $t('objects.columns.created') }} + {{ $t('common.created') }} {{ $t('tables.bookings.wash_certificate_type') }} {{ $t('tables.actions') }} @@ -386,14 +386,14 @@ const canUserEditObject = (object) => { :permission-check-function="() => canUserEditObject(object)" /> {{ object.notes }} - {{ object.pickup_bool == 1 ? $t('global.yes') : $t('global.no') }} + {{ object.pickup_bool == 1 ? $t('common.yes') : $t('common.no') }} {{ object.created_at }}

{{ $t('objects.columns.id') }}: {{ object.id }}

{{ $t('objects.columns.department') }}: {{ getDepartmentName(object.department) }}

-

{{ $t('objects.columns.date') }}: {{ object.date }}

+

{{ $t('common.date') }}: {{ object.date }}

{{ $t('tables.bookings.reg_tractor') }}: {{ object.regNrTraekker }}

{{ $t('tables.bookings.reg_trailer') }}: {{ object.regNrTrailer }}

-

{{ $t('objects.columns.reference') }}: {{ object.reference_number }}

+

{{ $t('common.reference') }}: {{ object.reference_number }}

{{ $t('objects.columns.notes') }}: {{ object.notes }}

-

{{ $t('tables.bookings.pickup_required') }} {{ object.pickup_bool == 1 ? $t('global.yes') : $t('global.no') }}

-

{{ $t('objects.columns.created') }}: {{ object.created_at }}

+

{{ $t('tables.bookings.pickup_required') }} {{ object.pickup_bool == 1 ? $t('common.yes') : $t('common.no') }}

+

{{ $t('common.created') }}: {{ object.created_at }}

{{ $t('tables.bookings.wash_certificate_type') }}: + @@ -73,7 +73,7 @@ window.addEventListener('resize', () => { - {{ $t('global.show') }} + {{ $t('common.show') }}

@@ -82,7 +82,7 @@ window.addEventListener('resize', () => {

{{ $t('objects.bookings.columns.reg_1') }}: {{ object.reg_1 }}

{{ $t('objects.orders.columns.reg_2') }}: {{ object.reg_2 }}

{{ $t('objects.orders.columns.reg_3') }}: {{ object.reg_3 }}

-

{{ $t('objects.columns.date') }}: {{ object.created_at }}

+

{{ $t('common.date') }}: {{ object.created_at }}

diff --git a/src/components/displays/user/notifications/UserNotificationListItem.vue b/src/components/displays/user/notifications/UserNotificationListItem.vue index 523b4cf0..b7f0e701 100644 --- a/src/components/displays/user/notifications/UserNotificationListItem.vue +++ b/src/components/displays/user/notifications/UserNotificationListItem.vue @@ -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' ) diff --git a/src/components/displays/user/vehicles/vehiclesTable.vue b/src/components/displays/user/vehicles/vehiclesTable.vue index fa153b4c..015d2884 100644 --- a/src/components/displays/user/vehicles/vehiclesTable.vue +++ b/src/components/displays/user/vehicles/vehiclesTable.vue @@ -144,7 +144,7 @@ const getProductOptionsLabel = (vehicle) => { {{ $t('vehicles.type') }} {{ $t('objects.vehicles.columns.wash_subscription') }} {{ SessionUser.objects.product_options.meta.title }} - {{ $t('objects.columns.reference') }} + {{ $t('common.reference') }} @@ -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'); }" /> diff --git a/src/components/forms/auth/SubuserLoginForm.vue b/src/components/forms/auth/SubuserLoginForm.vue index 15864864..fc9ba94f 100644 --- a/src/components/forms/auth/SubuserLoginForm.vue +++ b/src/components/forms/auth/SubuserLoginForm.vue @@ -125,7 +125,7 @@ const loginWithPasskey = async () => { @click="loginMethod = 'phone'" id="subuser_login_method_phone_button" > - {{ t('auth.phone') }} + {{ t('common.phone') }} +
  • Outgoing requests {{ networkTotals.outgoingRequests }} @@ -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; diff --git a/src/components/menus/superuser/SuperUserLeftMenu.vue b/src/components/menus/superuser/SuperUserLeftMenu.vue index eabfa08b..b18cca39 100644 --- a/src/components/menus/superuser/SuperUserLeftMenu.vue +++ b/src/components/menus/superuser/SuperUserLeftMenu.vue @@ -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, diff --git a/src/components/models/navigation/items/NavigationMenuItemsGlobal.vue b/src/components/models/navigation/items/NavigationMenuItemsGlobal.vue index e533bb61..fba05361 100644 --- a/src/components/models/navigation/items/NavigationMenuItemsGlobal.vue +++ b/src/components/models/navigation/items/NavigationMenuItemsGlobal.vue @@ -76,14 +76,14 @@ const items = computed(() => [ * } */ { - 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 diff --git a/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue b/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue index b555a525..fe97a213 100644 --- a/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue +++ b/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue @@ -289,7 +289,7 @@ setInterval(() => { const items = computed(() => [ // 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(() => [ }, ]}, // 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(() => [ { 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(() => [ // 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' }, ]}, ]); diff --git a/src/components/models/navigation/items/NavigationMenuItemsUser.vue b/src/components/models/navigation/items/NavigationMenuItemsUser.vue index 3cdd73a2..65538b64 100644 --- a/src/components/models/navigation/items/NavigationMenuItemsUser.vue +++ b/src/components/models/navigation/items/NavigationMenuItemsUser.vue @@ -54,7 +54,7 @@ const items = computed(() => [ 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(() => [ permissions: ['user', 'ORDERS_LIST'], }, { - label: t('nav.invoices'), + label: t('common.invoices'), to: `/user/invoices`, type: 'link', permissions: ['user'], diff --git a/src/components/session/token/SessionUser/Objects/Bookings.vue b/src/components/session/token/SessionUser/Objects/Bookings.vue index 4a3f5348..45fdd53c 100644 --- a/src/components/session/token/SessionUser/Objects/Bookings.vue +++ b/src/components/session/token/SessionUser/Objects/Bookings.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/Categories.vue b/src/components/session/token/SessionUser/Objects/Categories.vue index 49a70ca0..44a7b098 100644 --- a/src/components/session/token/SessionUser/Objects/Categories.vue +++ b/src/components/session/token/SessionUser/Objects/Categories.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue b/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue index 8441ceb2..3020e9c5 100644 --- a/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue +++ b/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/DepartmentDailyReportComplaints.vue b/src/components/session/token/SessionUser/Objects/DepartmentDailyReportComplaints.vue index 3931de21..228120da 100644 --- a/src/components/session/token/SessionUser/Objects/DepartmentDailyReportComplaints.vue +++ b/src/components/session/token/SessionUser/Objects/DepartmentDailyReportComplaints.vue @@ -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 { diff --git a/src/components/session/token/SessionUser/Objects/DepartmentDailyReports.vue b/src/components/session/token/SessionUser/Objects/DepartmentDailyReports.vue index 499c5d10..e4417a4e 100644 --- a/src/components/session/token/SessionUser/Objects/DepartmentDailyReports.vue +++ b/src/components/session/token/SessionUser/Objects/DepartmentDailyReports.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/DepartmentGates.vue b/src/components/session/token/SessionUser/Objects/DepartmentGates.vue index fb0aa17a..14c69c01 100644 --- a/src/components/session/token/SessionUser/Objects/DepartmentGates.vue +++ b/src/components/session/token/SessionUser/Objects/DepartmentGates.vue @@ -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 } diff --git a/src/components/session/token/SessionUser/Objects/DepartmentLanes.vue b/src/components/session/token/SessionUser/Objects/DepartmentLanes.vue index 59e5a528..94cf9ff3 100644 --- a/src/components/session/token/SessionUser/Objects/DepartmentLanes.vue +++ b/src/components/session/token/SessionUser/Objects/DepartmentLanes.vue @@ -58,7 +58,7 @@ } }, name: { - label: t('objects.columns.name'), + label: t('common.name'), type: "string", sortable: true, creation: { diff --git a/src/components/session/token/SessionUser/Objects/DepartmentNotificationSms.vue b/src/components/session/token/SessionUser/Objects/DepartmentNotificationSms.vue index c286e5f9..5939fc72 100644 --- a/src/components/session/token/SessionUser/Objects/DepartmentNotificationSms.vue +++ b/src/components/session/token/SessionUser/Objects/DepartmentNotificationSms.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/DepartmentRelays.vue b/src/components/session/token/SessionUser/Objects/DepartmentRelays.vue index e910f9a5..105812b6 100644 --- a/src/components/session/token/SessionUser/Objects/DepartmentRelays.vue +++ b/src/components/session/token/SessionUser/Objects/DepartmentRelays.vue @@ -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 } diff --git a/src/components/session/token/SessionUser/Objects/Departments.vue b/src/components/session/token/SessionUser/Objects/Departments.vue index 5bd3e729..8fe31643 100644 --- a/src/components/session/token/SessionUser/Objects/Departments.vue +++ b/src/components/session/token/SessionUser/Objects/Departments.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/ObjectsGlobal.vue b/src/components/session/token/SessionUser/Objects/ObjectsGlobal.vue index 030fee87..57611fd6 100644 --- a/src/components/session/token/SessionUser/Objects/ObjectsGlobal.vue +++ b/src/components/session/token/SessionUser/Objects/ObjectsGlobal.vue @@ -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'); }, }; diff --git a/src/components/session/token/SessionUser/Objects/Orders.vue b/src/components/session/token/SessionUser/Objects/Orders.vue index 9ce3c68c..d7b3ec34 100644 --- a/src/components/session/token/SessionUser/Objects/Orders.vue +++ b/src/components/session/token/SessionUser/Objects/Orders.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/Permissions.vue b/src/components/session/token/SessionUser/Objects/Permissions.vue index b1aca44e..ab86d4dd 100644 --- a/src/components/session/token/SessionUser/Objects/Permissions.vue +++ b/src/components/session/token/SessionUser/Objects/Permissions.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/ProductOptions.vue b/src/components/session/token/SessionUser/Objects/ProductOptions.vue index 0a89f7e9..82b1ca6d 100644 --- a/src/components/session/token/SessionUser/Objects/ProductOptions.vue +++ b/src/components/session/token/SessionUser/Objects/ProductOptions.vue @@ -83,7 +83,7 @@ export const getProductOptions = async (id) => { } }, name: { - label: t('objects.columns.name'), + label: t('common.name'), type: "string", sortable: true, creation: { diff --git a/src/components/session/token/SessionUser/Objects/Products.vue b/src/components/session/token/SessionUser/Objects/Products.vue index 8bf58324..43e1836a 100644 --- a/src/components/session/token/SessionUser/Objects/Products.vue +++ b/src/components/session/token/SessionUser/Objects/Products.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/Roles.vue b/src/components/session/token/SessionUser/Objects/Roles.vue index d0080475..6e42dedf 100644 --- a/src/components/session/token/SessionUser/Objects/Roles.vue +++ b/src/components/session/token/SessionUser/Objects/Roles.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/Vehicles.vue b/src/components/session/token/SessionUser/Objects/Vehicles.vue index bb442d87..b0f3e779 100644 --- a/src/components/session/token/SessionUser/Objects/Vehicles.vue +++ b/src/components/session/token/SessionUser/Objects/Vehicles.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/economicDepartments.vue b/src/components/session/token/SessionUser/Objects/economicDepartments.vue index f42fd858..0f35550c 100644 --- a/src/components/session/token/SessionUser/Objects/economicDepartments.vue +++ b/src/components/session/token/SessionUser/Objects/economicDepartments.vue @@ -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: { diff --git a/src/components/session/token/SessionUser/Objects/economicProducts.vue b/src/components/session/token/SessionUser/Objects/economicProducts.vue index 4bb31a0c..41afc64a 100644 --- a/src/components/session/token/SessionUser/Objects/economicProducts.vue +++ b/src/components/session/token/SessionUser/Objects/economicProducts.vue @@ -57,7 +57,7 @@ export const EconomicProducts = { } }, name: { - label: t('objects.columns.name'), + label: t('common.name'), type: "string", sortable: true, creation: { diff --git a/src/components/session/token/superUser/modules/coolify/Config.vue b/src/components/session/token/superUser/modules/coolify/Config.vue new file mode 100644 index 00000000..5a4c2f31 --- /dev/null +++ b/src/components/session/token/superUser/modules/coolify/Config.vue @@ -0,0 +1,13 @@ + diff --git a/src/components/session/token/superUser/modules/coolify/Coolify.vue b/src/components/session/token/superUser/modules/coolify/Coolify.vue new file mode 100644 index 00000000..549f7793 --- /dev/null +++ b/src/components/session/token/superUser/modules/coolify/Coolify.vue @@ -0,0 +1,20 @@ + diff --git a/src/components/session/token/superUser/modules/failover/Config.vue b/src/components/session/token/superUser/modules/failover/Config.vue new file mode 100644 index 00000000..ce2dbde5 --- /dev/null +++ b/src/components/session/token/superUser/modules/failover/Config.vue @@ -0,0 +1,48 @@ + diff --git a/src/components/session/token/superUser/modules/failover/Failover.vue b/src/components/session/token/superUser/modules/failover/Failover.vue new file mode 100644 index 00000000..891d3740 --- /dev/null +++ b/src/components/session/token/superUser/modules/failover/Failover.vue @@ -0,0 +1,18 @@ + diff --git a/src/components/session/token/superUserObject.vue b/src/components/session/token/superUserObject.vue index 392a542f..34577ff5 100644 --- a/src/components/session/token/superUserObject.vue +++ b/src/components/session/token/superUserObject.vue @@ -24,6 +24,8 @@ import { Shelly } from "@/components/session/token/superUser/modules/shelly/Shel import { SelfServe } from "@/components/session/token/superUser/modules/selfserve/SelfServe.vue"; import { Bird } from "@/components/session/token/superUser/modules/bird/Bird.vue"; import { EdgeGateway } from "@/components/session/token/superUser/modules/edgegateway/EdgeGateway.vue"; +import { Failover } from "@/components/session/token/superUser/modules/failover/Failover.vue"; +import { Coolify } from "@/components/session/token/superUser/modules/coolify/Coolify.vue"; let qrCodeModulePromise; @@ -65,6 +67,8 @@ export const SuperUserObject = { get selfserve() { return SelfServe; }, get bird() { return Bird; }, get edgegateway() { return EdgeGateway; }, + get failover() { return Failover; }, + get coolify() { return Coolify; }, }, /** Intimidate a user */ intimidate: { diff --git a/src/components/viewport/page/headers/DesktopHeader.vue b/src/components/viewport/page/headers/DesktopHeader.vue index 6eb47f2e..eccaf3ae 100644 --- a/src/components/viewport/page/headers/DesktopHeader.vue +++ b/src/components/viewport/page/headers/DesktopHeader.vue @@ -51,7 +51,7 @@ const route = useRoute(); - {{ $t('header.departments') }} + {{ $t('common.departments') }}