# Translations

> App strings for the Nasaq locale: TranslationsProvider and useT() give t(key, vars) over your dictionaries, with nested and namespaced keys, interpolation, CLDR plurals, locale fallback, a remembered choice and seedLocale for server defaults.

Source: https://docs.nasaqui.com/components/translations

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/translations.json
```

A small i18n layer for your own strings that shares one locale with Nasaq. `TranslationsProvider` reads and
drives `NasaqProvider`'s locale, so switching language also flips direction and the built-in component strings.
`useT()` returns `t(key, vars)` over dictionaries you pass in. No i18n library is needed.

## When to use

- An app built on Nasaq that needs its own strings in English, Arabic or more.
- When the server suggests a language (an account setting, `Accept-Language`) that should apply only until the
  reader picks one: `seedLocale`.

## When not to use

- Nasaq components' own strings: they already follow the locale; pass `labels` to change them.
- An app already on i18next or FormatJS: keep it, and sync its language with `useNasaq().setLocale`.

## Import

```tsx
import { TranslationsProvider, useT } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
const messages = {
  en: { nav: { home: "Home" }, inbox_one: "{count} message", inbox_other: "{count} messages" },
  ar: { nav: { home: "الرئيسية" }, inbox_zero: "لا رسائل", inbox_one: "رسالة واحدة", inbox_two: "رسالتان", inbox_few: "{count} رسائل", inbox_many: "{count} رسالة", inbox_other: "{count} رسالة" },
};

<NasaqProvider>
  <TranslationsProvider messages={messages}>
    <App />
  </TranslationsProvider>
</NasaqProvider>;

function Nav({ unread }: { unread: number }) {
  const { t, setLocale } = useT();
  return (
    <>
      <a href="/">{t("nav.home")}</a> · {t("inbox", { count: unread })}
      <button onClick={() => setLocale("ar")}>العربية</button>
    </>
  );
}
```

## Anatomy

```
TranslationsProvider   context only, renders no element
└─ useT()              { t, locale, setLocale, seedLocale, hasStoredLocale }
```

## API

### `TranslationsProvider`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `messages` | `MessageBundle` | required | `{ [locale]: Messages }`. Nested objects or flat `"a.b"` keys. |
| `fallbackLocale?` | `string` | `"en"` | Searched when a key is missing. |
| `storageKey?` | `string \| null` | `"nasaq-locale"` | Where the reader's choice is kept in `localStorage`; restored on mount. `null` turns it off. |
| `locale?` | `string` | `"en"` | Only used without a `NasaqProvider` above. |

### `t(key, vars?)`

- **Keys**: `nav.home`, `nav:home` (namespace), or a flat key containing dots. A flat key wins.
- **Interpolation**: `{name}` and `{{name}}`. Numbers are formatted for the locale (Arabic digits in `ar`).
- **Plurals**: with a numeric `count`, `key_zero` (for 0), then the CLDR form (`_one`, `_two`, `_few`, `_many`) and
  `key_other` are tried, then `key`.
- **Fallback**: `ar-EG`, `ar`, the fallback locale, then `vars.defaultValue`, then the key itself.

### `useT()`

| Field | Description |
| --- | --- |
| `t` | The translator for the active locale. |
| `locale` | The active locale. |
| `setLocale(l)` | Switches and remembers the choice. |
| `seedLocale(l)` | Switches only if nothing is stored, and does not store it. |
| `hasStoredLocale()` | Whether the reader has made a choice. |

`useOptionalT()` returns `null` outside a provider. `createTranslator(bundle, locale, fallback)` is the same `t`
without React, for servers and tests.

## Accessibility

- `NasaqProvider` sets `lang` and `dir` from the locale, so screen readers switch voice and the layout mirrors.
- Keep whole sentences as one key so translators can reorder words.

## RTL & i18n

Arabic has six plural forms; give at least `_zero`, `_one`, `_two`, `_few`, `_many` and `_other`. Missing forms
fall back to `_other`.

## Styling & tokens

None: this is logic only.

## Do / Don't

- Do use `seedLocale` for server defaults so a reader's own choice is never overwritten.
- Don't build sentences from fragments (`t("you_have") + n + t("messages")`); use one key with `{count}`.

## Related

- [Switchers](https://docs.nasaqui.com/components/switchers)
- [LangTag](https://docs.nasaqui.com/components/lang-tag)

## Lab

https://docs.nasaqui.com/?path=/docs/components-utilities-translations--docs

## Code

### React

```tsx
const messages = {
  en: { nav: { home: "Home" }, inbox_one: "{count} message", inbox_other: "{count} messages" },
  ar: { nav: { home: "الرئيسية" }, inbox_zero: "لا رسائل", inbox_one: "رسالة واحدة", inbox_two: "رسالتان", inbox_few: "{count} رسائل", inbox_many: "{count} رسالة", inbox_other: "{count} رسالة" },
};

