Internationalization (i18n)
MangoStudio uses a pure TypeScript i18n system with compile-time translation verification.
Architecture
apps/shared/src/i18n/
pt-BR.ts → Source of truth (all keys defined here)
en.ts → English translations (typed against Messages)
types.ts → Messages type + Locale union
apps/frontend/src/lib/
locale-dictionaries.ts → English eager, every other locale loaded as its own chunk
apps/frontend/src/hooks/
use-i18n.tsx → React Context hook over the loaded dictionary
Key Convention
Keys use dot-separated notation organized by feature scope:
auth.loginTitle
chat.input.placeholder
settings.providers.caching.title
tools.generate_image.title
The Messages type is inferred from the pt-BR.ts dictionary using as const. Adding a key to pt-BR without translating it in en.ts is a compile-time error because en.ts is typed as Messages.
How It Works
Type System
types.ts defines:
type DeepStringRecord<T> = {
[K in keyof T]: T[K] extends string
? string
: T[K] extends object
? DeepStringRecord<T[K]>
: never;
};
type Messages = DeepStringRecord<typeof ptBR>;
type Locale = 'pt-BR' | 'en';
This ensures that en.ts must have all the same keys as pt-BR.ts but values can be different strings.
React Hook
useI18n() returns { t, locale, setLocale }:
import { useI18n } from '@/hooks/use-i18n';
function MyComponent() {
const { t } = useI18n();
return <h1>{t.auth.loginTitle}</h1>;
}
- Locale detection: Checks
localStorage(mangostudio:locale) first, thennavigator.language, defaults toen. - Lazy loading: English is bundled in the entry chunk and is the fallback. Every other locale
is its own chunk, imported through its
@mangostudio/shared/i18n/<locale>subpath bylib/locale-dictionaries.ts.main.tsxstarts loading the startup locale before the first render, alongside the session request. A runtime switch keeps the current text until the next dictionary arrives; a failed load renders English and keeps the stored choice for the next start. - Persistence:
setLocale()updates both React state andlocalStorage.
Provider
I18nProvider wraps the app root and provides the i18n context:
<I18nProvider>
<App />
</I18nProvider>
Uses React 19’s use() API for context consumption. Throws if useI18n() is called outside the provider.
Adding a New Language
- Create the dictionary file (e.g.,
apps/shared/src/i18n/es.ts). - Copy the key structure from
pt-BR.tsand translate all values. - Type the file as
Messagesto ensure structural compatibility:import type { Messages } from './types'; export const messages: Messages = { /* translations */ }; - Add the locale to the
Localetype intypes.ts:type Locale = 'pt-BR' | 'en' | 'es'; - Wire the lazy loading: a
./i18n/essubpath export inapps/shared/package.json, a loader inapps/frontend/src/lib/locale-dictionaries.ts, a branch in itsdetectLocale(), and anoRestrictedImportsentry for the subpath inbiome.json. Shared modules import a dictionary from its own file, never a runtime value from thei18nbarrel, or every dictionary lands in the entry chunk. - Add the locale option in the appearance settings UI if desired.
Adding New Keys
- Add the key to
pt-BR.tsin the appropriate section. - Add the translation to
en.tsin the same path. If you miss this step, the build will fail becauseen.tsmust satisfy theMessagestype. - Use the key via
t.path.to.keyin components.
Current Translations
| Locale | Coverage | File |
|---|---|---|
pt-BR |
Full (source of truth) | pt-BR.ts (~444 lines) |
en |
Full | en.ts (~440 lines) |
Key Sections
The dictionary is organized into these top-level sections:
auth— Login, signup, error messageschat— Chat UI, input bar, feed, context, continuation, system eventsstudio— Image workspacegallery— Gallery pagesettings— Tabs, context, prompts, general, connectors, providers, tools, appearancecommon— Shared labels (save, cancel, delete, confirm)providers— Provider display names and descriptionsthinking— Thinking/reasoning labelstools— Tool names, categories, parameter labelserrors— Error messagesapi— API status messagesgeneration— Generation-related labels