Advanced
Internationalization
The library ships no translations and no i18n dependency. What it ships is the seam: one provider that renames the handful of strings it renders on its own, and sets the writing direction for everything — including the overlays that render through portals.
Labels
Most user-facing text is yours already — it arrives as children or as a prop. What is left is the text the library writes itself: the drawer’s landmark name, the rail’s expand and collapse buttons, the header’s menu toggle, the breadcrumb trail’s name, a search placeholder, an overflow badge. Override any subset; unspecified keys keep their English default, so adding the provider never blanks a string.
import { I18nProvider, AppShell } from "appshell-react";
// Override any subset; the rest keep their English defaults.
<I18nProvider
labels={{
navigationMenu: "Menu de navigation",
search: "Rechercher",
cancel: "Annuler",
notificationsUnread: "Notifications ({count} non lues)",
}}
>
<AppShell safeArea>{children}</AppShell>
</I18nProvider>Values interpolate {token} placeholders: notificationsUnread gets {count} and badgeOverflow gets {max}. Precedence runs component prop → provider → English default, so a one-off aria-label still wins locally. The full English dictionary is exported as defaultLabels — read it for the complete key list, or spread it yourself when building a translated copy from only a partial catalogue.
Bringing your own i18n library
Pass t instead of a dictionary and the provider asks your catalogue for every key. It hands you the key, the English default, and any interpolation variables — so nothing breaks on the day a key is still missing.
// react-i18next / react-intl / Lingui — hand the provider your
// translate function. It receives the English default, so it works
// before the keys exist in your catalogue.
const { t } = useTranslation();
<I18nProvider
t={(key, defaultValue, vars) =>
t(`appshell.${key}`, { defaultValue, ...vars })
}
>
<App />
</I18nProvider>Compiled-message libraries such as Paraglide have no key-based t at all; for those, build the dictionary instead. Either door works, and a server component can resolve the strings before they reach the client:
// next-intl: resolve on the server, hand plain strings to the client.
// This is why label values are strings and not functions — functions
// would not cross the server/client boundary.
import { getTranslations } from "next-intl/server";
export default async function Layout({ children }) {
const t = await getTranslations("appshell");
return (
<I18nProvider
dir={t("dir") === "rtl" ? "rtl" : "ltr"}
labels={{ navigationMenu: t("navigationMenu"), search: t("search") }}
>
{children}
</I18nProvider>
);
}Writing direction
Direction is an explicit dir prop rather than something inferred from a locale — the same choice Radix, Base UI and Mantine make. It matters more here than in most libraries: the SearchModal, BottomSheet and the anchored menus render through portals, and a portal does not inherit dir from its React parent. The provider threads direction through context and sets it on each portal root, so an RTL layout stays RTL inside every overlay.
dir="rtl" mirrors padding, margins and badge offsets without extra work. The Sidebar’s side defaults to "start", so the drawer opens from the leading edge automatically under dir="rtl" — pass the physical "left"/"right" values instead when a docked panel’s side is a layout decision rather than a language one.API
| Prop | Type | Default | Description |
|---|---|---|---|
| labels | Partial<AppShellLabels> | — | Overrides for the library's own strings. Any subset; unspecified keys keep their English default. Values interpolate {token} placeholders, e.g. "Notifications ({count} unread)". |
| t | (key, defaultValue, vars?) => string | — | Resolve every key through your own i18n library instead. Receives the English default, so it degrades gracefully before the keys exist in your catalogue. Takes precedence over labels; a component's own prop still wins over both. |
| dir | "ltr" | "rtl" | "ltr" | Writing direction for the shell. Explicit rather than inferred from a locale, and delivered through context — portalled overlays (SearchModal, BottomSheet, menus) cannot inherit dir from the DOM. |
| children* | ReactNode | — | The app. |