<NasaqProvider>
  <TranslationsProvider messages={messages}>
    <App />
  </TranslationsProvider>
</NasaqProvider>;

function Nav({ unread }: { unread: number }) {
  const { t, setLocale } = useT();
  return (
    <>
      <a href="/">{t("nav.home")}</a> · {t("inbox", { count: unread })}
      <button onClick={() => setLocale("ar")}>العربية</button>
    </>
  );
}
```

### shadcn

```tsx
const messages = {
  en: { nav: { home: "Home" }, inbox_one: "{count} message", inbox_other: "{count} messages" },
  ar: { nav: { home: "الرئيسية" }, inbox_zero: "لا رسائل", inbox_one: "رسالة واحدة", inbox_two: "رسالتان", inbox_few: "{count} رسائل", inbox_many: "{count} رسالة", inbox_other: "{count} رسالة" },
};

<NasaqProvider>
  <TranslationsProvider messages={messages}>
    <App />
  </TranslationsProvider>
</NasaqProvider>;

function Nav({ unread }: { unread: number }) {
  const { t, setLocale } = useT();
  return (
    <>
      <a href="/">{t("nav.home")}</a> · {t("inbox", { count: unread })}
      <button onClick={() => setLocale("ar")}>العربية</button>
    </>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NasaqProvider, NqButton, NqTranslationsProvider, useTranslations } from "@fadymondy/nasaq/vue";
import { defineComponent, h } from "vue";

const messages = {
  en: { nav: { home: "Home" }, inbox_one: "{count} message", inbox_other: "{count} messages" },
  ar: {
    nav: { home: "الرئيسية" },
    inbox_zero: "لا رسائل",
    inbox_one: "رسالة واحدة",
    inbox_two: "رسالتان",
    inbox_few: "{count} رسائل",
    inbox_many: "{count} رسالة",
    inbox_other: "{count} رسالة",
  },
};

// A child reads `t` and `setLocale` from the nearest provider.
const Nav = defineComponent({
  props: { unread: { type: Number, required: true } },
  setup(props) {
    const tr = useTranslations();
    return () =>
      h("div", { class: "flex items-center gap-3" }, [
        h("a", { href: "/" }, tr.value.t("nav.home")),
        h("span", tr.value.t("inbox", { count: props.unread })),
        h(NqButton, { variant: "outline", size: "sm", onClick: () => tr.value.setLocale(tr.value.locale === "ar" ? "en" : "ar") }, () => "العربية / English"),
      ]);
  },
});
</script>

<template>
  <NasaqProvider target="scope">
    <NqTranslationsProvider :messages="messages" :storage-key="null">
      <Nav :unread="3" />
    </NqTranslationsProvider>
  </NasaqProvider>
</template>
```

### Blade

```blade
<x-nq::translations.provider :messages="[
    'en' => ['greeting' => 'Hello, {name}', 'cart' => ['items_one' => '{count} item in your cart', 'items_other' => '{count} items in your cart']],
    'ar' => ['greeting' => 'مرحبا، {name}', 'cart' => ['items_zero' => 'سلتك فارغة', 'items_one' => 'منتج واحد في سلتك', 'items_two' => 'منتجان في سلتك', 'items_few' => '{count} منتجات في سلتك', 'items_many' => '{count} منتجا في سلتك', 'items_other' => '{count} منتج في سلتك']],
]">
    <div class="flex flex-col gap-2">
        <p class="text-title"><x-nq::translations.text key="greeting" :vars="['name' => 'Sara']" /></p>
        <p><x-nq::translations.text key="cart.items" :vars="['count' => 1]" /></p>
        <p><x-nq::translations.text key="cart.items" :vars="['count' => 3]" /></p>
        <p><x-nq::translations.text key="missing.key" :vars="['defaultValue' => 'Fallback text']" /></p>
        <div class="flex gap-2">
            <x-nq::button variant="outline" size="sm" x-on:click="setLocale(`en`)">English</x-nq::button>
            <x-nq::button variant="outline" size="sm" x-on:click="setLocale(`ar`)">العربية</x-nq::button>
        </div>
    </div>
