Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

61 KiB

Scratchpad -- MSP i18n: Quotes Sub-batch

  • Plan slug: 2026-04-09-msp-i18n-quotes
  • Created: 2026-04-09
  • Last synced to codebase: 2026-04-17

Status Recheck (2026-04-17)

Still 0% implemented. Verified against the current codebase:

  • server/public/locales/en/msp/quotes.jsondoes not exist.
  • All 12 files under packages/billing/src/components/billing-dashboard/quotes/ still have useTranslation=0 (confirmed by grep).
  • features.json / tests.json: 0/22 features, 0/27 tests marked implemented.
  • nav.billing.sections.quotes, nav.billing.quotes, nav.billing.quoteBusinessTemplates, nav.billing.quoteLayouts still missing from server/public/locales/en/msp/core.json (verified via grep). The F022/T027 backfill remains outstanding.

Upstream changes since the 2026-04-10 addendum (affect this plan)

Commit What changed Impact on this plan
7da29f66c add footer for most of them (touches QuoteForm.tsx, QuoteDetail.tsx, QuoteLineItemsEditor.tsx) Footer UI added to quote components. Include footer labels in the existing quoteForm.* / quoteDetail.* / quoteLineItems.* groups. No new namespace needed.
ead79deec Fix quote recipients field build errors Build fix on QuoteSendRecipientsField.tsx. QuoteSendRecipientsField.tsx is now 396 LOC (PRD estimated 403). Strings enumerated in 2026-04-10 addendum remain the same; F021/quoteRecipients.* group still correct.
8528a0816 enums translated (merged via i18n/more_enum_hooks / PR #2344) — finishes the shared enum-labels pattern for billing enums. useBillingFrequencyOptions / useFormatBillingFrequency / useContractLineTypeOptions / useFormatContractLineType published from @alga-psa/billing/hooks/useBillingEnumOptions; keys in features/billing.json#enums.*. QuoteLineItemsEditor.tsx renders a frequency select (Weekly / Monthly / Quarterly / Annually — see "Decisions" below). QuoteStatusBadge.tsx still blocked on status-metadata translation. New guidance: replace the local frequency option list in QuoteLineItemsEditor.tsx with useBillingFrequencyOptions(). Do NOT duplicate those labels into msp/quotes. Verify /msp/billing, /msp/quote-approvals, and /msp/quote-document-templates entries in ROUTE_NAMESPACES load features/billing (the PRD already adds msp/quotes to /msp/billing, which already has features/billing).
QuoteLineItemsEditor.tsx now 666 LOC (PRD estimate: 676). No behavioral drift beyond markup badge already captured in the 2026-04-10 addendum. No change to F006 / F020.

PRD correction — QuoteStatusBadge follow-up is now unblocked

The 2026-04-09 decision excluded QuoteStatusBadge.tsx because QUOTE_STATUS_METADATA labels were baked in at the types layer. The enum-labels pattern shipped 2026-04-14 is the pattern that solves this (see .ai/translation/enum-labels-pattern.md). Options:

  1. Preferred: publish a useQuoteStatusLabel() / useQuoteStatusOptions() hook from @alga-psa/types or @alga-psa/billing/hooks/, with keys under features/billing.json#enums.quoteStatus.*. Then QuoteStatusBadge.tsx becomes a thin consumer of the hook. Zero changes needed in msp/quotes.
  2. Leave out-of-scope per original plan, and track a dedicated "quote status labels via enum-hook" plan.

Recommend (1) — it removes the only remaining untranslated quote surface and matches the pattern now adopted project-wide. Add as a new feature F023 (wire QuoteStatusBadge via shared hook) and a corresponding T028 (pseudo-locale visibility test on status badges).

No structural changes otherwise

Existing F001-F022 / T001-T027 remain valid. Proceed with the corrected frequency-options source and, if accepted, add F023/T028 for QuoteStatusBadge.


Decisions

  • (2026-04-09) QuoteStatusBadge.tsx excluded from wiring: This component reads labels from QUOTE_STATUS_METADATA in @alga-psa/types. Translating those labels requires a types-level change (making the metadata accept a t() function or switching to key-based lookup). Out of scope for this batch; track as follow-up.
  • (2026-04-09) quoteLineItemDraft.ts excluded from wiring: Pure logic/data utility with no rendered JSX. The only user-visible output is formatDraftQuoteMoney() which uses Intl.NumberFormat -- already locale-capable via the formatter replacement (F012).
  • (2026-04-09) Single namespace for all 12 files: All quote components live in the same directory and share significant vocabulary (Cancel, Back, Save, Send, Delete, etc.). A single msp/quotes namespace avoids cross-namespace imports and keeps the bundle small.
  • (2026-04-09) Execution order: F001 (scaffold namespace) -> F002-F011 (wire components, largest first) -> F012-F013 (formatters) -> F014-F017 (translations + validation) -> F018 (routes) -> F019 (build check).
  • (2026-04-09) Currency formatting replacement: Five components define local formatCurrency() helpers using new Intl.NumberFormat('en-US', ...). These should be replaced with useFormatters().formatCurrency() or a shared locale-aware helper. The quoteLineItemDraft.ts utility also has formatDraftQuoteMoney() with en-US -- this should be made locale-aware as part of F012.
  • (2026-04-09) Date formatting replacement: Four components define local formatDate() helpers using .toLocaleDateString() with no locale argument (defaults to browser locale, which is correct). However, QuoteDocumentTemplateEditor.tsx uses .toLocaleString() for timestamps. These should use useFormatters() for consistency.

Post-planning additions (2026-04-10)

  • (2026-04-10) MSP sidebar Quotes section renders raw English even in xx/de. While doing the billing-dashboard xx smoke test, the left-hand MSP navigation consistently showed Quotes / Quote Templates / Quote Layouts in English under a QUOTES section header, while every other entry pseudo-translated cleanly. Root cause: server/src/config/menuConfig.ts:308-317 added a Quotes section that references four translation keys that were never backfilled into msp/core.json:

    • nav.billing.sections.quotes (section header)
    • nav.billing.quotes (Quotes item)
    • nav.billing.quoteBusinessTemplates (Quote Templates item; note: href=/msp/billing?tab=quote-business-templates)
    • nav.billing.quoteLayouts (Quote Layouts item; note: href=/msp/billing?tab=quote-templates)

    Every other nav.billing.* key is present in all 9 locale files under msp/core.json (contractTemplates, clientContracts, invoicing, accountingExports, usageTracking, etc.). Only the Quotes block is missing — so the sidebar falls back to the hardcoded English name field in menuConfig.ts.

    Decision: keep these keys in msp/core (where the rest of nav.billing.* lives) rather than introducing cross-namespace navigation. The fix is a pure locale-file backfill — no code change in menuConfig.ts, and no dependency on the msp/quotes namespace load order. Tracked as F022 / T027 below.

  • (2026-04-10) Product markup on quote line items landed after the planning commit (commits 6a40d09fa "Show product markup on quote line items" and 3afa5763f "Add recipient picker and discount-aware markup"). QuoteLineItemsEditor.tsx now renders, for product-kind items only:

    • A live markup badge in the Unit Price cell: `${sign}${markup.toFixed(1)}% markup` -- needs a translation key with {{value}} interpolation (and probably a separate key for the sign, or format the whole string via t with {{signedValue}}).
    • A "Markup unavailable" label + tooltip when cost_currency differs from the quote currency. Tooltip text: `Markup can't be calculated because cost is tracked in ${item.cost_currency} and this quote is in ${currencyCode}.` -- needs {{costCurrency}} and {{quoteCurrency}} interpolation.
    • These all live under a new quoteLineItems.markup.* key group. F006 already covers QuoteLineItemsEditor wiring but predates the markup UI, so the wiring pass must explicitly hit these new strings (tracked as F020).
  • (2026-04-10) New component QuoteSendRecipientsField.tsx (403 LOC) was added in commits 3afa5763f and ead79deec and is consumed by QuoteDetail.tsx and QuoteForm.tsx in their send dialogs. It is a searchable combobox with its own user-visible strings, none of which are covered by F002/F003:

    • Trigger label (three-way ternary): 'Select a client first', 'No users or contacts available', 'Add internal user or client contact…'.
    • Search input placeholder="Search by name or email…".
    • Empty states: 'No recipients available' and 'No matches'.
    • Kind badge values: 'Internal' / 'Contact'.
    • Remove button aria-label={Remove ${r.email}} -- needs {{email}} interpolation.
    • Tracked as F021 / T025 / T026 below. New key group quoteRecipients.*.

Discoveries / Constraints

QuoteForm.tsx (1,072 LOC)

  • Largest component by string density. Contains: form field labels (Title, Description/Scope, Client, Contact, Currency, Quote Date, Valid Until, PO Number, Notes to Client, Terms & Conditions, Quote Layout), heading variants (New Quote, Edit Quote, New Quote Template, Edit Quote Template, Quote QQQ vN), workflow action buttons for every status, three embedded dialogs (send, approval, conversion preview), totals section (Subtotal, Discounts, Tax, Total), status banners (Accepted, Rejected, Converted), read-only notice.
  • The heading logic is complex with conditional template/edit/read-only/quote-number branching -- use a computed key or switch statement with t().
  • Conversion dialog preview is duplicated between QuoteForm and QuoteDetail. Could share keys under quoteConversion.*.
  • formatCurrency is a local closure using form.currency_code -- will need to use the formatter's currency parameter.

QuoteDetail.tsx (1,106 LOC)

  • Similar to QuoteForm but read-only. Additional sections: Version History, Scope of Work, Activity Log, Quote Layout (with template selector), Client Notes, Internal Notes, Terms & Conditions.
  • Has formatQuoteNumber() helper -- "Template quote" fallback string needs translation.
  • Accepted/rejected status sections have inline status text with bold labels ("Accepted by:", "Accepted on:", "Rejected on:", "Reason:").
  • Line item table differs from QuoteLineItemsEditor (read-only, simpler columns). Has client selection badges for optional items ("Client selected this optional item" / "Client declined this optional item").
  • Activity log renders activity.activity_type.replace(/_/g, ' ') -- these are server-side enum values, likely should stay untranslated or get a separate mapping. Leave as-is for now.

QuotesTab.tsx (656 LOC)

  • Contains BASE_QUOTE_COLUMNS defined at module level as a constant array. Column titles ('Quote #', 'Client', 'Title', 'Total', 'Status', 'Date') need t() but the array is outside the component. Will need to move column definitions inside the component or use a factory function.
  • Sub-tab labels use template literals with counts: `Active (${count})`. Use t() with {{count}} interpolation.
  • Two dialogs: delete confirmation and send quote. Send dialog has additional recipients and message fields.
  • QuoteSubTabContent is an inner component -- needs to share the same t() or get its own hook call.

QuoteDocumentTemplateEditor.tsx (596 LOC)

  • Has design/code/preview tabs with pipeline status labels (Shape, Render).
  • Template name/version form fields.
  • "Code view is generated from the Visual workspace and is read-only." info alert.
  • Preview section has "Sample Scenario" label, scenario descriptions (from imported data -- may not be translatable here), and pipeline error messages.
  • Created/Updated timestamps.
  • "Template name is required." validation message.

QuoteLineItemsEditor.tsx (585 LOC)

  • Table headers at module level inside renderItemRows: Move, Item, Billing, Flags, Qty, Unit Price, Total, Actions.
  • Checkbox labels: "Optional", "Recurring".
  • Frequency select options: Weekly, Monthly, Quarterly, Annually.
  • Discount panel: "Percentage discount", "Fixed discount", "Whole quote", "Specific item", "Specific service", "Applies to the full quote subtotal".
  • Phase/section label: "Phase / Section", placeholder "e.g. Discovery, Rollout, Ongoing".
  • Dynamic text: "Custom item", section item count "{n} item(s)", "Set price", "No price in {currency}".
  • Collapse/Expand toggle labels.
  • "Ungrouped Items" as a phase fallback label.

QuoteDocumentTemplatesPage.tsx (299 LOC)

  • Page heading "Quote Layouts", description, "New Layout" button.
  • Table columns: Name, Source, Default, Actions.
  • Action menu: Edit, Edit as Copy, Clone, Set as Default, Delete.
  • Uses confirm() for delete -- should be replaced with a proper translated confirmation, but that is a UX change beyond i18n scope. At minimum translate the confirm message string.
  • "(Standard)" suffix on template names.

QuoteConversionDialog.tsx (293 LOC)

  • Standalone dialog used from multiple places (though QuoteForm and QuoteDetail have their own inline conversion dialogs too).
  • Mode labels: "Contract Only", "Invoice Only", "Contract + Invoice".
  • Mode descriptions are full sentences explaining what each mode does.
  • Item mapping preview with category headings and counts.
  • Summary section: "Quote Total", "Status After Conversion" -> "Converted".
  • Partial conversion alert with dynamic text.

QuoteApprovalDashboard.tsx (245 LOC)

  • Heading "Quote Approvals", description paragraph.
  • Approval required toggle with two conditional description strings.
  • Status filter: "Pending Approval", "Approved".
  • Table columns: Quote #, Client, Title, Amount, Status, Quote Date, Valid Until.
  • Empty state with dynamic status text.
  • "Back to Quotes" button.

QuoteTemplatesList.tsx (215 LOC)

  • Description paragraph about what templates do.
  • Table columns: Title, Items, Currency, Created, Actions.
  • Action menu: Edit Template, Create Quote from Template, Delete.
  • Empty state with "Save as Template" reference.
  • Delete confirmation dialog.
  • "New Template" button.

QuotePreviewPanel.tsx (215 LOC)

  • Panel heading "Quote Preview".
  • Template selector with "(Standard)" suffix.
  • "Open Quote" and "Download PDF" buttons.
  • Empty state: "Select a quote to preview" with icon.
  • Loading state: "Loading Preview..."
  • Error fallback: "Could not display preview. Data might be missing."

Key Gotchas

  1. Column definitions at module scope: BASE_QUOTE_COLUMNS in QuotesTab.tsx and column arrays in QuoteApprovalDashboard.tsx / QuoteTemplatesList.tsx are defined as constants outside the component. They need t() for titles but t() is only available inside the component. Options: (a) move inside component with useMemo, (b) use factory function that takes t as parameter. Option (a) is simplest and consistent with other translated components.
  2. Shared conversion dialog content: QuoteForm.tsx, QuoteDetail.tsx, and QuoteConversionDialog.tsx all have conversion preview UI. Share keys under quoteConversion.* to avoid triple-maintaining the same strings.
  3. Template string in headings: QuoteForm heading uses complex conditional logic. Keep the branching but wrap each branch in t().
  4. Interpolation-heavy strings: Sub-tab labels like Active (${count}), section counts like ${count} item(s), and notice messages like Created draft contract ${name} need careful interpolation variable placement. Use {{count}}, {{name}} patterns.
  5. formatCurrency/formatDate helper replacement: These are defined as standalone functions, not as component methods. Replacing them with useFormatters() requires either moving the formatting call inside the component or passing the formatter down. Since these functions are used in column definitions (which will move inside the component per gotcha #1), this should align naturally.
  6. Keep stable values untranslated: Tab IDs (active, sent, closed, approval), query param values (quotes, edit, detail), status enum values (draft, sent, accepted, etc.), CSS class names, element IDs, route paths, aria-label values that are already descriptive English.
  7. QuoteStatusBadge is a pass-through: It renders metadata.label from a types constant. The label translation belongs in the types layer or a shared status-label mapping, not in this namespace. Leave it untranslated for now.
  8. Shared billing-frequency hook was missing weekly: QuoteLineItemsEditor.tsx still exposes Weekly / Monthly / Quarterly / Annually, but useBillingFrequencyOptions() only had monthly/quarterly/annually when this batch started. Added follow-on F024/T029 so the quote editor can adopt the shared enum hook without losing the existing weekly option.
  9. Plan missed remaining formatter hotspots: After completing F012/F013 for the originally-listed files, QuoteForm.tsx still had accepted/rejected banner dates plus a local en-US money helper, and quoteLineItemDraft.ts still exported a draft-money formatter pinned to en-US that surfaced through QuoteForm and QuoteLineItemsEditor. Added F025/T030 so the checklist matches the PRD acceptance criteria instead of silently leaving those locale-pinned displays behind.

Progress Log

  • (2026-04-19) Completed F001: created server/public/locales/en/msp/quotes.json with the planned component-scoped roots (common, quotesTab, quoteForm, quoteDetail, quoteLineItems, quoteRecipients, quoteConversion, quoteApproval, quoteTemplates, quotePreview, templateEditor, templatesPage) and seeded the first shared/base English keys from the PRD + component inventory. Expect follow-on wiring passes (F002-F021) to expand individual leaf keys as each component is converted.

  • (2026-04-19) Completed T001: added packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a node-side contract test that parses en/msp/quotes.json and asserts the namespace exposes the planned top-level groups in order. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F002: wired packages/billing/src/components/billing-dashboard/quotes/QuoteForm.tsx to useTranslation('msp/quotes') for the form chrome, workflow actions, notices/errors, send/approval/conversion dialogs, status banners, and totals footer. Expanded server/public/locales/en/msp/quotes.json with the quoteForm.* leaf keys now consumed by the component. Validation:

    • rg -n "text=\\\"|title=\\\"|placeholder=\\\"|>[^<{]*[A-Za-z][^<{]*<" packages/billing/src/components/billing-dashboard/quotes/QuoteForm.tsx -> only non-UI match left was the runWorkflowAction helper signature; all rendered copy now flows through t(...).
    • cd packages/billing && npm run typecheck -> blocked by a pre-existing upstream issue outside this batch: packages/ui/src/lib/dateFnsLocale.ts is missing the pt locale mapping required by the now-expanded locale union. Track this for the later build/typecheck gate (F019/T024) unless another batch lands the fix first.
  • (2026-04-19) Completed T003: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuoteForm contract test that asserts the useTranslation('msp/quotes') hook is present and that representative form/workflow/dialog keys resolve from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed T004: added a negative-source regression check to packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts covering the major raw-English literals that used to be rendered directly by QuoteForm.tsx (buttons, labels, placeholders, dialog titles, conversion preview headings, and loading text). Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F003: wired packages/billing/src/components/billing-dashboard/quotes/QuoteDetail.tsx to useTranslation('msp/quotes') across the page shell, field summaries, totals, version history, status banners, line-item table, converted-record links, and the conversion/preview/send/approval dialogs. Reused the shared quoteForm.*, quoteConversion.*, quoteLineItems.*, and common.* keys where the UI copy matches, and expanded server/public/locales/en/msp/quotes.json with the remaining quoteDetail.* leaves (actions, alerts, notices, errors, preview/loading, and table labels). Validation:

    • rg -n "text=\\\"|title=\\\"|placeholder=\\\"|>[^<{]*[A-Za-z][^<{]*<" packages/billing/src/components/billing-dashboard/quotes/QuoteDetail.tsx -> no matches; the detail component no longer renders raw English JSX literals directly.
    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the quoteDetail.* additions.
  • (2026-04-19) Completed T005: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuoteDetail contract test that asserts the translation hook is present and that representative detail, dialog, badge, and conversion-preview keys resolve from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed T006: added a negative-source regression check to packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts covering the major raw-English literals that used to be rendered directly by QuoteDetail.tsx (page actions, section headings, converted-record links, dialog titles, and table headers). Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F004: wired packages/billing/src/components/billing-dashboard/quotes/QuotesTab.tsx and its inner QuoteSubTabContent to useTranslation('msp/quotes'), including the sub-tab labels, table column titles, row-action menu labels/aria copy, client filter, empty state, loading state, and the delete/send dialogs. Moved the previously module-scoped quote columns into a translation-aware useMemo, and expanded server/public/locales/en/msp/quotes.json with the remaining quotesTab.* leaves (actions, filters.allClients, empty.byCategory, errors, loading, and the revised send/delete dialog copy). Validation:

    • rg -n "text=\\\"|title=\\\"|placeholder=\\\"|>[^<{]*[A-Za-z][^<{]*<" packages/billing/src/components/billing-dashboard/quotes/QuotesTab.tsx -> only remaining matches are non-UI function-type signatures (onDownload, onDuplicate, onDownloadPdf) inside the props interface.
    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the quotesTab.* additions.
  • (2026-04-19) Completed T007: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuotesTab contract test that asserts the translation hook is present and that representative tab, table, action-menu, and dialog keys resolve from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed T008: added a negative-source regression check to packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts covering the major raw-English literals that used to be rendered directly by QuotesTab.tsx (page heading, row-action labels, filter labels, send-dialog copy, and loading text). Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F005: wired packages/billing/src/components/billing-dashboard/quotes/QuoteDocumentTemplateEditor.tsx to useTranslation('msp/quotes') for the page chrome, layout-detail form labels, tab labels, preview-sample controls, preview-pipeline status/error copy, code-view read-only alert, and footer timestamps. Expanded server/public/locales/en/msp/quotes.json with the templateEditor.* action/default/error/footer/heading/placeholder/preview/pipeline keys now consumed by the component. Validation:

    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the templateEditor.* additions.
    • Source sweep via rg confirmed the remaining plain string literals in QuoteDocumentTemplateEditor.tsx are technical/internal values ('visual', 'preview', quote-designer-preview-*, AST kind names, etc.) rather than rendered UI copy.
  • (2026-04-19) Completed T009: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuoteDocumentTemplateEditor contract test that asserts the translation hook is present and that representative editor, preview-pipeline, and footer keys resolve from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed T010: added a negative-source regression check to packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts covering the major raw-English literals that used to be rendered directly by QuoteDocumentTemplateEditor.tsx (editor headings, detail labels, tab labels, preview status copy, and footer labels). Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F024: extended the shared billing-frequency enum contract with weekly in packages/billing/src/constants/billing.ts and all 9 server/public/locales/*/features/billing.json variants. This was required before QuoteLineItemsEditor.tsx could adopt useBillingFrequencyOptions() without regressing the existing weekly recurrence option noted in the PRD addendum. Validation:

    • node - <<'NODE' ... NODE audit confirmed every locale now exposes enums.billingFrequency.weekly alongside monthly, quarterly, and annually.
  • (2026-04-19) Completed T029: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a source-level contract test for the shared billing-frequency enum additions. It asserts billing.ts now exports weekly in both BILLING_FREQUENCY_VALUES and BILLING_FREQUENCY_LABEL_DEFAULTS, and that all 9 features/billing.json locale files define enums.billingFrequency.weekly. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F006: wired packages/billing/src/components/billing-dashboard/quotes/QuoteLineItemsEditor.tsx to useTranslation('msp/quotes') for the section heading, table headers, section fallback/count labels, optional/recurring controls, discount badge + target descriptions, discount-panel selectors/actions, empty state, remove button, and inline pricing hints. This pass also switched recurring-frequency options over to useBillingFrequencyOptions() / useFormatBillingFrequency() and moved the product-markup badge/tooltip text behind t(...) in preparation for F020. Expanded server/public/locales/en/msp/quotes.json with the new quoteLineItems.actions, billingMethods, discounts.description/targets/types, labels.itemCount*, and picker-placeholder keys. Validation:

    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the quoteLineItems.* additions.
    • node - <<'NODE' ... NODE audit confirmed the component now references the planned quoteLineItems.* keys for table chrome, controls, and inline hints.
  • (2026-04-19) Completed T011: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuoteLineItemsEditor contract test that asserts the msp/quotes hook is present, the shared billing-frequency hooks are used, and representative quoteLineItems.* keys resolve from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed T012: added a negative-source regression check to packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts covering the old raw-English table headers, discount controls, inline price hints, and markup text that used to be rendered directly by QuoteLineItemsEditor.tsx. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F007: wired packages/billing/src/components/billing-dashboard/quotes/QuoteDocumentTemplatesPage.tsx to useTranslation('msp/quotes') for the page heading/description, error alert title, table column titles, action-menu labels, standard/custom source labels, "Open menu" screen-reader text, and the delete-confirm prompt. Reused existing common.actions.*, common.columns.*, and common.badges.standard keys where the wording already matched, and expanded server/public/locales/en/msp/quotes.json with the remaining templatesPage.actions/dialogs/errors/labels.* leaves. Validation:

    • node - <<'NODE' ... NODE audit confirmed the component now references the planned templatesPage.* and shared common.* keys for all page chrome and menu actions.
    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the templatesPage.* additions.
  • (2026-04-19) Completed T013: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuoteDocumentTemplatesPage contract test that asserts the page uses useTranslation('msp/quotes') and that representative page, menu, error, and shared table/action keys resolve from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F008: wired packages/billing/src/components/billing-dashboard/quotes/QuoteConversionDialog.tsx to useTranslation('msp/quotes') for the dialog title/description, loading and error states, partial-conversion alert copy, mode labels/descriptions, item-mapping headings, summary labels, and cancel/convert button states. Also replaced the local en-US currency formatter with useFormatters().formatCurrency() here, which partially advances F012 for this dialog. Expanded server/public/locales/en/msp/quotes.json with the remaining quoteConversion.description, quoteConversion.errors.*, quoteConversion.sections.contractItems, quoteConversion.partial.*, and quoteConversion.actions.convertQuote leaves. Validation:

    • node - <<'NODE' ... NODE audit confirmed the component now references the planned quoteConversion.* keys for dialog copy, sections, and actions.
    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the quoteConversion.* additions.
  • (2026-04-19) Completed T014: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuoteConversionDialog contract test that asserts the standalone dialog uses useTranslation('msp/quotes') and that representative dialog, mode, partial-alert, summary, and action keys resolve from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F009: wired packages/billing/src/components/billing-dashboard/quotes/QuoteApprovalDashboard.tsx to useTranslation('msp/quotes') for the page title/description, approval-settings label and toggle copy, status filter label/options, back button, loading state, error alert title, empty-state messages, and all table columns. This pass also replaced the local en-US amount/date helpers with useFormatters().formatCurrency() / useFormatters().formatDate(), which partially advances F012 and F013 for the approval dashboard. Expanded server/public/locales/en/msp/quotes.json with the remaining quoteApproval.actions/errors/loading/empty.* leaves and aligned the existing settings strings with the current rendered copy. Validation:

    • node - <<'NODE' ... NODE audit confirmed the component now references the planned quoteApproval.* and shared common.columns.* keys, plus the locale-aware formatters hook.
    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the quoteApproval.* additions.
  • (2026-04-19) Completed T015: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuoteApprovalDashboard contract test that asserts the dashboard uses useTranslation('msp/quotes'), adopts the locale-aware formatters hook, and resolves representative page/filter/empty-state/column keys from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F010: wired packages/billing/src/components/billing-dashboard/quotes/QuoteTemplatesList.tsx to useTranslation('msp/quotes') for the description text, new-template button, loading state, alert title, empty-state sentence, row-action menu labels/aria copy, and delete confirmation dialog. This pass also replaced the local en-US currency/date helpers with useFormatters().formatCurrency() / useFormatters().formatDate(), which partially advances F012 and F013 for the templates list. Expanded server/public/locales/en/msp/quotes.json with the remaining quoteTemplates.title/loading/errors/dialogs/actions.templateActions/empty.inline leaves while reusing shared common.columns.* and common.actions.* keys for the table chrome. Validation:

    • node - <<'NODE' ... NODE audit confirmed the component now references the planned quoteTemplates.*, shared common.*, and locale-aware formatter hooks.
    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the quoteTemplates.* additions.
  • (2026-04-19) Completed T016: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuoteTemplatesList contract test that asserts the list uses useTranslation('msp/quotes'), adopts the locale-aware formatters hook, and resolves representative list/action/dialog keys from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F011: wired packages/billing/src/components/billing-dashboard/quotes/QuotePreviewPanel.tsx to useTranslation('msp/quotes') for the panel heading, no-selection empty state, template-selector placeholder and standard-template suffix, loading state, open/download button labels, preview-unavailable fallback, and action/load error messaging. Expanded server/public/locales/en/msp/quotes.json with the missing quotePreview.errors.* leaves and reused common.actions.downloadPdf / common.badges.standard for the shared button/suffix copy. Validation:

    • node - <<'NODE' ... NODE audit confirmed the component now references the planned quotePreview.* and shared common.* keys for panel chrome and actions.
    • node -e "const fs=require('fs'); JSON.parse(fs.readFileSync('server/public/locales/en/msp/quotes.json','utf8')); console.log('quotes.json ok');" -> parsed successfully after the quotePreview.* additions.
  • (2026-04-19) Completed T017: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a QuotePreviewPanel contract test that asserts the panel uses useTranslation('msp/quotes') and resolves representative panel, action, empty/loading/error, and shared suffix/button keys from en/msp/quotes.json. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F012: replaced the remaining hardcoded Intl.NumberFormat('en-US', ...) quote currency helpers in QuotesTab.tsx, QuoteDetail.tsx, QuoteConversionDialog.tsx, QuoteApprovalDashboard.tsx, and QuoteTemplatesList.tsx with useFormatters().formatCurrency(). Amount renderers now pass major-unit values into the shared formatter instead of manually instantiating locale-pinned Intl.NumberFormat. Validation:

    • rg -n "Intl\\.NumberFormat\\('en-US'" packages/billing/src/components/billing-dashboard/quotes/{QuotesTab,QuoteDetail,QuoteConversionDialog,QuoteApprovalDashboard,QuoteTemplatesList}.tsx -> 0 matches.
    • node - <<'NODE' ... NODE audit confirmed all F012 target files now import/use useFormatters.
  • (2026-04-19) Completed F013: replaced the remaining quote date/timestamp helpers with useFormatters().formatDate() across QuotesTab.tsx, QuoteDetail.tsx, QuoteApprovalDashboard.tsx, and QuoteDocumentTemplateEditor.tsx. The detail accepted/rejected banners now flow through the shared formatter, and the template editor footer timestamps use locale-aware date+time options instead of toLocaleString(). Validation:

    • rg -n "toLocaleDateString\\(|toLocaleString\\(" packages/billing/src/components/billing-dashboard/quotes/{QuotesTab,QuoteDetail,QuoteApprovalDashboard,QuoteDocumentTemplateEditor}.tsx -> 0 matches.
    • node - <<'NODE' ... NODE audit confirmed all F013 target files now import/use useFormatters.
  • (2026-04-19) Completed F025: cleaned up the remaining user-visible formatter gaps outside the original F012/F013 file list. QuoteForm.tsx now uses useFormatters() for quote totals and accepted/rejected banner dates, QuoteLineItemsEditor.tsx uses useFormatters().formatCurrency() for inline unit/total money displays, and quoteLineItemDraft.ts no longer hardcodes en-US in formatDraftQuoteMoney(). Validation:

    • rg -n "Intl\\.NumberFormat\\('en-US'|toLocaleDateString\\(" packages/billing/src/components/billing-dashboard/quotes/{QuoteForm,QuoteLineItemsEditor,quoteLineItemDraft}.ts* -> 0 matches.
    • node - <<'NODE' ... NODE audit confirmed the remaining user-facing formatter consumers now use locale-aware paths.
  • (2026-04-19) Completed T018: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a source-level formatter regression check covering the five original F012 currency targets. It asserts each file imports useFormatters and no longer contains Intl.NumberFormat('en-US', ...). Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed T019: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a source-level formatter regression check covering the four original F013 date targets. It asserts each file imports useFormatters and no longer contains .toLocaleDateString() / .toLocaleString(). Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed T030: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a follow-on source-level formatter regression check for QuoteForm.tsx, QuoteLineItemsEditor.tsx, and quoteLineItemDraft.ts. It guards the newly-added F025 cleanup by asserting the components use useFormatters() and the draft-money utility no longer hardcodes en-US. Validation command: cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts.

  • (2026-04-19) Completed F014: generated the six production msp/quotes locale files at server/public/locales/de/msp/quotes.json, server/public/locales/es/msp/quotes.json, server/public/locales/fr/msp/quotes.json, server/public/locales/it/msp/quotes.json, server/public/locales/nl/msp/quotes.json, and server/public/locales/pl/msp/quotes.json. Seeded them from the English namespace with placeholder-protected machine translation, then manually corrected the business-domain strings where quote/markup drifted into citation/Zitat/cytat/formatting language during QA. Validation:

    • node - <<'NODE' ... NODE parity audit confirmed all six locale files now match en/msp/quotes.json at 420 leaf keys each with missing: 0, extra: 0, and badVars: 0.
    • rg -n "citation|cotation|citaat|cytat|Zitat|citazione|Marcado|__ALGA" server/public/locales/{de,es,fr,it,nl,pl}/msp/quotes.json -> 0 matches after the domain-term cleanup and interpolation-token sweep.
  • (2026-04-19) Completed F015: audited server/public/locales/it/msp/quotes.json for the common missing-accent regressions (perche, piu, puo, cosi, gia, weekday forms, etc.) and found no exact-word matches. Accent presence spot-counts confirmed the file already contains the expected accented vowels (à: 15, è: 12, é: 1, ò: 2), so the audit passed without further accent corrections.

  • (2026-04-19) Completed F016: regenerated pseudo-locales via node scripts/generate-pseudo-locales.cjs, which rebuilt 68 pseudo-locale files from 34 English sources. For this batch the only newly-created artifacts were server/public/locales/xx/msp/quotes.json and server/public/locales/yy/msp/quotes.json. Spot-check validation:

    • quotesTab.title -> 11111 in xx, 55555 in yy
    • quoteRecipients.removeAriaLabel preserved {{email}} as 11111 {{email}} 11111 / 55555 {{email}} 55555
    • quoteLineItems.markup.badge preserved both {{sign}} and {{value}} in the expected pseudo-locale fill pattern.
  • (2026-04-19) Completed F017: ran node scripts/validate-translations.cjs and got Errors: 0, Warnings: 0, PASSED across de, es, fr, it, nl, pl, pt, xx, and yy. The only prerequisite fix was repo-level fallback plumbing for Portuguese: bootstrapped server/public/locales/pt/msp/quotes.json from the English namespace and added the missing enums.billingFrequency.weekly key to server/public/locales/pt/features/billing.json, because the validator checks every real locale directory under server/public/locales/, not just the six translated quote locales plus English.

  • (2026-04-19) Completed F018: updated packages/core/src/lib/i18n/config.ts so /msp/billing now loads msp/quotes, and added explicit route entries for /msp/quote-approvals and /msp/quote-document-templates. Kept features/billing on the standalone quote routes as well so the billing enum/status resources remain available there. Validation:

    • rg -n \"'/msp/billing'|'/msp/quote-approvals'|'/msp/quote-document-templates'|msp/quotes\" packages/core/src/lib/i18n/config.ts -> verified all three route entries now reference msp/quotes in the source.
  • (2026-04-19) Completed F019: npm run build now succeeds end-to-end. Fixes required to clear the build gate:

  • (2026-04-19) Completed F020: verified the post-planning product-markup UI in packages/billing/src/components/billing-dashboard/quotes/QuoteLineItemsEditor.tsx already routes the badge, unavailable state, and currency-mismatch tooltip through t('quoteLineItems.markup.*') with the required {{sign}}, {{value}}, {{costCurrency}}, and {{quoteCurrency}} interpolations. Locale audit confirmed quoteLineItems.markup exists in en, de, es, fr, it, nl, pl, the pt fallback tree, and both pseudo-locales.

  • (2026-04-19) Completed F021: wired packages/billing/src/components/billing-dashboard/quotes/QuoteSendRecipientsField.tsx to useTranslation('msp/quotes') for all remaining user-visible combobox strings: the three trigger states, the search placeholder, the no-results/no-options empty states, the internal/contact kind badges, and the interpolated remove-recipient aria-label. The quoteRecipients.* keys were already present in en/msp/quotes.json and had already propagated to the translated / pseudo locale files during F014 and F016, so this step was limited to component wiring. Validation:

    • node - <<'NODE' ... NODE source audit confirmed the component now references quoteRecipients.trigger.*, quoteRecipients.searchPlaceholder, quoteRecipients.empty.*, quoteRecipients.kind.*, and quoteRecipients.removeAriaLabel.
    • node - <<'NODE' ... NODE locale audit confirmed quoteRecipients is present in every current msp/quotes.json variant (en, de, es, fr, it, nl, pl, pt, xx, yy).
  • (2026-04-19) Completed F022: backfilled the missing MSP sidebar quote-navigation keys in server/public/locales/en/msp/core.json, server/public/locales/de/msp/core.json, server/public/locales/es/msp/core.json, server/public/locales/fr/msp/core.json, server/public/locales/it/msp/core.json, server/public/locales/nl/msp/core.json, server/public/locales/pl/msp/core.json, and the pt fallback tree at server/public/locales/pt/msp/core.json. Added nav.billing.sections.quotes, nav.billing.quotes, nav.billing.quoteBusinessTemplates, and nav.billing.quoteLayouts, then regenerated pseudo-locales so server/public/locales/xx/msp/core.json / server/public/locales/yy/msp/core.json picked up the new keys automatically. Validation:

    • node - <<'NODE' ... NODE spot check confirmed de now renders Angebote / Angebotsvorlagen / Angebotslayouts and xx renders 11111 for the Quotes section header and all three items.
    • node scripts/generate-pseudo-locales.cjs -> regenerated pseudo locale artifacts after the English core backfill.
    • node scripts/validate-translations.cjs -> Errors: 0, Warnings: 0, PASSED after the msp/core additions.
  • (2026-04-19) Completed T002: reran node scripts/validate-translations.cjs after the final locale work (F017 + F022) and the translation validator still reports Locales checked: 9, Errors: 0, Warnings: 0, PASSED. This covers msp/quotes key parity / empty-value checks across de, es, fr, it, nl, pl, pt, xx, and yy.

  • (2026-04-19) Completed T020: reran the Italian accent audit on server/public/locales/it/msp/quotes.json. A source scan found no unaccented regressions for the common problem words (perche, piu, puo, cosi, gia), and the file still contains the expected accented-vowel mix (à: 15, è: 12, é: 1, ò: 2).

  • (2026-04-19) Completed T021: recursively walked server/public/locales/xx/msp/quotes.json and confirmed every string leaf contains the expected 11111 pseudo-locale fill pattern (missing: 0). This covers the visual-QA expectation that the quote namespace no longer falls back to readable English in xx.

  • (2026-04-19) Completed T022: ran an explicit interpolation-parity audit between server/public/locales/en/msp/quotes.json and every translated / pseudo locale variant (de, es, fr, it, nl, pl, pt, xx, yy). The audit flattened all 420 leaf keys and confirmed the placeholder sets match exactly for every locale (badVars: 0), covering strings like {{count}}, {{name}}, {{email}}, {{costCurrency}}, {{quoteCurrency}}, {{sign}}, and {{value}}.

  • (2026-04-19) Completed T023: refreshed packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts so the route-namespace contract checks the current ROUTE_NAMESPACES source instead of brittle one-line import snapshots. The test now asserts /msp/billing, /msp/quote-approvals, and /msp/quote-document-templates all include msp/quotes in packages/core/src/lib/i18n/config.ts. Validation:

    • cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts -> 24 passed, including the new T023 route assertion and the refreshed earlier wiring checks.
  • (2026-04-19) Completed T024: reran npm run build after the final locale backfills and contract-test refresh. The build completed successfully (exit code 0), including the next build --webpack phase and the full route manifest generation, with only the existing non-blocking webpack warnings that were already present during F019.

  • (2026-04-19) Completed T025: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with an explicit markup contract for QuoteLineItemsEditor.tsx. The test now asserts the component routes the badge / unavailable label / unavailable tooltip through quoteLineItems.markup.*, guards against the old raw template-literal render paths, and verifies quoteLineItems.markup.badge, .unavailable, and .unavailableTooltip exist in en, de, es, fr, it, nl, pl, xx, and yy. Validation:

    • cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts -> 24 passed, including T025.
  • (2026-04-19) Completed T026: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a dedicated recipients-field contract for QuoteSendRecipientsField.tsx. The test asserts the component imports useTranslation('msp/quotes'), references every quoteRecipients.* leaf used by the combobox UI, rejects the old raw placeholder / ternary / aria-label={Remove ${r.email}} render paths, and verifies the locale keys exist in en, de, es, fr, it, nl, pl, xx, and yy. Validation:

    • cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts -> 24 passed, including T026.
  • (2026-04-19) Completed T027: extended packages/billing/tests/billing-dashboard/QuotesSubbatch.i18n.test.ts with a sidebar-locale contract for the Quotes nav block. The test verifies nav.billing.sections.quotes, nav.billing.quotes, nav.billing.quoteBusinessTemplates, and nav.billing.quoteLayouts exist in en, de, es, fr, it, nl, pl, xx, and yy, and it hard-checks the smoke-test values called out in the PRD (de -> Angebote / Angebotsvorlagen / Angebotslayouts, xx -> 11111 fill for the section + all items). Validation:

    • cd packages/billing && npx vitest run tests/billing-dashboard/QuotesSubbatch.i18n.test.ts -> 24 passed, including T027.

Commands / Runbook

  • git status --short
  • rg -n '>[[:space:]]*[A-Z][^<{]*<' packages/billing/src/components/billing-dashboard/quotes/*.tsx
  • rg -n "'[^']*[A-Za-z][^']*'|\"[^\"]*[A-Za-z][^\"]*\"" packages/billing/src/components/billing-dashboard/quotes/*.tsx