Locales
packages/i18n/config.ts:
Other settings:
defaultLocale: "he", defaultCurrency: "ILS", localeCookieName: "NEXT_LOCALE", localeCookieMaxAge one year.
getLocaleDirection(locale) returns rtl for he and ar, otherwise ltr.
The workspace
CLAUDE.md and the frontend README list five locales. The code has six. Arabic was added later.Where strings live
Top level namespaces in
saas.json include app, auth, common, errors, pages, settings, organizations, clients, clientCard, workboard, automatedTasks, flowBuilder, checkin, trainingPrograms, nutritionPrograms, templates, formBuilder, forms, formSend, sign, content, homeBanner, calendar, crm, exercises, foods, plans, planLimit, choosePlan, assistant, start and admin.
Conventions worth knowing:
app.menu.*holds the sidebar labels.pages.<area>.titleandpages.<area>.subtitlehold page headers.common.*holds shared strings:common.confirmation,common.pagination,common.unsavedChanges.errors.title,errors.descriptionanderrors.retryare the generic load error.
Coverage
Counted from the files at the time of writing:Loading messages
apps/saas/modules/i18n/request.ts is the next-intl request config, wired in next.config.ts with nextIntlPlugin("./modules/i18n/request.ts").
getMessagesForLocale(locale, "saas") in packages/i18n/lib/get-messages.ts:
- Imports
{locale}/saas.jsonand{locale}/shared.jsonand deep merges them. - When the locale is not the default, loads the default locale’s files and merges the locale on top.
NextIntlClientProvider, so client components receive the whole catalog.
Using translations
Type checking
apps/saas/intl.d.ts declares:
SaasMessages is typeof the English saas.json merged with shared.json. A key that does not exist in the English file is a type error. English is the source of truth for types, Hebrew for runtime fallback. Keep both complete.
Fragment files
28 files named_*-i18n-keys.json sit next to feature code, for example workboard/_task-context-shell-i18n-keys.json and forms/ai/_forms-ai-i18n-keys.json. Each maps a full key to its he and en text. Comments in workboard/panel-i18n.ts and forms/ai/forms-ai-shared.tsx explain them: the keys ship as fragments with the feature and are merged into packages/i18n at release time.
Until a fragment is merged, the typed t does not know its keys. Those features use a loosened signature:
Adding a key
1
Add it to English
Edit
packages/i18n/translations/en/saas.json. Put it in the namespace of the feature. This makes the key valid for TypeScript.2
Add it to Hebrew
Edit
he/saas.json at the same path. Hebrew is the default locale and the runtime fallback, so a key missing here renders as the raw key for most users.3
Add the other locales when you can
ar, de, es and fr fall back to Hebrew for anything missing.4
Use it
Call
t("namespace.key"). Do not hardcode user facing strings in components.Hardcoded Hebrew does exist in a few standalone surfaces: the
/start wizard, the plan picker’s tier names in plan-picker.ts, and the static privacy and support pages. They are branded flows written for the Israeli market. Follow the translated pattern for anything inside the dashboard.Switching locale
The cookie action
modules/i18n/lib/update-locale.ts:
maxAge is what makes the choice survive a browser restart. Without it the cookie would be a session cookie.
The three switches
UserMenuLanguage is only rendered when canSwitchLanguage(email) is true. That function checks the signed in email against config.languageSwitchEmails, a comma separated list from NEXT_PUBLIC_LANGUAGE_SWITCH_EMAILS (matching is case insensitive). The settings form and the auth page switch are not gated.
Each switch keeps the selected value in local state, seeded from useLocale(), so the radio shows the new choice while the refresh is in flight.
All three return null when only one locale is configured.
HtmlLocaleSync
body in the root layout. The server already renders lang and dir on html. But router.refresh() re-renders server components without replacing the html element’s attributes, so after a switch the page would keep the old direction until a full reload. This effect applies the new values as soon as the refreshed tree arrives.
The saved user.locale is written by the switches. Nothing in the web app reads it back to set the cookie on sign in, so on a new browser the UI starts in the cookie’s locale (Hebrew when unset) until the user switches.
Formatting
- Currency:
useLocaleCurrency()inmodules/shared/hooks/locale-currency.tsxreturns the locale’s currency, which isILSfor all six. - Dates and numbers: use next-intl’s
useFormatter()(the admin pages useformat.number), or date-fns for date math. - The plan picker formats prices with its own
formatIls.