</x-nq::translations.provider>
```

### HTML + Alpine

```html
<div data-slot="translations-provider" x-data="nqTranslations(JSON.parse('{\u0022en\u0022:{\u0022greeting\u0022:\u0022Hello, {name}\u0022,\u0022cart\u0022:{\u0022items_one\u0022:\u0022{count} item in your cart\u0022,\u0022items_other\u0022:\u0022{count} items in your cart\u0022}},\u0022ar\u0022:{\u0022greeting\u0022:\u0022\\u0645\\u0631\\u062d\\u0628\\u0627\\u060c {name}\u0022,\u0022cart\u0022:{\u0022items_zero\u0022:\u0022\\u0633\\u0644\\u062a\\u0643 \\u0641\\u0627\\u0631\\u063a\\u0629\u0022,\u0022items_one\u0022:\u0022\\u0645\\u0646\\u062a\\u062c \\u0648\\u0627\\u062d\\u062f \\u0641\\u064a \\u0633\\u0644\\u062a\\u0643\u0022,\u0022items_two\u0022:\u0022\\u0645\\u0646\\u062a\\u062c\\u0627\\u0646 \\u0641\\u064a \\u0633\\u0644\\u062a\\u0643\u0022,\u0022items_few\u0022:\u0022{count} \\u0645\\u0646\\u062a\\u062c\\u0627\\u062a \\u0641\\u064a \\u0633\\u0644\\u062a\\u0643\u0022,\u0022items_many\u0022:\u0022{count} \\u0645\\u0646\\u062a\\u062c\\u0627 \\u0641\\u064a \\u0633\\u0644\\u062a\\u0643\u0022,\u0022items_other\u0022:\u0022{count} \\u0645\\u0646\\u062a\\u062c \\u0641\\u064a \\u0633\\u0644\\u062a\\u0643\u0022}}}'), JSON.parse('{\u0022fallbackLocale\u0022:\u0022en\u0022,\u0022storageKey\u0022:\u0022nasaq-locale\u0022}'))" class="contents"><div class="flex flex-col gap-2">
        <p class="text-title"><span data-slot="translations-text" x-text="t('greeting', JSON.parse('{\u0022name\u0022:\u0022Sara\u0022}'))" >Hello, Sara</span>
</p>
        <p><span data-slot="translations-text" x-text="t('cart.items', JSON.parse('{\u0022count\u0022:1}'))" >1 item in your cart</span>
</p>
        <p><span data-slot="translations-text" x-text="t('cart.items', JSON.parse('{\u0022count\u0022:3}'))" >3 items in your cart</span>
</p>
        <p><span data-slot="translations-text" x-text="t('missing.key', JSON.parse('{\u0022defaultValue\u0022:\u0022Fallback text\u0022}'))" >Fallback text</span>
</p>
        <div class="flex gap-2">
            <button data-slot="button"
     type="button"                         x-on:click="setLocale(`en`)" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        English</button>
            <button data-slot="button"
     type="button"                         x-on:click="setLocale(`ar`)" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        العربية</button>
        </div>
    </div></div>
```
