@burdenoff/microfe-movethewheels 2026.828.1 → 2026.830.1

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,62 @@ 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
+ { key: 'googleMaps', name: tr('movethewheels.settings.integrations.googleMaps', 'Google Maps') },
100
+ { key: 'stripe', name: tr('movethewheels.settings.integrations.stripe', 'Stripe') },
101
+ { key: 'twilio', name: tr('movethewheels.settings.integrations.twilio', 'Twilio') },
102
+ ],
103
+ [tr]
104
+ );
105
+
106
+ const notificationItems: { key: keyof NotificationPrefs; label: string; desc: string }[] = useMemo(
107
+ () => [
108
+ {
109
+ key: 'orderUpdates',
110
+ label: tr('movethewheels.settings.notifications.orderUpdates.label', 'Order updates'),
111
+ desc: tr(
112
+ 'movethewheels.settings.notifications.orderUpdates.desc',
113
+ 'Receive notifications for order status changes'
114
+ ),
115
+ },
116
+ {
117
+ key: 'deliveryAlerts',
118
+ label: tr('movethewheels.settings.notifications.deliveryAlerts.label', 'Delivery alerts'),
119
+ desc: tr(
120
+ 'movethewheels.settings.notifications.deliveryAlerts.desc',
121
+ 'Get notified about delivery issues'
122
+ ),
123
+ },
124
+ {
125
+ key: 'systemNotifications',
126
+ label: tr(
127
+ 'movethewheels.settings.notifications.systemNotifications.label',
128
+ 'System notifications'
129
+ ),
130
+ desc: tr(
131
+ 'movethewheels.settings.notifications.systemNotifications.desc',
132
+ 'Important system updates and maintenance'
133
+ ),
134
+ },
135
+ {
136
+ key: 'weeklyReports',
137
+ label: tr('movethewheels.settings.notifications.weeklyReports.label', 'Weekly reports'),
138
+ desc: tr(
139
+ 'movethewheels.settings.notifications.weeklyReports.desc',
140
+ 'Receive weekly performance summary'
141
+ ),
142
+ },
143
+ ],
144
+ [tr]
145
+ );
146
+
116
147
  const loadedForm = useMemo(() => toForm(preferences), [preferences]);
