# Switch the locale

Switch the locale at runtime, set a fallback chain, remember the user's choice, react to the change outside the component tree, and refresh loaded catalogs.

Canonical: https://comvi.io/docs/i18n/language-switching

Switch the locale at runtime and every component that reads the instance re-renders. You need
the instance from the [Quick start](/docs/i18n/quick-start) with a second locale in its catalog.

## Switch the locale
**React**

`src/LanguageSwitcher.tsx`

```tsx
import { useI18n } from '@comvi/react';

const languages = ['en', 'de', 'fr', 'ja'];

export function LanguageSwitcher() {
    const { locale, setLocale, isLoading } = useI18n();
    return (
      <select value={locale} onChange={(e) => setLocale(e.currentTarget.value)} disabled={isLoading}>
        {languages.map((lang) => (
          <option key={lang} value={lang}>
            {lang}
          </option>
        ))}
      </select>
    );
}
```

**Vue**

`src/LanguageSwitcher.vue`

```vue
<script setup lang="ts">
import { useI18n } from '@comvi/vue';

const languages = ['en', 'de', 'fr', 'ja'];
const { locale, setLocale, isLoading } = useI18n();
</script>

<template>
    <select :value="locale" :disabled="isLoading" @change="setLocale(($event.target as HTMLSelectElement).value)">
      <option v-for="lang in languages" :key="lang" :value="lang">{{ lang }}</option>
    </select>
</template>
```

**Nuxt**

`nuxt.config.ts`

```ts
import { defineNuxtConfig } from 'nuxt/config';

export default defineNuxtConfig({
    modules: ['@comvi/nuxt'],
    comvi: { defaultLocale: 'en', locales: [{ code: 'en', name: 'English' }, { code: 'de', name: 'Deutsch' }] },
});
```

`app/components/LanguageSwitcher.vue`

```vue
<script setup lang="ts">
const { locale, locales } = useI18n();
const switchLocalePath = useSwitchLocalePath();
</script>

<template>
    <nav>
      <NuxtLink v-for="lang in locales" :key="lang" :to="switchLocalePath(lang)" :aria-current="lang === locale ? 'page' : undefined">{{ lang }}</NuxtLink>
    </nav>
</template>
```

**Svelte**

`src/LanguageSwitcher.svelte`

```svelte
<script lang="ts">
    import { useI18n } from '@comvi/svelte';

    const languages = ['en', 'de', 'fr', 'ja'];
    const { locale, setLocale, isLoading } = useI18n();
</script>

<select value={$locale} disabled={$isLoading} onchange={(e) => setLocale(e.currentTarget.value)}>
    {#each languages as lang (lang)}
      <option value={lang}>{lang}</option>
    {/each}
</select>
```

**Solid**

`src/LanguageSwitcher.tsx`

```tsx
import { For } from 'solid-js';
import { useI18n } from '@comvi/solid';

const languages = ['en', 'de', 'fr', 'ja'];

export function LanguageSwitcher() {
    const { locale, setLocale, isLoading } = useI18n();
    return (
      <select value={locale()} disabled={isLoading()} onChange={(e) => setLocale(e.currentTarget.value)}>
        <For each={languages}>{(lang) => <option value={lang}>{lang}</option>}</For>
      </select>
    );
}
```

**Vanilla**

`src/language-switcher.ts`

```ts
import { createI18n } from '@comvi/core';

const i18n = createI18n({ locale: 'en', fallbackLocale: 'en' });

const select = document.querySelector<HTMLSelectElement>('#language-switcher')!;
select.addEventListener('change', () => {
    select.disabled = true;
    void i18n.setLocale(select.value).finally(() => {
      select.disabled = false;
    });
});
```

`setLocale()` in a binding calls the instance's `setLocale()`, which returns a promise; the
`locale` setter is the same call with the promise dropped. Both emit `localeChanged`:

`src/switch.ts`

```ts

i18n.locale = 'fr'; // fire and forget
await i18n.setLocale('fr'); // resolves once the locale is applied
```

Disabling the selector while `isLoading` is true keeps a second switch from starting mid-load.
In Nuxt the URL carries the locale, so the switcher navigates to `useSwitchLocalePath()(code)`
instead of calling `setLocale()`; the route middleware applies the locale and writes the cookie.
`setLocale()` alone switches in place and is enough only under `localePrefix: 'never'`.
On an instance with no loader there is nothing to fetch: the locale is applied at once from the
catalogs already in the cache, and `await` returns immediately.

## Fallback chain

`src/i18n.ts`

```ts

export const i18n = createI18n({
locale: 'de',
fallbackLocale: 'en',
});

i18n.setFallbackLocale(['de-AT', 'de', 'en']); // tried in order
```

