> ## Documentation Index
> Fetch the complete documentation index at: https://docs.textsetu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Using a Distribution in Your App

> Fetch a manifest and load published translations over plain HTTPS, with no SDK.

Once you've [published a distribution](/docs/guides/content-delivery), consuming it is
plain HTTPS. There is no SDK, no authentication and no handshake — just a
manifest and the files it points at.

This page covers the client side. For the manifest's exact shape and cache
headers, see the
[Content delivery manifest reference](/docs/reference/content-delivery-manifest).

***

## Quick start

```js theme={null}
// 1. Read the manifest (small, cached ~60s).
const manifest = await fetch(MANIFEST_URL).then((r) => r.json());

// 2. Pick the ONE language you need.
const file = manifest.files.find((f) => f.language === "fr");

// 3. Fetch it. Immutable and cacheable forever — the URL changes when content does.
const translations = await fetch(file.url).then((r) => r.json());
```

That's the whole contract.

<Warning>
  **Fetch one language, not all of them**

  The manifest lists every language the distribution publishes, but a screen
  renders one. Fetching them all costs N requests and N× the bytes to display the
  same page — on a 12-language project that's 12× the payload for nothing.

  Take the language from your app's locale, not from iterating `manifest.files`.
</Warning>

You never construct a file URL yourself — take `files[].url` from the manifest.
It's absolute, so a distribution published to your own bucket or CDN needs no
client change.

***

## Web apps

### i18next

Register a backend so i18next requests exactly the language it needs, when it
needs it. Do **not** preload every file into `resources`.

```js theme={null}
import i18next from "i18next";

const manifest = await fetch(MANIFEST_URL).then((r) => r.json());

await i18next.init({
  lng: "fr",
  fallbackLng: manifest.project.sourceLanguage,
  // Keeps any locally bundled baseline visible when a remote file is missing,
  // rather than rendering raw keys.
  partialBundledLanguages: true,
  backend: {},
});

i18next.services.backendConnector.backend = {
  type: "backend",
  init() {},
  read(language, namespace, callback) {
    const file = manifest.files.find((f) => f.language === language);
    if (!file) return callback(null, {});
    fetch(file.url)
      .then((r) => r.json())
      .then((data) => callback(null, data))
      .catch((err) => callback(err, null));
  },
};
```

### Split by module (i18next namespaces)

If the distribution has **Split by module** enabled, it publishes one file per
`(language, module)` instead of one per language, laid out as
`<language>/<module>.json`. Match on both:

```js theme={null}
const manifest = await fetch(MANIFEST_URL).then((r) => r.json());

await i18next.init({
  lng: "fr",
  fallbackLng: manifest.project.sourceLanguage,
  ns: [...new Set(manifest.files.map((f) => f.module))],
  defaultNS: "common",
  partialBundledLanguages: true,
  backend: {},
});

i18next.services.backendConnector.backend = {
  type: "backend",
  init() {},
  read(language, namespace, callback) {
    const file = manifest.files.find(
      (f) => f.language === language && f.module === namespace
    );
    if (!file) return callback(null, {});
    fetch(file.url)
      .then((r) => r.json())
      .then((data) => callback(null, data))
      .catch((err) => callback(err, null));
  },
};
```

Then consume a namespace where you need it:

```jsx theme={null}
import { useTranslation } from "react-i18next";

function CheckoutPage() {
  // Fetches fr/checkout.json on first render, then caches it.
  const { t } = useTranslation("checkout");
  return <h1>{t("title")}</h1>;
}
```

<Warning>
  **Keys resolve *within* their namespace**

  A key stored in TextSetu as `checkout.title` ships inside `checkout.json` as
  `title`. So it's `t("title")` from the `checkout` namespace, or
  `t("checkout:title")` from anywhere else — **not** `t("checkout.title")`.

  That's required for i18next to resolve it at all, which is why the prefix is
  stripped rather than kept.
</Warning>

To avoid a loading flash on a route transition, warm the namespace first:

```js theme={null}
await i18next.loadNamespaces("checkout");
```

**Official i18next documentation:**

