@dainamite/cpq 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +3 -9
  3. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
  4. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
  5. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
  6. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
  7. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
  8. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
  9. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
  10. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +2 -2
  11. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
  12. package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
  13. package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
  14. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +154 -178
  15. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  16. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
  17. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
  18. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
  19. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
  20. package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
  21. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
  22. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +69 -66
  23. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  24. package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
  25. package/dist/modules/cpq/backend/cpq/orders/page.js.map +2 -2
  26. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +41 -36
  27. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
  28. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
  29. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
  30. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
  31. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
  32. package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
  33. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
  34. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
  35. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
  36. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
  37. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
  38. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
  39. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
  40. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
  41. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
  42. package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
  43. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
  44. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
  45. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
  46. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +6 -3
  47. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +2 -2
  48. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +129 -94
  49. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +2 -2
  50. package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
  51. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
  52. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +19 -15
  53. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
  54. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
  55. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
  56. package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
  57. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
  58. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
  59. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
  60. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
  61. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
  62. package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
  63. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
  64. package/dist/modules/cpq/components/ArcLineDiff.js +2 -2
  65. package/dist/modules/cpq/components/ArcLineDiff.js.map +2 -2
  66. package/dist/modules/cpq/components/ChargeBreakdownTable.js +4 -10
  67. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +2 -2
  68. package/dist/modules/cpq/components/PriceWaterfall.js +3 -1
  69. package/dist/modules/cpq/components/PriceWaterfall.js.map +2 -2
  70. package/dist/modules/cpq/components/PricingSummaryCard.js +1 -6
  71. package/dist/modules/cpq/components/PricingSummaryCard.js.map +2 -2
  72. package/dist/modules/cpq/components/StatusPath.js +5 -3
  73. package/dist/modules/cpq/components/StatusPath.js.map +2 -2
  74. package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
  75. package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
  76. package/dist/modules/cpq/i18n/de.json +1220 -0
  77. package/dist/modules/cpq/i18n/en.json +1220 -0
  78. package/dist/modules/cpq/i18n/es.json +1220 -0
  79. package/dist/modules/cpq/i18n/pl.json +1220 -0
  80. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
  81. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
  82. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
  83. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
  84. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
  85. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
  86. package/dist/modules/cpq/widgets/injection-table.js +2 -2
  87. package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
  88. package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
  89. package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
  90. package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
  91. package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
  92. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
  93. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
  94. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
  95. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
  96. package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
  97. package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
  98. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
  99. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
  100. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
  101. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
  102. package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
  103. package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
  104. package/dist/modules/cpq/workflows/steps/index.js +12 -0
  105. package/dist/modules/cpq/workflows/steps/index.js.map +2 -2
  106. package/package.json +1 -1
  107. package/src/modules/cpq/__tests__/i18n/dictionaries.test.ts +62 -0
  108. package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
  109. package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -0
  110. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +3 -9
  111. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
  112. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
  113. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
  114. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +2 -2
  115. package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
  116. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
  117. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +134 -112
  118. package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
  119. package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
  120. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +66 -43
  121. package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
  122. package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
  123. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
  124. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
  125. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
  126. package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
  127. package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
  128. package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
  129. package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
  130. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  131. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
  132. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +3 -3
  133. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +134 -89
  134. package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
  135. package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
  136. package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
  137. package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
  138. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
  139. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
  140. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  141. package/src/modules/cpq/components/ArcLineDiff.tsx +2 -2
  142. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +4 -2
  143. package/src/modules/cpq/components/PriceWaterfall.tsx +4 -2
  144. package/src/modules/cpq/components/PricingSummaryCard.tsx +1 -1
  145. package/src/modules/cpq/components/StatusPath.tsx +7 -4
  146. package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
  147. package/src/modules/cpq/i18n/de.json +1220 -0
  148. package/src/modules/cpq/i18n/en.json +1220 -0
  149. package/src/modules/cpq/i18n/es.json +1220 -0
  150. package/src/modules/cpq/i18n/pl.json +1220 -0
  151. package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
  152. package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
  153. package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
  154. package/src/modules/cpq/widgets/injection-table.ts +8 -2
  155. package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
  156. package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
  157. package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
  158. package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
  159. package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
  160. package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
  161. package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
  162. package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
  163. package/src/modules/cpq/workflows/steps/index.ts +12 -0
  164. package/src/modules/cpq/workflows/types.ts +4 -0
@@ -2,14 +2,15 @@
2
2
  import * as React from 'react'
