@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
@@ -9,7 +9,7 @@ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar
9
9
  import { CpqListView, useCpqListData } from '../../../components/CpqListView'
10
10
  import {
11
11
  PriceRuleForm,
12
- RULE_TYPE_LABELS,
12
+ buildRuleTypeLabels,
13
13
  emptyPriceRuleForm,
14
14
  priceRuleFormToPayload,
15
15
  type PriceRule,
@@ -62,18 +62,20 @@ export default function PriceRulesPage() {
62
62
  .catch(() => {})
63
63
  }, [])
64
64
 
65
+ const ruleTypeLabels = React.useMemo(() => buildRuleTypeLabels(t), [t])
66
+
65
67
  const ruleTypeOptions = React.useMemo(
66
- () => Object.entries(RULE_TYPE_LABELS).map(([value, label]) => ({ value, label })),
67
- [],
68
+ () => Object.entries(ruleTypeLabels).map(([value, label]) => ({ value, label })),
69
+ [ruleTypeLabels],
68
70
  )
69
71
 
70
72
  const chargeTypeOptions = React.useMemo(
71
73
  () => [
72
- { value: 'nrc', label: 'NRC' },
73
- { value: 'mrc', label: 'MRC' },
74
- { value: 'usage', label: 'Usage' },
74
+ { value: 'nrc', label: t('cpq.priceRules.chargeTypes.nrc', 'NRC') },
75
+ { value: 'mrc', label: t('cpq.priceRules.chargeTypes.mrc', 'MRC') },
76
+ { value: 'usage', label: t('cpq.priceRules.chargeTypes.usage', 'Usage') },
75
77
  ],
76
- [],
78
+ [t],
77
79
  )
78
80
 
79
81
  const filters = React.useMemo<FilterDef[]>(
@@ -122,16 +124,20 @@ export default function PriceRulesPage() {
122
124
  })
123
125
  if (!res.ok) {
124
126
  const err = await res.json()
125
- throw new Error(err.details ? JSON.stringify(err.details) : err.error ?? 'Failed to create')
127
+ throw new Error(
128
+ err.details
129
+ ? JSON.stringify(err.details)
130
+ : err.error ?? t('cpq.priceRules.errors.createFailed', 'Failed to create'),
131
+ )
126
132
  }
127
133
  setShowForm(false)
128
134
  data.reload()
129
135
  } catch (err: unknown) {
130
- setError(err instanceof Error ? err.message : 'Unknown error')
136
+ setError(err instanceof Error ? err.message : t('cpq.priceRules.errors.unknown', 'Unknown error'))
131
137
  } finally {
132
138
  setSaving(false)
133
139
  }
134
- }, [data, form])
140
+ }, [data, form, t])
135
141
 
136
142
  const columns = React.useMemo<ColumnDef<PriceRule>[]>(
137
143
  () => [
@@ -154,7 +160,7 @@ export default function PriceRulesPage() {
154
160
  accessorKey: 'ruleType',
155
161
  header: t('cpq.priceRules.table.type', 'Type'),
156
162
  cell: ({ row }) => (
157
- <span>{RULE_TYPE_LABELS[row.original.ruleType] ?? row.original.ruleType}</span>
163
+ <span>{ruleTypeLabels[row.original.ruleType] ?? row.original.ruleType}</span>
158
164
  ),
159
165
  },
160
166
  {
@@ -168,8 +174,9 @@ export default function PriceRulesPage() {
168
174
  cell: ({ row }) => (
169
175
  <span className="text-xs">
170
176
  {row.original.productOfferingId
171
- ? row.original.productOfferingName ?? 'Product-scoped'
172
- : 'Global'}
177
+ ? row.original.productOfferingName ??
178
+ t('cpq.priceRules.scope.productScoped', 'Product-scoped')
179
+ : t('cpq.priceRules.scope.global', 'Global')}
173
180
  </span>
174
181
  ),
175
182
  },
@@ -201,12 +208,14 @@ export default function PriceRulesPage() {
201
208
  header: t('cpq.priceRules.table.status', 'Status'),
202
209
  cell: ({ row }) => (
203
210
  <Tag variant={row.original.isActive ? 'success' : 'neutral'} dot>
204
- {row.original.isActive ? 'Active' : 'Inactive'}
211
+ {row.original.isActive
212
+ ? t('cpq.common.active', 'Active')
213
+ : t('cpq.common.inactive', 'Inactive')}
205
214
  </Tag>
206
215
  ),
207
216
  },
208
217
  ],
209
- [t],
218
+ [ruleTypeLabels, t],
210
219
  )