* [Namespaces](https://www.i18next.com/principles/namespaces) — what they are and how keys resolve
* [`loadNamespaces`](https://www.i18next.com/overview/api#loadnamespaces) — loading one on demand
* [`useTranslation`](https://react.i18next.com/latest/usetranslation-hook) — the React hook used above
* [Writing your own backend](https://www.i18next.com/misc/creating-own-plugins#backend) — the plugin contract implemented above

### Plain JSON, without a framework

```js theme={null}
const CACHE_KEY = "translations";

async function loadTranslations(lang) {
  const cached = JSON.parse(localStorage.getItem(CACHE_KEY) ?? "null");

  const res = await fetch(MANIFEST_URL, {
    headers: cached?.etag ? { "If-None-Match": cached.etag } : {},
  });

  // Nothing changed — keep what we have and skip the second request entirely.
  if (res.status === 304 && cached) return cached.data[lang];

  const manifest = await res.json();
  const file = manifest.files.find((f) => f.language === lang);
  if (!file) return {};

  const data = await fetch(file.url).then((r) => r.json());
  localStorage.setItem(
    CACHE_KEY,
    JSON.stringify({
      etag: res.headers.get("ETag"),
      data: { ...cached?.data, [lang]: data },
    })
  );
  return data;
}
```

### A language picker, with no extra request

`languageDetails` carries the label, flag and text direction, so the picker
renders from the manifest you already fetched:

```js theme={null}
const options = (
  manifest.languageDetails ??
  manifest.languages.map((c) => ({ code: c, label: c }))
).map((l) => ({
  value: l.code,
  label: `${l.icon ?? ""} ${l.label}`.trim(),
  dir: l.direction,
}));
```

Always keep that fallback: `languageDetails` is optional, and releases published
before the field existed don't carry it.

***

## Mobile apps

Packaged resources — `strings.xml`, `Localizable.strings`, `.arb` — are resolved
at build time and cannot be replaced at runtime. So the pattern is: **fetch,
cache to disk, parse yourself**, and keep the bundled files as the fallback for
a first launch with no network.

```dart theme={null}
// Flutter / .arb
Future<Map<String, String>> loadRemote(String lang) async {
  final manifest = jsonDecode((await http.get(Uri.parse(manifestUrl))).body);
  final file = (manifest['files'] as List)
      .firstWhere((f) => f['language'] == lang, orElse: () => null);
  if (file == null) return {};

  final dir = await getApplicationSupportDirectory();
  final cached = File('${dir.path}/${file['contentHash']}.arb');

  // The hash IS the filename, so a cache hit means the bytes are current.
  if (await cached.exists()) {
    return Map<String, String>.from(jsonDecode(await cached.readAsString()));
  }

  final body = (await http.get(Uri.parse(file['url']))).body;
  await cached.writeAsString(body);
  return Map<String, String>.from(jsonDecode(body));
}
```

The same shape works on iOS and Android. Two things worth doing:

* **Key the disk cache on `contentHash`.** It changes exactly when the content
  changes, so you never need to guess whether your copy is stale.
* **Load at launch, apply at next launch** if your UI can't re-render mid-session.
  Fetching in the background and swapping on the next cold start avoids strings
  changing under the user.

***

## Server-side rendering

Fetch the manifest at boot, then refresh on an interval or on a webhook.

```js theme={null}
let cache = { etag: null, translations: {} };

async function refresh() {
  const res = await fetch(MANIFEST_URL, {
    headers: cache.etag ? { "If-None-Match": cache.etag } : {},
  });
  if (res.status === 304) return;

  const manifest = await res.json();
  const loaded = {};
  for (const file of manifest.files) {
    loaded[file.language] = await fetch(file.url).then((r) => r.json());
  }
  cache = { etag: res.headers.get("ETag"), translations: loaded };
}

await refresh();
setInterval(refresh, 60_000);
```

Loading every language *is* right here — a server renders all of them. This is
the one case where the "fetch one language" rule doesn't apply.

Better than polling: subscribe to the `distribution.published`
[webhook](/docs/guides/webhooks) and call `refresh()` when it arrives.

***

## Pulling in CI

For interchange formats, or to commit translations into your repo:

```bash theme={null}
curl -s "$MANIFEST_URL" \
  | jq -r '.files[] | "\(.language)\t\(.url)"' \
  | while IFS=$'\t' read -r lang url; do
      curl -s "$url" -o "translations/$lang.json"
    done
```

To fail a build when a language is missing:

```bash theme={null}
MISSING=$(curl -s "$MANIFEST_URL" \
  | jq -r '[.languages[]] - [.files[].language] | .[]')
if [ -n "$MISSING" ]; then
  echo "Missing published files for: $MISSING" >&2
  exit 1
fi
```

***

## Handling errors

Your client should handle four cases. See the
[status code reference](/docs/reference/content-delivery-manifest#status-codes)
for the full list.

```js theme={null}
async function fetchManifest(url, etag) {
  const res = await fetch(url, {
    headers: etag ? { "If-None-Match": etag } : {},
  });

  if (res.status === 304) return null;                 // unchanged
  if (res.status === 202) {                            // first release building
    const wait = Number(res.headers.get("Retry-After") ?? 5);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return fetchManifest(url, etag);
  }
  if (!res.ok) throw new Error(`Manifest ${res.status}`);

  return res.json();
}
```

**Always have a local fallback.** Ship a baseline copy of your source language
with the app and treat the remote files as an upgrade. A network failure then
degrades to slightly stale strings instead of an empty screen:

```js theme={null}
import bundled from "./locales/en.json";

let translations = bundled;
try {
  translations = await loadTranslations(userLang);
} catch {
  // Keep the bundled copy. Never render raw keys.
}
```

***

## Detecting a new release

Two options, no polling of the file contents required:

* **`X-TextSetu-Release`** — the manifest response carries the release version
  as a header, so you can detect a change without parsing the body.
* **Webhooks** — subscribe to `distribution.published` for a push the moment a
  release lands. See the [Webhooks guide](/docs/guides/webhooks).

***

## Related

* [Content Delivery](/docs/guides/content-delivery) — creating and publishing distributions
* [Content delivery manifest](/docs/reference/content-delivery-manifest) — schema, caching and status codes
* [API Reference](/api/introduction) — the generated endpoint reference
* [Webhooks](/docs/guides/webhooks) — react to a release
