Skip to content

feat: add next-intl internationalization with 4 locales#78

Closed
gzhang33 wants to merge 7 commits into
Anyesh:mainfrom
gzhang33:feature/i18n-pure
Closed

feat: add next-intl internationalization with 4 locales#78
gzhang33 wants to merge 7 commits into
Anyesh:mainfrom
gzhang33:feature/i18n-pure

Conversation

@gzhang33
Copy link
Copy Markdown

@gzhang33 gzhang33 commented May 13, 2026

Summary

  • Integrate next-intl for full i18n support across the frontend
  • Support 4 locales: English, Chinese (zh), French (fr), Italian (it)
  • Extract all hardcoded strings to translation files (~970 keys per locale)
  • Add locale switcher component with dropdown menu
  • Migrate all pages, components, dialogs, and cards to use useTranslations()
  • Replace hardcoded toast messages, title attributes, placeholders, and inline text
  • Add use-translated-constants hook for dynamic locale-aware constants
  • Add formatWornAgo i18n helper
  • Update tests with next-intl mock setup

No dependency on other PRs. This PR is fully standalone and targets main directly.

Test Plan

  • npx tsc --noEmit — zero TypeScript errors
  • npx vitest run — all tests pass
  • npm run build — production build succeeds
  • Translation key parity: all keys match across en/zh/fr/it

🤖 Generated with Claude Code

apple and others added 7 commits May 13, 2026 02:41
Add geocoding service to resolve location names to coordinates using
Nominatim API. Add frontend location utilities for network-based
location detection and reverse geocoding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Move geocoding user-agent configuration to Settings class. Add
environment variable overrides for geocoding and network location
provider. Update frontend location utilities with configurable URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Simplify exception handling in geocoding service. Fix unit system
reference in settings page body measurements initialization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate next-intl for full i18n support across the frontend:
- Add next-intl package and configure middleware + request config
- Create locale switcher component with dropdown menu
- Extract all hardcoded strings to translation files (967 keys)
- Support English, Chinese, French, and Italian locales
- Migrate all pages, components, dialogs, and cards to use useTranslations()
- Replace hardcoded toast messages, titles, placeholders, and inline text
- Add use-translated-constants hook for dynamic locale-aware constants
- Add formatWornAgo i18n helper and location error message translations
- Update tests with next-intl mock setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace tShared.raw with tShared in formatWornAgo call so ICU params
  like {days} are properly interpolated
- Use t('success', { count }) instead of regex-based string manipulation
  in generate-pairings-dialog
- Replace t('leaveFamily').split(' ')[0] with t('confirmLeave') in
  family leave action button to avoid locale-dependent word splitting
- Add dedicated schedule.dialogDescription translation key instead of
  truncating with split('.')[0] in notifications page
- Add outfits.new.saveError key and use it instead of success message
  outfitUpdated as error fallback
- Enhance next-intl test mocks to include raw/rich/markup methods
- Remove unused getDaysSinceDateInTimezone import from utils.test.ts
- Type computeWarnings t parameter as (key: string) => string instead
  of any
- Fix formatWornAgo t parameter type to match next-intl TranslationValues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 13, 2026 01:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR integrates next-intl into the Next.js frontend to support 4 locales (en/zh/fr/it). It externalizes ~970 user-facing strings per locale, adds a request-config + locale-switcher + cookie-driven locale resolution, threads useTranslations() through every page/component/dialog, and provides hooks for translated CLOTHING_TYPES/COLORS/OCCASIONS. It also adds an i18n-aware formatWornAgo helper, location-error message translations, tests/mocks, and a Dockerfile change to ship messages/.

Changes:

  • Wire up next-intl plugin, request config, message bundles, locale switcher, and provider in the layout.
  • Migrate hardcoded English in pages, dialogs, cards, toasts, placeholders, and constants to translation keys with namespace-scoped useTranslations.
  • Add formatWornAgo/getGeolocationFailureMessage i18n helpers and supporting tests/mocks.

Reviewed changes

