@burdenoff/microfe-movethewheels 2026.828.1 → 2026.830.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,7 @@ import {
31
31
  } from '../components/ui';
32
32
  import { EmphasisPanel, PagePurpose, NextSteps } from '@burdenoff/fe-libs/ui';
33
33
  import { useModuleSettings, type ModulePreferences } from '../hooks/useSettings';
34
+ import { useTr } from '../hooks';
34
35
 
35
36
  interface CompanyPrefs {
36
37
  name: string;
@@ -57,27 +58,6 @@ interface SettingsForm {
57
58
  integrations: Record<string, boolean>;
58
59
  }
59
60
 
60
- const INTEGRATIONS = [
61
- { key: 'googleMaps', name: 'Google Maps' },
62
- { key: 'stripe', name: 'Stripe' },
63
- { key: 'twilio', name: 'Twilio' },
64
- ];
65
-
66
- const NOTIFICATION_ITEMS: { key: keyof NotificationPrefs; label: string; desc: string }[] = [
67
- {
68
- key: 'orderUpdates',
69
- label: 'Order updates',
70
- desc: 'Receive notifications for order status changes',
71
- },
72
- { key: 'deliveryAlerts', label: 'Delivery alerts', desc: 'Get notified about delivery issues' },
73
- {
74
- key: 'systemNotifications',
75
- label: 'System notifications',
76
- desc: 'Important system updates and maintenance',
77
- },
78
- { key: 'weeklyReports', label: 'Weekly reports', desc: 'Receive weekly performance summary' },
79
- ];
80
-
81
61
  function toForm(preferences: ModulePreferences): SettingsForm {
82
62
  const company = (preferences.company as Partial<CompanyPrefs> | undefined) ?? {};
83
63
  const localization = (preferences.localization as Partial<LocalizationPrefs> | undefined) ?? {};
@@ -108,11 +88,66 @@ function toForm(preferences: ModulePreferences): SettingsForm {
108
88
  }
109
89
 
110
90
  export default function SettingsPage() {
91
+ const tr = useTr();
111
92
  const { preferences, isLoading, isSaving, error, savePreferences } = useModuleSettings();
112
93
  const [tab, setTab] = useState('general');
113
94
  const [form, setForm] = useState<SettingsForm>(() => toForm({}));
114
95
  const [saved, setSaved] = useState(false);
115
96
 
97
+ const integrations = useMemo(
98
+ () => [
99
+ {
100
+ key: 'googleMaps',
101
+ name: tr('movethewheels.settings.integrations.googleMaps', 'Google Maps'),
102
+ },
103
+ { key: 'stripe', name: tr('movethewheels.settings.integrations.stripe', 'Stripe') },
104
+ { key: 'twilio', name: tr('movethewheels.settings.integrations.twilio', 'Twilio') },
105
+ ],
106
+ [tr]
107
+ );
108
+
109
+ const notificationItems: { key: keyof NotificationPrefs; label: string; desc: string }[] =
110
+ useMemo(
111
+ () => [
112
+ {
113
+ key: 'orderUpdates',
114
+ label: tr('movethewheels.settings.notifications.orderUpdates.label', 'Order updates'),
115
+ desc: tr(
116
+ 'movethewheels.settings.notifications.orderUpdates.desc',
117
+ 'Receive notifications for order status changes'
118
+ ),
119
+ },
120
+ {
121
+ key: 'deliveryAlerts',
122
+ label: tr('movethewheels.settings.notifications.deliveryAlerts.label', 'Delivery alerts'),
123
+ desc: tr(
124
+ 'movethewheels.settings.notifications.deliveryAlerts.desc',
125
+ 'Get notified about delivery issues'
126
+ ),
127
+ },
128
+ {
129
+ key: 'systemNotifications',
130
+ label: tr(
131
+ 'movethewheels.settings.notifications.systemNotifications.label',
132
+ 'System notifications'
133
+ ),
134
+ desc: tr(
135
+ 'movethewheels.settings.notifications.systemNotifications.desc',
136
+ 'Important system updates and maintenance'
137
+ ),
138
+ },
139
+ {
140
+ key: 'weeklyReports',
141
+ label: tr('movethewheels.settings.notifications.weeklyReports.label', 'Weekly reports'),
142
+ desc: tr(
143
+ 'movethewheels.settings.notifications.weeklyReports.desc',
144
+ 'Receive weekly performance summary'
145
+ ),
146
+ },
147
+ ],
148
+ [tr]
149
+ );
150
+
116
151
  const loadedForm = useMemo(() => toForm(preferences), [preferences]);
117
152
  useEffect(() => {
118
153
  setForm(loadedForm);
@@ -143,15 +178,15 @@ export default function SettingsPage() {
143
178
 
144
179
  return (
145
180
  <PageLayout
146
- title="Settings"
147
- subtitle="Manage your account and preferences"
181
+ title={tr('movethewheels.settings.title', 'Settings')}
182
+ subtitle={tr('movethewheels.settings.subtitle', 'Manage your account and preferences')}
148
183
  icon={<Settings size={24} />}
149
184
  >
150
185
  <PagePurpose icon={<Settings size={18} />}>
151
- Configure how MoveTheWheels works for your business — company details and localization,
152
- which notifications you receive, account security, and the integrations (maps, payments,
153
- messaging) that power deliveries. Set these once so the rest of the app behaves the way you
154
- expect.
186
+ {tr(
187
+ 'movethewheels.settings.purpose',
188
+ 'Configure how MoveTheWheels works for your business — company details and localization, which notifications you receive, account security, and the integrations (maps, payments, messaging) that power deliveries. Set these once so the rest of the app behaves the way you expect.'
189
+ )}
155
190
  </PagePurpose>
156
191
 
157
192
  {(saved || error) && (
@@ -163,7 +198,7 @@ export default function SettingsPage() {
163
198
  : 'bg-status-success-bg-subtle text-status-success-text'
164
199
  }`}
165
200
  >
166
- {error ? error.message : 'Settings saved.'}
201
+ {error ? error.message : tr('movethewheels.settings.saved', 'Settings saved.')}
167
202
  </div>
168
203
  )}
169
204
 
@@ -172,29 +207,47 @@ export default function SettingsPage() {
172
207
  steps={[
173
208
  {
174
209
  id: 'company',
175
- label: 'Confirm company details & localization',
176
- description: 'Set your name, contact info, timezone, currency and language.',
210
+ label: tr(
211
+ 'movethewheels.settings.nextSteps.company.label',
212
+ 'Confirm company details & localization'
213
+ ),
214
+ description: tr(
215
+ 'movethewheels.settings.nextSteps.company.description',
216
+ 'Set your name, contact info, timezone, currency and language.'
217
+ ),
177
218
  icon: <User size={16} />,
178
219
  onClick: () => setTab('general'),
179
220
  },
180
221
  {
181
222
  id: 'notifications',
182
- label: 'Choose your notifications',
183
- description: 'Decide which order, delivery and report emails you receive.',
223
+ label: tr(
224
+ 'movethewheels.settings.nextSteps.notifications.label',
225
+ 'Choose your notifications'
226
+ ),
227
+ description: tr(
228
+ 'movethewheels.settings.nextSteps.notifications.description',
229
+ 'Decide which order, delivery and report emails you receive.'
230
+ ),
184
231
  icon: <Bell size={16} />,
185
232
  onClick: () => setTab('notifications'),
186
233
  },
187
234
  {
188
235
  id: 'security',
189
- label: 'Secure your account',
190
- description: 'Enable two-factor authentication for an extra layer of security.',
236
+ label: tr('movethewheels.settings.nextSteps.security.label', 'Secure your account'),
237
+ description: tr(
238
+ 'movethewheels.settings.nextSteps.security.description',
239
+ 'Enable two-factor authentication for an extra layer of security.'
240
+ ),
191
241
  icon: <Shield size={16} />,
192
242
  onClick: () => setTab('security'),
193
243
  },
194
244
  {
195
245
  id: 'integrations',
196
- label: 'Connect your tools',
197
- description: 'Link maps, payments and messaging providers.',
246
+ label: tr('movethewheels.settings.nextSteps.integrations.label', 'Connect your tools'),
247
+ description: tr(
248
+ 'movethewheels.settings.nextSteps.integrations.description',
249
+ 'Link maps, payments and messaging providers.'
250
+ ),
198
251
  icon: <Webhook size={16} />,
199
252
  onClick: () => setTab('integrations'),
200
253
  },
@@ -204,58 +257,74 @@ export default function SettingsPage() {
204
257
  <Tabs value={tab} onValueChange={setTab}>
205
258
  <TabsList className="mb-6">
206
259
  <TabsTrigger value="general">
207
- <User size={16} /> General
260
+ <User size={16} /> {tr('movethewheels.settings.tabs.general', 'General')}
208
261
  </TabsTrigger>
209
262
  <TabsTrigger value="notifications">
210
- <Bell size={16} /> Notifications
263
+ <Bell size={16} /> {tr('movethewheels.settings.tabs.notifications', 'Notifications')}
211
264
  </TabsTrigger>
212
265
  <TabsTrigger value="security">
213
- <Shield size={16} /> Security
266
+ <Shield size={16} /> {tr('movethewheels.settings.tabs.security', 'Security')}
214
267
  </TabsTrigger>
215
268
  <TabsTrigger value="integrations">
216
- <Webhook size={16} /> Integrations
269
+ <Webhook size={16} /> {tr('movethewheels.settings.tabs.integrations', 'Integrations')}
217
270
  </TabsTrigger>
218
271
  </TabsList>
219
272
 
220
273
  <TabsContent value="general" className="space-y-6">
221
274
  {/* Company Information — the page's single emphasis zone */}
222
275
  <EmphasisPanel>
223
- <h2 className="text-base font-semibold text-text-primary">Company Information</h2>
224
- <p className="text-sm text-text-secondary mb-4">Basic company details</p>
276
+ <h2 className="text-base font-semibold text-text-primary">
277
+ {tr('movethewheels.settings.company.title', 'Company Information')}
278
+ </h2>
279
+ <p className="text-sm text-text-secondary mb-4">
280
+ {tr('movethewheels.settings.company.basicDetails', 'Basic company details')}
281
+ </p>
225
282
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
226
283
  <div>
227
- <Label>Company Name</Label>
284
+ <Label>{tr('movethewheels.settings.company.name', 'Company Name')}</Label>
228
285
  <Input
229
286
  value={form.company.name}
230
- placeholder="Your company name"
287
+ placeholder={tr(
288
+ 'movethewheels.settings.company.placeholders.name',
289
+ 'Your company name'
290
+ )}
231
291
  disabled={isLoading}
232
292
  onChange={(e) => patchCompany({ name: e.target.value })}
233
293
  />
234
294
  </div>
235
295
  <div>
236
- <Label>Contact Email</Label>
296
+ <Label>{tr('movethewheels.settings.company.email', 'Contact Email')}</Label>
237
297
  <Input
238
298
  type="email"
239
299
  value={form.company.email}
240
- placeholder="contact@example.com"
300
+ placeholder={tr(
301
+ 'movethewheels.settings.company.placeholders.email',
302
+ 'contact@example.com'
303
+ )}
241
304
  disabled={isLoading}
242
305
  onChange={(e) => patchCompany({ email: e.target.value })}
243
306
  />
244
307
  </div>
245
308
  <div>
246
- <Label>Phone</Label>
309
+ <Label>{tr('movethewheels.settings.company.phone', 'Phone')}</Label>
247
310
  <Input
248
311
  value={form.company.phone}
249
- placeholder="+1-555-0100"
312
+ placeholder={tr(
313
+ 'movethewheels.settings.company.placeholders.phone',
314
+ '+1-555-0100'
315
+ )}
250
316
  disabled={isLoading}
251
317
  onChange={(e) => patchCompany({ phone: e.target.value })}
252
318
  />
253
319
  </div>
254
320
  <div>
255
- <Label>Website</Label>
321
+ <Label>{tr('movethewheels.settings.company.website', 'Website')}</Label>
256
322
  <Input
257
323
  value={form.company.website}
258
- placeholder="https://example.com"
324
+ placeholder={tr(
325
+ 'movethewheels.settings.company.placeholders.website',
326
+ 'https://example.com'
327
+ )}
259
328
  disabled={isLoading}
260
329
  onChange={(e) => patchCompany({ website: e.target.value })}
261
330
  />
@@ -266,13 +335,14 @@ export default function SettingsPage() {
266
335
  <Card>
267
336
  <CardHeader>
268
337
  <CardTitle className="flex items-center gap-2">
269
- <Globe size={18} /> Localization
338
+ <Globe size={18} />{' '}
339
+ {tr('movethewheels.settings.localization.title', 'Localization')}
270
340
  </CardTitle>
271
341
  </CardHeader>
272
342
  <CardContent className="space-y-4">
273
343
  <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
274
344
  <div>
275
- <Label>Timezone</Label>
345
+ <Label>{tr('movethewheels.settings.localization.timezone', 'Timezone')}</Label>
276
346
  <Select
277
347
  value={form.localization.timezone}
278
348
  onValueChange={(value) => patchLocalization({ timezone: value })}
@@ -288,7 +358,7 @@ export default function SettingsPage() {
288
358
  </Select>
289
359
  </div>
290
360
  <div>
291
- <Label>Currency</Label>
361
+ <Label>{tr('movethewheels.settings.localization.currency', 'Currency')}</Label>
292
362
  <Select
293
363
  value={form.localization.currency}
294
364
  onValueChange={(value) => patchLocalization({ currency: value })}
@@ -304,7 +374,7 @@ export default function SettingsPage() {
304
374
  </Select>
305
375
  </div>
306
376
  <div>
307
- <Label>Language</Label>
377
+ <Label>{tr('movethewheels.settings.localization.language', 'Language')}</Label>
308
378
  <Select
309
379
  value={form.localization.language}
310
380
  onValueChange={(value) => patchLocalization({ language: value })}
@@ -327,10 +397,12 @@ export default function SettingsPage() {
327
397
  <TabsContent value="notifications" className="space-y-6">
328
398
  <Card>
329
399
  <CardHeader>
330
- <CardTitle>Email Notifications</CardTitle>
400
+ <CardTitle>
401
+ {tr('movethewheels.settings.notifications.title', 'Email Notifications')}
402
+ </CardTitle>
331
403
  </CardHeader>
332
404
  <CardContent className="space-y-4">
333
- {NOTIFICATION_ITEMS.map((item) => (
405
+ {notificationItems.map((item) => (
334
406
  <div key={item.key} className="flex items-center justify-between">
335
407
  <div>
336
408
  <p className="font-medium">{item.label}</p>
@@ -350,22 +422,33 @@ export default function SettingsPage() {
350
422
  <TabsContent value="security" className="space-y-6">
351
423
  <Card>
352
424
  <CardHeader>
353
- <CardTitle>Password</CardTitle>
425
+ <CardTitle>
426
+ {tr('movethewheels.settings.security.password.title', 'Password')}
427
+ </CardTitle>
354
428
  <CardDescription>
355
- Your password and sign-in credentials are managed centrally by your workspace
356
- identity provider — update them from your account security settings, not here.
429
+ {tr(
430
+ 'movethewheels.settings.security.password.description',
431
+ 'Your password and sign-in credentials are managed centrally by your workspace identity provider — update them from your account security settings, not here.'
432
+ )}
357
433
  </CardDescription>
358
434
  </CardHeader>
359
435
  </Card>
360
436
  <Card>
361
437
  <CardHeader>
362
- <CardTitle>Two-Factor Authentication</CardTitle>
363
- <CardDescription>Add an extra layer of security</CardDescription>
438
+ <CardTitle>
439
+ {tr('movethewheels.settings.security.twoFactor.title', 'Two-Factor Authentication')}
440
+ </CardTitle>
441
+ <CardDescription>
442
+ {tr(
443
+ 'movethewheels.settings.security.twoFactor.description',
444
+ 'Add an extra layer of security'
445
+ )}
446
+ </CardDescription>
364
447
  </CardHeader>
365
448
  <CardContent>
366
449
  <div className="flex items-center justify-between">
367
450
  <p>
368
- Status:{' '}
451
+ {tr('movethewheels.settings.security.twoFactor.status', 'Status:')}{' '}
369
452
  <span
370
453
  className={`font-medium ${
371
454
  form.twoFactorEnabled
@@ -373,7 +456,9 @@ export default function SettingsPage() {
373
456
  : 'text-status-warning-text'
374
457
  }`}
375
458
  >
376
- {form.twoFactorEnabled ? 'Enabled' : 'Disabled'}
459
+ {form.twoFactorEnabled
460
+ ? tr('movethewheels.settings.security.twoFactor.enabled', 'Enabled')
461
+ : tr('movethewheels.settings.security.twoFactor.disabled', 'Disabled')}
377
462
  </span>
378
463
  </p>
379
464
  <Button
@@ -383,7 +468,9 @@ export default function SettingsPage() {
383
468
  setForm((prev) => ({ ...prev, twoFactorEnabled: !prev.twoFactorEnabled }))
384
469
  }
385
470
  >
386
- {form.twoFactorEnabled ? 'Disable 2FA' : 'Enable 2FA'}
471
+ {form.twoFactorEnabled
472
+ ? tr('movethewheels.settings.security.twoFactor.disable', 'Disable 2FA')
473
+ : tr('movethewheels.settings.security.twoFactor.enable', 'Enable 2FA')}
387
474
  </Button>
388
475
  </div>
389
476
  </CardContent>
@@ -393,13 +480,18 @@ export default function SettingsPage() {
393
480
  <TabsContent value="integrations" className="space-y-6">
394
481
  <Card>
395
482
  <CardHeader>
396
- <CardTitle>Connected Services</CardTitle>
483
+ <CardTitle>
484
+ {tr('movethewheels.settings.integrations.title', 'Connected Services')}
485
+ </CardTitle>
397
486
  <CardDescription>
398
- Connect the providers that power maps, payments and messaging.
487
+ {tr(
488
+ 'movethewheels.settings.integrations.description',
489
+ 'Connect the providers that power maps, payments and messaging.'
490
+ )}
399
491
  </CardDescription>
400
492
  </CardHeader>
401
493
  <CardContent className="space-y-3">
402
- {INTEGRATIONS.map((service) => {
494
+ {integrations.map((service) => {
403
495
  const connected = form.integrations[service.key] === true;
404
496
  return (
405
497
  <div
@@ -409,7 +501,12 @@ export default function SettingsPage() {
409
501
  <div>
410
502
  <span className="font-medium">{service.name}</span>
411
503
  <p className="text-sm text-text-secondary">
412
- {connected ? 'Connected' : 'Not configured'}
504
+ {connected
505
+ ? tr('movethewheels.settings.integrations.connected', 'Connected')
506
+ : tr(
507
+ 'movethewheels.settings.integrations.notConfigured',
508
+ 'Not configured'
509
+ )}
413
510
  </p>
414
511
  </div>
415
512
  <Button
@@ -426,7 +523,9 @@ export default function SettingsPage() {
426
523
  }))
427
524
  }
428
525
  >
429
- {connected ? 'Disconnect' : 'Connect'}
526
+ {connected
527
+ ? tr('movethewheels.settings.integrations.disconnect', 'Disconnect')
528
+ : tr('movethewheels.settings.integrations.connect', 'Connect')}
430
529
  </Button>
431
530
  </div>
432
531
  );
@@ -438,7 +537,10 @@ export default function SettingsPage() {
438
537
 
439
538
  <div className="flex justify-end">
440
539
  <Button onClick={() => void handleSave()} disabled={isSaving || isLoading}>
441
- <Save size={16} /> {isSaving ? 'Saving…' : 'Save Changes'}
540
+ <Save size={16} />{' '}
541
+ {isSaving
542
+ ? tr('movethewheels.settings.saving', 'Saving…')
543
+ : tr('movethewheels.settings.saveChanges', 'Save Changes')}
442
544
  </Button>
443
545
  </div>
444
546
  </PageLayout>
@@ -0,0 +1,37 @@
1
+ import { useCallback } from 'react';
2
+ import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
3
+
4
+ /**
5
+ * useTr — translation hook with fallback semantics.
6
+ *
7
+ * Wraps `useI18n().t()` so that when a key has no translation (i.e. the
8
+ * provider returns the key itself), the caller's English fallback is used
9
+ * instead. This keeps consumer call sites consistent and avoids leaking
10
+ * untranslated keys into the UI.
11
+ *
12
+ * Supports optional interpolation params using `{{key}}` placeholders.
13
+ *
14
+ * @example
15
+ * const tr = useTr();
16
+ * return <h1>{tr('movethewheels.dashboard.title', 'Dashboard')}</h1>;
17
+ * return <p>{tr('movethewheels.delete.confirm', 'Delete {{count}} items?', { count: 5 })}</p>;
18
+ */
19
+ export function useTr(): (
20
+ key: string,
21
+ fallback: string,
22
+ params?: Record<string, string | number>
23
+ ) => string {
24
+ const { t } = useI18n();
25
+ return useCallback(
26
+ (key: string, fallback: string, params?: Record<string, string | number>): string => {
27
+ const translated = t(key);
28
+ const raw = translated === key ? fallback : translated;
29
+ if (!params) return raw;
30
+ return Object.entries(params).reduce(
31
+ (str, [k, v]) => str.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), String(v)),
32
+ raw
33
+ );
34
+ },
35
+ [t]
36
+ );
37
+ }