---
title: "Languages"
manual: "TCA_API"
version: "2.2"
permalink: "https://docs.typo3.org/permalink/maikschneider/tca-api:languages@2.2"
source: "Configuration/Languages.rst"
rendered: "2026-10-12T13:17:40+00:00"
---

# Languages {#languages}

TCA_API is language-aware. For TYPO3 sites that declare multiple languages, the
extension resolves the requested locale per request, applies the matching
`sys_language_uid` constraint to queries, overlays translations on the
default-language rows according to the site's `fallbackType`, and isolates
cache entries per locale.

## Resolution order {#resolution-order}

For every API request the dispatcher resolves a single `SiteLanguage` using
this precedence:

1.  **The URL** — if the path starts with a language base segment declared on
    the site (e.g. `/de/api/...` matches the German language whose
    `base: /de/`), that language is selected. This piggybacks on TYPO3's own
    `typo3/cms-frontend/site` middleware, which has already populated
    `$request->getAttribute('language')` before TCA_API runs.
1.  **The \`\`X-Locale\`\` header** — if set, overrides the URL-derived language.
    The value is the TYPO3 `languageId` integer (not a locale string).

    ```http
    GET /api/articles
    X-Locale: 1
    ```

    is equivalent to:

    ```http
    GET /de/api/articles
    ```
1.  **The site's default language** — when neither the URL nor the header
    resolves a language.

Invalid `X-Locale` values (non-integer, unknown id, or pointing at a disabled
language) return `400 Bad Request` with a `hydra:Error` body listing the
available enabled language ids:

```json
{
  "@context": "http://www.w3.org/ns/hydra/context.jsonld",
  "@type": "hydra:Error",
  "hydra:title": "Invalid language",
  "hydra:description": "Invalid language \"99\". Available enabled language ids: 0, 1"
}
```

## Query and overlay behaviour {#query-and-overlay-behaviour}

For tables whose TCA declares a `ctrl.languageField` (almost every
translatable TYPO3 table — `pages`, `tt_content`, `sys_category`,
`sys_file_metadata`, and any extension table marked translatable), the
repository:

1.  Restricts the base query to `sys_language_uid IN (0, -1)` — default-language
    rows plus "all-languages" sentinel rows.
1.  Bulk-loads matching translation rows (`sys_language_uid = <requested>`,
    `l10n_parent IN <base uids>`).
1.  Applies an overlay row-by-row.

The overlay honours the site's `fallbackType`:

| `fallbackType` | Behaviour for a base row that has no translation |
| --- | --- |
| `strict` | Row is **omitted** from the response. |
| `fallback` | Default-language row is returned **unchanged** (English fallback). |
| `free` | Same as `fallback` for the overlay path. |

Rows flagged `sys_language_uid = -1` ("visible in all languages") are
**always returned untouched**, regardless of `fallbackType` — they are by
definition language-agnostic.

### IRI stability {#iri-stability}

The Hydra `@id` for a record always uses the **default-language UID**, even
when the response payload contains the translated values. A German consumer of
`/de/api/articles/1` sees:

```json
{
  "@id": "/api/articles/1",
  "@type": "Article",
  "title": "Hallo Welt"
}
```

The translation row's own UID (e.g. `51`) is never exposed.

## Opt-out per resource {#opt-out-per-resource}

A resource can disable language filtering entirely with the
`general.language.mode` key:

```php
return [
    'general' => [
        'table'        => 'sys_category',
        'resourceName' => 'categories-all',
        'resourceType' => 'CategoryAll',
        'language'     => ['mode' => 'ignore'],
    ],
    // …
];
```

| `mode` | Default | Behaviour |
| --- | --- | --- |
| `auto` | ✓ | Apply language constraint + overlay when the table's TCA declares a `languageField` (the standard case). |
| `ignore` |  | Skip the language constraint and the overlay. Every row in every language is returned as a distinct member. Useful for admin listings or for tables that are language-agnostic by design. |

Tables whose TCA does not declare `ctrl.languageField` (rare —
non-translatable extension tables) are already exempt; the `ignore` mode is
only needed when you want to opt out of filtering on a normally-translatable
table.

## Response headers {#response-headers}

Every API response carries:

| Header | Value |
| --- | --- |
| `Content-Language` | The resolved language's `hreflang` (falls back to the locale's primary language code, e.g. `de`). |
| `Vary` | Includes `X-Locale` so shared caches (Varnish, Cloudflare, etc.) isolate entries per locale. When CORS is enabled the value is `Origin, X-Locale`. |

CORS preflight (`OPTIONS`) responses also advertise `X-Locale` in
`Access-Control-Allow-Headers` so browsers can send the override.

## Cache key isolation {#cache-key-isolation}

When [response caching](https://docs.typo3.org/permalink/maikschneider/tca-api:caching@2.2) is enabled, the resolved language id is
part of the cache key composition. A request to `/api/articles` and one to
`/de/api/articles` (or the same path with `X-Locale: 1`) produce **distinct
cache entries** and cannot return each other's payload.

## Site fixture example {#site-fixture-example}

A typical multi-language site set-up:

```yaml
# config/sites/main/config.yaml
base: 'https://example.com/'
languages:
  -
    title: English
    languageId: 0
    locale: en_US
    base: /
    hreflang: en
    fallbackType: strict
  -
    title: German
    languageId: 1
    locale: de_DE
    base: /de/
    hreflang: de
    fallbackType: fallback
    fallbacks: '0'
settings:
  tca_api:
    enabled: true
    apiPrefix: '/api/'
```

With this configuration:

-   `GET /api/articles` → English rows only.
-   `GET /de/api/articles` → German overlays where translations exist,
    English fallback otherwise (because `fallbackType: fallback`).
-   `GET /api/articles` with header `X-Locale: 1` → same as the previous.
-   `GET /api/articles` with header `X-Locale: 99` → `400 Bad Request`.

## What TCA_API does *not* do {#what-tca-api-does-not-do}

-   **Accept-Language header**. Browser language preferences are deliberately
    *not* consulted. Locale selection is either explicit (URL base segment or
    `X-Locale` header) or falls back to the site default — never silently
    derived from the user agent.
-   **Region/country subtags**. `X-Locale` is the TYPO3 `languageId`
    integer, not an IETF tag — there is no `en-US` vs `en-GB` matching.
-   **Per-locale IRIs**. `/api/articles/1` is the canonical IRI regardless of
    language; locale is a request-time dimension, not part of the resource
    identity.
-   **Mass-edit translations through the API**. The standard CRUD operations
    always target default-language rows. Creating or updating translation rows
    is currently the TYPO3 backend's job.
