@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
@@ -114,12 +114,12 @@ export default function SpecificationsListPage() {
114
114
  {row.original.name}
115
115
  {row.original.specType === 'bundle' && (
116
116
  <Tag variant="brand" className="ml-2 px-2 text-[10px]">
117
- bundle
117
+ {t('cpq.specifications.list.tag.bundle', 'bundle')}
118
118
  </Tag>
119
119
  )}
120
120
  {row.original.isAssetizable && (
121
121
  <Tag variant="warning" className="ml-1 px-2 text-[10px]">
122
- asset
122
+ {t('cpq.specifications.list.tag.asset', 'asset')}
123
123
  </Tag>
124
124
  )}
125
125
  </span>
@@ -2,6 +2,7 @@
2
2
  import * as React from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
4
  import { Alert } from '@open-mercato/ui/primitives/alert'
5
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
5
6
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
6
7
  import { Tag } from '@open-mercato/ui/primitives/tag'
7
8
  import type {
@@ -11,9 +12,9 @@ import type {
11
12
  WizardApplicability,
12
13
  } from '../../../../../workflows/types'
13
14
 
14
- async function apiJson<T>(url: string): Promise<T> {
15
+ async function apiJson<T>(url: string, t: TranslateFn): Promise<T> {
15
16
  const res = await fetch(url)
16
- if (!res.ok) throw new Error(`API error ${res.status}`)
17
+ if (!res.ok) throw new Error(t('cpq.wizards.runner.errors.api', 'API error {status}', { status: res.status }))
17
18
  return res.json() as Promise<T>
18
19
  }
19
20
 
@@ -43,7 +44,8 @@ function KV({ label, children }: { label: string; children: React.ReactNode }) {
43
44
  }
44
45
 
45
46
  function JsonBlock({ data }: { data: unknown }) {
46
- if (data == null) return <span className="text-xs text-muted-foreground">none</span>
47
+ const t = useT()
48
+ if (data == null) return <span className="text-xs text-muted-foreground">{t('cpq.wizards.detail.none', 'none')}</span>
47
49
  return (
48
50
  <pre className="rounded-md bg-muted/50 p-3 text-xs overflow-x-auto max-h-48">
49
51
  {JSON.stringify(data, null, 2)}
@@ -51,20 +53,24 @@ function JsonBlock({ data }: { data: unknown }) {
51
53
  )
52
54
  }
53
55
 
54
- function TransitionRow({ t, index }: { t: WizardTransition; index: number }) {
56
+ function TransitionRow({ transition, index }: { transition: WizardTransition; index: number }) {
57
+ const t = useT()
55
58
  return (
56
59
  <div className="flex items-start gap-3 text-xs">
57
60
  <span className="font-mono text-muted-foreground w-4 shrink-0">{index + 1}.</span>
58
61
  <div className="space-y-0.5">
59
62
  <div className="flex items-center gap-2">
60
- <span className="font-medium">-&gt; {t.targetStepId}</span>
61
- {t.isDefault && <Badge variant="muted">default</Badge>}
62
- {t.label && <span className="text-muted-foreground">({t.label})</span>}
63
+ <span className="font-medium">{'->'} {transition.targetStepId}</span>
64
+ {transition.isDefault && <Badge variant="muted">{t('cpq.wizards.detail.transitionDefault', 'default')}</Badge>}
65
+ {transition.label && <span className="text-muted-foreground">({transition.label})</span>}
63
66
  </div>
64
- {t.condition && (
67
+ {transition.condition && (
65
68
  <div className="font-mono text-muted-foreground">
66
- when {t.condition.field} {t.condition.operator}
67
- {t.condition.value !== undefined ? ` ${JSON.stringify(t.condition.value)}` : ''}
69
+ {t('cpq.wizards.detail.transitionWhen', 'when {field} {operator}', {
70
+ field: transition.condition.field,
71
+ operator: transition.condition.operator,
72
+ })}
73
+ {transition.condition.value !== undefined ? ` ${JSON.stringify(transition.condition.value)}` : ''}
68
74
  </div>
69
75
  )}
70
76
  </div>
@@ -73,6 +79,7 @@ function TransitionRow({ t, index }: { t: WizardTransition; index: number }) {
73
79
  }
74
80
 
75
81
  function StepCard({ step, index }: { step: WizardStepDefinition; index: number }) {
82
+ const t = useT()
76
83
  const [open, setOpen] = React.useState(false)
77
84
  const configKeys = Object.keys(step.config)
78
85
 
@@ -90,7 +97,7 @@ function StepCard({ step, index }: { step: WizardStepDefinition; index: number }
90
97
  <div className="flex items-center gap-2">
91
98
  <span className="text-sm font-medium truncate">{step.title}</span>
92
99
  <Badge>{step.type}</Badge>
93
- {step.isOptional && <Badge variant="muted">optional</Badge>}
100
+ {step.isOptional && <Badge variant="muted">{t('cpq.wizards.detail.optional', 'optional')}</Badge>}
94
101
  </div>
95
102
  {step.description && (
96
103
  <p className="text-xs text-muted-foreground truncate mt-0.5">{step.description}</p>
@@ -102,13 +109,13 @@ function StepCard({ step, index }: { step: WizardStepDefinition; index: number }
102
109
  {open && (
103
110
  <div className="border-t px-4 py-3 space-y-4">
104
111
  <div className="grid grid-cols-2 gap-4">
105
- <KV label="Step ID"><span className="font-mono text-xs">{step.stepId}</span></KV>
106
- <KV label="Type"><span className="font-mono text-xs">{step.type}</span></KV>
112
+ <KV label={t('cpq.wizards.detail.stepId', 'Step ID')}><span className="font-mono text-xs">{step.stepId}</span></KV>
113
+ <KV label={t('cpq.wizards.detail.type', 'Type')}><span className="font-mono text-xs">{step.type}</span></KV>
107
114
  </div>
108
115
 
109
116
  {configKeys.length > 0 && (
110
117
  <div className="space-y-1">
111
- <span className="text-xs font-medium text-muted-foreground">Config</span>
118
+ <span className="text-xs font-medium text-muted-foreground">{t('cpq.wizards.detail.config', 'Config')}</span>
112
119
  <JsonBlock data={step.config} />
113
120
  </div>
114
121
  )}
@@ -116,23 +123,23 @@ function StepCard({ step, index }: { step: WizardStepDefinition; index: number }
116
123
  {step.transitions.length > 0 && (
117
124
  <div className="space-y-2">
118
125
  <span className="text-xs font-medium text-muted-foreground">
119
- Transitions ({step.transitions.length})
126
+ {t('cpq.wizards.detail.transitions', 'Transitions ({count})', { count: step.transitions.length })}
120
127
  </span>
121
128
  <div className="space-y-1.5">
122
- {step.transitions.map((t, i) => (
123
- <TransitionRow key={`${t.targetStepId}-${i}`} t={t} index={i} />
129
+ {step.transitions.map((transition, i) => (
130
+ <TransitionRow key={`${transition.targetStepId}-${i}`} transition={transition} index={i} />
124
131
  ))}
125
132
  </div>
126
133
  </div>
127
134
  )}
128
135
 
129
136
  {step.transitions.length === 0 && (
130
- <div className="text-xs text-muted-foreground">No transitions (terminal step)</div>
137
+ <div className="text-xs text-muted-foreground">{t('cpq.wizards.detail.noTransitions', 'No transitions (terminal step)')}</div>
131
138
  )}
132
139
 
133
140
  {step.validationRules && step.validationRules.length > 0 && (
134
141
  <div className="space-y-1">
135
- <span className="text-xs font-medium text-muted-foreground">Validation Rules</span>
142
+ <span className="text-xs font-medium text-muted-foreground">{t('cpq.wizards.detail.validationRules', 'Validation Rules')}</span>
136
143
  <JsonBlock data={step.validationRules} />
137
144
  </div>
138
145
  )}
@@ -143,16 +150,17 @@ function StepCard({ step, index }: { step: WizardStepDefinition; index: number }
143
150
  }
144
151
 
145
152
  function ApplicabilityPanel({ applicability }: { applicability: WizardApplicability | null }) {
153
+ const t = useT()
146
154
  if (!applicability) {
147
- return <span className="text-sm text-muted-foreground">No applicability constraints</span>
155
+ return <span className="text-sm text-muted-foreground">{t('cpq.wizards.detail.noApplicability', 'No applicability constraints')}</span>
148
156
  }
149
157
  return (
150
158
  <div className="grid grid-cols-2 gap-4">
151
159
  {applicability.priority != null && (
152
- <KV label="Priority">{applicability.priority}</KV>
160
+ <KV label={t('cpq.wizards.detail.priority', 'Priority')}>{applicability.priority}</KV>
153
161
  )}
154
162
  {applicability.specCodes?.length ? (
155
- <KV label="Spec Codes">
163
+ <KV label={t('cpq.wizards.detail.specCodes', 'Spec Codes')}>
156
164
  <div className="flex flex-wrap gap-1">
157
165
  {applicability.specCodes.map((c) => (
158
166
  <span key={c} className="font-mono text-xs rounded bg-muted px-1.5 py-0.5">{c}</span>
@@ -161,7 +169,7 @@ function ApplicabilityPanel({ applicability }: { applicability: WizardApplicabil
161
169
  </KV>
162
170
  ) : null}
163
171
  {applicability.offeringCodes?.length ? (
164
- <KV label="Offering Codes">
172
+ <KV label={t('cpq.wizards.detail.offeringCodes', 'Offering Codes')}>
165
173
  <div className="flex flex-wrap gap-1">
166
174
  {applicability.offeringCodes.map((c) => (
167
175
  <span key={c} className="font-mono text-xs rounded bg-muted px-1.5 py-0.5">{c}</span>
@@ -170,7 +178,7 @@ function ApplicabilityPanel({ applicability }: { applicability: WizardApplicabil
170
178
  </KV>
171
179
  ) : null}
172
180
  {applicability.customerSegments?.length ? (
173
- <KV label="Customer Segments">
181
+ <KV label={t('cpq.wizards.detail.customerSegments', 'Customer Segments')}>
174
182
  <div className="flex flex-wrap gap-1">
175
183
  {applicability.customerSegments.map((c) => (
176
184
  <span key={c} className="font-mono text-xs rounded bg-muted px-1.5 py-0.5">{c}</span>
@@ -185,6 +193,7 @@ function ApplicabilityPanel({ applicability }: { applicability: WizardApplicabil
185
193
  // ─── Main Page ───────────────────────────────────────────────────
186
194
 
187
195
  export default function CpqWizardDetailPage({ params }: { params: Record<string, string | string[]> }) {
196
+ const t = useT()
188
197
  const router = useRouter()
189
198
  const definitionCode = params.definitionCode as string
190
199
 
@@ -200,19 +209,20 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
200
209
  try {
201
210
  const def = await apiJson<WizardDefinitionResult>(
202
211
  `/api/cpq/wizards?code=${encodeURIComponent(definitionCode)}`,
212
+ t,
203
213
  )
204
214
  setDefinition(def)
205
215
  } catch (err) {
206
- setError(err instanceof Error ? err.message : 'Failed to load wizard')
216
+ setError(err instanceof Error ? err.message : t('cpq.wizards.runner.errors.load', 'Failed to load wizard'))
207
217
  } finally {
208
218
  setLoading(false)
209
219
  }
210
220
  }
211
221
  load()
212
- }, [definitionCode])
222
+ }, [definitionCode, t])
213
223
 
214
224
  if (loading) {
215
- return <div className="text-sm text-muted-foreground">Loading...</div>
225
+ return <div className="text-sm text-muted-foreground">{t('cpq.common.loading', 'Loading...')}</div>
216
226
  }
217
227
 
218
228
  if (error) {
@@ -220,7 +230,7 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
220
230
  <div className="space-y-4">
221
231
  <Alert variant="destructive">{error}</Alert>
222
232
  <button type="button" onClick={() => router.push('/backend/cpq/wizards')} className="text-sm text-muted-foreground hover:text-foreground">
223
- &larr; Back to Wizards
233
+ ← {t('cpq.wizards.runner.backToList', 'Back to Wizards')}
224
234
  </button>
225
235
  </div>
226
236
  )
@@ -229,7 +239,7 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
229
239
  if (!definition) {
230
240
  return (
231
241
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
232
- Wizard not found.
242
+ {t('cpq.wizards.runner.notFound', 'Wizard not found.')}
233
243
  </div>
234
244
  )
235
245
  }
@@ -243,7 +253,7 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
243
253
  onClick={() => router.push('/backend/cpq/wizards')}
244
254
  className="text-sm text-muted-foreground hover:text-foreground"
245
255
  >
246
- &larr; All Wizards
256
+ ← {t('cpq.wizards.runner.allWizards', 'All Wizards')}
247
257
  </button>
248
258
  </div>
249
259
 
@@ -261,7 +271,7 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
261
271
  items={[
262
272
  {
263
273
  id: 'preview',
264
- label: 'Preview',
274
+ label: t('cpq.wizards.actions.preview', 'Preview'),
265
275
  onSelect: () => router.push(`/backend/cpq/wizards/${definitionCode}`),
266
276
  },
267
277
  ]}
@@ -270,15 +280,15 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
270
280
  </div>
271
281
 
272
282
  {/* General Info */}
273
- <Section title="General">
283
+ <Section title={t('cpq.wizards.detail.sections.general', 'General')}>
274
284
  <div className="rounded-lg border p-4">
275
285
  <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
276
- <KV label="Code"><span className="font-mono text-xs">{definition.code}</span></KV>
277
- <KV label="Version">v{definition.version}</KV>
278
- <KV label="Surface">{definition.surface}</KV>
279
- <KV label="Active">
286
+ <KV label={t('cpq.wizards.detail.code', 'Code')}><span className="font-mono text-xs">{definition.code}</span></KV>
287
+ <KV label={t('cpq.wizards.detail.version', 'Version')}>v{definition.version}</KV>
288
+ <KV label={t('cpq.wizards.detail.surface', 'Surface')}>{definition.surface}</KV>
289
+ <KV label={t('cpq.wizards.detail.active', 'Active')}>
280
290
  <Badge variant={definition.isActive ? 'success' : 'muted'}>
281
- {definition.isActive ? 'Yes' : 'No'}
291
+ {definition.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
282
292
  </Badge>
283
293
  </KV>
284
294
  </div>
@@ -286,7 +296,7 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
286
296
  </Section>
287
297
 
288
298
  {/* Applicability */}
289
- <Section title="Applicability">
299
+ <Section title={t('cpq.wizards.detail.sections.applicability', 'Applicability')}>
290
300
  <div className="rounded-lg border p-4">
291
301
  <ApplicabilityPanel applicability={definition.applicability} />
292
302
  </div>
@@ -294,13 +304,13 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
294
304
 
295
305
  {/* Params Schema */}
296
306
  {definition.paramsSchema && Object.keys(definition.paramsSchema).length > 0 && (
297
- <Section title="Parameters Schema">
307
+ <Section title={t('cpq.wizards.detail.sections.paramsSchema', 'Parameters Schema')}>
298
308
  <JsonBlock data={definition.paramsSchema} />
299
309
  </Section>
300
310
  )}
301
311
 
302
312
  {/* Steps */}
303
- <Section title={`Steps (${definition.steps.length})`}>
313
+ <Section title={t('cpq.wizards.detail.sections.steps', 'Steps ({count})', { count: definition.steps.length })}>
304
314
  <div className="space-y-2">
305
315
  {definition.steps.map((step, i) => (
306
316
  <StepCard key={step.stepId} step={step} index={i} />
@@ -310,7 +320,7 @@ export default function CpqWizardDetailPage({ params }: { params: Record<string,
310
320
 
311
321
  {/* Metadata */}
312
322
  {definition.metadata && Object.keys(definition.metadata).length > 0 && (
313
- <Section title="Metadata">
323
+ <Section title={t('cpq.wizards.detail.sections.metadata', 'Metadata')}>
314
324
  <JsonBlock data={definition.metadata} />
315
325
  </Section>
316
326
  )}
@@ -2,6 +2,7 @@
2
2
  import * as React from 'react'
3
3
  import { useRouter, useSearchParams } from 'next/navigation'
4
4
  import { Alert } from '@open-mercato/ui/primitives/alert'
5
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
5
6
  import { WizardRunner } from '../../../../workflows/WizardRunner'
6
7
  import { registerBuiltInStepTypes } from '../../../../workflows/steps'
7
8
  import type { WizardDefinitionResult } from '../../../../workflows/types'
@@ -9,9 +10,9 @@ import type { WizardDefinitionResult } from '../../../../workflows/types'
9
10
  // Register built-in step types once on module load
10
11
  registerBuiltInStepTypes()
11
12
 
12
- async function apiJson<T>(url: string): Promise<T> {
13
+ async function apiJson<T>(url: string, t: TranslateFn): Promise<T> {
13
14
  const res = await fetch(url)
14
- if (!res.ok) throw new Error(`API error ${res.status}`)
15
+ if (!res.ok) throw new Error(t('cpq.wizards.runner.errors.api', 'API error {status}', { status: res.status }))
15
16
  return res.json() as Promise<T>
16
17
  }
17
18
 
@@ -30,6 +31,7 @@ function ParamsForm({
30
31
  schema: ParamsSchema
31
32
  onStart: (params: Record<string, string>) => void
32
33
  }) {
34
+ const t = useT()
33
35
  const properties = schema.properties ?? {}
34
36
  const requiredFields = schema.required ?? []
35
37
  const fieldNames = Object.keys(properties)
@@ -45,8 +47,8 @@ function ParamsForm({
45
47
  return (
46
48
  <div className="space-y-4">
47
49
  <div className="rounded-lg border bg-muted/30 p-4 space-y-1">
48
- <h4 className="text-sm font-medium">This wizard requires input parameters</h4>
49
- <p className="text-xs text-muted-foreground">Fill in the required fields before starting.</p>
50
+ <h4 className="text-sm font-medium">{t('cpq.wizards.runner.params.title', 'This wizard requires input parameters')}</h4>
51
+ <p className="text-xs text-muted-foreground">{t('cpq.wizards.runner.params.hint', 'Fill in the required fields before starting.')}</p>
50
52
  </div>
51
53
 
52
54
  <div className="space-y-3">
@@ -67,7 +69,7 @@ function ParamsForm({
67
69
  className="w-full rounded-md border px-3 py-2 text-sm font-mono"
68
70
  />
69
71
  {prop?.format && (
70
- <p className="text-xs text-muted-foreground mt-0.5">Format: {prop.format}</p>
72
+ <p className="text-xs text-muted-foreground mt-0.5">{t('cpq.wizards.runner.params.format', 'Format: {format}', { format: prop.format })}</p>
71
73
  )}
72
74
  </div>
73
75
  )
@@ -80,7 +82,7 @@ function ParamsForm({
80
82
  disabled={!isValid}
81
83
  className="inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
82
84
  >
83
- Preview Wizard
85
+ {t('cpq.wizards.runner.params.start', 'Preview Wizard')}
84
86
  </button>
85
87
  </div>
86
88
  )
@@ -89,6 +91,7 @@ function ParamsForm({
89
91
  // ─── Main page ──────────────────────────────────────────────────
90
92
 
91
93
  export default function CpqWizardRunnerPage({ params }: { params: Record<string, string | string[]> }) {
94
+ const t = useT()
92
95
  const router = useRouter()
93
96
  const searchParams = useSearchParams()
94
97
  const definitionCode = params.definitionCode as string
@@ -114,7 +117,7 @@ export default function CpqWizardRunnerPage({ params }: { params: Record<string,
114
117
  React.useEffect(() => {
115
118
  async function load() {
116
119
  if (!definitionCode || definitionCode === 'undefined') {
117
- setError('Invalid wizard definition code')
120
+ setError(t('cpq.wizards.runner.errors.invalidCode', 'Invalid wizard definition code'))
118
121
  setLoading(false)
119
122
  return
120
123
  }
@@ -123,6 +126,7 @@ export default function CpqWizardRunnerPage({ params }: { params: Record<string,
123
126
  try {
124
127
  const def = await apiJson<WizardDefinitionResult>(
125
128
  `/api/cpq/wizards?code=${encodeURIComponent(definitionCode)}`,
129
+ t,
126
130
  )
127
131
  setDefinition(def)
128
132
 
@@ -133,13 +137,13 @@ export default function CpqWizardRunnerPage({ params }: { params: Record<string,
133
137
  setStarted(true)
134
138
  }
135
139
  } catch (err) {
136
- setError(err instanceof Error ? err.message : 'Failed to load wizard')
140
+ setError(err instanceof Error ? err.message : t('cpq.wizards.runner.errors.load', 'Failed to load wizard'))
137
141
  } finally {
138
142
  setLoading(false)
139
143
  }
140
144
  }
141
145
  load()
142
- }, [definitionCode, queryParams])
146
+ }, [definitionCode, queryParams, t])
143
147
 
144
148
  const handleComplete = React.useCallback((stepData: Record<string, Record<string, unknown>>) => {
145
149
  // Check for orderId first (e.g. purchasing wizard creates an order)
@@ -164,7 +168,7 @@ export default function CpqWizardRunnerPage({ params }: { params: Record<string,
164
168
  }, [])
165
169
 
166
170
  if (loading) {
167
- return <div className="text-sm text-muted-foreground">Loading wizard...</div>
171
+ return <div className="text-sm text-muted-foreground">{t('cpq.wizards.runner.loading', 'Loading wizard...')}</div>
168
172
  }
169
173
 
170
174
  if (error) {
@@ -176,7 +180,7 @@ export default function CpqWizardRunnerPage({ params }: { params: Record<string,
176
180
  onClick={() => router.push('/backend/cpq/wizards')}
177
181
  className="inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted"
178
182
  >
179
- Back to Wizards
183
+ {t('cpq.wizards.runner.backToList', 'Back to Wizards')}
180
184
  </button>
181
185
  </div>
182
186
  )
@@ -185,7 +189,7 @@ export default function CpqWizardRunnerPage({ params }: { params: Record<string,
185
189
  if (!definition) {
186
190
  return (
187
191
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
188
- Wizard not found.
192
+ {t('cpq.wizards.runner.notFound', 'Wizard not found.')}
189
193
  </div>
190
194
  )
191
195
  }
@@ -202,7 +206,7 @@ export default function CpqWizardRunnerPage({ params }: { params: Record<string,
202
206
  onClick={() => router.push('/backend/cpq/wizards')}
203
207
  className="text-sm text-muted-foreground hover:text-foreground"
204
208
  >
205
- &larr; All Wizards
209
+ ← {t('cpq.wizards.runner.allWizards', 'All Wizards')}
206
210
  </button>
207
211
  </div>
208
212
 
@@ -95,7 +95,7 @@ export default function CpqWizardsPage() {
95
95
  header: t('cpq.wizards.table.active', 'Active'),
96
96
  cell: ({ row }) => (
97
97
  <Tag variant={row.original.isActive ? 'success' : 'neutral'} dot>
98
- {row.original.isActive ? 'Yes' : 'No'}
98
+ {row.original.isActive ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')}
99
99
  </Tag>
100
100
  ),
101
101
  },
@@ -39,8 +39,8 @@ export function ArcLineDiff({ line, currency }: { line: ArcDiffLine; currency: s
39
39
  const isCancel = line.action === 'cancel'
40
40
  const rows: Array<{ label: string; before: number; after: number; asCurrency: boolean }> = [
41
41
  { label: t('cpq.arc.diff.quantity', 'Quantity'), before: src.quantity, after: isCancel ? 0 : line.quantity, asCurrency: false },
42
- { label: 'MRC', before: src.mrcAmount, after: isCancel ? 0 : line.mrcTotal, asCurrency: true },
43
- { label: 'NRC', before: src.nrcAmount, after: isCancel ? 0 : line.nrcTotal, asCurrency: true },
42
+ { label: t('cpq.arc.diff.mrc', 'MRC'), before: src.mrcAmount, after: isCancel ? 0 : line.mrcTotal, asCurrency: true },
43
+ { label: t('cpq.arc.diff.nrc', 'NRC'), before: src.nrcAmount, after: isCancel ? 0 : line.nrcTotal, asCurrency: true },
44
44
  ]
45
45
  const fmtVal = (value: number, asCurrency: boolean) => (asCurrency ? formatMoney(value, currency) : String(value))
46
46
 
@@ -94,7 +94,9 @@ export function ChargeBreakdownTable<C extends ChargeRowView>({
94
94
  <span>{c.chargeName}</span>
95
95
  {errors.length > 0 && (
96
96
  <Tag variant="warning" className="ml-1.5 px-1.5 text-xs" data-testid="cpq-adjustment-invalid">
97
- {t('cpq.priceAdjustments.invalid', 'Invalid')}: {t(`cpq.priceAdjustments.errors.${errors[0].code}`, ADJUSTMENT_ERROR_FALLBACKS[errors[0].code] ?? errors[0].code)}
97
+ {t('cpq.priceAdjustments.invalidReason', 'Invalid: {reason}', {
98
+ reason: t(`cpq.priceAdjustments.errors.${errors[0].code}`, ADJUSTMENT_ERROR_FALLBACKS[errors[0].code] ?? errors[0].code),
99
+ })}
98
100
  </Tag>
99
101
  )}
100
102
  </td>
@@ -149,7 +151,7 @@ function ChargePricingDetail({ charge: c, currency, reasonLabels, renderManualAc
149
151
  return (
150
152
  <div className="rounded border bg-background p-3 space-y-3 text-xs">
151
153
  <div className="flex items-center justify-between">
152
- <span className="font-semibold text-sm">{t('cpq.charges.pricingDetails', 'Pricing Details')} — {c.chargeName}</span>
154
+ <span className="font-semibold text-sm">{t('cpq.charges.pricingDetailsFor', 'Pricing Details — {name}', { name: c.chargeName })}</span>
153
155
  <span className="text-muted-foreground">{c.pricingMethod ?? 'flat'}</span>
154
156
  </div>
155
157
 
@@ -73,8 +73,10 @@ export function PriceWaterfall({ charge, currency, reasonLabels, renderManualAct
73
73
  )
74
74
  }
75
75
  const source = row.kind === 'rule'
76
- ? `${t('cpq.priceAdjustments.waterfall.rule', 'Price rule')}: ${row.label}`
77
- : `${t('cpq.priceAdjustments.waterfall.manual', 'Negotiated')}: ${reasonLabels?.[row.reasonCode ?? ''] ?? row.reasonCode ?? row.label}`
76
+ ? t('cpq.priceAdjustments.waterfall.ruleSource', 'Price rule: {name}', { name: row.label })
77
+ : t('cpq.priceAdjustments.waterfall.manualSource', 'Negotiated: {reason}', {
78
+ reason: reasonLabels?.[row.reasonCode ?? ''] ?? row.reasonCode ?? row.label,
79
+ })
78
80
  return (
79
81
  <tr key={`${row.kind}-${index}`} className="border-t border-border/30">
80
82
  <td className="py-0.5 pl-3 text-muted-foreground">{t(`cpq.priceAdjustments.types.${row.ruleType}`, TYPE_FALLBACKS[row.ruleType] ?? row.ruleType)}</td>
@@ -94,7 +94,7 @@ export function PricingSummaryCard({ summary, currency, arcDelta }: {
94
94
  <div>
95
95
  <p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.usageCharges', 'Usage Charges')}</p>
96
96
  {usageCharges.map((uc, i) => (
97
- <p key={i} className="text-xs mt-0.5">{uc.chargeName}: {formatMoney(uc.unitPrice, currency)}/unit</p>
97
+ <p key={i} className="text-xs mt-0.5">{t('cpq.quotes.summary.usageChargeLine', '{name}: {price}/unit', { name: uc.chargeName, price: formatMoney(uc.unitPrice, currency) })}</p>
98
98
  ))}
99
99
  </div>
100
100
  )}
@@ -2,6 +2,7 @@
2
2
  import * as React from 'react'
3
3
  import { ChevronRight } from 'lucide-react'
4
4
  import { Tag } from '@open-mercato/ui/primitives/tag'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
6
  import type { TagMap } from '@open-mercato/ui/primitives/tag'
6
7
  import { formatStatusLabel } from './statusMaps'
7
8
 
@@ -30,7 +31,7 @@ export function StatusPath({
30
31
  onTransition,
31
32
  allowedTransitions,
32
33
  disabled,
33
- ariaLabel = 'Status path',
34
+ ariaLabel,
34
35
  }: {
35
36
  current: string
36
37
  /** Happy-path statuses in order. */
@@ -39,15 +40,17 @@ export function StatusPath({
39
40
  terminals?: string[]
40
41
  /** Status → Tag variant. Missing entries fall back to "neutral". */
41
42
  statusMap: Record<string, TagVariant>
42
- /** Optional human labels (overrides `formatStatusLabel`). */
43
+ /** Optional human labels, already translated by the caller (overrides `formatStatusLabel`). */
43
44
  labels?: Record<string, string>
44
45
  /** Click handler. Omit for read-only path. */
45
46
  onTransition?: (target: string) => void
46
47
  /** If set, only these targets are clickable. Else any non-current. */
47
48
  allowedTransitions?: string[]
48
49
  disabled?: boolean
50
+ /** Translated accessible name; defaults to "Status path". */
49
51
  ariaLabel?: string
50
52
  }) {
53
+ const t = useT()
51
54
  // Build the displayed sequence. Aliases can swap into the happy
52
55
  // path slot (e.g. quote's `pre_approved` shown as `approved`). We
53
56
  // don't handle aliasing here — consumers can swap entries in
@@ -61,7 +64,7 @@ export function StatusPath({
61
64
  const interactiveMode = typeof onTransition === 'function'
62
65
 
63
66
  return (
64
- <nav aria-label={ariaLabel} className="flex items-center gap-1.5 flex-wrap text-xs">
67
+ <nav aria-label={ariaLabel ?? t('cpq.statusPath.ariaLabel', 'Status path')} className="flex items-center gap-1.5 flex-wrap text-xs">
65
68
  {sequence.map((status, idx) => {
66
69
  const reached = currentIdx >= 0 && idx <= currentIdx
67
70
  const isCurrent = status === current
@@ -91,7 +94,7 @@ export function StatusPath({
91
94
  type="button"
92
95
  onClick={() => onTransition!(status)}
93
96
  aria-current={isCurrent ? 'step' : undefined}
94
- title={`Transition to ${label}`}
97
+ title={t('cpq.statusPath.transitionTo', 'Transition to {status}', { status: label })}
95
98
  className={wrapperClass}
96
99
  >
97
100
  <Tag variant={variant} dot={isCurrent} className={tagClass}>