Commit 7900923468f0ecfbdc879c210cd8d370a875394c

Authored by zichun
1 parent 95ed53bd

feat(web): complete i18n — externalize all hardcoded strings

Replace every hardcoded English string across all 35 SPA page files
with useT() calls backed by message keys in messages.ts. Both en-US
and zh-CN translations are provided for every key (~200 new keys).

Pages updated: DashboardPage, LoginPage, FormDesignerPage,
ListViewDesignerPage, MetadataAdminPage, SalesOrderDetailPage,
PurchaseOrderDetailPage, WorkOrderDetailPage, ShopFloorPage,
UserTasksPage, TaskDetailPage, UserDetailPage, all Create* pages,
all Edit* pages, and all list pages (Items, Partners, Locations,
Balances, Movements, SalesOrders, PurchaseOrders, WorkOrders,
Users, Roles, Accounts, JournalEntries, UoMs, AdjustStock).
web/src/pages/AccountsPage.tsx
... ... @@ -5,10 +5,12 @@ import { PageHeader } from '@/components/PageHeader'
5 5 import { Loading } from '@/components/Loading'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { DataTable, type Column } from '@/components/DataTable'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 const ACCOUNT_TYPES = ['ASSET', 'LIABILITY', 'EQUITY', 'REVENUE', 'EXPENSE'] as const
10 11  
11 12 export function AccountsPage() {
  13 + const t = useT()
12 14 const [rows, setRows] = useState<Account[]>([])
13 15 const [error, setError] = useState<Error | null>(null)
14 16 const [loading, setLoading] = useState(true)
... ... @@ -46,44 +48,44 @@ export function AccountsPage() {
46 48 }
47 49  
48 50 const columns: Column<Account>[] = [
49   - { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
50   - { header: 'Name', key: 'name' },
51   - { header: 'Type', key: 'accountType' },
52   - { header: 'Description', key: 'description', render: (r) => r.description ?? '—' },
  51 + { header: t('label.code'), key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
  52 + { header: t('label.name'), key: 'name' },
  53 + { header: t('label.type'), key: 'accountType' },
  54 + { header: t('label.description'), key: 'description', render: (r) => r.description ?? '\u2014' },
53 55 ]
54 56  
55 57 return (
56 58 <div>
57 59 <PageHeader
58   - title="Chart of Accounts"
59   - subtitle="GL accounts that journal entries debit and credit. 6 accounts seeded by default."
  60 + title={t('page.accounts.title')}
  61 + subtitle={t('page.accounts.subtitle')}
60 62 actions={
61 63 <button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
62   - {showCreate ? 'Cancel' : '+ New Account'}
  64 + {showCreate ? t('action.cancel') : t('action.newAccount')}
63 65 </button>
64 66 }
65 67 />
66 68 {showCreate && (
67 69 <form onSubmit={onCreate} className="card p-4 mb-4 max-w-2xl flex flex-wrap items-end gap-3">
68 70 <div className="w-24">
69   - <label className="block text-xs font-medium text-slate-700">Code</label>
  71 + <label className="block text-xs font-medium text-slate-700">{t('label.code')}</label>
70 72 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
71 73 placeholder="1300" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
72 74 </div>
73 75 <div className="flex-1 min-w-[150px]">
74   - <label className="block text-xs font-medium text-slate-700">Name</label>
  76 + <label className="block text-xs font-medium text-slate-700">{t('label.name')}</label>
75 77 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
76 78 placeholder="Prepaid expenses" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
77 79 </div>
78 80 <div className="w-32">
79   - <label className="block text-xs font-medium text-slate-700">Type</label>
  81 + <label className="block text-xs font-medium text-slate-700">{t('label.type')}</label>
80 82 <select value={accountType} onChange={(e) => setAccountType(e.target.value)}
81 83 className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm">
82   - {ACCOUNT_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
  84 + {ACCOUNT_TYPES.map((at) => <option key={at} value={at}>{at}</option>)}
83 85 </select>
84 86 </div>
85 87 <button type="submit" className="btn-primary" disabled={creating}>
86   - {creating ? '...' : 'Create'}
  88 + {creating ? '...' : t('action.create')}
87 89 </button>
88 90 </form>
89 91 )}
... ...
web/src/pages/AdjustStockPage.tsx
... ... @@ -4,9 +4,11 @@ import { catalog, inventory } from &#39;@/api/client&#39;
4 4 import type { Item, Location } from '@/types/api'
5 5 import { PageHeader } from '@/components/PageHeader'
6 6 import { ErrorBox } from '@/components/ErrorBox'
  7 +import { useT } from '@/i18n/LocaleContext'
7 8  
8 9 export function AdjustStockPage() {
9 10 const navigate = useNavigate()
  11 + const t = useT()
10 12 const [items, setItems] = useState<Item[]>([])
11 13 const [locations, setLocations] = useState<Location[]>([])
12 14 const [itemCode, setItemCode] = useState('')
... ... @@ -37,7 +39,9 @@ export function AdjustStockPage() {
37 39 quantity: Number(quantity),
38 40 })
39 41 setResult(
40   - `Balance set: ${bal.itemCode} @ location = ${bal.quantity}`,
  42 + t('page.adjustStock.result')
  43 + .replace('{itemCode}', bal.itemCode)
  44 + .replace('{quantity}', String(bal.quantity)),
41 45 )
42 46 } catch (err: unknown) {
43 47 setError(err instanceof Error ? err : new Error(String(err)))
... ... @@ -49,27 +53,27 @@ export function AdjustStockPage() {
49 53 return (
50 54 <div>
51 55 <PageHeader
52   - title="Adjust Stock"
53   - subtitle="Set the on-hand quantity for an item at a location. Creates the balance row if it doesn't exist."
54   - actions={<button className="btn-secondary" onClick={() => navigate('/balances')}>← Balances</button>}
  56 + title={t('page.adjustStock.title')}
  57 + subtitle={t('page.adjustStock.subtitle')}
  58 + actions={<button className="btn-secondary" onClick={() => navigate('/balances')}>{t('action.back')}</button>}
55 59 />
56 60 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg">
57 61 <div>
58   - <label className="block text-sm font-medium text-slate-700">Item</label>
  62 + <label className="block text-sm font-medium text-slate-700">{t('label.item')}</label>
59 63 <select required value={itemCode} onChange={(e) => setItemCode(e.target.value)}
60 64 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
61 65 {items.map((i) => <option key={i.id} value={i.code}>{i.code} — {i.name}</option>)}
62 66 </select>
63 67 </div>
64 68 <div>
65   - <label className="block text-sm font-medium text-slate-700">Location</label>
  69 + <label className="block text-sm font-medium text-slate-700">{t('label.location')}</label>
66 70 <select required value={locationId} onChange={(e) => setLocationId(e.target.value)}
67 71 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
68 72 {locations.map((l) => <option key={l.id} value={l.id}>{l.code} — {l.name}</option>)}
69 73 </select>
70 74 </div>
71 75 <div>
72   - <label className="block text-sm font-medium text-slate-700">Quantity (absolute, not delta)</label>
  76 + <label className="block text-sm font-medium text-slate-700">{t('label.quantityAbsolute')}</label>
73 77 <input type="number" required min="0" step="1" value={quantity}
74 78 onChange={(e) => setQuantity(e.target.value)}
75 79 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm text-right" />
... ... @@ -81,7 +85,7 @@ export function AdjustStockPage() {
81 85 </div>
82 86 )}
83 87 <button type="submit" className="btn-primary" disabled={submitting}>
84   - {submitting ? 'Adjusting...' : 'Set Balance'}
  88 + {submitting ? t('action.adjusting') : t('action.setBalance')}
85 89 </button>
86 90 </form>
87 91 </div>
... ...
web/src/pages/BalancesPage.tsx
... ... @@ -6,6 +6,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 6 import { Loading } from '@/components/Loading'
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10  
10 11 interface Row extends Record<string, unknown> {
11 12 id: string
... ... @@ -15,6 +16,7 @@ interface Row extends Record&lt;string, unknown&gt; {
15 16 }
16 17  
17 18 export function BalancesPage() {
  19 + const t = useT()
18 20 const [rows, setRows] = useState<Row[]>([])
19 21 const [error, setError] = useState<Error | null>(null)
20 22 const [loading, setLoading] = useState(true)
... ... @@ -37,14 +39,14 @@ export function BalancesPage() {
37 39 }, [])
38 40  
39 41 const columns: Column<Row>[] = [
40   - { header: 'Item', key: 'itemCode', render: (r) => <span className="font-mono">{r.itemCode}</span> },
  42 + { header: t('label.item'), key: 'itemCode', render: (r) => <span className="font-mono">{r.itemCode}</span> },
41 43 {
42   - header: 'Location',
  44 + header: t('label.location'),
43 45 key: 'locationCode',
44 46 render: (r) => <span className="font-mono">{r.locationCode}</span>,
45 47 },
46 48 {
47   - header: 'Quantity',
  49 + header: t('label.quantity'),
48 50 key: 'quantity',
49 51 render: (r) => <span className="font-mono tabular-nums">{String(r.quantity)}</span>,
50 52 },
... ... @@ -53,9 +55,9 @@ export function BalancesPage() {
53 55 return (
54 56 <div>
55 57 <PageHeader
56   - title="Stock Balances"
57   - subtitle="On-hand quantities per (item, location). Updates atomically with every movement."
58   - actions={<Link to="/balances/adjust" className="btn-primary">Adjust Stock</Link>}
  58 + title={t('page.balances.title')}
  59 + subtitle={t('page.balances.subtitle')}
  60 + actions={<Link to="/balances/adjust" className="btn-primary">{t('action.adjustStock')}</Link>}
59 61 />
60 62 {loading && <Loading />}
61 63 {error && <ErrorBox error={error} />}
... ...
web/src/pages/CreateItemPage.tsx
... ... @@ -5,11 +5,13 @@ import type { Uom } from &#39;@/types/api&#39;
5 5 import { PageHeader } from '@/components/PageHeader'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { DynamicExtFields } from '@/components/DynamicExtFields'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const
10 11  
11 12 export function CreateItemPage() {
12 13 const navigate = useNavigate()
  14 + const t = useT()
13 15 const [code, setCode] = useState('')
14 16 const [name, setName] = useState('')
15 17 const [description, setDescription] = useState('')
... ... @@ -49,31 +51,31 @@ export function CreateItemPage() {
49 51 return (
50 52 <div>
51 53 <PageHeader
52   - title="New Item"
53   - subtitle="Add a raw material, finished good, or service to the catalog."
54   - actions={<button className="btn-secondary" onClick={() => navigate('/items')}>Cancel</button>}
  54 + title={t('page.createItem.title')}
  55 + subtitle={t('page.createItem.subtitle')}
  56 + actions={<button className="btn-secondary" onClick={() => navigate('/items')}>{t('action.cancel')}</button>}
55 57 />
56 58 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl">
57 59 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
58 60 <div>
59   - <label className="block text-sm font-medium text-slate-700">Item code</label>
  61 + <label className="block text-sm font-medium text-slate-700">{t('label.itemCode')}</label>
60 62 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
61 63 placeholder="PAPER-120G-A3" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
62 64 </div>
63 65 <div>
64   - <label className="block text-sm font-medium text-slate-700">Name</label>
  66 + <label className="block text-sm font-medium text-slate-700">{t('label.name')}</label>
65 67 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
66 68 placeholder="120g A3 coated paper" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
67 69 </div>
68 70 <div>
69   - <label className="block text-sm font-medium text-slate-700">Type</label>
  71 + <label className="block text-sm font-medium text-slate-700">{t('label.type')}</label>
70 72 <select value={itemType} onChange={(e) => setItemType(e.target.value)}
71 73 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
72   - {ITEM_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
  74 + {ITEM_TYPES.map((it) => <option key={it} value={it}>{it}</option>)}
73 75 </select>
74 76 </div>
75 77 <div>
76   - <label className="block text-sm font-medium text-slate-700">Base UoM</label>
  78 + <label className="block text-sm font-medium text-slate-700">{t('label.baseUom')}</label>
77 79 <select value={baseUomCode} onChange={(e) => setBaseUomCode(e.target.value)}
78 80 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
79 81 {uoms.map((u) => <option key={u.id} value={u.code}>{u.code} — {u.name}</option>)}
... ... @@ -81,7 +83,7 @@ export function CreateItemPage() {
81 83 </div>
82 84 </div>
83 85 <div>
84   - <label className="block text-sm font-medium text-slate-700">Description (optional)</label>
  86 + <label className="block text-sm font-medium text-slate-700">{t('label.descriptionOptional')}</label>
85 87 <input type="text" value={description} onChange={(e) => setDescription(e.target.value)}
86 88 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
87 89 </div>
... ... @@ -92,7 +94,7 @@ export function CreateItemPage() {
92 94 />
93 95 {error && <ErrorBox error={error} />}
94 96 <button type="submit" className="btn-primary" disabled={submitting}>
95   - {submitting ? 'Creating...' : 'Create Item'}
  97 + {submitting ? t('action.creating') : t('page.createItem.submit')}
96 98 </button>
97 99 </form>
98 100 </div>
... ...
web/src/pages/CreateLocationPage.tsx
... ... @@ -4,11 +4,13 @@ import { inventory } from &#39;@/api/client&#39;
4 4 import { PageHeader } from '@/components/PageHeader'
5 5 import { ErrorBox } from '@/components/ErrorBox'
6 6 import { DynamicExtFields } from '@/components/DynamicExtFields'
  7 +import { useT } from '@/i18n/LocaleContext'
7 8  
8 9 const LOCATION_TYPES = ['WAREHOUSE', 'BIN', 'VIRTUAL'] as const
9 10  
10 11 export function CreateLocationPage() {
11 12 const navigate = useNavigate()
  13 + const t = useT()
12 14 const [code, setCode] = useState('')
13 15 const [name, setName] = useState('')
14 16 const [type, setType] = useState<string>('WAREHOUSE')
... ... @@ -34,26 +36,26 @@ export function CreateLocationPage() {
34 36 return (
35 37 <div>
36 38 <PageHeader
37   - title="New Location"
38   - subtitle="Add a warehouse, bin, or virtual location for inventory tracking."
39   - actions={<button className="btn-secondary" onClick={() => navigate('/locations')}>Cancel</button>}
  39 + title={t('page.createLocation.title')}
  40 + subtitle={t('page.createLocation.subtitle')}
  41 + actions={<button className="btn-secondary" onClick={() => navigate('/locations')}>{t('action.cancel')}</button>}
40 42 />
41 43 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg">
42 44 <div>
43   - <label className="block text-sm font-medium text-slate-700">Location code</label>
  45 + <label className="block text-sm font-medium text-slate-700">{t('label.locationCode')}</label>
44 46 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
45 47 placeholder="WH-NEW" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
46 48 </div>
47 49 <div>
48   - <label className="block text-sm font-medium text-slate-700">Name</label>
  50 + <label className="block text-sm font-medium text-slate-700">{t('label.name')}</label>
49 51 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
50 52 placeholder="New Warehouse" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
51 53 </div>
52 54 <div>
53   - <label className="block text-sm font-medium text-slate-700">Type</label>
  55 + <label className="block text-sm font-medium text-slate-700">{t('label.type')}</label>
54 56 <select value={type} onChange={(e) => setType(e.target.value)}
55 57 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
56   - {LOCATION_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
  58 + {LOCATION_TYPES.map((lt) => <option key={lt} value={lt}>{lt}</option>)}
57 59 </select>
58 60 </div>
59 61 <DynamicExtFields
... ... @@ -63,7 +65,7 @@ export function CreateLocationPage() {
63 65 />
64 66 {error && <ErrorBox error={error} />}
65 67 <button type="submit" className="btn-primary" disabled={submitting}>
66   - {submitting ? 'Creating...' : 'Create Location'}
  68 + {submitting ? t('action.creating') : t('page.createLocation.submit')}
67 69 </button>
68 70 </form>
69 71 </div>
... ...
web/src/pages/CreatePartnerPage.tsx
... ... @@ -4,11 +4,13 @@ import { partners } from &#39;@/api/client&#39;
4 4 import { PageHeader } from '@/components/PageHeader'
5 5 import { ErrorBox } from '@/components/ErrorBox'
6 6 import { DynamicExtFields } from '@/components/DynamicExtFields'
  7 +import { useT } from '@/i18n/LocaleContext'
7 8  
8 9 const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const
9 10  
10 11 export function CreatePartnerPage() {
11 12 const navigate = useNavigate()
  13 + const t = useT()
12 14 const [code, setCode] = useState('')
13 15 const [name, setName] = useState('')
14 16 const [type, setType] = useState<string>('CUSTOMER')
... ... @@ -41,36 +43,36 @@ export function CreatePartnerPage() {
41 43 return (
42 44 <div>
43 45 <PageHeader
44   - title="New Partner"
45   - subtitle="Add a customer, supplier, or dual-role partner."
46   - actions={<button className="btn-secondary" onClick={() => navigate('/partners')}>Cancel</button>}
  46 + title={t('page.createPartner.title')}
  47 + subtitle={t('page.createPartner.subtitle')}
  48 + actions={<button className="btn-secondary" onClick={() => navigate('/partners')}>{t('action.cancel')}</button>}
47 49 />
48 50 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl">
49 51 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
50 52 <div>
51   - <label className="block text-sm font-medium text-slate-700">Partner code</label>
  53 + <label className="block text-sm font-medium text-slate-700">{t('label.partnerCode')}</label>
52 54 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
53 55 placeholder="CUST-NEWCO" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
54 56 </div>
55 57 <div>
56   - <label className="block text-sm font-medium text-slate-700">Name</label>
  58 + <label className="block text-sm font-medium text-slate-700">{t('label.name')}</label>
57 59 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
58 60 placeholder="New Company Ltd." className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
59 61 </div>
60 62 <div>
61   - <label className="block text-sm font-medium text-slate-700">Type</label>
  63 + <label className="block text-sm font-medium text-slate-700">{t('label.type')}</label>
62 64 <select value={type} onChange={(e) => setType(e.target.value)}
63 65 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
64   - {PARTNER_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
  66 + {PARTNER_TYPES.map((pt) => <option key={pt} value={pt}>{pt}</option>)}
65 67 </select>
66 68 </div>
67 69 <div>
68   - <label className="block text-sm font-medium text-slate-700">Email (optional)</label>
  70 + <label className="block text-sm font-medium text-slate-700">{t('label.emailOptional')}</label>
69 71 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
70 72 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
71 73 </div>
72 74 <div>
73   - <label className="block text-sm font-medium text-slate-700">Phone (optional)</label>
  75 + <label className="block text-sm font-medium text-slate-700">{t('label.phoneOptional')}</label>
74 76 <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)}
75 77 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
76 78 </div>
... ... @@ -82,7 +84,7 @@ export function CreatePartnerPage() {
82 84 />
83 85 {error && <ErrorBox error={error} />}
84 86 <button type="submit" className="btn-primary" disabled={submitting}>
85   - {submitting ? 'Creating...' : 'Create Partner'}
  87 + {submitting ? t('action.creating') : t('page.createPartner.submit')}
86 88 </button>
87 89 </form>
88 90 </div>
... ...
web/src/pages/CreatePurchaseOrderPage.tsx
... ... @@ -5,6 +5,7 @@ import type { Item, Partner } from &#39;@/types/api&#39;
5 5 import { PageHeader } from '@/components/PageHeader'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { DynamicExtFields } from '@/components/DynamicExtFields'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 interface LineInput {
10 11 itemCode: string
... ... @@ -14,6 +15,7 @@ interface LineInput {
14 15  
15 16 export function CreatePurchaseOrderPage() {
16 17 const navigate = useNavigate()
  18 + const t = useT()
17 19 const [code, setCode] = useState('')
18 20 const [partnerCode, setPartnerCode] = useState('')
19 21 const [expectedDate, setExpectedDate] = useState('')
... ... @@ -78,19 +80,19 @@ export function CreatePurchaseOrderPage() {
78 80 return (
79 81 <div>
80 82 <PageHeader
81   - title="New Purchase Order"
82   - subtitle="Order materials from a supplier. Confirm and receive to credit inventory."
83   - actions={<button className="btn-secondary" onClick={() => navigate('/purchase-orders')}>Cancel</button>}
  83 + title={t('page.createPurchaseOrder.title')}
  84 + subtitle={t('page.createPurchaseOrder.subtitle')}
  85 + actions={<button className="btn-secondary" onClick={() => navigate('/purchase-orders')}>{t('action.cancel')}</button>}
84 86 />
85 87 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl">
86 88 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
87 89 <div>
88   - <label className="block text-sm font-medium text-slate-700">Order code</label>
  90 + <label className="block text-sm font-medium text-slate-700">{t('label.orderCode')}</label>
89 91 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
90 92 placeholder="PO-2026-0002" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
91 93 </div>
92 94 <div>
93   - <label className="block text-sm font-medium text-slate-700">Supplier</label>
  95 + <label className="block text-sm font-medium text-slate-700">{t('label.supplier')}</label>
94 96 <select required value={partnerCode} onChange={(e) => setPartnerCode(e.target.value)}
95 97 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
96 98 {supplierList.map((p) => (
... ... @@ -99,7 +101,7 @@ export function CreatePurchaseOrderPage() {
99 101 </select>
100 102 </div>
101 103 <div>
102   - <label className="block text-sm font-medium text-slate-700">Expected date</label>
  104 + <label className="block text-sm font-medium text-slate-700">{t('label.expectedDate')}</label>
103 105 <input type="date" value={expectedDate} onChange={(e) => setExpectedDate(e.target.value)}
104 106 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
105 107 </div>
... ... @@ -107,8 +109,8 @@ export function CreatePurchaseOrderPage() {
107 109  
108 110 <div>
109 111 <div className="flex items-center justify-between mb-2">
110   - <label className="text-sm font-medium text-slate-700">Order lines</label>
111   - <button type="button" className="btn-secondary text-xs" onClick={addLine}>+ Add line</button>
  112 + <label className="text-sm font-medium text-slate-700">{t('label.orderLines')}</label>
  113 + <button type="button" className="btn-secondary text-xs" onClick={addLine}>{t('action.addLine')}</button>
112 114 </div>
113 115 <div className="space-y-2">
114 116 {lines.map((line, idx) => (
... ... @@ -116,17 +118,17 @@ export function CreatePurchaseOrderPage() {
116 118 <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span>
117 119 <select value={line.itemCode} onChange={(e) => updateLine(idx, 'itemCode', e.target.value)}
118 120 className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm">
119   - <option value="">Select item...</option>
  121 + <option value="">{t('action.selectItem')}</option>
120 122 {items.map((it) => <option key={it.id} value={it.code}>{it.code} — {it.name}</option>)}
121 123 </select>
122   - <input type="number" min="1" step="1" placeholder="Qty" value={line.quantity}
  124 + <input type="number" min="1" step="1" placeholder={t('label.qty')} value={line.quantity}
123 125 onChange={(e) => updateLine(idx, 'quantity', e.target.value)}
124 126 className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
125   - <input type="number" min="0" step="0.01" placeholder="Price" value={line.unitPrice}
  127 + <input type="number" min="0" step="0.01" placeholder={t('label.unitPrice')} value={line.unitPrice}
126 128 onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)}
127 129 className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
128 130 <button type="button" className="text-slate-400 hover:text-rose-500"
129   - onClick={() => removeLine(idx)} title="Remove line">&times;</button>
  131 + onClick={() => removeLine(idx)} title={t('action.delete')}>&times;</button>
130 132 </div>
131 133 ))}
132 134 </div>
... ... @@ -139,7 +141,7 @@ export function CreatePurchaseOrderPage() {
139 141 />
140 142 {error && <ErrorBox error={error} />}
141 143 <button type="submit" className="btn-primary" disabled={submitting}>
142   - {submitting ? 'Creating...' : 'Create Purchase Order'}
  144 + {submitting ? t('action.creating') : t('page.createPurchaseOrder.submit')}
143 145 </button>
144 146 </form>
145 147 </div>
... ...
web/src/pages/CreateSalesOrderPage.tsx
... ... @@ -5,6 +5,7 @@ import type { Item, Partner } from &#39;@/types/api&#39;
5 5 import { PageHeader } from '@/components/PageHeader'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { DynamicExtFields } from '@/components/DynamicExtFields'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 interface LineInput {
10 11 itemCode: string
... ... @@ -14,6 +15,7 @@ interface LineInput {
14 15  
15 16 export function CreateSalesOrderPage() {
16 17 const navigate = useNavigate()
  18 + const t = useT()
17 19 const [code, setCode] = useState('')
18 20 const [partnerCode, setPartnerCode] = useState('')
19 21 const [currencyCode] = useState('USD')
... ... @@ -80,18 +82,18 @@ export function CreateSalesOrderPage() {
80 82 return (
81 83 <div>
82 84 <PageHeader
83   - title="New Sales Order"
84   - subtitle="Create a sales order. Confirming it will auto-generate production work orders."
  85 + title={t('page.createSalesOrder.title')}
  86 + subtitle={t('page.createSalesOrder.subtitle')}
85 87 actions={
86 88 <button className="btn-secondary" onClick={() => navigate('/sales-orders')}>
87   - Cancel
  89 + {t('action.cancel')}
88 90 </button>
89 91 }
90 92 />
91 93 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl">
92 94 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
93 95 <div>
94   - <label className="block text-sm font-medium text-slate-700">Order code</label>
  96 + <label className="block text-sm font-medium text-slate-700">{t('label.orderCode')}</label>
95 97 <input
96 98 type="text"
97 99 required
... ... @@ -102,7 +104,7 @@ export function CreateSalesOrderPage() {
102 104 />
103 105 </div>
104 106 <div>
105   - <label className="block text-sm font-medium text-slate-700">Customer</label>
  107 + <label className="block text-sm font-medium text-slate-700">{t('label.customer')}</label>
106 108 <select
107 109 required
108 110 value={partnerCode}
... ... @@ -117,7 +119,7 @@ export function CreateSalesOrderPage() {
117 119 </select>
118 120 </div>
119 121 <div>
120   - <label className="block text-sm font-medium text-slate-700">Currency</label>
  122 + <label className="block text-sm font-medium text-slate-700">{t('label.currency')}</label>
121 123 <input
122 124 type="text"
123 125 value={currencyCode}
... ... @@ -129,9 +131,9 @@ export function CreateSalesOrderPage() {
129 131  
130 132 <div>
131 133 <div className="flex items-center justify-between mb-2">
132   - <label className="text-sm font-medium text-slate-700">Order lines</label>
  134 + <label className="text-sm font-medium text-slate-700">{t('label.orderLines')}</label>
133 135 <button type="button" className="btn-secondary text-xs" onClick={addLine}>
134   - + Add line
  136 + {t('action.addLine')}
135 137 </button>
136 138 </div>
137 139 <div className="space-y-2">
... ... @@ -143,7 +145,7 @@ export function CreateSalesOrderPage() {
143 145 onChange={(e) => updateLine(idx, 'itemCode', e.target.value)}
144 146 className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm"
145 147 >
146   - <option value="">Select item...</option>
  148 + <option value="">{t('action.selectItem')}</option>
147 149 {items.map((it) => (
148 150 <option key={it.id} value={it.code}>
149 151 {it.code} — {it.name}
... ... @@ -154,7 +156,7 @@ export function CreateSalesOrderPage() {
154 156 type="number"
155 157 min="1"
156 158 step="1"
157   - placeholder="Qty"
  159 + placeholder={t('label.qty')}
158 160 value={line.quantity}
159 161 onChange={(e) => updateLine(idx, 'quantity', e.target.value)}
160 162 className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right"
... ... @@ -163,7 +165,7 @@ export function CreateSalesOrderPage() {
163 165 type="number"
164 166 min="0"
165 167 step="0.01"
166   - placeholder="Price"
  168 + placeholder={t('label.unitPrice')}
167 169 value={line.unitPrice}
168 170 onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)}
169 171 className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right"
... ... @@ -172,7 +174,7 @@ export function CreateSalesOrderPage() {
172 174 type="button"
173 175 className="text-slate-400 hover:text-rose-500"
174 176 onClick={() => removeLine(idx)}
175   - title="Remove line"
  177 + title={t('action.delete')}
176 178 >
177 179 &times;
178 180 </button>
... ... @@ -191,10 +193,10 @@ export function CreateSalesOrderPage() {
191 193  
192 194 <div className="flex items-center gap-3 pt-2">
193 195 <button type="submit" className="btn-primary" disabled={submitting}>
194   - {submitting ? 'Creating...' : 'Create Sales Order'}
  196 + {submitting ? t('action.creating') : t('page.createSalesOrder.submit')}
195 197 </button>
196 198 <span className="text-xs text-slate-400">
197   - After creation, confirm the order to auto-generate work orders.
  199 + {t('page.createSalesOrder.afterHint')}
198 200 </span>
199 201 </div>
200 202 </form>
... ...
web/src/pages/CreateUserPage.tsx
... ... @@ -3,9 +3,11 @@ import { useNavigate } from &#39;react-router-dom&#39;
3 3 import { identity } from '@/api/client'
4 4 import { PageHeader } from '@/components/PageHeader'
5 5 import { ErrorBox } from '@/components/ErrorBox'
  6 +import { useT } from '@/i18n/LocaleContext'
6 7  
7 8 export function CreateUserPage() {
8 9 const navigate = useNavigate()
  10 + const t = useT()
9 11 const [username, setUsername] = useState('')
10 12 const [displayName, setDisplayName] = useState('')
11 13 const [email, setEmail] = useState('')
... ... @@ -31,29 +33,29 @@ export function CreateUserPage() {
31 33 return (
32 34 <div>
33 35 <PageHeader
34   - title="New User"
35   - subtitle="Create a user account. Assign roles on the detail page after creation."
36   - actions={<button className="btn-secondary" onClick={() => navigate('/users')}>Cancel</button>}
  36 + title={t('page.createUser.title')}
  37 + subtitle={t('page.createUser.subtitle')}
  38 + actions={<button className="btn-secondary" onClick={() => navigate('/users')}>{t('action.cancel')}</button>}
37 39 />
38 40 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg">
39 41 <div>
40   - <label className="block text-sm font-medium text-slate-700">Username</label>
  42 + <label className="block text-sm font-medium text-slate-700">{t('label.username')}</label>
41 43 <input type="text" required value={username} onChange={(e) => setUsername(e.target.value)}
42 44 placeholder="jdoe" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
43 45 </div>
44 46 <div>
45   - <label className="block text-sm font-medium text-slate-700">Display name</label>
  47 + <label className="block text-sm font-medium text-slate-700">{t('label.displayName')}</label>
46 48 <input type="text" required value={displayName} onChange={(e) => setDisplayName(e.target.value)}
47 49 placeholder="Jane Doe" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
48 50 </div>
49 51 <div>
50   - <label className="block text-sm font-medium text-slate-700">Email (optional)</label>
  52 + <label className="block text-sm font-medium text-slate-700">{t('label.emailOptional')}</label>
51 53 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
52 54 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
53 55 </div>
54 56 {error && <ErrorBox error={error} />}
55 57 <button type="submit" className="btn-primary" disabled={submitting}>
56   - {submitting ? 'Creating...' : 'Create User'}
  58 + {submitting ? t('action.creating') : t('page.createUser.submit')}
57 59 </button>
58 60 </form>
59 61 </div>
... ...
web/src/pages/CreateWorkOrderPage.tsx
... ... @@ -5,12 +5,14 @@ import type { Item, Location } from &#39;@/types/api&#39;
5 5 import { PageHeader } from '@/components/PageHeader'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { DynamicExtFields } from '@/components/DynamicExtFields'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 interface BomLine { itemCode: string; quantityPerUnit: string; sourceLocationCode: string }
10 11 interface OpLine { operationCode: string; workCenter: string; standardMinutes: string }
11 12  
12 13 export function CreateWorkOrderPage() {
13 14 const navigate = useNavigate()
  15 + const t = useT()
14 16 const [code, setCode] = useState('')
15 17 const [outputItemCode, setOutputItemCode] = useState('')
16 18 const [outputQuantity, setOutputQuantity] = useState('')
... ... @@ -77,55 +79,55 @@ export function CreateWorkOrderPage() {
77 79 return (
78 80 <div>
79 81 <PageHeader
80   - title="New Work Order"
81   - subtitle="Create a production work order with optional BOM inputs and routing operations."
82   - actions={<button className="btn-secondary" onClick={() => navigate('/work-orders')}>Cancel</button>}
  82 + title={t('page.createWorkOrder.title')}
  83 + subtitle={t('page.createWorkOrder.subtitle')}
  84 + actions={<button className="btn-secondary" onClick={() => navigate('/work-orders')}>{t('action.cancel')}</button>}
83 85 />
84 86 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl">
85 87 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
86 88 <div>
87   - <label className="block text-sm font-medium text-slate-700">WO code</label>
  89 + <label className="block text-sm font-medium text-slate-700">{t('label.woCode')}</label>
88 90 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
89 91 placeholder="WO-PRINT-0002" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
90 92 </div>
91 93 <div>
92   - <label className="block text-sm font-medium text-slate-700">Output item</label>
  94 + <label className="block text-sm font-medium text-slate-700">{t('label.outputItem')}</label>
93 95 <select required value={outputItemCode} onChange={(e) => setOutputItemCode(e.target.value)}
94 96 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
95   - <option value="">Select item...</option>
  97 + <option value="">{t('action.selectItem')}</option>
96 98 {items.map((it) => <option key={it.id} value={it.code}>{it.code} — {it.name}</option>)}
97 99 </select>
98 100 </div>
99 101 <div>
100   - <label className="block text-sm font-medium text-slate-700">Output qty</label>
  102 + <label className="block text-sm font-medium text-slate-700">{t('label.outputQty')}</label>
101 103 <input type="number" required min="1" step="1" value={outputQuantity}
102 104 onChange={(e) => setOutputQuantity(e.target.value)}
103 105 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm text-right" />
104 106 </div>
105 107 </div>
106 108 <div>
107   - <label className="block text-sm font-medium text-slate-700">Due date (optional)</label>
  109 + <label className="block text-sm font-medium text-slate-700">{t('label.dueDate')}</label>
108 110 <input type="date" value={dueDate} onChange={(e) => setDueDate(e.target.value)}
109 111 className="mt-1 max-w-xs rounded-md border border-slate-300 px-3 py-2 text-sm" />
110 112 </div>
111 113  
112   - {/* ─── BOM inputs ─────────────────────────────────────── */}
  114 + {/* BOM inputs */}
113 115 <div>
114 116 <div className="flex items-center justify-between mb-2">
115   - <label className="text-sm font-medium text-slate-700">BOM inputs (materials consumed per unit of output)</label>
116   - <button type="button" className="btn-secondary text-xs" onClick={addBom}>+ Add input</button>
  117 + <label className="text-sm font-medium text-slate-700">{t('label.bomInputsDesc')}</label>
  118 + <button type="button" className="btn-secondary text-xs" onClick={addBom}>{t('action.addInput')}</button>
117 119 </div>
118   - {bom.length === 0 && <p className="text-xs text-slate-400">No BOM lines. Output will be produced without consuming materials.</p>}
  120 + {bom.length === 0 && <p className="text-xs text-slate-400">{t('label.noBomHint')}</p>}
119 121 <div className="space-y-2">
120 122 {bom.map((b, idx) => (
121 123 <div key={idx} className="flex items-center gap-2">
122 124 <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span>
123 125 <select value={b.itemCode} onChange={(e) => updateBom(idx, 'itemCode', e.target.value)}
124 126 className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm">
125   - <option value="">Item...</option>
  127 + <option value="">{t('label.item')}...</option>
126 128 {items.map((it) => <option key={it.id} value={it.code}>{it.code}</option>)}
127 129 </select>
128   - <input type="number" min="0.01" step="0.01" placeholder="Qty/unit" value={b.quantityPerUnit}
  130 + <input type="number" min="0.01" step="0.01" placeholder={t('label.qtyPerUnit')} value={b.quantityPerUnit}
129 131 onChange={(e) => updateBom(idx, 'quantityPerUnit', e.target.value)}
130 132 className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
131 133 <select value={b.sourceLocationCode} onChange={(e) => updateBom(idx, 'sourceLocationCode', e.target.value)}
... ... @@ -138,24 +140,24 @@ export function CreateWorkOrderPage() {
138 140 </div>
139 141 </div>
140 142  
141   - {/* ─── Routing operations ─────────────────────────────── */}
  143 + {/* Routing operations */}
142 144 <div>
143 145 <div className="flex items-center justify-between mb-2">
144   - <label className="text-sm font-medium text-slate-700">Routing operations (sequential steps)</label>
145   - <button type="button" className="btn-secondary text-xs" onClick={addOp}>+ Add operation</button>
  146 + <label className="text-sm font-medium text-slate-700">{t('label.routingOpsDesc')}</label>
  147 + <button type="button" className="btn-secondary text-xs" onClick={addOp}>{t('action.addOperation')}</button>
146 148 </div>
147   - {ops.length === 0 && <p className="text-xs text-slate-400">No routing. Work order completes in one step.</p>}
  149 + {ops.length === 0 && <p className="text-xs text-slate-400">{t('label.noRoutingHint')}</p>}
148 150 <div className="space-y-2">
149 151 {ops.map((o, idx) => (
150 152 <div key={idx} className="flex items-center gap-2">
151 153 <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span>
152   - <input type="text" required placeholder="Op code (e.g. CTP)" value={o.operationCode}
  154 + <input type="text" required placeholder={t('label.operation')} value={o.operationCode}
153 155 onChange={(e) => updateOp(idx, 'operationCode', e.target.value)}
154 156 className="w-28 rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
155   - <input type="text" required placeholder="Work center" value={o.workCenter}
  157 + <input type="text" required placeholder={t('label.workCenter')} value={o.workCenter}
156 158 onChange={(e) => updateOp(idx, 'workCenter', e.target.value)}
157 159 className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
158   - <input type="number" min="1" step="1" placeholder="Std min" value={o.standardMinutes}
  160 + <input type="number" min="1" step="1" placeholder={t('label.stdMin')} value={o.standardMinutes}
159 161 onChange={(e) => updateOp(idx, 'standardMinutes', e.target.value)}
160 162 className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
161 163 <button type="button" className="text-slate-400 hover:text-rose-500" onClick={() => removeOp(idx)}>&times;</button>
... ... @@ -171,7 +173,7 @@ export function CreateWorkOrderPage() {
171 173 />
172 174 {error && <ErrorBox error={error} />}
173 175 <button type="submit" className="btn-primary" disabled={submitting}>
174   - {submitting ? 'Creating...' : 'Create Work Order'}
  176 + {submitting ? t('action.creating') : t('page.createWorkOrder.submit')}
175 177 </button>
176 178 </form>
177 179 </div>
... ...
web/src/pages/DashboardPage.tsx
... ... @@ -13,6 +13,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
13 13 import { Loading } from '@/components/Loading'
14 14 import { ErrorBox } from '@/components/ErrorBox'
15 15 import { useAuth } from '@/auth/AuthContext'
  16 +import { useT } from '@/i18n/LocaleContext'
16 17  
17 18 interface DashboardCounts {
18 19 items: number
... ... @@ -27,6 +28,7 @@ interface DashboardCounts {
27 28  
28 29 export function DashboardPage() {
29 30 const { username } = useAuth()
  31 + const t = useT()
30 32 const [counts, setCounts] = useState<DashboardCounts | null>(null)
31 33 const [error, setError] = useState<Error | null>(null)
32 34 const [loading, setLoading] = useState(true)
... ... @@ -69,73 +71,68 @@ export function DashboardPage() {
69 71 return (
70 72 <div>
71 73 <PageHeader
72   - title={`Welcome${username ? ', ' + username : ''}`}
73   - subtitle="The framework's buy-make-sell loop, end to end through the same Postgres."
  74 + title={`${t('page.dashboard.welcome')}${username ? ', ' + username : ''}`}
  75 + subtitle={t('page.dashboard.subtitle')}
74 76 />
75 77 {loading && <Loading />}
76 78 {error && <ErrorBox error={error} />}
77 79 {counts && (
78 80 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
79   - <DashboardCard label="Items" value={counts.items} to="/items" />
80   - <DashboardCard label="Partners" value={counts.partners} to="/partners" />
81   - <DashboardCard label="Locations" value={counts.locations} to="/locations" />
  81 + <DashboardCard label={t('page.dashboard.cardItems')} value={counts.items} to="/items" />
  82 + <DashboardCard label={t('page.dashboard.cardPartners')} value={counts.partners} to="/partners" />
  83 + <DashboardCard label={t('page.dashboard.cardLocations')} value={counts.locations} to="/locations" />
82 84 <DashboardCard
83   - label="Work orders in progress"
  85 + label={t('page.dashboard.cardWoInProgress')}
84 86 value={counts.inProgressWorkOrders}
85 87 to="/shop-floor"
86 88 highlight
87 89 />
88   - <DashboardCard label="Sales orders" value={counts.salesOrders} to="/sales-orders" />
  90 + <DashboardCard label={t('page.dashboard.cardSalesOrders')} value={counts.salesOrders} to="/sales-orders" />
89 91 <DashboardCard
90   - label="Purchase orders"
  92 + label={t('page.dashboard.cardPurchaseOrders')}
91 93 value={counts.purchaseOrders}
92 94 to="/purchase-orders"
93 95 />
94   - <DashboardCard label="Work orders" value={counts.workOrders} to="/work-orders" />
  96 + <DashboardCard label={t('page.dashboard.cardWorkOrders')} value={counts.workOrders} to="/work-orders" />
95 97 <DashboardCard
96   - label="Journal entries"
  98 + label={t('page.dashboard.cardJournalEntries')}
97 99 value={counts.journalEntries}
98 100 to="/journal-entries"
99 101 />
100 102 </div>
101 103 )}
102 104 <div className="mt-8 card p-5 text-sm text-slate-600">
103   - <h2 className="mb-2 text-base font-semibold text-slate-800">Getting started</h2>
  105 + <h2 className="mb-2 text-base font-semibold text-slate-800">{t('page.dashboard.gettingStarted')}</h2>
104 106 <p className="mb-3 text-slate-500">
105   - The framework's buy-make-sell loop, end to end.
  107 + {t('page.dashboard.gettingStartedDesc')}
106 108 </p>
107 109 <ol className="list-decimal space-y-2 pl-5">
108 110 <li>
109   - <strong>Set up master data</strong> — create{' '}
110   - <Link to="/items/new" className="text-brand-600 hover:underline">items</Link>,{' '}
111   - <Link to="/partners/new" className="text-brand-600 hover:underline">partners</Link>, and{' '}
112   - <Link to="/locations/new" className="text-brand-600 hover:underline">locations</Link>.
113   - Then{' '}
114   - <Link to="/balances/adjust" className="text-brand-600 hover:underline">adjust stock</Link>{' '}
115   - to set opening balances.
  111 + <strong>{t('page.dashboard.step1')}</strong> — {t('page.dashboard.step1Desc')}{' '}
  112 + <Link to="/items/new" className="text-brand-600 hover:underline">{t('page.dashboard.step1DescItems')}</Link>,{' '}
  113 + <Link to="/partners/new" className="text-brand-600 hover:underline">{t('page.dashboard.step1DescPartners')}</Link>{t('page.dashboard.step1DescAnd')}{' '}
  114 + <Link to="/locations/new" className="text-brand-600 hover:underline">{t('page.dashboard.step1DescLocations')}</Link>{t('page.dashboard.step1DescThen')}{' '}
  115 + <Link to="/balances/adjust" className="text-brand-600 hover:underline">{t('page.dashboard.step1DescAdjust')}</Link>{' '}
  116 + {t('page.dashboard.step1DescEnd')}
116 117 </li>
117 118 <li>
118   - <strong>Create a sales order</strong> —{' '}
119   - <Link to="/sales-orders/new" className="text-brand-600 hover:underline">new order</Link>{' '}
120   - with line items. Confirm it — the system auto-generates production work orders and
121   - posts an AR journal entry with double-entry lines (DR Accounts Receivable, CR Revenue).
  119 + <strong>{t('page.dashboard.step2')}</strong> —{' '}
  120 + <Link to="/sales-orders/new" className="text-brand-600 hover:underline">{t('page.dashboard.step2Link')}</Link>{' '}
  121 + {t('page.dashboard.step2Desc')}
122 122 </li>
123 123 <li>
124   - <strong>Walk the work order</strong> — start it, walk routing operations on the{' '}
125   - <Link to="/shop-floor" className="text-brand-600 hover:underline">Shop Floor</Link>,
126   - then complete it. Materials are consumed, finished goods credited.
  124 + <strong>{t('page.dashboard.step3')}</strong> — {t('page.dashboard.step3Desc1')}{' '}
  125 + <Link to="/shop-floor" className="text-brand-600 hover:underline">{t('page.dashboard.step3ShopFloor')}</Link>{t('page.dashboard.step3Desc2')}
127 126 </li>
128 127 <li>
129   - <strong>Ship the sales order</strong> — stock leaves the warehouse, the AR journal
130   - entry settles. View the ledger in{' '}
131   - <Link to="/movements" className="text-brand-600 hover:underline">Movements</Link>{' '}
132   - and double-entry lines in{' '}
133   - <Link to="/journal-entries" className="text-brand-600 hover:underline">Journal Entries</Link>.
  128 + <strong>{t('page.dashboard.step4')}</strong> — {t('page.dashboard.step4Desc1')}{' '}
  129 + <Link to="/movements" className="text-brand-600 hover:underline">{t('page.dashboard.step4Movements')}</Link>{' '}
  130 + {t('page.dashboard.step4Desc2')}{' '}
  131 + <Link to="/journal-entries" className="text-brand-600 hover:underline">{t('page.dashboard.step4JE')}</Link>.
134 132 </li>
135 133 <li>
136   - <strong>Restock via purchase</strong> — create a{' '}
137   - <Link to="/purchase-orders/new" className="text-brand-600 hover:underline">purchase order</Link>,
138   - confirm, and receive into a warehouse. AP journal entry posts and settles.
  134 + <strong>{t('page.dashboard.step5')}</strong> — {t('page.dashboard.step5Desc1')}{' '}
  135 + <Link to="/purchase-orders/new" className="text-brand-600 hover:underline">{t('page.dashboard.step5Link')}</Link>{t('page.dashboard.step5Desc2')}
139 136 </li>
140 137 </ol>
141 138 </div>
... ...
web/src/pages/EditItemPage.tsx
... ... @@ -6,12 +6,14 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 6 import { Loading } from '@/components/Loading'
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DynamicExtFields } from '@/components/DynamicExtFields'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10  
10 11 const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const
11 12  
12 13 export function EditItemPage() {
13 14 const { id = '' } = useParams<{ id: string }>()
14 15 const navigate = useNavigate()
  16 + const t = useT()
15 17 const [item, setItem] = useState<Item | null>(null)
16 18 const [name, setName] = useState('')
17 19 const [description, setDescription] = useState('')
... ... @@ -60,39 +62,39 @@ export function EditItemPage() {
60 62 return (
61 63 <div>
62 64 <PageHeader
63   - title={`Edit ${item.code}`}
64   - subtitle={`Base UoM: ${item.baseUomCode} (read-only after creation)`}
65   - actions={<button className="btn-secondary" onClick={() => navigate('/items')}>Cancel</button>}
  65 + title={t('page.editItem.title').replace('{code}', item.code)}
  66 + subtitle={t('page.editItem.subtitle').replace('{uom}', item.baseUomCode)}
  67 + actions={<button className="btn-secondary" onClick={() => navigate('/items')}>{t('action.cancel')}</button>}
66 68 />
67 69 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl">
68 70 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
69 71 <div>
70   - <label className="block text-sm font-medium text-slate-700">Name</label>
  72 + <label className="block text-sm font-medium text-slate-700">{t('label.name')}</label>
71 73 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
72 74 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
73 75 </div>
74 76 <div>
75   - <label className="block text-sm font-medium text-slate-700">Type</label>
  77 + <label className="block text-sm font-medium text-slate-700">{t('label.type')}</label>
76 78 <select value={itemType} onChange={(e) => setItemType(e.target.value)}
77 79 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
78   - {ITEM_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
  80 + {ITEM_TYPES.map((it) => <option key={it} value={it}>{it}</option>)}
79 81 </select>
80 82 </div>
81 83 </div>
82 84 <div>
83   - <label className="block text-sm font-medium text-slate-700">Description</label>
  85 + <label className="block text-sm font-medium text-slate-700">{t('label.description')}</label>
84 86 <input type="text" value={description} onChange={(e) => setDescription(e.target.value)}
85 87 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
86 88 </div>
87 89 <div className="flex items-center gap-2">
88 90 <input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)}
89 91 className="rounded border-slate-300" id="active" />
90   - <label htmlFor="active" className="text-sm text-slate-700">Active</label>
  92 + <label htmlFor="active" className="text-sm text-slate-700">{t('label.active')}</label>
91 93 </div>
92 94 <DynamicExtFields entityName="Item" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} />
93 95 {error && <ErrorBox error={error} />}
94 96 <button type="submit" className="btn-primary" disabled={submitting}>
95   - {submitting ? 'Saving…' : 'Save Changes'}
  97 + {submitting ? t('action.saving') : t('action.saveChanges')}
96 98 </button>
97 99 </form>
98 100 </div>
... ...
web/src/pages/EditPartnerPage.tsx
... ... @@ -6,12 +6,14 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 6 import { Loading } from '@/components/Loading'
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DynamicExtFields } from '@/components/DynamicExtFields'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10  
10 11 const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const
11 12  
12 13 export function EditPartnerPage() {
13 14 const { id = '' } = useParams<{ id: string }>()
14 15 const navigate = useNavigate()
  16 + const t = useT()
15 17 const [partner, setPartner] = useState<Partner | null>(null)
16 18 const [name, setName] = useState('')
17 19 const [type, setType] = useState<string>('CUSTOMER')
... ... @@ -61,31 +63,31 @@ export function EditPartnerPage() {
61 63 return (
62 64 <div>
63 65 <PageHeader
64   - title={`Edit ${partner.code}`}
65   - subtitle={`Partner code is read-only after creation`}
66   - actions={<button className="btn-secondary" onClick={() => navigate('/partners')}>Cancel</button>}
  66 + title={t('page.editPartner.title').replace('{code}', partner.code)}
  67 + subtitle={t('page.editPartner.subtitle')}
  68 + actions={<button className="btn-secondary" onClick={() => navigate('/partners')}>{t('action.cancel')}</button>}
67 69 />
68 70 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl">
69 71 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
70 72 <div>
71   - <label className="block text-sm font-medium text-slate-700">Name</label>
  73 + <label className="block text-sm font-medium text-slate-700">{t('label.name')}</label>
72 74 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
73 75 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
74 76 </div>
75 77 <div>
76   - <label className="block text-sm font-medium text-slate-700">Type</label>
  78 + <label className="block text-sm font-medium text-slate-700">{t('label.type')}</label>
77 79 <select value={type} onChange={(e) => setType(e.target.value)}
78 80 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
79   - {PARTNER_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
  81 + {PARTNER_TYPES.map((pt) => <option key={pt} value={pt}>{pt}</option>)}
80 82 </select>
81 83 </div>
82 84 <div>
83   - <label className="block text-sm font-medium text-slate-700">Email</label>
  85 + <label className="block text-sm font-medium text-slate-700">{t('label.email')}</label>
84 86 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
85 87 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
86 88 </div>
87 89 <div>
88   - <label className="block text-sm font-medium text-slate-700">Phone</label>
  90 + <label className="block text-sm font-medium text-slate-700">{t('label.phone')}</label>
89 91 <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)}
90 92 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
91 93 </div>
... ... @@ -93,7 +95,7 @@ export function EditPartnerPage() {
93 95 <DynamicExtFields entityName="Partner" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} />
94 96 {error && <ErrorBox error={error} />}
95 97 <button type="submit" className="btn-primary" disabled={submitting}>
96   - {submitting ? 'Saving…' : 'Save Changes'}
  98 + {submitting ? t('action.saving') : t('action.saveChanges')}
97 99 </button>
98 100 </form>
99 101 </div>
... ...
web/src/pages/FormDesignerPage.tsx
... ... @@ -17,6 +17,7 @@ import { ErrorBox } from &#39;@/components/ErrorBox&#39;
17 17 import { Loading } from '@/components/Loading'
18 18 import { vibeWidgets } from '@/components/form-widgets'
19 19 import { vibeTemplates } from '@/components/form-widgets/vibeErpTheme'
  20 +import { useT } from '@/i18n/LocaleContext'
20 21  
21 22 // ── Types ──────────────────────────────────────────────────────────
22 23  
... ... @@ -165,6 +166,7 @@ function hydrateFields(def: FormDefinition): DesignerField[] {
165 166 export function FormDesignerPage() {
166 167 const { slug: routeSlug } = useParams<{ slug: string }>()
167 168 const navigate = useNavigate()
  169 + const t = useT()
168 170 const isEdit = Boolean(routeSlug)
169 171  
170 172 // ── Top bar state ──
... ... @@ -302,15 +304,15 @@ export function FormDesignerPage() {
302 304 return (
303 305 <div>
304 306 <PageHeader
305   - title={isEdit ? 'Edit Form Definition' : 'New Form Definition'}
306   - subtitle="Design a metadata-driven form with a live preview."
  307 + title={isEdit ? t('page.formDesigner.editTitle') : t('page.formDesigner.newTitle')}
  308 + subtitle={t('page.formDesigner.subtitle')}
307 309 />
308 310  
309 311 {/* ── Top bar ── */}
310 312 <div className="card p-4 mb-4">
311 313 <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
312 314 <div>
313   - <label className="block text-sm font-medium text-slate-700">Title</label>
  315 + <label className="block text-sm font-medium text-slate-700">{t('label.title')}</label>
314 316 <input
315 317 type="text"
316 318 value={title}
... ... @@ -320,7 +322,7 @@ export function FormDesignerPage() {
320 322 />
321 323 </div>
322 324 <div>
323   - <label className="block text-sm font-medium text-slate-700">Entity name</label>
  325 + <label className="block text-sm font-medium text-slate-700">{t('label.entityName')}</label>
324 326 <input
325 327 type="text"
326 328 value={entityName}
... ... @@ -330,7 +332,7 @@ export function FormDesignerPage() {
330 332 />
331 333 </div>
332 334 <div>
333   - <label className="block text-sm font-medium text-slate-700">Purpose</label>
  335 + <label className="block text-sm font-medium text-slate-700">{t('label.purpose')}</label>
334 336 <select
335 337 value={purpose}
336 338 onChange={(e) => setPurpose(e.target.value as FormPurpose)}
... ... @@ -344,7 +346,7 @@ export function FormDesignerPage() {
344 346 </select>
345 347 </div>
346 348 <div>
347   - <label className="block text-sm font-medium text-slate-700">Slug</label>
  349 + <label className="block text-sm font-medium text-slate-700">{t('label.slug')}</label>
348 350 <input
349 351 type="text"
350 352 value={slug}
... ... @@ -363,14 +365,14 @@ export function FormDesignerPage() {
363 365 disabled={saving}
364 366 onClick={handleSave}
365 367 >
366   - {saving ? 'Saving...' : 'Save'}
  368 + {saving ? t('action.saving') : t('action.save')}
367 369 </button>
368 370 <button
369 371 type="button"
370 372 className="btn-secondary"
371 373 onClick={() => navigate('/admin/metadata')}
372 374 >
373   - Discard
  375 + {t('action.discard')}
374 376 </button>
375 377 </div>
376 378 </div>
... ... @@ -387,7 +389,7 @@ export function FormDesignerPage() {
387 389 {/* ── Left panel: field list (3/5 = 60%) ── */}
388 390 <div className="lg:col-span-3 space-y-2">
389 391 <h2 className="text-xs font-semibold uppercase tracking-wide text-slate-400 mb-3">
390   - Fields
  392 + {t('label.fields')}
391 393 </h2>
392 394  
393 395 {fields.map((field, idx) => (
... ... @@ -409,10 +411,10 @@ export function FormDesignerPage() {
409 411  
410 412 <div className="flex gap-2 pt-2">
411 413 <button type="button" className="btn-secondary" onClick={addField}>
412   - + Add Field
  414 + {t('action.addField')}
413 415 </button>
414 416 <button type="button" className="btn-secondary" onClick={addSection}>
415   - + Add Section Divider
  417 + {t('action.addSectionDivider')}
416 418 </button>
417 419 </div>
418 420 </div>
... ... @@ -420,12 +422,12 @@ export function FormDesignerPage() {
420 422 {/* ── Right panel: live preview (2/5 = 40%) ── */}
421 423 <div className="lg:col-span-2">
422 424 <h2 className="text-xs font-semibold uppercase tracking-wide text-slate-400 mb-3">
423   - Live Preview
  425 + {t('label.livePreview')}
424 426 </h2>
425 427 <div className="card p-6">
426 428 {Object.keys(jsonSchema.properties ?? {}).length === 0 ? (
427 429 <p className="text-sm text-slate-400 italic">
428   - Add at least one field with a key to see the preview.
  430 + {t('label.addFieldHint')}
429 431 </p>
430 432 ) : (
431 433 <Form
... ... @@ -475,6 +477,8 @@ function FieldRow({
475 477 onRemove,
476 478 onMove,
477 479 }: FieldRowProps) {
  480 + const t = useT()
  481 +
478 482 if (field.isSectionDivider) {
479 483 return (
480 484 <div className="card p-3 border-l-4 border-indigo-300">
... ... @@ -485,20 +489,20 @@ function FieldRow({
485 489 onMove={onMove}
486 490 />
487 491 <span className="text-xs font-semibold uppercase text-indigo-500 mr-2">
488   - Section
  492 + {t('label.section')}
489 493 </span>
490 494 <input
491 495 type="text"
492 496 value={field.sectionTitle ?? ''}
493 497 onChange={(e) => onUpdate({ sectionTitle: e.target.value })}
494   - placeholder="Section title"
  498 + placeholder={t('label.sectionTitle')}
495 499 className="flex-1 rounded-md border border-slate-300 px-2 py-1 text-sm"
496 500 />
497 501 <button
498 502 type="button"
499 503 onClick={onRemove}
500 504 className="text-slate-400 hover:text-rose-500 text-sm px-1"
501   - title="Remove section"
  505 + title={t('label.removeSection')}
502 506 >
503 507 x
504 508 </button>
... ... @@ -524,7 +528,7 @@ function FieldRow({
524 528 type="text"
525 529 value={field.label}
526 530 onChange={(e) => onUpdate({ label: e.target.value })}
527   - placeholder="Label"
  531 + placeholder={t('label.label')}
528 532 className="flex-1 rounded-md border border-slate-300 px-2 py-1 text-sm"
529 533 />
530 534 <select
... ... @@ -534,9 +538,9 @@ function FieldRow({
534 538 }
535 539 className="w-24 rounded-md border border-slate-300 px-2 py-1 text-sm"
536 540 >
537   - {FIELD_TYPES.map((t) => (
538   - <option key={t} value={t}>
539   - {t}
  541 + {FIELD_TYPES.map((ft) => (
  542 + <option key={ft} value={ft}>
  543 + {ft}
540 544 </option>
541 545 ))}
542 546 </select>
... ... @@ -547,7 +551,7 @@ function FieldRow({
547 551 onChange={(e) => onUpdate({ required: e.target.checked })}
548 552 className="rounded border-slate-300"
549 553 />
550   - Req
  554 + {t('label.req')}
551 555 </label>
552 556 <select
553 557 value={field.width}
... ... @@ -555,11 +559,11 @@ function FieldRow({
555 559 onUpdate({ width: Number(e.target.value) as 1 | 2 | 3 })
556 560 }
557 561 className="w-20 rounded-md border border-slate-300 px-2 py-1 text-sm"
558   - title="Column span"
  562 + title={t('label.colSpan')}
559 563 >
560 564 {WIDTH_OPTIONS.map((w) => (
561 565 <option key={w} value={w}>
562   - {w} col{w > 1 ? 's' : ''}
  566 + {w > 1 ? t('label.cols').replace('{n}', String(w)) : t('label.col').replace('{n}', String(w))}
563 567 </option>
564 568 ))}
565 569 </select>
... ... @@ -567,7 +571,7 @@ function FieldRow({
567 571 type="button"
568 572 onClick={onToggleExpand}
569 573 className="text-slate-400 hover:text-slate-600 text-sm px-1"
570   - title="Toggle details"
  574 + title={t('label.toggleDetails')}
571 575 >
572 576 {isExpanded ? '\u25B2' : '\u25BC'}
573 577 </button>
... ... @@ -575,7 +579,7 @@ function FieldRow({
575 579 type="button"
576 580 onClick={onRemove}
577 581 className="text-slate-400 hover:text-rose-500 text-sm px-1"
578   - title="Remove field"
  582 + title={t('label.removeField')}
579 583 >
580 584 x
581 585 </button>
... ... @@ -586,7 +590,7 @@ function FieldRow({
586 590 <div className="mt-3 ml-10 grid grid-cols-1 gap-3 sm:grid-cols-2 border-t border-slate-100 pt-3">
587 591 <div>
588 592 <label className="block text-sm font-medium text-slate-700">
589   - Label (English)
  593 + {t('label.labelEnglish')}
590 594 </label>
591 595 <input
592 596 type="text"
... ... @@ -597,7 +601,7 @@ function FieldRow({
597 601 </div>
598 602 <div>
599 603 <label className="block text-sm font-medium text-slate-700">
600   - Placeholder
  604 + {t('label.placeholder')}
601 605 </label>
602 606 <input
603 607 type="text"
... ... @@ -610,7 +614,7 @@ function FieldRow({
610 614 </div>
611 615 <div className="sm:col-span-2">
612 616 <label className="block text-sm font-medium text-slate-700">
613   - Help text
  617 + {t('label.helpText')}
614 618 </label>
615 619 <input
616 620 type="text"
... ... @@ -623,7 +627,7 @@ function FieldRow({
623 627 </div>
624 628 <div>
625 629 <label className="block text-sm font-medium text-slate-700">
626   - Widget override
  630 + {t('label.widgetOverride')}
627 631 </label>
628 632 <select
629 633 value={field.widgetOverride ?? ''}
... ... @@ -634,17 +638,17 @@ function FieldRow({
634 638 >
635 639 {WIDGET_OPTIONS.map((w) => (
636 640 <option key={w} value={w}>
637   - {w || '(default)'}
  641 + {w || t('label.widgetDefault')}
638 642 </option>
639 643 ))}
640 644 </select>
641 645 </div>
642 646 <div>
643 647 <label className="block text-sm font-medium text-slate-700">
644   - Visibility condition
  648 + {t('label.visibilityCondition')}
645 649 </label>
646 650 <div className="mt-1 flex items-center gap-2">
647   - <span className="text-sm text-slate-500">Show when</span>
  651 + <span className="text-sm text-slate-500">{t('label.showWhen')}</span>
648 652 <select
649 653 value={field.visibleWhen?.field ?? ''}
650 654 onChange={(e) =>
... ... @@ -659,7 +663,7 @@ function FieldRow({
659 663 }
660 664 className="rounded-md border border-slate-300 px-2 py-1 text-sm"
661 665 >
662   - <option value="">(always)</option>
  666 + <option value="">{t('label.always')}</option>
663 667 {fieldKeys
664 668 .filter((k) => k !== field.key)
665 669 .map((k) => (
... ... @@ -668,7 +672,7 @@ function FieldRow({
668 672 </option>
669 673 ))}
670 674 </select>
671   - <span className="text-sm text-slate-500">equals</span>
  675 + <span className="text-sm text-slate-500">{t('label.equals')}</span>
672 676 <input
673 677 type="text"
674 678 value={field.visibleWhen?.equals ?? ''}
... ... @@ -702,6 +706,7 @@ function ReorderButtons({
702 706 total: number
703 707 onMove: (direction: 'up' | 'down') => void
704 708 }) {
  709 + const t = useT()
705 710 return (
706 711 <div className="flex flex-col">
707 712 <button
... ... @@ -709,7 +714,7 @@ function ReorderButtons({
709 714 onClick={() => onMove('up')}
710 715 disabled={index === 0}
711 716 className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-xs leading-none"
712   - title="Move up"
  717 + title={t('label.moveUp')}
713 718 >
714 719 &#9650;
715 720 </button>
... ... @@ -718,7 +723,7 @@ function ReorderButtons({
718 723 onClick={() => onMove('down')}
719 724 disabled={index === total - 1}
720 725 className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-xs leading-none"
721   - title="Move down"
  726 + title={t('label.moveDown')}
722 727 >
723 728 &#9660;
724 729 </button>
... ...
web/src/pages/ItemsPage.tsx
... ... @@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 6 import { Loading } from '@/components/Loading'
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10  
10 11 export function ItemsPage() {
  12 + const t = useT()
11 13 const [rows, setRows] = useState<Item[]>([])
12 14 const [error, setError] = useState<Error | null>(null)
13 15 const [loading, setLoading] = useState(true)
... ... @@ -22,28 +24,28 @@ export function ItemsPage() {
22 24  
23 25 const columns: Column<Item>[] = [
24 26 {
25   - header: 'Code',
  27 + header: t('label.code'),
26 28 key: 'code',
27 29 render: (r) => (
28 30 <Link to={`/items/${r.id}/edit`} className="font-mono text-brand-600 hover:underline">{r.code}</Link>
29 31 ),
30 32 },
31   - { header: 'Name', key: 'name' },
32   - { header: 'Type', key: 'itemType' },
  33 + { header: t('label.name'), key: 'name' },
  34 + { header: t('label.type'), key: 'itemType' },
33 35 { header: 'UoM', key: 'baseUomCode', render: (r) => <span className="font-mono">{r.baseUomCode}</span> },
34 36 {
35   - header: 'Active',
  37 + header: t('label.active'),
36 38 key: 'active',
37   - render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>),
  39 + render: (r) => (r.active ? <span className="text-emerald-600">{'\u25CF'}</span> : <span className="text-slate-300">{'\u25CF'}</span>),
38 40 },
39 41 ]
40 42  
41 43 return (
42 44 <div>
43 45 <PageHeader
44   - title="Items"
45   - subtitle="Catalog of items the framework can transact: raw materials, finished goods, services."
46   - actions={<Link to="/items/new" className="btn-primary">+ New Item</Link>}
  46 + title={t('page.items.title')}
  47 + subtitle={t('page.items.subtitle')}
  48 + actions={<Link to="/items/new" className="btn-primary">{t('action.newItem')}</Link>}
47 49 />
48 50 {loading && <Loading />}
49 51 {error && <ErrorBox error={error} />}
... ...
web/src/pages/JournalEntriesPage.tsx
... ... @@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
5 5 import { Loading } from '@/components/Loading'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { StatusBadge } from '@/components/StatusBadge'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 export function JournalEntriesPage() {
  11 + const t = useT()
10 12 const [rows, setRows] = useState<JournalEntry[]>([])
11 13 const [error, setError] = useState<Error | null>(null)
12 14 const [loading, setLoading] = useState(true)
... ... @@ -34,26 +36,26 @@ export function JournalEntriesPage() {
34 36 return (
35 37 <div>
36 38 <PageHeader
37   - title="Journal Entries"
38   - subtitle="Double-entry GL entries posted by pbc-finance from order lifecycle events. Click a row to see debit/credit lines."
  39 + title={t('page.journalEntries.title')}
  40 + subtitle={t('page.journalEntries.subtitle')}
39 41 />
40 42 {loading && <Loading />}
41 43 {error && <ErrorBox error={error} />}
42 44 {!loading && !error && rows.length === 0 && (
43   - <div className="card p-6 text-sm text-slate-400">No journal entries yet.</div>
  45 + <div className="card p-6 text-sm text-slate-400">{t('label.noJournalEntriesYet')}</div>
44 46 )}
45 47 {!loading && !error && rows.length > 0 && (
46 48 <div className="card overflow-x-auto">
47 49 <table className="table-base">
48 50 <thead className="bg-slate-50">
49 51 <tr>
50   - <th>Posted</th>
51   - <th>Type</th>
52   - <th>Status</th>
53   - <th>Order</th>
54   - <th>Partner</th>
55   - <th>Amount</th>
56   - <th>Lines</th>
  52 + <th>{t('label.posted')}</th>
  53 + <th>{t('label.type')}</th>
  54 + <th>{t('label.status')}</th>
  55 + <th>{t('label.order')}</th>
  56 + <th>{t('label.partner')}</th>
  57 + <th>{t('label.amount')}</th>
  58 + <th>{t('label.lines')}</th>
57 59 </tr>
58 60 </thead>
59 61 <tbody className="divide-y divide-slate-100">
... ... @@ -66,7 +68,7 @@ export function JournalEntriesPage() {
66 68 className="hover:bg-slate-50 cursor-pointer"
67 69 onClick={() => toggle(je.id)}
68 70 >
69   - <td>{je.postedAt ? new Date(je.postedAt).toLocaleString() : ''}</td>
  71 + <td>{je.postedAt ? new Date(je.postedAt).toLocaleString() : '\u2014'}</td>
70 72 <td>{je.type}</td>
71 73 <td><StatusBadge status={je.status} /></td>
72 74 <td className="font-mono">{je.orderCode}</td>
... ... @@ -83,11 +85,11 @@ export function JournalEntriesPage() {
83 85 <table className="min-w-full text-xs">
84 86 <thead>
85 87 <tr className="text-slate-400">
86   - <th className="text-left px-2 py-1">#</th>
87   - <th className="text-left px-2 py-1">Account</th>
88   - <th className="text-right px-2 py-1">Debit</th>
89   - <th className="text-right px-2 py-1">Credit</th>
90   - <th className="text-left px-2 py-1">Description</th>
  88 + <th className="text-left px-2 py-1">{t('label.lineNo')}</th>
  89 + <th className="text-left px-2 py-1">{t('label.accountType')}</th>
  90 + <th className="text-right px-2 py-1">{t('label.debit')}</th>
  91 + <th className="text-right px-2 py-1">{t('label.credit')}</th>
  92 + <th className="text-left px-2 py-1">{t('label.description')}</th>
91 93 </tr>
92 94 </thead>
93 95 <tbody>
... ...
web/src/pages/ListViewDesignerPage.tsx
... ... @@ -16,6 +16,7 @@ import { metadata } from &#39;@/api/client&#39;
16 16 import { PageHeader } from '@/components/PageHeader'
17 17 import { ErrorBox } from '@/components/ErrorBox'
18 18 import { DataTable, type Column } from '@/components/DataTable'
  19 +import { useT } from '@/i18n/LocaleContext'
19 20  
20 21 // ─── Designer state types ──────────────────────────────────────────
21 22  
... ... @@ -65,6 +66,7 @@ function emptyState(): DesignerState {
65 66  
66 67 export function ListViewDesignerPage() {
67 68 const navigate = useNavigate()
  69 + const t = useT()
68 70 const { slug: routeSlug } = useParams<{ slug: string }>()
69 71 const isEdit = Boolean(routeSlug)
70 72  
... ... @@ -254,7 +256,7 @@ export function ListViewDesignerPage() {
254 256  
255 257 if (loading) {
256 258 return (
257   - <div className="p-6 text-sm text-slate-500">Loading...</div>
  259 + <div className="p-6 text-sm text-slate-500">{t('label.loading')}</div>
258 260 )
259 261 }
260 262  
... ... @@ -268,14 +270,14 @@ export function ListViewDesignerPage() {
268 270 return (
269 271 <div>
270 272 <PageHeader
271   - title={isEdit ? 'Edit List View' : 'New List View'}
272   - subtitle="Configure columns, filters, sorting, and pagination for an entity list view."
  273 + title={isEdit ? t('page.listViewDesigner.editTitle') : t('page.listViewDesigner.newTitle')}
  274 + subtitle={t('page.listViewDesigner.subtitle')}
273 275 actions={
274 276 <button
275 277 className="btn-secondary"
276 278 onClick={() => navigate('/admin/metadata')}
277 279 >
278   - Cancel
  280 + {t('action.cancel')}
279 281 </button>
280 282 }
281 283 />
... ... @@ -285,11 +287,11 @@ export function ListViewDesignerPage() {
285 287  
286 288 {/* ── Top bar: title, entity, slug ───────────────────── */}
287 289 <div className="card p-6 space-y-4">
288   - <h2 className="text-lg font-semibold text-slate-800">General</h2>
  290 + <h2 className="text-lg font-semibold text-slate-800">{t('label.general')}</h2>
289 291 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
290 292 <div>
291 293 <label className="block text-sm font-medium text-slate-700">
292   - Title
  294 + {t('label.title')}
293 295 </label>
294 296 <input
295 297 type="text"
... ... @@ -302,7 +304,7 @@ export function ListViewDesignerPage() {
302 304 </div>
303 305 <div>
304 306 <label className="block text-sm font-medium text-slate-700">
305   - Entity Name
  307 + {t('label.entityName')}
306 308 </label>
307 309 <input
308 310 type="text"
... ... @@ -315,7 +317,7 @@ export function ListViewDesignerPage() {
315 317 </div>
316 318 <div>
317 319 <label className="block text-sm font-medium text-slate-700">
318   - Slug
  320 + {t('label.slug')}
319 321 </label>
320 322 <input
321 323 type="text"
... ... @@ -333,31 +335,31 @@ export function ListViewDesignerPage() {
333 335 {/* ── Columns section ────────────────────────────────── */}
334 336 <div className="card p-6 space-y-4">
335 337 <div className="flex items-center justify-between">
336   - <h2 className="text-lg font-semibold text-slate-800">Columns</h2>
  338 + <h2 className="text-lg font-semibold text-slate-800">{t('label.columns')}</h2>
337 339 <button
338 340 type="button"
339 341 className="btn-secondary text-xs"
340 342 onClick={addColumn}
341 343 >
342   - + Add Column
  344 + {t('action.addColumn')}
343 345 </button>
344 346 </div>
345 347  
346 348 {state.columns.length === 0 ? (
347 349 <p className="text-sm text-slate-400">
348   - No columns defined yet. Click "Add Column" to start.
  350 + {t('label.noColumnsHint')}
349 351 </p>
350 352 ) : (
351 353 <div className="overflow-x-auto">
352 354 <table className="table-base text-sm">
353 355 <thead className="bg-slate-50">
354 356 <tr>
355   - <th className="w-10">Show</th>
356   - <th>Field</th>
357   - <th>Label</th>
358   - <th className="w-28">Format</th>
359   - <th className="w-16">Sortable</th>
360   - <th className="w-24">Order</th>
  357 + <th className="w-10">{t('label.show')}</th>
  358 + <th>{t('label.field')}</th>
  359 + <th>{t('label.label')}</th>
  360 + <th className="w-28">{t('label.format')}</th>
  361 + <th className="w-16">{t('label.sortable')}</th>
  362 + <th className="w-24">{t('label.orderMeta')}</th>
361 363 <th className="w-10"></th>
362 364 </tr>
363 365 </thead>
... ... @@ -428,7 +430,7 @@ export function ListViewDesignerPage() {
428 430 className="rounded p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 disabled:opacity-30"
429 431 disabled={idx === 0}
430 432 onClick={() => moveColumn(idx, -1)}
431   - title="Move up"
  433 + title={t('label.moveUp')}
432 434 >
433 435 &#9650;
434 436 </button>
... ... @@ -437,7 +439,7 @@ export function ListViewDesignerPage() {
437 439 className="rounded p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 disabled:opacity-30"
438 440 disabled={idx === state.columns.length - 1}
439 441 onClick={() => moveColumn(idx, 1)}
440   - title="Move down"
  442 + title={t('label.moveDown')}
441 443 >
442 444 &#9660;
443 445 </button>
... ... @@ -448,7 +450,7 @@ export function ListViewDesignerPage() {
448 450 type="button"
449 451 className="text-slate-400 hover:text-rose-500"
450 452 onClick={() => removeColumn(idx)}
451   - title="Remove column"
  453 + title={t('label.removeColumn')}
452 454 >
453 455 &times;
454 456 </button>
... ... @@ -464,19 +466,19 @@ export function ListViewDesignerPage() {
464 466 {/* ── Filters section ────────────────────────────────── */}
465 467 <div className="card p-6 space-y-4">
466 468 <div className="flex items-center justify-between">
467   - <h2 className="text-lg font-semibold text-slate-800">Filters</h2>
  469 + <h2 className="text-lg font-semibold text-slate-800">{t('label.filters')}</h2>
468 470 <button
469 471 type="button"
470 472 className="btn-secondary text-xs"
471 473 onClick={addFilter}
472 474 >
473   - + Add Filter
  475 + {t('action.addFilter')}
474 476 </button>
475 477 </div>
476 478  
477 479 {state.filters.length === 0 ? (
478 480 <p className="text-sm text-slate-400">
479   - No filters defined. Click "Add Filter" to add filterable fields.
  481 + {t('label.noFiltersHint')}
480 482 </p>
481 483 ) : (
482 484 <div className="space-y-2">
... ... @@ -488,7 +490,7 @@ export function ListViewDesignerPage() {
488 490 onChange={(e) =>
489 491 updateFilter(idx, { field: e.target.value })
490 492 }
491   - placeholder="Field name"
  493 + placeholder={t('label.fieldName')}
492 494 className={smallInputCls + ' flex-1'}
493 495 />
494 496 <select
... ... @@ -510,14 +512,14 @@ export function ListViewDesignerPage() {
510 512 onChange={(e) =>
511 513 updateFilter(idx, { label: e.target.value })
512 514 }
513   - placeholder="Display label"
  515 + placeholder={t('label.displayLabel')}
514 516 className={smallInputCls + ' flex-1'}
515 517 />
516 518 <button
517 519 type="button"
518 520 className="text-slate-400 hover:text-rose-500"
519 521 onClick={() => removeFilter(idx)}
520   - title="Remove filter"
  522 + title={t('label.removeFilter')}
521 523 >
522 524 &times;
523 525 </button>
... ... @@ -530,12 +532,12 @@ export function ListViewDesignerPage() {
530 532 {/* ── Sorting & page size section ────────────────────── */}
531 533 <div className="card p-6 space-y-4">
532 534 <h2 className="text-lg font-semibold text-slate-800">
533   - Sorting &amp; Pagination
  535 + {t('label.sortingPagination')}
534 536 </h2>
535 537 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
536 538 <div>
537 539 <label className="block text-sm font-medium text-slate-700">
538   - Default Sort Field
  540 + {t('label.defaultSortField')}
539 541 </label>
540 542 <select
541 543 value={state.defaultSort?.field ?? ''}
... ... @@ -552,7 +554,7 @@ export function ListViewDesignerPage() {
552 554 }}
553 555 className={inputCls}
554 556 >
555   - <option value="">-- none --</option>
  557 + <option value="">{t('label.none')}</option>
556 558 {state.columns
557 559 .filter((c) => c.field.trim())
558 560 .map((c) => (
... ... @@ -564,7 +566,7 @@ export function ListViewDesignerPage() {
564 566 </div>
565 567 <div>
566 568 <label className="block text-sm font-medium text-slate-700">
567   - Direction
  569 + {t('label.direction')}
568 570 </label>
569 571 <select
570 572 value={state.defaultSort?.direction ?? 'asc'}
... ... @@ -578,13 +580,13 @@ export function ListViewDesignerPage() {
578 580 disabled={!state.defaultSort}
579 581 className={`${inputCls}${!state.defaultSort ? ' bg-slate-50 text-slate-500' : ''}`}
580 582 >
581   - <option value="asc">Ascending</option>
582   - <option value="desc">Descending</option>
  583 + <option value="asc">{t('label.ascending')}</option>
  584 + <option value="desc">{t('label.descending')}</option>
583 585 </select>
584 586 </div>
585 587 <div>
586 588 <label className="block text-sm font-medium text-slate-700">
587   - Page Size
  589 + {t('label.pageSize')}
588 590 </label>
589 591 <input
590 592 type="number"
... ... @@ -602,10 +604,10 @@ export function ListViewDesignerPage() {
602 604  
603 605 {/* ── Preview section ────────────────────────────────── */}
604 606 <div className="card p-6 space-y-4">
605   - <h2 className="text-lg font-semibold text-slate-800">Preview</h2>
  607 + <h2 className="text-lg font-semibold text-slate-800">{t('label.preview')}</h2>
606 608 {previewColumns.length === 0 ? (
607 609 <p className="text-sm text-slate-400">
608   - Add visible columns with a field name to see a preview.
  610 + {t('label.addColumnsHint')}
609 611 </p>
610 612 ) : (
611 613 <DataTable
... ... @@ -619,14 +621,14 @@ export function ListViewDesignerPage() {
619 621 {/* ── Actions ────────────────────────────────────────── */}
620 622 <div className="flex items-center gap-3">
621 623 <button type="submit" className="btn-primary" disabled={saving}>
622   - {saving ? 'Saving...' : 'Save List View'}
  624 + {saving ? t('action.saving') : t('action.saveListView')}
623 625 </button>
624 626 <button
625 627 type="button"
626 628 className="btn-secondary"
627 629 onClick={() => navigate('/admin/metadata')}
628 630 >
629   - Cancel
  631 + {t('action.cancel')}
630 632 </button>
631 633 </div>
632 634 </form>
... ...
web/src/pages/LocationsPage.tsx
... ... @@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 6 import { Loading } from '@/components/Loading'
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10  
10 11 export function LocationsPage() {
  12 + const t = useT()
11 13 const [rows, setRows] = useState<Location[]>([])
12 14 const [error, setError] = useState<Error | null>(null)
13 15 const [loading, setLoading] = useState(true)
... ... @@ -21,22 +23,22 @@ export function LocationsPage() {
21 23 }, [])
22 24  
23 25 const columns: Column<Location>[] = [
24   - { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
25   - { header: 'Name', key: 'name' },
26   - { header: 'Type', key: 'type' },
  26 + { header: t('label.code'), key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
  27 + { header: t('label.name'), key: 'name' },
  28 + { header: t('label.type'), key: 'type' },
27 29 {
28   - header: 'Active',
  30 + header: t('label.active'),
29 31 key: 'active',
30   - render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>),
  32 + render: (r) => (r.active ? <span className="text-emerald-600">{'\u25CF'}</span> : <span className="text-slate-300">{'\u25CF'}</span>),
31 33 },
32 34 ]
33 35  
34 36 return (
35 37 <div>
36 38 <PageHeader
37   - title="Locations"
38   - subtitle="Warehouses, bins, and virtual locations the inventory PBC tracks."
39   - actions={<Link to="/locations/new" className="btn-primary">+ New Location</Link>}
  39 + title={t('page.locations.title')}
  40 + subtitle={t('page.locations.subtitle')}
  41 + actions={<Link to="/locations/new" className="btn-primary">{t('action.newLocation')}</Link>}
40 42 />
41 43 {loading && <Loading />}
42 44 {error && <ErrorBox error={error} />}
... ...
web/src/pages/LoginPage.tsx
... ... @@ -4,6 +4,7 @@ import { useAuth } from &#39;@/auth/AuthContext&#39;
4 4 import { meta } from '@/api/client'
5 5 import type { MetaInfo } from '@/types/api'
6 6 import { ErrorBox } from '@/components/ErrorBox'
  7 +import { useT } from '@/i18n/LocaleContext'
7 8  
8 9 interface LocationState {
9 10 from?: string
... ... @@ -13,6 +14,7 @@ export function LoginPage() {
13 14 const { login, token, loading } = useAuth()
14 15 const navigate = useNavigate()
15 16 const location = useLocation()
  17 + const t = useT()
16 18 const [username, setUsername] = useState('admin')
17 19 const [password, setPassword] = useState('')
18 20 const [error, setError] = useState<Error | null>(null)
... ... @@ -47,13 +49,13 @@ export function LoginPage() {
47 49 <div className="mb-6 text-center">
48 50 <h1 className="text-3xl font-bold text-brand-600">vibe_erp</h1>
49 51 <p className="mt-1 text-sm text-slate-500">
50   - Composable ERP framework for the printing industry
  52 + {t('page.login.tagline')}
51 53 </p>
52 54 </div>
53 55 <div className="card p-6">
54 56 <form onSubmit={onSubmit} className="space-y-4">
55 57 <div>
56   - <label className="block text-sm font-medium text-slate-700">Username</label>
  58 + <label className="block text-sm font-medium text-slate-700">{t('label.username')}</label>
57 59 <input
58 60 type="text"
59 61 value={username}
... ... @@ -64,7 +66,7 @@ export function LoginPage() {
64 66 />
65 67 </div>
66 68 <div>
67   - <label className="block text-sm font-medium text-slate-700">Password</label>
  69 + <label className="block text-sm font-medium text-slate-700">{t('label.password')}</label>
68 70 <input
69 71 type="password"
70 72 value={password}
... ... @@ -73,23 +75,23 @@ export function LoginPage() {
73 75 required
74 76 />
75 77 <p className="mt-1 text-xs text-slate-400">
76   - The bootstrap admin password is printed to the application boot log on first start.
  78 + {t('page.login.passwordHint')}
77 79 </p>
78 80 </div>
79 81 {error ? <ErrorBox error={error} /> : null}
80 82 <button type="submit" className="btn-primary w-full" disabled={loading}>
81   - {loading ? 'Signing in…' : 'Sign in'}
  83 + {loading ? t('action.signingIn') : t('action.signIn')}
82 84 </button>
83 85 </form>
84 86 </div>
85 87 <p className="mt-4 text-center text-xs text-slate-400">
86 88 {info ? (
87 89 <>
88   - Connected to <span className="font-mono">{info.name}</span>{' '}
  90 + {t('page.login.connectedTo')} <span className="font-mono">{info.name}</span>{' '}
89 91 <span className="font-mono">{info.implementationVersion}</span>
90 92 </>
91 93 ) : (
92   - 'Connecting…'
  94 + t('page.login.connecting')
93 95 )}
94 96 </p>
95 97 </div>
... ...
web/src/pages/MetadataAdminPage.tsx
... ... @@ -363,8 +363,8 @@ export function MetadataAdminPage() {
363 363  
364 364 const entityCols: Column<MetadataEntity>[] = [
365 365 { header: t('label.name'), key: 'name' },
366   - { header: 'PBC', key: 'pbc' },
367   - { header: 'Table', key: 'table' },
  366 + { header: t('label.pbc'), key: 'pbc' },
  367 + { header: t('label.table'), key: 'table' },
368 368 { header: t('label.description'), key: 'description', render: (r) => r.description ?? '—' },
369 369 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
370 370 ]
... ... @@ -373,8 +373,8 @@ export function MetadataAdminPage() {
373 373 { header: t('label.fieldKey'), key: 'key', render: (r) => <span className="font-mono">{r.key}</span> },
374 374 { header: t('label.targetEntity'), key: 'targetEntity' },
375 375 { header: t('label.fieldType'), key: 'type', render: (r) => r.type.kind },
376   - { header: 'Required', key: 'required', render: (r) => (r.required ? 'Yes' : 'No') },
377   - { header: 'PII', key: 'pii', render: (r) => (r.pii ? 'Yes' : 'No') },
  376 + { header: t('label.required'), key: 'required', render: (r) => (r.required ? t('label.yes') : t('label.no')) },
  377 + { header: t('label.pii'), key: 'pii', render: (r) => (r.pii ? t('label.yes') : t('label.no')) },
378 378 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
379 379 {
380 380 header: '',
... ... @@ -386,7 +386,7 @@ export function MetadataAdminPage() {
386 386 className="text-xs text-blue-600 hover:underline"
387 387 onClick={() => openCfEdit(r)}
388 388 >
389   - Edit
  389 + {t('action.edit')}
390 390 </button>
391 391 <button
392 392 className="text-xs text-rose-600 hover:underline"
... ... @@ -400,17 +400,17 @@ export function MetadataAdminPage() {
400 400 ]
401 401  
402 402 const permissionCols: Column<MetadataPermission>[] = [
403   - { header: 'Key', key: 'key', render: (r) => <span className="font-mono">{r.key}</span> },
  403 + { header: t('label.key'), key: 'key', render: (r) => <span className="font-mono">{r.key}</span> },
404 404 { header: t('label.description'), key: 'description' },
405 405 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
406 406 ]
407 407  
408 408 const menuCols: Column<MenuRow>[] = [
409   - { header: 'Path', key: 'path', render: (r) => <span className="font-mono">{r.path}</span> },
410   - { header: 'Label', key: 'label' },
411   - { header: 'Icon', key: 'icon', render: (r) => r.icon ?? '—' },
412   - { header: 'Section', key: 'section', render: (r) => r.section ?? '—' },
413   - { header: 'Order', key: 'order', render: (r) => r.order ?? '—' },
  409 + { header: t('label.path'), key: 'path', render: (r) => <span className="font-mono">{r.path}</span> },
  410 + { header: t('label.label'), key: 'label' },
  411 + { header: t('label.icon'), key: 'icon', render: (r) => r.icon ?? '—' },
  412 + { header: t('label.sectionMeta'), key: 'section', render: (r) => r.section ?? '—' },
  413 + { header: t('label.orderMeta'), key: 'order', render: (r) => r.order ?? '—' },
414 414 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
415 415 ]
416 416  
... ... @@ -428,9 +428,9 @@ export function MetadataAdminPage() {
428 428 ),
429 429 },
430 430 { header: t('label.entity'), key: 'entityName' },
431   - { header: 'Title', key: 'title' },
  431 + { header: t('label.title'), key: 'title' },
432 432 { header: t('label.purpose'), key: 'purpose' },
433   - { header: 'Version', key: 'version' },
  433 + { header: t('label.version'), key: 'version' },
434 434 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
435 435 {
436 436 header: '',
... ... @@ -463,9 +463,9 @@ export function MetadataAdminPage() {
463 463 ),
464 464 },
465 465 { header: t('label.entity'), key: 'entityName' },
466   - { header: 'Title', key: 'title' },
  466 + { header: t('label.title'), key: 'title' },
467 467 { header: t('label.pageSize'), key: 'pageSize' },
468   - { header: 'Version', key: 'version' },
  468 + { header: t('label.version'), key: 'version' },
469 469 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
470 470 {
471 471 header: '',
... ... @@ -521,7 +521,7 @@ export function MetadataAdminPage() {
521 521 className="text-xs text-blue-600 hover:underline"
522 522 onClick={() => openRuleEdit(r)}
523 523 >
524   - Edit
  524 + {t('action.edit')}
525 525 </button>
526 526 <button
527 527 className="text-xs text-rose-600 hover:underline"
... ... @@ -624,7 +624,7 @@ export function MetadataAdminPage() {
624 624 {cfTypeKind === 'enum' && (
625 625 <div>
626 626 <label className="block text-xs font-medium text-slate-700">
627   - Allowed values (comma-separated)
  627 + {t('label.allowedValues')}
628 628 </label>
629 629 <input
630 630 type="text"
... ... @@ -638,7 +638,7 @@ export function MetadataAdminPage() {
638 638 {cfTypeKind === 'string' && (
639 639 <div>
640 640 <label className="block text-xs font-medium text-slate-700">
641   - Max length
  641 + {t('label.maxLength')}
642 642 </label>
643 643 <input
644 644 type="number"
... ... @@ -656,7 +656,7 @@ export function MetadataAdminPage() {
656 656 checked={cfRequired}
657 657 onChange={(e) => setCfRequired(e.target.checked)}
658 658 />
659   - Required
  659 + {t('label.required')}
660 660 </label>
661 661 <label className="flex items-center gap-1.5 text-sm text-slate-700">
662 662 <input
... ... @@ -664,14 +664,14 @@ export function MetadataAdminPage() {
664 664 checked={cfPii}
665 665 onChange={(e) => setCfPii(e.target.checked)}
666 666 />
667   - PII
  667 + {t('label.pii')}
668 668 </label>
669 669 </div>
670 670 </div>
671 671 <div className="grid grid-cols-2 gap-3">
672 672 <div>
673 673 <label className="block text-xs font-medium text-slate-700">
674   - Label EN
  674 + {t('label.labelEn')}
675 675 </label>
676 676 <input
677 677 type="text"
... ... @@ -683,7 +683,7 @@ export function MetadataAdminPage() {
683 683 </div>
684 684 <div>
685 685 <label className="block text-xs font-medium text-slate-700">
686   - Label zh-CN
  686 + {t('label.labelZhCn')}
687 687 </label>
688 688 <input
689 689 type="text"
... ... @@ -745,7 +745,7 @@ export function MetadataAdminPage() {
745 745 className="btn-primary"
746 746 onClick={() => navigate('/admin/metadata/forms/new')}
747 747 >
748   - + New Form
  748 + {t('action.newForm')}
749 749 </button>
750 750 </div>
751 751 <DataTable
... ... @@ -764,7 +764,7 @@ export function MetadataAdminPage() {
764 764 className="btn-primary"
765 765 onClick={() => navigate('/admin/metadata/list-views/new')}
766 766 >
767   - + New List View
  767 + {t('action.newListView')}
768 768 </button>
769 769 </div>
770 770 <DataTable
... ... @@ -915,7 +915,7 @@ export function MetadataAdminPage() {
915 915 ])
916 916 }
917 917 >
918   - + Add Condition
  918 + {t('action.addCondition')}
919 919 </button>
920 920 </div>
921 921  
... ... @@ -982,7 +982,7 @@ export function MetadataAdminPage() {
982 982 ])
983 983 }
984 984 >
985   - + Add Action
  985 + {t('action.addAction')}
986 986 </button>
987 987 </div>
988 988  
... ... @@ -1030,7 +1030,7 @@ export function MetadataAdminPage() {
1030 1030 <div>
1031 1031 <PageHeader
1032 1032 title={t('page.metadataAdmin.title')}
1033   - subtitle="Browse and manage metadata definitions"
  1033 + subtitle={t('page.metadataAdmin.subtitle')}
1034 1034 />
1035 1035  
1036 1036 {/* Tab bar */}
... ...
web/src/pages/MovementsPage.tsx
... ... @@ -5,6 +5,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
5 5 import { Loading } from '@/components/Loading'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { DataTable, type Column } from '@/components/DataTable'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 interface Row extends Record<string, unknown> {
10 11 id: string
... ... @@ -17,6 +18,7 @@ interface Row extends Record&lt;string, unknown&gt; {
17 18 }
18 19  
19 20 export function MovementsPage() {
  21 + const t = useT()
20 22 const [rows, setRows] = useState<Row[]>([])
21 23 const [error, setError] = useState<Error | null>(null)
22 24 const [loading, setLoading] = useState(true)
... ... @@ -46,15 +48,15 @@ export function MovementsPage() {
46 48  
47 49 const columns: Column<Row>[] = [
48 50 {
49   - header: 'Occurred',
  51 + header: t('label.occurred'),
50 52 key: 'occurredAt',
51 53 render: (r) =>
52   - r.occurredAt ? new Date(r.occurredAt).toLocaleString() : '',
  54 + r.occurredAt ? new Date(r.occurredAt).toLocaleString() : '\u2014',
53 55 },
54   - { header: 'Item', key: 'itemCode', render: (r) => <span className="font-mono">{r.itemCode}</span> },
55   - { header: 'Location', key: 'locationCode', render: (r) => <span className="font-mono">{r.locationCode}</span> },
  56 + { header: t('label.item'), key: 'itemCode', render: (r) => <span className="font-mono">{r.itemCode}</span> },
  57 + { header: t('label.location'), key: 'locationCode', render: (r) => <span className="font-mono">{r.locationCode}</span> },
56 58 {
57   - header: 'Δ',
  59 + header: t('label.delta'),
58 60 key: 'delta',
59 61 render: (r) => {
60 62 const n = Number(r.delta)
... ... @@ -62,15 +64,15 @@ export function MovementsPage() {
62 64 return <span className={`font-mono tabular-nums ${cls}`}>{String(r.delta)}</span>
63 65 },
64 66 },
65   - { header: 'Reason', key: 'reason' },
66   - { header: 'Reference', key: 'reference', render: (r) => r.reference ?? '—' },
  67 + { header: t('label.reason'), key: 'reason' },
  68 + { header: t('label.reference'), key: 'reference', render: (r) => r.reference ?? '\u2014' },
67 69 ]
68 70  
69 71 return (
70 72 <div>
71 73 <PageHeader
72   - title="Stock Movements"
73   - subtitle="Append-only ledger of every change to inventory. Most-recent first."
  74 + title={t('page.movements.title')}
  75 + subtitle={t('page.movements.subtitle')}
74 76 />
75 77 {loading && <Loading />}
76 78 {error && <ErrorBox error={error} />}
... ...
web/src/pages/PartnersPage.tsx
... ... @@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 6 import { Loading } from '@/components/Loading'
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10  
10 11 export function PartnersPage() {
  12 + const t = useT()
11 13 const [rows, setRows] = useState<Partner[]>([])
12 14 const [error, setError] = useState<Error | null>(null)
13 15 const [loading, setLoading] = useState(true)
... ... @@ -22,29 +24,29 @@ export function PartnersPage() {
22 24  
23 25 const columns: Column<Partner>[] = [
24 26 {
25   - header: 'Code',
  27 + header: t('label.code'),
26 28 key: 'code',
27 29 render: (r) => (
28 30 <Link to={`/partners/${r.id}/edit`} className="font-mono text-brand-600 hover:underline">{r.code}</Link>
29 31 ),
30 32 },
31   - { header: 'Name', key: 'name' },
32   - { header: 'Type', key: 'type' },
33   - { header: 'Email', key: 'email', render: (r) => r.email ?? '—' },
34   - { header: 'Phone', key: 'phone', render: (r) => r.phone ?? '—' },
  33 + { header: t('label.name'), key: 'name' },
  34 + { header: t('label.type'), key: 'type' },
  35 + { header: t('label.email'), key: 'email', render: (r) => r.email ?? '\u2014' },
  36 + { header: t('label.phone'), key: 'phone', render: (r) => r.phone ?? '\u2014' },
35 37 {
36   - header: 'Active',
  38 + header: t('label.active'),
37 39 key: 'active',
38   - render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>),
  40 + render: (r) => (r.active ? <span className="text-emerald-600">{'\u25CF'}</span> : <span className="text-slate-300">{'\u25CF'}</span>),
39 41 },
40 42 ]
41 43  
42 44 return (
43 45 <div>
44 46 <PageHeader
45   - title="Partners"
46   - subtitle="Customers, suppliers, and dual-role partners."
47   - actions={<Link to="/partners/new" className="btn-primary">+ New Partner</Link>}
  47 + title={t('page.partners.title')}
  48 + subtitle={t('page.partners.subtitle')}
  49 + actions={<Link to="/partners/new" className="btn-primary">{t('action.newPartner')}</Link>}
48 50 />
49 51 {loading && <Loading />}
50 52 {error && <ErrorBox error={error} />}
... ...
web/src/pages/PurchaseOrderDetailPage.tsx
1   -// Purchase-order detail screen symmetric to SalesOrderDetailPage.
  1 +// Purchase-order detail screen -- symmetric to SalesOrderDetailPage.
2 2 // Confirm a DRAFT, receive a CONFIRMED into a warehouse, or cancel
3 3 // either. Each action surfaces the corresponding stock movement
4   -// (PURCHASE_RECEIPT) and pbc-finance journal entry (AP, POSTED
  4 +// (PURCHASE_RECEIPT) and pbc-finance journal entry (AP, POSTED ->
5 5 // SETTLED on receive) inline.
6 6  
7 7 import { useCallback, useEffect, useState } from 'react'
... ... @@ -12,10 +12,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
12 12 import { Loading } from '@/components/Loading'
13 13 import { ErrorBox } from '@/components/ErrorBox'
14 14 import { StatusBadge } from '@/components/StatusBadge'
  15 +import { useT } from '@/i18n/LocaleContext'
15 16  
16 17 export function PurchaseOrderDetailPage() {
17 18 const { id = '' } = useParams<{ id: string }>()
18 19 const navigate = useNavigate()
  20 + const t = useT()
19 21 const [order, setOrder] = useState<PurchaseOrder | null>(null)
20 22 const [locations, setLocations] = useState<Location[]>([])
21 23 const [receivingLocation, setReceivingLocation] = useState<string>('')
... ... @@ -68,7 +70,7 @@ export function PurchaseOrderDetailPage() {
68 70 const updated = await purchaseOrders.confirm(order.id)
69 71 setOrder(updated)
70 72 await reloadSideEffects(updated.code)
71   - setActionMessage('Confirmed. pbc-finance has posted an AP journal entry.')
  73 + setActionMessage(t('page.purchaseOrderDetail.confirmMsg'))
72 74 } catch (e: unknown) {
73 75 setError(e instanceof Error ? e : new Error(String(e)))
74 76 } finally {
... ... @@ -78,7 +80,7 @@ export function PurchaseOrderDetailPage() {
78 80  
79 81 const onReceive = async () => {
80 82 if (!receivingLocation) {
81   - setError(new Error('Pick a receiving location first.'))
  83 + setError(new Error(t('page.purchaseOrderDetail.pickLocation')))
82 84 return
83 85 }
84 86 setActing(true)
... ... @@ -89,7 +91,7 @@ export function PurchaseOrderDetailPage() {
89 91 setOrder(updated)
90 92 await reloadSideEffects(updated.code)
91 93 setActionMessage(
92   - `Received into ${receivingLocation}. Stock credited, journal entry settled.`,
  94 + t('page.purchaseOrderDetail.receiveMsg').replace('{location}', receivingLocation),
93 95 )
94 96 } catch (e: unknown) {
95 97 setError(e instanceof Error ? e : new Error(String(e)))
... ... @@ -106,7 +108,7 @@ export function PurchaseOrderDetailPage() {
106 108 const updated = await purchaseOrders.cancel(order.id)
107 109 setOrder(updated)
108 110 await reloadSideEffects(updated.code)
109   - setActionMessage('Cancelled. pbc-finance has reversed any open journal entry.')
  111 + setActionMessage(t('page.purchaseOrderDetail.cancelMsg'))
110 112 } catch (e: unknown) {
111 113 setError(e instanceof Error ? e : new Error(String(e)))
112 114 } finally {
... ... @@ -121,11 +123,11 @@ export function PurchaseOrderDetailPage() {
121 123 return (
122 124 <div>
123 125 <PageHeader
124   - title={`Purchase Order ${order.code}`}
125   - subtitle={`Supplier ${order.partnerCode} · ${order.orderDate} · ${order.currencyCode}`}
  126 + title={t('page.purchaseOrderDetail.title').replace('{code}', order.code)}
  127 + subtitle={`${t('page.purchaseOrderDetail.subtitle').replace('{partner}', order.partnerCode)} \u00B7 ${order.orderDate} \u00B7 ${order.currencyCode}`}
126 128 actions={
127 129 <button className="btn-secondary" onClick={() => navigate('/purchase-orders')}>
128   - ← Back
  130 + {t('action.back')}
129 131 </button>
130 132 }
131 133 />
... ... @@ -133,11 +135,11 @@ export function PurchaseOrderDetailPage() {
133 135 <div className="card mb-6 p-5">
134 136 <div className="flex flex-wrap items-center justify-between gap-4">
135 137 <div className="flex items-center gap-3">
136   - <span className="text-sm text-slate-500">Status:</span>
  138 + <span className="text-sm text-slate-500">{t('label.status')}:</span>
137 139 <StatusBadge status={order.status} />
138 140 </div>
139 141 <div className="text-right">
140   - <div className="text-xs uppercase text-slate-400">Total</div>
  142 + <div className="text-xs uppercase text-slate-400">{t('label.total')}</div>
141 143 <div className="font-mono text-xl font-semibold">
142 144 {Number(order.totalAmount).toLocaleString(undefined, {
143 145 minimumFractionDigits: 2,
... ... @@ -154,10 +156,10 @@ export function PurchaseOrderDetailPage() {
154 156 </div>
155 157  
156 158 <div className="card mb-6 p-5">
157   - <h2 className="mb-3 text-base font-semibold text-slate-800">Actions</h2>
  159 + <h2 className="mb-3 text-base font-semibold text-slate-800">{t('label.actions')}</h2>
158 160 <div className="flex flex-wrap items-center gap-3">
159 161 <button className="btn-primary" disabled={!canConfirm || acting} onClick={onConfirm}>
160   - Confirm
  162 + {t('action.confirm')}
161 163 </button>
162 164 <div className="flex items-center gap-2">
163 165 <select
... ... @@ -173,27 +175,27 @@ export function PurchaseOrderDetailPage() {
173 175 ))}
174 176 </select>
175 177 <button className="btn-primary" disabled={!canReceive || acting} onClick={onReceive}>
176   - Receive
  178 + {t('action.receive')}
177 179 </button>
178 180 </div>
179 181 <button className="btn-danger" disabled={!canCancel || acting} onClick={onCancel}>
180   - Cancel
  182 + {t('action.cancel')}
181 183 </button>
182 184 </div>
183 185 </div>
184 186  
185 187 <div className="card mb-6">
186 188 <div className="border-b border-slate-200 px-5 py-3">
187   - <h2 className="text-base font-semibold text-slate-800">Lines</h2>
  189 + <h2 className="text-base font-semibold text-slate-800">{t('label.lines')}</h2>
188 190 </div>
189 191 <table className="table-base">
190 192 <thead className="bg-slate-50">
191 193 <tr>
192   - <th>#</th>
193   - <th>Item</th>
194   - <th>Qty</th>
195   - <th>Unit price</th>
196   - <th>Line total</th>
  194 + <th>{t('label.lineNo')}</th>
  195 + <th>{t('label.item')}</th>
  196 + <th>{t('label.qty')}</th>
  197 + <th>{t('label.unitPrice')}</th>
  198 + <th>{t('label.lineTotal')}</th>
197 199 </tr>
198 200 </thead>
199 201 <tbody className="divide-y divide-slate-100">
... ... @@ -222,18 +224,18 @@ export function PurchaseOrderDetailPage() {
222 224 <div className="grid gap-6 md:grid-cols-2">
223 225 <div className="card">
224 226 <div className="border-b border-slate-200 px-5 py-3">
225   - <h2 className="text-base font-semibold text-slate-800">Inventory movements</h2>
  227 + <h2 className="text-base font-semibold text-slate-800">{t('label.inventoryMovements')}</h2>
226 228 </div>
227 229 {movements.length === 0 ? (
228   - <div className="p-5 text-sm text-slate-400">No movements yet.</div>
  230 + <div className="p-5 text-sm text-slate-400">{t('label.noMovementsYet')}</div>
229 231 ) : (
230 232 <table className="table-base">
231 233 <thead className="bg-slate-50">
232 234 <tr>
233   - <th>Item</th>
234   - <th>Δ</th>
235   - <th>Reason</th>
236   - <th>Reference</th>
  235 + <th>{t('label.item')}</th>
  236 + <th>{t('label.delta')}</th>
  237 + <th>{t('label.reason')}</th>
  238 + <th>{t('label.reference')}</th>
237 239 </tr>
238 240 </thead>
239 241 <tbody className="divide-y divide-slate-100">
... ... @@ -242,7 +244,7 @@ export function PurchaseOrderDetailPage() {
242 244 <td className="font-mono">{m.itemCode}</td>
243 245 <td className="font-mono tabular-nums text-emerald-600">{String(m.delta)}</td>
244 246 <td>{m.reason}</td>
245   - <td className="font-mono text-xs text-slate-500">{m.reference ?? ''}</td>
  247 + <td className="font-mono text-xs text-slate-500">{m.reference ?? '\u2014'}</td>
246 248 </tr>
247 249 ))}
248 250 </tbody>
... ... @@ -251,18 +253,18 @@ export function PurchaseOrderDetailPage() {
251 253 </div>
252 254 <div className="card">
253 255 <div className="border-b border-slate-200 px-5 py-3">
254   - <h2 className="text-base font-semibold text-slate-800">Journal entries</h2>
  256 + <h2 className="text-base font-semibold text-slate-800">{t('label.journalEntries')}</h2>
255 257 </div>
256 258 {journalEntries.length === 0 ? (
257   - <div className="p-5 text-sm text-slate-400">No entries yet.</div>
  259 + <div className="p-5 text-sm text-slate-400">{t('label.noEntriesYet')}</div>
258 260 ) : (
259 261 <table className="table-base">
260 262 <thead className="bg-slate-50">
261 263 <tr>
262   - <th>Code</th>
263   - <th>Type</th>
264   - <th>Status</th>
265   - <th>Amount</th>
  264 + <th>{t('label.code')}</th>
  265 + <th>{t('label.type')}</th>
  266 + <th>{t('label.status')}</th>
  267 + <th>{t('label.amount')}</th>
266 268 </tr>
267 269 </thead>
268 270 <tbody className="divide-y divide-slate-100">
... ...
web/src/pages/PurchaseOrdersPage.tsx
... ... @@ -7,8 +7,10 @@ import { Loading } from &#39;@/components/Loading&#39;
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DataTable, type Column } from '@/components/DataTable'
9 9 import { StatusBadge } from '@/components/StatusBadge'
  10 +import { useT } from '@/i18n/LocaleContext'
10 11  
11 12 export function PurchaseOrdersPage() {
  13 + const t = useT()
12 14 const [rows, setRows] = useState<PurchaseOrder[]>([])
13 15 const [error, setError] = useState<Error | null>(null)
14 16 const [loading, setLoading] = useState(true)
... ... @@ -23,7 +25,7 @@ export function PurchaseOrdersPage() {
23 25  
24 26 const columns: Column<PurchaseOrder>[] = [
25 27 {
26   - header: 'Code',
  28 + header: t('label.code'),
27 29 key: 'code',
28 30 render: (r) => (
29 31 <Link to={`/purchase-orders/${r.id}`} className="font-mono text-brand-600 hover:underline">
... ... @@ -31,12 +33,12 @@ export function PurchaseOrdersPage() {
31 33 </Link>
32 34 ),
33 35 },
34   - { header: 'Supplier', key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> },
35   - { header: 'Order date', key: 'orderDate' },
36   - { header: 'Expected', key: 'expectedDate', render: (r) => r.expectedDate ?? '—' },
37   - { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> },
  36 + { header: t('label.supplier'), key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> },
  37 + { header: t('label.orderDate'), key: 'orderDate' },
  38 + { header: t('label.expected'), key: 'expectedDate', render: (r) => r.expectedDate ?? '\u2014' },
  39 + { header: t('label.status'), key: 'status', render: (r) => <StatusBadge status={r.status} /> },
38 40 {
39   - header: 'Total',
  41 + header: t('label.total'),
40 42 key: 'totalAmount',
41 43 render: (r) => (
42 44 <span className="font-mono tabular-nums">
... ... @@ -52,9 +54,9 @@ export function PurchaseOrdersPage() {
52 54 return (
53 55 <div>
54 56 <PageHeader
55   - title="Purchase Orders"
56   - subtitle="Supplier-facing orders. Confirm and receive to credit inventory."
57   - actions={<Link to="/purchase-orders/new" className="btn-primary">+ New Order</Link>}
  57 + title={t('page.purchaseOrders.title')}
  58 + subtitle={t('page.purchaseOrders.subtitle')}
  59 + actions={<Link to="/purchase-orders/new" className="btn-primary">{t('action.newOrder')}</Link>}
58 60 />
59 61 {loading && <Loading />}
60 62 {error && <ErrorBox error={error} />}
... ...
web/src/pages/RolesPage.tsx
... ... @@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
5 5 import { Loading } from '@/components/Loading'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { DataTable, type Column } from '@/components/DataTable'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 export function RolesPage() {
  11 + const t = useT()
10 12 const [rows, setRows] = useState<Role[]>([])
11 13 const [error, setError] = useState<Error | null>(null)
12 14 const [loading, setLoading] = useState(true)
... ... @@ -43,36 +45,36 @@ export function RolesPage() {
43 45 }
44 46  
45 47 const columns: Column<Role>[] = [
46   - { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
47   - { header: 'Name', key: 'name' },
48   - { header: 'Description', key: 'description', render: (r) => r.description ?? '—' },
  48 + { header: t('label.code'), key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
  49 + { header: t('label.name'), key: 'name' },
  50 + { header: t('label.description'), key: 'description', render: (r) => r.description ?? '\u2014' },
49 51 ]
50 52  
51 53 return (
52 54 <div>
53 55 <PageHeader
54   - title="Roles"
55   - subtitle="Named bundles of permissions. The 'admin' role has all permissions by default."
  56 + title={t('page.roles.title')}
  57 + subtitle={t('page.roles.subtitle')}
56 58 actions={
57 59 <button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
58   - {showCreate ? 'Cancel' : '+ New Role'}
  60 + {showCreate ? t('action.cancel') : t('action.newRole')}
59 61 </button>
60 62 }
61 63 />
62 64 {showCreate && (
63 65 <form onSubmit={onCreate} className="card p-4 mb-4 max-w-lg flex items-end gap-3">
64 66 <div className="flex-1">
65   - <label className="block text-xs font-medium text-slate-700">Code</label>
  67 + <label className="block text-xs font-medium text-slate-700">{t('label.code')}</label>
66 68 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
67 69 placeholder="sales-clerk" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
68 70 </div>
69 71 <div className="flex-1">
70   - <label className="block text-xs font-medium text-slate-700">Name</label>
  72 + <label className="block text-xs font-medium text-slate-700">{t('label.name')}</label>
71 73 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
72 74 placeholder="Sales Clerk" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
73 75 </div>
74 76 <button type="submit" className="btn-primary" disabled={creating}>
75   - {creating ? '...' : 'Create'}
  77 + {creating ? '...' : t('action.create')}
76 78 </button>
77 79 </form>
78 80 )}
... ...
web/src/pages/SalesOrderDetailPage.tsx
... ... @@ -3,11 +3,11 @@
3 3 // **The headline demo flow.** Confirm a DRAFT, ship a CONFIRMED,
4 4 // or cancel either. Each action updates the order in place,
5 5 // reloads the journal entry list to show the AR row appearing
6   -// (POSTED SETTLED), and reloads stock movements to show the
  6 +// (POSTED -> SETTLED), and reloads stock movements to show the
7 7 // SALES_SHIPMENT ledger entry appearing.
8 8 //
9 9 // **Why ship asks for a location.** The framework requires every
10   -// shipment to name the warehouse the goods came from the
  10 +// shipment to name the warehouse the goods came from -- the
11 11 // inventory ledger tags the row with that location and the audit
12 12 // trail must always answer "which warehouse shipped this". The UI
13 13 // proposes the first WAREHOUSE-typed location it finds; an
... ... @@ -21,10 +21,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
21 21 import { Loading } from '@/components/Loading'
22 22 import { ErrorBox } from '@/components/ErrorBox'
23 23 import { StatusBadge } from '@/components/StatusBadge'
  24 +import { useT } from '@/i18n/LocaleContext'
24 25  
25 26 export function SalesOrderDetailPage() {
26 27 const { id = '' } = useParams<{ id: string }>()
27 28 const navigate = useNavigate()
  29 + const t = useT()
28 30 const [order, setOrder] = useState<SalesOrder | null>(null)
29 31 const [locations, setLocations] = useState<Location[]>([])
30 32 const [shippingLocation, setShippingLocation] = useState<string>('')
... ... @@ -77,7 +79,7 @@ export function SalesOrderDetailPage() {
77 79 const updated = await salesOrders.confirm(order.id)
78 80 setOrder(updated)
79 81 await reloadSideEffects(updated.code)
80   - setActionMessage('Confirmed. pbc-finance has posted an AR journal entry.')
  82 + setActionMessage(t('page.salesOrderDetail.confirmMsg'))
81 83 } catch (e: unknown) {
82 84 setError(e instanceof Error ? e : new Error(String(e)))
83 85 } finally {
... ... @@ -87,7 +89,7 @@ export function SalesOrderDetailPage() {
87 89  
88 90 const onShip = async () => {
89 91 if (!shippingLocation) {
90   - setError(new Error('Pick a shipping location first.'))
  92 + setError(new Error(t('page.salesOrderDetail.pickLocation')))
91 93 return
92 94 }
93 95 setActing(true)
... ... @@ -98,7 +100,7 @@ export function SalesOrderDetailPage() {
98 100 setOrder(updated)
99 101 await reloadSideEffects(updated.code)
100 102 setActionMessage(
101   - `Shipped from ${shippingLocation}. Stock debited, journal entry settled.`,
  103 + t('page.salesOrderDetail.shipMsg').replace('{location}', shippingLocation),
102 104 )
103 105 } catch (e: unknown) {
104 106 setError(e instanceof Error ? e : new Error(String(e)))
... ... @@ -115,7 +117,7 @@ export function SalesOrderDetailPage() {
115 117 const updated = await salesOrders.cancel(order.id)
116 118 setOrder(updated)
117 119 await reloadSideEffects(updated.code)
118   - setActionMessage('Cancelled. pbc-finance has reversed any open journal entry.')
  120 + setActionMessage(t('page.salesOrderDetail.cancelMsg'))
119 121 } catch (e: unknown) {
120 122 setError(e instanceof Error ? e : new Error(String(e)))
121 123 } finally {
... ... @@ -130,11 +132,11 @@ export function SalesOrderDetailPage() {
130 132 return (
131 133 <div>
132 134 <PageHeader
133   - title={`Sales Order ${order.code}`}
134   - subtitle={`Customer ${order.partnerCode} · ${order.orderDate} · ${order.currencyCode}`}
  135 + title={t('page.salesOrderDetail.title').replace('{code}', order.code)}
  136 + subtitle={`${t('page.salesOrderDetail.subtitle').replace('{partner}', order.partnerCode)} \u00B7 ${order.orderDate} \u00B7 ${order.currencyCode}`}
135 137 actions={
136 138 <button className="btn-secondary" onClick={() => navigate('/sales-orders')}>
137   - ← Back
  139 + {t('action.back')}
138 140 </button>
139 141 }
140 142 />
... ... @@ -142,11 +144,11 @@ export function SalesOrderDetailPage() {
142 144 <div className="card mb-6 p-5">
143 145 <div className="flex flex-wrap items-center justify-between gap-4">
144 146 <div className="flex items-center gap-3">
145   - <span className="text-sm text-slate-500">Status:</span>
  147 + <span className="text-sm text-slate-500">{t('label.status')}:</span>
146 148 <StatusBadge status={order.status} />
147 149 </div>
148 150 <div className="text-right">
149   - <div className="text-xs uppercase text-slate-400">Total</div>
  151 + <div className="text-xs uppercase text-slate-400">{t('label.total')}</div>
150 152 <div className="font-mono text-xl font-semibold">
151 153 {Number(order.totalAmount).toLocaleString(undefined, {
152 154 minimumFractionDigits: 2,
... ... @@ -164,10 +166,10 @@ export function SalesOrderDetailPage() {
164 166 </div>
165 167  
166 168 <div className="card mb-6 p-5">
167   - <h2 className="mb-3 text-base font-semibold text-slate-800">Actions</h2>
  169 + <h2 className="mb-3 text-base font-semibold text-slate-800">{t('label.actions')}</h2>
168 170 <div className="flex flex-wrap items-center gap-3">
169 171 <button className="btn-primary" disabled={!canConfirm || acting} onClick={onConfirm}>
170   - Confirm
  172 + {t('action.confirm')}
171 173 </button>
172 174 <div className="flex items-center gap-2">
173 175 <select
... ... @@ -183,27 +185,27 @@ export function SalesOrderDetailPage() {
183 185 ))}
184 186 </select>
185 187 <button className="btn-primary" disabled={!canShip || acting} onClick={onShip}>
186   - Ship
  188 + {t('action.ship')}
187 189 </button>
188 190 </div>
189 191 <button className="btn-danger" disabled={!canCancel || acting} onClick={onCancel}>
190   - Cancel
  192 + {t('action.cancel')}
191 193 </button>
192 194 </div>
193 195 </div>
194 196  
195 197 <div className="card mb-6">
196 198 <div className="border-b border-slate-200 px-5 py-3">
197   - <h2 className="text-base font-semibold text-slate-800">Lines</h2>
  199 + <h2 className="text-base font-semibold text-slate-800">{t('label.lines')}</h2>
198 200 </div>
199 201 <table className="table-base">
200 202 <thead className="bg-slate-50">
201 203 <tr>
202   - <th>#</th>
203   - <th>Item</th>
204   - <th>Qty</th>
205   - <th>Unit price</th>
206   - <th>Line total</th>
  204 + <th>{t('label.lineNo')}</th>
  205 + <th>{t('label.item')}</th>
  206 + <th>{t('label.qty')}</th>
  207 + <th>{t('label.unitPrice')}</th>
  208 + <th>{t('label.lineTotal')}</th>
207 209 </tr>
208 210 </thead>
209 211 <tbody className="divide-y divide-slate-100">
... ... @@ -232,18 +234,18 @@ export function SalesOrderDetailPage() {
232 234 <div className="grid gap-6 md:grid-cols-2">
233 235 <div className="card">
234 236 <div className="border-b border-slate-200 px-5 py-3">
235   - <h2 className="text-base font-semibold text-slate-800">Inventory movements</h2>
  237 + <h2 className="text-base font-semibold text-slate-800">{t('label.inventoryMovements')}</h2>
236 238 </div>
237 239 {movements.length === 0 ? (
238   - <div className="p-5 text-sm text-slate-400">No movements yet.</div>
  240 + <div className="p-5 text-sm text-slate-400">{t('label.noMovementsYet')}</div>
239 241 ) : (
240 242 <table className="table-base">
241 243 <thead className="bg-slate-50">
242 244 <tr>
243   - <th>Item</th>
244   - <th>Δ</th>
245   - <th>Reason</th>
246   - <th>Reference</th>
  245 + <th>{t('label.item')}</th>
  246 + <th>{t('label.delta')}</th>
  247 + <th>{t('label.reason')}</th>
  248 + <th>{t('label.reference')}</th>
247 249 </tr>
248 250 </thead>
249 251 <tbody className="divide-y divide-slate-100">
... ... @@ -252,7 +254,7 @@ export function SalesOrderDetailPage() {
252 254 <td className="font-mono">{m.itemCode}</td>
253 255 <td className="font-mono tabular-nums text-rose-600">{String(m.delta)}</td>
254 256 <td>{m.reason}</td>
255   - <td className="font-mono text-xs text-slate-500">{m.reference ?? ''}</td>
  257 + <td className="font-mono text-xs text-slate-500">{m.reference ?? '\u2014'}</td>
256 258 </tr>
257 259 ))}
258 260 </tbody>
... ... @@ -261,18 +263,18 @@ export function SalesOrderDetailPage() {
261 263 </div>
262 264 <div className="card">
263 265 <div className="border-b border-slate-200 px-5 py-3">
264   - <h2 className="text-base font-semibold text-slate-800">Journal entries</h2>
  266 + <h2 className="text-base font-semibold text-slate-800">{t('label.journalEntries')}</h2>
265 267 </div>
266 268 {journalEntries.length === 0 ? (
267   - <div className="p-5 text-sm text-slate-400">No entries yet.</div>
  269 + <div className="p-5 text-sm text-slate-400">{t('label.noEntriesYet')}</div>
268 270 ) : (
269 271 <table className="table-base">
270 272 <thead className="bg-slate-50">
271 273 <tr>
272   - <th>Code</th>
273   - <th>Type</th>
274   - <th>Status</th>
275   - <th>Amount</th>
  274 + <th>{t('label.code')}</th>
  275 + <th>{t('label.type')}</th>
  276 + <th>{t('label.status')}</th>
  277 + <th>{t('label.amount')}</th>
276 278 </tr>
277 279 </thead>
278 280 <tbody className="divide-y divide-slate-100">
... ... @@ -293,7 +295,7 @@ export function SalesOrderDetailPage() {
293 295 </table>
294 296 )}
295 297 <div className="border-t border-slate-200 px-5 py-3 text-xs text-slate-400">
296   - <Link to="/journal-entries" className="hover:underline">View all journal entries →</Link>
  298 + <Link to="/journal-entries" className="hover:underline">{t('label.viewAllJournalEntries')} &rarr;</Link>
297 299 </div>
298 300 </div>
299 301 </div>
... ...
web/src/pages/SalesOrdersPage.tsx
... ... @@ -7,8 +7,10 @@ import { Loading } from &#39;@/components/Loading&#39;
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DataTable, type Column } from '@/components/DataTable'
9 9 import { StatusBadge } from '@/components/StatusBadge'
  10 +import { useT } from '@/i18n/LocaleContext'
10 11  
11 12 export function SalesOrdersPage() {
  13 + const t = useT()
12 14 const [rows, setRows] = useState<SalesOrder[]>([])
13 15 const [error, setError] = useState<Error | null>(null)
14 16 const [loading, setLoading] = useState(true)
... ... @@ -23,7 +25,7 @@ export function SalesOrdersPage() {
23 25  
24 26 const columns: Column<SalesOrder>[] = [
25 27 {
26   - header: 'Code',
  28 + header: t('label.code'),
27 29 key: 'code',
28 30 render: (r) => (
29 31 <Link to={`/sales-orders/${r.id}`} className="font-mono text-brand-600 hover:underline">
... ... @@ -31,11 +33,11 @@ export function SalesOrdersPage() {
31 33 </Link>
32 34 ),
33 35 },
34   - { header: 'Customer', key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> },
35   - { header: 'Date', key: 'orderDate' },
36   - { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> },
  36 + { header: t('label.customer'), key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> },
  37 + { header: t('label.date'), key: 'orderDate' },
  38 + { header: t('label.status'), key: 'status', render: (r) => <StatusBadge status={r.status} /> },
37 39 {
38   - header: 'Total',
  40 + header: t('label.total'),
39 41 key: 'totalAmount',
40 42 render: (r) => (
41 43 <span className="font-mono tabular-nums">
... ... @@ -48,7 +50,7 @@ export function SalesOrdersPage() {
48 50 ),
49 51 },
50 52 {
51   - header: 'Lines',
  53 + header: t('label.lines'),
52 54 key: 'lines',
53 55 render: (r) => <span className="text-slate-500">{r.lines.length}</span>,
54 56 },
... ... @@ -57,10 +59,10 @@ export function SalesOrdersPage() {
57 59 return (
58 60 <div>
59 61 <PageHeader
60   - title="Sales Orders"
61   - subtitle="Customer-facing orders. Confirm to auto-generate production work orders."
  62 + title={t('page.salesOrders.title')}
  63 + subtitle={t('page.salesOrders.subtitle')}
62 64 actions={
63   - <Link to="/sales-orders/new" className="btn-primary">+ New Order</Link>
  65 + <Link to="/sales-orders/new" className="btn-primary">{t('action.newOrder')}</Link>
64 66 }
65 67 />
66 68 {loading && <Loading />}
... ...
web/src/pages/ShopFloorPage.tsx
... ... @@ -3,7 +3,7 @@
3 3 // Polls /api/v1/production/work-orders/shop-floor every 5s and
4 4 // renders one card per IN_PROGRESS work order with its current
5 5 // operation, planned vs actual minutes, and operations completed.
6   -// Designed to be projected on a wall-mounted screen the cards
  6 +// Designed to be projected on a wall-mounted screen -- the cards
7 7 // are large, the typography is high-contrast, and the only state
8 8 // is "what's running right now".
9 9  
... ... @@ -15,10 +15,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
15 15 import { Loading } from '@/components/Loading'
16 16 import { ErrorBox } from '@/components/ErrorBox'
17 17 import { StatusBadge } from '@/components/StatusBadge'
  18 +import { useT } from '@/i18n/LocaleContext'
18 19  
19 20 const POLL_MS = 5000
20 21  
21 22 export function ShopFloorPage() {
  23 + const t = useT()
22 24 const [rows, setRows] = useState<ShopFloorEntry[]>([])
23 25 const [error, setError] = useState<Error | null>(null)
24 26 const [loading, setLoading] = useState(true)
... ... @@ -52,16 +54,16 @@ export function ShopFloorPage() {
52 54 return (
53 55 <div>
54 56 <PageHeader
55   - title="Shop Floor"
56   - subtitle={`Live view of every work order in progress · refreshes every ${POLL_MS / 1000}s${
57   - updatedAt ? ' · last update ' + updatedAt.toLocaleTimeString() : ''
  57 + title={t('page.shopFloor.title')}
  58 + subtitle={`${t('page.shopFloor.subtitle')} \u00B7 ${t('page.shopFloor.refreshInterval').replace('{seconds}', String(POLL_MS / 1000))}${
  59 + updatedAt ? ' \u00B7 ' + t('page.shopFloor.lastUpdate') + ' ' + updatedAt.toLocaleTimeString() : ''
58 60 }`}
59 61 />
60 62 {loading && <Loading />}
61 63 {error && <ErrorBox error={error} />}
62 64 {!loading && rows.length === 0 && (
63 65 <div className="card p-8 text-center text-sm text-slate-400">
64   - No work orders are in progress right now.
  66 + {t('page.shopFloor.noWorkOrders')}
65 67 </div>
66 68 )}
67 69 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
... ... @@ -80,15 +82,15 @@ export function ShopFloorPage() {
80 82 {r.workOrderCode}
81 83 </span>
82 84 <span className="text-xs text-slate-500">
83   - {r.operationsCompleted} / {r.operationsTotal} ops
  85 + {r.operationsCompleted} / {r.operationsTotal} {t('page.shopFloor.ops')}
84 86 </span>
85 87 </div>
86 88 <div className="mb-3 text-xs text-slate-500">
87   - Output: <span className="font-mono">{r.outputItemCode}</span> ×{' '}
  89 + {t('label.output')}: <span className="font-mono">{r.outputItemCode}</span> {'\u00D7'}{' '}
88 90 {String(r.outputQuantity)}
89 91 </div>
90 92 <div className="mb-3">
91   - <div className="text-xs text-slate-400">Current operation</div>
  93 + <div className="text-xs text-slate-400">{t('page.shopFloor.currentOp')}</div>
92 94 {r.currentOperationCode ? (
93 95 <div className="mt-1 flex items-center gap-2">
94 96 <span className="font-mono font-medium">{r.currentOperationCode}</span>
... ... @@ -96,13 +98,13 @@ export function ShopFloorPage() {
96 98 {r.currentOperationStatus && <StatusBadge status={r.currentOperationStatus} />}
97 99 </div>
98 100 ) : (
99   - <div className="mt-1 text-sm text-slate-400">No routing</div>
  101 + <div className="mt-1 text-sm text-slate-400">{t('page.shopFloor.noRouting')}</div>
100 102 )}
101 103 </div>
102 104 <div className="mt-2">
103 105 <div className="mb-1 flex items-center justify-between text-xs text-slate-500">
104   - <span>{act.toFixed(0)} actual min</span>
105   - <span>{std.toFixed(0)} std min</span>
  106 + <span>{act.toFixed(0)} {t('page.shopFloor.actualMin')}</span>
  107 + <span>{std.toFixed(0)} {t('page.shopFloor.stdMin')}</span>
106 108 </div>
107 109 <div className="h-2 overflow-hidden rounded-full bg-slate-100">
108 110 <div
... ...
web/src/pages/TaskDetailPage.tsx
... ... @@ -77,15 +77,15 @@ export function TaskDetailPage() {
77 77  
78 78 <div className="card p-4 space-y-4">
79 79 <dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm max-w-lg">
80   - <dt className="font-medium text-slate-500">Task ID</dt>
  80 + <dt className="font-medium text-slate-500">{t('label.taskId')}</dt>
81 81 <dd className="font-mono">{task.taskId}</dd>
82   - <dt className="font-medium text-slate-500">Process</dt>
  82 + <dt className="font-medium text-slate-500">{t('label.process')}</dt>
83 83 <dd>{task.processDefinitionKey}</dd>
84   - <dt className="font-medium text-slate-500">Created</dt>
  84 + <dt className="font-medium text-slate-500">{t('label.created')}</dt>
85 85 <dd>{task.createTime}</dd>
86   - <dt className="font-medium text-slate-500">Assignee</dt>
  86 + <dt className="font-medium text-slate-500">{t('label.assignee')}</dt>
87 87 <dd>{task.assignee ?? '\u2014'}</dd>
88   - <dt className="font-medium text-slate-500">Form Key</dt>
  88 + <dt className="font-medium text-slate-500">{t('label.formKey')}</dt>
89 89 <dd className="font-mono">{task.formKey ?? '\u2014'}</dd>
90 90 </dl>
91 91  
... ... @@ -100,7 +100,7 @@ export function TaskDetailPage() {
100 100 ) : (
101 101 <div className="space-y-4">
102 102 <div>
103   - <h3 className="text-sm font-medium text-slate-700 mb-2">Variables</h3>
  103 + <h3 className="text-sm font-medium text-slate-700 mb-2">{t('label.variables')}</h3>
104 104 <pre className="rounded-md bg-slate-50 border border-slate-200 p-3 text-xs overflow-x-auto max-h-64">
105 105 {JSON.stringify(task.variables, null, 2)}
106 106 </pre>
... ...
web/src/pages/UomsPage.tsx
... ... @@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
5 5 import { Loading } from '@/components/Loading'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7 7 import { DataTable, type Column } from '@/components/DataTable'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9  
9 10 export function UomsPage() {
  11 + const t = useT()
10 12 const [rows, setRows] = useState<Uom[]>([])
11 13 const [error, setError] = useState<Error | null>(null)
12 14 const [loading, setLoading] = useState(true)
... ... @@ -20,16 +22,16 @@ export function UomsPage() {
20 22 }, [])
21 23  
22 24 const columns: Column<Uom>[] = [
23   - { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
24   - { header: 'Name', key: 'name' },
25   - { header: 'Dimension', key: 'dimension' },
  25 + { header: t('label.code'), key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
  26 + { header: t('label.name'), key: 'name' },
  27 + { header: t('label.dimension'), key: 'dimension' },
26 28 ]
27 29  
28 30 return (
29 31 <div>
30 32 <PageHeader
31   - title="Units of Measure"
32   - subtitle="Seeded set of UoMs the catalog and inventory PBCs use to quantify items."
  33 + title={t('page.uoms.title')}
  34 + subtitle={t('page.uoms.subtitle')}
33 35 />
34 36 {loading && <Loading />}
35 37 {error && <ErrorBox error={error} />}
... ...
web/src/pages/UserDetailPage.tsx
... ... @@ -11,10 +11,12 @@ import type { Role, User } from &#39;@/types/api&#39;
11 11 import { PageHeader } from '@/components/PageHeader'
12 12 import { Loading } from '@/components/Loading'
13 13 import { ErrorBox } from '@/components/ErrorBox'
  14 +import { useT } from '@/i18n/LocaleContext'
14 15  
15 16 export function UserDetailPage() {
16 17 const { id = '' } = useParams<{ id: string }>()
17 18 const navigate = useNavigate()
  19 + const t = useT()
18 20 const [user, setUser] = useState<User | null>(null)
19 21 const [allRoles, setAllRoles] = useState<Role[]>([])
20 22 const [userRoleCodes, setUserRoleCodes] = useState<string[]>([])
... ... @@ -64,10 +66,10 @@ export function UserDetailPage() {
64 66 <div>
65 67 <PageHeader
66 68 title={user.displayName}
67   - subtitle={`@${user.username} · ${user.enabled ? 'Active' : 'Disabled'}${user.email ? ' · ' + user.email : ''}`}
  69 + subtitle={`@${user.username} \u00B7 ${user.enabled ? t('label.activeStatus') : t('label.disabled')}${user.email ? ' \u00B7 ' + user.email : ''}`}
68 70 actions={
69 71 <button className="btn-secondary" onClick={() => navigate('/users')}>
70   - ← Back
  72 + {t('action.back')}
71 73 </button>
72 74 }
73 75 />
... ... @@ -75,12 +77,12 @@ export function UserDetailPage() {
75 77 {error && <ErrorBox error={error} />}
76 78  
77 79 <div className="card p-5 max-w-lg">
78   - <h2 className="mb-3 text-base font-semibold text-slate-800">Roles</h2>
  80 + <h2 className="mb-3 text-base font-semibold text-slate-800">{t('page.userDetail.roles')}</h2>
79 81 <p className="mb-4 text-xs text-slate-400">
80   - Toggle roles on/off. Changes take effect on the user's next login.
  82 + {t('page.userDetail.rolesHint')}
81 83 </p>
82 84 {allRoles.length === 0 && (
83   - <p className="text-sm text-slate-400">No roles defined yet. Create one on the Roles page.</p>
  85 + <p className="text-sm text-slate-400">{t('page.userDetail.noRoles')}</p>
84 86 )}
85 87 <div className="space-y-2">
86 88 {allRoles.map((role) => {
... ... @@ -99,7 +101,7 @@ export function UserDetailPage() {
99 101 disabled={acting}
100 102 onClick={() => toggle(role.code, has)}
101 103 >
102   - {has ? 'Revoke' : 'Assign'}
  104 + {has ? t('action.revoke') : t('action.assign')}
103 105 </button>
104 106 </div>
105 107 )
... ...
web/src/pages/UserTasksPage.tsx
... ... @@ -31,7 +31,7 @@ export function UserTasksPage() {
31 31  
32 32 const columns: Column<UserTaskSummary>[] = [
33 33 {
34   - header: 'Task Name',
  34 + header: t('label.taskName'),
35 35 key: 'taskName',
36 36 render: (r) => (
37 37 <button
... ... @@ -42,10 +42,10 @@ export function UserTasksPage() {
42 42 </button>
43 43 ),
44 44 },
45   - { header: 'Process', key: 'processDefinitionKey' },
46   - { header: 'Form Key', key: 'formKey', render: (r) => r.formKey ?? '\u2014' },
47   - { header: 'Created', key: 'createTime' },
48   - { header: 'Assignee', key: 'assignee', render: (r) => r.assignee ?? '\u2014' },
  45 + { header: t('label.process'), key: 'processDefinitionKey' },
  46 + { header: t('label.formKey'), key: 'formKey', render: (r) => r.formKey ?? '\u2014' },
  47 + { header: t('label.created'), key: 'createTime' },
  48 + { header: t('label.assignee'), key: 'assignee', render: (r) => r.assignee ?? '\u2014' },
49 49 ]
50 50  
51 51 return (
... ... @@ -61,7 +61,7 @@ export function UserTasksPage() {
61 61 rows={rows}
62 62 columns={columns}
63 63 rowKey={(r) => r.taskId}
64   - empty={<div className="p-6 text-sm text-slate-400">No pending tasks</div>}
  64 + empty={<div className="p-6 text-sm text-slate-400">{t('label.noPendingTasks')}</div>}
65 65 />
66 66 )}
67 67 </div>
... ...
web/src/pages/UsersPage.tsx
... ... @@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 6 import { Loading } from '@/components/Loading'
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10  
10 11 export function UsersPage() {
  12 + const t = useT()
11 13 const [rows, setRows] = useState<User[]>([])
12 14 const [error, setError] = useState<Error | null>(null)
13 15 const [loading, setLoading] = useState(true)
... ... @@ -22,7 +24,7 @@ export function UsersPage() {
22 24  
23 25 const columns: Column<User>[] = [
24 26 {
25   - header: 'Username',
  27 + header: t('label.username'),
26 28 key: 'username',
27 29 render: (r) => (
28 30 <Link to={`/users/${r.id}`} className="font-mono text-brand-600 hover:underline">
... ... @@ -30,16 +32,16 @@ export function UsersPage() {
30 32 </Link>
31 33 ),
32 34 },
33   - { header: 'Display name', key: 'displayName' },
34   - { header: 'Email', key: 'email', render: (r) => r.email ?? '—' },
  35 + { header: t('label.displayName'), key: 'displayName' },
  36 + { header: t('label.email'), key: 'email', render: (r) => r.email ?? '\u2014' },
35 37 {
36   - header: 'Enabled',
  38 + header: t('label.enabled'),
37 39 key: 'enabled',
38 40 render: (r) =>
39 41 r.enabled ? (
40   - <span className="text-emerald-600">Active</span>
  42 + <span className="text-emerald-600">{t('label.activeStatus')}</span>
41 43 ) : (
42   - <span className="text-slate-400">Disabled</span>
  44 + <span className="text-slate-400">{t('label.disabled')}</span>
43 45 ),
44 46 },
45 47 ]
... ... @@ -47,9 +49,9 @@ export function UsersPage() {
47 49 return (
48 50 <div>
49 51 <PageHeader
50   - title="Users"
51   - subtitle="User accounts in this instance. The admin role has all permissions."
52   - actions={<Link to="/users/new" className="btn-primary">+ New User</Link>}
  52 + title={t('page.users.title')}
  53 + subtitle={t('page.users.subtitle')}
  54 + actions={<Link to="/users/new" className="btn-primary">{t('action.newUser')}</Link>}
53 55 />
54 56 {loading && <Loading />}
55 57 {error && <ErrorBox error={error} />}
... ...
web/src/pages/WorkOrderDetailPage.tsx
1   -// Work-order detail screen read-only header + start/complete
  1 +// Work-order detail screen -- read-only header + start/complete
2 2 // action verbs that drive the v2 state machine. The shop-floor
3 3 // dashboard at /shop-floor handles the per-operation walk for v3
4 4 // routing-equipped orders. v1 SPA keeps this screen simple: start a
... ... @@ -12,10 +12,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
12 12 import { Loading } from '@/components/Loading'
13 13 import { ErrorBox } from '@/components/ErrorBox'
14 14 import { StatusBadge } from '@/components/StatusBadge'
  15 +import { useT } from '@/i18n/LocaleContext'
15 16  
16 17 export function WorkOrderDetailPage() {
17 18 const { id = '' } = useParams<{ id: string }>()
18 19 const navigate = useNavigate()
  20 + const t = useT()
19 21 const [order, setOrder] = useState<WorkOrder | null>(null)
20 22 const [locations, setLocations] = useState<Location[]>([])
21 23 const [outputLocation, setOutputLocation] = useState<string>('')
... ... @@ -60,7 +62,7 @@ export function WorkOrderDetailPage() {
60 62 try {
61 63 await production.startWorkOrder(order.id)
62 64 await refresh()
63   - setActionMessage('Started. Operations can now be walked from the Shop Floor screen.')
  65 + setActionMessage(t('page.workOrderDetail.startMsg'))
64 66 } catch (e: unknown) {
65 67 setError(e instanceof Error ? e : new Error(String(e)))
66 68 } finally {
... ... @@ -70,7 +72,7 @@ export function WorkOrderDetailPage() {
70 72  
71 73 const onComplete = async () => {
72 74 if (!outputLocation) {
73   - setError(new Error('Pick an output location first.'))
  75 + setError(new Error(t('page.workOrderDetail.pickLocation')))
74 76 return
75 77 }
76 78 setActing(true)
... ... @@ -80,7 +82,7 @@ export function WorkOrderDetailPage() {
80 82 await production.completeWorkOrder(order.id, outputLocation)
81 83 await refresh()
82 84 setActionMessage(
83   - `Completed. Materials issued, finished goods credited to ${outputLocation}.`,
  85 + t('page.workOrderDetail.completeMsg').replace('{location}', outputLocation),
84 86 )
85 87 } catch (e: unknown) {
86 88 setError(e instanceof Error ? e : new Error(String(e)))
... ... @@ -95,20 +97,20 @@ export function WorkOrderDetailPage() {
95 97 return (
96 98 <div>
97 99 <PageHeader
98   - title={`Work Order ${order.code}`}
99   - subtitle={`Output: ${order.outputItemCode} × ${order.outputQuantity}${
100   - order.sourceSalesOrderCode ? ' · from SO ' + order.sourceSalesOrderCode : ''
  100 + title={t('page.workOrderDetail.title').replace('{code}', order.code)}
  101 + subtitle={`${t('label.output')}: ${order.outputItemCode} \u00D7 ${order.outputQuantity}${
  102 + order.sourceSalesOrderCode ? ' \u00B7 from SO ' + order.sourceSalesOrderCode : ''
101 103 }`}
102 104 actions={
103 105 <button className="btn-secondary" onClick={() => navigate('/work-orders')}>
104   - ← Back
  106 + {t('action.back')}
105 107 </button>
106 108 }
107 109 />
108 110  
109 111 <div className="card mb-6 p-5">
110 112 <div className="flex items-center gap-3">
111   - <span className="text-sm text-slate-500">Status:</span>
  113 + <span className="text-sm text-slate-500">{t('label.status')}:</span>
112 114 <StatusBadge status={order.status} />
113 115 </div>
114 116 {actionMessage && (
... ... @@ -119,10 +121,10 @@ export function WorkOrderDetailPage() {
119 121 </div>
120 122  
121 123 <div className="card mb-6 p-5">
122   - <h2 className="mb-3 text-base font-semibold text-slate-800">Actions</h2>
  124 + <h2 className="mb-3 text-base font-semibold text-slate-800">{t('label.actions')}</h2>
123 125 <div className="flex flex-wrap items-center gap-3">
124 126 <button className="btn-primary" disabled={!canStart || acting} onClick={onStart}>
125   - Start
  127 + {t('action.start')}
126 128 </button>
127 129 <div className="flex items-center gap-2">
128 130 <select
... ... @@ -138,7 +140,7 @@ export function WorkOrderDetailPage() {
138 140 ))}
139 141 </select>
140 142 <button className="btn-primary" disabled={!canComplete || acting} onClick={onComplete}>
141   - Complete
  143 + {t('action.complete')}
142 144 </button>
143 145 </div>
144 146 </div>
... ... @@ -147,18 +149,18 @@ export function WorkOrderDetailPage() {
147 149 <div className="grid gap-6 md:grid-cols-2">
148 150 <div className="card">
149 151 <div className="border-b border-slate-200 px-5 py-3">
150   - <h2 className="text-base font-semibold text-slate-800">BOM inputs</h2>
  152 + <h2 className="text-base font-semibold text-slate-800">{t('label.bomInputs')}</h2>
151 153 </div>
152 154 {order.inputs.length === 0 ? (
153   - <div className="p-5 text-sm text-slate-400">No BOM lines.</div>
  155 + <div className="p-5 text-sm text-slate-400">{t('label.noBomLines')}</div>
154 156 ) : (
155 157 <table className="table-base">
156 158 <thead className="bg-slate-50">
157 159 <tr>
158   - <th>#</th>
159   - <th>Item</th>
160   - <th>Qty / unit</th>
161   - <th>Source loc</th>
  160 + <th>{t('label.lineNo')}</th>
  161 + <th>{t('label.item')}</th>
  162 + <th>{t('label.qtyPerUnit')}</th>
  163 + <th>{t('label.sourceLoc')}</th>
162 164 </tr>
163 165 </thead>
164 166 <tbody className="divide-y divide-slate-100">
... ... @@ -176,19 +178,19 @@ export function WorkOrderDetailPage() {
176 178 </div>
177 179 <div className="card">
178 180 <div className="border-b border-slate-200 px-5 py-3">
179   - <h2 className="text-base font-semibold text-slate-800">Routing operations</h2>
  181 + <h2 className="text-base font-semibold text-slate-800">{t('label.routingOperations')}</h2>
180 182 </div>
181 183 {order.operations.length === 0 ? (
182   - <div className="p-5 text-sm text-slate-400">No routing.</div>
  184 + <div className="p-5 text-sm text-slate-400">{t('label.noRouting')}</div>
183 185 ) : (
184 186 <table className="table-base">
185 187 <thead className="bg-slate-50">
186 188 <tr>
187   - <th>#</th>
188   - <th>Operation</th>
189   - <th>Work center</th>
190   - <th>Std min</th>
191   - <th>Status</th>
  189 + <th>{t('label.lineNo')}</th>
  190 + <th>{t('label.operation')}</th>
  191 + <th>{t('label.workCenter')}</th>
  192 + <th>{t('label.stdMin')}</th>
  193 + <th>{t('label.status')}</th>
192 194 </tr>
193 195 </thead>
194 196 <tbody className="divide-y divide-slate-100">
... ...
web/src/pages/WorkOrdersPage.tsx
... ... @@ -7,8 +7,10 @@ import { Loading } from &#39;@/components/Loading&#39;
7 7 import { ErrorBox } from '@/components/ErrorBox'
8 8 import { DataTable, type Column } from '@/components/DataTable'
9 9 import { StatusBadge } from '@/components/StatusBadge'
  10 +import { useT } from '@/i18n/LocaleContext'
10 11  
11 12 export function WorkOrdersPage() {
  13 + const t = useT()
12 14 const [rows, setRows] = useState<WorkOrder[]>([])
13 15 const [error, setError] = useState<Error | null>(null)
14 16 const [loading, setLoading] = useState(true)
... ... @@ -23,7 +25,7 @@ export function WorkOrdersPage() {
23 25  
24 26 const columns: Column<WorkOrder>[] = [
25 27 {
26   - header: 'Code',
  28 + header: t('label.code'),
27 29 key: 'code',
28 30 render: (r) => (
29 31 <Link to={`/work-orders/${r.id}`} className="font-mono text-brand-600 hover:underline">
... ... @@ -31,20 +33,20 @@ export function WorkOrdersPage() {
31 33 </Link>
32 34 ),
33 35 },
34   - { header: 'Output', key: 'outputItemCode', render: (r) => <span className="font-mono">{r.outputItemCode}</span> },
  36 + { header: t('label.output'), key: 'outputItemCode', render: (r) => <span className="font-mono">{r.outputItemCode}</span> },
35 37 {
36   - header: 'Qty',
  38 + header: t('label.qty'),
37 39 key: 'outputQuantity',
38 40 render: (r) => <span className="font-mono tabular-nums">{String(r.outputQuantity)}</span>,
39 41 },
40   - { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> },
  42 + { header: t('label.status'), key: 'status', render: (r) => <StatusBadge status={r.status} /> },
41 43 {
42   - header: 'Source SO',
  44 + header: t('label.sourceSO'),
43 45 key: 'sourceSalesOrderCode',
44   - render: (r) => r.sourceSalesOrderCode ?? '',
  46 + render: (r) => r.sourceSalesOrderCode ?? '\u2014',
45 47 },
46 48 {
47   - header: 'Inputs / Ops',
  49 + header: t('label.inputsOps'),
48 50 key: 'opsCount',
49 51 render: (r) => `${r.inputs.length} / ${r.operations.length}`,
50 52 },
... ... @@ -53,9 +55,9 @@ export function WorkOrdersPage() {
53 55 return (
54 56 <div>
55 57 <PageHeader
56   - title="Work Orders"
57   - subtitle="Production orders with BOM inputs and routing operations."
58   - actions={<Link to="/work-orders/new" className="btn-primary">+ New Work Order</Link>}
  58 + title={t('page.workOrders.title')}
  59 + subtitle={t('page.workOrders.subtitle')}
  60 + actions={<Link to="/work-orders/new" className="btn-primary">{t('action.newWorkOrder')}</Link>}
59 61 />
60 62 {loading && <Loading />}
61 63 {error && <ErrorBox error={error} />}
... ...