Admin Panel Locale Management#
The Strapi admin panel manages UI language through a layered system: a Redux slice holds the active locale, a React Intl provider reads from it to translate strings, and DOM mutations keep the HTML lang attribute in sync. Several recent bug fixes harden this pipeline against race conditions and performance regressions.
Two locales, one admin: The admin UI locale (which language the interface renders in) is stored in Redux and
localStorage. The content i18n locale (which locale's content is being edited) is stored in the URL query string as?plugins[i18n][locale]=<tag>. These are independent and must not be confused.
Redux State: admin_app.language#
The locale is stored in the admin Redux slice (exposed as admin_app) in reducer.ts. The relevant shape is:
language: {
locale: string; // active BCP-47 tag, e.g. "fr"
localeNames: Record<string, string>;
}
The setLocale action does three things atomically:
- Updates
state.language.localein Redux. - Writes the value to
localStorageunder the keystrapi-admin-language. - Sets
document.documentElement.langvia DOM mutation.
Initialization sequence:
- App startup:
StrapiApp.render()readslocalStorage(LANGUAGE_LOCAL_STORAGE_KEY), normalizes the value withnormalizeAdminLocale(), and seeds the ReduxinitialStatebefore any React render.App.tsxmirrors this by settingdocument.documentElement.langin a one-timeuseEffect. - Post-login:
Auth.tsxdispatchessetLocale(normalizeAdminLocale(user.preferedLanguage))whenever user data is loaded, overriding the localStorage default with the per-user preference .
React Intl Layer: LanguageProvider#
LanguageProvider is a thin wrapper around React Intl's IntlProvider. It reads state.admin_app.language.locale via useTypedSelector and deep-merges translations: defaultsDeep(messages[locale], messages.en), so any untranslated key falls back to English.
A onError handler silences MISSING_TRANSLATION errors . Without it, content types with many fields in a non-English locale flood the event loop with hundreds of thrown errors per keystroke, causing 1–3 second typing delays in the Content Manager. Non-MISSING_TRANSLATION errors are still surfaced via console.error.
i18n Locale in Content Manager URLs#
The i18n plugin stores the active content locale in the URL query string as ?plugins[i18n][locale]=<tag>. This is separate from the admin UI locale held in Redux.
Navigation race condition (PR #26167): LeftMenu.tsx previously only set pathname on SubNav.Link targets. The i18n plugin's useEffect re-added the locale query param after mount, leaving one render cycle where locale=undefined — enough to trigger a TypeError: Cannot read properties of undefined (reading 'attributes') crash . The fix reads useLocation().search and forwards the current plugins[i18n][locale] param directly in the to.search property of each SubNav.Link.
Component Schema Race Condition#
useDocumentLayout builds edit layouts by resolving component schemas from the RTK Query cache. During content-type navigation, the content-type configuration response (useGetContentTypeConfigurationQuery) can resolve before the new content type's component schemas are in cache. Accessing components[uid].attributes on an undefined entry throws the same TypeError .
The fix in useDocumentLayout.ts and ComponentConfigurationPage.tsx adds an early-return guard inside the Object.entries(data.components).reduce(...) loop: if components[uid] is absent, the entry is skipped and the useMemo re-runs once schemas load.
Stale Admin Configuration Guard (PR #26625)#
A broader hardening pass addresses the same class of crash — Cannot read properties of undefined — triggered by persisted Content Manager layouts that reference deleted fields, missing metadata, or invalid mainField values. Key additions:
- A
normalizeContentManagerLayoututility strips stale layout entries before they reach render paths. - Guards added in
Filters.tsx,Media.tsx,DynamicComponent.tsx, anduseDocumentLayout.tsfor missing attributes, MIME types, and component UIDs. useMenu.ts/useSettingsMenu.tsnow guard against missing admin menu/settings arrays.
The fix is intentionally non-mutating — invalid entries are dropped at render time, not written back to the persisted configuration.
Key Files#
| File | Role |
|---|---|
packages/core/admin/admin/src/reducer.ts | setLocale action; LANGUAGE_LOCAL_STORAGE_KEY constant |
packages/core/admin/admin/src/components/LanguageProvider.tsx | IntlProvider wrapper; reads Redux locale, merges translations |
packages/core/admin/admin/src/App.tsx | Sets document.documentElement.lang on mount |
packages/core/admin/admin/src/features/Auth.tsx | Dispatches setLocale post-login from user.preferedLanguage |
packages/core/content-manager/admin/src/components/LeftMenu.tsx | Preserves i18n locale in SubNav.Link navigation |
packages/core/content-manager/admin/src/hooks/useDocumentLayout.ts | Guards against undefined component schema during layout build |
packages/core/content-manager/admin/src/utils/layouts/normalizeContentManagerLayout.ts | Normalizes stale layout data before render |