117
148
  useEffect(() => {
118
149
  setForm(loadedForm);
@@ -143,15 +174,15 @@ export default function SettingsPage() {
143
174
 
144
175
  return (
145
176
  <PageLayout
146
- title="Settings"
147
- subtitle="Manage your account and preferences"
177
+ title={tr('movethewheels.settings.title', 'Settings')}
178
+ subtitle={tr('movethewheels.settings.subtitle', 'Manage your account and preferences')}
148
179
  icon={<Settings size={24} />}
149
180
  >
150
181
  <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.
182
+ {tr(
183
+ 'movethewheels.settings.purpose',
184
+ '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.'
185
+ )}
155
186
  </PagePurpose>
156
187
 
157
188
  {(saved || error) && (
@@ -163,7 +194,7 @@ export default function SettingsPage() {
163
194
  : 'bg-status-success-bg-subtle text-status-success-text'
164
195
  }`}
165
196
  >
166
- {error ? error.message : 'Settings saved.'}
197
+ {error ? error.message : tr('movethewheels.settings.saved', 'Settings saved.')}
167
198
  </div>
168
199
  )}
169
200
 
@@ -172,29 +203,41 @@ export default function SettingsPage() {
172
203
  steps={[
173
204
  {
174
205
  id: 'company',
175
- label: 'Confirm company details & localization',
176
- description: 'Set your name, contact info, timezone, currency and language.',
206
+ label: tr('movethewheels.settings.nextSteps.company.label', 'Confirm company details & localization'),
207
+ description: tr(
208
+ 'movethewheels.settings.nextSteps.company.description',
209
+ 'Set your name, contact info, timezone, currency and language.'
210
+ ),
177
211
  icon: <User size={16} />,
178
212
  onClick: () => setTab('general'),
179
213
  },
180
214
  {
181
215
  id: 'notifications',
182
- label: 'Choose your notifications',
183
- description: 'Decide which order, delivery and report emails you receive.',
216
+ label: tr('movethewheels.settings.nextSteps.notifications.label', 'Choose your notifications'),
217
+ description: tr(
218
+ 'movethewheels.settings.nextSteps.notifications.description',
219
+ 'Decide which order, delivery and report emails you receive.'
220
+ ),
184
221
  icon: <Bell size={16} />,
185
222
  onClick: () => setTab('notifications'),
186
223
  },
187
224
  {
188
225
  id: 'security',
189
- label: 'Secure your account',
190
- description: 'Enable two-factor authentication for an extra layer of security.',
226
+ label: tr('movethewheels.settings.nextSteps.security.label', 'Secure your account'),
227
+ description: tr(
228
+ 'movethewheels.settings.nextSteps.security.description',
229
+ 'Enable two-factor authentication for an extra layer of security.'
230
+ ),
191
231
  icon: <Shield size={16} />,
192
232
  onClick: () => setTab('security'),
193
233
  },
194
234
  {
195
235
  id: 'integrations',
196
- label: 'Connect your tools',
197
- description: 'Link maps, payments and messaging providers.',
236
+ label: tr('movethewheels.settings.nextSteps.integrations.label', 'Connect your tools'),
237
+ description: tr(
238
+ 'movethewheels.settings.nextSteps.integrations.description',
239
+ 'Link maps, payments and messaging providers.'
240
+ ),
198
241
  icon: <Webhook size={16} />,
199
242
  onClick: () => setTab('integrations'),
200
243
  },
@@ -204,58 +247,74 @@ export default function SettingsPage() {
204
247
  <Tabs value={tab} onValueChange={setTab}>
205
248
  <TabsList className="mb-6">
206
249
  <TabsTrigger value="general">
207
- <User size={16} /> General
250
+ <User size={16} /> {tr('movethewheels.settings.tabs.general', 'General')}
208
251
  </TabsTrigger>
209
252
  <TabsTrigger value="notifications">
210
- <Bell size={16} /> Notifications
253
+ <Bell size={16} /> {tr('movethewheels.settings.tabs.notifications', 'Notifications')}
211
254
  </TabsTrigger>
212
255
  <TabsTrigger value="security">
213
- <Shield size={16} /> Security
256
+ <Shield size={16} /> {tr('movethewheels.settings.tabs.security', 'Security')}
214
257
  </TabsTrigger>
215
258
  <TabsTrigger value="integrations">
216
- <Webhook size={16} /> Integrations
259
+ <Webhook size={16} /> {tr('movethewheels.settings.tabs.integrations', 'Integrations')}
217
260
  </TabsTrigger>
218
261
  </TabsList>
219
262
 
220
263
  <TabsContent value="general" className="space-y-6">
221
264
  {/* Company Information — the page's single emphasis zone */}
222
265
  <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>
266
+ <h2 className="text-base font-semibold text-text-primary">
267
+ {tr('movethewheels.settings.company.title', 'Company Information')}
268
+ </h2>
269
+ <p className="text-sm text-text-secondary mb-4">
270
+ {tr('movethewheels.settings.company.basicDetails', 'Basic company details')}
271
+ </p>
225
272
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
226
273
  <div>
227
- <Label>Company Name</Label>
274
+ <Label>{tr('movethewheels.settings.company.name', 'Company Name')}</Label>
228
275
  <Input
229
276
  value={form.company.name}
230
- placeholder="Your company name"
277
+ placeholder={tr(
278
+ 'movethewheels.settings.company.placeholders.name',
279
+ 'Your company name'
280
+ )}
231
281
  disabled={isLoading}
232
282
  onChange={(e) => patchCompany({ name: e.target.value })}
233
283
  />
234
284
  </div>
235
285
  <div>
236
- <Label>Contact Email</Label>
286
+ <Label>{tr('movethewheels.settings.company.email', 'Contact Email')}</Label>
237
287
  <Input
238
288
  type="email"
239
289
  value={form.company.email}
240
- placeholder="contact@example.com"
290
+ placeholder={tr(
291
+ 'movethewheels.settings.company.placeholders.email',
292
+ 'contact@example.com'
293
+ )}
241
294
  disabled={isLoading}
242
295
  onChange={(e) => patchCompany({ email: e.target.value })}
243
296
  />
244
297
  </div>
245
298
  <div>
246
- <Label>Phone</Label>
299
+ <Label>{tr('movethewheels.settings.company.phone', 'Phone')}</Label>
247
300
  <Input
248
301
  value={form.company.phone}
249
- placeholder="+1-555-0100"
302
+ placeholder={tr(
303
+ 'movethewheels.settings.company.placeholders.phone',
304
+ '+1-555-0100'
305
+ )}
250
306
  disabled={isLoading}
251
307
  onChange={(e) => patchCompany({ phone: e.target.value })}
252
308
  />
253
309
  </div>
254
310
  <div>
255
- <Label>Website</Label>
311
+ <Label>{tr('movethewheels.settings.company.website', 'Website')}</Label>
256
312
  <Input
257
313
  value={form.company.website}
258
- placeholder="https://example.com"
314
+ placeholder={tr(
315
+ 'movethewheels.settings.company.placeholders.website',
316
+ 'https://example.com'
317
+ )}
259
318
  disabled={isLoading}
260
319
  onChange={(e) => patchCompany({ website: e.target.value })}
261
320
  />
@@ -266,13 +325,13 @@ export default function SettingsPage() {
266
325
  <Card>
267
326
  <CardHeader>
268
327
  <CardTitle className="flex items-center gap-2">
269
- <Globe size={18} /> Localization
328
+ <Globe size={18} /> {tr('movethewheels.settings.localization.title', 'Localization')}
270
329
  </CardTitle>
271
330
  </CardHeader>
272
331
  <CardContent className="space-y-4">
273
332
  <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
274
333
  <div>
275
- <Label>Timezone</Label>
334
+ <Label>{tr('movethewheels.settings.localization.timezone', 'Timezone')}</Label>
276
335
  <Select
277
336
  value={form.localization.timezone}
278
337
  onValueChange={(value) => patchLocalization({ timezone: value })}
@@ -288,7 +347,7 @@ export default function SettingsPage() {
288
347
  </Select>
289
348
  </div>
290
349
  <div>
291
- <Label>Currency</Label>
350
+ <Label>{tr('movethewheels.settings.localization.currency', 'Currency')}</Label>
292
351
  <Select
293
352
  value={form.localization.currency}
294
353
  onValueChange={(value) => patchLocalization({ currency: value })}
@@ -304,7 +363,7 @@ export default function SettingsPage() {
304
363
  </Select>
305
364
  </div>
306
365
  <div>
307
- <Label>Language</Label>
366
+ <Label>{tr('movethewheels.settings.localization.language', 'Language')}</Label>
308
367
  <Select
309
368
  value={form.localization.language}
310
369
  onValueChange={(value) => patchLocalization({ language: value })}
@@ -327,10 +386,10 @@ export default function SettingsPage() {
327
386
  <TabsContent value="notifications" className="space-y-6">
328
387
  <Card>
329
388
  <CardHeader>
330
- <CardTitle>Email Notifications</CardTitle>
389
+ <CardTitle>{tr('movethewheels.settings.notifications.title', 'Email Notifications')}</CardTitle>
331
390
  </CardHeader>
332
391
  <CardContent className="space-y-4">
333
- {NOTIFICATION_ITEMS.map((item) => (
392
+ {notificationItems.map((item) => (
334
393
  <div key={item.key} className="flex items-center justify-between">
335
394
  <div>
336
395
  <p className="font-medium">{item.label}</p>
@@ -350,22 +409,31 @@ export default function SettingsPage() {
350
409
  <TabsContent value="security" className="space-y-6">
351
410
  <Card>
352
411
  <CardHeader>
353
- <CardTitle>Password</CardTitle>
412
+ <CardTitle>{tr('movethewheels.settings.security.password.title', 'Password')}</CardTitle>
354
413
  <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.
414
+ {tr(
415
+ 'movethewheels.settings.security.password.description',
416
+ 'Your password and sign-in credentials are managed centrally by your workspace identity provider — update them from your account security settings, not here.'
417
+ )}
357
418
  </CardDescription>
358
419
  </CardHeader>
359
420
  </Card>
360
421
  <Card>
361
422
  <CardHeader>
362
- <CardTitle>Two-Factor Authentication</CardTitle>
363
- <CardDescription>Add an extra layer of security</CardDescription>
423
+ <CardTitle>
424
+ {tr('movethewheels.settings.security.twoFactor.title', 'Two-Factor Authentication')}
425
+ </CardTitle>
426
+ <CardDescription>
427
+ {tr(
428
+ 'movethewheels.settings.security.twoFactor.description',
429
+ 'Add an extra layer of security'
430
+ )}
431
+ </CardDescription>
364
432
  </CardHeader>
365
433
  <CardContent>
366
434
  <div className="flex items-center justify-between">
367
435
  <p>
368
- Status:{' '}
436
+ {tr('movethewheels.settings.security.twoFactor.status', 'Status:')}{' '}
369
437
  <span
370
438
  className={`font-medium ${
371
439
  form.twoFactorEnabled
@@ -373,7 +441,9 @@ export default function SettingsPage() {
373
441
  : 'text-status-warning-text'
374
442
  }`}
375
443
  >
376
- {form.twoFactorEnabled ? 'Enabled' : 'Disabled'}
444
+ {form.twoFactorEnabled
445
+ ? tr('movethewheels.settings.security.twoFactor.enabled', 'Enabled')
446
+ : tr('movethewheels.settings.security.twoFactor.disabled', 'Disabled')}
377
447
  </span>
378
448
  </p>
379
449
  <Button
@@ -383,7 +453,9 @@ export default function SettingsPage() {
383
453
  setForm((prev) => ({ ...prev, twoFactorEnabled: !prev.twoFactorEnabled }))
384
454
  }
385
455
  >
386
- {form.twoFactorEnabled ? 'Disable 2FA' : 'Enable 2FA'}
456
+ {form.twoFactorEnabled
457
+ ? tr('movethewheels.settings.security.twoFactor.disable', 'Disable 2FA')
458
+ : tr('movethewheels.settings.security.twoFactor.enable', 'Enable 2FA')}
387
459
  </Button>
388
460
  </div>
389
461
  </CardContent>
@@ -393,13 +465,18 @@ export default function SettingsPage() {
393
465
  <TabsContent value="integrations" className="space-y-6">
394
466
  <Card>
395
467
  <CardHeader>
396
- <CardTitle>Connected Services</CardTitle>
468
+ <CardTitle>
469
+ {tr('movethewheels.settings.integrations.title', 'Connected Services')}
470
+ </CardTitle>
397
471
  <CardDescription>
398
- Connect the providers that power maps, payments and messaging.
472
+ {tr(
473
+ 'movethewheels.settings.integrations.description',
474
+ 'Connect the providers that power maps, payments and messaging.'
475
+ )}
399
476
  </CardDescription>
400
477
  </CardHeader>
401
478
  <CardContent className="space-y-3">
402
- {INTEGRATIONS.map((service) => {
479
+ {integrations.map((service) => {
403
480
  const connected = form.integrations[service.key] === true;
404
481
  return (
405
482
  <div
@@ -409,7 +486,9 @@ export default function SettingsPage() {
409
486
  <div>
410
487
  <span className="font-medium">{service.name}</span>
411
488
  <p className="text-sm text-text-secondary">
412
- {connected ? 'Connected' : 'Not configured'}
489
+ {connected
490
+ ? tr('movethewheels.settings.integrations.connected', 'Connected')
491
+ : tr('movethewheels.settings.integrations.notConfigured', 'Not configured')}
413
492
  </p>
414
493
  </div>
415
494
  <Button
@@ -426,7 +505,9 @@ export default function SettingsPage() {
426
505
  }))
427
506
  }
428
507
  >
429
- {connected ? 'Disconnect' : 'Connect'}
508
+ {connected
509
+ ? tr('movethewheels.settings.integrations.disconnect', 'Disconnect')
510
+ : tr('movethewheels.settings.integrations.connect', 'Connect')}
430
511
  </Button>
431
512
  </div>
432
513
  );
@@ -438,7 +519,10 @@ export default function SettingsPage() {
438
519
 
439
520
  <div className="flex justify-end">
440
521
  <Button onClick={() => void handleSave()} disabled={isSaving || isLoading}>
441
- <Save size={16} /> {isSaving ? 'Saving…' : 'Save Changes'}
522
+ <Save size={16} />{' '}
523
+ {isSaving
524
+ ? tr('movethewheels.settings.saving', 'Saving…')
525
+ : tr('movethewheels.settings.saveChanges', 'Save Changes')}
442
526
  </Button>
443
527
  </div>
444
528
  </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
+ }