3
3
  import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
4
4
  import { useRouter } from 'next/navigation'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
6
  import { Tag } from '@open-mercato/ui/primitives/tag'
6
7
  import {
7
8
  assetStatusMap,
8
- formatStatusLabel,
9
9
  subscriptionStatusMap,
10
10
  type AssetStatus,
11
11
  type SubscriptionStatus,
12
12
  } from '../../../components/statusMaps'
13
+ import { inventoryStatusLabel } from '../../../backend/cpq/inventory/shared'
13
14
 
14
15
  // ─── Types ───────────────────────────────────────────────────────
15
16
 
@@ -79,13 +80,14 @@ function fmt(amount: number, currency: string): string {
79
80
  }
80
81
 
81
82
  function StatusBadge({ status }: { status: string }) {
83
+ const t = useT()
82
84
  const variant =
83
85
  subscriptionStatusMap[status as SubscriptionStatus] ??
84
86
  assetStatusMap[status as AssetStatus] ??
85
87
  'neutral'
86
88
  return (
87
89
  <Tag variant={variant} dot>
88
- {formatStatusLabel(status)}
90
+ {inventoryStatusLabel(t, status)}
89
91
  </Tag>
90
92
  )
91
93
  }
@@ -93,6 +95,7 @@ function StatusBadge({ status }: { status: string }) {
93
95
  // ─── Main Widget ─────────────────────────────────────────────────
94
96
 
95
97
  export default function CustomerInventoryWidget({ data, context }: InjectionWidgetComponentProps & { context?: Record<string, unknown> }) {
98
+ const t = useT()
96
99
  const router = useRouter()
97
100
  const record = data as Record<string, unknown> | null
98
101
  const companyRecord = record?.company as Record<string, unknown> | undefined
@@ -121,12 +124,12 @@ export default function CustomerInventoryWidget({ data, context }: InjectionWidg
121
124
  return () => { cancelled = true }
122
125
  }, [customerId])
123
126
 
124
- if (!customerId) return <div className="p-4 text-sm text-muted-foreground">No customer context available.</div>
127
+ if (!customerId) return <div className="p-4 text-sm text-muted-foreground">{t('cpq.widgets.customerInventory.noContext', 'No customer context available.')}</div>
125
128
 
126
129
  if (loading) {
127
130
  return (
128
131
  <div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
129
- <Spinner /> Loading inventory…
132
+ <Spinner /> {t('cpq.widgets.customerInventory.loading', 'Loading inventory…')}
130
133
  </div>
131
134
  )
132
135
  }
@@ -139,7 +142,7 @@ export default function CustomerInventoryWidget({ data, context }: InjectionWidg
139
142
  <path strokeLinecap="round" strokeLinejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
140
143
  </svg>
141
144
  </div>
142
- <p className="text-sm text-muted-foreground">No inventory records found.</p>
145
+ <p className="text-sm text-muted-foreground">{t('cpq.widgets.customerInventory.empty', 'No inventory records found.')}</p>
143
146
  </div>
144
147
  )
145
148
  }
