# Import a file containing translations

`POST /api/translations/import`

- **Success:** `200 OK`
- **Products:** Operator (`FOH`)
- **Features:** Front of House Generic Access (`FOHGENERIC`)

HTML version: [Import a file containing translations](https://support.gladstonesoftware.cloud/reference/configuration/import-a-file-containing-translations)

## Overview

Uploads a CSV file of translated text for one language and one translation type, and writes it to the platform. Despite the name, this is a write operation, not a staging or preview step — every accepted row is saved before the response is returned.

**It merges; it never replaces.** Each row is created or updated against the combination of row ID, translation type, culture and property. A translation that already exists for that combination is overwritten with the value in your file; one that does not is inserted. **Rows you do not mention are left exactly as they were, and nothing is ever deleted.** There is no way to remove a translation through this endpoint: a blank cell is skipped, not treated as an instruction to clear the existing value. To empty a translation you must overwrite it with some other value, or use the translation write endpoints directly.

The request is `multipart/form-data` with two parts, both required:

| Part | Contents |
|---|---|
| `request` | A JSON string — not a JSON body — with two properties: `Language` (the culture code to import into, matched case-insensitively against the platform's culture list) and `TranslationType` (matched case-insensitively against the configured translation types, for example `Site`, `Product`, `ActivityGroup`, `PriceLevel`). Both must be present and non-empty. |
| `file` | The CSV. Its `Content-Type` must be exactly `text/csv` — it must match exactly, so `application/vnd.ms-excel` or an omitted type is rejected with `400`. |

### The CSV shape

**Export first.** The column count is derived from the translation type's own configuration and is validated strictly against your header row, so hand-building a file is the most common way to fail this call. A type has three columns as standard, five if it carries a name, and seven if it carries both a name and web comments. The exported header labels the read-only source columns `(DO NOT CHANGE)`:

```text title="Header for a type with a name (5 columns)"
ID (DO NOT CHANGE),Name (DO NOT CHANGE),Translated Name,Description (DO NOT CHANGE),Translated Description
```

Only the first column and the `Translated …` columns are read on import. The `(DO NOT CHANGE)` columns are there so a human editing the file can see the source text; their contents are ignored.

One quirk worth knowing: for a translation type that does **not** carry a name, the value in the `Translated Name` column is saved against the `description` property rather than `name`. If you read those translations back and find your text under `description`, that is why.

### HTML in translated text

Angle brackets break the CSV parsing, so markup is carried in double square brackets. Export writes `<b>` as `[[b]]`, and import converts `[[b]]` back to `<b>` before saving. Write markup that way in files you build yourself, and never "fix" the brackets by hand in an exported file.

### Reading the result

The whole file is validated first — language, translation type, minimum line count, header column count. If any of those fail, nothing is written and you get a `400`. Once past those checks the import is **per row and partial**: valid rows are saved and invalid ones are reported, in a single `200`. Always inspect `erroredRows` rather than treating `200` as success.

## Endpoint

`POST {base_url}/api/translations/import`

`{base_url}` is the environment URL issued to your integration. See [Authenticating with Gladstone APIs](../../guides/authenticating-with-gladstone-apis.md).

## Sample request

```bash title="cURL"
curl -X POST '{base_url}/api/translations/import' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true'
```

```bash title="cURL — with both form parts"
curl -X POST '{base_url}/api/translations/import' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -F 'request={"Language":"cy-GB","TranslationType":"Site"}' \
  -F 'file=@site-translations.csv;type=text/csv'
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `totalRows` | integer (int32) | The number of data rows found in the file — every non-empty line except the header. This is the denominator, not a success count. **It does not always equal `rowsImported` + the length of `erroredRows`**: a row that carries an ID but leaves every `Translated …` column blank, and a row that is blank throughout, are both skipped silently — counted here, reported nowhere else, and written nowhere. A gap between the three numbers usually means blank cells, not a fault. |
| `rowsImported` | integer (int32) | The number of rows that were written. Note that a single row can write up to three translation records — name, description and web comments — but still counts as one here. When this is `0` nothing is written at all, so an all-blank file is a no-op rather than an error. |
| `erroredRows` | array of string | One human-readable string per rejected row, prefixed with its 1-based line number in the file so you can point a user at it — for example `Line 4: Translations for ID "GYM01" have already been provided in the file`. The three messages you can get are `Incorrect number of columns` (that row has a different column count from the header), `ID cannot be blank when translations are provided`, and `Translations for ID "…" have already been provided in the file` (the same ID appears twice, compared case-insensitively — only the first occurrence is imported). Empty when every row was accepted or silently skipped. |

## Sample response

```json title="200 OK"
{
  "totalRows": 1,
  "rowsImported": 1,
  "erroredRows": [
    "string"
  ]
}
```

## Errors

Every `400` below is whole-file: nothing is written. Per-row problems do not produce a `400` — they come back as `erroredRows` inside a `200`. The response body is the bare message string, not a JSON error object.

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | `No request has been supplied` — the `request` form part is missing or empty. |
| `400` | Bad request | `Unable to read request object, possibly invalid JSON format` — the `request` part is not parseable JSON. |
| `400` | Bad request | `The format of the request is incorrect, both Language amd TranslationType must be supplied` — the JSON parsed but `Language` or `TranslationType` is missing or empty. The typo in `amd` is present in the live message. |
| `400` | Bad request | `No file has been supplied` — the `file` form part is missing. |
| `400` | Bad request | `File has no content` — the file was uploaded but is zero bytes. |
| `400` | Bad request | `File must be in text/csv format` — the uploaded part's content type is not exactly `text/csv`. |
| `400` | Bad request | `Language "{language}" is not a valid language` — the `Language` value does not match any culture configured on the tenant. |
| `400` | Bad request | `TranslationType "{type}" is not a valid translation type` — the `TranslationType` value does not match any configured translation type. Take the value from the translation types endpoint rather than typing it. |
| `400` | Bad request | `File must contain more than one line (the first line is the header)` — the file has fewer than two non-empty lines. A header on its own is not enough. |
| `400` | Bad request | `File must contain {expected} columns but actually contains {actual}` — the header row's column count does not match what the translation type requires. Export the file first rather than guessing. |
| `500` | Internal server error | An unexpected server-side failure during parsing or saving. The response body is empty. Note that a translated value containing certain special characters — a lone backslash, for instance — can fail the whole call here rather than being reported as a row error. |
