BIO.RE
i18n

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 OKApiResponseOf<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[])

FieldTypeNotes
codestringISO 639-1 code (2-5 lowercase chars). Use as the path param for /api/v1/translations/:locale.
namestringEnglish display name (e.g. "Turkish"). Use in admin pickers, not end-user UI.
nativeNamestringNative display name (e.g. "Türkçe"). Render this in the locale switcher.
isRtlbooleanSet <html dir="rtl"> (or equivalent) when true.
isDefaultbooleanThe fallback locale used when a translation is missing. Exactly one row in the list will have this set.

versions map

FieldTypeNotes
versionsRecord<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

HTTPReason
429Rate limit exceeded (60 req/min).

This endpoint has no other documented error paths.

Side effects

  1. languageService.listActive() — reads from cache (L1 60s → L2 Redis 5min → DB). On DB miss/cold start: prisma.language.findMany({ orderBy: { sortOrder: 'asc' } }), then filters active = true in memory.
  2. For each active language, read i18n:version:<code> from Redis (cache.getVersion(code)). Returns 0 if Redis is unreachable.
  3. Project to the sanitized 5-field shape.
  4. Return { locales, versions }. No DB writes, no logs, no side channels.

Code samples

curl https://api.bio.re/api/v1/locales
type 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

GET
/api/v1/locales

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

SourcePathLines
Controllerapps/api-core/src/modules/i18n/public-i18n.controller.ts23–34 (class), 36–58 (getLocales)
DTO (response)apps/api-core/src/modules/i18n/dto/i18n.dto.ts296–311 (LocaleDto), 317–328 (LocaleListResponseDto)
Service (languages)apps/api-core/src/modules/i18n/language.service.ts16–25 (list — cache then DB), 27–30 (listActive — filter active)
Service (versions)apps/api-core/src/modules/i18n/i18n-cache.service.ts93–101 (getVersion), 103–110 (incrementVersion)
Cache layersapps/api-core/src/modules/i18n/i18n-cache.service.ts8–9 (L1_TTL_MS = 60_000, L2_TTL_SECONDS = 300), 189–219 (L1+L2 read/write)
Prisma modelpackages/prisma/prisma/schema.prismaLanguage lines 2385–2398 (filter: active = true, sort: sortOrder ASC)

On this page