Copilot reviewed 71 out of 72 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
frontend/package.json Adds next-intl dependency.
frontend/next.config.js Wraps Next config with createNextIntlPlugin.
frontend/Dockerfile Copies messages/ into the standalone runtime image.
frontend/i18n/request.ts Cookie-based locale resolution and message loader; duplicates supported-locales list with the switcher.
frontend/components/locale-switcher.tsx Globe dropdown that sets NEXT_LOCALE cookie and triggers a full page reload inside startTransition.
frontend/components/ui/dropdown-menu.tsx New thin Radix dropdown wrapper used by the switcher.
frontend/lib/hooks/use-translated-constants.ts Hooks that map static type/color/occasion constants to translated labels; redundant locale in useMemo deps.
frontend/lib/utils.ts formatWornAgo accepts a t function; default fallback returns key+JSON which is unfriendly outside tests.
frontend/lib/location.ts Network-location resolver and getGeolocationFailureMessage(t).
frontend/app/dashboard/page.tsx Translates the next-scheduled card; dayNames is Sunday=0 but schedule.day_of_week follows Monday=0, producing a wrong label.
frontend/app/dashboard/notifications/page.tsx DAY_KEYS Monday=0 convention preserved; complex inline IIFE in t() params.
frontend/app/dashboard/learning/page.tsx Concatenation antipatterns ({n}{t('paired')}, {p}{t('confident')}) and a button bound to a misleading styleInsights.noData key.
frontend/app/dashboard/family/page.tsx Migrated to useTranslations; t.rich used for plain interpolation, the admin-specific leave warning has been collapsed to a single string, and English member${s} plural remains.
frontend/components/add-item-dialog.tsx Cancel buttons reuse bulk.discardConfirm.keepEditing ("Keep editing"); some <Label>s use keys named *Placeholder.
frontend/components/studio/details-panel.tsx Threads t through computeWarnings; toasts and labels translated.
frontend/components/studio/canvas-panel.tsx Empty-state and other strings translated.
frontend/components/outfits/outfit-card.tsx Helpers accept t: any, losing next-intl typing.
frontend/messages/en.json Source-of-truth English messages; a few keys are misnamed (styleInsights.noData = "New Insights") or designed for unsafe concatenation (paired, confident).
frontend/tests/setup.ts Mocks next-intl and next-intl/server returning identity translation functions.
frontend/tests/utils.test.ts Tests for formatWornAgo with mocked t.
frontend/tests/location.test.ts New tests for network/reverse-geocoding helpers and geolocation error mapping.
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

frontend/components/add-item-dialog.tsx:476

  • This Cancel button on the bulk upload tab also reuses t('bulk.discardConfirm.keepEditing') (= "Keep editing"). Same issue as the single-tab Cancel — the button should display "Cancel" rather than "Keep editing". The label is only appropriate inside the discard-confirmation AlertDialog at line 574.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={handleCloseRequest}>
Cancel
{t('bulk.discardConfirm.keepEditing')}
Comment thread frontend/messages/en.json
"styleInsights": {
"title": "Style Insights",
"description": "What we've learned about your preferences",
"noData": "New Insights"
Comment thread frontend/messages/en.json
Comment on lines +678 to +679
"paired": "x paired",
"confident": "% confident",
Comment on lines +335 to +337
tDays('days.sunday'), tDays('days.monday'), tDays('days.tuesday'),
tDays('days.wednesday'), tDays('days.thursday'), tDays('days.friday'),
tDays('days.saturday'),
{isAdmin && family.members.length > 1
? 'You are an admin. Make sure another member is an admin before leaving, or remove all other members first.'
: 'Are you sure you want to leave this family?'}
{t.rich('leaveConfirm.description', { name: family.name })}
Comment on lines 314 to +358
@@ -351,19 +355,19 @@ export function AddItemDialog({ open, onOpenChange }: AddItemDialogProps) {
</div>

<div className="space-y-2">
<Label htmlFor="notes">Notes</Label>
<Label htmlFor="notes">{t('notesPlaceholder')}</Label>
</div>
<div className="text-xs text-muted-foreground">
{pair.times_paired}x paired
{pair.times_paired}{t('paired')}
</Badge>
<span className="text-xs text-muted-foreground">
{Math.round(insight.confidence * 100)}% confidence
{Math.round(insight.confidence * 100)}{t('confident')}
<Button variant="ghost" size="sm" onClick={handleGenerateInsights}>
<RefreshCw className="h-4 w-4 mr-1" />
New Insights
{t('styleInsights.noData')}
Comment on lines +15 to +50
export function useClothingTypes() {
const t = useTranslations('types.clothingTypes');
const locale = useLocale();

return useMemo(() => CLOTHING_TYPES.map((ct) => ({
...ct,
label: t(ct.value),
})), [t, locale]);
}

/**
* Returns clothing colors with translated names.
* The `value` and `hex` properties are preserved.
*/
export function useClothingColors() {
const t = useTranslations('types.clothingColors');
const locale = useLocale();

return useMemo(() => CLOTHING_COLORS.map((cc) => ({
...cc,
name: t(cc.value),
})), [t, locale]);
}

/**
* Returns occasions with translated labels.
* The `value` property is preserved.
*/
export function useOccasions() {
const t = useTranslations('types.occasions');
const locale = useLocale();

return useMemo(() => OCCASIONS.map((o) => ({
...o,
label: t(o.value),
})), [t, locale]);
@gzhang33
Copy link
Copy Markdown
Author

Closing in favor of #79 which is a standalone i18n PR (no dependency on other PRs).

@gzhang33 gzhang33 closed this May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants