// @vitest-environment node import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; function readJson(relativePath: string): T { return JSON.parse( fs.readFileSync(path.resolve(__dirname, relativePath), 'utf8'), ) as T; } function read(relativePath: string): string { return fs.readFileSync(path.resolve(__dirname, relativePath), 'utf8'); } function expectNamedImport(source: string, modulePath: string, names: string[]): void { const pattern = new RegExp( `import\\s+\\{[^}]*\\}\\s+from '${modulePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}';`, ); const match = source.match(pattern); expect(match, `expected import from ${modulePath}`).toBeTruthy(); const importLine = match?.[0] ?? ''; for (const name of names) { expect(importLine).toContain(name); } } 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('Quotes i18n wiring contract', () => { it('T001: english quotes namespace exposes the planned top-level groups', () => { const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expect(Object.keys(en)).toEqual([ 'common', 'quotesTab', 'quoteForm', 'quoteDetail', 'quoteLineItems', 'quoteRecipients', 'quoteConversion', 'quoteApproval', 'quoteTemplates', 'quotePreview', 'templateEditor', 'templatesPage', ]); }); it('T003: QuoteForm uses msp/quotes translation keys for form chrome, workflow actions, and dialogs', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteForm.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); const keyChecks = [ 'quoteForm.headings.editQuote', 'quoteForm.headings.newTemplate', 'common.actions.submitForApproval', 'quoteForm.actions.cancelQuote', 'quoteForm.actions.convertToBoth', 'quoteForm.fields.createFromTemplate', 'quoteForm.fields.recipients', 'quoteForm.dialogs.send.title', 'quoteForm.dialogs.approval.approveTitle', 'quoteForm.dialogs.conversion.title', 'quoteForm.notices.sent', 'quoteForm.errors.save', 'quoteForm.validation.clientRequired', 'common.labels.quoteLayout', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T004: QuoteForm no longer renders bare English JSX literals for form labels, actions, or dialog copy', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteForm.tsx'); const residualPatterns = [ /text="Loading quote form\.\.\."/, />Quote AcceptedQuote RejectedQuote ConvertedSubmit for ApprovalSend to ClientCancel QuoteConvert to ContractConvert to InvoiceConvert to BothCreate New RevisionTitleDescription \/ ScopeRecipientsAdditional email addresses \(comma-separated\)Message \(optional\)Approve QuoteRequest ChangesConversion PreviewContract ItemsInvoice ItemsExcluded ItemsWill Become Contract LinesWill Become Invoice ChargesExcluded from Conversion { const source = read('../../src/components/billing-dashboard/quotes/QuoteDetail.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); const keyChecks = [ 'quoteDetail.sections.quoteLayout', 'quoteDetail.sections.versionHistory', 'quoteDetail.sections.activityLog', 'quoteDetail.actions.back', 'quoteDetail.actions.openConvertedContract', 'quoteDetail.alerts.clientConfigurationSubmitted', 'quoteDetail.clientSelections.selectedOptionalItem', 'quoteDetail.dialogs.approval.approveDescription', 'quoteDetail.dialogs.send.message', 'quoteDetail.errors.load', 'quoteDetail.notices.templateAssigned', 'quoteDetail.preview.loading', 'quoteDetail.table.description', 'quoteDetail.labels.phase', 'quoteForm.dialogs.conversion.title', 'quoteConversion.sections.willBecomeInvoiceCharges', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T006: QuoteDetail no longer renders bare English JSX literals for headings, actions, dialogs, or table labels', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteDetail.tsx'); const residualPatterns = [ /text="Loading quote details\.\.\."/, />Back to QuotesQuote DetailBackPreviewDownload PDFOpen Converted ContractOpen Converted InvoiceQuote LayoutVersion HistoryScope of WorkQuote AcceptedQuote RejectedLine ItemsClient Configuration SubmittedDescriptionBillingUnit PriceClient NotesInternal NotesActivity LogConversion PreviewContract ItemsInvoice ItemsExcluded ItemsQuote PreviewCloseSend Quote to ClientRecipientsOptional message to include in the emailApprove QuoteRequest Changes { const source = read('../../src/components/billing-dashboard/quotes/QuotesTab.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); const keyChecks = [ 'quotesTab.title', 'quotesTab.tabs.active', 'quotesTab.tabs.approval', 'quotesTab.actions.quoteActions', 'quotesTab.filters.client', 'quotesTab.filters.allClients', 'quotesTab.empty.byCategory', 'quotesTab.dialogs.delete.title', 'quotesTab.dialogs.send.title', 'quotesTab.dialogs.send.additionalRecipients', 'quotesTab.dialogs.send.messagePlaceholder', 'quotesTab.errors.load', 'quotesTab.loading', 'common.columns.quoteNumber', 'common.columns.actions', 'common.actions.newQuote', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T008: QuotesTab no longer renders bare English JSX literals for tabs, table labels, menus, or dialogs', () => { const source = read('../../src/components/billing-dashboard/quotes/QuotesTab.tsx'); const residualPatterns = [ /text="Loading quotes\.\.\."/, />QuotesNew QuoteOpenSend to ClientResendSend ReminderDownload PDFDuplicateDeleteClientAll clientsApproval QueueSend QuoteAdditional recipients \(comma-separated\)Message \(optional\)Add a personal note for the recipient\.\.\. { const source = read('../../src/components/billing-dashboard/quotes/QuoteDocumentTemplateEditor.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); const keyChecks = [ 'templateEditor.title', 'templateEditor.headings.newLayout', 'templateEditor.actions.backToLayouts', 'templateEditor.actions.saveLayout', 'templateEditor.fields.layoutDetails', 'templateEditor.fields.templateName', 'templateEditor.tabs.visual', 'templateEditor.tabs.preview', 'templateEditor.preview.sampleScenario', 'templateEditor.preview.selectScenarioPrompt', 'templateEditor.pipeline.shape', 'templateEditor.actions.rerun', 'templateEditor.codeReadonly', 'templateEditor.footer.created', 'templateEditor.errors.saveFailed', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T010: QuoteDocumentTemplateEditor no longer renders bare English JSX literals for editor chrome or preview status copy', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteDocumentTemplateEditor.tsx'); const residualPatterns = [ />New Quote LayoutEdit Quote LayoutBack to LayoutsSave LayoutLayout DetailsTemplate NameVersionVisualCodeDesignTransformsPreviewSample ScenarioShapeRenderRe-runSelect a sample scenario to generate an authoritative preview\.Shaping and rendering preview\.\.\.Code view is generated from the Visual workspace and is read-only\.Created:Last Updated: { const source = read('../../src/components/billing-dashboard/quotes/QuoteLineItemsEditor.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); expectNamedImport(source, '@alga-psa/billing/hooks/useBillingEnumOptions', [ 'useBillingFrequencyOptions', 'useFormatBillingFrequency', ]); expect(source).toContain('const billingFrequencyOptions = useBillingFrequencyOptions();'); expect(source).toContain('const formatBillingFrequency = useFormatBillingFrequency();'); const keyChecks = [ 'quoteLineItems.title', 'quoteLineItems.columns.move', 'quoteLineItems.columns.unitPrice', 'quoteLineItems.labels.phaseSection', 'quoteLineItems.labels.optional', 'quoteLineItems.labels.recurring', 'quoteLineItems.actions.addDiscount', 'quoteLineItems.actions.hideDiscount', 'quoteLineItems.discounts.percentage', 'quoteLineItems.discounts.fixed', 'quoteLineItems.discounts.targets.namedItem', 'quoteLineItems.placeholders.servicePicker', 'quoteLineItems.empty', 'quoteLineItems.actions.remove', 'quoteLineItems.labels.setPrice', 'quoteLineItems.labels.noPriceInCurrency', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T012: QuoteLineItemsEditor no longer renders bare English JSX literals for line-item tables, controls, or hints', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteLineItemsEditor.tsx'); const residualPatterns = [ />Line ItemsHide DiscountAdd DiscountPercentage discountFixed discountWhole quoteSpecific itemSpecific serviceApplies to the full quote subtotalNo line items yet\. Use the catalog search above to add your first item\.MoveItemBillingFlagsQtyUnit PriceTotalActionsPhase \/ SectionSet priceRemoveExpandCollapseDiscountMarkup unavailable { const source = read('../../src/components/billing-dashboard/quotes/QuoteDocumentTemplatesPage.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); const keyChecks = [ 'templatesPage.title', 'templatesPage.description', 'templatesPage.cards.availableLayouts', 'templatesPage.actions.openMenu', 'templatesPage.dialogs.deleteConfirm', 'templatesPage.labels.custom', 'templatesPage.errors.load', 'templatesPage.errors.clone', 'templatesPage.errors.editCopy', 'templatesPage.errors.setDefault', 'templatesPage.errors.delete', 'common.actions.newLayout', 'common.actions.edit', 'common.actions.editAsCopy', 'common.actions.clone', 'common.actions.setAsDefault', 'common.actions.delete', 'common.columns.name', 'common.columns.source', 'common.columns.default', 'common.columns.actions', 'common.badges.standard', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T014: QuoteConversionDialog uses msp/quotes keys for dialog copy, mode descriptions, and summary labels', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteConversionDialog.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useFormatters', 'useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); const keyChecks = [ 'quoteConversion.title', 'quoteConversion.description', 'quoteConversion.loading', 'quoteConversion.errors.title', 'quoteConversion.errors.load', 'quoteConversion.errors.convert', 'quoteConversion.partial.title', 'quoteConversion.partial.alreadyConverted', 'quoteConversion.partial.contractCreated', 'quoteConversion.partial.invoiceCreated', 'quoteConversion.partial.remainingItems', 'quoteConversion.mode.contract.label', 'quoteConversion.mode.contract.description', 'quoteConversion.mode.invoice.label', 'quoteConversion.mode.invoice.description', 'quoteConversion.mode.both.label', 'quoteConversion.mode.both.description', 'quoteConversion.sections.conversionMode', 'quoteConversion.sections.itemMappingPreview', 'quoteConversion.sections.contractItems', 'quoteConversion.sections.invoiceItems', 'quoteConversion.sections.excludedItems', 'quoteConversion.sections.quoteTotal', 'quoteConversion.sections.statusAfterConversion', 'quoteConversion.summary.fixed', 'quoteConversion.summary.discount', 'quoteConversion.summary.converted', 'quoteConversion.actions.converting', 'quoteConversion.actions.convertQuote', 'common.actions.cancel', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T015: QuoteApprovalDashboard uses msp/quotes keys for page labels, filters, loading/empty states, and table columns', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteApprovalDashboard.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useFormatters', 'useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); expect(source).toContain('const { formatCurrency, formatDate } = useFormatters();'); const keyChecks = [ 'quoteApproval.title', 'quoteApproval.description', 'quoteApproval.settings.label', 'quoteApproval.settings.enabled', 'quoteApproval.settings.disabled', 'quoteApproval.filters.status', 'quoteApproval.filters.pendingApproval', 'quoteApproval.filters.approved', 'quoteApproval.actions.backToQuotes', 'quoteApproval.loading', 'quoteApproval.empty.title', 'quoteApproval.empty.pendingApproval', 'quoteApproval.empty.approved', 'quoteApproval.errors.load', 'quoteApproval.errors.settings', 'common.columns.quoteNumber', 'common.columns.client', 'common.columns.title', 'common.columns.amount', 'common.columns.status', 'common.columns.quoteDate', 'common.columns.validUntil', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T016: QuoteTemplatesList uses msp/quotes keys for list chrome, empty/loading states, and delete confirmation', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteTemplatesList.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useFormatters', 'useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); expect(source).toContain('const { formatCurrency, formatDate } = useFormatters();'); const keyChecks = [ 'quoteTemplates.title', 'quoteTemplates.description', 'quoteTemplates.loading', 'quoteTemplates.empty.inline', 'quoteTemplates.actions.templateActions', 'quoteTemplates.actions.editTemplate', 'quoteTemplates.actions.createQuoteFromTemplate', 'quoteTemplates.actions.delete', 'quoteTemplates.dialogs.delete.title', 'quoteTemplates.dialogs.delete.message', 'quoteTemplates.errors.load', 'quoteTemplates.errors.delete', 'common.actions.newTemplate', 'common.actions.delete', 'common.actions.cancel', 'common.columns.title', 'common.columns.items', 'common.columns.currency', 'common.columns.created', 'common.columns.actions', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T017: QuotePreviewPanel uses msp/quotes keys for panel chrome, actions, and empty/loading/error states', () => { const source = read('../../src/components/billing-dashboard/quotes/QuotePreviewPanel.tsx'); const en = readJson>( '../../../../server/public/locales/en/msp/quotes.json', ); expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); const keyChecks = [ 'quotePreview.title', 'quotePreview.empty.selectQuote', 'quotePreview.empty.unavailable', 'quotePreview.placeholders.selectLayout', 'quotePreview.loading', 'quotePreview.actions.openQuote', 'quotePreview.errors.downloadPdf', 'quotePreview.errors.load', 'common.actions.downloadPdf', 'common.badges.standard', ]; for (const key of keyChecks) { expect(source).toContain(`t('${key}'`); expect(getLeaf(en, key)).toBeDefined(); } }); it('T018: F012 currency formatter targets use locale-aware formatters instead of en-US helpers', () => { const files = [ '../../src/components/billing-dashboard/quotes/QuotesTab.tsx', '../../src/components/billing-dashboard/quotes/QuoteDetail.tsx', '../../src/components/billing-dashboard/quotes/QuoteConversionDialog.tsx', '../../src/components/billing-dashboard/quotes/QuoteApprovalDashboard.tsx', '../../src/components/billing-dashboard/quotes/QuoteTemplatesList.tsx', ].map(read); for (const source of files) { expect(source).toContain('useFormatters'); expect(source).not.toContain("Intl.NumberFormat('en-US'"); } }); it('T019: F013 date formatter targets use locale-aware formatters instead of toLocaleDateString/toLocaleString', () => { const files = [ '../../src/components/billing-dashboard/quotes/QuotesTab.tsx', '../../src/components/billing-dashboard/quotes/QuoteDetail.tsx', '../../src/components/billing-dashboard/quotes/QuoteApprovalDashboard.tsx', '../../src/components/billing-dashboard/quotes/QuoteDocumentTemplateEditor.tsx', ].map(read); for (const source of files) { expect(source).toContain('useFormatters'); expect(source).not.toContain('.toLocaleDateString('); expect(source).not.toContain('.toLocaleString('); } }); it('T023: route namespaces load msp/quotes on billing and standalone quote routes', () => { const source = read('../../../../packages/core/src/lib/i18n/config.ts'); expect(source).toContain("'/msp/billing': ['common', 'msp/core', 'features/billing', 'msp/quotes'"); expect(source).toContain("'/msp/quote-approvals': ['common', 'msp/core', 'features/billing', 'msp/quotes']"); expect(source).toContain("'/msp/quote-document-templates': ['common', 'msp/core', 'features/billing', 'msp/quotes']"); }); it('T025: QuoteLineItemsEditor markup chrome uses translation keys and locale files expose quoteLineItems.markup', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteLineItemsEditor.tsx'); const locales = ['en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'xx', 'yy']; expect(source).toContain("t('quoteLineItems.markup.unavailableTooltip'"); expect(source).toContain("t('quoteLineItems.markup.unavailable'"); expect(source).toContain("t('quoteLineItems.markup.badge'"); expect(source).not.toContain("`${sign}${markup.toFixed(1)}% markup`"); expect(source).not.toContain("content={`Markup can't be calculated because cost is tracked in ${item.cost_currency} and this quote is in ${currencyCode}.`}"); for (const locale of locales) { const messages = readJson>( `../../../../server/public/locales/${locale}/msp/quotes.json`, ); expect(getLeaf(messages, 'quoteLineItems.markup.badge')).toBeDefined(); expect(getLeaf(messages, 'quoteLineItems.markup.unavailable')).toBeDefined(); expect(getLeaf(messages, 'quoteLineItems.markup.unavailableTooltip')).toBeDefined(); } }); it('T026: QuoteSendRecipientsField uses quoteRecipients keys without raw combobox literals in rendered JSX paths', () => { const source = read('../../src/components/billing-dashboard/quotes/QuoteSendRecipientsField.tsx'); const locales = ['en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'xx', 'yy']; expectNamedImport(source, '@alga-psa/ui/lib/i18n/client', ['useTranslation']); expect(source).toContain("const { t } = useTranslation('msp/quotes');"); expect(source).toContain("t('quoteRecipients.trigger.noClient'"); expect(source).toContain("t('quoteRecipients.trigger.noneAvailable'"); expect(source).toContain("t('quoteRecipients.trigger.add'"); expect(source).toContain("t('quoteRecipients.searchPlaceholder'"); expect(source).toContain("t('quoteRecipients.empty.noneAvailable'"); expect(source).toContain("t('quoteRecipients.empty.noMatches'"); expect(source).toContain("t('quoteRecipients.kind.internal'"); expect(source).toContain("t('quoteRecipients.kind.contact'"); expect(source).toContain("t('quoteRecipients.removeAriaLabel'"); expect(source).not.toContain("placeholder=\"Search by name or email…\""); expect(source).not.toContain("{rows.length === 0 ? 'No recipients available' : 'No matches'}"); expect(source).not.toContain("{row.kind === 'internal' ? 'Internal' : 'Contact'}"); expect(source).not.toContain('aria-label={`Remove ${r.email}`}'); for (const locale of locales) { const messages = readJson>( `../../../../server/public/locales/${locale}/msp/quotes.json`, ); expect(getLeaf(messages, 'quoteRecipients.trigger.noClient')).toBeDefined(); expect(getLeaf(messages, 'quoteRecipients.trigger.noneAvailable')).toBeDefined(); expect(getLeaf(messages, 'quoteRecipients.trigger.add')).toBeDefined(); expect(getLeaf(messages, 'quoteRecipients.searchPlaceholder')).toBeDefined(); expect(getLeaf(messages, 'quoteRecipients.empty.noneAvailable')).toBeDefined(); expect(getLeaf(messages, 'quoteRecipients.empty.noMatches')).toBeDefined(); expect(getLeaf(messages, 'quoteRecipients.kind.internal')).toBeDefined(); expect(getLeaf(messages, 'quoteRecipients.kind.contact')).toBeDefined(); expect(getLeaf(messages, 'quoteRecipients.removeAriaLabel')).toBeDefined(); } }); it('T027: msp/core locales expose quote sidebar keys and pseudo/de values are localized', () => { const locales = ['en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'xx', 'yy']; for (const locale of locales) { const core = readJson>( `../../../../server/public/locales/${locale}/msp/core.json`, ); expect(getLeaf(core, 'nav.billing.sections.quotes')).toBeDefined(); expect(getLeaf(core, 'nav.billing.quotes')).toBeDefined(); expect(getLeaf(core, 'nav.billing.quoteBusinessTemplates')).toBeDefined(); expect(getLeaf(core, 'nav.billing.quoteLayouts')).toBeDefined(); } const de = readJson>( '../../../../server/public/locales/de/msp/core.json', ); const xx = readJson>( '../../../../server/public/locales/xx/msp/core.json', ); expect(getLeaf(de, 'nav.billing.sections.quotes')).toBe('Angebote'); expect(getLeaf(de, 'nav.billing.quoteBusinessTemplates')).toBe('Angebotsvorlagen'); expect(getLeaf(de, 'nav.billing.quoteLayouts')).toBe('Angebotslayouts'); expect(getLeaf(xx, 'nav.billing.sections.quotes')).toBe('11111'); expect(getLeaf(xx, 'nav.billing.quotes')).toBe('11111'); expect(getLeaf(xx, 'nav.billing.quoteBusinessTemplates')).toBe('11111'); expect(getLeaf(xx, 'nav.billing.quoteLayouts')).toBe('11111'); }); it('T030: follow-on formatter cleanup removes remaining locale-pinned quote form and draft-money formatting', () => { const quoteForm = read('../../src/components/billing-dashboard/quotes/QuoteForm.tsx'); const lineItemsEditor = read('../../src/components/billing-dashboard/quotes/QuoteLineItemsEditor.tsx'); const draftMoney = read('../../src/components/billing-dashboard/quotes/quoteLineItemDraft.ts'); expect(quoteForm).toContain('useFormatters'); expect(quoteForm).not.toContain("Intl.NumberFormat('en-US'"); expect(quoteForm).not.toContain('.toLocaleDateString('); expect(lineItemsEditor).toContain('useFormatters'); expect(lineItemsEditor).not.toContain('formatDraftQuoteMoney('); expect(draftMoney).toContain('new Intl.NumberFormat(undefined,'); expect(draftMoney).not.toContain("Intl.NumberFormat('en-US'"); }); it('T029: shared billing-frequency enums expose weekly across constants and all locale files', () => { const billingConstants = read('../../src/constants/billing.ts'); const locales = ['en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'xx', 'yy']; expect(billingConstants).toContain("export const BILLING_FREQUENCY_VALUES = ['weekly', 'monthly', 'quarterly', 'annually'] as const;"); expect(billingConstants).toContain("weekly: 'Weekly'"); for (const locale of locales) { const billing = readJson>( `../../../../server/public/locales/${locale}/features/billing.json`, ); expect(getLeaf(billing, 'enums.billingFrequency.weekly')).toBeDefined(); } }); });