@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
@@ -57,6 +57,7 @@ type Offering = {
57
57
  const PAGE_SIZE = 50
58
58
 
59
59
  function ChargePopover({ charge }: { charge: Charge }) {
60
+ const t = useT()
60
61
  const [open, setOpen] = React.useState(false)
61
62
  const variant = chargeTypeMap[charge.chargeType as ChargeType] ?? 'neutral'
62
63
  return (
@@ -74,14 +75,14 @@ function ChargePopover({ charge }: { charge: Charge }) {
74
75
  <div className="text-muted-foreground font-mono">{charge.code}</div>
75
76
  {charge.description && <div className="text-muted-foreground">{charge.description}</div>}
76
77
  <div className="flex items-center justify-between pt-1 border-t">
77
- <span>Pricing: {charge.pricingMethod}</span>
78
+ <span>{t('cpq.offerings.list.chargePopover.pricing', 'Pricing: {method}', { method: charge.pricingMethod })}</span>
78
79
  {charge.fixedPrice != null && (
79
80
  <span className="font-medium">
80
81
  {charge.currencyCode ?? 'USD'} {charge.fixedPrice}
81
82
  </span>
82
83
  )}
83
84
  </div>
84
- {!charge.isActive && <Tag variant="warning" dot>Inactive</Tag>}
85
+ {!charge.isActive && <Tag variant="warning" dot>{t('cpq.common.inactive', 'Inactive')}</Tag>}
85
86
  <div className="absolute left-1/2 -translate-x-1/2 top-full w-2 h-2 bg-popover border-b border-r rotate-45 -mt-1" />
86
87
  </div>
87
88
  )}
@@ -207,7 +208,7 @@ export default function OfferingsListPage() {
207
208
  })
208
209
  if (!res.ok) {
209
210
  const body = await res.json().catch(() => ({} as { error?: string }))
210
- setError(body.error ?? `Failed to save charge for ${offering.name}`)
211
+ setError(body.error ?? t('cpq.offerings.bulk.error.saveChargeFor', 'Failed to save charge for {name}', { name: offering.name }))
211
212
  setSaving(false)
212
213
  return
213
214
  }
@@ -217,7 +218,7 @@ export default function OfferingsListPage() {
217
218
  setBulkChargeTargets([])
218
219
  data.reload()
219
220
  } catch {
220
- setError('Failed to save charges')
221
+ setError(t('cpq.offerings.bulk.error.saveCharges', 'Failed to save charges'))
221
222
  } finally {
222
223
  setSaving(false)
223
224
  }
@@ -245,7 +246,7 @@ export default function OfferingsListPage() {
245
246
  {row.original.name}
246
247
  {row.original.offeringType === 'bundle' && (
247
248
  <Tag variant="brand" className="ml-2 px-2 text-[10px]">
248
- bundle
249
+ {t('cpq.offerings.list.tag.bundle', 'bundle')}
249
250
  </Tag>
250
251
  )}
251
252
  </span>
@@ -319,30 +320,37 @@ export default function OfferingsListPage() {
319
320
  </h4>
320
321
 
321
322
  <Alert>
322
- Bulk creation supports <strong>flat pricing only</strong>. For <em>per unit</em> or <em>tiered</em> pricing,
323
- open each offering individually — those methods need a pricing table and a numeric attribute that lives on
324
- the offering&apos;s spec, which usually differs between offerings.
323
+ {t('cpq.offerings.bulk.notice.supports', 'Bulk creation supports')}{' '}
324
+ <strong>{t('cpq.offerings.bulk.notice.flatOnly', 'flat pricing only')}</strong>
325
+ {t('cpq.offerings.bulk.notice.for', '. For')}{' '}
326
+ <em>{t('cpq.offerings.bulk.notice.perUnit', 'per unit')}</em>{' '}
327
+ {t('cpq.offerings.bulk.notice.or', 'or')}{' '}
328
+ <em>{t('cpq.offerings.bulk.notice.tiered', 'tiered')}</em>{' '}
329
+ {t(
330
+ 'cpq.offerings.bulk.notice.rest',
331
+ "pricing, open each offering individually — those methods need a pricing table and a numeric attribute that lives on the offering's spec, which usually differs between offerings.",
332
+ )}
325
333
  </Alert>
326
334
 
327
335
  <div className="grid grid-cols-3 gap-3">
328
336
  <div>
329
- <label className="block text-xs font-medium mb-1">Code</label>
337
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.bulk.fields.code', 'Code')}</label>
330
338
  <Input
331
339
  value={chargeForm.code}
332
340
  onChange={(e) => setChargeForm({ ...chargeForm, code: e.target.value })}
333
- placeholder="e.g. mrc-monthly"
341
+ placeholder={t('cpq.offerings.bulk.fields.codePlaceholder', 'e.g. mrc-monthly')}
334
342
  />
335
343
  </div>
336
344
  <div>
337
- <label className="block text-xs font-medium mb-1">Name</label>
345
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.bulk.fields.name', 'Name')}</label>
338
346
  <Input
339
347
  value={chargeForm.name}
340
348
  onChange={(e) => setChargeForm({ ...chargeForm, name: e.target.value })}
341
- placeholder="e.g. Monthly Recurring"
349
+ placeholder={t('cpq.offerings.bulk.fields.namePlaceholder', 'e.g. Monthly Recurring')}
342
350
  />
343
351
  </div>
344
352
  <div>
345
- <label className="block text-xs font-medium mb-1">Type</label>
353
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.bulk.fields.type', 'Type')}</label>
346
354
  <Select
347
355
  value={chargeForm.chargeType}
348
356
  onValueChange={(value) => setChargeForm({ ...chargeForm, chargeType: value })}
@@ -351,26 +359,26 @@ export default function OfferingsListPage() {
351
359
  <SelectValue />
352
360
  </SelectTrigger>
353
361
  <SelectContent>
354
- <SelectItem value="nrc">NRC (Non-Recurring)</SelectItem>
355
- <SelectItem value="mrc">MRC (Monthly Recurring)</SelectItem>
356
- <SelectItem value="usage">Usage</SelectItem>
362
+ <SelectItem value="nrc">{t('cpq.offerings.chargeType.nrc', 'NRC (Non-Recurring)')}</SelectItem>
363
+ <SelectItem value="mrc">{t('cpq.offerings.chargeType.mrc', 'MRC (Monthly Recurring)')}</SelectItem>
364
+ <SelectItem value="usage">{t('cpq.offerings.chargeType.usage', 'Usage')}</SelectItem>
357
365
  </SelectContent>
358
366
  </Select>
359
367
  </div>
360
368
  </div>
361
369
 
362
370
  <div>
363
- <label className="block text-xs font-medium mb-1">Description</label>
371
+ <label className="block text-xs font-medium mb-1">{t('cpq.offerings.bulk.fields.description', 'Description')}</label>
364
372
  <Input
365
373
  value={chargeForm.description}
366
374
  onChange={(e) => setChargeForm({ ...chargeForm, description: e.target.value })}
367
- placeholder="User-facing charge description"
375
+ placeholder={t('cpq.offerings.bulk.fields.descriptionPlaceholder', 'User-facing charge description')}
368
376
  />
369
377
  </div>
370
378
 
371
379
  <div className="space-y-2">
372
380
  <div className="flex items-center gap-3">
373
- <label className="text-xs font-medium">Currency</label>
381
+ <label className="text-xs font-medium">{t('cpq.offerings.bulk.fields.currency', 'Currency')}</label>
374
382
  <Input
375
383
  value={chargeCurrency}
376
384
  onChange={(e) => setChargeCurrency(e.target.value.toUpperCase())}
@@ -382,8 +390,8 @@ export default function OfferingsListPage() {
382
390
  <table className="w-full text-sm">
383
391
  <thead>
384
392
  <tr className="border-b bg-muted/30">
385
- <th className="px-3 py-2 text-left font-medium text-xs">Offering</th>
386
- <th className="px-3 py-2 text-left font-medium text-xs">Price ({chargeCurrency})</th>
393
+ <th className="px-3 py-2 text-left font-medium text-xs">{t('cpq.offerings.bulk.table.offering', 'Offering')}</th>
394
+ <th className="px-3 py-2 text-left font-medium text-xs">{t('cpq.offerings.bulk.table.price', 'Price ({currency})', { currency: chargeCurrency })}</th>
387
395
  </tr>
388
396
  </thead>
389
397
  <tbody>
@@ -416,11 +424,13 @@ export default function OfferingsListPage() {
416
424
  disabled={saving || !chargeForm.code || !chargeForm.name}
417
425
  >
418
426
  {saving
419
- ? 'Saving...'
420
- : `Create Charge for ${bulkChargeTargets.length} Offering${bulkChargeTargets.length > 1 ? 's' : ''}`}
427
+ ? t('cpq.common.saving', 'Saving...')
428
+ : bulkChargeTargets.length > 1
429
+ ? t('cpq.offerings.bulk.submitPlural', 'Create Charge for {count} Offerings', { count: bulkChargeTargets.length })
430
+ : t('cpq.offerings.bulk.submitSingular', 'Create Charge for {count} Offering', { count: bulkChargeTargets.length })}
421
431
  </Button>
422
432
  <Button type="button" variant="outline" onClick={() => setBulkChargeOpen(false)}>
423
- Cancel
433
+ {t('cpq.common.cancel', 'Cancel')}
424
434
  </Button>
425
435
  </div>
426
436
  </div>
@@ -17,7 +17,7 @@ import { PricingSummaryCard, type PricingSummaryView } from '../../../../compone
17
17
  import { ArcLineDiff, LineActionBadge } from '../../../../components/ArcLineDiff'
18
18
  import { arcRecurringDelta } from '../../../../lib/quoteSummaryView'
19
19
  import { isArcCancelledLine } from '../../../../services/arcLineRemoval'
20
- import { useT } from '@open-mercato/shared/lib/i18n/context'
20
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
21
21
 
22
22
  // ─── Types ───────────────────────────────────────────────────────
23
23
 
@@ -95,6 +95,19 @@ const ARC_BADGE_VARIANTS: Record<string, 'brand' | 'success' | 'error'> = {
95
95
  cancel: 'error',
96
96
  }
97
97
 
98
+ function quoteTypeLabel(t: TranslateFn, quoteType: string): string {
99
+ switch (quoteType) {
100
+ case 'amend':
101
+ return t('cpq.quotes.type.amend', 'amend')
102
+ case 'renew':
103
+ return t('cpq.quotes.type.renew', 'renew')
104
+ case 'cancel':
105
+ return t('cpq.quotes.type.cancel', 'cancel')
106
+ default:
107
+ return quoteType
108
+ }
109
+ }
110
+
98
111
 
99
112
  // ─── Constants ───────────────────────────────────────────────────
100
113
 
@@ -102,10 +115,10 @@ function fmt(amount: number, currency: string): string {
102
115
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
103
116
  }
104
117
 
105
- async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
118
+ async function apiJson<T>(t: TranslateFn, url: string, init?: RequestInit): Promise<T> {
106
119
  const res = await fetch(url, init)
107
120
  const json = await res.json()
108
- if (!res.ok) throw new Error(json?.error ?? `Request failed (${res.status})`)
121
+ if (!res.ok) throw new Error(json?.error ?? t('cpq.common.requestFailed', 'Request failed ({status})', { status: res.status }))
109
122
  return json as T
110
123
  }
111
124
 
@@ -113,6 +126,7 @@ async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
113
126
 
114
127
  export default function CpqOrderDetailPage(props: { params?: { id?: string } }) {
115
128
  const router = useRouter()
129
+ const t = useT()
116
130
  const urlParams = useParams()
117
131
  const cpqOrderId = (props.params?.id ?? urlParams?.id) as string
118
132
 
@@ -151,8 +165,8 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
151
165
  .then(async (d) => {
152
166
  const targets: Array<{ subscriptionId: string }> = d?.items ?? []
153
167
  const fetched = await Promise.all(
154
- targets.map((t) =>
155
- fetch(`/api/cpq/inventory/subscriptions?id=${t.subscriptionId}`)
168
+ targets.map((target) =>
169
+ fetch(`/api/cpq/inventory/subscriptions?id=${target.subscriptionId}`)
156
170
  .then((r) => (r.ok ? r.json() : null))
157
171
  .catch(() => null),
158
172
  ),
@@ -181,7 +195,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
181
195
  let cancelled = false
182
196
  async function load() {
183
197
  try {
184
- const result = await apiJson<OrderResult>(`/api/cpq/orders/${cpqOrderId}`)
198
+ const result = await apiJson<OrderResult>(t, `/api/cpq/orders/${cpqOrderId}`)
185
199
  if (!cancelled) {
186
200
  setOrder(result)
187
201
  fetch(`/api/customers/companies?id=${result.customerId}&pageSize=1`)
@@ -204,14 +218,14 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
204
218
  }
205
219
  load()
206
220
  return () => { cancelled = true }
207
- }, [cpqOrderId, fetchLinkedSubscriptions])
221
+ }, [cpqOrderId, fetchLinkedSubscriptions, t])
208
222
 
209
223
  const activateOrder = async () => {
210
224
  if (!order) return
211
225
  try {
212
226
  setActivating(true)
213
227
  setError(null)
214
- const result = await apiJson<OrderResult>(`/api/cpq/orders/${order.id}/activate`, {
228
+ const result = await apiJson<OrderResult>(t, `/api/cpq/orders/${order.id}/activate`, {
215
229
  method: 'POST',
216
230
  headers: { 'Content-Type': 'application/json' },
217
231
  body: JSON.stringify({}),
@@ -229,7 +243,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
229
243
  if (!order) return
230
244
  try {
231
245
  setError(null)
232
- const result = await apiJson<OrderResult>(`/api/cpq/orders/${order.id}/status`, {
246
+ const result = await apiJson<OrderResult>(t, `/api/cpq/orders/${order.id}/status`, {
233
247
  method: 'POST',
234
248
  headers: { 'Content-Type': 'application/json' },
235
249
  body: JSON.stringify({ targetStatus: 'cancelled' }),
@@ -249,7 +263,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
249
263
  if (loading) {
250
264
  return (
251
265
  <div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
252
- <Spinner /> Loading…
266
+ <Spinner /> {t('cpq.orders.detail.loading', 'Loading…')}
253
267
  </div>
254
268
  )
255
269
  }
@@ -257,9 +271,9 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
257
271
  if (!order) {
258
272
  return (
259
273
  <div className="space-y-4">
260
- <button onClick={() => router.push('/backend/cpq/orders')} className="text-sm text-muted-foreground hover:text-foreground">← Back to Orders</button>
274
+ <button onClick={() => router.push('/backend/cpq/orders')} className="text-sm text-muted-foreground hover:text-foreground">{t('cpq.orders.detail.backToOrders', '← Back to Orders')}</button>
261
275
  {error && <ErrorBanner message={error} />}
262
- <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Order not found.</div>
276
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">{t('cpq.orders.detail.notFound', 'Order not found.')}</div>
263
277
  </div>
264
278
  )
265
279
  }
@@ -286,8 +300,8 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
286
300
  <FormHeader
287
301
  mode="detail"
288
302
  backHref="/backend/cpq/orders"
289
- backLabel="Back to CPQ Orders"
290
- entityTypeLabel="CPQ Order"
303
+ backLabel={t('cpq.orders.detail.backLabel', 'Back to CPQ Orders')}
304
+ entityTypeLabel={t('cpq.orders.detail.entityType', 'CPQ Order')}
291
305
  title={order.orderNumber || order.orderId.slice(0, 8)}
292
306
  statusBadge={
293
307
  <div className="flex flex-wrap items-center gap-2 pt-1">
@@ -296,7 +310,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
296
310
  </Tag>
297
311
  {order.quoteType && order.quoteType !== 'new' && (
298
312
  <Tag variant={ARC_BADGE_VARIANTS[order.quoteType] ?? 'neutral'} className="uppercase">
299
- {order.quoteType}
313
+ {quoteTypeLabel(t, order.quoteType)}
300
314
  </Tag>
301
315
  )}
302
316
  </div>
@@ -309,7 +323,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
309
323
  onClick={() => router.push(`/backend/sales/orders/${order.orderId}`)}
310
324
  >
311
325
  <ExternalLink className="h-3.5 w-3.5" />
312
- View Sales Order
326
+ {t('cpq.orders.detail.viewSalesOrder', 'View Sales Order')}
313
327
  </Button>
314
328
  {order.sourceQuoteId && (
315
329
  <Button
@@ -317,19 +331,19 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
317
331
  variant="outline"
318
332
  onClick={() => router.push(`/backend/cpq/quotes/${order.sourceQuoteId}`)}
319
333
  >
320
- Source Quote
334
+ {t('cpq.orders.detail.sourceQuote', 'Source Quote')}
321
335
  </Button>
322
336
  )}
323
337
  {canActivate && (
324
338
  <Button type="button" onClick={activateOrder} disabled={activating}>
325
339
  {activating ? <Spinner /> : <Play className="h-4 w-4" />}
326
- Activate
340
+ {t('cpq.orders.detail.activate', 'Activate')}
327
341
  </Button>
328
342
  )}
329
343
  {canCancel && (
330
344
  <Button type="button" variant="destructive-outline" onClick={cancelOrder}>
331
345
  <X className="h-4 w-4" />
332
- Cancel
346
+ {t('cpq.common.cancel', 'Cancel')}
333
347
  </Button>
334
348
  )}
335
349
  </>
@@ -348,7 +362,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
348
362
  path={['draft', 'pending_activation', 'active', 'fulfilled']}
349
363
  terminals={['cancelled']}
350
364
  statusMap={orderCpqStatusMap}
351
- ariaLabel="Order status path"
365
+ ariaLabel={t('cpq.orders.detail.statusPath', 'Order status path')}
352
366
  />
353
367
  </div>
354
368
 
@@ -368,7 +382,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
368
382
  <div className="flex-1 min-w-0 space-y-1">
369
383
  <div className="flex items-center gap-2 flex-wrap">
370
384
  <span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
371
- Linked Subscription
385
+ {t('cpq.orders.detail.linkedSubscription', 'Linked Subscription')}
372
386
  </span>
373
387
  <Tag
374
388
  variant={sub.status === 'active' ? 'success' : 'info'}
@@ -384,7 +398,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
384
398
  </div>
385
399
  {order.activatedAt && (
386
400
  <div className="text-xs text-muted-foreground">
387
- Activated on {new Date(order.activatedAt).toLocaleString()}
401
+ {t('cpq.orders.detail.activatedOn', 'Activated on {date}', { date: new Date(order.activatedAt).toLocaleString() })}
388
402
  </div>
389
403
  )}
390
404
  </div>
@@ -394,7 +408,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
394
408
  onClick={() => router.push(`/backend/cpq/inventory/subscriptions/${sub.id}`)}
395
409
  >
396
410
  <ArrowRight className="h-3.5 w-3.5" />
397
- Open
411
+ {t('cpq.orders.actions.open', 'Open')}
398
412
  </Button>
399
413
  </div>
400
414
  ))}
@@ -403,7 +417,7 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
403
417
 
404
418
  {/* Meta cards — secondary reference info, matches the quote layout. */}
405
419
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
406
- <MetaCard label="Customer">
420
+ <MetaCard label={t('cpq.orders.detail.meta.customer', 'Customer')}>
407
421
  {customerName ? (
408
422
  <span className="font-medium">{customerName}</span>
409
423
  ) : (
@@ -412,10 +426,10 @@ export default function CpqOrderDetailPage(props: { params?: { id?: string } })
412
426
  </span>
413
427
  )}
414
428
  </MetaCard>
415
- <MetaCard label="Created">
429
+ <MetaCard label={t('cpq.orders.detail.meta.created', 'Created')}>
416
430
  <span className="font-medium">{new Date(order.createdAt).toLocaleDateString()}</span>
417
431
  </MetaCard>
418
- <MetaCard label="Currency">
432
+ <MetaCard label={t('cpq.orders.detail.meta.currency', 'Currency')}>
419
433
  <span className="font-medium">{currency}</span>
420
434
  </MetaCard>
421
435
  </div>
@@ -472,6 +486,7 @@ function OrderLineTree({
472
486
  onToggleLine: (id: string) => void
473
487
  onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
474
488
  }) {
489
+ const t = useT()
475
490
  const rootLines = lines.filter((l) => !l.parentLineId)
476
491
  const childrenByParent = new Map<string, OrderLineResult[]>()
477
492
  for (const line of lines) {
@@ -489,10 +504,10 @@ function OrderLineTree({
489
504
 
490
505
  return (
491
506
  <div>
492
- <h3 className="text-sm font-medium mb-2 px-1">Line Items ({lines.length})</h3>
507
+ <h3 className="text-sm font-medium mb-2 px-1">{t('cpq.orders.detail.lineItems', 'Line Items ({count})', { count: lines.length })}</h3>
493
508
  {lines.length === 0 ? (
494
509
  <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
495
- No line items.
510
+ {t('cpq.orders.detail.noLineItems', 'No line items.')}
496
511
  </div>
497
512
  ) : (
498
513
  <div className="rounded-lg border bg-card divide-y">
@@ -519,7 +534,7 @@ function OrderLineTree({
519
534
  {isBundle && isExpanded && children.length > 0 && (
520
535
  <div className="border-t border-dashed border-brand-violet/30 bg-brand-violet/5">
521
536
  <div className="px-6 py-1.5">
522
- <span className="text-[10px] font-medium text-brand-violet uppercase tracking-wider">Bundle Components ({children.length})</span>
537
+ <span className="text-[10px] font-medium text-brand-violet uppercase tracking-wider">{t('cpq.lines.bundleComponents', 'Bundle Components ({count})', { count: children.length })}</span>
523
538
  </div>
524
539
  {children.map((child) => {
525
540
  const childIdx = indexOf.get(child.lineId) ?? 0
@@ -582,7 +597,7 @@ function OrderLineRow({ line, lineIdx, quoteType, currency, isExpanded, isBundle
582
597
  <div className="flex items-center gap-2">
583
598
  {indent > 0 && <span className="text-muted-foreground text-xs">└</span>}
584
599
  {isBundle && (
585
- <Tag variant="brand" className="px-1.5 text-xs">bundle</Tag>
600
+ <Tag variant="brand" className="px-1.5 text-xs">{t('cpq.lines.bundle', 'bundle')}</Tag>
586
601
  )}
587
602
  <span className={`font-medium text-sm ${isRemoved ? 'text-muted-foreground line-through' : ''}`}>{line.offeringName}</span>
588
603
  {line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
@@ -591,19 +606,25 @@ function OrderLineRow({ line, lineIdx, quoteType, currency, isExpanded, isBundle
591
606
  label={isRemoved ? t('cpq.quotes.arc.lineRemoved', 'remove') : undefined}
592
607
  title={isRemoved ? t('cpq.quotes.arc.lineRemovedHint', 'This item is cancelled on the subscription when the order is activated') : undefined}
593
608
  />
594
- {isBundle && <span className="text-xs text-muted-foreground">{childCount} component{childCount !== 1 ? 's' : ''}</span>}
609
+ {isBundle && (
610
+ <span className="text-xs text-muted-foreground">
611
+ {childCount !== 1
612
+ ? t('cpq.lines.componentCount.other', '{count} components', { count: childCount })
613
+ : t('cpq.lines.componentCount.one', '{count} component', { count: childCount })}
614
+ </span>
615
+ )}
595
616
  </div>
596
617
  </div>
597
618
 
598
619
  <div className="shrink-0 text-right text-xs space-y-0.5">
599
620
  {isRemoved && line.arcSource && line.arcSource.mrcAmount > 0 && (
600
621
  <div>
601
- <span className="text-muted-foreground">MRC </span>
622
+ <span className="text-muted-foreground">{t('cpq.lines.mrc', 'MRC')} </span>
602
623
  <span className="font-mono text-muted-foreground line-through">{fmt(line.arcSource.mrcAmount, currency)}</span>
603
624
  </div>
604
625
  )}
605
- {line.nrcTotal > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
606
- {line.mrcTotal > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
626
+ {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>}
627
+ {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>}
607
628
  </div>
608
629
  </div>
609
630
 
@@ -627,16 +648,17 @@ function OrderLineRow({ line, lineIdx, quoteType, currency, isExpanded, isBundle
627
648
  {isEmpty ? (
628
649
  <div className="text-xs text-muted-foreground space-y-1">
629
650
  <div>
630
- No detailed breakdown for this line.
631
- {mirroredFromItemId && ' Mirrored from existing subscription item.'}
651
+ {t('cpq.orders.detail.noBreakdown', 'No detailed breakdown for this line.')}
652
+ {mirroredFromItemId && ' '}
653
+ {mirroredFromItemId && t('cpq.orders.detail.mirroredItem', 'Mirrored from existing subscription item.')}
632
654
  </div>
633
655
  {(line.nrcTotal > 0 || line.mrcTotal > 0) && (
634
656
  <div className="flex gap-4">
635
657
  {line.nrcTotal > 0 && (
636
- <span>NRC <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
658
+ <span>{t('cpq.lines.nrc', 'NRC')} <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
637
659
  )}
638
660
  {line.mrcTotal > 0 && (
639
- <span>MRC <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
661
+ <span>{t('cpq.lines.mrc', 'MRC')} <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
640
662
  )}
641
663
  </div>
642
664
  )}
@@ -645,7 +667,7 @@ function OrderLineRow({ line, lineIdx, quoteType, currency, isExpanded, isBundle
645
667
  <>
646
668
  {hasConfig && (
647
669
  <div>
648
- <p className="text-xs font-medium text-muted-foreground mb-1">Configuration</p>
670
+ <p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.lines.configuration', 'Configuration')}</p>
649
671
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
650
672
  {visibleConfigEntries.map(([key, value]) => (
651
673
  <div key={key} className="text-xs">
@@ -668,9 +690,9 @@ function OrderLineRow({ line, lineIdx, quoteType, currency, isExpanded, isBundle
668
690
 
669
691
  {hasTerm && (
670
692
  <div className="flex gap-4 text-xs text-muted-foreground">
671
- {line.startDate && <span>Start: {new Date(line.startDate).toLocaleDateString()}</span>}
672
- {line.termMonths && <span>Term: {line.termMonths}mo</span>}
673
- {line.endDate && <span>End: {new Date(line.endDate).toLocaleDateString()}</span>}
693
+ {line.startDate && <span>{t('cpq.lines.term.start', 'Start: {date}', { date: new Date(line.startDate).toLocaleDateString() })}</span>}
694
+ {line.termMonths && <span>{t('cpq.lines.term.months', 'Term: {months}mo', { months: line.termMonths })}</span>}
695
+ {line.endDate && <span>{t('cpq.lines.term.end', 'End: {date}', { date: new Date(line.endDate).toLocaleDateString() })}</span>}
674
696
  </div>
675
697
  )}
676
698
 
@@ -695,6 +717,7 @@ function Spinner() {
695
717
  }
696
718
 
697
719
  function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () => void }) {
720
+ const t = useT()
698
721
  return (
699
722
  <Alert variant="destructive" className="flex items-start gap-2">
700
723
  <span className="flex-1">{message}</span>
@@ -702,7 +725,7 @@ function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () =
702
725
  <button
703
726
  onClick={onDismiss}
704
727
  className="text-destructive/70 hover:text-destructive text-sm font-bold"
705
- aria-label="Dismiss"
728
+ aria-label={t('cpq.common.dismiss', 'Dismiss')}
706
729
  >
707
730
  ×
708
731
  </button>
@@ -57,13 +57,13 @@ export default function CpqOrdersListPage() {
57
57
 
58
58
  const statusOptions = React.useMemo(
59
59
  () => [
60
- { value: 'draft', label: 'Draft' },
61
- { value: 'pending_activation', label: 'Pending Activation' },
62
- { value: 'active', label: 'Active' },
63
- { value: 'fulfilled', label: 'Fulfilled' },
64
- { value: 'cancelled', label: 'Cancelled' },
60
+ { value: 'draft', label: t('cpq.orders.status.draft', 'Draft') },
61
+ { value: 'pending_activation', label: t('cpq.orders.status.pending_activation', 'Pending Activation') },
62
+ { value: 'active', label: t('cpq.orders.status.active', 'Active') },
63
+ { value: 'fulfilled', label: t('cpq.orders.status.fulfilled', 'Fulfilled') },
64
+ { value: 'cancelled', label: t('cpq.orders.status.cancelled', 'Cancelled') },
65
65
  ],
66
- [],
66
+ [t],
67
67
  )
68
68
 
69
69
  const filters = React.useMemo<FilterDef[]>(