// @vitest-environment node import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; function read(relativePath: string): string { return fs.readFileSync(path.resolve(__dirname, relativePath), 'utf8'); } function readJson(relativePath: string): T { return JSON.parse(read(relativePath)) as T; } function getLeaf(record: Record, dottedPath: string): unknown { return dottedPath.split('.').reduce((value, key) => { if (!value || typeof value !== 'object' || Array.isArray(value)) { return undefined; } return (value as Record)[key]; }, record); } describe('Contracts i18n wiring contract', () => { it('T002: ContractDetail tab labels use msp/contracts translation keys', () => { const source = read('../../src/components/billing-dashboard/contracts/ContractDetail.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/contracts.json' ); expect(source).toContain("const { t } = useTranslation('msp/contracts');"); const tabKeyExpectations: Array<[string, string]> = [ ['contractDetail.tabs.overview', 'Overview'], ['contractDetail.tabs.lines', 'Contract Lines'], ['contractDetail.tabs.pricing', 'Pricing Schedules'], ['contractDetail.tabs.documents', 'Documents'], ['contractDetail.tabs.invoices', 'Invoices'], ]; for (const [key, fallback] of tabKeyExpectations) { expect(source).toContain(`t('${key}', { defaultValue: '${fallback}' })`); expect(getLeaf(en, key)).toBe(fallback); } }); it('T003: ContractDetail unsaved + save-success alerts use msp/contracts keys', () => { const source = read('../../src/components/billing-dashboard/contracts/ContractDetail.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/contracts.json' ); expect(source).toContain( "t('contractDetail.alerts.unsavedChanges', {" ); expect(source).toContain( "t('contractDetail.alerts.saveSuccess', {" ); expect(getLeaf(en, 'contractDetail.alerts.unsavedChanges')).toBe( 'You have unsaved changes. Click "Save Changes" to apply them.' ); expect(getLeaf(en, 'contractDetail.alerts.saveSuccess')).toBe('Contract saved successfully!'); }); it('T004: ContractDetail details-card labels and edit/save/cancel actions use msp/contracts keys', () => { const source = read('../../src/components/billing-dashboard/contracts/ContractDetail.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/contracts.json' ); const keys = [ 'contractDetail.detailsCard.title', 'contractDetail.detailsCard.contractNameLabel', 'contractDetail.detailsCard.descriptionLabel', 'contractDetail.detailsCard.actions.editName', 'contractDetail.detailsCard.actions.saveName', 'contractDetail.detailsCard.actions.cancelName', 'contractDetail.detailsCard.actions.editDescription', 'contractDetail.detailsCard.actions.saveDescription', 'contractDetail.detailsCard.actions.cancelDescription', 'contractDetail.labels.noDescription', ]; for (const key of keys) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T005: ContractDetail header-card status, billing, currency, and renewal labels use msp/contracts keys', () => { const source = read('../../src/components/billing-dashboard/contracts/ContractDetail.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/contracts.json' ); const keyChecks = [ 'contractDetail.headerCard.title', 'contractDetail.headerCard.assignmentStatus', 'contractDetail.headerCard.billingFrequencyLabel', 'common.labels.currency', 'common.labels.created', 'common.labels.lastUpdated', 'contractDetail.headerCard.renewalHeading', 'contractDetail.headerCard.notice', 'contractDetail.headerCard.tenantDefaults', 'contractDetail.headerCard.customSettings', 'renewal.labels.mode', 'renewal.labels.source', 'renewal.labels.decisionDue', 'status.active', 'status.draft', 'status.terminated', 'status.expired', 'renewal.modes.auto', 'renewal.modes.manual', 'renewal.modes.none', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T006: ContractDetail ownership and assignment cards use translated labels, PO fields, and empty-state copy', () => { const source = read('../../src/components/billing-dashboard/contracts/ContractDetail.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/contracts.json' ); const keyChecks = [ 'contractDetail.clientOwnership.title', 'contractDetail.clientOwnership.systemManaged', 'contractDetail.clientOwnership.ownerClient', 'contractDetail.clientOwnership.clientName', 'contractDetail.clientOwnership.assignmentStatus', 'contractDetail.clientOwnership.startDate', 'contractDetail.clientOwnership.endDate', 'contractDetail.labels.noClientAssigned', 'contractDetail.clientAssignment.title', 'contractDetail.clientAssignment.empty', 'contractDetail.clientAssignment.startDate', 'contractDetail.clientAssignment.endDate', 'contractDetail.clientAssignment.required', 'contractDetail.clientAssignment.notRequired', 'po.labels.required', 'po.labels.number', 'po.labels.amount', 'common.labels.yes', 'common.labels.no', 'common.empty.ongoing', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T007: ContractDetail quick-actions, save/cancel row, and confirmation dialogs use msp/contracts keys', () => { const source = read('../../src/components/billing-dashboard/contracts/ContractDetail.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/contracts.json' ); const keyChecks = [ 'contractDetail.quickActions.title', 'contractDetail.quickActions.manageContractLines', 'contractDetail.quickActions.managePricingSchedules', 'contractDetail.quickActions.viewDocuments', 'contractDetail.quickActions.viewInvoices', 'contractDetail.quickActions.deleteContract', 'contractDetail.dialogs.discard.title', 'contractDetail.dialogs.discard.message', 'contractDetail.dialogs.discard.confirm', 'contractDetail.dialogs.discard.cancel', 'contractDetail.dialogs.unsaved.title', 'contractDetail.dialogs.unsaved.message', 'contractDetail.dialogs.unsaved.confirm', 'contractDetail.dialogs.unsaved.cancel', 'contractDetail.dialogs.delete.title', 'contractDetail.dialogs.delete.message', 'contractDetail.dialogs.delete.confirm', 'contractDetail.dialogs.delete.deleting', 'common.actions.cancel', 'common.actions.saveChanges', 'common.actions.saveChangesDirty', 'common.actions.saving', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T008: ContractDetail invoice tab columns and loading/empty states use translated keys', () => { const source = read('../../src/components/billing-dashboard/contracts/ContractDetail.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/contracts.json' ); const keyChecks = [ 'contractDetail.invoices.title', 'contractDetail.invoices.selectForPreview', 'contractDetail.invoices.loading', 'contractDetail.invoices.empty', 'contractDetail.invoices.noTemplatesAvailable', 'contractDetail.invoices.preview', 'contractDetail.invoices.columns.invoiceNumber', 'contractDetail.invoices.columns.status', 'contractDetail.invoices.columns.invoiceDate', 'contractDetail.invoices.columns.dueDate', 'contractDetail.invoices.columns.amount', 'contractDetail.invoices.columns.preview', 'common.actions.refresh', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T009: ContractDetail translation keys resolve to pseudo-locale values in xx (no English fallback leakage)', () => { const source = read('../../src/components/billing-dashboard/contracts/ContractDetail.tsx'); const xx = readJson>( '../../../../server/public/locales/xx/msp/contracts.json' ); const keys = Array.from( new Set(Array.from(source.matchAll(/(?:^|[^\w])t\('([^']+)'/g), (match) => match[1])) ); expect(keys.length).toBeGreaterThan(120); for (const key of keys) { const value = getLeaf(xx, key); expect(typeof value).toBe('string'); expect(value).toContain('11111'); } }); });