211
220
 
212
221
  return (
@@ -245,7 +254,7 @@ export default function PriceRulesPage() {
245
254
  }
246
255
  emptyState={
247
256
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
248
- No price rules found. Click &quot;Create Rule&quot; to add one.
257
+ {t('cpq.priceRules.empty', 'No price rules found. Click "Create Rule" to add one.')}
249
258
  </div>
250
259
  }
251
260
  />
@@ -11,6 +11,7 @@ import {
11
11
  SelectValue,
12
12
  } from '@open-mercato/ui/primitives/select'
13
13
  import { Trash2 } from 'lucide-react'
14
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
14
15
 
15
16
  // ─── Types ──────────────────────────────────────────────────────
16
17
 
@@ -54,6 +55,7 @@ export function EntriesEditor({
54
55
  initialEntries: Entry[]
55
56
  onSaved: (entries: Entry[]) => void
56
57
  }) {
58
+ const t = useT()
57
59
  const [drafts, setDrafts] = React.useState<DraftEntry[]>(() =>
58
60
  initialEntries.map((e) => ({ ...e, _state: null, _localId: e.id })),
59
61
  )
@@ -142,7 +144,7 @@ export function EntriesEditor({
142
144
  })
143
145
  if (!res.ok) {
144
146
  const body = await res.json().catch(() => ({}))
145
- return { ok: false, error: body.error ?? 'Failed to create entries' }
147
+ return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.createFailed', 'Failed to create entries') }
146
148
  }
147
149
  }
148
150
 
@@ -164,7 +166,7 @@ export function EntriesEditor({
164
166
  })
165
167
  if (!res.ok) {
166
168
  const body = await res.json().catch(() => ({}))
167
- return { ok: false, error: body.error ?? `Failed to update entry ${d.id}` }
169
+ return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.updateFailed', 'Failed to update entry {id}', { id: d.id }) }
168
170
  }
169
171
  }
170
172
 
@@ -178,13 +180,13 @@ export function EntriesEditor({
178
180
  })
179
181
  if (!res.ok) {
180
182
  const body = await res.json().catch(() => ({}))
181
- return { ok: false, error: body.error ?? `Failed to delete entry ${d.id}` }
183
+ return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.deleteFailed', 'Failed to delete entry {id}', { id: d.id }) }
182
184
  }
183
185
  }
184
186
 
185
187
  return { ok: true }
186
188
  } catch (err) {
187
- return { ok: false, error: err instanceof Error ? err.message : 'Failed to save entries' }
189
+ return { ok: false, error: err instanceof Error ? err.message : t('cpq.pricing.entries.errors.saveFailed', 'Failed to save entries') }
188
190
  } finally {
189
191
  setSaving(false)
190
192
  }
