BIO.RE
i18n

Get Namespace Bundle

Single-namespace translation bundle. Like the full bundle but scoped to one namespace and with non-prefixed keys. Same ETag + version-pinning semantics.

GET /api/v1/translations/:locale/:namespace โ€” ๐ŸŒ Public ยท Rate limit: 60 req / minute

Returns the APPROVED translations for a single namespace inside a locale. Use this when you want to lazy-load (auth namespace on the auth screen, chat namespace inside the chat surface) instead of pulling the entire bundle up-front. Cache semantics, ETag shape, and ?v=N immutable pinning behave the same as GET /api/v1/translations/:locale โ€” the only differences are the path, the ETag (which embeds the namespace), and the response key shape.

Keys are NOT prefixed in this response. When you pull the full bundle the keys look like auth.login.title. When you pull a namespace bundle the same key is just login.title โ€” the namespace is implied by the URL, so the prefix is dropped. Don't apply a <namespace>. prefix on the client when you call this endpoint.

ETag includes the namespace. Format is "i18n-<locale>-<namespace>-<version>". The version itself is per-locale (not per-namespace) โ€” bumping any translation in any namespace under a locale invalidates every namespace bundle for that locale. There's currently no per-namespace versioning.

Unknown namespace returns an empty bundle, not 404. Same behavior as the full-bundle endpoint with an unknown locale โ€” the WHERE clause produces zero rows and the server happily returns 200 OK with {}. There's no list-namespaces public endpoint to validate against; the canonical client-side approach is to harden against missing keys (fall back to the key itself).

Request

Path parameters

ParamTypeRequiredNotes
localestringโœ“ISO 639-1 code (e.g. en, tr).
namespacestringโœ“Namespace identifier (e.g. auth, chat, common). Defined by the admin tooling โ€” there is no list endpoint exposed publicly.

Query parameters

ParamTypeDefaultNotes
vintegerโ€”Same as the full-bundle endpoint. Pass versions[locale] from GET /locales to opt into the 1-year immutable cache.

Headers

HeaderRequiredNotes
If-None-MatchoptionalSend the previous response's ETag ("i18n-<locale>-<namespace>-<version>") for a 304 Not Modified short-circuit.

No body, no auth required.

Response

200 OK โ€” Record<string, string> (flat map, no namespace prefix)

{
  "login.title": "Sign in to your account",
  "login.button": "Sign in",
  "signup.cta": "Create account",
  "errors.invalid_credentials": "Invalid email or password"
}

304 Not Modified

Returned when If-None-Match matches the current ETag. No body.

Response headers (always)

HeaderValueNotes
ETag"i18n-<locale>-<namespace>-<version>"Embeds the namespace โ€” distinct from the full-bundle ETag.
Cache-Controlpublic, max-age=31536000, immutable OR public, max-age=60, stale-while-revalidate=300Immutable when ?v=N matches; SWR fallback otherwise.

Errors

HTTPReason
429Rate limit exceeded.

No other documented errors. Unknown :locale or :namespace โ†’ 200 + empty {}.

Side effects

  1. cache.getVersion(locale) โ€” read i18n:version:<locale> from Redis (returns 0 if unavailable).
  2. Build ETag = "i18n-<locale>-<namespace>-<version>". If If-None-Match === ETag โ†’ res.status(304); return.
  3. translationService.getBundle(locale, namespace) โ€” multi-layer cache lookup:
    • L1 (bundle:<locale>:<namespace>, 60s) โ†’ if hit, return.
    • L2 (Redis, i18n:bundle:<locale>:<namespace>, 5 min) โ†’ if hit, promote to L1, return.
    • On miss: prisma.translation.findMany({ where: { languageCode, status: 'APPROVED', translationKey: { namespace } } }).
  4. Project to flat map. The key is just translation.translationKey.key โ€” no <namespace>. prefix (because the namespace is implied by the URL).
  5. Write to L2 + L1.
  6. Set ETag header. Set Cache-Control based on ?v= match.
  7. Return the bundle.

Invalidation note

The write-side invalidateLocale(locale) clears all namespace bundles for that locale (it loops through known namespaces and DELs each Redis key). There's no narrower per-namespace invalidation today.

Code samples

curl -i 'https://api.bio.re/api/v1/translations/en/auth?v=42' \
  -H 'If-None-Match: "i18n-en-auth-42"'
# 304 if version matches and ETag matches; else 200 + JSON body.
type NamespaceBundle = Record<string, string>;

async function getNamespaceBundle(
  locale: string,
  namespace: string,
  version: number,
): Promise<NamespaceBundle> {
  const url = `https://api.bio.re/api/v1/translations/${locale}/${namespace}?v=${version}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Bundle fetch failed: ${res.status}`);
  return (await res.json()) as NamespaceBundle;
}

// Usage in an auth screen โ€” the `auth` namespace is the only thing this view needs:
const auth = await getNamespaceBundle('en', 'auth', 42);
const title = auth['login.title'] ?? 'Sign in';
import { useQuery } from '@tanstack/react-query';

export function useNamespaceBundle(locale: string, namespace: string, version: number) {
  return useQuery({
    queryKey: ['i18n', 'bundle', locale, namespace, version],
    queryFn: async () => {
      const res = await fetch(`/api/v1/translations/${locale}/${namespace}?v=${version}`);
      if (!res.ok) throw new Error(`Bundle fetch failed: ${res.status}`);
      return (await res.json()) as Record<string, string>;
    },
    enabled: version > 0,
    staleTime: Infinity,
    gcTime: 60 * 60_000,
  });
}

Try it

GET
/api/v1/translations/{locale}/{namespace}

Path Parameters

locale*string

ISO 639-1 locale code

namespace*string

Translation namespace

Query Parameters

v?number

Version number โ€” if matches current version, returns immutable cached response

Header Parameters

if-none-match?string

ETag from previous response for conditional GET (304 on match)

Response Body

application/json

curl -X GET "https://loading/api/v1/translations/en/auth"
{}
Empty

Source

SourcePathLines
Controllerapps/api-core/src/modules/i18n/public-i18n.controller.ts94โ€“126 (getNamespaceBundle โ€” ETag with namespace, ?v= pinning, SWR fallback)
Service (bundle build)apps/api-core/src/modules/i18n/translation.service.ts301โ€“336 (getBundle with optional namespace arg โ€” when set, key is translationKey.key without prefix; line 328)
Cache layersapps/api-core/src/modules/i18n/i18n-cache.service.ts70โ€“89 (getBundle/setBundle โ€” namespace-aware key), 124โ€“148 (invalidateLocale clears every namespace under a locale)
Prisma modelpackages/prisma/prisma/schema.prismaTranslation lines 2417โ€“2434 (filter: languageCode = ? AND status = 'APPROVED'); TranslationKey lines 2400โ€“2415 (filter namespace = ?)

On this page