From 0149e06c4282940f117730e1c67960db0a43ddf5 Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Mon, 3 Aug 2026 07:04:48 +0200 Subject: [PATCH] Fix invoice period i18n release gate (#245) Replace unreviewed dynamic invoice-period translation calls with a bounded literal-key contract and focused fallback coverage. --- .../displays/layout/Right.vue | 6 +- .../services/invoicingPeriodTranslation.js | 142 ++++++++++++++++++ .../views/InvoicingBillingPeriodViewAll.vue | 8 +- .../invoicing-period-review-workspace.spec.js | 16 ++ 4 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTranslation.js diff --git a/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue b/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue index c315c91a..f528af01 100644 --- a/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue +++ b/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue @@ -25,14 +25,12 @@ import { import { buildPossibleDuplicateGroups } from "../../utils/possibleDuplicateGroups.js"; import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js"; import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js"; +import { translateInvoicingPeriodReview } from "../../services/invoicingPeriodTranslation.js"; const period_result = ref(null); const route = useRoute(); const router = useRouter(); -const tr = (key: string, fallback: string) => { - const translated = i18n.global.t(`invoicing_period.review_workspace.${key}`); - return translated === `invoicing_period.review_workspace.${key}` ? fallback : translated; -}; +const tr = (key: string, fallback: string) => translateInvoicingPeriodReview(i18n.global, key, fallback); const DRAFT_STATUS_REFRESH_INTERVAL_MS = 130_000; let draftStatusRefreshTimer: ReturnType | null = null; let draftStatusRefreshInFlight = false; diff --git a/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTranslation.js b/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTranslation.js new file mode 100644 index 00000000..962a6121 --- /dev/null +++ b/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTranslation.js @@ -0,0 +1,142 @@ +const REVIEW_WORKSPACE_TRANSLATORS = { + "analytics.title": (composer, params) => + composer.t("invoicing_period.review_workspace.analytics.title", params), + "detail.next": (composer, params) => composer.t("invoicing_period.review_workspace.detail.next", params), + "detail.open_prompt": (composer, params) => + composer.t("invoicing_period.review_workspace.detail.open_prompt", params), + "detail.previous": (composer, params) => + composer.t("invoicing_period.review_workspace.detail.previous", params), + "detail.selected_customer": (composer, params) => + composer.t("invoicing_period.review_workspace.detail.selected_customer", params), + "headline.aria_label": (composer, params) => + composer.t("invoicing_period.review_workspace.headline.aria_label", params), + "headline.booked": (composer, params) => + composer.t("invoicing_period.review_workspace.headline.booked", params), + "headline.not_booked": (composer, params) => + composer.t("invoicing_period.review_workspace.headline.not_booked", params), + "headline.total": (composer, params) => + composer.t("invoicing_period.review_workspace.headline.total", params), + "navigation.agreements": (composer, params) => + composer.t("invoicing_period.review_workspace.navigation.agreements", params), + "navigation.aria_label": (composer, params) => + composer.t("invoicing_period.review_workspace.navigation.aria_label", params), + "navigation.billing": (composer, params) => + composer.t("invoicing_period.review_workspace.navigation.billing", params), + "navigation.operations": (composer, params) => + composer.t("invoicing_period.review_workspace.navigation.operations", params), + "navigation.review": (composer, params) => + composer.t("invoicing_period.review_workspace.navigation.review", params), + "next_actions.create_invoice": (composer, params) => + composer.t("invoicing_period.review_workspace.next_actions.create_invoice", params), + "next_actions.none": (composer, params) => + composer.t("invoicing_period.review_workspace.next_actions.none", params), + "next_actions.resolve_collection_errors": (composer, params) => + composer.t("invoicing_period.review_workspace.next_actions.resolve_collection_errors", params), + "next_actions.resolve_draft": (composer, params) => + composer.t("invoicing_period.review_workspace.next_actions.resolve_draft", params), + "next_actions.resolve_manual_flags": (composer, params) => + composer.t("invoicing_period.review_workspace.next_actions.resolve_manual_flags", params), + "next_actions.review_warnings": (composer, params) => + composer.t("invoicing_period.review_workspace.next_actions.review_warnings", params), + "next_actions.wait_for_export": (composer, params) => + composer.t("invoicing_period.review_workspace.next_actions.wait_for_export", params), + "queue.count": (composer, params) => composer.t("invoicing_period.review_workspace.queue.count", params), + "queue.title": (composer, params) => composer.t("invoicing_period.review_workspace.queue.title", params), + "reasons.automatic_flags": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.automatic_flags", params), + "reasons.automatic_warnings": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.automatic_warnings", params), + "reasons.collection_errors": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.collection_errors", params), + "reasons.completed": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.completed", params), + "reasons.draft": (composer, params) => composer.t("invoicing_period.review_workspace.reasons.draft", params), + "reasons.draft_blocks_action": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.draft_blocks_action", params), + "reasons.export_in_progress": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.export_in_progress", params), + "reasons.manual_flags": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.manual_flags", params), + "reasons.queued": (composer, params) => composer.t("invoicing_period.review_workspace.reasons.queued", params), + "reasons.ready_to_invoice": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.ready_to_invoice", params), + "reasons.requires_action": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.requires_action", params), + "reasons.unbooked_transactions": (composer, params) => + composer.t("invoicing_period.review_workspace.reasons.unbooked_transactions", params), + "severities.blue": (composer, params) => + composer.t("invoicing_period.review_workspace.severities.blue", params), + "severities.green": (composer, params) => + composer.t("invoicing_period.review_workspace.severities.green", params), + "severities.red": (composer, params) => composer.t("invoicing_period.review_workspace.severities.red", params), + "severities.yellow": (composer, params) => + composer.t("invoicing_period.review_workspace.severities.yellow", params), + "sort.customer_name": (composer, params) => + composer.t("invoicing_period.review_workspace.sort.customer_name", params), + "sort.customer_number": (composer, params) => + composer.t("invoicing_period.review_workspace.sort.customer_number", params), + "sort.priority": (composer, params) => composer.t("invoicing_period.review_workspace.sort.priority", params), + "sort.total_amount": (composer, params) => + composer.t("invoicing_period.review_workspace.sort.total_amount", params), + "states.attention": (composer, params) => + composer.t("invoicing_period.review_workspace.states.attention", params), + "states.blocked": (composer, params) => composer.t("invoicing_period.review_workspace.states.blocked", params), + "states.completed": (composer, params) => + composer.t("invoicing_period.review_workspace.states.completed", params), + "states.empty_body": (composer, params) => + composer.t("invoicing_period.review_workspace.states.empty_body", params), + "states.empty_title": (composer, params) => + composer.t("invoicing_period.review_workspace.states.empty_title", params), + "states.error_body": (composer, params) => + composer.t("invoicing_period.review_workspace.states.error_body", params), + "states.error_title": (composer, params) => + composer.t("invoicing_period.review_workspace.states.error_title", params), + "states.filtered_empty_body": (composer, params) => + composer.t("invoicing_period.review_workspace.states.filtered_empty_body", params), + "states.filtered_empty_title": (composer, params) => + composer.t("invoicing_period.review_workspace.states.filtered_empty_title", params), + "states.loading_body": (composer, params) => + composer.t("invoicing_period.review_workspace.states.loading_body", params), + "states.loading_title": (composer, params) => + composer.t("invoicing_period.review_workspace.states.loading_title", params), + "states.queued": (composer, params) => composer.t("invoicing_period.review_workspace.states.queued", params), + "states.ready": (composer, params) => composer.t("invoicing_period.review_workspace.states.ready", params), + "states.refreshing": (composer, params) => + composer.t("invoicing_period.review_workspace.states.refreshing", params), + "states.retained_error": (composer, params) => + composer.t("invoicing_period.review_workspace.states.retained_error", params), + "toolbar.all_departments": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.all_departments", params), + "toolbar.all_invoice_states": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.all_invoice_states", params), + "toolbar.all_severities": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.all_severities", params), + "toolbar.all_states": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.all_states", params), + "toolbar.change_direction": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.change_direction", params), + "toolbar.clear_filters": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.clear_filters", params), + "toolbar.department": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.department", params), + "toolbar.invoice_state": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.invoice_state", params), + "toolbar.reload": (composer, params) => composer.t("invoicing_period.review_workspace.toolbar.reload", params), + "toolbar.retry": (composer, params) => composer.t("invoicing_period.review_workspace.toolbar.retry", params), + "toolbar.search": (composer, params) => composer.t("invoicing_period.review_workspace.toolbar.search", params), + "toolbar.severity": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.severity", params), + "toolbar.sort": (composer, params) => composer.t("invoicing_period.review_workspace.toolbar.sort", params), + "toolbar.workflow": (composer, params) => + composer.t("invoicing_period.review_workspace.toolbar.workflow", params), +}; + +export const translateInvoicingPeriodReview = (composer, key, fallback, params = {}) => { + const translate = REVIEW_WORKSPACE_TRANSLATORS[key]; + if (typeof translate !== "function" || typeof composer?.t !== "function") { + return fallback; + } + + const translated = translate(composer, params); + return translated === `invoicing_period.review_workspace.${key}` ? fallback : translated; +}; diff --git a/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue b/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue index ae4138a3..0a724d94 100644 --- a/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue +++ b/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue @@ -37,13 +37,11 @@ import { getDuplicateTransactionPlate, } from "../utils/possibleDuplicateGroups.js"; import { deriveLegacyPeriodCustomerReview } from "../services/invoicingPeriodReview.js"; +import { translateInvoicingPeriodReview } from "../services/invoicingPeriodTranslation.js"; const expandedDuplicateGroupKey = ref(null); -const tr = (key: string, fallback: string, params: Record = {}) => { - const fullKey = `invoicing_period.review_workspace.${key}`; - const translated = i18n.global.t(fullKey, params); - return translated === fullKey ? fallback : translated; -}; +const tr = (key: string, fallback: string, params: Record = {}) => + translateInvoicingPeriodReview(i18n.global, key, fallback, params); const showAnalytics = ref(false); const onClickCustomer = (customer: any) => { diff --git a/tests/unit/invoicing-period-review-workspace.spec.js b/tests/unit/invoicing-period-review-workspace.spec.js index 1a1e7cd1..70fa4710 100644 --- a/tests/unit/invoicing-period-review-workspace.spec.js +++ b/tests/unit/invoicing-period-review-workspace.spec.js @@ -3,10 +3,26 @@ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { buildPeriodCacheKey } from "../../src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportPaging.js"; import { deriveLegacyPeriodCustomerReview } from "../../src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodReview.js"; +import { translateInvoicingPeriodReview } from "../../src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTranslation.js"; const readProjectFile = (path) => readFileSync(fileURLToPath(new URL(`../../${path}`, import.meta.url)), "utf8"); describe("invoicing period review workspace", () => { + it("translates only the bounded review-workspace key contract", () => { + const calls = []; + const composer = { + t(key, params) { + calls.push([key, params]); + return key === "invoicing_period.review_workspace.queue.count" ? `${params.count} kunder` : key; + }, + }; + + expect(translateInvoicingPeriodReview(composer, "queue.count", "fallback", { count: 3 })).toBe("3 kunder"); + expect(translateInvoicingPeriodReview(composer, "queue.unknown", "fallback")).toBe("fallback"); + expect(translateInvoicingPeriodReview(composer, "../queue.count", "fallback")).toBe("fallback"); + expect(calls).toEqual([["invoicing_period.review_workspace.queue.count", { count: 3 }]]); + }); + it("scopes cached pages by every server-side review and sorting input", () => { const base = { dateFrom: "2026-08-01",