@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
@@ -1,18 +1,19 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
3
  import { useRouter, useParams } from 'next/navigation'
4
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
4
5
  import { Alert } from '@open-mercato/ui/primitives/alert'
5
6
  import { Button } from '@open-mercato/ui/primitives/button'
6
7
  import { Tag } from '@open-mercato/ui/primitives/tag'
7
8
  import { FormHeader } from '@open-mercato/ui/backend/forms'
8
9
  import {
9
- formatStatusLabel,
10
10
  subscriptionStatusMap,
11
11
  type SubscriptionStatus,
12
12
  } from '../../../../../components/statusMaps'
13
13
  import { StatusPath } from '../../../../../components/StatusPath'
14
14
  import { DepthBadge, PriceWaterfall } from '../../../../../components/PriceWaterfall'
15
15
  import { parseStoredCharge } from '../../../../../lib/priceAdjustmentView'
16
+ import { buildInventoryStatusLabels, inventoryStatusLabel } from '../../shared'
16
17
 
17
18
  // XD-250 — ARC integration on subscription detail.
18
19
  // • header buttons (Amend / Renew / Cancel) call /from-subscription
@@ -64,13 +65,16 @@ type ChangeLogRow = {
64
65
  mergedFromSubscriptionIds: string[] | null
65
66
  }
66
67
 
67
- const CHANGE_TYPE_LABELS: Record<string, string> = {
68
- created: 'Created',
69
- amend: 'Amended',
70
- renew: 'Renewed',
71
- cancel: 'Cancelled',
72
- 'merge-result': 'Created from merging contracts',
73
- 'merge-source': 'Merged into a new contract',
68
+ function changeTypeLabel(t: TranslateFn, changeType: string): string {
69
+ switch (changeType) {
70
+ case 'created': return t('cpq.inventory.subscriptions.detail.changeType.created', 'Created')
71
+ case 'amend': return t('cpq.inventory.subscriptions.detail.changeType.amend', 'Amended')
72
+ case 'renew': return t('cpq.inventory.subscriptions.detail.changeType.renew', 'Renewed')
73
+ case 'cancel': return t('cpq.inventory.subscriptions.detail.changeType.cancel', 'Cancelled')
74
+ case 'merge-result': return t('cpq.inventory.subscriptions.detail.changeType.mergeResult', 'Created from merging contracts')
75
+ case 'merge-source': return t('cpq.inventory.subscriptions.detail.changeType.mergeSource', 'Merged into a new contract')
76
+ default: return changeType
77
+ }
74
78
  }
75
79
 
76
80
  const CHANGE_TYPE_VARIANTS: Record<string, 'info' | 'success' | 'error' | 'brand' | 'warning' | 'neutral'> = {
@@ -156,27 +160,23 @@ const ALLOWED_TRANSITIONS: Record<string, string[]> = {
156
160
  expired: [],
157
161
  }
158
162
 
159
- const STATUS_LABELS: Record<string, string> = {
160
- pending: 'Pending',
161
- active: 'Active',
162
- suspended: 'Suspended',
163
- terminated: 'Terminated',
164
- expired: 'Expired',
165
- }
163
+ const SUBSCRIPTION_STATUSES = ['pending', 'active', 'suspended', 'terminated', 'expired'] as const
166
164
 
167
165
  function fmt(amount: number | string, currency: string): string {
168
166
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(Number(amount))
169
167
  }
170
168
 
171
169
  function StatusBadge({ status }: { status: string }) {
170
+ const t = useT()
172
171
  return (
173
172
  <Tag variant={subscriptionStatusMap[status as SubscriptionStatus] ?? 'neutral'} dot>
174
- {formatStatusLabel(status)}
173
+ {inventoryStatusLabel(t, status)}
175
174
  </Tag>
176
175
  )
177
176
  }
178
177
 
179
178
  export default function SubscriptionDetailPage(props: { params?: { id?: string } }) {
179
+ const t = useT()
180
180
  const router = useRouter()
181
181
  const urlParams = useParams()
182
182
  const subId = (props.params?.id ?? urlParams?.id) as string
@@ -220,15 +220,16 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
220
220
  // Offer to navigate to that quote instead of showing a dead alert.
221
221
  if (body.existingQuoteId) {
222
222
  const ok = window.confirm(
223
- `${body.error ?? 'Subscription has another ARC quote in progress.'}\n\n` +
224
- `Open the existing quote now?`,
223
+ t('cpq.inventory.subscriptions.detail.arc.existingQuoteConfirm', '{error}\n\nOpen the existing quote now?', {
224
+ error: body.error ?? t('cpq.inventory.subscriptions.detail.arc.existingQuoteInProgress', 'Subscription has another ARC quote in progress.'),
225
+ }),
225
226
  )
226
227
  if (ok) {
227
228
  router.push(`/backend/cpq/quotes/${body.existingQuoteId}`)
228
229
  }
229
230
  return
230
231
  }
231
- alert(`Failed to start ${type} quote: ${body.error ?? res.statusText}`)
232
+ alert(t('cpq.inventory.subscriptions.detail.arc.startFailed', 'Failed to start {type} quote: {error}', { type, error: body.error ?? res.statusText }))
232
233
  return
233
234
  }
234
235
  const data = await res.json()
@@ -237,7 +238,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
237
238
  setArcInFlight(null)
238
239
  }
239
240
  },
