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
| Param | Type | Required | Notes |
|---|---|---|---|
locale | string | โ | ISO 639-1 code (e.g. en, tr). |
namespace | string | โ | Namespace identifier (e.g. auth, chat, common). Defined by the admin tooling โ there is no list endpoint exposed publicly. |
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
v | integer | โ | Same as the full-bundle endpoint. Pass versions[locale] from GET /locales to opt into the 1-year immutable cache. |
Headers
| Header | Required | Notes |
|---|---|---|
If-None-Match | optional | Send 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)
| Header | Value | Notes |
|---|---|---|
ETag | "i18n-<locale>-<namespace>-<version>" | Embeds the namespace โ distinct from the full-bundle ETag. |
Cache-Control | public, max-age=31536000, immutable OR public, max-age=60, stale-while-revalidate=300 | Immutable when ?v=N matches; SWR fallback otherwise. |
Errors
| HTTP | Reason |
|---|---|
429 | Rate limit exceeded. |
No other documented errors. Unknown :locale or :namespace โ 200 + empty {}.
Side effects
cache.getVersion(locale)โ readi18n:version:<locale>from Redis (returns0if unavailable).- Build ETag =
"i18n-<locale>-<namespace>-<version>". IfIf-None-Match === ETagโres.status(304); return. 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 } } }).
- L1 (
- Project to flat map. The key is just
translation.translationKey.keyโ no<namespace>.prefix (because the namespace is implied by the URL). - Write to L2 + L1.
- Set
ETagheader. SetCache-Controlbased on?v=match. - 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
Path Parameters
ISO 639-1 locale code
Translation namespace
Query Parameters
Version number โ if matches current version, returns immutable cached response
Header Parameters
ETag from previous response for conditional GET (304 on match)
Response Body
application/json
curl -X GET "https://loading/api/v1/translations/en/auth"{}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/i18n/public-i18n.controller.ts | 94โ126 (getNamespaceBundle โ ETag with namespace, ?v= pinning, SWR fallback) |
| Service (bundle build) | apps/api-core/src/modules/i18n/translation.service.ts | 301โ336 (getBundle with optional namespace arg โ when set, key is translationKey.key without prefix; line 328) |
| Cache layers | apps/api-core/src/modules/i18n/i18n-cache.service.ts | 70โ89 (getBundle/setBundle โ namespace-aware key), 124โ148 (invalidateLocale clears every namespace under a locale) |
| Prisma model | packages/prisma/prisma/schema.prisma | Translation lines 2417โ2434 (filter: languageCode = ? AND status = 'APPROVED'); TranslationKey lines 2400โ2415 (filter namespace = ?) |
Get Translation Bundle
Full APPROVED translation bundle for a locale. Flat map keyed by "namespace.key". ETag + If-None-Match for conditional GET, optional ?v=N for 1-year immutable cache.
Get Privacy Policy
Public read of the platform privacy policy. Content is admin-managed via ConfigService. Returns the body plus the last-updated timestamp.