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,10 +5,12 @@ import { PageHeader } from '@/components/PageHeader'
5 import { Loading } from '@/components/Loading' 5 import { Loading } from '@/components/Loading'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { DataTable, type Column } from '@/components/DataTable' 7 import { DataTable, type Column } from '@/components/DataTable'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 const ACCOUNT_TYPES = ['ASSET', 'LIABILITY', 'EQUITY', 'REVENUE', 'EXPENSE'] as const 10 const ACCOUNT_TYPES = ['ASSET', 'LIABILITY', 'EQUITY', 'REVENUE', 'EXPENSE'] as const
10 11
11 export function AccountsPage() { 12 export function AccountsPage() {
  13 + const t = useT()
12 const [rows, setRows] = useState<Account[]>([]) 14 const [rows, setRows] = useState<Account[]>([])
13 const [error, setError] = useState<Error | null>(null) 15 const [error, setError] = useState<Error | null>(null)
14 const [loading, setLoading] = useState(true) 16 const [loading, setLoading] = useState(true)
@@ -46,44 +48,44 @@ export function AccountsPage() { @@ -46,44 +48,44 @@ export function AccountsPage() {
46 } 48 }
47 49
48 const columns: Column<Account>[] = [ 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 return ( 57 return (
56 <div> 58 <div>
57 <PageHeader 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 actions={ 62 actions={
61 <button className="btn-primary" onClick={() => setShowCreate(!showCreate)}> 63 <button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
62 - {showCreate ? 'Cancel' : '+ New Account'} 64 + {showCreate ? t('action.cancel') : t('action.newAccount')}
63 </button> 65 </button>
64 } 66 }
65 /> 67 />
66 {showCreate && ( 68 {showCreate && (
67 <form onSubmit={onCreate} className="card p-4 mb-4 max-w-2xl flex flex-wrap items-end gap-3"> 69 <form onSubmit={onCreate} className="card p-4 mb-4 max-w-2xl flex flex-wrap items-end gap-3">
68 <div className="w-24"> 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 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)} 72 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
71 placeholder="1300" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" /> 73 placeholder="1300" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
72 </div> 74 </div>
73 <div className="flex-1 min-w-[150px]"> 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 <input type="text" required value={name} onChange={(e) => setName(e.target.value)} 77 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
76 placeholder="Prepaid expenses" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" /> 78 placeholder="Prepaid expenses" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
77 </div> 79 </div>
78 <div className="w-32"> 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 <select value={accountType} onChange={(e) => setAccountType(e.target.value)} 82 <select value={accountType} onChange={(e) => setAccountType(e.target.value)}
81 className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm"> 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 </select> 85 </select>
84 </div> 86 </div>
85 <button type="submit" className="btn-primary" disabled={creating}> 87 <button type="submit" className="btn-primary" disabled={creating}>
86 - {creating ? '...' : 'Create'} 88 + {creating ? '...' : t('action.create')}
87 </button> 89 </button>
88 </form> 90 </form>
89 )} 91 )}
web/src/pages/AdjustStockPage.tsx
@@ -4,9 +4,11 @@ import { catalog, inventory } from &#39;@/api/client&#39; @@ -4,9 +4,11 @@ import { catalog, inventory } from &#39;@/api/client&#39;
4 import type { Item, Location } from '@/types/api' 4 import type { Item, Location } from '@/types/api'
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
  7 +import { useT } from '@/i18n/LocaleContext'
7 8
8 export function AdjustStockPage() { 9 export function AdjustStockPage() {
9 const navigate = useNavigate() 10 const navigate = useNavigate()
  11 + const t = useT()
10 const [items, setItems] = useState<Item[]>([]) 12 const [items, setItems] = useState<Item[]>([])
11 const [locations, setLocations] = useState<Location[]>([]) 13 const [locations, setLocations] = useState<Location[]>([])
12 const [itemCode, setItemCode] = useState('') 14 const [itemCode, setItemCode] = useState('')
@@ -37,7 +39,9 @@ export function AdjustStockPage() { @@ -37,7 +39,9 @@ export function AdjustStockPage() {
37 quantity: Number(quantity), 39 quantity: Number(quantity),
38 }) 40 })
39 setResult( 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 } catch (err: unknown) { 46 } catch (err: unknown) {
43 setError(err instanceof Error ? err : new Error(String(err))) 47 setError(err instanceof Error ? err : new Error(String(err)))
@@ -49,27 +53,27 @@ export function AdjustStockPage() { @@ -49,27 +53,27 @@ export function AdjustStockPage() {
49 return ( 53 return (
50 <div> 54 <div>
51 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg"> 60 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg">
57 <div> 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 <select required value={itemCode} onChange={(e) => setItemCode(e.target.value)} 63 <select required value={itemCode} onChange={(e) => setItemCode(e.target.value)}
60 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 64 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
61 {items.map((i) => <option key={i.id} value={i.code}>{i.code} — {i.name}</option>)} 65 {items.map((i) => <option key={i.id} value={i.code}>{i.code} — {i.name}</option>)}
62 </select> 66 </select>
63 </div> 67 </div>
64 <div> 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 <select required value={locationId} onChange={(e) => setLocationId(e.target.value)} 70 <select required value={locationId} onChange={(e) => setLocationId(e.target.value)}
67 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 71 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
68 {locations.map((l) => <option key={l.id} value={l.id}>{l.code} — {l.name}</option>)} 72 {locations.map((l) => <option key={l.id} value={l.id}>{l.code} — {l.name}</option>)}
69 </select> 73 </select>
70 </div> 74 </div>
71 <div> 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 <input type="number" required min="0" step="1" value={quantity} 77 <input type="number" required min="0" step="1" value={quantity}
74 onChange={(e) => setQuantity(e.target.value)} 78 onChange={(e) => setQuantity(e.target.value)}
75 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm text-right" /> 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,7 +85,7 @@ export function AdjustStockPage() {
81 </div> 85 </div>
82 )} 86 )}
83 <button type="submit" className="btn-primary" disabled={submitting}> 87 <button type="submit" className="btn-primary" disabled={submitting}>
84 - {submitting ? 'Adjusting...' : 'Set Balance'} 88 + {submitting ? t('action.adjusting') : t('action.setBalance')}
85 </button> 89 </button>
86 </form> 90 </form>
87 </div> 91 </div>
web/src/pages/BalancesPage.tsx
@@ -6,6 +6,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -6,6 +6,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DataTable, type Column } from '@/components/DataTable' 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10
10 interface Row extends Record<string, unknown> { 11 interface Row extends Record<string, unknown> {
11 id: string 12 id: string
@@ -15,6 +16,7 @@ interface Row extends Record&lt;string, unknown&gt; { @@ -15,6 +16,7 @@ interface Row extends Record&lt;string, unknown&gt; {
15 } 16 }
16 17
17 export function BalancesPage() { 18 export function BalancesPage() {
  19 + const t = useT()
18 const [rows, setRows] = useState<Row[]>([]) 20 const [rows, setRows] = useState<Row[]>([])
19 const [error, setError] = useState<Error | null>(null) 21 const [error, setError] = useState<Error | null>(null)
20 const [loading, setLoading] = useState(true) 22 const [loading, setLoading] = useState(true)
@@ -37,14 +39,14 @@ export function BalancesPage() { @@ -37,14 +39,14 @@ export function BalancesPage() {
37 }, []) 39 }, [])
38 40
39 const columns: Column<Row>[] = [ 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 key: 'locationCode', 45 key: 'locationCode',
44 render: (r) => <span className="font-mono">{r.locationCode}</span>, 46 render: (r) => <span className="font-mono">{r.locationCode}</span>,
45 }, 47 },
46 { 48 {
47 - header: 'Quantity', 49 + header: t('label.quantity'),
48 key: 'quantity', 50 key: 'quantity',
49 render: (r) => <span className="font-mono tabular-nums">{String(r.quantity)}</span>, 51 render: (r) => <span className="font-mono tabular-nums">{String(r.quantity)}</span>,
50 }, 52 },
@@ -53,9 +55,9 @@ export function BalancesPage() { @@ -53,9 +55,9 @@ export function BalancesPage() {
53 return ( 55 return (
54 <div> 56 <div>
55 <PageHeader 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 {loading && <Loading />} 62 {loading && <Loading />}
61 {error && <ErrorBox error={error} />} 63 {error && <ErrorBox error={error} />}
web/src/pages/CreateItemPage.tsx
@@ -5,11 +5,13 @@ import type { Uom } from &#39;@/types/api&#39; @@ -5,11 +5,13 @@ import type { Uom } from &#39;@/types/api&#39;
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { DynamicExtFields } from '@/components/DynamicExtFields' 7 import { DynamicExtFields } from '@/components/DynamicExtFields'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const 10 const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const
10 11
11 export function CreateItemPage() { 12 export function CreateItemPage() {
12 const navigate = useNavigate() 13 const navigate = useNavigate()
  14 + const t = useT()
13 const [code, setCode] = useState('') 15 const [code, setCode] = useState('')
14 const [name, setName] = useState('') 16 const [name, setName] = useState('')
15 const [description, setDescription] = useState('') 17 const [description, setDescription] = useState('')
@@ -49,31 +51,31 @@ export function CreateItemPage() { @@ -49,31 +51,31 @@ export function CreateItemPage() {
49 return ( 51 return (
50 <div> 52 <div>
51 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl"> 58 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl">
57 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> 59 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
58 <div> 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 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)} 62 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
61 placeholder="PAPER-120G-A3" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 63 placeholder="PAPER-120G-A3" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
62 </div> 64 </div>
63 <div> 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 <input type="text" required value={name} onChange={(e) => setName(e.target.value)} 67 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
66 placeholder="120g A3 coated paper" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 68 placeholder="120g A3 coated paper" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
67 </div> 69 </div>
68 <div> 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 <select value={itemType} onChange={(e) => setItemType(e.target.value)} 72 <select value={itemType} onChange={(e) => setItemType(e.target.value)}
71 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 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 </select> 75 </select>
74 </div> 76 </div>
75 <div> 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 <select value={baseUomCode} onChange={(e) => setBaseUomCode(e.target.value)} 79 <select value={baseUomCode} onChange={(e) => setBaseUomCode(e.target.value)}
78 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 80 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
79 {uoms.map((u) => <option key={u.id} value={u.code}>{u.code} — {u.name}</option>)} 81 {uoms.map((u) => <option key={u.id} value={u.code}>{u.code} — {u.name}</option>)}
@@ -81,7 +83,7 @@ export function CreateItemPage() { @@ -81,7 +83,7 @@ export function CreateItemPage() {
81 </div> 83 </div>
82 </div> 84 </div>
83 <div> 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 <input type="text" value={description} onChange={(e) => setDescription(e.target.value)} 87 <input type="text" value={description} onChange={(e) => setDescription(e.target.value)}
86 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 88 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
87 </div> 89 </div>
@@ -92,7 +94,7 @@ export function CreateItemPage() { @@ -92,7 +94,7 @@ export function CreateItemPage() {
92 /> 94 />
93 {error && <ErrorBox error={error} />} 95 {error && <ErrorBox error={error} />}
94 <button type="submit" className="btn-primary" disabled={submitting}> 96 <button type="submit" className="btn-primary" disabled={submitting}>
95 - {submitting ? 'Creating...' : 'Create Item'} 97 + {submitting ? t('action.creating') : t('page.createItem.submit')}
96 </button> 98 </button>
97 </form> 99 </form>
98 </div> 100 </div>
web/src/pages/CreateLocationPage.tsx
@@ -4,11 +4,13 @@ import { inventory } from &#39;@/api/client&#39; @@ -4,11 +4,13 @@ import { inventory } from &#39;@/api/client&#39;
4 import { PageHeader } from '@/components/PageHeader' 4 import { PageHeader } from '@/components/PageHeader'
5 import { ErrorBox } from '@/components/ErrorBox' 5 import { ErrorBox } from '@/components/ErrorBox'
6 import { DynamicExtFields } from '@/components/DynamicExtFields' 6 import { DynamicExtFields } from '@/components/DynamicExtFields'
  7 +import { useT } from '@/i18n/LocaleContext'
7 8
8 const LOCATION_TYPES = ['WAREHOUSE', 'BIN', 'VIRTUAL'] as const 9 const LOCATION_TYPES = ['WAREHOUSE', 'BIN', 'VIRTUAL'] as const
9 10
10 export function CreateLocationPage() { 11 export function CreateLocationPage() {
11 const navigate = useNavigate() 12 const navigate = useNavigate()
  13 + const t = useT()
12 const [code, setCode] = useState('') 14 const [code, setCode] = useState('')
13 const [name, setName] = useState('') 15 const [name, setName] = useState('')
14 const [type, setType] = useState<string>('WAREHOUSE') 16 const [type, setType] = useState<string>('WAREHOUSE')
@@ -34,26 +36,26 @@ export function CreateLocationPage() { @@ -34,26 +36,26 @@ export function CreateLocationPage() {
34 return ( 36 return (
35 <div> 37 <div>
36 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg"> 43 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg">
42 <div> 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 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)} 46 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
45 placeholder="WH-NEW" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 47 placeholder="WH-NEW" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
46 </div> 48 </div>
47 <div> 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 <input type="text" required value={name} onChange={(e) => setName(e.target.value)} 51 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
50 placeholder="New Warehouse" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 52 placeholder="New Warehouse" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
51 </div> 53 </div>
52 <div> 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 <select value={type} onChange={(e) => setType(e.target.value)} 56 <select value={type} onChange={(e) => setType(e.target.value)}
55 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 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 </select> 59 </select>
58 </div> 60 </div>
59 <DynamicExtFields 61 <DynamicExtFields
@@ -63,7 +65,7 @@ export function CreateLocationPage() { @@ -63,7 +65,7 @@ export function CreateLocationPage() {
63 /> 65 />
64 {error && <ErrorBox error={error} />} 66 {error && <ErrorBox error={error} />}
65 <button type="submit" className="btn-primary" disabled={submitting}> 67 <button type="submit" className="btn-primary" disabled={submitting}>
66 - {submitting ? 'Creating...' : 'Create Location'} 68 + {submitting ? t('action.creating') : t('page.createLocation.submit')}
67 </button> 69 </button>
68 </form> 70 </form>
69 </div> 71 </div>
web/src/pages/CreatePartnerPage.tsx
@@ -4,11 +4,13 @@ import { partners } from &#39;@/api/client&#39; @@ -4,11 +4,13 @@ import { partners } from &#39;@/api/client&#39;
4 import { PageHeader } from '@/components/PageHeader' 4 import { PageHeader } from '@/components/PageHeader'
5 import { ErrorBox } from '@/components/ErrorBox' 5 import { ErrorBox } from '@/components/ErrorBox'
6 import { DynamicExtFields } from '@/components/DynamicExtFields' 6 import { DynamicExtFields } from '@/components/DynamicExtFields'
  7 +import { useT } from '@/i18n/LocaleContext'
7 8
8 const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const 9 const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const
9 10
10 export function CreatePartnerPage() { 11 export function CreatePartnerPage() {
11 const navigate = useNavigate() 12 const navigate = useNavigate()
  13 + const t = useT()
12 const [code, setCode] = useState('') 14 const [code, setCode] = useState('')
13 const [name, setName] = useState('') 15 const [name, setName] = useState('')
14 const [type, setType] = useState<string>('CUSTOMER') 16 const [type, setType] = useState<string>('CUSTOMER')
@@ -41,36 +43,36 @@ export function CreatePartnerPage() { @@ -41,36 +43,36 @@ export function CreatePartnerPage() {
41 return ( 43 return (
42 <div> 44 <div>
43 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl"> 50 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl">
49 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> 51 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
50 <div> 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 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)} 54 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
53 placeholder="CUST-NEWCO" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 55 placeholder="CUST-NEWCO" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
54 </div> 56 </div>
55 <div> 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 <input type="text" required value={name} onChange={(e) => setName(e.target.value)} 59 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
58 placeholder="New Company Ltd." className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 60 placeholder="New Company Ltd." className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
59 </div> 61 </div>
60 <div> 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 <select value={type} onChange={(e) => setType(e.target.value)} 64 <select value={type} onChange={(e) => setType(e.target.value)}
63 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 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 </select> 67 </select>
66 </div> 68 </div>
67 <div> 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 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} 71 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
70 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 72 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
71 </div> 73 </div>
72 <div> 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 <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} 76 <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)}
75 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 77 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
76 </div> 78 </div>
@@ -82,7 +84,7 @@ export function CreatePartnerPage() { @@ -82,7 +84,7 @@ export function CreatePartnerPage() {
82 /> 84 />
83 {error && <ErrorBox error={error} />} 85 {error && <ErrorBox error={error} />}
84 <button type="submit" className="btn-primary" disabled={submitting}> 86 <button type="submit" className="btn-primary" disabled={submitting}>
85 - {submitting ? 'Creating...' : 'Create Partner'} 87 + {submitting ? t('action.creating') : t('page.createPartner.submit')}
86 </button> 88 </button>
87 </form> 89 </form>
88 </div> 90 </div>
web/src/pages/CreatePurchaseOrderPage.tsx
@@ -5,6 +5,7 @@ import type { Item, Partner } from &#39;@/types/api&#39; @@ -5,6 +5,7 @@ import type { Item, Partner } from &#39;@/types/api&#39;
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { DynamicExtFields } from '@/components/DynamicExtFields' 7 import { DynamicExtFields } from '@/components/DynamicExtFields'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 interface LineInput { 10 interface LineInput {
10 itemCode: string 11 itemCode: string
@@ -14,6 +15,7 @@ interface LineInput { @@ -14,6 +15,7 @@ interface LineInput {
14 15
15 export function CreatePurchaseOrderPage() { 16 export function CreatePurchaseOrderPage() {
16 const navigate = useNavigate() 17 const navigate = useNavigate()
  18 + const t = useT()
17 const [code, setCode] = useState('') 19 const [code, setCode] = useState('')
18 const [partnerCode, setPartnerCode] = useState('') 20 const [partnerCode, setPartnerCode] = useState('')
19 const [expectedDate, setExpectedDate] = useState('') 21 const [expectedDate, setExpectedDate] = useState('')
@@ -78,19 +80,19 @@ export function CreatePurchaseOrderPage() { @@ -78,19 +80,19 @@ export function CreatePurchaseOrderPage() {
78 return ( 80 return (
79 <div> 81 <div>
80 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl"> 87 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl">
86 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> 88 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
87 <div> 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 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)} 91 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
90 placeholder="PO-2026-0002" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 92 placeholder="PO-2026-0002" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
91 </div> 93 </div>
92 <div> 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 <select required value={partnerCode} onChange={(e) => setPartnerCode(e.target.value)} 96 <select required value={partnerCode} onChange={(e) => setPartnerCode(e.target.value)}
95 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 97 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
96 {supplierList.map((p) => ( 98 {supplierList.map((p) => (
@@ -99,7 +101,7 @@ export function CreatePurchaseOrderPage() { @@ -99,7 +101,7 @@ export function CreatePurchaseOrderPage() {
99 </select> 101 </select>
100 </div> 102 </div>
101 <div> 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 <input type="date" value={expectedDate} onChange={(e) => setExpectedDate(e.target.value)} 105 <input type="date" value={expectedDate} onChange={(e) => setExpectedDate(e.target.value)}
104 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 106 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
105 </div> 107 </div>
@@ -107,8 +109,8 @@ export function CreatePurchaseOrderPage() { @@ -107,8 +109,8 @@ export function CreatePurchaseOrderPage() {
107 109
108 <div> 110 <div>
109 <div className="flex items-center justify-between mb-2"> 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 </div> 114 </div>
113 <div className="space-y-2"> 115 <div className="space-y-2">
114 {lines.map((line, idx) => ( 116 {lines.map((line, idx) => (
@@ -116,17 +118,17 @@ export function CreatePurchaseOrderPage() { @@ -116,17 +118,17 @@ export function CreatePurchaseOrderPage() {
116 <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span> 118 <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span>
117 <select value={line.itemCode} onChange={(e) => updateLine(idx, 'itemCode', e.target.value)} 119 <select value={line.itemCode} onChange={(e) => updateLine(idx, 'itemCode', e.target.value)}
118 className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm"> 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 {items.map((it) => <option key={it.id} value={it.code}>{it.code} — {it.name}</option>)} 122 {items.map((it) => <option key={it.id} value={it.code}>{it.code} — {it.name}</option>)}
121 </select> 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 onChange={(e) => updateLine(idx, 'quantity', e.target.value)} 125 onChange={(e) => updateLine(idx, 'quantity', e.target.value)}
124 className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" /> 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 onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)} 128 onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)}
127 className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" /> 129 className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
128 <button type="button" className="text-slate-400 hover:text-rose-500" 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 </div> 132 </div>
131 ))} 133 ))}
132 </div> 134 </div>
@@ -139,7 +141,7 @@ export function CreatePurchaseOrderPage() { @@ -139,7 +141,7 @@ export function CreatePurchaseOrderPage() {
139 /> 141 />
140 {error && <ErrorBox error={error} />} 142 {error && <ErrorBox error={error} />}
141 <button type="submit" className="btn-primary" disabled={submitting}> 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 </button> 145 </button>
144 </form> 146 </form>
145 </div> 147 </div>
web/src/pages/CreateSalesOrderPage.tsx
@@ -5,6 +5,7 @@ import type { Item, Partner } from &#39;@/types/api&#39; @@ -5,6 +5,7 @@ import type { Item, Partner } from &#39;@/types/api&#39;
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { DynamicExtFields } from '@/components/DynamicExtFields' 7 import { DynamicExtFields } from '@/components/DynamicExtFields'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 interface LineInput { 10 interface LineInput {
10 itemCode: string 11 itemCode: string
@@ -14,6 +15,7 @@ interface LineInput { @@ -14,6 +15,7 @@ interface LineInput {
14 15
15 export function CreateSalesOrderPage() { 16 export function CreateSalesOrderPage() {
16 const navigate = useNavigate() 17 const navigate = useNavigate()
  18 + const t = useT()
17 const [code, setCode] = useState('') 19 const [code, setCode] = useState('')
18 const [partnerCode, setPartnerCode] = useState('') 20 const [partnerCode, setPartnerCode] = useState('')
19 const [currencyCode] = useState('USD') 21 const [currencyCode] = useState('USD')
@@ -80,18 +82,18 @@ export function CreateSalesOrderPage() { @@ -80,18 +82,18 @@ export function CreateSalesOrderPage() {
80 return ( 82 return (
81 <div> 83 <div>
82 <PageHeader 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 actions={ 87 actions={
86 <button className="btn-secondary" onClick={() => navigate('/sales-orders')}> 88 <button className="btn-secondary" onClick={() => navigate('/sales-orders')}>
87 - Cancel 89 + {t('action.cancel')}
88 </button> 90 </button>
89 } 91 }
90 /> 92 />
91 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl"> 93 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl">
92 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> 94 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
93 <div> 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 <input 97 <input
96 type="text" 98 type="text"
97 required 99 required
@@ -102,7 +104,7 @@ export function CreateSalesOrderPage() { @@ -102,7 +104,7 @@ export function CreateSalesOrderPage() {
102 /> 104 />
103 </div> 105 </div>
104 <div> 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 <select 108 <select
107 required 109 required
108 value={partnerCode} 110 value={partnerCode}
@@ -117,7 +119,7 @@ export function CreateSalesOrderPage() { @@ -117,7 +119,7 @@ export function CreateSalesOrderPage() {
117 </select> 119 </select>
118 </div> 120 </div>
119 <div> 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 <input 123 <input
122 type="text" 124 type="text"
123 value={currencyCode} 125 value={currencyCode}
@@ -129,9 +131,9 @@ export function CreateSalesOrderPage() { @@ -129,9 +131,9 @@ export function CreateSalesOrderPage() {
129 131
130 <div> 132 <div>
131 <div className="flex items-center justify-between mb-2"> 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 <button type="button" className="btn-secondary text-xs" onClick={addLine}> 135 <button type="button" className="btn-secondary text-xs" onClick={addLine}>
134 - + Add line 136 + {t('action.addLine')}
135 </button> 137 </button>
136 </div> 138 </div>
137 <div className="space-y-2"> 139 <div className="space-y-2">
@@ -143,7 +145,7 @@ export function CreateSalesOrderPage() { @@ -143,7 +145,7 @@ export function CreateSalesOrderPage() {
143 onChange={(e) => updateLine(idx, 'itemCode', e.target.value)} 145 onChange={(e) => updateLine(idx, 'itemCode', e.target.value)}
144 className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm" 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 {items.map((it) => ( 149 {items.map((it) => (
148 <option key={it.id} value={it.code}> 150 <option key={it.id} value={it.code}>
149 {it.code} — {it.name} 151 {it.code} — {it.name}
@@ -154,7 +156,7 @@ export function CreateSalesOrderPage() { @@ -154,7 +156,7 @@ export function CreateSalesOrderPage() {
154 type="number" 156 type="number"
155 min="1" 157 min="1"
156 step="1" 158 step="1"
157 - placeholder="Qty" 159 + placeholder={t('label.qty')}
158 value={line.quantity} 160 value={line.quantity}
159 onChange={(e) => updateLine(idx, 'quantity', e.target.value)} 161 onChange={(e) => updateLine(idx, 'quantity', e.target.value)}
160 className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" 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,7 +165,7 @@ export function CreateSalesOrderPage() {
163 type="number" 165 type="number"
164 min="0" 166 min="0"
165 step="0.01" 167 step="0.01"
166 - placeholder="Price" 168 + placeholder={t('label.unitPrice')}
167 value={line.unitPrice} 169 value={line.unitPrice}
168 onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)} 170 onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)}
169 className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" 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,7 +174,7 @@ export function CreateSalesOrderPage() {
172 type="button" 174 type="button"
173 className="text-slate-400 hover:text-rose-500" 175 className="text-slate-400 hover:text-rose-500"
174 onClick={() => removeLine(idx)} 176 onClick={() => removeLine(idx)}
175 - title="Remove line" 177 + title={t('action.delete')}
176 > 178 >
177 &times; 179 &times;
178 </button> 180 </button>
@@ -191,10 +193,10 @@ export function CreateSalesOrderPage() { @@ -191,10 +193,10 @@ export function CreateSalesOrderPage() {
191 193
192 <div className="flex items-center gap-3 pt-2"> 194 <div className="flex items-center gap-3 pt-2">
193 <button type="submit" className="btn-primary" disabled={submitting}> 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 </button> 197 </button>
196 <span className="text-xs text-slate-400"> 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 </span> 200 </span>
199 </div> 201 </div>
200 </form> 202 </form>
web/src/pages/CreateUserPage.tsx
@@ -3,9 +3,11 @@ import { useNavigate } from &#39;react-router-dom&#39; @@ -3,9 +3,11 @@ import { useNavigate } from &#39;react-router-dom&#39;
3 import { identity } from '@/api/client' 3 import { identity } from '@/api/client'
4 import { PageHeader } from '@/components/PageHeader' 4 import { PageHeader } from '@/components/PageHeader'
5 import { ErrorBox } from '@/components/ErrorBox' 5 import { ErrorBox } from '@/components/ErrorBox'
  6 +import { useT } from '@/i18n/LocaleContext'
6 7
7 export function CreateUserPage() { 8 export function CreateUserPage() {
8 const navigate = useNavigate() 9 const navigate = useNavigate()
  10 + const t = useT()
9 const [username, setUsername] = useState('') 11 const [username, setUsername] = useState('')
10 const [displayName, setDisplayName] = useState('') 12 const [displayName, setDisplayName] = useState('')
11 const [email, setEmail] = useState('') 13 const [email, setEmail] = useState('')
@@ -31,29 +33,29 @@ export function CreateUserPage() { @@ -31,29 +33,29 @@ export function CreateUserPage() {
31 return ( 33 return (
32 <div> 34 <div>
33 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg"> 40 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-lg">
39 <div> 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 <input type="text" required value={username} onChange={(e) => setUsername(e.target.value)} 43 <input type="text" required value={username} onChange={(e) => setUsername(e.target.value)}
42 placeholder="jdoe" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 44 placeholder="jdoe" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
43 </div> 45 </div>
44 <div> 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 <input type="text" required value={displayName} onChange={(e) => setDisplayName(e.target.value)} 48 <input type="text" required value={displayName} onChange={(e) => setDisplayName(e.target.value)}
47 placeholder="Jane Doe" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 49 placeholder="Jane Doe" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
48 </div> 50 </div>
49 <div> 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 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} 53 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
52 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 54 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
53 </div> 55 </div>
54 {error && <ErrorBox error={error} />} 56 {error && <ErrorBox error={error} />}
55 <button type="submit" className="btn-primary" disabled={submitting}> 57 <button type="submit" className="btn-primary" disabled={submitting}>
56 - {submitting ? 'Creating...' : 'Create User'} 58 + {submitting ? t('action.creating') : t('page.createUser.submit')}
57 </button> 59 </button>
58 </form> 60 </form>
59 </div> 61 </div>
web/src/pages/CreateWorkOrderPage.tsx
@@ -5,12 +5,14 @@ import type { Item, Location } from &#39;@/types/api&#39; @@ -5,12 +5,14 @@ import type { Item, Location } from &#39;@/types/api&#39;
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { DynamicExtFields } from '@/components/DynamicExtFields' 7 import { DynamicExtFields } from '@/components/DynamicExtFields'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 interface BomLine { itemCode: string; quantityPerUnit: string; sourceLocationCode: string } 10 interface BomLine { itemCode: string; quantityPerUnit: string; sourceLocationCode: string }
10 interface OpLine { operationCode: string; workCenter: string; standardMinutes: string } 11 interface OpLine { operationCode: string; workCenter: string; standardMinutes: string }
11 12
12 export function CreateWorkOrderPage() { 13 export function CreateWorkOrderPage() {
13 const navigate = useNavigate() 14 const navigate = useNavigate()
  15 + const t = useT()
14 const [code, setCode] = useState('') 16 const [code, setCode] = useState('')
15 const [outputItemCode, setOutputItemCode] = useState('') 17 const [outputItemCode, setOutputItemCode] = useState('')
16 const [outputQuantity, setOutputQuantity] = useState('') 18 const [outputQuantity, setOutputQuantity] = useState('')
@@ -77,55 +79,55 @@ export function CreateWorkOrderPage() { @@ -77,55 +79,55 @@ export function CreateWorkOrderPage() {
77 return ( 79 return (
78 <div> 80 <div>
79 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl"> 86 <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl">
85 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> 87 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
86 <div> 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 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)} 90 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
89 placeholder="WO-PRINT-0002" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 91 placeholder="WO-PRINT-0002" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
90 </div> 92 </div>
91 <div> 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 <select required value={outputItemCode} onChange={(e) => setOutputItemCode(e.target.value)} 95 <select required value={outputItemCode} onChange={(e) => setOutputItemCode(e.target.value)}
94 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 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 {items.map((it) => <option key={it.id} value={it.code}>{it.code} — {it.name}</option>)} 98 {items.map((it) => <option key={it.id} value={it.code}>{it.code} — {it.name}</option>)}
97 </select> 99 </select>
98 </div> 100 </div>
99 <div> 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 <input type="number" required min="1" step="1" value={outputQuantity} 103 <input type="number" required min="1" step="1" value={outputQuantity}
102 onChange={(e) => setOutputQuantity(e.target.value)} 104 onChange={(e) => setOutputQuantity(e.target.value)}
103 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm text-right" /> 105 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm text-right" />
104 </div> 106 </div>
105 </div> 107 </div>
106 <div> 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 <input type="date" value={dueDate} onChange={(e) => setDueDate(e.target.value)} 110 <input type="date" value={dueDate} onChange={(e) => setDueDate(e.target.value)}
109 className="mt-1 max-w-xs rounded-md border border-slate-300 px-3 py-2 text-sm" /> 111 className="mt-1 max-w-xs rounded-md border border-slate-300 px-3 py-2 text-sm" />
110 </div> 112 </div>
111 113
112 - {/* ─── BOM inputs ─────────────────────────────────────── */} 114 + {/* BOM inputs */}
113 <div> 115 <div>
114 <div className="flex items-center justify-between mb-2"> 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 </div> 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 <div className="space-y-2"> 121 <div className="space-y-2">
120 {bom.map((b, idx) => ( 122 {bom.map((b, idx) => (
121 <div key={idx} className="flex items-center gap-2"> 123 <div key={idx} className="flex items-center gap-2">
122 <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span> 124 <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span>
123 <select value={b.itemCode} onChange={(e) => updateBom(idx, 'itemCode', e.target.value)} 125 <select value={b.itemCode} onChange={(e) => updateBom(idx, 'itemCode', e.target.value)}
124 className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm"> 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 {items.map((it) => <option key={it.id} value={it.code}>{it.code}</option>)} 128 {items.map((it) => <option key={it.id} value={it.code}>{it.code}</option>)}
127 </select> 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 onChange={(e) => updateBom(idx, 'quantityPerUnit', e.target.value)} 131 onChange={(e) => updateBom(idx, 'quantityPerUnit', e.target.value)}
130 className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" /> 132 className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
131 <select value={b.sourceLocationCode} onChange={(e) => updateBom(idx, 'sourceLocationCode', e.target.value)} 133 <select value={b.sourceLocationCode} onChange={(e) => updateBom(idx, 'sourceLocationCode', e.target.value)}
@@ -138,24 +140,24 @@ export function CreateWorkOrderPage() { @@ -138,24 +140,24 @@ export function CreateWorkOrderPage() {
138 </div> 140 </div>
139 </div> 141 </div>
140 142
141 - {/* ─── Routing operations ─────────────────────────────── */} 143 + {/* Routing operations */}
142 <div> 144 <div>
143 <div className="flex items-center justify-between mb-2"> 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 </div> 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 <div className="space-y-2"> 150 <div className="space-y-2">
149 {ops.map((o, idx) => ( 151 {ops.map((o, idx) => (
150 <div key={idx} className="flex items-center gap-2"> 152 <div key={idx} className="flex items-center gap-2">
151 <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span> 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 onChange={(e) => updateOp(idx, 'operationCode', e.target.value)} 155 onChange={(e) => updateOp(idx, 'operationCode', e.target.value)}
154 className="w-28 rounded-md border border-slate-300 px-2 py-1.5 text-sm" /> 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 onChange={(e) => updateOp(idx, 'workCenter', e.target.value)} 158 onChange={(e) => updateOp(idx, 'workCenter', e.target.value)}
157 className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm" /> 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 onChange={(e) => updateOp(idx, 'standardMinutes', e.target.value)} 161 onChange={(e) => updateOp(idx, 'standardMinutes', e.target.value)}
160 className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" /> 162 className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
161 <button type="button" className="text-slate-400 hover:text-rose-500" onClick={() => removeOp(idx)}>&times;</button> 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,7 +173,7 @@ export function CreateWorkOrderPage() {
171 /> 173 />
172 {error && <ErrorBox error={error} />} 174 {error && <ErrorBox error={error} />}
173 <button type="submit" className="btn-primary" disabled={submitting}> 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 </button> 177 </button>
176 </form> 178 </form>
177 </div> 179 </div>
web/src/pages/DashboardPage.tsx
@@ -13,6 +13,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -13,6 +13,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
13 import { Loading } from '@/components/Loading' 13 import { Loading } from '@/components/Loading'
14 import { ErrorBox } from '@/components/ErrorBox' 14 import { ErrorBox } from '@/components/ErrorBox'
15 import { useAuth } from '@/auth/AuthContext' 15 import { useAuth } from '@/auth/AuthContext'
  16 +import { useT } from '@/i18n/LocaleContext'
16 17
17 interface DashboardCounts { 18 interface DashboardCounts {
18 items: number 19 items: number
@@ -27,6 +28,7 @@ interface DashboardCounts { @@ -27,6 +28,7 @@ interface DashboardCounts {
27 28
28 export function DashboardPage() { 29 export function DashboardPage() {
29 const { username } = useAuth() 30 const { username } = useAuth()
  31 + const t = useT()
30 const [counts, setCounts] = useState<DashboardCounts | null>(null) 32 const [counts, setCounts] = useState<DashboardCounts | null>(null)
31 const [error, setError] = useState<Error | null>(null) 33 const [error, setError] = useState<Error | null>(null)
32 const [loading, setLoading] = useState(true) 34 const [loading, setLoading] = useState(true)
@@ -69,73 +71,68 @@ export function DashboardPage() { @@ -69,73 +71,68 @@ export function DashboardPage() {
69 return ( 71 return (
70 <div> 72 <div>
71 <PageHeader 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 {loading && <Loading />} 77 {loading && <Loading />}
76 {error && <ErrorBox error={error} />} 78 {error && <ErrorBox error={error} />}
77 {counts && ( 79 {counts && (
78 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> 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 <DashboardCard 84 <DashboardCard
83 - label="Work orders in progress" 85 + label={t('page.dashboard.cardWoInProgress')}
84 value={counts.inProgressWorkOrders} 86 value={counts.inProgressWorkOrders}
85 to="/shop-floor" 87 to="/shop-floor"
86 highlight 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 <DashboardCard 91 <DashboardCard
90 - label="Purchase orders" 92 + label={t('page.dashboard.cardPurchaseOrders')}
91 value={counts.purchaseOrders} 93 value={counts.purchaseOrders}
92 to="/purchase-orders" 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 <DashboardCard 97 <DashboardCard
96 - label="Journal entries" 98 + label={t('page.dashboard.cardJournalEntries')}
97 value={counts.journalEntries} 99 value={counts.journalEntries}
98 to="/journal-entries" 100 to="/journal-entries"
99 /> 101 />
100 </div> 102 </div>
101 )} 103 )}
102 <div className="mt-8 card p-5 text-sm text-slate-600"> 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 <p className="mb-3 text-slate-500"> 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 </p> 108 </p>
107 <ol className="list-decimal space-y-2 pl-5"> 109 <ol className="list-decimal space-y-2 pl-5">
108 <li> 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 </li> 117 </li>
117 <li> 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 </li> 122 </li>
123 <li> 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 </li> 126 </li>
128 <li> 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 </li> 132 </li>
135 <li> 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 </li> 136 </li>
140 </ol> 137 </ol>
141 </div> 138 </div>
web/src/pages/EditItemPage.tsx
@@ -6,12 +6,14 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -6,12 +6,14 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DynamicExtFields } from '@/components/DynamicExtFields' 8 import { DynamicExtFields } from '@/components/DynamicExtFields'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10
10 const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const 11 const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const
11 12
12 export function EditItemPage() { 13 export function EditItemPage() {
13 const { id = '' } = useParams<{ id: string }>() 14 const { id = '' } = useParams<{ id: string }>()
14 const navigate = useNavigate() 15 const navigate = useNavigate()
  16 + const t = useT()
15 const [item, setItem] = useState<Item | null>(null) 17 const [item, setItem] = useState<Item | null>(null)
16 const [name, setName] = useState('') 18 const [name, setName] = useState('')
17 const [description, setDescription] = useState('') 19 const [description, setDescription] = useState('')
@@ -60,39 +62,39 @@ export function EditItemPage() { @@ -60,39 +62,39 @@ export function EditItemPage() {
60 return ( 62 return (
61 <div> 63 <div>
62 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl"> 69 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl">
68 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> 70 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
69 <div> 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 <input type="text" required value={name} onChange={(e) => setName(e.target.value)} 73 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
72 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 74 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
73 </div> 75 </div>
74 <div> 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 <select value={itemType} onChange={(e) => setItemType(e.target.value)} 78 <select value={itemType} onChange={(e) => setItemType(e.target.value)}
77 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 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 </select> 81 </select>
80 </div> 82 </div>
81 </div> 83 </div>
82 <div> 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 <input type="text" value={description} onChange={(e) => setDescription(e.target.value)} 86 <input type="text" value={description} onChange={(e) => setDescription(e.target.value)}
85 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 87 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
86 </div> 88 </div>
87 <div className="flex items-center gap-2"> 89 <div className="flex items-center gap-2">
88 <input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} 90 <input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)}
89 className="rounded border-slate-300" id="active" /> 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 </div> 93 </div>
92 <DynamicExtFields entityName="Item" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} /> 94 <DynamicExtFields entityName="Item" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} />
93 {error && <ErrorBox error={error} />} 95 {error && <ErrorBox error={error} />}
94 <button type="submit" className="btn-primary" disabled={submitting}> 96 <button type="submit" className="btn-primary" disabled={submitting}>
95 - {submitting ? 'Saving…' : 'Save Changes'} 97 + {submitting ? t('action.saving') : t('action.saveChanges')}
96 </button> 98 </button>
97 </form> 99 </form>
98 </div> 100 </div>
web/src/pages/EditPartnerPage.tsx
@@ -6,12 +6,14 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -6,12 +6,14 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DynamicExtFields } from '@/components/DynamicExtFields' 8 import { DynamicExtFields } from '@/components/DynamicExtFields'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10
10 const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const 11 const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const
11 12
12 export function EditPartnerPage() { 13 export function EditPartnerPage() {
13 const { id = '' } = useParams<{ id: string }>() 14 const { id = '' } = useParams<{ id: string }>()
14 const navigate = useNavigate() 15 const navigate = useNavigate()
  16 + const t = useT()
15 const [partner, setPartner] = useState<Partner | null>(null) 17 const [partner, setPartner] = useState<Partner | null>(null)
16 const [name, setName] = useState('') 18 const [name, setName] = useState('')
17 const [type, setType] = useState<string>('CUSTOMER') 19 const [type, setType] = useState<string>('CUSTOMER')
@@ -61,31 +63,31 @@ export function EditPartnerPage() { @@ -61,31 +63,31 @@ export function EditPartnerPage() {
61 return ( 63 return (
62 <div> 64 <div>
63 <PageHeader 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 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl"> 70 <form onSubmit={onSubmit} className="card p-6 space-y-4 max-w-2xl">
69 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> 71 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
70 <div> 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 <input type="text" required value={name} onChange={(e) => setName(e.target.value)} 74 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
73 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 75 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
74 </div> 76 </div>
75 <div> 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 <select value={type} onChange={(e) => setType(e.target.value)} 79 <select value={type} onChange={(e) => setType(e.target.value)}
78 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"> 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 </select> 82 </select>
81 </div> 83 </div>
82 <div> 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 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} 86 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
85 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 87 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
86 </div> 88 </div>
87 <div> 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 <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} 91 <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)}
90 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 92 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
91 </div> 93 </div>
@@ -93,7 +95,7 @@ export function EditPartnerPage() { @@ -93,7 +95,7 @@ export function EditPartnerPage() {
93 <DynamicExtFields entityName="Partner" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} /> 95 <DynamicExtFields entityName="Partner" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} />
94 {error && <ErrorBox error={error} />} 96 {error && <ErrorBox error={error} />}
95 <button type="submit" className="btn-primary" disabled={submitting}> 97 <button type="submit" className="btn-primary" disabled={submitting}>
96 - {submitting ? 'Saving…' : 'Save Changes'} 98 + {submitting ? t('action.saving') : t('action.saveChanges')}
97 </button> 99 </button>
98 </form> 100 </form>
99 </div> 101 </div>
web/src/pages/FormDesignerPage.tsx
@@ -17,6 +17,7 @@ import { ErrorBox } from &#39;@/components/ErrorBox&#39; @@ -17,6 +17,7 @@ import { ErrorBox } from &#39;@/components/ErrorBox&#39;
17 import { Loading } from '@/components/Loading' 17 import { Loading } from '@/components/Loading'
18 import { vibeWidgets } from '@/components/form-widgets' 18 import { vibeWidgets } from '@/components/form-widgets'
19 import { vibeTemplates } from '@/components/form-widgets/vibeErpTheme' 19 import { vibeTemplates } from '@/components/form-widgets/vibeErpTheme'
  20 +import { useT } from '@/i18n/LocaleContext'
20 21
21 // ── Types ────────────────────────────────────────────────────────── 22 // ── Types ──────────────────────────────────────────────────────────
22 23
@@ -165,6 +166,7 @@ function hydrateFields(def: FormDefinition): DesignerField[] { @@ -165,6 +166,7 @@ function hydrateFields(def: FormDefinition): DesignerField[] {
165 export function FormDesignerPage() { 166 export function FormDesignerPage() {
166 const { slug: routeSlug } = useParams<{ slug: string }>() 167 const { slug: routeSlug } = useParams<{ slug: string }>()
167 const navigate = useNavigate() 168 const navigate = useNavigate()
  169 + const t = useT()
168 const isEdit = Boolean(routeSlug) 170 const isEdit = Boolean(routeSlug)
169 171
170 // ── Top bar state ── 172 // ── Top bar state ──
@@ -302,15 +304,15 @@ export function FormDesignerPage() { @@ -302,15 +304,15 @@ export function FormDesignerPage() {
302 return ( 304 return (
303 <div> 305 <div>
304 <PageHeader 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 {/* ── Top bar ── */} 311 {/* ── Top bar ── */}
310 <div className="card p-4 mb-4"> 312 <div className="card p-4 mb-4">
311 <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5"> 313 <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
312 <div> 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 <input 316 <input
315 type="text" 317 type="text"
316 value={title} 318 value={title}
@@ -320,7 +322,7 @@ export function FormDesignerPage() { @@ -320,7 +322,7 @@ export function FormDesignerPage() {
320 /> 322 />
321 </div> 323 </div>
322 <div> 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 <input 326 <input
325 type="text" 327 type="text"
326 value={entityName} 328 value={entityName}
@@ -330,7 +332,7 @@ export function FormDesignerPage() { @@ -330,7 +332,7 @@ export function FormDesignerPage() {
330 /> 332 />
331 </div> 333 </div>
332 <div> 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 <select 336 <select
335 value={purpose} 337 value={purpose}
336 onChange={(e) => setPurpose(e.target.value as FormPurpose)} 338 onChange={(e) => setPurpose(e.target.value as FormPurpose)}
@@ -344,7 +346,7 @@ export function FormDesignerPage() { @@ -344,7 +346,7 @@ export function FormDesignerPage() {
344 </select> 346 </select>
345 </div> 347 </div>
346 <div> 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 <input 350 <input
349 type="text" 351 type="text"
350 value={slug} 352 value={slug}
@@ -363,14 +365,14 @@ export function FormDesignerPage() { @@ -363,14 +365,14 @@ export function FormDesignerPage() {
363 disabled={saving} 365 disabled={saving}
364 onClick={handleSave} 366 onClick={handleSave}
365 > 367 >
366 - {saving ? 'Saving...' : 'Save'} 368 + {saving ? t('action.saving') : t('action.save')}
367 </button> 369 </button>
368 <button 370 <button
369 type="button" 371 type="button"
370 className="btn-secondary" 372 className="btn-secondary"
371 onClick={() => navigate('/admin/metadata')} 373 onClick={() => navigate('/admin/metadata')}
372 > 374 >
373 - Discard 375 + {t('action.discard')}
374 </button> 376 </button>
375 </div> 377 </div>
376 </div> 378 </div>
@@ -387,7 +389,7 @@ export function FormDesignerPage() { @@ -387,7 +389,7 @@ export function FormDesignerPage() {
387 {/* ── Left panel: field list (3/5 = 60%) ── */} 389 {/* ── Left panel: field list (3/5 = 60%) ── */}
388 <div className="lg:col-span-3 space-y-2"> 390 <div className="lg:col-span-3 space-y-2">
389 <h2 className="text-xs font-semibold uppercase tracking-wide text-slate-400 mb-3"> 391 <h2 className="text-xs font-semibold uppercase tracking-wide text-slate-400 mb-3">
390 - Fields 392 + {t('label.fields')}
391 </h2> 393 </h2>
392 394
393 {fields.map((field, idx) => ( 395 {fields.map((field, idx) => (
@@ -409,10 +411,10 @@ export function FormDesignerPage() { @@ -409,10 +411,10 @@ export function FormDesignerPage() {
409 411
410 <div className="flex gap-2 pt-2"> 412 <div className="flex gap-2 pt-2">
411 <button type="button" className="btn-secondary" onClick={addField}> 413 <button type="button" className="btn-secondary" onClick={addField}>
412 - + Add Field 414 + {t('action.addField')}
413 </button> 415 </button>
414 <button type="button" className="btn-secondary" onClick={addSection}> 416 <button type="button" className="btn-secondary" onClick={addSection}>
415 - + Add Section Divider 417 + {t('action.addSectionDivider')}
416 </button> 418 </button>
417 </div> 419 </div>
418 </div> 420 </div>
@@ -420,12 +422,12 @@ export function FormDesignerPage() { @@ -420,12 +422,12 @@ export function FormDesignerPage() {
420 {/* ── Right panel: live preview (2/5 = 40%) ── */} 422 {/* ── Right panel: live preview (2/5 = 40%) ── */}
421 <div className="lg:col-span-2"> 423 <div className="lg:col-span-2">
422 <h2 className="text-xs font-semibold uppercase tracking-wide text-slate-400 mb-3"> 424 <h2 className="text-xs font-semibold uppercase tracking-wide text-slate-400 mb-3">
423 - Live Preview 425 + {t('label.livePreview')}
424 </h2> 426 </h2>
425 <div className="card p-6"> 427 <div className="card p-6">
426 {Object.keys(jsonSchema.properties ?? {}).length === 0 ? ( 428 {Object.keys(jsonSchema.properties ?? {}).length === 0 ? (
427 <p className="text-sm text-slate-400 italic"> 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 </p> 431 </p>
430 ) : ( 432 ) : (
431 <Form 433 <Form
@@ -475,6 +477,8 @@ function FieldRow({ @@ -475,6 +477,8 @@ function FieldRow({
475 onRemove, 477 onRemove,
476 onMove, 478 onMove,
477 }: FieldRowProps) { 479 }: FieldRowProps) {
  480 + const t = useT()
  481 +
478 if (field.isSectionDivider) { 482 if (field.isSectionDivider) {
479 return ( 483 return (
480 <div className="card p-3 border-l-4 border-indigo-300"> 484 <div className="card p-3 border-l-4 border-indigo-300">
@@ -485,20 +489,20 @@ function FieldRow({ @@ -485,20 +489,20 @@ function FieldRow({
485 onMove={onMove} 489 onMove={onMove}
486 /> 490 />
487 <span className="text-xs font-semibold uppercase text-indigo-500 mr-2"> 491 <span className="text-xs font-semibold uppercase text-indigo-500 mr-2">
488 - Section 492 + {t('label.section')}
489 </span> 493 </span>
490 <input 494 <input
491 type="text" 495 type="text"
492 value={field.sectionTitle ?? ''} 496 value={field.sectionTitle ?? ''}
493 onChange={(e) => onUpdate({ sectionTitle: e.target.value })} 497 onChange={(e) => onUpdate({ sectionTitle: e.target.value })}
494 - placeholder="Section title" 498 + placeholder={t('label.sectionTitle')}
495 className="flex-1 rounded-md border border-slate-300 px-2 py-1 text-sm" 499 className="flex-1 rounded-md border border-slate-300 px-2 py-1 text-sm"
496 /> 500 />
497 <button 501 <button
498 type="button" 502 type="button"
499 onClick={onRemove} 503 onClick={onRemove}
500 className="text-slate-400 hover:text-rose-500 text-sm px-1" 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 x 507 x
504 </button> 508 </button>
@@ -524,7 +528,7 @@ function FieldRow({ @@ -524,7 +528,7 @@ function FieldRow({
524 type="text" 528 type="text"
525 value={field.label} 529 value={field.label}
526 onChange={(e) => onUpdate({ label: e.target.value })} 530 onChange={(e) => onUpdate({ label: e.target.value })}
527 - placeholder="Label" 531 + placeholder={t('label.label')}
528 className="flex-1 rounded-md border border-slate-300 px-2 py-1 text-sm" 532 className="flex-1 rounded-md border border-slate-300 px-2 py-1 text-sm"
529 /> 533 />
530 <select 534 <select
@@ -534,9 +538,9 @@ function FieldRow({ @@ -534,9 +538,9 @@ function FieldRow({
534 } 538 }
535 className="w-24 rounded-md border border-slate-300 px-2 py-1 text-sm" 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 </option> 544 </option>
541 ))} 545 ))}
542 </select> 546 </select>
@@ -547,7 +551,7 @@ function FieldRow({ @@ -547,7 +551,7 @@ function FieldRow({
547 onChange={(e) => onUpdate({ required: e.target.checked })} 551 onChange={(e) => onUpdate({ required: e.target.checked })}
548 className="rounded border-slate-300" 552 className="rounded border-slate-300"
549 /> 553 />
550 - Req 554 + {t('label.req')}
551 </label> 555 </label>
552 <select 556 <select
553 value={field.width} 557 value={field.width}
@@ -555,11 +559,11 @@ function FieldRow({ @@ -555,11 +559,11 @@ function FieldRow({
555 onUpdate({ width: Number(e.target.value) as 1 | 2 | 3 }) 559 onUpdate({ width: Number(e.target.value) as 1 | 2 | 3 })
556 } 560 }
557 className="w-20 rounded-md border border-slate-300 px-2 py-1 text-sm" 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 {WIDTH_OPTIONS.map((w) => ( 564 {WIDTH_OPTIONS.map((w) => (
561 <option key={w} value={w}> 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 </option> 567 </option>
564 ))} 568 ))}
565 </select> 569 </select>
@@ -567,7 +571,7 @@ function FieldRow({ @@ -567,7 +571,7 @@ function FieldRow({
567 type="button" 571 type="button"
568 onClick={onToggleExpand} 572 onClick={onToggleExpand}
569 className="text-slate-400 hover:text-slate-600 text-sm px-1" 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 {isExpanded ? '\u25B2' : '\u25BC'} 576 {isExpanded ? '\u25B2' : '\u25BC'}
573 </button> 577 </button>
@@ -575,7 +579,7 @@ function FieldRow({ @@ -575,7 +579,7 @@ function FieldRow({
575 type="button" 579 type="button"
576 onClick={onRemove} 580 onClick={onRemove}
577 className="text-slate-400 hover:text-rose-500 text-sm px-1" 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 x 584 x
581 </button> 585 </button>
@@ -586,7 +590,7 @@ function FieldRow({ @@ -586,7 +590,7 @@ function FieldRow({
586 <div className="mt-3 ml-10 grid grid-cols-1 gap-3 sm:grid-cols-2 border-t border-slate-100 pt-3"> 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 <div> 591 <div>
588 <label className="block text-sm font-medium text-slate-700"> 592 <label className="block text-sm font-medium text-slate-700">
589 - Label (English) 593 + {t('label.labelEnglish')}
590 </label> 594 </label>
591 <input 595 <input
592 type="text" 596 type="text"
@@ -597,7 +601,7 @@ function FieldRow({ @@ -597,7 +601,7 @@ function FieldRow({
597 </div> 601 </div>
598 <div> 602 <div>
599 <label className="block text-sm font-medium text-slate-700"> 603 <label className="block text-sm font-medium text-slate-700">
600 - Placeholder 604 + {t('label.placeholder')}
601 </label> 605 </label>
602 <input 606 <input
603 type="text" 607 type="text"
@@ -610,7 +614,7 @@ function FieldRow({ @@ -610,7 +614,7 @@ function FieldRow({
610 </div> 614 </div>
611 <div className="sm:col-span-2"> 615 <div className="sm:col-span-2">
612 <label className="block text-sm font-medium text-slate-700"> 616 <label className="block text-sm font-medium text-slate-700">
613 - Help text 617 + {t('label.helpText')}
614 </label> 618 </label>
615 <input 619 <input
616 type="text" 620 type="text"
@@ -623,7 +627,7 @@ function FieldRow({ @@ -623,7 +627,7 @@ function FieldRow({
623 </div> 627 </div>
624 <div> 628 <div>
625 <label className="block text-sm font-medium text-slate-700"> 629 <label className="block text-sm font-medium text-slate-700">
626 - Widget override 630 + {t('label.widgetOverride')}
627 </label> 631 </label>
628 <select 632 <select
629 value={field.widgetOverride ?? ''} 633 value={field.widgetOverride ?? ''}
@@ -634,17 +638,17 @@ function FieldRow({ @@ -634,17 +638,17 @@ function FieldRow({
634 > 638 >
635 {WIDGET_OPTIONS.map((w) => ( 639 {WIDGET_OPTIONS.map((w) => (
636 <option key={w} value={w}> 640 <option key={w} value={w}>
637 - {w || '(default)'} 641 + {w || t('label.widgetDefault')}
638 </option> 642 </option>
639 ))} 643 ))}
640 </select> 644 </select>
641 </div> 645 </div>
642 <div> 646 <div>
643 <label className="block text-sm font-medium text-slate-700"> 647 <label className="block text-sm font-medium text-slate-700">
644 - Visibility condition 648 + {t('label.visibilityCondition')}
645 </label> 649 </label>
646 <div className="mt-1 flex items-center gap-2"> 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 <select 652 <select
649 value={field.visibleWhen?.field ?? ''} 653 value={field.visibleWhen?.field ?? ''}
650 onChange={(e) => 654 onChange={(e) =>
@@ -659,7 +663,7 @@ function FieldRow({ @@ -659,7 +663,7 @@ function FieldRow({
659 } 663 }
660 className="rounded-md border border-slate-300 px-2 py-1 text-sm" 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 {fieldKeys 667 {fieldKeys
664 .filter((k) => k !== field.key) 668 .filter((k) => k !== field.key)
665 .map((k) => ( 669 .map((k) => (
@@ -668,7 +672,7 @@ function FieldRow({ @@ -668,7 +672,7 @@ function FieldRow({
668 </option> 672 </option>
669 ))} 673 ))}
670 </select> 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 <input 676 <input
673 type="text" 677 type="text"
674 value={field.visibleWhen?.equals ?? ''} 678 value={field.visibleWhen?.equals ?? ''}
@@ -702,6 +706,7 @@ function ReorderButtons({ @@ -702,6 +706,7 @@ function ReorderButtons({
702 total: number 706 total: number
703 onMove: (direction: 'up' | 'down') => void 707 onMove: (direction: 'up' | 'down') => void
704 }) { 708 }) {
  709 + const t = useT()
705 return ( 710 return (
706 <div className="flex flex-col"> 711 <div className="flex flex-col">
707 <button 712 <button
@@ -709,7 +714,7 @@ function ReorderButtons({ @@ -709,7 +714,7 @@ function ReorderButtons({
709 onClick={() => onMove('up')} 714 onClick={() => onMove('up')}
710 disabled={index === 0} 715 disabled={index === 0}
711 className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-xs leading-none" 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 &#9650; 719 &#9650;
715 </button> 720 </button>
@@ -718,7 +723,7 @@ function ReorderButtons({ @@ -718,7 +723,7 @@ function ReorderButtons({
718 onClick={() => onMove('down')} 723 onClick={() => onMove('down')}
719 disabled={index === total - 1} 724 disabled={index === total - 1}
720 className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-xs leading-none" 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 &#9660; 728 &#9660;
724 </button> 729 </button>
web/src/pages/ItemsPage.tsx
@@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DataTable, type Column } from '@/components/DataTable' 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10
10 export function ItemsPage() { 11 export function ItemsPage() {
  12 + const t = useT()
11 const [rows, setRows] = useState<Item[]>([]) 13 const [rows, setRows] = useState<Item[]>([])
12 const [error, setError] = useState<Error | null>(null) 14 const [error, setError] = useState<Error | null>(null)
13 const [loading, setLoading] = useState(true) 15 const [loading, setLoading] = useState(true)
@@ -22,28 +24,28 @@ export function ItemsPage() { @@ -22,28 +24,28 @@ export function ItemsPage() {
22 24
23 const columns: Column<Item>[] = [ 25 const columns: Column<Item>[] = [
24 { 26 {
25 - header: 'Code', 27 + header: t('label.code'),
26 key: 'code', 28 key: 'code',
27 render: (r) => ( 29 render: (r) => (
28 <Link to={`/items/${r.id}/edit`} className="font-mono text-brand-600 hover:underline">{r.code}</Link> 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 { header: 'UoM', key: 'baseUomCode', render: (r) => <span className="font-mono">{r.baseUomCode}</span> }, 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 key: 'active', 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 return ( 43 return (
42 <div> 44 <div>
43 <PageHeader 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 {loading && <Loading />} 50 {loading && <Loading />}
49 {error && <ErrorBox error={error} />} 51 {error && <ErrorBox error={error} />}
web/src/pages/JournalEntriesPage.tsx
@@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
5 import { Loading } from '@/components/Loading' 5 import { Loading } from '@/components/Loading'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { StatusBadge } from '@/components/StatusBadge' 7 import { StatusBadge } from '@/components/StatusBadge'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 export function JournalEntriesPage() { 10 export function JournalEntriesPage() {
  11 + const t = useT()
10 const [rows, setRows] = useState<JournalEntry[]>([]) 12 const [rows, setRows] = useState<JournalEntry[]>([])
11 const [error, setError] = useState<Error | null>(null) 13 const [error, setError] = useState<Error | null>(null)
12 const [loading, setLoading] = useState(true) 14 const [loading, setLoading] = useState(true)
@@ -34,26 +36,26 @@ export function JournalEntriesPage() { @@ -34,26 +36,26 @@ export function JournalEntriesPage() {
34 return ( 36 return (
35 <div> 37 <div>
36 <PageHeader 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 {loading && <Loading />} 42 {loading && <Loading />}
41 {error && <ErrorBox error={error} />} 43 {error && <ErrorBox error={error} />}
42 {!loading && !error && rows.length === 0 && ( 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 {!loading && !error && rows.length > 0 && ( 47 {!loading && !error && rows.length > 0 && (
46 <div className="card overflow-x-auto"> 48 <div className="card overflow-x-auto">
47 <table className="table-base"> 49 <table className="table-base">
48 <thead className="bg-slate-50"> 50 <thead className="bg-slate-50">
49 <tr> 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 </tr> 59 </tr>
58 </thead> 60 </thead>
59 <tbody className="divide-y divide-slate-100"> 61 <tbody className="divide-y divide-slate-100">
@@ -66,7 +68,7 @@ export function JournalEntriesPage() { @@ -66,7 +68,7 @@ export function JournalEntriesPage() {
66 className="hover:bg-slate-50 cursor-pointer" 68 className="hover:bg-slate-50 cursor-pointer"
67 onClick={() => toggle(je.id)} 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 <td>{je.type}</td> 72 <td>{je.type}</td>
71 <td><StatusBadge status={je.status} /></td> 73 <td><StatusBadge status={je.status} /></td>
72 <td className="font-mono">{je.orderCode}</td> 74 <td className="font-mono">{je.orderCode}</td>
@@ -83,11 +85,11 @@ export function JournalEntriesPage() { @@ -83,11 +85,11 @@ export function JournalEntriesPage() {
83 <table className="min-w-full text-xs"> 85 <table className="min-w-full text-xs">
84 <thead> 86 <thead>
85 <tr className="text-slate-400"> 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 </tr> 93 </tr>
92 </thead> 94 </thead>
93 <tbody> 95 <tbody>
web/src/pages/ListViewDesignerPage.tsx
@@ -16,6 +16,7 @@ import { metadata } from &#39;@/api/client&#39; @@ -16,6 +16,7 @@ import { metadata } from &#39;@/api/client&#39;
16 import { PageHeader } from '@/components/PageHeader' 16 import { PageHeader } from '@/components/PageHeader'
17 import { ErrorBox } from '@/components/ErrorBox' 17 import { ErrorBox } from '@/components/ErrorBox'
18 import { DataTable, type Column } from '@/components/DataTable' 18 import { DataTable, type Column } from '@/components/DataTable'
  19 +import { useT } from '@/i18n/LocaleContext'
19 20
20 // ─── Designer state types ────────────────────────────────────────── 21 // ─── Designer state types ──────────────────────────────────────────
21 22
@@ -65,6 +66,7 @@ function emptyState(): DesignerState { @@ -65,6 +66,7 @@ function emptyState(): DesignerState {
65 66
66 export function ListViewDesignerPage() { 67 export function ListViewDesignerPage() {
67 const navigate = useNavigate() 68 const navigate = useNavigate()
  69 + const t = useT()
68 const { slug: routeSlug } = useParams<{ slug: string }>() 70 const { slug: routeSlug } = useParams<{ slug: string }>()
69 const isEdit = Boolean(routeSlug) 71 const isEdit = Boolean(routeSlug)
70 72
@@ -254,7 +256,7 @@ export function ListViewDesignerPage() { @@ -254,7 +256,7 @@ export function ListViewDesignerPage() {
254 256
255 if (loading) { 257 if (loading) {
256 return ( 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,14 +270,14 @@ export function ListViewDesignerPage() {
268 return ( 270 return (
269 <div> 271 <div>
270 <PageHeader 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 actions={ 275 actions={
274 <button 276 <button
275 className="btn-secondary" 277 className="btn-secondary"
276 onClick={() => navigate('/admin/metadata')} 278 onClick={() => navigate('/admin/metadata')}
277 > 279 >
278 - Cancel 280 + {t('action.cancel')}
279 </button> 281 </button>
280 } 282 }
281 /> 283 />
@@ -285,11 +287,11 @@ export function ListViewDesignerPage() { @@ -285,11 +287,11 @@ export function ListViewDesignerPage() {
285 287
286 {/* ── Top bar: title, entity, slug ───────────────────── */} 288 {/* ── Top bar: title, entity, slug ───────────────────── */}
287 <div className="card p-6 space-y-4"> 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 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> 291 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
290 <div> 292 <div>
291 <label className="block text-sm font-medium text-slate-700"> 293 <label className="block text-sm font-medium text-slate-700">
292 - Title 294 + {t('label.title')}
293 </label> 295 </label>
294 <input 296 <input
295 type="text" 297 type="text"
@@ -302,7 +304,7 @@ export function ListViewDesignerPage() { @@ -302,7 +304,7 @@ export function ListViewDesignerPage() {
302 </div> 304 </div>
303 <div> 305 <div>
304 <label className="block text-sm font-medium text-slate-700"> 306 <label className="block text-sm font-medium text-slate-700">
305 - Entity Name 307 + {t('label.entityName')}
306 </label> 308 </label>
307 <input 309 <input
308 type="text" 310 type="text"
@@ -315,7 +317,7 @@ export function ListViewDesignerPage() { @@ -315,7 +317,7 @@ export function ListViewDesignerPage() {
315 </div> 317 </div>
316 <div> 318 <div>
317 <label className="block text-sm font-medium text-slate-700"> 319 <label className="block text-sm font-medium text-slate-700">
318 - Slug 320 + {t('label.slug')}
319 </label> 321 </label>
320 <input 322 <input
321 type="text" 323 type="text"
@@ -333,31 +335,31 @@ export function ListViewDesignerPage() { @@ -333,31 +335,31 @@ export function ListViewDesignerPage() {
333 {/* ── Columns section ────────────────────────────────── */} 335 {/* ── Columns section ────────────────────────────────── */}
334 <div className="card p-6 space-y-4"> 336 <div className="card p-6 space-y-4">
335 <div className="flex items-center justify-between"> 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 <button 339 <button
338 type="button" 340 type="button"
339 className="btn-secondary text-xs" 341 className="btn-secondary text-xs"
340 onClick={addColumn} 342 onClick={addColumn}
341 > 343 >
342 - + Add Column 344 + {t('action.addColumn')}
343 </button> 345 </button>
344 </div> 346 </div>
345 347
346 {state.columns.length === 0 ? ( 348 {state.columns.length === 0 ? (
347 <p className="text-sm text-slate-400"> 349 <p className="text-sm text-slate-400">
348 - No columns defined yet. Click "Add Column" to start. 350 + {t('label.noColumnsHint')}
349 </p> 351 </p>
350 ) : ( 352 ) : (
351 <div className="overflow-x-auto"> 353 <div className="overflow-x-auto">
352 <table className="table-base text-sm"> 354 <table className="table-base text-sm">
353 <thead className="bg-slate-50"> 355 <thead className="bg-slate-50">
354 <tr> 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 <th className="w-10"></th> 363 <th className="w-10"></th>
362 </tr> 364 </tr>
363 </thead> 365 </thead>
@@ -428,7 +430,7 @@ export function ListViewDesignerPage() { @@ -428,7 +430,7 @@ export function ListViewDesignerPage() {
428 className="rounded p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 disabled:opacity-30" 430 className="rounded p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 disabled:opacity-30"
429 disabled={idx === 0} 431 disabled={idx === 0}
430 onClick={() => moveColumn(idx, -1)} 432 onClick={() => moveColumn(idx, -1)}
431 - title="Move up" 433 + title={t('label.moveUp')}
432 > 434 >
433 &#9650; 435 &#9650;
434 </button> 436 </button>
@@ -437,7 +439,7 @@ export function ListViewDesignerPage() { @@ -437,7 +439,7 @@ export function ListViewDesignerPage() {
437 className="rounded p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 disabled:opacity-30" 439 className="rounded p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 disabled:opacity-30"
438 disabled={idx === state.columns.length - 1} 440 disabled={idx === state.columns.length - 1}
439 onClick={() => moveColumn(idx, 1)} 441 onClick={() => moveColumn(idx, 1)}
440 - title="Move down" 442 + title={t('label.moveDown')}
441 > 443 >
442 &#9660; 444 &#9660;
443 </button> 445 </button>
@@ -448,7 +450,7 @@ export function ListViewDesignerPage() { @@ -448,7 +450,7 @@ export function ListViewDesignerPage() {
448 type="button" 450 type="button"
449 className="text-slate-400 hover:text-rose-500" 451 className="text-slate-400 hover:text-rose-500"
450 onClick={() => removeColumn(idx)} 452 onClick={() => removeColumn(idx)}
451 - title="Remove column" 453 + title={t('label.removeColumn')}
452 > 454 >
453 &times; 455 &times;
454 </button> 456 </button>
@@ -464,19 +466,19 @@ export function ListViewDesignerPage() { @@ -464,19 +466,19 @@ export function ListViewDesignerPage() {
464 {/* ── Filters section ────────────────────────────────── */} 466 {/* ── Filters section ────────────────────────────────── */}
465 <div className="card p-6 space-y-4"> 467 <div className="card p-6 space-y-4">
466 <div className="flex items-center justify-between"> 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 <button 470 <button
469 type="button" 471 type="button"
470 className="btn-secondary text-xs" 472 className="btn-secondary text-xs"
471 onClick={addFilter} 473 onClick={addFilter}
472 > 474 >
473 - + Add Filter 475 + {t('action.addFilter')}
474 </button> 476 </button>
475 </div> 477 </div>
476 478
477 {state.filters.length === 0 ? ( 479 {state.filters.length === 0 ? (
478 <p className="text-sm text-slate-400"> 480 <p className="text-sm text-slate-400">
479 - No filters defined. Click "Add Filter" to add filterable fields. 481 + {t('label.noFiltersHint')}
480 </p> 482 </p>
481 ) : ( 483 ) : (
482 <div className="space-y-2"> 484 <div className="space-y-2">
@@ -488,7 +490,7 @@ export function ListViewDesignerPage() { @@ -488,7 +490,7 @@ export function ListViewDesignerPage() {
488 onChange={(e) => 490 onChange={(e) =>
489 updateFilter(idx, { field: e.target.value }) 491 updateFilter(idx, { field: e.target.value })
490 } 492 }
491 - placeholder="Field name" 493 + placeholder={t('label.fieldName')}
492 className={smallInputCls + ' flex-1'} 494 className={smallInputCls + ' flex-1'}
493 /> 495 />
494 <select 496 <select
@@ -510,14 +512,14 @@ export function ListViewDesignerPage() { @@ -510,14 +512,14 @@ export function ListViewDesignerPage() {
510 onChange={(e) => 512 onChange={(e) =>
511 updateFilter(idx, { label: e.target.value }) 513 updateFilter(idx, { label: e.target.value })
512 } 514 }
513 - placeholder="Display label" 515 + placeholder={t('label.displayLabel')}
514 className={smallInputCls + ' flex-1'} 516 className={smallInputCls + ' flex-1'}
515 /> 517 />
516 <button 518 <button
517 type="button" 519 type="button"
518 className="text-slate-400 hover:text-rose-500" 520 className="text-slate-400 hover:text-rose-500"
519 onClick={() => removeFilter(idx)} 521 onClick={() => removeFilter(idx)}
520 - title="Remove filter" 522 + title={t('label.removeFilter')}
521 > 523 >
522 &times; 524 &times;
523 </button> 525 </button>
@@ -530,12 +532,12 @@ export function ListViewDesignerPage() { @@ -530,12 +532,12 @@ export function ListViewDesignerPage() {
530 {/* ── Sorting & page size section ────────────────────── */} 532 {/* ── Sorting & page size section ────────────────────── */}
531 <div className="card p-6 space-y-4"> 533 <div className="card p-6 space-y-4">
532 <h2 className="text-lg font-semibold text-slate-800"> 534 <h2 className="text-lg font-semibold text-slate-800">
533 - Sorting &amp; Pagination 535 + {t('label.sortingPagination')}
534 </h2> 536 </h2>
535 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> 537 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
536 <div> 538 <div>
537 <label className="block text-sm font-medium text-slate-700"> 539 <label className="block text-sm font-medium text-slate-700">
538 - Default Sort Field 540 + {t('label.defaultSortField')}
539 </label> 541 </label>
540 <select 542 <select
541 value={state.defaultSort?.field ?? ''} 543 value={state.defaultSort?.field ?? ''}
@@ -552,7 +554,7 @@ export function ListViewDesignerPage() { @@ -552,7 +554,7 @@ export function ListViewDesignerPage() {
552 }} 554 }}
553 className={inputCls} 555 className={inputCls}
554 > 556 >
555 - <option value="">-- none --</option> 557 + <option value="">{t('label.none')}</option>
556 {state.columns 558 {state.columns
557 .filter((c) => c.field.trim()) 559 .filter((c) => c.field.trim())
558 .map((c) => ( 560 .map((c) => (
@@ -564,7 +566,7 @@ export function ListViewDesignerPage() { @@ -564,7 +566,7 @@ export function ListViewDesignerPage() {
564 </div> 566 </div>
565 <div> 567 <div>
566 <label className="block text-sm font-medium text-slate-700"> 568 <label className="block text-sm font-medium text-slate-700">
567 - Direction 569 + {t('label.direction')}
568 </label> 570 </label>
569 <select 571 <select
570 value={state.defaultSort?.direction ?? 'asc'} 572 value={state.defaultSort?.direction ?? 'asc'}
@@ -578,13 +580,13 @@ export function ListViewDesignerPage() { @@ -578,13 +580,13 @@ export function ListViewDesignerPage() {
578 disabled={!state.defaultSort} 580 disabled={!state.defaultSort}
579 className={`${inputCls}${!state.defaultSort ? ' bg-slate-50 text-slate-500' : ''}`} 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 </select> 585 </select>
584 </div> 586 </div>
585 <div> 587 <div>
586 <label className="block text-sm font-medium text-slate-700"> 588 <label className="block text-sm font-medium text-slate-700">
587 - Page Size 589 + {t('label.pageSize')}
588 </label> 590 </label>
589 <input 591 <input
590 type="number" 592 type="number"
@@ -602,10 +604,10 @@ export function ListViewDesignerPage() { @@ -602,10 +604,10 @@ export function ListViewDesignerPage() {
602 604
603 {/* ── Preview section ────────────────────────────────── */} 605 {/* ── Preview section ────────────────────────────────── */}
604 <div className="card p-6 space-y-4"> 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 {previewColumns.length === 0 ? ( 608 {previewColumns.length === 0 ? (
607 <p className="text-sm text-slate-400"> 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 </p> 611 </p>
610 ) : ( 612 ) : (
611 <DataTable 613 <DataTable
@@ -619,14 +621,14 @@ export function ListViewDesignerPage() { @@ -619,14 +621,14 @@ export function ListViewDesignerPage() {
619 {/* ── Actions ────────────────────────────────────────── */} 621 {/* ── Actions ────────────────────────────────────────── */}
620 <div className="flex items-center gap-3"> 622 <div className="flex items-center gap-3">
621 <button type="submit" className="btn-primary" disabled={saving}> 623 <button type="submit" className="btn-primary" disabled={saving}>
622 - {saving ? 'Saving...' : 'Save List View'} 624 + {saving ? t('action.saving') : t('action.saveListView')}
623 </button> 625 </button>
624 <button 626 <button
625 type="button" 627 type="button"
626 className="btn-secondary" 628 className="btn-secondary"
627 onClick={() => navigate('/admin/metadata')} 629 onClick={() => navigate('/admin/metadata')}
628 > 630 >
629 - Cancel 631 + {t('action.cancel')}
630 </button> 632 </button>
631 </div> 633 </div>
632 </form> 634 </form>
web/src/pages/LocationsPage.tsx
@@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DataTable, type Column } from '@/components/DataTable' 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10
10 export function LocationsPage() { 11 export function LocationsPage() {
  12 + const t = useT()
11 const [rows, setRows] = useState<Location[]>([]) 13 const [rows, setRows] = useState<Location[]>([])
12 const [error, setError] = useState<Error | null>(null) 14 const [error, setError] = useState<Error | null>(null)
13 const [loading, setLoading] = useState(true) 15 const [loading, setLoading] = useState(true)
@@ -21,22 +23,22 @@ export function LocationsPage() { @@ -21,22 +23,22 @@ export function LocationsPage() {
21 }, []) 23 }, [])
22 24
23 const columns: Column<Location>[] = [ 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 key: 'active', 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 return ( 36 return (
35 <div> 37 <div>
36 <PageHeader 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 {loading && <Loading />} 43 {loading && <Loading />}
42 {error && <ErrorBox error={error} />} 44 {error && <ErrorBox error={error} />}
web/src/pages/LoginPage.tsx
@@ -4,6 +4,7 @@ import { useAuth } from &#39;@/auth/AuthContext&#39; @@ -4,6 +4,7 @@ import { useAuth } from &#39;@/auth/AuthContext&#39;
4 import { meta } from '@/api/client' 4 import { meta } from '@/api/client'
5 import type { MetaInfo } from '@/types/api' 5 import type { MetaInfo } from '@/types/api'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
  7 +import { useT } from '@/i18n/LocaleContext'
7 8
8 interface LocationState { 9 interface LocationState {
9 from?: string 10 from?: string
@@ -13,6 +14,7 @@ export function LoginPage() { @@ -13,6 +14,7 @@ export function LoginPage() {
13 const { login, token, loading } = useAuth() 14 const { login, token, loading } = useAuth()
14 const navigate = useNavigate() 15 const navigate = useNavigate()
15 const location = useLocation() 16 const location = useLocation()
  17 + const t = useT()
16 const [username, setUsername] = useState('admin') 18 const [username, setUsername] = useState('admin')
17 const [password, setPassword] = useState('') 19 const [password, setPassword] = useState('')
18 const [error, setError] = useState<Error | null>(null) 20 const [error, setError] = useState<Error | null>(null)
@@ -47,13 +49,13 @@ export function LoginPage() { @@ -47,13 +49,13 @@ export function LoginPage() {
47 <div className="mb-6 text-center"> 49 <div className="mb-6 text-center">
48 <h1 className="text-3xl font-bold text-brand-600">vibe_erp</h1> 50 <h1 className="text-3xl font-bold text-brand-600">vibe_erp</h1>
49 <p className="mt-1 text-sm text-slate-500"> 51 <p className="mt-1 text-sm text-slate-500">
50 - Composable ERP framework for the printing industry 52 + {t('page.login.tagline')}
51 </p> 53 </p>
52 </div> 54 </div>
53 <div className="card p-6"> 55 <div className="card p-6">
54 <form onSubmit={onSubmit} className="space-y-4"> 56 <form onSubmit={onSubmit} className="space-y-4">
55 <div> 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 <input 59 <input
58 type="text" 60 type="text"
59 value={username} 61 value={username}
@@ -64,7 +66,7 @@ export function LoginPage() { @@ -64,7 +66,7 @@ export function LoginPage() {
64 /> 66 />
65 </div> 67 </div>
66 <div> 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 <input 70 <input
69 type="password" 71 type="password"
70 value={password} 72 value={password}
@@ -73,23 +75,23 @@ export function LoginPage() { @@ -73,23 +75,23 @@ export function LoginPage() {
73 required 75 required
74 /> 76 />
75 <p className="mt-1 text-xs text-slate-400"> 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 </p> 79 </p>
78 </div> 80 </div>
79 {error ? <ErrorBox error={error} /> : null} 81 {error ? <ErrorBox error={error} /> : null}
80 <button type="submit" className="btn-primary w-full" disabled={loading}> 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 </button> 84 </button>
83 </form> 85 </form>
84 </div> 86 </div>
85 <p className="mt-4 text-center text-xs text-slate-400"> 87 <p className="mt-4 text-center text-xs text-slate-400">
86 {info ? ( 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 <span className="font-mono">{info.implementationVersion}</span> 91 <span className="font-mono">{info.implementationVersion}</span>
90 </> 92 </>
91 ) : ( 93 ) : (
92 - 'Connecting…' 94 + t('page.login.connecting')
93 )} 95 )}
94 </p> 96 </p>
95 </div> 97 </div>
web/src/pages/MetadataAdminPage.tsx
@@ -363,8 +363,8 @@ export function MetadataAdminPage() { @@ -363,8 +363,8 @@ export function MetadataAdminPage() {
363 363
364 const entityCols: Column<MetadataEntity>[] = [ 364 const entityCols: Column<MetadataEntity>[] = [
365 { header: t('label.name'), key: 'name' }, 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 { header: t('label.description'), key: 'description', render: (r) => r.description ?? '—' }, 368 { header: t('label.description'), key: 'description', render: (r) => r.description ?? '—' },
369 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> }, 369 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
370 ] 370 ]
@@ -373,8 +373,8 @@ export function MetadataAdminPage() { @@ -373,8 +373,8 @@ export function MetadataAdminPage() {
373 { header: t('label.fieldKey'), key: 'key', render: (r) => <span className="font-mono">{r.key}</span> }, 373 { header: t('label.fieldKey'), key: 'key', render: (r) => <span className="font-mono">{r.key}</span> },
374 { header: t('label.targetEntity'), key: 'targetEntity' }, 374 { header: t('label.targetEntity'), key: 'targetEntity' },
375 { header: t('label.fieldType'), key: 'type', render: (r) => r.type.kind }, 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 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> }, 378 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
379 { 379 {
380 header: '', 380 header: '',
@@ -386,7 +386,7 @@ export function MetadataAdminPage() { @@ -386,7 +386,7 @@ export function MetadataAdminPage() {
386 className="text-xs text-blue-600 hover:underline" 386 className="text-xs text-blue-600 hover:underline"
387 onClick={() => openCfEdit(r)} 387 onClick={() => openCfEdit(r)}
388 > 388 >
389 - Edit 389 + {t('action.edit')}
390 </button> 390 </button>
391 <button 391 <button
392 className="text-xs text-rose-600 hover:underline" 392 className="text-xs text-rose-600 hover:underline"
@@ -400,17 +400,17 @@ export function MetadataAdminPage() { @@ -400,17 +400,17 @@ export function MetadataAdminPage() {
400 ] 400 ]
401 401
402 const permissionCols: Column<MetadataPermission>[] = [ 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 { header: t('label.description'), key: 'description' }, 404 { header: t('label.description'), key: 'description' },
405 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> }, 405 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
406 ] 406 ]
407 407
408 const menuCols: Column<MenuRow>[] = [ 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 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> }, 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,9 +428,9 @@ export function MetadataAdminPage() {
428 ), 428 ),
429 }, 429 },
430 { header: t('label.entity'), key: 'entityName' }, 430 { header: t('label.entity'), key: 'entityName' },
431 - { header: 'Title', key: 'title' }, 431 + { header: t('label.title'), key: 'title' },
432 { header: t('label.purpose'), key: 'purpose' }, 432 { header: t('label.purpose'), key: 'purpose' },
433 - { header: 'Version', key: 'version' }, 433 + { header: t('label.version'), key: 'version' },
434 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> }, 434 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
435 { 435 {
436 header: '', 436 header: '',
@@ -463,9 +463,9 @@ export function MetadataAdminPage() { @@ -463,9 +463,9 @@ export function MetadataAdminPage() {
463 ), 463 ),
464 }, 464 },
465 { header: t('label.entity'), key: 'entityName' }, 465 { header: t('label.entity'), key: 'entityName' },
466 - { header: 'Title', key: 'title' }, 466 + { header: t('label.title'), key: 'title' },
467 { header: t('label.pageSize'), key: 'pageSize' }, 467 { header: t('label.pageSize'), key: 'pageSize' },
468 - { header: 'Version', key: 'version' }, 468 + { header: t('label.version'), key: 'version' },
469 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> }, 469 { header: t('label.source'), key: 'source', render: (r) => <SourceBadge source={r.source ?? 'core'} /> },
470 { 470 {
471 header: '', 471 header: '',
@@ -521,7 +521,7 @@ export function MetadataAdminPage() { @@ -521,7 +521,7 @@ export function MetadataAdminPage() {
521 className="text-xs text-blue-600 hover:underline" 521 className="text-xs text-blue-600 hover:underline"
522 onClick={() => openRuleEdit(r)} 522 onClick={() => openRuleEdit(r)}
523 > 523 >
524 - Edit 524 + {t('action.edit')}
525 </button> 525 </button>
526 <button 526 <button
527 className="text-xs text-rose-600 hover:underline" 527 className="text-xs text-rose-600 hover:underline"
@@ -624,7 +624,7 @@ export function MetadataAdminPage() { @@ -624,7 +624,7 @@ export function MetadataAdminPage() {
624 {cfTypeKind === 'enum' && ( 624 {cfTypeKind === 'enum' && (
625 <div> 625 <div>
626 <label className="block text-xs font-medium text-slate-700"> 626 <label className="block text-xs font-medium text-slate-700">
627 - Allowed values (comma-separated) 627 + {t('label.allowedValues')}
628 </label> 628 </label>
629 <input 629 <input
630 type="text" 630 type="text"
@@ -638,7 +638,7 @@ export function MetadataAdminPage() { @@ -638,7 +638,7 @@ export function MetadataAdminPage() {
638 {cfTypeKind === 'string' && ( 638 {cfTypeKind === 'string' && (
639 <div> 639 <div>
640 <label className="block text-xs font-medium text-slate-700"> 640 <label className="block text-xs font-medium text-slate-700">
641 - Max length 641 + {t('label.maxLength')}
642 </label> 642 </label>
643 <input 643 <input
644 type="number" 644 type="number"
@@ -656,7 +656,7 @@ export function MetadataAdminPage() { @@ -656,7 +656,7 @@ export function MetadataAdminPage() {
656 checked={cfRequired} 656 checked={cfRequired}
657 onChange={(e) => setCfRequired(e.target.checked)} 657 onChange={(e) => setCfRequired(e.target.checked)}
658 /> 658 />
659 - Required 659 + {t('label.required')}
660 </label> 660 </label>
661 <label className="flex items-center gap-1.5 text-sm text-slate-700"> 661 <label className="flex items-center gap-1.5 text-sm text-slate-700">
662 <input 662 <input
@@ -664,14 +664,14 @@ export function MetadataAdminPage() { @@ -664,14 +664,14 @@ export function MetadataAdminPage() {
664 checked={cfPii} 664 checked={cfPii}
665 onChange={(e) => setCfPii(e.target.checked)} 665 onChange={(e) => setCfPii(e.target.checked)}
666 /> 666 />
667 - PII 667 + {t('label.pii')}
668 </label> 668 </label>
669 </div> 669 </div>
670 </div> 670 </div>
671 <div className="grid grid-cols-2 gap-3"> 671 <div className="grid grid-cols-2 gap-3">
672 <div> 672 <div>
673 <label className="block text-xs font-medium text-slate-700"> 673 <label className="block text-xs font-medium text-slate-700">
674 - Label EN 674 + {t('label.labelEn')}
675 </label> 675 </label>
676 <input 676 <input
677 type="text" 677 type="text"
@@ -683,7 +683,7 @@ export function MetadataAdminPage() { @@ -683,7 +683,7 @@ export function MetadataAdminPage() {
683 </div> 683 </div>
684 <div> 684 <div>
685 <label className="block text-xs font-medium text-slate-700"> 685 <label className="block text-xs font-medium text-slate-700">
686 - Label zh-CN 686 + {t('label.labelZhCn')}
687 </label> 687 </label>
688 <input 688 <input
689 type="text" 689 type="text"
@@ -745,7 +745,7 @@ export function MetadataAdminPage() { @@ -745,7 +745,7 @@ export function MetadataAdminPage() {
745 className="btn-primary" 745 className="btn-primary"
746 onClick={() => navigate('/admin/metadata/forms/new')} 746 onClick={() => navigate('/admin/metadata/forms/new')}
747 > 747 >
748 - + New Form 748 + {t('action.newForm')}
749 </button> 749 </button>
750 </div> 750 </div>
751 <DataTable 751 <DataTable
@@ -764,7 +764,7 @@ export function MetadataAdminPage() { @@ -764,7 +764,7 @@ export function MetadataAdminPage() {
764 className="btn-primary" 764 className="btn-primary"
765 onClick={() => navigate('/admin/metadata/list-views/new')} 765 onClick={() => navigate('/admin/metadata/list-views/new')}
766 > 766 >
767 - + New List View 767 + {t('action.newListView')}
768 </button> 768 </button>
769 </div> 769 </div>
770 <DataTable 770 <DataTable
@@ -915,7 +915,7 @@ export function MetadataAdminPage() { @@ -915,7 +915,7 @@ export function MetadataAdminPage() {
915 ]) 915 ])
916 } 916 }
917 > 917 >
918 - + Add Condition 918 + {t('action.addCondition')}
919 </button> 919 </button>
920 </div> 920 </div>
921 921
@@ -982,7 +982,7 @@ export function MetadataAdminPage() { @@ -982,7 +982,7 @@ export function MetadataAdminPage() {
982 ]) 982 ])
983 } 983 }
984 > 984 >
985 - + Add Action 985 + {t('action.addAction')}
986 </button> 986 </button>
987 </div> 987 </div>
988 988
@@ -1030,7 +1030,7 @@ export function MetadataAdminPage() { @@ -1030,7 +1030,7 @@ export function MetadataAdminPage() {
1030 <div> 1030 <div>
1031 <PageHeader 1031 <PageHeader
1032 title={t('page.metadataAdmin.title')} 1032 title={t('page.metadataAdmin.title')}
1033 - subtitle="Browse and manage metadata definitions" 1033 + subtitle={t('page.metadataAdmin.subtitle')}
1034 /> 1034 />
1035 1035
1036 {/* Tab bar */} 1036 {/* Tab bar */}
web/src/pages/MovementsPage.tsx
@@ -5,6 +5,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -5,6 +5,7 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
5 import { Loading } from '@/components/Loading' 5 import { Loading } from '@/components/Loading'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { DataTable, type Column } from '@/components/DataTable' 7 import { DataTable, type Column } from '@/components/DataTable'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 interface Row extends Record<string, unknown> { 10 interface Row extends Record<string, unknown> {
10 id: string 11 id: string
@@ -17,6 +18,7 @@ interface Row extends Record&lt;string, unknown&gt; { @@ -17,6 +18,7 @@ interface Row extends Record&lt;string, unknown&gt; {
17 } 18 }
18 19
19 export function MovementsPage() { 20 export function MovementsPage() {
  21 + const t = useT()
20 const [rows, setRows] = useState<Row[]>([]) 22 const [rows, setRows] = useState<Row[]>([])
21 const [error, setError] = useState<Error | null>(null) 23 const [error, setError] = useState<Error | null>(null)
22 const [loading, setLoading] = useState(true) 24 const [loading, setLoading] = useState(true)
@@ -46,15 +48,15 @@ export function MovementsPage() { @@ -46,15 +48,15 @@ export function MovementsPage() {
46 48
47 const columns: Column<Row>[] = [ 49 const columns: Column<Row>[] = [
48 { 50 {
49 - header: 'Occurred', 51 + header: t('label.occurred'),
50 key: 'occurredAt', 52 key: 'occurredAt',
51 render: (r) => 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 key: 'delta', 60 key: 'delta',
59 render: (r) => { 61 render: (r) => {
60 const n = Number(r.delta) 62 const n = Number(r.delta)
@@ -62,15 +64,15 @@ export function MovementsPage() { @@ -62,15 +64,15 @@ export function MovementsPage() {
62 return <span className={`font-mono tabular-nums ${cls}`}>{String(r.delta)}</span> 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 return ( 71 return (
70 <div> 72 <div>
71 <PageHeader 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 {loading && <Loading />} 77 {loading && <Loading />}
76 {error && <ErrorBox error={error} />} 78 {error && <ErrorBox error={error} />}
web/src/pages/PartnersPage.tsx
@@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DataTable, type Column } from '@/components/DataTable' 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10
10 export function PartnersPage() { 11 export function PartnersPage() {
  12 + const t = useT()
11 const [rows, setRows] = useState<Partner[]>([]) 13 const [rows, setRows] = useState<Partner[]>([])
12 const [error, setError] = useState<Error | null>(null) 14 const [error, setError] = useState<Error | null>(null)
13 const [loading, setLoading] = useState(true) 15 const [loading, setLoading] = useState(true)
@@ -22,29 +24,29 @@ export function PartnersPage() { @@ -22,29 +24,29 @@ export function PartnersPage() {
22 24
23 const columns: Column<Partner>[] = [ 25 const columns: Column<Partner>[] = [
24 { 26 {
25 - header: 'Code', 27 + header: t('label.code'),
26 key: 'code', 28 key: 'code',
27 render: (r) => ( 29 render: (r) => (
28 <Link to={`/partners/${r.id}/edit`} className="font-mono text-brand-600 hover:underline">{r.code}</Link> 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 key: 'active', 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 return ( 44 return (
43 <div> 45 <div>
44 <PageHeader 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 {loading && <Loading />} 51 {loading && <Loading />}
50 {error && <ErrorBox error={error} />} 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 // Confirm a DRAFT, receive a CONFIRMED into a warehouse, or cancel 2 // Confirm a DRAFT, receive a CONFIRMED into a warehouse, or cancel
3 // either. Each action surfaces the corresponding stock movement 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 // SETTLED on receive) inline. 5 // SETTLED on receive) inline.
6 6
7 import { useCallback, useEffect, useState } from 'react' 7 import { useCallback, useEffect, useState } from 'react'
@@ -12,10 +12,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -12,10 +12,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
12 import { Loading } from '@/components/Loading' 12 import { Loading } from '@/components/Loading'
13 import { ErrorBox } from '@/components/ErrorBox' 13 import { ErrorBox } from '@/components/ErrorBox'
14 import { StatusBadge } from '@/components/StatusBadge' 14 import { StatusBadge } from '@/components/StatusBadge'
  15 +import { useT } from '@/i18n/LocaleContext'
15 16
16 export function PurchaseOrderDetailPage() { 17 export function PurchaseOrderDetailPage() {
17 const { id = '' } = useParams<{ id: string }>() 18 const { id = '' } = useParams<{ id: string }>()
18 const navigate = useNavigate() 19 const navigate = useNavigate()
  20 + const t = useT()
19 const [order, setOrder] = useState<PurchaseOrder | null>(null) 21 const [order, setOrder] = useState<PurchaseOrder | null>(null)
20 const [locations, setLocations] = useState<Location[]>([]) 22 const [locations, setLocations] = useState<Location[]>([])
21 const [receivingLocation, setReceivingLocation] = useState<string>('') 23 const [receivingLocation, setReceivingLocation] = useState<string>('')
@@ -68,7 +70,7 @@ export function PurchaseOrderDetailPage() { @@ -68,7 +70,7 @@ export function PurchaseOrderDetailPage() {
68 const updated = await purchaseOrders.confirm(order.id) 70 const updated = await purchaseOrders.confirm(order.id)
69 setOrder(updated) 71 setOrder(updated)
70 await reloadSideEffects(updated.code) 72 await reloadSideEffects(updated.code)
71 - setActionMessage('Confirmed. pbc-finance has posted an AP journal entry.') 73 + setActionMessage(t('page.purchaseOrderDetail.confirmMsg'))
72 } catch (e: unknown) { 74 } catch (e: unknown) {
73 setError(e instanceof Error ? e : new Error(String(e))) 75 setError(e instanceof Error ? e : new Error(String(e)))
74 } finally { 76 } finally {
@@ -78,7 +80,7 @@ export function PurchaseOrderDetailPage() { @@ -78,7 +80,7 @@ export function PurchaseOrderDetailPage() {
78 80
79 const onReceive = async () => { 81 const onReceive = async () => {
80 if (!receivingLocation) { 82 if (!receivingLocation) {
81 - setError(new Error('Pick a receiving location first.')) 83 + setError(new Error(t('page.purchaseOrderDetail.pickLocation')))
82 return 84 return
83 } 85 }
84 setActing(true) 86 setActing(true)
@@ -89,7 +91,7 @@ export function PurchaseOrderDetailPage() { @@ -89,7 +91,7 @@ export function PurchaseOrderDetailPage() {
89 setOrder(updated) 91 setOrder(updated)
90 await reloadSideEffects(updated.code) 92 await reloadSideEffects(updated.code)
91 setActionMessage( 93 setActionMessage(
92 - `Received into ${receivingLocation}. Stock credited, journal entry settled.`, 94 + t('page.purchaseOrderDetail.receiveMsg').replace('{location}', receivingLocation),
93 ) 95 )
94 } catch (e: unknown) { 96 } catch (e: unknown) {
95 setError(e instanceof Error ? e : new Error(String(e))) 97 setError(e instanceof Error ? e : new Error(String(e)))
@@ -106,7 +108,7 @@ export function PurchaseOrderDetailPage() { @@ -106,7 +108,7 @@ export function PurchaseOrderDetailPage() {
106 const updated = await purchaseOrders.cancel(order.id) 108 const updated = await purchaseOrders.cancel(order.id)
107 setOrder(updated) 109 setOrder(updated)
108 await reloadSideEffects(updated.code) 110 await reloadSideEffects(updated.code)
109 - setActionMessage('Cancelled. pbc-finance has reversed any open journal entry.') 111 + setActionMessage(t('page.purchaseOrderDetail.cancelMsg'))
110 } catch (e: unknown) { 112 } catch (e: unknown) {
111 setError(e instanceof Error ? e : new Error(String(e))) 113 setError(e instanceof Error ? e : new Error(String(e)))
112 } finally { 114 } finally {
@@ -121,11 +123,11 @@ export function PurchaseOrderDetailPage() { @@ -121,11 +123,11 @@ export function PurchaseOrderDetailPage() {
121 return ( 123 return (
122 <div> 124 <div>
123 <PageHeader 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 actions={ 128 actions={
127 <button className="btn-secondary" onClick={() => navigate('/purchase-orders')}> 129 <button className="btn-secondary" onClick={() => navigate('/purchase-orders')}>
128 - ← Back 130 + {t('action.back')}
129 </button> 131 </button>
130 } 132 }
131 /> 133 />
@@ -133,11 +135,11 @@ export function PurchaseOrderDetailPage() { @@ -133,11 +135,11 @@ export function PurchaseOrderDetailPage() {
133 <div className="card mb-6 p-5"> 135 <div className="card mb-6 p-5">
134 <div className="flex flex-wrap items-center justify-between gap-4"> 136 <div className="flex flex-wrap items-center justify-between gap-4">
135 <div className="flex items-center gap-3"> 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 <StatusBadge status={order.status} /> 139 <StatusBadge status={order.status} />
138 </div> 140 </div>
139 <div className="text-right"> 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 <div className="font-mono text-xl font-semibold"> 143 <div className="font-mono text-xl font-semibold">
142 {Number(order.totalAmount).toLocaleString(undefined, { 144 {Number(order.totalAmount).toLocaleString(undefined, {
143 minimumFractionDigits: 2, 145 minimumFractionDigits: 2,
@@ -154,10 +156,10 @@ export function PurchaseOrderDetailPage() { @@ -154,10 +156,10 @@ export function PurchaseOrderDetailPage() {
154 </div> 156 </div>
155 157
156 <div className="card mb-6 p-5"> 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 <div className="flex flex-wrap items-center gap-3"> 160 <div className="flex flex-wrap items-center gap-3">
159 <button className="btn-primary" disabled={!canConfirm || acting} onClick={onConfirm}> 161 <button className="btn-primary" disabled={!canConfirm || acting} onClick={onConfirm}>
160 - Confirm 162 + {t('action.confirm')}
161 </button> 163 </button>
162 <div className="flex items-center gap-2"> 164 <div className="flex items-center gap-2">
163 <select 165 <select
@@ -173,27 +175,27 @@ export function PurchaseOrderDetailPage() { @@ -173,27 +175,27 @@ export function PurchaseOrderDetailPage() {
173 ))} 175 ))}
174 </select> 176 </select>
175 <button className="btn-primary" disabled={!canReceive || acting} onClick={onReceive}> 177 <button className="btn-primary" disabled={!canReceive || acting} onClick={onReceive}>
176 - Receive 178 + {t('action.receive')}
177 </button> 179 </button>
178 </div> 180 </div>
179 <button className="btn-danger" disabled={!canCancel || acting} onClick={onCancel}> 181 <button className="btn-danger" disabled={!canCancel || acting} onClick={onCancel}>
180 - Cancel 182 + {t('action.cancel')}
181 </button> 183 </button>
182 </div> 184 </div>
183 </div> 185 </div>
184 186
185 <div className="card mb-6"> 187 <div className="card mb-6">
186 <div className="border-b border-slate-200 px-5 py-3"> 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 </div> 190 </div>
189 <table className="table-base"> 191 <table className="table-base">
190 <thead className="bg-slate-50"> 192 <thead className="bg-slate-50">
191 <tr> 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 </tr> 199 </tr>
198 </thead> 200 </thead>
199 <tbody className="divide-y divide-slate-100"> 201 <tbody className="divide-y divide-slate-100">
@@ -222,18 +224,18 @@ export function PurchaseOrderDetailPage() { @@ -222,18 +224,18 @@ export function PurchaseOrderDetailPage() {
222 <div className="grid gap-6 md:grid-cols-2"> 224 <div className="grid gap-6 md:grid-cols-2">
223 <div className="card"> 225 <div className="card">
224 <div className="border-b border-slate-200 px-5 py-3"> 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 </div> 228 </div>
227 {movements.length === 0 ? ( 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 <table className="table-base"> 232 <table className="table-base">
231 <thead className="bg-slate-50"> 233 <thead className="bg-slate-50">
232 <tr> 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 </tr> 239 </tr>
238 </thead> 240 </thead>
239 <tbody className="divide-y divide-slate-100"> 241 <tbody className="divide-y divide-slate-100">
@@ -242,7 +244,7 @@ export function PurchaseOrderDetailPage() { @@ -242,7 +244,7 @@ export function PurchaseOrderDetailPage() {
242 <td className="font-mono">{m.itemCode}</td> 244 <td className="font-mono">{m.itemCode}</td>
243 <td className="font-mono tabular-nums text-emerald-600">{String(m.delta)}</td> 245 <td className="font-mono tabular-nums text-emerald-600">{String(m.delta)}</td>
244 <td>{m.reason}</td> 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 </tr> 248 </tr>
247 ))} 249 ))}
248 </tbody> 250 </tbody>
@@ -251,18 +253,18 @@ export function PurchaseOrderDetailPage() { @@ -251,18 +253,18 @@ export function PurchaseOrderDetailPage() {
251 </div> 253 </div>
252 <div className="card"> 254 <div className="card">
253 <div className="border-b border-slate-200 px-5 py-3"> 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 </div> 257 </div>
256 {journalEntries.length === 0 ? ( 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 <table className="table-base"> 261 <table className="table-base">
260 <thead className="bg-slate-50"> 262 <thead className="bg-slate-50">
261 <tr> 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 </tr> 268 </tr>
267 </thead> 269 </thead>
268 <tbody className="divide-y divide-slate-100"> 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,8 +7,10 @@ import { Loading } from &#39;@/components/Loading&#39;
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DataTable, type Column } from '@/components/DataTable' 8 import { DataTable, type Column } from '@/components/DataTable'
9 import { StatusBadge } from '@/components/StatusBadge' 9 import { StatusBadge } from '@/components/StatusBadge'
  10 +import { useT } from '@/i18n/LocaleContext'
10 11
11 export function PurchaseOrdersPage() { 12 export function PurchaseOrdersPage() {
  13 + const t = useT()
12 const [rows, setRows] = useState<PurchaseOrder[]>([]) 14 const [rows, setRows] = useState<PurchaseOrder[]>([])
13 const [error, setError] = useState<Error | null>(null) 15 const [error, setError] = useState<Error | null>(null)
14 const [loading, setLoading] = useState(true) 16 const [loading, setLoading] = useState(true)
@@ -23,7 +25,7 @@ export function PurchaseOrdersPage() { @@ -23,7 +25,7 @@ export function PurchaseOrdersPage() {
23 25
24 const columns: Column<PurchaseOrder>[] = [ 26 const columns: Column<PurchaseOrder>[] = [
25 { 27 {
26 - header: 'Code', 28 + header: t('label.code'),
27 key: 'code', 29 key: 'code',
28 render: (r) => ( 30 render: (r) => (
29 <Link to={`/purchase-orders/${r.id}`} className="font-mono text-brand-600 hover:underline"> 31 <Link to={`/purchase-orders/${r.id}`} className="font-mono text-brand-600 hover:underline">
@@ -31,12 +33,12 @@ export function PurchaseOrdersPage() { @@ -31,12 +33,12 @@ export function PurchaseOrdersPage() {
31 </Link> 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 key: 'totalAmount', 42 key: 'totalAmount',
41 render: (r) => ( 43 render: (r) => (
42 <span className="font-mono tabular-nums"> 44 <span className="font-mono tabular-nums">
@@ -52,9 +54,9 @@ export function PurchaseOrdersPage() { @@ -52,9 +54,9 @@ export function PurchaseOrdersPage() {
52 return ( 54 return (
53 <div> 55 <div>
54 <PageHeader 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 {loading && <Loading />} 61 {loading && <Loading />}
60 {error && <ErrorBox error={error} />} 62 {error && <ErrorBox error={error} />}
web/src/pages/RolesPage.tsx
@@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
5 import { Loading } from '@/components/Loading' 5 import { Loading } from '@/components/Loading'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { DataTable, type Column } from '@/components/DataTable' 7 import { DataTable, type Column } from '@/components/DataTable'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 export function RolesPage() { 10 export function RolesPage() {
  11 + const t = useT()
10 const [rows, setRows] = useState<Role[]>([]) 12 const [rows, setRows] = useState<Role[]>([])
11 const [error, setError] = useState<Error | null>(null) 13 const [error, setError] = useState<Error | null>(null)
12 const [loading, setLoading] = useState(true) 14 const [loading, setLoading] = useState(true)
@@ -43,36 +45,36 @@ export function RolesPage() { @@ -43,36 +45,36 @@ export function RolesPage() {
43 } 45 }
44 46
45 const columns: Column<Role>[] = [ 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 return ( 53 return (
52 <div> 54 <div>
53 <PageHeader 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 actions={ 58 actions={
57 <button className="btn-primary" onClick={() => setShowCreate(!showCreate)}> 59 <button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
58 - {showCreate ? 'Cancel' : '+ New Role'} 60 + {showCreate ? t('action.cancel') : t('action.newRole')}
59 </button> 61 </button>
60 } 62 }
61 /> 63 />
62 {showCreate && ( 64 {showCreate && (
63 <form onSubmit={onCreate} className="card p-4 mb-4 max-w-lg flex items-end gap-3"> 65 <form onSubmit={onCreate} className="card p-4 mb-4 max-w-lg flex items-end gap-3">
64 <div className="flex-1"> 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 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)} 68 <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
67 placeholder="sales-clerk" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" /> 69 placeholder="sales-clerk" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
68 </div> 70 </div>
69 <div className="flex-1"> 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 <input type="text" required value={name} onChange={(e) => setName(e.target.value)} 73 <input type="text" required value={name} onChange={(e) => setName(e.target.value)}
72 placeholder="Sales Clerk" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" /> 74 placeholder="Sales Clerk" className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
73 </div> 75 </div>
74 <button type="submit" className="btn-primary" disabled={creating}> 76 <button type="submit" className="btn-primary" disabled={creating}>
75 - {creating ? '...' : 'Create'} 77 + {creating ? '...' : t('action.create')}
76 </button> 78 </button>
77 </form> 79 </form>
78 )} 80 )}
web/src/pages/SalesOrderDetailPage.tsx
@@ -3,11 +3,11 @@ @@ -3,11 +3,11 @@
3 // **The headline demo flow.** Confirm a DRAFT, ship a CONFIRMED, 3 // **The headline demo flow.** Confirm a DRAFT, ship a CONFIRMED,
4 // or cancel either. Each action updates the order in place, 4 // or cancel either. Each action updates the order in place,
5 // reloads the journal entry list to show the AR row appearing 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 // SALES_SHIPMENT ledger entry appearing. 7 // SALES_SHIPMENT ledger entry appearing.
8 // 8 //
9 // **Why ship asks for a location.** The framework requires every 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 // inventory ledger tags the row with that location and the audit 11 // inventory ledger tags the row with that location and the audit
12 // trail must always answer "which warehouse shipped this". The UI 12 // trail must always answer "which warehouse shipped this". The UI
13 // proposes the first WAREHOUSE-typed location it finds; an 13 // proposes the first WAREHOUSE-typed location it finds; an
@@ -21,10 +21,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -21,10 +21,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
21 import { Loading } from '@/components/Loading' 21 import { Loading } from '@/components/Loading'
22 import { ErrorBox } from '@/components/ErrorBox' 22 import { ErrorBox } from '@/components/ErrorBox'
23 import { StatusBadge } from '@/components/StatusBadge' 23 import { StatusBadge } from '@/components/StatusBadge'
  24 +import { useT } from '@/i18n/LocaleContext'
24 25
25 export function SalesOrderDetailPage() { 26 export function SalesOrderDetailPage() {
26 const { id = '' } = useParams<{ id: string }>() 27 const { id = '' } = useParams<{ id: string }>()
27 const navigate = useNavigate() 28 const navigate = useNavigate()
  29 + const t = useT()
28 const [order, setOrder] = useState<SalesOrder | null>(null) 30 const [order, setOrder] = useState<SalesOrder | null>(null)
29 const [locations, setLocations] = useState<Location[]>([]) 31 const [locations, setLocations] = useState<Location[]>([])
30 const [shippingLocation, setShippingLocation] = useState<string>('') 32 const [shippingLocation, setShippingLocation] = useState<string>('')
@@ -77,7 +79,7 @@ export function SalesOrderDetailPage() { @@ -77,7 +79,7 @@ export function SalesOrderDetailPage() {
77 const updated = await salesOrders.confirm(order.id) 79 const updated = await salesOrders.confirm(order.id)
78 setOrder(updated) 80 setOrder(updated)
79 await reloadSideEffects(updated.code) 81 await reloadSideEffects(updated.code)
80 - setActionMessage('Confirmed. pbc-finance has posted an AR journal entry.') 82 + setActionMessage(t('page.salesOrderDetail.confirmMsg'))
81 } catch (e: unknown) { 83 } catch (e: unknown) {
82 setError(e instanceof Error ? e : new Error(String(e))) 84 setError(e instanceof Error ? e : new Error(String(e)))
83 } finally { 85 } finally {
@@ -87,7 +89,7 @@ export function SalesOrderDetailPage() { @@ -87,7 +89,7 @@ export function SalesOrderDetailPage() {
87 89
88 const onShip = async () => { 90 const onShip = async () => {
89 if (!shippingLocation) { 91 if (!shippingLocation) {
90 - setError(new Error('Pick a shipping location first.')) 92 + setError(new Error(t('page.salesOrderDetail.pickLocation')))
91 return 93 return
92 } 94 }
93 setActing(true) 95 setActing(true)
@@ -98,7 +100,7 @@ export function SalesOrderDetailPage() { @@ -98,7 +100,7 @@ export function SalesOrderDetailPage() {
98 setOrder(updated) 100 setOrder(updated)
99 await reloadSideEffects(updated.code) 101 await reloadSideEffects(updated.code)
100 setActionMessage( 102 setActionMessage(
101 - `Shipped from ${shippingLocation}. Stock debited, journal entry settled.`, 103 + t('page.salesOrderDetail.shipMsg').replace('{location}', shippingLocation),
102 ) 104 )
103 } catch (e: unknown) { 105 } catch (e: unknown) {
104 setError(e instanceof Error ? e : new Error(String(e))) 106 setError(e instanceof Error ? e : new Error(String(e)))
@@ -115,7 +117,7 @@ export function SalesOrderDetailPage() { @@ -115,7 +117,7 @@ export function SalesOrderDetailPage() {
115 const updated = await salesOrders.cancel(order.id) 117 const updated = await salesOrders.cancel(order.id)
116 setOrder(updated) 118 setOrder(updated)
117 await reloadSideEffects(updated.code) 119 await reloadSideEffects(updated.code)
118 - setActionMessage('Cancelled. pbc-finance has reversed any open journal entry.') 120 + setActionMessage(t('page.salesOrderDetail.cancelMsg'))
119 } catch (e: unknown) { 121 } catch (e: unknown) {
120 setError(e instanceof Error ? e : new Error(String(e))) 122 setError(e instanceof Error ? e : new Error(String(e)))
121 } finally { 123 } finally {
@@ -130,11 +132,11 @@ export function SalesOrderDetailPage() { @@ -130,11 +132,11 @@ export function SalesOrderDetailPage() {
130 return ( 132 return (
131 <div> 133 <div>
132 <PageHeader 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 actions={ 137 actions={
136 <button className="btn-secondary" onClick={() => navigate('/sales-orders')}> 138 <button className="btn-secondary" onClick={() => navigate('/sales-orders')}>
137 - ← Back 139 + {t('action.back')}
138 </button> 140 </button>
139 } 141 }
140 /> 142 />
@@ -142,11 +144,11 @@ export function SalesOrderDetailPage() { @@ -142,11 +144,11 @@ export function SalesOrderDetailPage() {
142 <div className="card mb-6 p-5"> 144 <div className="card mb-6 p-5">
143 <div className="flex flex-wrap items-center justify-between gap-4"> 145 <div className="flex flex-wrap items-center justify-between gap-4">
144 <div className="flex items-center gap-3"> 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 <StatusBadge status={order.status} /> 148 <StatusBadge status={order.status} />
147 </div> 149 </div>
148 <div className="text-right"> 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 <div className="font-mono text-xl font-semibold"> 152 <div className="font-mono text-xl font-semibold">
151 {Number(order.totalAmount).toLocaleString(undefined, { 153 {Number(order.totalAmount).toLocaleString(undefined, {
152 minimumFractionDigits: 2, 154 minimumFractionDigits: 2,
@@ -164,10 +166,10 @@ export function SalesOrderDetailPage() { @@ -164,10 +166,10 @@ export function SalesOrderDetailPage() {
164 </div> 166 </div>
165 167
166 <div className="card mb-6 p-5"> 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 <div className="flex flex-wrap items-center gap-3"> 170 <div className="flex flex-wrap items-center gap-3">
169 <button className="btn-primary" disabled={!canConfirm || acting} onClick={onConfirm}> 171 <button className="btn-primary" disabled={!canConfirm || acting} onClick={onConfirm}>
170 - Confirm 172 + {t('action.confirm')}
171 </button> 173 </button>
172 <div className="flex items-center gap-2"> 174 <div className="flex items-center gap-2">
173 <select 175 <select
@@ -183,27 +185,27 @@ export function SalesOrderDetailPage() { @@ -183,27 +185,27 @@ export function SalesOrderDetailPage() {
183 ))} 185 ))}
184 </select> 186 </select>
185 <button className="btn-primary" disabled={!canShip || acting} onClick={onShip}> 187 <button className="btn-primary" disabled={!canShip || acting} onClick={onShip}>
186 - Ship 188 + {t('action.ship')}
187 </button> 189 </button>
188 </div> 190 </div>
189 <button className="btn-danger" disabled={!canCancel || acting} onClick={onCancel}> 191 <button className="btn-danger" disabled={!canCancel || acting} onClick={onCancel}>
190 - Cancel 192 + {t('action.cancel')}
191 </button> 193 </button>
192 </div> 194 </div>
193 </div> 195 </div>
194 196
195 <div className="card mb-6"> 197 <div className="card mb-6">
196 <div className="border-b border-slate-200 px-5 py-3"> 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 </div> 200 </div>
199 <table className="table-base"> 201 <table className="table-base">
200 <thead className="bg-slate-50"> 202 <thead className="bg-slate-50">
201 <tr> 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 </tr> 209 </tr>
208 </thead> 210 </thead>
209 <tbody className="divide-y divide-slate-100"> 211 <tbody className="divide-y divide-slate-100">
@@ -232,18 +234,18 @@ export function SalesOrderDetailPage() { @@ -232,18 +234,18 @@ export function SalesOrderDetailPage() {
232 <div className="grid gap-6 md:grid-cols-2"> 234 <div className="grid gap-6 md:grid-cols-2">
233 <div className="card"> 235 <div className="card">
234 <div className="border-b border-slate-200 px-5 py-3"> 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 </div> 238 </div>
237 {movements.length === 0 ? ( 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 <table className="table-base"> 242 <table className="table-base">
241 <thead className="bg-slate-50"> 243 <thead className="bg-slate-50">
242 <tr> 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 </tr> 249 </tr>
248 </thead> 250 </thead>
249 <tbody className="divide-y divide-slate-100"> 251 <tbody className="divide-y divide-slate-100">
@@ -252,7 +254,7 @@ export function SalesOrderDetailPage() { @@ -252,7 +254,7 @@ export function SalesOrderDetailPage() {
252 <td className="font-mono">{m.itemCode}</td> 254 <td className="font-mono">{m.itemCode}</td>
253 <td className="font-mono tabular-nums text-rose-600">{String(m.delta)}</td> 255 <td className="font-mono tabular-nums text-rose-600">{String(m.delta)}</td>
254 <td>{m.reason}</td> 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 </tr> 258 </tr>
257 ))} 259 ))}
258 </tbody> 260 </tbody>
@@ -261,18 +263,18 @@ export function SalesOrderDetailPage() { @@ -261,18 +263,18 @@ export function SalesOrderDetailPage() {
261 </div> 263 </div>
262 <div className="card"> 264 <div className="card">
263 <div className="border-b border-slate-200 px-5 py-3"> 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 </div> 267 </div>
266 {journalEntries.length === 0 ? ( 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 <table className="table-base"> 271 <table className="table-base">
270 <thead className="bg-slate-50"> 272 <thead className="bg-slate-50">
271 <tr> 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 </tr> 278 </tr>
277 </thead> 279 </thead>
278 <tbody className="divide-y divide-slate-100"> 280 <tbody className="divide-y divide-slate-100">
@@ -293,7 +295,7 @@ export function SalesOrderDetailPage() { @@ -293,7 +295,7 @@ export function SalesOrderDetailPage() {
293 </table> 295 </table>
294 )} 296 )}
295 <div className="border-t border-slate-200 px-5 py-3 text-xs text-slate-400"> 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 </div> 299 </div>
298 </div> 300 </div>
299 </div> 301 </div>
web/src/pages/SalesOrdersPage.tsx
@@ -7,8 +7,10 @@ import { Loading } from &#39;@/components/Loading&#39; @@ -7,8 +7,10 @@ import { Loading } from &#39;@/components/Loading&#39;
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DataTable, type Column } from '@/components/DataTable' 8 import { DataTable, type Column } from '@/components/DataTable'
9 import { StatusBadge } from '@/components/StatusBadge' 9 import { StatusBadge } from '@/components/StatusBadge'
  10 +import { useT } from '@/i18n/LocaleContext'
10 11
11 export function SalesOrdersPage() { 12 export function SalesOrdersPage() {
  13 + const t = useT()
12 const [rows, setRows] = useState<SalesOrder[]>([]) 14 const [rows, setRows] = useState<SalesOrder[]>([])
13 const [error, setError] = useState<Error | null>(null) 15 const [error, setError] = useState<Error | null>(null)
14 const [loading, setLoading] = useState(true) 16 const [loading, setLoading] = useState(true)
@@ -23,7 +25,7 @@ export function SalesOrdersPage() { @@ -23,7 +25,7 @@ export function SalesOrdersPage() {
23 25
24 const columns: Column<SalesOrder>[] = [ 26 const columns: Column<SalesOrder>[] = [
25 { 27 {
26 - header: 'Code', 28 + header: t('label.code'),
27 key: 'code', 29 key: 'code',
28 render: (r) => ( 30 render: (r) => (
29 <Link to={`/sales-orders/${r.id}`} className="font-mono text-brand-600 hover:underline"> 31 <Link to={`/sales-orders/${r.id}`} className="font-mono text-brand-600 hover:underline">
@@ -31,11 +33,11 @@ export function SalesOrdersPage() { @@ -31,11 +33,11 @@ export function SalesOrdersPage() {
31 </Link> 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 key: 'totalAmount', 41 key: 'totalAmount',
40 render: (r) => ( 42 render: (r) => (
41 <span className="font-mono tabular-nums"> 43 <span className="font-mono tabular-nums">
@@ -48,7 +50,7 @@ export function SalesOrdersPage() { @@ -48,7 +50,7 @@ export function SalesOrdersPage() {
48 ), 50 ),
49 }, 51 },
50 { 52 {
51 - header: 'Lines', 53 + header: t('label.lines'),
52 key: 'lines', 54 key: 'lines',
53 render: (r) => <span className="text-slate-500">{r.lines.length}</span>, 55 render: (r) => <span className="text-slate-500">{r.lines.length}</span>,
54 }, 56 },
@@ -57,10 +59,10 @@ export function SalesOrdersPage() { @@ -57,10 +59,10 @@ export function SalesOrdersPage() {
57 return ( 59 return (
58 <div> 60 <div>
59 <PageHeader 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 actions={ 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 {loading && <Loading />} 68 {loading && <Loading />}
web/src/pages/ShopFloorPage.tsx
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 // Polls /api/v1/production/work-orders/shop-floor every 5s and 3 // Polls /api/v1/production/work-orders/shop-floor every 5s and
4 // renders one card per IN_PROGRESS work order with its current 4 // renders one card per IN_PROGRESS work order with its current
5 // operation, planned vs actual minutes, and operations completed. 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 // are large, the typography is high-contrast, and the only state 7 // are large, the typography is high-contrast, and the only state
8 // is "what's running right now". 8 // is "what's running right now".
9 9
@@ -15,10 +15,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -15,10 +15,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
15 import { Loading } from '@/components/Loading' 15 import { Loading } from '@/components/Loading'
16 import { ErrorBox } from '@/components/ErrorBox' 16 import { ErrorBox } from '@/components/ErrorBox'
17 import { StatusBadge } from '@/components/StatusBadge' 17 import { StatusBadge } from '@/components/StatusBadge'
  18 +import { useT } from '@/i18n/LocaleContext'
18 19
19 const POLL_MS = 5000 20 const POLL_MS = 5000
20 21
21 export function ShopFloorPage() { 22 export function ShopFloorPage() {
  23 + const t = useT()
22 const [rows, setRows] = useState<ShopFloorEntry[]>([]) 24 const [rows, setRows] = useState<ShopFloorEntry[]>([])
23 const [error, setError] = useState<Error | null>(null) 25 const [error, setError] = useState<Error | null>(null)
24 const [loading, setLoading] = useState(true) 26 const [loading, setLoading] = useState(true)
@@ -52,16 +54,16 @@ export function ShopFloorPage() { @@ -52,16 +54,16 @@ export function ShopFloorPage() {
52 return ( 54 return (
53 <div> 55 <div>
54 <PageHeader 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 {loading && <Loading />} 62 {loading && <Loading />}
61 {error && <ErrorBox error={error} />} 63 {error && <ErrorBox error={error} />}
62 {!loading && rows.length === 0 && ( 64 {!loading && rows.length === 0 && (
63 <div className="card p-8 text-center text-sm text-slate-400"> 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 </div> 67 </div>
66 )} 68 )}
67 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> 69 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -80,15 +82,15 @@ export function ShopFloorPage() { @@ -80,15 +82,15 @@ export function ShopFloorPage() {
80 {r.workOrderCode} 82 {r.workOrderCode}
81 </span> 83 </span>
82 <span className="text-xs text-slate-500"> 84 <span className="text-xs text-slate-500">
83 - {r.operationsCompleted} / {r.operationsTotal} ops 85 + {r.operationsCompleted} / {r.operationsTotal} {t('page.shopFloor.ops')}
84 </span> 86 </span>
85 </div> 87 </div>
86 <div className="mb-3 text-xs text-slate-500"> 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 {String(r.outputQuantity)} 90 {String(r.outputQuantity)}
89 </div> 91 </div>
90 <div className="mb-3"> 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 {r.currentOperationCode ? ( 94 {r.currentOperationCode ? (
93 <div className="mt-1 flex items-center gap-2"> 95 <div className="mt-1 flex items-center gap-2">
94 <span className="font-mono font-medium">{r.currentOperationCode}</span> 96 <span className="font-mono font-medium">{r.currentOperationCode}</span>
@@ -96,13 +98,13 @@ export function ShopFloorPage() { @@ -96,13 +98,13 @@ export function ShopFloorPage() {
96 {r.currentOperationStatus && <StatusBadge status={r.currentOperationStatus} />} 98 {r.currentOperationStatus && <StatusBadge status={r.currentOperationStatus} />}
97 </div> 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 </div> 103 </div>
102 <div className="mt-2"> 104 <div className="mt-2">
103 <div className="mb-1 flex items-center justify-between text-xs text-slate-500"> 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 </div> 108 </div>
107 <div className="h-2 overflow-hidden rounded-full bg-slate-100"> 109 <div className="h-2 overflow-hidden rounded-full bg-slate-100">
108 <div 110 <div
web/src/pages/TaskDetailPage.tsx
@@ -77,15 +77,15 @@ export function TaskDetailPage() { @@ -77,15 +77,15 @@ export function TaskDetailPage() {
77 77
78 <div className="card p-4 space-y-4"> 78 <div className="card p-4 space-y-4">
79 <dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm max-w-lg"> 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 <dd className="font-mono">{task.taskId}</dd> 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 <dd>{task.processDefinitionKey}</dd> 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 <dd>{task.createTime}</dd> 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 <dd>{task.assignee ?? '\u2014'}</dd> 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 <dd className="font-mono">{task.formKey ?? '\u2014'}</dd> 89 <dd className="font-mono">{task.formKey ?? '\u2014'}</dd>
90 </dl> 90 </dl>
91 91
@@ -100,7 +100,7 @@ export function TaskDetailPage() { @@ -100,7 +100,7 @@ export function TaskDetailPage() {
100 ) : ( 100 ) : (
101 <div className="space-y-4"> 101 <div className="space-y-4">
102 <div> 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 <pre className="rounded-md bg-slate-50 border border-slate-200 p-3 text-xs overflow-x-auto max-h-64"> 104 <pre className="rounded-md bg-slate-50 border border-slate-200 p-3 text-xs overflow-x-auto max-h-64">
105 {JSON.stringify(task.variables, null, 2)} 105 {JSON.stringify(task.variables, null, 2)}
106 </pre> 106 </pre>
web/src/pages/UomsPage.tsx
@@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -5,8 +5,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
5 import { Loading } from '@/components/Loading' 5 import { Loading } from '@/components/Loading'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
7 import { DataTable, type Column } from '@/components/DataTable' 7 import { DataTable, type Column } from '@/components/DataTable'
  8 +import { useT } from '@/i18n/LocaleContext'
8 9
9 export function UomsPage() { 10 export function UomsPage() {
  11 + const t = useT()
10 const [rows, setRows] = useState<Uom[]>([]) 12 const [rows, setRows] = useState<Uom[]>([])
11 const [error, setError] = useState<Error | null>(null) 13 const [error, setError] = useState<Error | null>(null)
12 const [loading, setLoading] = useState(true) 14 const [loading, setLoading] = useState(true)
@@ -20,16 +22,16 @@ export function UomsPage() { @@ -20,16 +22,16 @@ export function UomsPage() {
20 }, []) 22 }, [])
21 23
22 const columns: Column<Uom>[] = [ 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 return ( 30 return (
29 <div> 31 <div>
30 <PageHeader 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 {loading && <Loading />} 36 {loading && <Loading />}
35 {error && <ErrorBox error={error} />} 37 {error && <ErrorBox error={error} />}
web/src/pages/UserDetailPage.tsx
@@ -11,10 +11,12 @@ import type { Role, User } from &#39;@/types/api&#39; @@ -11,10 +11,12 @@ import type { Role, User } from &#39;@/types/api&#39;
11 import { PageHeader } from '@/components/PageHeader' 11 import { PageHeader } from '@/components/PageHeader'
12 import { Loading } from '@/components/Loading' 12 import { Loading } from '@/components/Loading'
13 import { ErrorBox } from '@/components/ErrorBox' 13 import { ErrorBox } from '@/components/ErrorBox'
  14 +import { useT } from '@/i18n/LocaleContext'
14 15
15 export function UserDetailPage() { 16 export function UserDetailPage() {
16 const { id = '' } = useParams<{ id: string }>() 17 const { id = '' } = useParams<{ id: string }>()
17 const navigate = useNavigate() 18 const navigate = useNavigate()
  19 + const t = useT()
18 const [user, setUser] = useState<User | null>(null) 20 const [user, setUser] = useState<User | null>(null)
19 const [allRoles, setAllRoles] = useState<Role[]>([]) 21 const [allRoles, setAllRoles] = useState<Role[]>([])
20 const [userRoleCodes, setUserRoleCodes] = useState<string[]>([]) 22 const [userRoleCodes, setUserRoleCodes] = useState<string[]>([])
@@ -64,10 +66,10 @@ export function UserDetailPage() { @@ -64,10 +66,10 @@ export function UserDetailPage() {
64 <div> 66 <div>
65 <PageHeader 67 <PageHeader
66 title={user.displayName} 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 actions={ 70 actions={
69 <button className="btn-secondary" onClick={() => navigate('/users')}> 71 <button className="btn-secondary" onClick={() => navigate('/users')}>
70 - ← Back 72 + {t('action.back')}
71 </button> 73 </button>
72 } 74 }
73 /> 75 />
@@ -75,12 +77,12 @@ export function UserDetailPage() { @@ -75,12 +77,12 @@ export function UserDetailPage() {
75 {error && <ErrorBox error={error} />} 77 {error && <ErrorBox error={error} />}
76 78
77 <div className="card p-5 max-w-lg"> 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 <p className="mb-4 text-xs text-slate-400"> 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 </p> 83 </p>
82 {allRoles.length === 0 && ( 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 <div className="space-y-2"> 87 <div className="space-y-2">
86 {allRoles.map((role) => { 88 {allRoles.map((role) => {
@@ -99,7 +101,7 @@ export function UserDetailPage() { @@ -99,7 +101,7 @@ export function UserDetailPage() {
99 disabled={acting} 101 disabled={acting}
100 onClick={() => toggle(role.code, has)} 102 onClick={() => toggle(role.code, has)}
101 > 103 >
102 - {has ? 'Revoke' : 'Assign'} 104 + {has ? t('action.revoke') : t('action.assign')}
103 </button> 105 </button>
104 </div> 106 </div>
105 ) 107 )
web/src/pages/UserTasksPage.tsx
@@ -31,7 +31,7 @@ export function UserTasksPage() { @@ -31,7 +31,7 @@ export function UserTasksPage() {
31 31
32 const columns: Column<UserTaskSummary>[] = [ 32 const columns: Column<UserTaskSummary>[] = [
33 { 33 {
34 - header: 'Task Name', 34 + header: t('label.taskName'),
35 key: 'taskName', 35 key: 'taskName',
36 render: (r) => ( 36 render: (r) => (
37 <button 37 <button
@@ -42,10 +42,10 @@ export function UserTasksPage() { @@ -42,10 +42,10 @@ export function UserTasksPage() {
42 </button> 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 return ( 51 return (
@@ -61,7 +61,7 @@ export function UserTasksPage() { @@ -61,7 +61,7 @@ export function UserTasksPage() {
61 rows={rows} 61 rows={rows}
62 columns={columns} 62 columns={columns}
63 rowKey={(r) => r.taskId} 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 </div> 67 </div>
web/src/pages/UsersPage.tsx
@@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -6,8 +6,10 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DataTable, type Column } from '@/components/DataTable' 8 import { DataTable, type Column } from '@/components/DataTable'
  9 +import { useT } from '@/i18n/LocaleContext'
9 10
10 export function UsersPage() { 11 export function UsersPage() {
  12 + const t = useT()
11 const [rows, setRows] = useState<User[]>([]) 13 const [rows, setRows] = useState<User[]>([])
12 const [error, setError] = useState<Error | null>(null) 14 const [error, setError] = useState<Error | null>(null)
13 const [loading, setLoading] = useState(true) 15 const [loading, setLoading] = useState(true)
@@ -22,7 +24,7 @@ export function UsersPage() { @@ -22,7 +24,7 @@ export function UsersPage() {
22 24
23 const columns: Column<User>[] = [ 25 const columns: Column<User>[] = [
24 { 26 {
25 - header: 'Username', 27 + header: t('label.username'),
26 key: 'username', 28 key: 'username',
27 render: (r) => ( 29 render: (r) => (
28 <Link to={`/users/${r.id}`} className="font-mono text-brand-600 hover:underline"> 30 <Link to={`/users/${r.id}`} className="font-mono text-brand-600 hover:underline">
@@ -30,16 +32,16 @@ export function UsersPage() { @@ -30,16 +32,16 @@ export function UsersPage() {
30 </Link> 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 key: 'enabled', 39 key: 'enabled',
38 render: (r) => 40 render: (r) =>
39 r.enabled ? ( 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,9 +49,9 @@ export function UsersPage() {
47 return ( 49 return (
48 <div> 50 <div>
49 <PageHeader 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 {loading && <Loading />} 56 {loading && <Loading />}
55 {error && <ErrorBox error={error} />} 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 // action verbs that drive the v2 state machine. The shop-floor 2 // action verbs that drive the v2 state machine. The shop-floor
3 // dashboard at /shop-floor handles the per-operation walk for v3 3 // dashboard at /shop-floor handles the per-operation walk for v3
4 // routing-equipped orders. v1 SPA keeps this screen simple: start a 4 // routing-equipped orders. v1 SPA keeps this screen simple: start a
@@ -12,10 +12,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39; @@ -12,10 +12,12 @@ import { PageHeader } from &#39;@/components/PageHeader&#39;
12 import { Loading } from '@/components/Loading' 12 import { Loading } from '@/components/Loading'
13 import { ErrorBox } from '@/components/ErrorBox' 13 import { ErrorBox } from '@/components/ErrorBox'
14 import { StatusBadge } from '@/components/StatusBadge' 14 import { StatusBadge } from '@/components/StatusBadge'
  15 +import { useT } from '@/i18n/LocaleContext'
15 16
16 export function WorkOrderDetailPage() { 17 export function WorkOrderDetailPage() {
17 const { id = '' } = useParams<{ id: string }>() 18 const { id = '' } = useParams<{ id: string }>()
18 const navigate = useNavigate() 19 const navigate = useNavigate()
  20 + const t = useT()
19 const [order, setOrder] = useState<WorkOrder | null>(null) 21 const [order, setOrder] = useState<WorkOrder | null>(null)
20 const [locations, setLocations] = useState<Location[]>([]) 22 const [locations, setLocations] = useState<Location[]>([])
21 const [outputLocation, setOutputLocation] = useState<string>('') 23 const [outputLocation, setOutputLocation] = useState<string>('')
@@ -60,7 +62,7 @@ export function WorkOrderDetailPage() { @@ -60,7 +62,7 @@ export function WorkOrderDetailPage() {
60 try { 62 try {
61 await production.startWorkOrder(order.id) 63 await production.startWorkOrder(order.id)
62 await refresh() 64 await refresh()
63 - setActionMessage('Started. Operations can now be walked from the Shop Floor screen.') 65 + setActionMessage(t('page.workOrderDetail.startMsg'))
64 } catch (e: unknown) { 66 } catch (e: unknown) {
65 setError(e instanceof Error ? e : new Error(String(e))) 67 setError(e instanceof Error ? e : new Error(String(e)))
66 } finally { 68 } finally {
@@ -70,7 +72,7 @@ export function WorkOrderDetailPage() { @@ -70,7 +72,7 @@ export function WorkOrderDetailPage() {
70 72
71 const onComplete = async () => { 73 const onComplete = async () => {
72 if (!outputLocation) { 74 if (!outputLocation) {
73 - setError(new Error('Pick an output location first.')) 75 + setError(new Error(t('page.workOrderDetail.pickLocation')))
74 return 76 return
75 } 77 }
76 setActing(true) 78 setActing(true)
@@ -80,7 +82,7 @@ export function WorkOrderDetailPage() { @@ -80,7 +82,7 @@ export function WorkOrderDetailPage() {
80 await production.completeWorkOrder(order.id, outputLocation) 82 await production.completeWorkOrder(order.id, outputLocation)
81 await refresh() 83 await refresh()
82 setActionMessage( 84 setActionMessage(
83 - `Completed. Materials issued, finished goods credited to ${outputLocation}.`, 85 + t('page.workOrderDetail.completeMsg').replace('{location}', outputLocation),
84 ) 86 )
85 } catch (e: unknown) { 87 } catch (e: unknown) {
86 setError(e instanceof Error ? e : new Error(String(e))) 88 setError(e instanceof Error ? e : new Error(String(e)))
@@ -95,20 +97,20 @@ export function WorkOrderDetailPage() { @@ -95,20 +97,20 @@ export function WorkOrderDetailPage() {
95 return ( 97 return (
96 <div> 98 <div>
97 <PageHeader 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 actions={ 104 actions={
103 <button className="btn-secondary" onClick={() => navigate('/work-orders')}> 105 <button className="btn-secondary" onClick={() => navigate('/work-orders')}>
104 - ← Back 106 + {t('action.back')}
105 </button> 107 </button>
106 } 108 }
107 /> 109 />
108 110
109 <div className="card mb-6 p-5"> 111 <div className="card mb-6 p-5">
110 <div className="flex items-center gap-3"> 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 <StatusBadge status={order.status} /> 114 <StatusBadge status={order.status} />
113 </div> 115 </div>
114 {actionMessage && ( 116 {actionMessage && (
@@ -119,10 +121,10 @@ export function WorkOrderDetailPage() { @@ -119,10 +121,10 @@ export function WorkOrderDetailPage() {
119 </div> 121 </div>
120 122
121 <div className="card mb-6 p-5"> 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 <div className="flex flex-wrap items-center gap-3"> 125 <div className="flex flex-wrap items-center gap-3">
124 <button className="btn-primary" disabled={!canStart || acting} onClick={onStart}> 126 <button className="btn-primary" disabled={!canStart || acting} onClick={onStart}>
125 - Start 127 + {t('action.start')}
126 </button> 128 </button>
127 <div className="flex items-center gap-2"> 129 <div className="flex items-center gap-2">
128 <select 130 <select
@@ -138,7 +140,7 @@ export function WorkOrderDetailPage() { @@ -138,7 +140,7 @@ export function WorkOrderDetailPage() {
138 ))} 140 ))}
139 </select> 141 </select>
140 <button className="btn-primary" disabled={!canComplete || acting} onClick={onComplete}> 142 <button className="btn-primary" disabled={!canComplete || acting} onClick={onComplete}>
141 - Complete 143 + {t('action.complete')}
142 </button> 144 </button>
143 </div> 145 </div>
144 </div> 146 </div>
@@ -147,18 +149,18 @@ export function WorkOrderDetailPage() { @@ -147,18 +149,18 @@ export function WorkOrderDetailPage() {
147 <div className="grid gap-6 md:grid-cols-2"> 149 <div className="grid gap-6 md:grid-cols-2">
148 <div className="card"> 150 <div className="card">
149 <div className="border-b border-slate-200 px-5 py-3"> 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 </div> 153 </div>
152 {order.inputs.length === 0 ? ( 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 <table className="table-base"> 157 <table className="table-base">
156 <thead className="bg-slate-50"> 158 <thead className="bg-slate-50">
157 <tr> 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 </tr> 164 </tr>
163 </thead> 165 </thead>
164 <tbody className="divide-y divide-slate-100"> 166 <tbody className="divide-y divide-slate-100">
@@ -176,19 +178,19 @@ export function WorkOrderDetailPage() { @@ -176,19 +178,19 @@ export function WorkOrderDetailPage() {
176 </div> 178 </div>
177 <div className="card"> 179 <div className="card">
178 <div className="border-b border-slate-200 px-5 py-3"> 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 </div> 182 </div>
181 {order.operations.length === 0 ? ( 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 <table className="table-base"> 186 <table className="table-base">
185 <thead className="bg-slate-50"> 187 <thead className="bg-slate-50">
186 <tr> 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 </tr> 194 </tr>
193 </thead> 195 </thead>
194 <tbody className="divide-y divide-slate-100"> 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,8 +7,10 @@ import { Loading } from &#39;@/components/Loading&#39;
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
8 import { DataTable, type Column } from '@/components/DataTable' 8 import { DataTable, type Column } from '@/components/DataTable'
9 import { StatusBadge } from '@/components/StatusBadge' 9 import { StatusBadge } from '@/components/StatusBadge'
  10 +import { useT } from '@/i18n/LocaleContext'
10 11
11 export function WorkOrdersPage() { 12 export function WorkOrdersPage() {
  13 + const t = useT()
12 const [rows, setRows] = useState<WorkOrder[]>([]) 14 const [rows, setRows] = useState<WorkOrder[]>([])
13 const [error, setError] = useState<Error | null>(null) 15 const [error, setError] = useState<Error | null>(null)
14 const [loading, setLoading] = useState(true) 16 const [loading, setLoading] = useState(true)
@@ -23,7 +25,7 @@ export function WorkOrdersPage() { @@ -23,7 +25,7 @@ export function WorkOrdersPage() {
23 25
24 const columns: Column<WorkOrder>[] = [ 26 const columns: Column<WorkOrder>[] = [
25 { 27 {
26 - header: 'Code', 28 + header: t('label.code'),
27 key: 'code', 29 key: 'code',
28 render: (r) => ( 30 render: (r) => (
29 <Link to={`/work-orders/${r.id}`} className="font-mono text-brand-600 hover:underline"> 31 <Link to={`/work-orders/${r.id}`} className="font-mono text-brand-600 hover:underline">
@@ -31,20 +33,20 @@ export function WorkOrdersPage() { @@ -31,20 +33,20 @@ export function WorkOrdersPage() {
31 </Link> 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 key: 'outputQuantity', 39 key: 'outputQuantity',
38 render: (r) => <span className="font-mono tabular-nums">{String(r.outputQuantity)}</span>, 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 key: 'sourceSalesOrderCode', 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 key: 'opsCount', 50 key: 'opsCount',
49 render: (r) => `${r.inputs.length} / ${r.operations.length}`, 51 render: (r) => `${r.inputs.length} / ${r.operations.length}`,
50 }, 52 },
@@ -53,9 +55,9 @@ export function WorkOrdersPage() { @@ -53,9 +55,9 @@ export function WorkOrdersPage() {
53 return ( 55 return (
54 <div> 56 <div>
55 <PageHeader 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 {loading && <Loading />} 62 {loading && <Loading />}
61 {error && <ErrorBox error={error} />} 63 {error && <ErrorBox error={error} />}