240
- [router, sub],
241
+ [router, sub, t],
241
242
  )
242
243
 
243
244
  const transitionStatus = async (targetStatus: string) => {
@@ -251,7 +252,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
251
252
  body: JSON.stringify({ id: sub.id, targetStatus }),
252
253
  })
253
254
  const data = await res.json()
254
- if (!res.ok) throw new Error(data?.error ?? 'Status transition failed')
255
+ if (!res.ok) throw new Error(data?.error ?? t('cpq.inventory.subscriptions.detail.error.transitionFailed', 'Status transition failed'))
255
256
  // The status endpoint cascades to items and linked assets server-side,
256
257
  // but only returns the subscription scalar fields. Re-fetch the full
257
258
  // detail (which includes the item tree) and the asset list so the UI
@@ -324,7 +325,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
324
325
  async function load() {
325
326
  try {
326
327
  const res = await fetch(`/api/cpq/inventory/subscriptions?id=${subId}`)
327
- if (!res.ok) throw new Error('Subscription not found')
328
+ if (!res.ok) throw new Error(t('cpq.inventory.subscriptions.detail.error.notFound', 'Subscription not found'))
328
329
  const data: SubscriptionDetail = await res.json()
329
330
  if (!cancelled) {
330
331
  setSub(data)
@@ -411,7 +412,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
411
412
  }
412
413
  load()
413
414
  return () => { cancelled = true }
414
- }, [subId])
415
+ }, [subId, t])
415
416
 
