@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
@@ -10,16 +10,24 @@ import {
10
10
  SelectTrigger,
11
11
  SelectValue,
12
12
  } from '@open-mercato/ui/primitives/select'
13
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
13
14
  import { NumberInput } from '../../../components/NumberInput'
14
15
 
15
16
  // ─── Shared types ────────────────────────────────────────────────
16
17
 
17
- export const RULE_TYPE_LABELS: Record<string, string> = {
18
- discount_percent: 'Discount %',
19
- discount_absolute: 'Discount $',
20
- surcharge_percent: 'Surcharge %',
21
- surcharge_absolute: 'Surcharge $',
22
- price_override: 'Override',
18
+ /**
19
+ * Display labels for price-rule types, keyed by the persisted `ruleType`
20
+ * value. Single translatable source shared by the list, the create form and
21
+ * the detail page — call inside render with the component's `t`.
22
+ */
23
+ export function buildRuleTypeLabels(t: TranslateFn): Record<string, string> {
24
+ return {
25
+ discount_percent: t('cpq.priceRules.ruleTypes.discountPercent', 'Discount %'),
26
+ discount_absolute: t('cpq.priceRules.ruleTypes.discountAbsolute', 'Discount $'),
27
+ surcharge_percent: t('cpq.priceRules.ruleTypes.surchargePercent', 'Surcharge %'),
28
+ surcharge_absolute: t('cpq.priceRules.ruleTypes.surchargeAbsolute', 'Surcharge $'),
29
+ price_override: t('cpq.priceRules.ruleTypes.priceOverride', 'Override'),
30
+ }
23
31
  }
24
32
 
25
33
  export type PriceRuleApplicabilityCondition = {
@@ -145,15 +153,17 @@ export function PriceRuleForm({
145
153
  onSave,
146
154
  onCancel,
147
155
  }: PriceRuleFormProps) {
156
+ const t = useT()
157
+ const ruleTypeLabels = buildRuleTypeLabels(t)
148
158
  return (
149
159
  <div className="rounded-lg border bg-card p-6 space-y-4">
150
- <h2 className="text-lg font-semibold">{editingId ? 'Edit Rule' : 'New Rule'}</h2>
160
+ <h2 className="text-lg font-semibold">{editingId ? t('cpq.priceRules.form.editTitle', 'Edit Rule') : t('cpq.priceRules.form.newTitle', 'New Rule')}</h2>
151
161
  {error && (
152
162
  <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{error}</div>
153
163
  )}
154
164
  <div className="grid grid-cols-2 gap-4">
155
165
  <label className="space-y-1">
156
- <span className="text-sm font-medium">Code</span>
166
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.code', 'Code')}</span>
157
167
  <Input
158
168
  value={form.code}
159
169
  onChange={(e) => onFormChange({ ...form, code: e.target.value })}
@@ -161,14 +171,14 @@ export function PriceRuleForm({
161
171
  />
162
172
  </label>
163
173
  <label className="space-y-1">
164
- <span className="text-sm font-medium">Name</span>
174
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.name', 'Name')}</span>
165
175
  <Input
166
176
  value={form.name}
167
177
  onChange={(e) => onFormChange({ ...form, name: e.target.value })}
168
178
  />
169
179
  </label>
170
180
  <label className="space-y-1">
171
- <span className="text-sm font-medium">Rule Type</span>
181
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.ruleType', 'Rule Type')}</span>
172
182
  <Select
173
183
  value={form.ruleType}
174
184
  onValueChange={(value) => onFormChange({ ...form, ruleType: value })}
@@ -177,7 +187,7 @@ export function PriceRuleForm({
177
187
  <SelectValue />
178
188
  </SelectTrigger>
179
189
  <SelectContent>
180
- {Object.entries(RULE_TYPE_LABELS).map(([k, v]) => (
190
+ {Object.entries(ruleTypeLabels).map(([k, v]) => (
181
191
  <SelectItem key={k} value={k}>
182
192
  {v}
183
193
  </SelectItem>
@@ -186,14 +196,14 @@ export function PriceRuleForm({
186
196
  </Select>
187
197
  </label>
188
198
  <label className="space-y-1">
189
- <span className="text-sm font-medium">Value</span>
199
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.value', 'Value')}</span>
190
200
  <NumberInput
191
201
  value={form.value === '' ? null : Number(form.value)}
192
202
  onChange={(n) => onFormChange({ ...form, value: n == null ? '' : String(n) })}
193
203
  />
194
204
  </label>
195
205
  <label className="space-y-1">
196
- <span className="text-sm font-medium">Charge Type Filter</span>
206
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.chargeTypeFilter', 'Charge Type Filter')}</span>
197
207
  <Select
198
208
  value={form.chargeTypeFilter || '__all__'}
199
209
  onValueChange={(value) =>
@@ -204,23 +214,23 @@ export function PriceRuleForm({
204
214
  <SelectValue />
205
215
  </SelectTrigger>
206
216
  <SelectContent>
207
- <SelectItem value="__all__">All charge types</SelectItem>
208
- <SelectItem value="nrc">NRC only</SelectItem>
209
- <SelectItem value="mrc">MRC only</SelectItem>
210
- <SelectItem value="usage">Usage only</SelectItem>
217
+ <SelectItem value="__all__">{t('cpq.priceRules.form.allChargeTypes', 'All charge types')}</SelectItem>
218
+ <SelectItem value="nrc">{t('cpq.priceRules.form.nrcOnly', 'NRC only')}</SelectItem>
219
+ <SelectItem value="mrc">{t('cpq.priceRules.form.mrcOnly', 'MRC only')}</SelectItem>
220
+ <SelectItem value="usage">{t('cpq.priceRules.form.usageOnly', 'Usage only')}</SelectItem>
211
221
  </SelectContent>
212
222
  </Select>
213
223
  </label>
214
224
  <label className="space-y-1">
215
- <span className="text-sm font-medium">Charge Code Filter</span>
225
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.chargeCodeFilter', 'Charge Code Filter')}</span>
216
226
  <Input
217
- placeholder="e.g. setup_fee (leave empty for all)"
227
+ placeholder={t('cpq.priceRules.form.chargeCodeFilterPlaceholder', 'e.g. setup_fee (leave empty for all)')}
218
228
  value={form.chargeCodeFilter}
219
229
  onChange={(e) => onFormChange({ ...form, chargeCodeFilter: e.target.value })}
220
230
  />
221
231
  </label>
222
232
  <label className="space-y-1">
223
- <span className="text-sm font-medium">Sort Order</span>
233
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.sortOrder', 'Sort Order')}</span>
224
234
  <NumberInput
225
235
  integer
226
236
  value={form.sortOrder === '' ? null : Number(form.sortOrder)}
@@ -228,15 +238,15 @@ export function PriceRuleForm({
228
238
  />
229
239
  </label>
230
240
  <label className="space-y-1">
231
- <span className="text-sm font-medium">Description</span>
241
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.description', 'Description')}</span>
232
242
  <Input
233
- placeholder="Optional"
243
+ placeholder={t('cpq.priceRules.form.descriptionPlaceholder', 'Optional')}
234
244
  value={form.description}
235
245
  onChange={(e) => onFormChange({ ...form, description: e.target.value })}
236
246
  />
237
247
  </label>
238
248
  <label className="space-y-1 col-span-2">
239
- <span className="text-sm font-medium">Product Offering</span>
249
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.productOffering', 'Product Offering')}</span>
240
250
  <Select
241
251
  value={form.productOfferingId || '__global__'}
242
252
  onValueChange={(value) =>
@@ -247,7 +257,7 @@ export function PriceRuleForm({
247
257
  <SelectValue />
248
258
  </SelectTrigger>
249
259
  <SelectContent>
250
- <SelectItem value="__global__">Global (all products)</SelectItem>
260
+ <SelectItem value="__global__">{t('cpq.priceRules.form.globalAllProducts', 'Global (all products)')}</SelectItem>
251
261
  {offerings.map((o) => (
252
262
  <SelectItem key={o.id} value={o.id}>
253
263
  {o.name} ({o.code})
@@ -256,17 +266,17 @@ export function PriceRuleForm({
256
266
  </SelectContent>
257
267
  </Select>
258
268
  <span className="text-xs text-muted-foreground">
259
- Leave as &quot;Global&quot; to apply to all products, or select a specific offering
269
+ {t('cpq.priceRules.form.productOfferingHint', 'Leave as "Global" to apply to all products, or select a specific offering')}
260
270
  </span>
261
271
  </label>
262
272
  <div className="col-span-2 space-y-1">
263
- <span className="text-sm font-medium">Applicability Condition</span>
273
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.applicabilityCondition', 'Applicability Condition')}</span>
264
274
  <span className="ml-2 text-xs text-muted-foreground">
265
- (optional — only apply when a product attribute matches)
275
+ {t('cpq.priceRules.form.applicabilityConditionHint', '(optional — only apply when a product attribute matches)')}
266
276
  </span>
267
277
  <div className="grid grid-cols-3 gap-2">
268
278
  <Input
269
- placeholder="Attribute (e.g. port_size)"
279
+ placeholder={t('cpq.priceRules.form.conditionAttributePlaceholder', 'Attribute (e.g. port_size)')}
270
280
  value={form.conditionAttribute}
271
281
  onChange={(e) => onFormChange({ ...form, conditionAttribute: e.target.value })}
272
282
  />
@@ -278,12 +288,12 @@ export function PriceRuleForm({
278
288
  <SelectValue />
279
289
  </SelectTrigger>
280
290
  <SelectContent>
281
- <SelectItem value="eq">equals (=)</SelectItem>
282
- <SelectItem value="neq">not equals (≠)</SelectItem>
291
+ <SelectItem value="eq">{t('cpq.priceRules.form.operatorEq', 'equals (=)')}</SelectItem>
292
+ <SelectItem value="neq">{t('cpq.priceRules.form.operatorNeq', 'not equals (≠)')}</SelectItem>
283
293
  </SelectContent>
284
294
  </Select>
285
295
  <Input
286
- placeholder="Value (e.g. 100G)"
296
+ placeholder={t('cpq.priceRules.form.conditionValuePlaceholder', 'Value (e.g. 100G)')}
287
297
  value={form.conditionValue}
288
298
  onChange={(e) => onFormChange({ ...form, conditionValue: e.target.value })}
289
299
  />
@@ -295,14 +305,18 @@ export function PriceRuleForm({
295
305
  checked={form.isActive}
296
306
  onCheckedChange={(checked) => onFormChange({ ...form, isActive: checked === true })}
297
307
  />
298
- Active
308
+ {t('cpq.common.active', 'Active')}
299
309
  </label>
300
310
  <div className="flex gap-2">
301
311
  <Button type="button" onClick={onSave} disabled={saving}>
302
- {saving ? 'Saving...' : editingId ? 'Update' : 'Create'}
312
+ {saving
313
+ ? t('cpq.common.saving', 'Saving...')
314
+ : editingId
315
+ ? t('cpq.priceRules.form.update', 'Update')
316
+ : t('cpq.priceRules.form.create', 'Create')}
303
317
  </Button>
304
318
  <Button type="button" variant="outline" onClick={onCancel}>
305
- Cancel
319
+ {t('cpq.common.cancel', 'Cancel')}
306
320
  </Button>
307
321
  </div>
308
322
  </div>
@@ -4,6 +4,6 @@ export const metadata = {
4
4
  navHidden: true,
5
5
  breadcrumb: [
6
6
  { label: 'Price Rules', labelKey: 'cpq.priceRules.list.title', href: '/backend/cpq/price-rules' },
7
- { label: 'Rule Detail' },
7
+ { label: 'Rule Detail', labelKey: 'cpq.priceRules.detail.breadcrumb' },
8
8
  ],
9
9
  } as const
@@ -3,15 +3,9 @@ import * as React from 'react'
3
3
  import { useRouter, useParams } from 'next/navigation'
4
4
  import { Button } from '@open-mercato/ui/primitives/button'
5
5
  import { Tag } from '@open-mercato/ui/primitives/tag'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
7
  import { NumberInput } from '../../../../components/NumberInput'
7
-
8
- const RULE_TYPE_LABELS: Record<string, string> = {
9
- discount_percent: 'Discount %',
10
- discount_absolute: 'Discount $',
11
- surcharge_percent: 'Surcharge %',
12
- surcharge_absolute: 'Surcharge $',
13
- price_override: 'Override',
14
- }
8
+ import { buildRuleTypeLabels } from '../PriceRuleForm'
15
9
 
16
10
  type PriceRule = {
17
11
  id: string
@@ -66,6 +60,8 @@ function ruleToForm(rule: PriceRule): FormData {
66
60
  }
67
61
 
68
62
  export default function PriceRuleDetailPage(props: { params?: { id?: string } }) {
63
+ const t = useT()
64
+ const ruleTypeLabels = React.useMemo(() => buildRuleTypeLabels(t), [t])
69
65
  const router = useRouter()
70
66
  const urlParams = useParams()
71
67
  const ruleId = (props.params?.id ?? urlParams?.id) as string
@@ -102,7 +98,7 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
102
98
  setError(null)
103
99
  fetch(`/api/cpq/price-rules?id=${ruleId}`)
104
100
  .then(async (r) => {
105
- if (!r.ok) throw new Error('Not found')
101
+ if (!r.ok) throw new Error(t('cpq.priceRules.detail.notFound', 'Not found'))
106
102
  return r.json()
107
103
  })
108
104
  .then((data: PriceRule) => {
@@ -110,7 +106,7 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
110
106
  })
111
107
  .catch((err) => setError(err.message))
112
108
  .finally(() => setLoading(false))
113
- }, [ruleId])
109
+ }, [ruleId, t])
114
110
 
115
111
  React.useEffect(() => { loadRule() }, [loadRule])
116
112
 
@@ -164,14 +160,14 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
164
160
  })
165
161
  if (!res.ok) {
166
162
  const err = await res.json()
167
- throw new Error(err.error ?? 'Failed to update')
163
+ throw new Error(err.error ?? t('cpq.priceRules.errors.updateFailed', 'Failed to update'))
168
164
  }
169
165
  const updated = await res.json()
170
166
  setRule(updated)
171
167
  setEditing(false)
172
168
  setForm(null)
173
169
  } catch (err: unknown) {
174
- setSaveError(err instanceof Error ? err.message : 'Unknown error')
170
+ setSaveError(err instanceof Error ? err.message : t('cpq.priceRules.errors.unknown', 'Unknown error'))
175
171
  } finally {
176
172
  setSaving(false)
177
173
  }
@@ -205,18 +201,18 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
205
201
  }
206
202
 
207
203
  if (loading) {
208
- return <div className="text-sm text-muted-foreground py-12 text-center">Loading...</div>
204
+ return <div className="text-sm text-muted-foreground py-12 text-center">{t('cpq.common.loading', 'Loading...')}</div>
209
205
  }
210
206
 
211
207
  if (error || !rule) {
212
208
  return (
213
209
  <div className="space-y-4 py-12 text-center">
214
- <div className="text-sm text-destructive">{error ?? 'Rule not found'}</div>
210
+ <div className="text-sm text-destructive">{error ?? t('cpq.priceRules.detail.ruleNotFound', 'Rule not found')}</div>
215
211
  <button
216
212
  onClick={() => router.push('/backend/cpq/price-rules')}
217
213
  className="text-sm text-primary hover:underline"
218
214
  >
219
- &larr; Back to Price Rules
215
+ ← {t('cpq.priceRules.detail.backToList', 'Back to Price Rules')}
220
216
  </button>
221
217
  </div>
222
218
  )
@@ -224,6 +220,8 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
224
220
 
225
221
  const cond = rule.applicabilityCondition as { attribute?: string; operator?: string; value?: string } | null
226
222
  const offering = rule.productOfferingId ? offeringLookup.get(rule.productOfferingId) : null
223
+ // `{name}` is left unresolved so the rule name can be rendered in <strong>.
224
+ const deleteConfirmParts = t('cpq.priceRules.detail.deleteConfirm', 'Are you sure you want to delete {name}?').split('{name}')
227
225
 
228
226
  return (
229
227
  <div className="space-y-6">
@@ -234,30 +232,32 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
234
232
  onClick={() => router.push('/backend/cpq/price-rules')}
235
233
  className="text-sm text-muted-foreground hover:text-foreground transition-colors"
236
234
  >
237
- &larr; Back
235
+ ← {t('cpq.common.back', 'Back')}
238
236
  </button>
239
237
  <h1 className="text-2xl font-bold">{rule.name}</h1>
240
238
  <Tag variant={rule.isActive ? 'success' : 'neutral'} dot>
241
- {rule.isActive ? 'Active' : 'Inactive'}
239
+ {rule.isActive ? t('cpq.common.active', 'Active') : t('cpq.common.inactive', 'Inactive')}
242
240
  </Tag>
243
241
  </div>
244
242
  <div className="flex items-center gap-2">
245
243
  <Button type="button" variant="outline" onClick={handleToggleActive}>
246
- {rule.isActive ? 'Deactivate' : 'Activate'}
244
+ {rule.isActive
245
+ ? t('cpq.priceRules.detail.deactivate', 'Deactivate')
246
+ : t('cpq.priceRules.detail.activate', 'Activate')}
247
247
  </Button>
248
248
  {!editing && (
249
249
  <button
250
250
  onClick={startEdit}
251
251
  className="rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
252
252
  >
253
- Edit
253
+ {t('cpq.common.edit', 'Edit')}
254
254
  </button>
255
255
  )}
256
256
  <button
257
257
  onClick={() => setShowDeleteConfirm(true)}
258
258
  className="rounded-md border border-destructive/30 px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10 transition-colors"
259
259
  >
260
- Delete
260
+ {t('cpq.common.delete', 'Delete')}
261
261
  </button>
262
262
  </div>
263
263
  </div>
@@ -265,20 +265,26 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
265
265
  {/* Delete confirmation */}
266
266
  {showDeleteConfirm && (
267
267
  <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 flex items-center justify-between">
268
- <span className="text-sm">Are you sure you want to delete <strong>{rule.name}</strong>?</span>
268
+ <span className="text-sm">
269
+ {deleteConfirmParts[0]}
270
+ <strong>{rule.name}</strong>
271
+ {deleteConfirmParts[1]}
272
+ </span>
269
273
  <div className="flex gap-2">
270
274
  <button
271
275
  onClick={handleDelete}
272
276
  disabled={deleting}
273
277
  className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50"
274
278
  >
275
- {deleting ? 'Deleting...' : 'Yes, Delete'}
279
+ {deleting
280
+ ? t('cpq.priceRules.detail.deleting', 'Deleting...')
281
+ : t('cpq.priceRules.detail.confirmDelete', 'Yes, Delete')}
276
282
  </button>
277
283
  <button
278
284
  onClick={() => setShowDeleteConfirm(false)}
279
285
  className="rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted/50"
280
286
  >
281
- Cancel
287
+ {t('cpq.common.cancel', 'Cancel')}
282
288
  </button>
283
289
  </div>
284
290
  </div>
@@ -287,32 +293,32 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
287
293
  {/* Edit mode */}
288
294
  {editing && form ? (
289
295
  <div className="rounded-lg border bg-card p-6 space-y-4">
290
- <h2 className="text-lg font-semibold">Edit Rule</h2>
296
+ <h2 className="text-lg font-semibold">{t('cpq.priceRules.form.editTitle', 'Edit Rule')}</h2>
291
297
  {saveError && <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{saveError}</div>}
292
298
  <div className="grid grid-cols-2 gap-4">
293
299
  <label className="space-y-1">
294
- <span className="text-sm font-medium">Code</span>
300
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.code', 'Code')}</span>
295
301
  <input className="w-full rounded-md border bg-muted/30 px-3 py-2 text-sm" value={rule.code} disabled />
296
302
  </label>
297
303
  <label className="space-y-1">
298
- <span className="text-sm font-medium">Name</span>
304
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.name', 'Name')}</span>
299
305
  <input className="w-full rounded-md border px-3 py-2 text-sm" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
300
306
  </label>
301
307
  <label className="space-y-1">
302
- <span className="text-sm font-medium">Rule Type</span>
308
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.ruleType', 'Rule Type')}</span>
303
309
  <select className="w-full rounded-md border px-3 py-2 text-sm" value={form.ruleType} onChange={(e) => setForm({ ...form, ruleType: e.target.value })}>
304
- {Object.entries(RULE_TYPE_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
310
+ {Object.entries(ruleTypeLabels).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
305
311
  </select>
306
312
  </label>
307
313
  <label className="space-y-1">
308
- <span className="text-sm font-medium">Value</span>
314
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.value', 'Value')}</span>
309
315
  <NumberInput
310
316
  value={form.value === '' ? null : Number(form.value)}
311
317
  onChange={(n) => setForm({ ...form, value: n == null ? '' : String(n) })}
312
318
  />
313
319
  </label>
314
320
  <label className="space-y-1">
315
- <span className="text-sm font-medium">Sort Order</span>
321
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.sortOrder', 'Sort Order')}</span>
316
322
  <NumberInput
317
323
  integer
318
324
  value={form.sortOrder === '' ? null : Number(form.sortOrder)}
@@ -320,52 +326,52 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
320
326
  />
321
327
  </label>
322
328
  <label className="space-y-1">
323
- <span className="text-sm font-medium">Description</span>
324
- <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder="Optional" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
329
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.description', 'Description')}</span>
330
+ <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.descriptionPlaceholder', 'Optional')} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
325
331
  </label>
326
332
  <label className="space-y-1">
327
- <span className="text-sm font-medium">Charge Type Filter</span>
333
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.chargeTypeFilter', 'Charge Type Filter')}</span>
328
334
  <select className="w-full rounded-md border px-3 py-2 text-sm" value={form.chargeTypeFilter} onChange={(e) => setForm({ ...form, chargeTypeFilter: e.target.value })}>
329
- <option value="">All charge types</option>
330
- <option value="nrc">NRC only</option>
331
- <option value="mrc">MRC only</option>
332
- <option value="usage">Usage only</option>
335
+ <option value="">{t('cpq.priceRules.form.allChargeTypes', 'All charge types')}</option>
336
+ <option value="nrc">{t('cpq.priceRules.form.nrcOnly', 'NRC only')}</option>
337
+ <option value="mrc">{t('cpq.priceRules.form.mrcOnly', 'MRC only')}</option>
338
+ <option value="usage">{t('cpq.priceRules.form.usageOnly', 'Usage only')}</option>
333
339
  </select>
334
340
  </label>
335
341
  <label className="space-y-1">
336
- <span className="text-sm font-medium">Charge Code Filter</span>
337
- <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder="e.g. setup_fee (leave empty for all)" value={form.chargeCodeFilter} onChange={(e) => setForm({ ...form, chargeCodeFilter: e.target.value })} />
342
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.chargeCodeFilter', 'Charge Code Filter')}</span>
343
+ <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.chargeCodeFilterPlaceholder', 'e.g. setup_fee (leave empty for all)')} value={form.chargeCodeFilter} onChange={(e) => setForm({ ...form, chargeCodeFilter: e.target.value })} />
338
344
  </label>
339
345
  <label className="space-y-1 col-span-2">
340
- <span className="text-sm font-medium">Product Offering</span>
346
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.productOffering', 'Product Offering')}</span>
341
347
  <select className="w-full rounded-md border px-3 py-2 text-sm" value={form.productOfferingId} onChange={(e) => setForm({ ...form, productOfferingId: e.target.value })}>
342
- <option value="">Global (all products)</option>
348
+ <option value="">{t('cpq.priceRules.form.globalAllProducts', 'Global (all products)')}</option>
343
349
  {offerings.map((o) => <option key={o.id} value={o.id}>{o.name} ({o.code})</option>)}
344
350
  </select>
345
351
  </label>
346
352
  <div className="col-span-2 space-y-1">
347
- <span className="text-sm font-medium">Applicability Condition</span>
348
- <span className="ml-2 text-xs text-muted-foreground">(optional)</span>
353
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.applicabilityCondition', 'Applicability Condition')}</span>
354
+ <span className="ml-2 text-xs text-muted-foreground">{t('cpq.priceRules.form.optionalHint', '(optional)')}</span>
349
355
  <div className="grid grid-cols-3 gap-2">
350
- <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder="Attribute (e.g. port_size)" value={form.conditionAttribute} onChange={(e) => setForm({ ...form, conditionAttribute: e.target.value })} />
356
+ <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.conditionAttributePlaceholder', 'Attribute (e.g. port_size)')} value={form.conditionAttribute} onChange={(e) => setForm({ ...form, conditionAttribute: e.target.value })} />
351
357
  <select className="w-full rounded-md border px-3 py-2 text-sm" value={form.conditionOperator} onChange={(e) => setForm({ ...form, conditionOperator: e.target.value })}>
352
- <option value="eq">equals (=)</option>
353
- <option value="neq">not equals (&#8800;)</option>
358
+ <option value="eq">{t('cpq.priceRules.form.operatorEq', 'equals (=)')}</option>
359
+ <option value="neq">{t('cpq.priceRules.form.operatorNeq', 'not equals (≠)')}</option>
354
360
  </select>
355
- <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder="Value" value={form.conditionValue} onChange={(e) => setForm({ ...form, conditionValue: e.target.value })} />
361
+ <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.conditionValuePlaceholderShort', 'Value')} value={form.conditionValue} onChange={(e) => setForm({ ...form, conditionValue: e.target.value })} />
356
362
  </div>
357
363
  </div>
358
364
  </div>
359
365
  <label className="flex items-center gap-2 text-sm">
360
366
  <input type="checkbox" checked={form.isActive} onChange={(e) => setForm({ ...form, isActive: e.target.checked })} />
361
- Active
367
+ {t('cpq.common.active', 'Active')}
362
368
  </label>
363
369
  <div className="flex gap-2">
364
370
  <button onClick={handleSave} disabled={saving} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">
365
- {saving ? 'Saving...' : 'Save Changes'}
371
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.priceRules.detail.saveChanges', 'Save Changes')}
366
372
  </button>
367
373
  <button onClick={cancelEdit} className="rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted/50">
368
- Cancel
374
+ {t('cpq.common.cancel', 'Cancel')}
369
375
  </button>
370
376
  </div>
371
377
  </div>
@@ -374,29 +380,29 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
374
380
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
375
381
  {/* Main info */}
376
382
  <div className="rounded-lg border bg-card p-6 space-y-4">
377
- <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Rule Configuration</h2>
383
+ <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.ruleConfiguration', 'Rule Configuration')}</h2>
378
384
  <dl className="space-y-3">
379
385
  <div className="flex justify-between">
380
- <dt className="text-sm text-muted-foreground">Code</dt>
386
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.code', 'Code')}</dt>
381
387
  <dd className="text-sm font-mono">{rule.code}</dd>
382
388
  </div>
383
389
  <div className="flex justify-between border-t pt-3">
384
- <dt className="text-sm text-muted-foreground">Rule Type</dt>
385
- <dd className="text-sm font-medium">{RULE_TYPE_LABELS[rule.ruleType] ?? rule.ruleType}</dd>
390
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.ruleType', 'Rule Type')}</dt>
391
+ <dd className="text-sm font-medium">{ruleTypeLabels[rule.ruleType] ?? rule.ruleType}</dd>
386
392
  </div>
387
393
  <div className="flex justify-between border-t pt-3">
388
- <dt className="text-sm text-muted-foreground">Value</dt>
394
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.value', 'Value')}</dt>
389
395
  <dd className="text-sm font-mono font-medium">
390
396
  {rule.ruleType.includes('percent') ? `${rule.value}%` : rule.ruleType === 'price_override' ? `= $${rule.value}` : `$${rule.value}`}
391
397
  </dd>
392
398
  </div>
393
399
  <div className="flex justify-between border-t pt-3">
394
- <dt className="text-sm text-muted-foreground">Sort Order</dt>
400
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.sortOrder', 'Sort Order')}</dt>
395
401
  <dd className="text-sm font-mono">{rule.sortOrder}</dd>
396
402
  </div>
397
403
  {rule.description && (
398
404
  <div className="border-t pt-3">
399
- <dt className="text-sm text-muted-foreground mb-1">Description</dt>
405
+ <dt className="text-sm text-muted-foreground mb-1">{t('cpq.priceRules.detail.description', 'Description')}</dt>
400
406
  <dd className="text-sm">{rule.description}</dd>
401
407
  </div>
402
408
  )}
@@ -405,10 +411,10 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
405
411
 
406
412
  {/* Scope & Filters */}
407
413
  <div className="rounded-lg border bg-card p-6 space-y-4">
408
- <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Scope &amp; Filters</h2>
414
+ <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.scopeAndFilters', 'Scope & Filters')}</h2>
409
415
  <dl className="space-y-3">
410
416
  <div className="flex justify-between">
411
- <dt className="text-sm text-muted-foreground">Scope</dt>
417
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.scope', 'Scope')}</dt>
412
418
  <dd className="text-sm">
413
419
  {offering ? (
414
420
  <span className="inline-flex items-center gap-1.5">
@@ -419,28 +425,28 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
419
425
  ) : (
420
426
  <span className="inline-flex items-center gap-1.5">
421
427
  <span className="inline-block w-2 h-2 rounded-full bg-muted-foreground" />
422
- Global (all offerings)
428
+ {t('cpq.priceRules.detail.globalAllOfferings', 'Global (all offerings)')}
423
429
  </span>
424
430
  )}
425
431
  </dd>
426
432
  </div>
427
433
  <div className="flex justify-between border-t pt-3">
428
- <dt className="text-sm text-muted-foreground">Charge Type</dt>
429
- <dd className="text-sm">{rule.chargeTypeFilter ? rule.chargeTypeFilter.toUpperCase() : 'All types'}</dd>
434
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.chargeType', 'Charge Type')}</dt>
435
+ <dd className="text-sm">{rule.chargeTypeFilter ? rule.chargeTypeFilter.toUpperCase() : t('cpq.priceRules.detail.allTypes', 'All types')}</dd>
430
436
  </div>
431
437
  <div className="flex justify-between border-t pt-3">
432
- <dt className="text-sm text-muted-foreground">Charge Code</dt>
433
- <dd className="text-sm font-mono">{rule.chargeCodeFilter ?? 'All charges'}</dd>
438
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.chargeCode', 'Charge Code')}</dt>
439
+ <dd className="text-sm font-mono">{rule.chargeCodeFilter ?? t('cpq.priceRules.detail.allCharges', 'All charges')}</dd>
434
440
  </div>
435
441
  <div className="border-t pt-3">
436
- <dt className="text-sm text-muted-foreground mb-1">Applicability Condition</dt>
442
+ <dt className="text-sm text-muted-foreground mb-1">{t('cpq.priceRules.detail.applicabilityCondition', 'Applicability Condition')}</dt>
437
443
  <dd className="text-sm">
438
444
  {cond?.attribute ? (
439
445
  <code className="rounded bg-muted px-2 py-0.5 text-xs">
440
446
  {cond.attribute} {cond.operator === 'neq' ? '≠' : '='} {cond.value ?? '""'}
441
447
  </code>
442
448
  ) : (
443
- <span className="text-muted-foreground">No condition (always applies)</span>
449
+ <span className="text-muted-foreground">{t('cpq.priceRules.detail.noCondition', 'No condition (always applies)')}</span>
444
450
  )}
445
451
  </dd>
446
452
  </div>
@@ -449,18 +455,18 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
449
455
 
450
456
  {/* Metadata */}
451
457
  <div className="rounded-lg border bg-card p-6 space-y-4 lg:col-span-2">
452
- <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Metadata</h2>
458
+ <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.metadata', 'Metadata')}</h2>
453
459
  <dl className="flex gap-8 text-sm">
454
460
  <div>
455
- <dt className="text-muted-foreground">Created</dt>
461
+ <dt className="text-muted-foreground">{t('cpq.priceRules.detail.created', 'Created')}</dt>
456
462
  <dd>{new Date(rule.createdAt).toLocaleString()}</dd>
457
463
  </div>
458
464
  <div>
459
- <dt className="text-muted-foreground">Updated</dt>
465
+ <dt className="text-muted-foreground">{t('cpq.priceRules.detail.updated', 'Updated')}</dt>
460
466
  <dd>{new Date(rule.updatedAt).toLocaleString()}</dd>
461
467
  </div>
462
468
  <div>
463
- <dt className="text-muted-foreground">ID</dt>
469
+ <dt className="text-muted-foreground">{t('cpq.priceRules.detail.id', 'ID')}</dt>
464
470
  <dd className="font-mono text-xs">{rule.id}</dd>
465
471
  </div>
466
472
  </dl>