@@ -148,19 +151,19 @@ export default function CustomerInventoryWidget({ data, context }: InjectionWidg
148
151
  const currency = summary.currencyCode || 'USD'
149
152
 
150
153
  const tabs: { key: TabKey; label: string; count: number }[] = [
151
- { key: 'subscriptions', label: 'Subscriptions', count: inventory.subscriptions.length },
152
- { key: 'items', label: 'Subscription Items', count: inventory.subscriptionItems.length },
153
- { key: 'assets', label: 'Assets', count: inventory.assets.length },
154
+ { key: 'subscriptions', label: t('cpq.widgets.customerInventory.tabs.subscriptions', 'Subscriptions'), count: inventory.subscriptions.length },
155
+ { key: 'items', label: t('cpq.widgets.customerInventory.tabs.items', 'Subscription Items'), count: inventory.subscriptionItems.length },
156
+ { key: 'assets', label: t('cpq.widgets.customerInventory.tabs.assets', 'Assets'), count: inventory.assets.length },
154
157
  ]
155
158
 
156
159
  return (
157
160
  <div className="space-y-4">
158
161
  {/* Summary Cards */}
159
162
  <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
160
- <SummaryCard label="Active Subscriptions" value={String(summary.activeSubscriptions)} />
161
- <SummaryCard label="Total MRC" value={fmt(summary.totalMrc, currency)} mono />
162
- <SummaryCard label="Active Assets" value={String(summary.activeAssets)} />
163
- <SummaryCard label="Pending Items" value={String(summary.pendingItems)} />
163
+ <SummaryCard label={t('cpq.widgets.customerInventory.summary.activeSubscriptions', 'Active Subscriptions')} value={String(summary.activeSubscriptions)} />
164
+ <SummaryCard label={t('cpq.widgets.customerInventory.summary.totalMrc', 'Total MRC')} value={fmt(summary.totalMrc, currency)} mono />
165
+ <SummaryCard label={t('cpq.widgets.customerInventory.summary.activeAssets', 'Active Assets')} value={String(summary.activeAssets)} />
166
+ <SummaryCard label={t('cpq.widgets.customerInventory.summary.pendingItems', 'Pending Items')} value={String(summary.pendingItems)} />
164
167
  </div>
165
168
 
166
169
  {/* Tab Switcher */}
@@ -189,7 +192,7 @@ export default function CustomerInventoryWidget({ data, context }: InjectionWidg
189
192
  {/* Link to full page */}
190
193
  <div className="pt-2 text-right">
191
194
  <a href="/backend/cpq/inventory" className="text-sm text-primary hover:underline">
192
- View full inventory →
195
+ {t('cpq.widgets.customerInventory.viewFull', 'View full inventory →')}
193
196
  </a>
194
197
  </div>
195
198
  </div>
@@ -210,18 +213,19 @@ function SummaryCard({ label, value, mono }: { label: string; value: string; mon
210
213
  // ─── Subscriptions Table ─────────────────────────────────────────
211
214
 
212
215
  function SubscriptionsTable({ items, currency, onOpen }: { items: Subscription[]; currency: string; onOpen: (id: string) => void }) {
216
+ const t = useT()
213
217
  if (items.length === 0) return <EmptyTab />
214
218
  return (
215
219
  <div className="rounded-lg border bg-card overflow-hidden">
216
220
  <table className="w-full text-sm">
217
221
  <thead>
218
222
  <tr className="border-b border-border bg-muted/40 text-muted-foreground">
219
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Name</th>
220
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Status</th>
221
- <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">MRC</th>
222
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Billing</th>
223
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Start Date</th>
224
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Term End</th>
223
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.name', 'Name')}</th>
224
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.status', 'Status')}</th>
225
+ <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.mrc', 'MRC')}</th>
226
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.billing', 'Billing')}</th>
227
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.startDate', 'Start Date')}</th>
228
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.termEnd', 'Term End')}</th>
225
229
  </tr>
226
230
  </thead>
227
231
  <tbody>
@@ -248,17 +252,18 @@ function SubscriptionsTable({ items, currency, onOpen }: { items: Subscription[]
248
252
  // ─── Subscription Items Table ────────────────────────────────────
249
253
 
250
254
  function SubscriptionItemsTable({ items, currency, onOpen }: { items: SubscriptionItem[]; currency: string; onOpen: (subscriptionId: string) => void }) {
255
+ const t = useT()
251
256
  if (items.length === 0) return <EmptyTab />
252
257
  return (
253
258
  <div className="rounded-lg border bg-card overflow-hidden">
254
259
  <table className="w-full text-sm">
255
260
  <thead>
256
261
  <tr className="border-b border-border bg-muted/40 text-muted-foreground">
257
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Name</th>
258
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Status</th>
259
- <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">MRC</th>
260
- <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">NRC</th>
261
- <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">Qty</th>
262
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.name', 'Name')}</th>
263
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.status', 'Status')}</th>
264
+ <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.mrc', 'MRC')}</th>
265
+ <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.nrc', 'NRC')}</th>
266
+ <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.qty', 'Qty')}</th>
262
267
  </tr>
263
268
  </thead>
264
269
  <tbody>
@@ -284,17 +289,18 @@ function SubscriptionItemsTable({ items, currency, onOpen }: { items: Subscripti
284
289
  // ─── Assets Table ────────────────────────────────────────────────
285
290
 
286
291
  function AssetsTable({ items, currency, onOpen }: { items: Asset[]; currency: string; onOpen: (id: string) => void }) {
292
+ const t = useT()
287
293
  if (items.length === 0) return <EmptyTab />
288
294
  return (
289
295
  <div className="rounded-lg border bg-card overflow-hidden">
290
296
  <table className="w-full text-sm">
291
297
  <thead>
292
298
  <tr className="border-b border-border bg-muted/40 text-muted-foreground">
293
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Name</th>
294
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Type</th>
295
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Status</th>
296
- <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">Price</th>
297
- <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">Linked To</th>
299
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.name', 'Name')}</th>
300
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.type', 'Type')}</th>
301
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.status', 'Status')}</th>
302
+ <th className="px-4 py-2.5 text-right text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.price', 'Price')}</th>
303
+ <th className="px-4 py-2.5 text-left text-xs font-medium uppercase tracking-wide">{t('cpq.widgets.customerInventory.table.linkedTo', 'Linked To')}</th>
298
304
  </tr>
299
305
  </thead>
300
306
  <tbody>
@@ -309,7 +315,7 @@ function AssetsTable({ items, currency, onOpen }: { items: Asset[]; currency: st
309
315
  <td className="px-4 py-3"><StatusBadge status={a.status} /></td>
310
316
  <td className="px-4 py-3 text-right font-mono">{fmt(a.purchasePrice, a.currencyCode || currency)}</td>
311
317
  <td className="px-4 py-3 text-muted-foreground">
312
- {a.subscriptionId ? `Sub ${a.subscriptionId.slice(0, 8)}…` : '—'}
318
+ {a.subscriptionId ? t('cpq.widgets.customerInventory.table.linkedSubscription', 'Sub {id}…', { id: a.subscriptionId.slice(0, 8) }) : '—'}
313
319
  </td>
314
320
  </tr>
315
321
  ))}
@@ -322,9 +328,10 @@ function AssetsTable({ items, currency, onOpen }: { items: Asset[]; currency: st
322
328
  // ─── Shared ──────────────────────────────────────────────────────
323
329
 
324
330
  function EmptyTab() {
331
+ const t = useT()
325
332
  return (
326
333
  <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
327
- No records in this category.
334
+ {t('cpq.widgets.customerInventory.emptyTab', 'No records in this category.')}
328
335
  </div>
329
336
  )
330
337
  }
@@ -3,6 +3,7 @@ import * as React from 'react'
3
3
  import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
4
4
  import { useRouter } from 'next/navigation'
5
5
  import { Tag } from '@open-mercato/ui/primitives/tag'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
7
  import {
7
8
  formatStatusLabel,
8
9
  orderCpqStatusMap,
@@ -28,6 +29,7 @@ function fmt(amount: number | undefined | null, currency: string): string {
28
29
 
29
30
  export default function OrderCpqTabWidget({ data }: InjectionWidgetComponentProps<any, any>) {
30
31
  const router = useRouter()
32
+ const t = useT()
31
33
  const salesOrderId = data?.id as string | undefined
32
34
  const [cpqOrders, setCpqOrders] = React.useState<CpqOrderSummary[]>([])
33
35
  const [loading, setLoading] = React.useState(true)
@@ -52,7 +54,7 @@ export default function OrderCpqTabWidget({ data }: InjectionWidgetComponentProp
52
54
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
53
55
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
54
56
  </svg>
55
- Loading CPQ orders…
57
+ {t('cpq.widgets.orderCpqTab.loading', 'Loading CPQ orders…')}
56
58
  </div>
57
59
  )
58
60
  }
@@ -60,7 +62,7 @@ export default function OrderCpqTabWidget({ data }: InjectionWidgetComponentProp
60
62
  if (cpqOrders.length === 0) {
61
63
  return (
62
64
  <div className="py-8 text-center text-sm text-muted-foreground">
63
- No CPQ orders linked to this sales order.
65
+ {t('cpq.widgets.orderCpqTab.empty', 'No CPQ orders linked to this sales order.')}
64
66
  </div>
65
67
  )
66
68
  }
@@ -82,12 +84,12 @@ export default function OrderCpqTabWidget({ data }: InjectionWidgetComponentProp
82
84
  <div className="flex items-center gap-4 text-xs">
83
85
  {o.pricingSummary && (
84
86
  <>
85
- <span><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(o.pricingSummary.nrcTotal, o.currencyCode)}</span></span>
86
- <span><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(o.pricingSummary.mrcTotal, o.currencyCode)}</span></span>
87
+ <span><span className="text-muted-foreground">{t('cpq.lines.nrc', 'NRC')} </span><span className="font-mono font-medium">{fmt(o.pricingSummary.nrcTotal, o.currencyCode)}</span></span>
88
+ <span><span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span><span className="font-mono font-medium">{fmt(o.pricingSummary.mrcTotal, o.currencyCode)}</span></span>
87
89
  </>
88
90
  )}
89
91
  <span className="text-muted-foreground">{new Date(o.createdAt).toLocaleDateString()}</span>
90
- <span className="text-primary font-medium">Open →</span>
92
+ <span className="text-primary font-medium">{t('cpq.widgets.orderCpqTab.open', 'Open →')}</span>
91
93
  </div>
92
94
  </div>
93
95
  ))}
@@ -3,6 +3,7 @@ import * as React from 'react'
3
3
  import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
4
4
  import { useRouter } from 'next/navigation'
5
5
  import { Tag } from '@open-mercato/ui/primitives/tag'
6
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
6
7
  import {
7
8
  formatStatusLabel,
8
9
  quoteCpqStatusMap,
@@ -58,10 +59,24 @@ function fmt(amount: number, currency: string): string {
58
59
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
59
60
  }
60
61
 
62
+ function chargeTypeLabel(t: TranslateFn, chargeType: ResolvedCharge['chargeType']): string {
63
+ switch (chargeType) {
64
+ case 'nrc':
65
+ return t('cpq.lines.chargeType.nrc', 'nrc')
66
+ case 'mrc':
67
+ return t('cpq.lines.chargeType.mrc', 'mrc')
68
+ case 'usage':
69
+ return t('cpq.lines.chargeType.usage', 'usage')
70
+ default:
71
+ return chargeType
72
+ }
73
+ }
74
+
61
75
  // ─── Main Widget (read-only) ─────────────────────────────────────
62
76
 
63
77
  export default function QuoteConfiguratorWidget({ data }: InjectionWidgetComponentProps) {
64
78
  const router = useRouter()
79
+ const t = useT()
65
80
  const record = data as Record<string, unknown> | null
66
81
  const quoteId = (record?.id as string) ?? null
67
82
 
@@ -83,12 +98,12 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
83
98
  return () => { cancelled = true }
84
99
  }, [quoteId])
85
100
 
86
- if (!quoteId) return <div className="p-4 text-sm text-muted-foreground">No quote context available.</div>
101
+ if (!quoteId) return <div className="p-4 text-sm text-muted-foreground">{t('cpq.widgets.quoteConfigurator.noContext', 'No quote context available.')}</div>
87
102
 
88
103
  if (loading) {
89
104
  return (
90
105
  <div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
91
- <Spinner /> Loading CPQ configuration…
106
+ <Spinner /> {t('cpq.widgets.quoteConfigurator.loading', 'Loading CPQ configuration…')}
92
107
  </div>
93
108
  )
94
109
  }
@@ -105,7 +120,7 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
105
120
  <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
106
121
  </svg>
107
122
  </div>
108
- <p className="text-sm text-muted-foreground mb-4">This quote has no CPQ configuration yet.</p>
123
+ <p className="text-sm text-muted-foreground mb-4">{t('cpq.widgets.quoteConfigurator.noConfig', 'This quote has no CPQ configuration yet.')}</p>
109
124
  <button
110
125
  onClick={() => router.push(`/backend/cpq/quotes/new?quoteId=${quoteId}&customerId=${custId}`)}
111
126
  className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
@@ -113,9 +128,9 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
113
128
  <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
114
129
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
115
130
  </svg>
116
- Create CPQ Quote
131
+ {t('cpq.widgets.quoteConfigurator.create', 'Create CPQ Quote')}
117
132
  </button>
118
- {!custId && <p className="mt-2 text-xs text-status-warning-text">Assign a customer to this quote first.</p>}
133
+ {!custId && <p className="mt-2 text-xs text-status-warning-text">{t('cpq.widgets.quoteConfigurator.assignCustomer', 'Assign a customer to this quote first.')}</p>}
119
134
  </div>
120
135
  )
121
136
  }
@@ -146,7 +161,7 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
146
161
  onClick={() => router.push(`/backend/cpq/quotes/${cpqQuote.id}`)}
147
162
  className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
148
163
  >
149
- Open CPQ Configurator
164
+ {t('cpq.widgets.quoteConfigurator.open', 'Open CPQ Configurator')}
150
165
  <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
151
166
  <path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
152
167
  </svg>
@@ -156,7 +171,7 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
156
171
  {/* Lines */}
157
172
  {cpqQuote.lines.length === 0 ? (
158
173
  <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
159
- No items configured yet.
174
+ {t('cpq.widgets.quoteConfigurator.noLines', 'No items configured yet.')}
160
175
  </div>
161
176
  ) : (
162
177
  <div className="rounded-lg border bg-card divide-y">
@@ -178,9 +193,9 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
178
193
  <span className="font-medium text-sm">{line.offeringName}</span>
179
194
  {line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
180
195
  {line.isConfigured ? (
181
- <Tag variant="success" className="px-1.5 text-xs">configured</Tag>
196
+ <Tag variant="success" className="px-1.5 text-xs">{t('cpq.quotes.detail.lineConfigured', 'configured')}</Tag>
182
197
  ) : (
183
- <Tag variant="warning" className="px-1.5 text-xs">incomplete</Tag>
198
+ <Tag variant="warning" className="px-1.5 text-xs">{t('cpq.quotes.detail.lineIncomplete', 'incomplete')}</Tag>
184
199
  )}
185
200
  </div>
186
201
  {line.validationErrors && line.validationErrors.length > 0 && (
@@ -188,8 +203,8 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
188
203
  )}
189
204
  </div>
190
205
  <div className="shrink-0 text-right text-xs space-y-0.5">
191
- {line.nrcTotal > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
192
- {line.mrcTotal > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
206
+ {line.nrcTotal > 0 && <div><span className="text-muted-foreground">{t('cpq.lines.nrc', 'NRC')} </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
207
+ {line.mrcTotal > 0 && <div><span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
193
208
  </div>
194
209
  </div>
195
210
 
@@ -199,18 +214,18 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
199
214
  <table className="w-full text-xs">
200
215
  <thead>
201
216
  <tr className="text-muted-foreground">
202
- <th className="text-left py-1 font-medium">Charge</th>
203
- <th className="text-left py-1 font-medium">Type</th>
204
- <th className="text-right py-1 font-medium">Unit Price</th>
205
- <th className="text-right py-1 font-medium">Qty</th>
206
- <th className="text-right py-1 font-medium">Total</th>
217
+ <th className="text-left py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.charge', 'Charge')}</th>
218
+ <th className="text-left py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.type', 'Type')}</th>
219
+ <th className="text-right py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.unitPrice', 'Unit Price')}</th>
220
+ <th className="text-right py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.qty', 'Qty')}</th>
221
+ <th className="text-right py-1 font-medium">{t('cpq.widgets.quoteConfigurator.charges.total', 'Total')}</th>
207
222
  </tr>
208
223
  </thead>
209
224
  <tbody>
210
225
  {line.charges.map((c, i) => (
211
226
  <tr key={i} className="border-t border-dashed border-border/50">
212
227
  <td className="py-1">{c.chargeName}</td>
213
- <td className="py-1 uppercase">{c.chargeType}</td>
228
+ <td className="py-1 uppercase">{chargeTypeLabel(t, c.chargeType)}</td>
214
229
  <td className="py-1 text-right font-mono">{fmt(c.unitPrice, currency)}</td>
215
230
  <td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
216
231
  <td className="py-1 text-right font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : c.note ?? '—'}</td>
@@ -231,19 +246,19 @@ export default function QuoteConfiguratorWidget({ data }: InjectionWidgetCompone
231
246
  <div className="rounded-lg border bg-card p-4">
232
247
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
233
248
  <div>
234
- <p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
249
+ <p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.nrc', 'Non-Recurring (NRC)')}</p>
235
250
  <p className="text-lg font-semibold font-mono">{fmt(summary.nrcTotal, currency)}</p>
236
251
  </div>
237
252
  <div>
238
- <p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
253
+ <p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.mrc', 'Monthly Recurring (MRC)')}</p>
239
254
  <p className="text-lg font-semibold font-mono">{fmt(summary.mrcTotal, currency)}</p>
240
255
  </div>
241
256
  {summary.usageCharges.length > 0 && (
242
257
  <div>
243
- <p className="text-xs text-muted-foreground">Usage Charges</p>
258
+ <p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.usageCharges', 'Usage Charges')}</p>
244
259
  <div className="mt-1 space-y-0.5">
245
260
  {summary.usageCharges.map((uc, i) => (
246
- <p key={i} className="text-xs">{uc.chargeName}: {fmt(uc.unitPrice, currency)}/unit</p>
261
+ <p key={i} className="text-xs">{uc.chargeName}: {t('cpq.lines.perUnit', '{price}/unit', { price: fmt(uc.unitPrice, currency) })}</p>
247
262
  ))}
248
263
  </div>
249
264
  </div>
@@ -1,11 +1,17 @@
1
1
  import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'
2
2
 
3
+ // Tab labels: the sales document page translates `groupLabel` as an i18n key
4
+ // (`t(groupLabel, metadata.title)`), so the sales tabs use CPQ keys. The
5
+ // customers detail pages (legacy v1) render `groupLabel` verbatim, so the
6
+ // customer-inventory tab keeps plain English until the host translates it
7
+ // (spec 2026-11-15-cpq-multi-language, Risks).
8
+
3
9
  export const injectionTable: ModuleInjectionTable = {
4
10
  'sales.document.detail.quote:tabs': [
5
11
  {
6
12
  widgetId: 'cpq.injection.quote-configurator',
7
13
  kind: 'tab',
8
- groupLabel: 'CPQ Configuration',
14
+ groupLabel: 'cpq.widgets.quoteConfigurator.tab',
9
15
  groupDescription: 'Configure, price, and manage quote offerings',
10
16
  priority: 70,
11
17
  },
@@ -14,7 +20,7 @@ export const injectionTable: ModuleInjectionTable = {
14
20
  {
15
21
  widgetId: 'cpq.injection.order-cpq-tab',
16
22
  kind: 'tab',
17
- groupLabel: 'CPQ Order',
23
+ groupLabel: 'cpq.widgets.orderCpqTab.tab',
18
24
  groupDescription: 'View the CPQ order associated with this sales order',
19
25
  priority: 60,
20
26
  },
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
4
5
  import { WizardEngineProvider, useWizardEngine } from './engine'
5
6
  import { getStepType } from './registry'
6
7
  import type { WizardDefinitionResult, WizardStepProps } from './types'
@@ -9,6 +10,7 @@ import type { TenantScope } from '../services/types'
9
10
  // ─── Step renderer ──────────────────────────────────────────────
10
11
 
11
12
  function StepRenderer({ scope }: { scope: TenantScope }) {
13
+ const t = useT()
12
14
  const { state, completeStep, goBack, skipStep } = useWizardEngine()
13
15
  const { currentStep, stepData, completedSteps, currentStepId, params, quoteId, customerId } = state
14
16
 
@@ -33,18 +35,18 @@ function StepRenderer({ scope }: { scope: TenantScope }) {
33
35
  return (
34
36
  <div className="rounded-lg border bg-card p-6 text-center space-y-3">
35
37
  <p className="text-sm text-destructive">
36
- Unknown step type: <strong>{currentStep.type}</strong>
38
+ {t('cpq.wizard.runner.unknownStepType', 'Unknown step type:')} <strong>{currentStep.type}</strong>
37
39
  </p>
38
- <p className="text-sm text-muted-foreground">This step type is not registered. Register it via <code className="font-mono text-xs bg-muted px-1 py-0.5 rounded">registerStepType()</code>.</p>
40
+ <p className="text-sm text-muted-foreground">{t('cpq.wizard.runner.notRegistered', 'This step type is not registered. Register it via')} <code className="font-mono text-xs bg-muted px-1 py-0.5 rounded">{'registerStepType()'}</code>.</p>
39
41
  <div className="flex gap-2 justify-center pt-2">
40
42
  {state.canGoBack && (
41
43
  <button type="button" onClick={goBack} className="inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted">
42
- Back
44
+ {t('cpq.common.back', 'Back')}
43
45
  </button>
44
46
  )}
45
47
  {state.canSkip && (
46
48
  <button type="button" onClick={skipStep} className="inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted">
47
- Skip
49
+ {t('cpq.wizard.skip', 'Skip')}
48
50
  </button>
49
51
  )}
50
52
  </div>
@@ -98,6 +100,7 @@ function WizardProgress() {
98
100
  // ─── Header ─────────────────────────────────────────────────────
99
101
 
100
102
  function WizardHeader() {
103
+ const t = useT()
101
104
  const { state } = useWizardEngine()
102
105
 
103
106
  return (
@@ -105,7 +108,7 @@ function WizardHeader() {
105
108
  <div>
106
109
  <h2 className="text-lg font-semibold">{state.definition.name}</h2>
107
110
  <p className="text-sm text-muted-foreground">
108
- Step: {state.currentStep.title}
111
+ {t('cpq.wizard.runner.currentStep', 'Step: {title}', { title: state.currentStep.title })}
109
112
  </p>
110
113
  </div>
111
114
  </div>
@@ -2,15 +2,16 @@
2
2
 
3
3
  import React, { useCallback, useState } from 'react'
4
4
  import { useRouter } from 'next/navigation'
5
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
5
6
  import type { WizardDefinitionResult } from './types'
6
7
 
7
8
  // ─── API helper ─────────────────────────────────────────────────
8
9
 
9
- async function apiJson<T>(url: string): Promise<T> {
10
+ async function apiJson<T>(t: TranslateFn, url: string): Promise<T> {
10
11
  const res = await fetch(url)
11
12
  if (!res.ok) {
12
13
  const body = await res.json().catch(() => ({}))
13
- throw new Error(body.error ?? `API error ${res.status}`)
14
+ throw new Error(body.error ?? t('cpq.wizard.trigger.apiError', 'API error {status}', { status: res.status }))
14
15
  }
15
16
  return res.json() as Promise<T>
16
17
  }
@@ -43,13 +44,16 @@ export function WizardTrigger({
43
44
  offeringCode,
44
45
  params: wizardParams = {},
45
46
  surface = 'backend',
46
- label = 'Start Wizard',
47
+ label,
47
48
  variant = 'button',
48
49
  className,
49
50
  basePath,
50
51
  onStart,
51
52
  }: WizardTriggerProps) {
52
53
  const router = useRouter()
54
+ const t = useT()
55
+ const buttonLabel = label ?? t('cpq.wizard.trigger.start', 'Start Wizard')
56
+ const startingLabel = t('cpq.wizard.trigger.starting', 'Starting...')
53
57
  const [loading, setLoading] = useState(false)
54
58
  const [error, setError] = useState<string | null>(null)
55
59
 
@@ -63,18 +67,20 @@ export function WizardTrigger({
63
67
 
64
68
  if (wizardCode) {
65
69
  definition = await apiJson<WizardDefinitionResult>(
70
+ t,
66
71
  `/api/cpq/wizards?code=${encodeURIComponent(wizardCode)}`,
67
72
  )
68
73
  } else {
69
74
  const resolveParams = new URLSearchParams({ surface })
70
75
  if (offeringCode) resolveParams.set('offeringCode', offeringCode)
71
76
  definition = await apiJson<WizardDefinitionResult>(
77
+ t,
72
78
  `/api/cpq/wizards/resolve?${resolveParams}`,
73
79
  )
74
80
  }
75
81
 
76
82
  if (!definition) {
77
- setError('No matching wizard found')
83
+ setError(t('cpq.wizard.trigger.notFound', 'No matching wizard found'))
78
84
  return
79
85
  }
80
86
 
@@ -90,11 +96,11 @@ export function WizardTrigger({
90
96
  router.push(`${path}/${definition.code}${qs ? `?${qs}` : ''}`)
91
97
  }
92
98
  } catch (err) {
93
- setError(err instanceof Error ? err.message : 'Failed to start wizard')
99
+ setError(err instanceof Error ? err.message : t('cpq.wizard.trigger.failed', 'Failed to start wizard'))
94
100
  } finally {
95
101
  setLoading(false)
96
102
  }
97
- }, [wizardCode, offeringCode, wizardParams, surface, basePath, onStart, router])
103
+ }, [wizardCode, offeringCode, wizardParams, surface, basePath, onStart, router, t])
98
104
 
99
105
  if (variant === 'link') {
100
106
  return (
@@ -106,7 +112,7 @@ export function WizardTrigger({
106
112
  handleClick()
107
113
  }}
108
114
  >
109
- {loading ? 'Starting...' : label}
115
+ {loading ? startingLabel : buttonLabel}
110
116
  {error && <span className="ml-2 text-xs text-destructive">{error}</span>}
111
117
  </a>
112
118
  )
@@ -121,7 +127,7 @@ export function WizardTrigger({
121
127
  tabIndex={0}
122
128
  onKeyDown={(e) => { if (e.key === 'Enter') handleClick() }}
123
129
  >
124
- <p className="font-semibold text-sm">{label}</p>
130
+ <p className="font-semibold text-sm">{buttonLabel}</p>
125
131
  {error && <p className="mt-1 text-xs text-destructive">{error}</p>}
126
132
  </div>
127
133
  )
@@ -135,7 +141,7 @@ export function WizardTrigger({
135
141
  onClick={handleClick}
136
142
  disabled={loading}
137
143
  >
138
- {loading ? 'Starting...' : label}
144
+ {loading ? startingLabel : buttonLabel}
139
145
  {error && <span className="ml-2 text-xs text-destructive/70">{error}</span>}
140
146
  </button>
141
147
  )