416
417
  const toggleItem = (id: string) => {
417
418
  setExpandedItems((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n })
@@ -420,7 +421,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
420
421
  if (loading) {
421
422
  return (
422
423
  <div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
423
- <Spinner /> Loading…
424
+ <Spinner /> {t('cpq.inventory.subscriptions.detail.loading', 'Loading…')}
424
425
  </div>
425
426
  )
426
427
  }
@@ -428,9 +429,9 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
428
429
  if (!sub) {
429
430
  return (
430
431
  <div className="space-y-4">
431
- <button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← Back</button>
432
+ <button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← {t('cpq.common.back', 'Back')}</button>
432
433
  {error && <ErrorBanner message={error} />}
433
- <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Subscription not found.</div>
434
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.notFound', 'Subscription not found.')}</div>
434
435
  </div>
435
436
  )
436
437
  }
@@ -440,6 +441,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
440
441
 
441
442
  const canArc = sub.status === 'active' || sub.status === 'suspended'
442
443
  const statusTransitions = ALLOWED_TRANSITIONS[sub.status] ?? []
444
+ const statusLabels = buildInventoryStatusLabels(t, SUBSCRIPTION_STATUSES)
443
445
 
444
446
  return (
445
447
  <div className="space-y-6">
@@ -452,13 +454,13 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
452
454
  <FormHeader
453
455
  mode="detail"
454
456
  backHref="/backend/cpq/inventory"
455
- backLabel="Back to Customer Inventory"
456
- entityTypeLabel="Subscription"
457
+ backLabel={t('cpq.inventory.detail.backToInventory', 'Back to Customer Inventory')}
458
+ entityTypeLabel={t('cpq.inventory.subscriptions.detail.entityType', 'Subscription')}
457
459
  title={sub.name}
458
460
  statusBadge={
459
461
  <div className="flex flex-wrap items-center gap-2 pt-1">
460
462
  <Tag variant={subscriptionStatusMap[sub.status as SubscriptionStatus] ?? 'neutral'} dot>
461
- {STATUS_LABELS[sub.status] ?? formatStatusLabel(sub.status)}
463
+ {inventoryStatusLabel(t, sub.status)}
462
464
  </Tag>
463
465
  <Tag variant="neutral" className="capitalize">{sub.billingCycle}</Tag>
464
466
  {transitioning && <Spinner />}
@@ -473,7 +475,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
473
475
  disabled={arcInFlight !== null}
474
476
  onClick={() => startArcQuote('amend')}
475
477
  >
476
- Amend
478
+ {t('cpq.inventory.subscriptions.detail.arc.amend', 'Amend')}
477
479
  </Button>
478
480
  <Button
479
481
  type="button"
@@ -481,7 +483,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
481
483
  disabled={arcInFlight !== null}
482
484
  onClick={() => startArcQuote('renew')}
483
485
  >
484
- Renew
486
+ {t('cpq.inventory.subscriptions.detail.arc.renew', 'Renew')}
485
487
  </Button>
486
488
  <Button
487
489
  type="button"
@@ -489,7 +491,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
489
491
  disabled={arcInFlight !== null}
490
492
  onClick={() => startArcQuote('cancel')}
491
493
  >
492
- Cancel
494
+ {t('cpq.inventory.subscriptions.detail.arc.cancel', 'Cancel')}
493
495
  </Button>
494
496
  </>
495
497
  ) : null
@@ -504,11 +506,11 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
504
506
  path={['pending', 'active']}
505
507
  terminals={['suspended', 'terminated', 'expired']}
506
508
  statusMap={subscriptionStatusMap}
507
- labels={STATUS_LABELS}
509
+ labels={statusLabels}
508
510
  onTransition={transitionStatus}
509
511
  allowedTransitions={statusTransitions}
510
512
  disabled={transitioning}
511
- ariaLabel="Subscription status path"
513
+ ariaLabel={t('cpq.inventory.subscriptions.detail.statusPath', 'Subscription status path')}
512
514
  />
513
515
  </div>
514
516
 
@@ -517,46 +519,46 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
517
519
  {/* Detail Cards */}
518
520
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
519
521
  <div className="rounded-lg border bg-card p-4 space-y-3">
520
- <h3 className="text-sm font-medium">Subscription Details</h3>
522
+ <h3 className="text-sm font-medium">{t('cpq.inventory.subscriptions.detail.sections.details', 'Subscription Details')}</h3>
521
523
  <div className="grid grid-cols-2 gap-x-4 gap-y-2">
522
- <Field label="Code" value={sub.code} mono />
523
- <Field label="Status" value={sub.status} />
524
- <Field label="Billing Cycle" value={sub.billingCycle} />
525
- <Field label="Currency" value={currency} />
526
- <Field label="Auto-Renew" value={sub.autoRenew ? 'Yes' : 'No'} />
527
- {sub.termMonths != null && <Field label="Term" value={`${sub.termMonths} months`} />}
528
- {sub.startDate && <Field label="Start Date" value={new Date(sub.startDate).toLocaleDateString()} />}
529
- {sub.currentTermEnd && <Field label="Term End" value={new Date(sub.currentTermEnd).toLocaleDateString()} />}
530
- {sub.activatedAt && <Field label="Activated" value={new Date(sub.activatedAt).toLocaleString()} />}
531
- {sub.suspendedAt && <Field label="Suspended" value={new Date(sub.suspendedAt).toLocaleString()} />}
532
- {sub.terminatedAt && <Field label="Terminated" value={new Date(sub.terminatedAt).toLocaleString()} />}
524
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.code', 'Code')} value={sub.code} mono />
525
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.status', 'Status')} value={sub.status} />
526
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.billingCycle', 'Billing Cycle')} value={sub.billingCycle} />
527
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.currency', 'Currency')} value={currency} />
528
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.autoRenew', 'Auto-Renew')} value={sub.autoRenew ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')} />
529
+ {sub.termMonths != null && <Field label={t('cpq.inventory.subscriptions.detail.fields.term', 'Term')} value={t('cpq.inventory.subscriptions.detail.fields.termMonths', '{count} months', { count: sub.termMonths })} />}
530
+ {sub.startDate && <Field label={t('cpq.inventory.subscriptions.detail.fields.startDate', 'Start Date')} value={new Date(sub.startDate).toLocaleDateString()} />}
531
+ {sub.currentTermEnd && <Field label={t('cpq.inventory.subscriptions.detail.fields.termEnd', 'Term End')} value={new Date(sub.currentTermEnd).toLocaleDateString()} />}
532
+ {sub.activatedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.activated', 'Activated')} value={new Date(sub.activatedAt).toLocaleString()} />}
533
+ {sub.suspendedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.suspended', 'Suspended')} value={new Date(sub.suspendedAt).toLocaleString()} />}
534
+ {sub.terminatedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.terminated', 'Terminated')} value={new Date(sub.terminatedAt).toLocaleString()} />}
533
535
  </div>
534
536
  {sub.description && (
535
537
  <div className="pt-1">
536
- <p className="text-xs text-muted-foreground">Description</p>
538
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.description', 'Description')}</p>
537
539
  <p className="text-sm">{sub.description}</p>
538
540
  </div>
539
541
  )}
540
542
  </div>
541
543
 
542
544
  <div className="rounded-lg border bg-card p-4 space-y-3">
543
- <h3 className="text-sm font-medium">Pricing & References</h3>
545
+ <h3 className="text-sm font-medium">{t('cpq.inventory.subscriptions.detail.sections.pricing', 'Pricing & References')}</h3>
544
546
  <div className="grid grid-cols-2 gap-4">
545
547
  <div>
546
- <p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
548
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.mrc', 'Monthly Recurring (MRC)')}</p>
547
549
  <p className="text-lg font-semibold font-mono">{fmt(sub.mrcAmount, currency)}</p>
548
550
  </div>
549
551
  <div>
550
- <p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
552
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.nrc', 'Non-Recurring (NRC)')}</p>
551
553
  <p className="text-lg font-semibold font-mono">{fmt(sub.nrcAmount, currency)}</p>
552
554
  </div>
553
555
  </div>
554
556
  <div className="grid grid-cols-2 gap-x-4 gap-y-2 pt-1">
555
- <Field label="Customer" value={customerName ?? sub.customerId.slice(0, 12) + '…'} />
556
- <Field label="Created" value={new Date(sub.createdAt).toLocaleString()} />
557
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.customer', 'Customer')} value={customerName ?? sub.customerId.slice(0, 12) + '…'} />
558
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.created', 'Created')} value={new Date(sub.createdAt).toLocaleString()} />
557
559
  {sub.sourceQuoteId && (
558
560
  <div>
559
- <p className="text-xs text-muted-foreground">Source Quote</p>
561
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.sourceQuote', 'Source Quote')}</p>
560
562
  <button
561
563
  onClick={() => router.push(`/backend/cpq/quotes/${sub.sourceQuoteId}`)}
562
564
  className="text-sm text-primary hover:underline"
@@ -567,7 +569,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
567
569
  )}
568
570
  {sub.sourceOrderId && (
569
571
  <div>
570
- <p className="text-xs text-muted-foreground">Source Order</p>
572
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.sourceOrder', 'Source Order')}</p>
571
573
  <button
572
574
  onClick={() => router.push(`/backend/cpq/orders/${sub.sourceOrderId}`)}
573
575
  className="text-sm text-primary hover:underline"
@@ -583,7 +585,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
583
585
  {/* Metadata */}
584
586
  {sub.metadata && Object.keys(sub.metadata).length > 0 && (
585
587
  <div className="rounded-lg border bg-card p-4">
586
- <h3 className="text-sm font-medium mb-2">Metadata</h3>
588
+ <h3 className="text-sm font-medium mb-2">{t('cpq.inventory.subscriptions.detail.sections.metadata', 'Metadata')}</h3>
587
589
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
588
590
  {Object.entries(sub.metadata).map(([key, value]) => (
589
591
  <div key={key} className="text-xs">
@@ -597,9 +599,9 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
597
599
 
598
600
  {/* Subscription Items */}
599
601
  <div>
600
- <h3 className="text-sm font-medium mb-2 px-1">Subscription Items ({flatItems.length})</h3>
602
+ <h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.items.title', 'Subscription Items ({count})', { count: flatItems.length })}</h3>
601
603
  {flatItems.length === 0 ? (
602
- <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No items.</div>
604
+ <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.empty', 'No items.')}</div>
603
605
  ) : (
604
606
  <div className="rounded-lg border bg-card divide-y">
605
607
  {flatItems.map((item) => {
@@ -625,8 +627,8 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
625
627
  </div>
626
628
  </div>
627
629
  <div className="shrink-0 text-right text-xs space-y-0.5">
628
- {Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
629
- {Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
630
+ {Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.mrc', 'MRC')}{' '}</span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
631
+ {Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.nrc', 'NRC')}{' '}</span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
630
632
  </div>
631
633
  </div>
632
634
 
@@ -635,7 +637,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
635
637
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2">
636
638
  {item.offeringId && (
637
639
  <RefField
638
- label="Offering"
640
+ label={t('cpq.inventory.subscriptions.detail.fields.offering', 'Offering')}
639
641
  name={offeringNames[item.offeringId]}
640
642
  fallback={item.offeringId}
641
643
  onClick={() => router.push(`/backend/cpq/offerings/${item.offeringId}`)}
@@ -643,7 +645,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
643
645
  )}
644
646
  {item.specId && (
645
647
  <RefField
646
- label="Specification"
648
+ label={t('cpq.inventory.subscriptions.detail.fields.specification', 'Specification')}
647
649
  name={specNames[item.specId]}
648
650
  fallback={item.specId}
649
651
  onClick={() => router.push(`/backend/cpq/specifications/${item.specId}`)}
@@ -651,20 +653,20 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
651
653
  )}
652
654
  {item.productId && (
653
655
  <RefField
654
- label="Product"
656
+ label={t('cpq.inventory.subscriptions.detail.fields.product', 'Product')}
655
657
  name={productNames[item.productId]}
656
658
  fallback={item.productId}
657
659
  onClick={() => router.push(`/backend/catalog/products/${item.productId}`)}
658
660
  />
659
661
  )}
660
- <Field label="Currency" value={item.currencyCode || currency} />
661
- <Field label="Quantity" value={String(item.quantity)} />
662
- {item.capacityTotal && <Field label="Capacity" value={`${item.capacityUsed ?? '0'} / ${item.capacityTotal} ${item.capacityUnit ?? ''}`} />}
662
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.currency', 'Currency')} value={item.currencyCode || currency} />
663
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.quantity', 'Quantity')} value={String(item.quantity)} />
664
+ {item.capacityTotal && <Field label={t('cpq.inventory.subscriptions.detail.fields.capacity', 'Capacity')} value={`${item.capacityUsed ?? '0'} / ${item.capacityTotal} ${item.capacityUnit ?? ''}`} />}
663
665
  </div>
664
666
 
665
667
  {item.configuration && Object.keys(item.configuration).length > 0 && (
666
668
  <div>
667
- <p className="text-xs font-medium text-muted-foreground mb-1">Configuration</p>
669
+ <p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.configuration', 'Configuration')}</p>
668
670
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
669
671
  {Object.entries(item.configuration).map(([key, value]) => (
670
672
  <div key={key} className="text-xs">
@@ -678,16 +680,16 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
678
680
 
679
681
  {item.charges && item.charges.length > 0 && (
680
682
  <div>
681
- <p className="text-xs font-medium text-muted-foreground mb-1">Charges</p>
683
+ <p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.charges', 'Charges')}</p>
682
684
  <table className="w-full text-xs">
683
685
  <thead>
684
686
  <tr className="text-muted-foreground">
685
- <th className="text-left py-1 font-medium">Charge</th>
686
- <th className="text-left py-1 font-medium">Type</th>
687
- <th className="text-right py-1 font-medium">List</th>
688
- <th className="text-right py-1 font-medium">Offer</th>
689
- <th className="text-right py-1 font-medium">Qty</th>
690
- <th className="text-right py-1 font-medium">Total</th>
687
+ <th className="text-left py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.charge', 'Charge')}</th>
688
+ <th className="text-left py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.type', 'Type')}</th>
689
+ <th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.list', 'List')}</th>
690
+ <th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.offer', 'Offer')}</th>
691
+ <th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.qty', 'Qty')}</th>
692
+ <th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.total', 'Total')}</th>
691
693
  </tr>
692
694
  </thead>
693
695
  <tbody>
@@ -726,7 +728,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
726
728
 
727
729
  {item.metadata && Object.keys(item.metadata).length > 0 && (
728
730
  <div>
729
- <p className="text-xs font-medium text-muted-foreground mb-1">Metadata</p>
731
+ <p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.metadata', 'Metadata')}</p>
730
732
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
731
733
  {Object.entries(item.metadata).map(([key, value]) => (
732
734
  <div key={key} className="text-xs">
@@ -739,8 +741,8 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
739
741
  )}
740
742
 
741
743
  <div className="flex gap-4 text-xs text-muted-foreground">
742
- <span>Created: {new Date(item.createdAt).toLocaleString()}</span>
743
- <span>Updated: {new Date(item.updatedAt).toLocaleString()}</span>
744
+ <span>{t('cpq.inventory.subscriptions.detail.items.createdAt', 'Created: {date}', { date: new Date(item.createdAt).toLocaleString() })}</span>
745
+ <span>{t('cpq.inventory.subscriptions.detail.items.updatedAt', 'Updated: {date}', { date: new Date(item.updatedAt).toLocaleString() })}</span>
744
746
  </div>
745
747
  </div>
746
748
  )}
@@ -753,18 +755,18 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
753
755
 
754
756
  {/* XD-250 Change History */}
755
757
  <div>
756
- <h3 className="text-sm font-medium mb-2 px-1">Change History ({changeLog.length})</h3>
758
+ <h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.changeLog.title', 'Change History ({count})', { count: changeLog.length })}</h3>
757
759
  {changeLogLoading ? (
758
- <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">Loading…</div>
760
+ <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.loading', 'Loading…')}</div>
759
761
  ) : changeLog.length === 0 ? (
760
- <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">No ARC changes yet.</div>
762
+ <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.changeLog.empty', 'No ARC changes yet.')}</div>
761
763
  ) : (
762
764
  <div className="rounded-lg border bg-card divide-y">
763
765
  {changeLog.map((row) => (
764
766
  <div key={row.id} className="px-4 py-3 space-y-1">
765
767
  <div className="flex items-center gap-2">
766
768
  <Tag variant={CHANGE_TYPE_VARIANTS[row.changeType] ?? 'neutral'}>
767
- {CHANGE_TYPE_LABELS[row.changeType] ?? row.changeType}
769
+ {changeTypeLabel(t, row.changeType)}
768
770
  </Tag>
769
771
  <span className="text-xs text-muted-foreground">
770
772
  {new Date(row.effectiveAt).toLocaleString()}
@@ -774,7 +776,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
774
776
  onClick={() => router.push(`/backend/cpq/quotes/${row.sourceQuoteId}`)}
775
777
  className="text-xs text-primary hover:underline"
776
778
  >
777
- Quote {logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…`}
779
+ {t('cpq.inventory.subscriptions.detail.changeLog.quote', 'Quote {number}', { number: logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…` })}
778
780
  </button>
779
781
  )}
780
782
  {row.sourceOrderId && (
@@ -782,42 +784,47 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
782
784
  onClick={() => router.push(`/backend/cpq/orders/${row.sourceOrderId}`)}
783
785
  className="text-xs text-primary hover:underline"
784
786
  >
785
- Order {logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…`}
787
+ {t('cpq.inventory.subscriptions.detail.changeLog.order', 'Order {number}', { number: logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…` })}
786
788
  </button>
787
789
  )}
788
790
  </div>
789
791
  <ChangeLogLineDetails row={row} currency={currency} />
790
792
  {row.termChange && (
791
793
  <div className="text-xs text-muted-foreground">
792
- Term: {String(row.termChange.oldTermEnd ?? '—')} → {String(row.termChange.newTermEnd ?? '—')}
794
+ {t('cpq.inventory.subscriptions.detail.changeLog.term', 'Term: {from} → {to}', {
795
+ from: String(row.termChange.oldTermEnd ?? '—'),
796
+ to: String(row.termChange.newTermEnd ?? '—'),
797
+ })}
793
798
  </div>
794
799
  )}
795
800
  {row.reasonCode && (
796
801
  <div className="text-xs text-muted-foreground">
797
- Reason: {row.reasonCode}{row.reasonText ? ` — ${row.reasonText}` : ''}
802
+ {row.reasonText
803
+ ? t('cpq.inventory.subscriptions.detail.changeLog.reasonWithText', 'Reason: {code} — {text}', { code: row.reasonCode, text: row.reasonText })
804
+ : t('cpq.inventory.subscriptions.detail.changeLog.reason', 'Reason: {code}', { code: row.reasonCode })}
798
805
  </div>
799
806
  )}
800
807
  {row.etfAmount && (
801
808
  <div className="text-xs text-muted-foreground">
802
- ETF: {row.etfAmount} {row.etfCurrency ?? ''}
809
+ {t('cpq.inventory.subscriptions.detail.changeLog.etf', 'ETF: {amount} {currency}', { amount: row.etfAmount, currency: row.etfCurrency ?? '' })}
803
810
  </div>
804
811
  )}
805
812
  {row.mergedIntoSubscriptionId && (
806
813
  <div className="text-xs text-muted-foreground">
807
- Merged into{' '}
814
+ {t('cpq.inventory.subscriptions.detail.changeLog.mergedInto', 'Merged into')}{' '}
808
815
  <button
809
816
  onClick={() =>
810
817
  router.push(`/backend/cpq/inventory/subscriptions/${row.mergedIntoSubscriptionId}`)
811
818
  }
812
819
  className="text-primary hover:underline"
813
820
  >
814
- subscription {row.mergedIntoSubscriptionId.slice(0, 8)}…
821
+ {t('cpq.inventory.subscriptions.detail.changeLog.mergedIntoSubscription', 'subscription {id}…', { id: row.mergedIntoSubscriptionId.slice(0, 8) })}
815
822
  </button>
816
823
  </div>
817
824
  )}
818
825
  {row.mergedFromSubscriptionIds && row.mergedFromSubscriptionIds.length > 0 && (
819
826
  <div className="text-xs text-muted-foreground">
820
- Merged from {row.mergedFromSubscriptionIds.length} subscriptions
827
+ {t('cpq.inventory.subscriptions.detail.changeLog.mergedFrom', 'Merged from {count} subscriptions', { count: row.mergedFromSubscriptionIds.length })}
821
828
  </div>
822
829
  )}
823
830
  </div>
@@ -828,20 +835,20 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
828
835
 
829
836
  {/* Linked Assets */}
830
837
  <div>
831
- <h3 className="text-sm font-medium mb-2 px-1">Linked Assets ({assets.length})</h3>
838
+ <h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.assets.title', 'Linked Assets ({count})', { count: assets.length })}</h3>
832
839
  {assets.length === 0 ? (
833
- <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No assets linked to this subscription.</div>
840
+ <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.assets.empty', 'No assets linked to this subscription.')}</div>
834
841
  ) : (
835
842
  <div className="rounded-lg border">
836
843
  <table className="w-full text-sm">
837
844
  <thead>
838
845
  <tr className="border-b bg-muted/50">
839
- <th className="px-4 py-3 text-left font-medium">Code</th>
840
- <th className="px-4 py-3 text-left font-medium">Name</th>
841
- <th className="px-4 py-3 text-left font-medium">Type</th>
842
- <th className="px-4 py-3 text-left font-medium">Status</th>
843
- <th className="px-4 py-3 text-right font-medium">Price</th>
844
- <th className="px-4 py-3 text-right font-medium">Qty</th>
846
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.code', 'Code')}</th>
847
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.name', 'Name')}</th>
848
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.type', 'Type')}</th>
849
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.status', 'Status')}</th>
850
+ <th className="px-4 py-3 text-right font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.price', 'Price')}</th>
851
+ <th className="px-4 py-3 text-right font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.qty', 'Qty')}</th>
845
852
  </tr>
846
853
  </thead>
847
854
  <tbody>
@@ -932,6 +939,8 @@ function ErrorBanner({ message }: { message: string }) {
932
939
  // names and before/after MRC values. Falls back gracefully when snapshots are
933
940
  // missing (older log rows) or when lineChanges is null (cancel / merge).
934
941
  function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency: string }) {
942
+ const t = useT()
943
+ const itemFallback = t('cpq.inventory.subscriptions.detail.changeLog.item', 'Item')
935
944
  const beforeItems = new Map<string, SnapshotItem>(
936
945
  (row.beforeSnapshot?.items ?? []).map((it) => [it.id, it]),
937
946
  )
@@ -965,7 +974,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
965
974
  derived.push({
966
975
  key: `${itemId}-${i}`,
967
976
  action: lc.action,
968
- name: item?.name ?? (lc.action === 'add' ? 'New item' : 'Item'),
977
+ name: item?.name ?? (lc.action === 'add' ? t('cpq.inventory.subscriptions.detail.changeLog.newItem', 'New item') : itemFallback),
969
978
  beforeMrc,
970
979
  afterMrc:
971
980
  lc.action === 'add' && afterMrc == null && lc.mrcAmount != null
@@ -981,7 +990,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
981
990
  derived.push({
982
991
  key: it.id,
983
992
  action: 'cancel',
984
- name: it.name ?? 'Item',
993
+ name: it.name ?? itemFallback,
985
994
  beforeMrc: mrc,
986
995
  afterMrc: 0,
987
996
  delta: -mrc,
@@ -993,7 +1002,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
993
1002
  derived.push({
994
1003
  key: it.id,
995
1004
  action: 'add',
996
- name: it.name ?? 'Item',
1005
+ name: it.name ?? itemFallback,
997
1006
  beforeMrc: null,
998
1007
  afterMrc: mrc,
999
1008
  delta: null,
@@ -1005,7 +1014,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
1005
1014
  derived.push({
1006
1015
  key: it.id,
1007
1016
  action: 'cancel',
1008
- name: it.name ?? 'Item',
1017
+ name: it.name ?? itemFallback,
1009
1018
  beforeMrc: mrc,
1010
1019
  afterMrc: 0,
1011
1020
  delta: -mrc,
@@ -1021,7 +1030,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
1021
1030
  if (row.changeType === 'amend' || row.changeType === 'renew') {
1022
1031
  return (
1023
1032
  <div className="mt-1.5 text-xs text-muted-foreground italic">
1024
- No item-level changes recorded.
1033
+ {t('cpq.inventory.subscriptions.detail.changeLog.noItemChanges', 'No item-level changes recorded.')}
1025
1034
  </div>
1026
1035
  )
1027
1036
  }
@@ -1037,10 +1046,18 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
1037
1046
  )
1038
1047
  }
1039
1048
 
1040
- const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string; label: string }> = {
1041
- add: { sign: '+', color: 'text-status-success-text', label: 'added' },
1042
- modify: { sign: '~', color: 'text-status-info-text', label: 'modified' },
1043
- cancel: { sign: '−', color: 'text-destructive', label: 'removed' },
1049
+ const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string }> = {
1050
+ add: { sign: '+', color: 'text-status-success-text' },
1051
+ modify: { sign: '~', color: 'text-status-info-text' },
1052
+ cancel: { sign: '−', color: 'text-destructive' },
1053
+ }
1054
+
1055
+ function actionGlyphLabel(t: TranslateFn, action: 'add' | 'modify' | 'cancel'): string {
1056
+ switch (action) {
1057
+ case 'add': return t('cpq.inventory.subscriptions.detail.changeLog.action.add', 'added')
1058
+ case 'modify': return t('cpq.inventory.subscriptions.detail.changeLog.action.modify', 'modified')
1059
+ case 'cancel': return t('cpq.inventory.subscriptions.detail.changeLog.action.cancel', 'removed')
1060
+ }
1044
1061
  }
1045
1062
 
1046
1063
  function ChangeLogLineRow({
@@ -1056,6 +1073,7 @@ function ChangeLogLineRow({
1056
1073
  }
1057
1074
  currency: string
1058
1075
  }) {
1076
+ const t = useT()
1059
1077
  const glyph = ACTION_GLYPH[change.action]
1060
1078
  const showPriceChange =
1061
1079
  change.action === 'modify' &&
@@ -1072,35 +1090,39 @@ function ChangeLogLineRow({
1072
1090
 
1073
1091
  return (
1074
1092
  <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
1075
- <span className={`font-bold w-3 text-center ${glyph.color}`} title={glyph.label}>
1093
+ <span className={`font-bold w-3 text-center ${glyph.color}`} title={actionGlyphLabel(t, change.action)}>
1076
1094
  {glyph.sign}
1077
1095
  </span>
1078
1096
  <span className="font-medium text-foreground">{change.name}</span>
1079
1097
  {showPriceChange && (
1080
1098
  <span className="font-mono text-muted-foreground">
1081
- {fmt(change.beforeMrc!, currency)} → {fmt(change.afterMrc!, currency)}/mo
1099
+ {t('cpq.inventory.subscriptions.detail.changeLog.priceChangePerMonth', '{before} → {after}/mo', {
1100
+ before: fmt(change.beforeMrc!, currency),
1101
+ after: fmt(change.afterMrc!, currency),
1102
+ })}
1082
1103
  </span>
1083
1104
  )}
1084
1105
  {showSinglePrice && change.action === 'add' && (
1085
1106
  <span className="font-mono text-muted-foreground">
1086
- {fmt(change.afterMrc!, currency)}/mo
1107
+ {t('cpq.inventory.subscriptions.detail.changeLog.pricePerMonth', '{price}/mo', { price: fmt(change.afterMrc!, currency) })}
1087
1108
  </span>
1088
1109
  )}
1089
1110
  {showSinglePrice && change.action === 'cancel' && (
1090
1111
  <span className="font-mono text-muted-foreground">
1091
- was {fmt(change.beforeMrc!, currency)}/mo
1112
+ {t('cpq.inventory.subscriptions.detail.changeLog.wasPricePerMonth', 'was {price}/mo', { price: fmt(change.beforeMrc!, currency) })}
1092
1113
  </span>
1093
1114
  )}
1094
1115
  {showDelta && (
1095
1116
  <span
1096
1117
  className={`font-mono font-medium ${change.delta! > 0 ? 'text-status-success-text' : 'text-destructive'}`}
1097
1118
  >
1098
- ({change.delta! > 0 ? '+' : ''}
1099
- {fmt(change.delta!, currency)}/mo)
1119
+ {t('cpq.inventory.subscriptions.detail.changeLog.deltaPerMonth', '({delta}/mo)', {
1120
+ delta: `${change.delta! > 0 ? '+' : ''}${fmt(change.delta!, currency)}`,
1121
+ })}
1100
1122
  </span>
1101
1123
  )}
1102
1124
  {isNoopModify && (
1103
- <span className="text-muted-foreground italic">no price change</span>
1125
+ <span className="text-muted-foreground italic">{t('cpq.inventory.subscriptions.detail.changeLog.noPriceChange', 'no price change')}</span>
1104
1126
  )}
1105
1127
  </div>
1106
1128
  )