@@ -208,26 +210,26 @@ export function EntriesEditor({
208
210
  <div className="space-y-3">
209
211
  <div className="flex items-center justify-between">
210
212
  <h2 className="text-lg font-semibold">
211
- Entries ({visibleDrafts.length})
213
+ {t('cpq.pricing.entries.title', 'Entries ({count})', { count: visibleDrafts.length })}
212
214
  </h2>
213
215
  <div className="flex items-center gap-2">
214
216
  {mode === 'view' ? (
215
217
  <Button type="button" variant="outline" size="sm" onClick={() => setMode('edit')}>
216
- Edit Entries
218
+ {t('cpq.pricing.entries.edit', 'Edit Entries')}
217
219
  </Button>
218
220
  ) : (
219
221
  <>
220
222
  <Button type="button" variant="outline" size="sm" onClick={() => addRow(false)}>
221
- + Add Row
223
+ {t('cpq.pricing.entries.addRow', '+ Add Row')}
222
224
  </Button>
223
225
  <Button type="button" variant="outline" size="sm" onClick={() => addRow(true)}>
224
- + Add Tier
226
+ {t('cpq.pricing.entries.addTier', '+ Add Tier')}
225
227
  </Button>
226
228
  <Button type="button" size="sm" onClick={handleSave} disabled={saving || !dirty}>
227
- {saving ? 'Saving...' : 'Save'}
229
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
228
230
  </Button>
229
231
  <Button type="button" variant="outline" size="sm" onClick={cancelEdit} disabled={saving}>
230
- Cancel
232
+ {t('cpq.common.cancel', 'Cancel')}
231
233
  </Button>
232
234
  </>
233
235
  )}
@@ -239,8 +241,11 @@ export function EntriesEditor({
239
241
  {visibleDrafts.length === 0 ? (
240
242
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
241
243
  {mode === 'edit'
242
- ? 'No entries yet — use "+ Add Row" (dimensions lookup) or "+ Add Tier" (range lookup).'
243
- : 'No entries in this pricing table.'}
244
+ ? t(
245
+ 'cpq.pricing.entries.emptyEditing',
246
+ 'No entries yet — use "+ Add Row" (dimensions lookup) or "+ Add Tier" (range lookup).',
247
+ )
248
+ : t('cpq.pricing.entries.empty', 'No entries in this pricing table.')}
244
249
  </div>
245
250
  ) : (
246
251
  <div className="rounded-lg border overflow-x-auto">
@@ -256,9 +261,9 @@ export function EntriesEditor({
256
261
  prices they qualify, so the row reads "what → how much". */}
257
262
  {hasTieredEntries && (
258
263
  <>
259
- <th className="px-3 py-3 text-left font-medium whitespace-nowrap">Tier #</th>
260
- <th className="px-3 py-3 text-left font-medium whitespace-nowrap">Range From</th>
261
- <th className="px-3 py-3 text-left font-medium whitespace-nowrap">Range To</th>
264
+ <th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.tierNumber', 'Tier #')}</th>
265
+ <th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.rangeFrom', 'Range From')}</th>
266
+ <th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.rangeTo', 'Range To')}</th>
262
267
  </>
263
268
  )}
264
269
  {table.priceColumns.map((col) => (
@@ -266,8 +271,8 @@ export function EntriesEditor({
266
271
  {col.label}
267
272
  </th>
268
273
  ))}
269
- <th className="px-3 py-3 text-left font-medium">Currency</th>
270
- {mode === 'edit' && <th className="px-3 py-3 w-8" aria-label="Actions" />}
274
+ <th className="px-3 py-3 text-left font-medium">{t('cpq.pricing.entries.currency', 'Currency')}</th>
275
+ {mode === 'edit' && <th className="px-3 py-3 w-8" aria-label={t('cpq.pricing.entries.actions', 'Actions')} />}
271
276
  </tr>
272
277
  </thead>
273
278
  <tbody>
@@ -307,6 +312,7 @@ function EntryRow({
307
312
  onChange: (patch: Partial<DraftEntry>) => void
308
313
  onDelete: () => void
309
314
  }) {
315
+ const t = useT()
310
316
  const readOnly = mode === 'view'
311
317
  const rowBg = entry._state === 'new'
312
318
  ? 'bg-success/5'
@@ -421,7 +427,7 @@ function EntryRow({
421
427
  type="button"
422
428
  onClick={onDelete}
423
429
  className="text-destructive hover:bg-destructive/10 rounded p-1"
424
- aria-label="Delete row"
430
+ aria-label={t('cpq.pricing.entries.deleteRow', 'Delete row')}
425
431
  >
426
432
  <Trash2 className="h-4 w-4" />
427
433
  </button>
@@ -3,6 +3,7 @@ import * as React from 'react'
3
3
  import { Alert } from '@open-mercato/ui/primitives/alert'
4
4
  import { Button } from '@open-mercato/ui/primitives/button'
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 type { ImportDiff } from './xlsxImport'
7
8
 
8
9
  export function ImportPreview({
@@ -16,22 +17,23 @@ export function ImportPreview({
16
17
  onConfirm: () => void
17
18
  onCancel: () => void
18
19
  }) {
20
+ const t = useT()
19
21
  const total = diff.inserts.length + diff.updates.length + diff.deletes.length
20
22
  return (
21
23
  <div className="rounded-lg border bg-card p-5 space-y-4">
22
24
  <div className="flex items-center justify-between">
23
- <h3 className="font-semibold">Import preview</h3>
25
+ <h3 className="font-semibold">{t('cpq.pricing.import.preview.title', 'Import preview')}</h3>
24
26
  <div className="flex gap-2 items-center text-sm">
25
- <Tag variant="success" dot>{diff.inserts.length} new</Tag>
26
- <Tag variant="warning" dot>{diff.updates.length} updated</Tag>
27
- <Tag variant="error" dot>{diff.deletes.length} deleted</Tag>
28
- <span className="text-muted-foreground">({diff.unchanged.length} unchanged)</span>
27
+ <Tag variant="success" dot>{t('cpq.pricing.import.preview.newCount', '{count} new', { count: diff.inserts.length })}</Tag>
28
+ <Tag variant="warning" dot>{t('cpq.pricing.import.preview.updatedCount', '{count} updated', { count: diff.updates.length })}</Tag>
29
+ <Tag variant="error" dot>{t('cpq.pricing.import.preview.deletedCount', '{count} deleted', { count: diff.deletes.length })}</Tag>
30
+ <span className="text-muted-foreground">{t('cpq.pricing.import.preview.unchangedCount', '({count} unchanged)', { count: diff.unchanged.length })}</span>
29
31
  </div>
30
32
  </div>
31
33
 
32
34
  {diff.warnings.length > 0 && (
33
35
  <Alert variant="default">
34
- <div className="font-medium mb-1">Warnings</div>
36
+ <div className="font-medium mb-1">{t('cpq.pricing.import.preview.warnings', 'Warnings')}</div>
35
37
  <ul className="list-disc pl-5 text-xs space-y-0.5">
36
38
  {diff.warnings.map((w, i) => <li key={i}>{w}</li>)}
37
39
  </ul>
@@ -40,19 +42,19 @@ export function ImportPreview({
40
42
 
41
43
  {total === 0 ? (
42
44
  <Alert>
43
- The file contains no changes — every row matches an existing entry.
45
+ {t('cpq.pricing.import.preview.noChanges', 'The file contains no changes — every row matches an existing entry.')}
44
46
  </Alert>
45
47
  ) : (
46
48
  <div className="space-y-2 text-xs">
47
49
  {diff.inserts.length > 0 && (
48
50
  <details className="rounded border p-2">
49
51
  <summary className="cursor-pointer font-medium text-success">
50
- {diff.inserts.length} new entries will be created
52
+ {t('cpq.pricing.import.preview.insertsSummary', '{count} new entries will be created', { count: diff.inserts.length })}
51
53
  </summary>
52
54
  <ul className="mt-2 pl-5 list-disc space-y-0.5 max-h-40 overflow-y-auto">
53
55
  {diff.inserts.map((r) => (
54
56
  <li key={r.rowNumber}>
55
- Row {r.rowNumber}: {summarizeRow(r)}
57
+ {t('cpq.pricing.import.preview.insertRow', 'Row {row}: {summary}', { row: r.rowNumber, summary: summarizeRow(r, t) })}
56
58
  </li>
57
59
  ))}
58
60
  </ul>
@@ -61,12 +63,15 @@ export function ImportPreview({
61
63
  {diff.updates.length > 0 && (
62
64
  <details className="rounded border p-2">
63
65
  <summary className="cursor-pointer font-medium text-warning">
64
- {diff.updates.length} entries will be updated
66
+ {t('cpq.pricing.import.preview.updatesSummary', '{count} entries will be updated', { count: diff.updates.length })}
65
67
  </summary>
66
68
  <ul className="mt-2 pl-5 list-disc space-y-0.5 max-h-40 overflow-y-auto">
67
69
  {diff.updates.map((u) => (
68
70
  <li key={u.before.id}>
69
- Row {u.after.rowNumber} (id <code className="font-mono">{u.before.id.slice(0, 8)}…</code>): {summarizeRow(u.after)}
71
+ {/* The truncated id renders as <code> between prefix and suffix. */}
72
+ {t('cpq.pricing.import.preview.updateRowPrefix', 'Row {row} (id ', { row: u.after.rowNumber })}
73
+ <code className="font-mono">{u.before.id.slice(0, 8)}…</code>
74
+ {t('cpq.pricing.import.preview.updateRowSuffix', '): {summary}', { summary: summarizeRow(u.after, t) })}
70
75
  </li>
71
76
  ))}
72
77
  </ul>
@@ -75,12 +80,12 @@ export function ImportPreview({
75
80
  {diff.deletes.length > 0 && (
76
81
  <details className="rounded border p-2">
77
82
  <summary className="cursor-pointer font-medium text-destructive">
78
- {diff.deletes.length} entries will be deleted
83
+ {t('cpq.pricing.import.preview.deletesSummary', '{count} entries will be deleted', { count: diff.deletes.length })}
79
84
  </summary>
80
85
  <ul className="mt-2 pl-5 list-disc space-y-0.5 max-h-40 overflow-y-auto">
81
86
  {diff.deletes.map((e) => (
82
87
  <li key={e.id}>
83
- <code className="font-mono">{e.id.slice(0, 8)}…</code>: {summarizeEntry(e)}
88
+ <code className="font-mono">{e.id.slice(0, 8)}…</code>: {summarizeEntry(e, t)}
84
89
  </li>
85
90
  ))}
86
91
  </ul>
@@ -91,19 +96,24 @@ export function ImportPreview({
91
96
 
92
97
  <div className="flex gap-2 pt-2 border-t">
93
98
  <Button type="button" onClick={onConfirm} disabled={saving || total === 0}>
94
- {saving ? 'Applying...' : `Apply changes (${total})`}
99
+ {saving
100
+ ? t('cpq.pricing.import.preview.applying', 'Applying...')
101
+ : t('cpq.pricing.import.preview.apply', 'Apply changes ({count})', { count: total })}
95
102
  </Button>
96
103
  <Button type="button" variant="outline" onClick={onCancel} disabled={saving}>
97
- Cancel
104
+ {t('cpq.common.cancel', 'Cancel')}
98
105
  </Button>
99
106
  </div>
100
107
  </div>
101
108
  )
102
109
  }
103
110
 
104
- function summarizeRow(r: ImportDiff['inserts'][number] | ImportDiff['updates'][number]['after']): string {
111
+ function summarizeRow(
112
+ r: ImportDiff['inserts'][number] | ImportDiff['updates'][number]['after'],
113
+ t: TranslateFn,
114
+ ): string {
105
115
  const bits: string[] = []
106
- if (r.tierNumber != null) bits.push(`tier ${r.tierNumber}`)
116
+ if (r.tierNumber != null) bits.push(t('cpq.pricing.import.preview.tier', 'tier {tier}', { tier: r.tierNumber }))
107
117
  if (r.rangeFrom != null || r.rangeTo != null) bits.push(`[${r.rangeFrom ?? '0'}..${r.rangeTo ?? '∞'}]`)
108
118
  for (const [k, v] of Object.entries(r.dimensionValues)) if (v) bits.push(`${k}=${v}`)
109
119
  bits.push(r.currencyCode)
@@ -112,9 +122,9 @@ function summarizeRow(r: ImportDiff['inserts'][number] | ImportDiff['updates'][n
112
122
  return bits.join(' · ')
113
123
  }
114
124
 
115
- function summarizeEntry(e: ImportDiff['deletes'][number]): string {
125
+ function summarizeEntry(e: ImportDiff['deletes'][number], t: TranslateFn): string {
116
126
  const bits: string[] = []
117
- if (e.tierNumber != null) bits.push(`tier ${e.tierNumber}`)
127
+ if (e.tierNumber != null) bits.push(t('cpq.pricing.import.preview.tier', 'tier {tier}', { tier: e.tierNumber }))
118
128
  if (e.rangeFrom != null || e.rangeTo != null) bits.push(`[${e.rangeFrom ?? '0'}..${e.rangeTo ?? '∞'}]`)
119
129
  for (const [k, v] of Object.entries(e.dimensionValues)) if (v) bits.push(`${k}=${v}`)
120
130
  bits.push(e.currencyCode)