A key missing from the current locale is looked up along the chain before `missingKey` fires.
The order is fixed: the current locale's catalog for the namespace first, then each entry of
`fallbackLocale` in array order, always in the same namespace. Put your most complete locale
last so every key has a terminal answer. What happens after the chain is exhausted is on
[Translate text with t()](/docs/i18n/translation-function#missing-keys).

## Persist the choice

`src/i18n.ts`

```ts

export const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
})
.use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }))
.use(
    localeDetector({
      supportedLocales: ['en', 'de', 'fr', 'ja'],
      order: ['localStorage', 'navigator'],
      caches: ['localStorage'],
    }),
);
```

Without the plugin, a reload starts from the configured `locale`. With it, the first visit
reads `navigator.languages`, a switch writes the choice to `localStorage`, and the next visit
starts from the saved value. List the locales you ship in `supportedLocales`, so a browser
language outside the list falls back to `locale` instead of becoming the active one; without the
list, development warns. Every source and cache is on
[Locale detector](/docs/i18n/plugins/locale-detector). Putting the locale in the URL is a
framework concern: [Next.js routing](/docs/i18n/guides/next-routing) and the
[Nuxt module](/docs/i18n/nuxt) each ship it.

## React to a change
**React**

`src/HtmlAttrs.tsx`

```tsx
import { useEffect } from 'react';
import { useI18n } from '@comvi/react';

export function HtmlAttrs() {
    const { dir, locale } = useI18n();
    useEffect(() => {
      document.documentElement.dir = dir;
      document.documentElement.lang = locale;
    }, [dir, locale]);
    return null;
}
```

**Vue**

`src/HtmlAttrs.vue`

```vue
<script setup lang="ts">
import { watchEffect } from 'vue';
import { useI18n } from '@comvi/vue';

const { dir, locale } = useI18n();

watchEffect(() => {
    document.documentElement.dir = dir.value;
    document.documentElement.lang = locale.value;
});
</script>

<template><slot /></template>
```

**Nuxt**

`app/app.vue`

```vue
<template>

</template>
```

The module writes `lang` and `dir` on `<html>` itself, on the server and on every switch, so
there is no component to add. `comvi.head: false` in `nuxt.config.ts` turns that off.

**Svelte**

`src/HtmlAttrs.svelte`

```svelte
<script lang="ts">
    import { useI18n } from '@comvi/svelte';

    const { dir, locale } = useI18n();

    $effect(() => {
      document.documentElement.dir = $dir;
      document.documentElement.lang = $locale;
    });
</script>
```

**Solid**

`src/HtmlAttrs.tsx`

```tsx
import { createEffect } from 'solid-js';
import { useI18n } from '@comvi/solid';

export function HtmlAttrs() {
    const { dir, locale } = useI18n();
    createEffect(() => {
      document.documentElement.dir = dir();
      document.documentElement.lang = locale();
    });
    return null;
}
```

The bindings re-render the component tree on their own. No binding writes `lang` or `dir` on
`<html>` for you, apart from the Nuxt module, so a small `HtmlAttrs` component that
writes both in an effect is where that happens. For side effects outside the tree, such as
analytics or a refetch, subscribe on the instance:

`src/locale-events.ts`

```ts

declare const analytics: { track(event: string, payload: Record<string, string>): void };

const unsubscribe = i18n.on('localeChanged', ({ from, to }) => {
document.documentElement.dir = getTextDirection(to);
document.documentElement.lang = to;
analytics.track('language_changed', { from, to });
});

unsubscribe();
```

`getTextDirection()` reads `Intl.Locale` where the runtime has it, so it covers script subtags
such as `ku-Arab`, region tags such as `he-IL` and every CLDR right-to-left locale that a
hand-written list would miss. With CSS logical properties such as `margin-inline-start`, most
layouts need no RTL-specific rules.

## Refresh translations
**React**

`src/RefreshButton.tsx`

```tsx
import { useI18nLoader } from '@comvi/react';

export function RefreshButton() {
    const { reloadMessages } = useI18nLoader();
    return <button onClick={() => reloadMessages()}>Refresh translations</button>;
}
```

**Vue**

`src/RefreshButton.vue`

```vue
<script setup lang="ts">
import { useI18nLoader } from '@comvi/vue';

const { reloadMessages } = useI18nLoader();
</script>

<template>
    <button @click="reloadMessages()">Refresh translations</button>
</template>
```

**Nuxt**

`app/components/RefreshButton.vue`

```vue
<script setup lang="ts">
const { reloadMessages } = useI18nLoader();
</script>

<template>
    <button @click="reloadMessages()">Refresh translations</button>
</template>
```

**Svelte**

`src/RefreshButton.svelte`

```svelte
<script lang="ts">
    import { useI18nLoader } from '@comvi/svelte';

    const { reloadMessages } = useI18nLoader();
</script>

<button onclick={() => reloadMessages()}>Refresh translations</button>
```

**Solid**

`src/RefreshButton.tsx`

```tsx
import { useI18nLoader } from '@comvi/solid';

export function RefreshButton() {
    const { reloadMessages } = useI18nLoader();
    return <button onClick={() => reloadMessages()}>Refresh translations</button>;
}
```

**Vanilla**

`src/refresh.ts`

```ts
import { createI18n } from '@comvi/core';
import { cdnLoader } from '@comvi/plugin-fetch-loader';

const i18n = createI18n({ locale: 'en' }).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }));
await i18n.init();

await i18n.reloadMessages(); // current locale and its fallback chain
await i18n.reloadMessages('de', 'dashboard'); // one namespace
```

`reloadMessages()` drops the cache entries it targets and fetches them again through the
loader: with no arguments the current locale and its fallback chain, with `'de'` every active
namespace for German, with `'de', 'dashboard'` one namespace. It lives on the loader
capability, `useI18nLoader()` in a component or the composed instance outside one; a component
reaching for it on an instance with no loader throws. Calling it twice fetches twice, so use it
for invalidation rather than as a preload.

## How it works

`setLocale()` from `useI18n()` is the instance's `setLocale()`, and `i18n.locale = 'fr'` is
the same call with its promise dropped: a failed load then surfaces as a `loadError` event
with the namespace `locale-change` instead of a rejection, so prefer `await setLocale()`
where the failure matters.

What the switch waits for depends on what is composed. On an instance with a loader, `.use(loader(map))`
or `.use(cdnLoader({ cdnUrl }))`, `setLocale('de')` runs in a fixed order. It marks the instance as
loading, so `isLoading` turns `true` and `loadingStateChanged` fires. It fetches `de` for every
active namespace that is not already in the cache, in parallel, and waits for all of them. Only
then does it write the locale and emit `localeChanged`, so no component ever renders the new
locale against an old catalog and nothing flashes untranslated. Finally it clears the loading
state. When every active namespace of the target locale is already cached, nothing loads, `isLoading`
never turns `true`, and the locale is written synchronously, inside the call: each loaded `(locale, namespace)` pair
stays cached until `clearMessages()`, `reloadMessages()` or `destroy()`, so a locale switched
back to applies at once. On an instance with no loader the whole sequence collapses to the last step:
the locale is written and `localeChanged` fires synchronously.

A switch that a newer switch overtakes applies nothing and reports nothing. Each call takes a
change id, and when it resumes after its fetch it checks that it is still the latest; if the
user clicked `fr` and then `ja` while `fr` was loading, the `fr` result lands in the cache but
the locale goes straight to `ja`. Reverting to the current locale while another switch is in
flight cancels that switch the same way. Only the last requested locale is ever observable,
so the selector can stay enabled.

Every binding listens to the same revision events, `localeChanged` and `namespaceLoaded` among
them, and turns them into its own reactivity. In React the provider holds one external-store
subscription, so every component that called `useI18n()` re-renders together and a component
that never did is untouched. In Vue the locale, the loading flags and the cache revision are
shallow refs, so a template re-renders only where it read `t`, `locale` or `dir`. Svelte exposes
them as stores, which is why the code reads `$locale` and `$isLoading`. Solid exposes signals,
`locale()` and `isLoading()`, and tracks them like any other signal. In each case the bound
formatters and `dir` derive from the same locale value, so they follow in the same render.

`setFallbackLocale()` emits `configChanged`, which is a revision event too, so the subscribed
components re-render with the new chain. A hit found in a fallback locale is compiled with the
rules of the locale it was found in, so a plural served from the English catalog uses English
categories.

Nothing on the instance persists the choice; a reload starts from the configured `locale`.
The locale detector is a plugin that runs during `init()`, before any namespace is fetched, so
the detected locale is the one the first load fetches. It reads the sources in `order` and
stops at the first one that returns a value, writes that detection to each target in `caches`
once `init()` succeeds, and afterwards listens for `localeChanged` and writes each new locale
the same way, so the next visit starts from the user's choice. Every source skips on the server, where `window` and `navigator` do not
exist, so the instance keeps its configured locale there.

## Read next

- [Locale detector](/docs/i18n/plugins/locale-detector): detect the locale from the browser, a cookie, the URL or a query string.
- [Load translations on demand](/docs/i18n/loading-translations): loaders, caching and the loading state.
- [Add a locale end to end](/docs/i18n/guides/add-a-locale): the platform side of a new language.
