List Locales
Active language catalog plus a per-locale version map. Sanitized output — only code, name, nativeName, isRtl, isDefault. Use the version map to decide when to refetch translation bundles.
GET /api/v1/locales — 🌐 Public · Rate limit: 60 req / minute
Returns the active languages currently served by the platform plus a versions map (one integer per locale code). The version is incremented every time the server invalidates a locale's translation bundle — clients keep this map and refetch the matching /translations/:locale only when its version changes.
No top-level /i18n/ prefix. This controller registers at the root under the global /api/v1 prefix — so the path is /api/v1/locales, not /api/v1/i18n/locales. Same goes for the translation endpoints (/api/v1/translations/:locale, etc.). The grouping in this docs sidebar is purely editorial.
Sanitized output. Internal fields (fallbackCode, sortOrder, active, createdAt, updatedAt) are stripped. The list contains only languages where active = true, sorted by sortOrder ASC. To bring up a new locale, an admin sets active = true server-side; this list reflects that within ~60s (L1) / ~5 min (L2) of the change due to cache layers.
versions[code] is 0 on cold cache. The version counter lives in Redis (key i18n:version:<code>, incremented on every invalidation). If Redis is unreachable when this endpoint runs, getVersion() returns 0 for every locale — your ETag/?v= checks against bundles will then look like "version 0," which is fine for cache-busting but not safe to combine with the immutable cache hint (you'd pin a stale bundle for a year). Keep version handling defensive: only treat version > 0 as authoritative.
Request
No body, no query params, no headers required.
Response
200 OK — ApiResponseOf<LocaleListResponseDto>
{
"success": true,
"data": {
"locales": [
{
"code": "en",
"name": "English",
"nativeName": "English",
"isRtl": false,
"isDefault": true
},
{
"code": "tr",
"name": "Turkish",
"nativeName": "Türkçe",
"isRtl": false,
"isDefault": false
}
],
"versions": {
"en": 42,
"tr": 15
}
}
}Item fields (locales[])
| Field | Type | Notes |
|---|---|---|
code | string | ISO 639-1 code (2-5 lowercase chars). Use as the path param for /api/v1/translations/:locale. |
name | string | English display name (e.g. "Turkish"). Use in admin pickers, not end-user UI. |
nativeName | string | Native display name (e.g. "Türkçe"). Render this in the locale switcher. |
isRtl | boolean | Set <html dir="rtl"> (or equivalent) when true. |
isDefault | boolean | The fallback locale used when a translation is missing. Exactly one row in the list will have this set. |
versions map
| Field | Type | Notes |
|---|---|---|
versions | Record<string, number> | Keyed by locale code. Each value is the current bundle version for that locale — bumped on every translation update or language metadata change. Use it to invalidate your client-side bundle cache. 0 means "cold Redis cache" — see the warn callout above. |
Stripped fields
fallbackCode, sortOrder, active, createdAt, updatedAt — internal, not returned.
Errors
| HTTP | Reason |
|---|---|
429 | Rate limit exceeded (60 req/min). |
This endpoint has no other documented error paths.
Side effects
languageService.listActive()— reads from cache (L1 60s → L2 Redis 5min → DB). On DB miss/cold start:prisma.language.findMany({ orderBy: { sortOrder: 'asc' } }), then filtersactive = truein memory.- For each active language, read
i18n:version:<code>from Redis (cache.getVersion(code)). Returns0if Redis is unreachable. - Project to the sanitized 5-field shape.
- Return
{ locales, versions }. No DB writes, no logs, no side channels.
Code samples
curl https://api.bio.re/api/v1/localestype Locale = {
code: string;
name: string;
nativeName: string;
isRtl: boolean;
isDefault: boolean;
};
type LocaleListResponse = {
locales: Locale[];
versions: Record<string, number>;
};
async function listLocales(): Promise<LocaleListResponse> {
const res = await fetch('https://api.bio.re/api/v1/locales');
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Locales fetch failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useQuery, useQueryClient } from '@tanstack/react-query';
export const i18nKeys = {
locales: () => ['i18n', 'locales'] as const,
bundle: (locale: string, version: number) =>
['i18n', 'bundle', locale, version] as const,
};
// Refresh the locale list every 5 min — matches the L2 Redis TTL.
// When `versions[locale]` changes, downstream useBundle queries with the
// same locale but a new version will be fresh cache misses (different key).
export function useLocales() {
return useQuery({
queryKey: i18nKeys.locales(),
queryFn: async () => {
const res = await fetch('/api/v1/locales');
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Locales fetch failed'), {
code: json?.error?.code,
});
}
return json.data as LocaleListResponse;
},
staleTime: 5 * 60_000,
});
}Try it
Response Body
application/json
curl -X GET "https://loading/api/v1/locales"{
"success": true,
"data": {
"locales": [
{
"code": "en",
"name": "English",
"nativeName": "English",
"isRtl": false,
"isDefault": true
}
],
"versions": {
"en": 42,
"tr": 15
}
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/i18n/public-i18n.controller.ts | 23–34 (class), 36–58 (getLocales) |
| DTO (response) | apps/api-core/src/modules/i18n/dto/i18n.dto.ts | 296–311 (LocaleDto), 317–328 (LocaleListResponseDto) |
| Service (languages) | apps/api-core/src/modules/i18n/language.service.ts | 16–25 (list — cache then DB), 27–30 (listActive — filter active) |
| Service (versions) | apps/api-core/src/modules/i18n/i18n-cache.service.ts | 93–101 (getVersion), 103–110 (incrementVersion) |
| Cache layers | apps/api-core/src/modules/i18n/i18n-cache.service.ts | 8–9 (L1_TTL_MS = 60_000, L2_TTL_SECONDS = 300), 189–219 (L1+L2 read/write) |
| Prisma model | packages/prisma/prisma/schema.prisma | Language lines 2385–2398 (filter: active = true, sort: sortOrder ASC) |
Clear Recently Viewed
Authenticated. Hard-deletes every RecentlyViewed row for the calling user. Returns the count actually deleted. No undo.
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.