---
title: "Hand off with Excel or CSV"
description: "Export untranslated strings to an Excel workbook or CSV files for a translator, then import the result with the same safety checks as an automated run."
type: "how-to"
---
# Hand off with Excel or CSV

Export untranslated strings to an Excel workbook or CSV files for a translator, then import the result with the same safety checks as an automated run.

Use this when a person should translate some strings: marketing copy, legal text, or the languages that matter most. You **export** the strings that need translating, a translator fills them in, and you **import** the file back into your locale files.

Export picks exactly the keys an automated run would translate (the new and changed ones), and import runs the same integrity checks as `translate`, so a hand-typed value that breaks a placeholder is withheld and reported instead of written. Neither command calls a provider or needs an API key.

You need a verbatra project with at least one target locale.

## The round-trip [#the-round-trip]

```bash
# 1. Export the strings that need translating
verbatra export

# 2. A translator fills the Translation column and sends the file back

# 3. Import the filled workbook
verbatra import verbatra-translations.xlsx
```

Export writes `verbatra-translations.xlsx` in the working directory. Import validates every filled row, writes the values that pass, and advances [the lock file](https://verbatra.kreitz-webdev.de/docs/the-lock-file.md) for exactly the keys it accepted, as a `translate` run would.

## What lands in the workbook [#what-lands-in-the-workbook]

The workbook opens with an **Instructions** sheet, then one sheet per target locale, named for its locale (`de`, `fr`, ...). Every locale sheet has the same columns:

| Column | Editable | Purpose |
| --- | --- | --- |
| `Key` | no | the key path; the only thing that maps a row back to a string |
| `Source` | no | the source-locale value |
| `Current translation` | no | the existing target value, if any |
| `Status` | no | `new`, `changed` (the source changed since it was translated), or `unchanged` (only with `--include-unchanged`) |
| `Translation` | **yes** | the only cell the translator fills |
| `Source hash` | no | hidden; detects a source that changed after export |
| `Context` | no | the key's description, when the source format carries one; never imported |
| `Review status` | no | `ok`, or `review` when a [review flag](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#review-flags) suggests a second look |
| `Review reasons` | no | the [review reasons](https://verbatra.kreitz-webdev.de/docs/error-codes.md#review-reason-codes) behind a `review` status |

Every column except `Translation` is locked by sheet protection, and `Translation` uses Excel's text format, so a value like `007` or one starting with `=` stays literal text. The review columns are advisory: import never reads them.

## Handing off [#handing-off]

Tell the translator these rules (the Instructions sheet repeats them):

- Fill the `Translation` column, nothing else.
- Do not rename, delete or reorder the language tabs. Each tab is matched to a locale by its exact name, so a renamed or missing tab fails that locale.
- Sorting and filtering rows is fine, because rows are matched by `Key`. Columns must stay where they are.
- Leave a cell empty for "not translated yet". A half-filled workbook can come back now and the rest later.
- To clear an existing value on purpose, type exactly `[[CLEAR]]`.

## What import accepts [#what-import-accepts]

Every filled row is judged against the live project before anything is written:

- **Source drift**: if the source string changed after export, the row is withheld, so a translation of an old source never lands.
- **Integrity**: placeholders, inline markup and ICU structure must match the source, as in every run. See [the integrity gate](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#the-integrity-gate).
- **Degenerate values**: a repetition loop, a runaway length or an empty value for a non-empty source is withheld.
- **Unknown keys**: a filled row whose key exists in neither the source nor the target fails that locale's sheet. A key that exists only in the target is left unwritten.
- **Unchanged rows** (from 0.13.0): in a CSV, TSV or xlsx handoff, a filled row equal to the value an up-to-date key already holds is left alone and reported unchanged, once it passes the same gate.

A withheld or blank row keeps its lock baseline, so the key is exported again until it is resolved; a blank row whose source drifted also raises [`BLANK_ROW_BASELINE_RETAINED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#blank_row_baseline_retained). An unreadable row or a repeated key is reported without failing the sheet. Each sheet is one locale in the run summary, and import uses the same exit codes as `translate`, so a handoff fits the same [CI checks](https://verbatra.kreitz-webdev.de/docs/ci-and-exit-codes.md) as an automated run. [`verbatra import`](https://verbatra.kreitz-webdev.de/docs/cli/import.md) has the details.

### Dry-run first [#dry-run-first]

Preview what a returned workbook would write, without touching a file:

```bash
verbatra import verbatra-translations.xlsx --dry-run
```

## CSV and TSV instead of a workbook [#csv-and-tsv-instead-of-a-workbook]

If a vendor or a pipeline wants plain text, `--format csv` or `--format tsv` writes the same columns as one file per locale:

```bash
# write handoff/de.csv and handoff/fr.csv
verbatra export --format csv --out handoff

# import every locale file in the directory back
verbatra import handoff --format csv
```

The locale comes from the file name, so keep the files named as they were exported. Compared with the workbook:

- **Nothing is protected.** Every column is editable and `Source hash` is visible. An edited hash is never trusted: the row is compared with the live source and withheld as drift.
- **There is no Instructions sheet.** Pass the rules above to the translator yourself.
- **Formulas are defused.** A value starting with `=`, `+`, `-`, `@`, a tab or a carriage return is written with a leading apostrophe, so a spreadsheet keeps it as text; import strips the apostrophe again. Still treat a returned file as untrusted input.
- **A narrower re-export retires older files.** A file left over from an earlier, wider export is refused on import instead of applied. Nothing is deleted.

## XLIFF for a CAT tool [#xliff-for-a-cat-tool]

<AvailableFrom version="0.12.0" />

If an agency works in a CAT tool such as Trados, memoQ or OmegaT, export XLIFF instead: see [Hand off to an agency](https://verbatra.kreitz-webdev.de/docs/translation-agency.md).

## TMX is a different path [#tmx-is-a-different-path]

<AvailableFrom version="0.11.0" />

A handoff carries the pending strings of each locale to a person. [`verbatra tmx`](https://verbatra.kreitz-webdev.de/docs/cli/tmx.md) moves the whole translation memory between tools as one multilingual file, so it is not an `export --format` value.

## Selecting what to export [#selecting-what-to-export]

By default, export includes only the missing and changed strings:

```bash
# Only the German and French sheets
verbatra export --locales de,fr

# Also include strings that are already up to date
verbatra export --include-unchanged

# Write to a specific path
verbatra export --out handoff/round-2.xlsx
```

Use `--include-unchanged` when a translator wants a whole locale for context. `--out` must stay inside the project and must not name a file the project depends on. See [`verbatra export`](https://verbatra.kreitz-webdev.de/docs/cli/export.md) for every flag and rule.

## The review columns [#the-review-columns]

Both columns are computed at export from the source and current values, so a row without a translation is always `ok`. `Review reasons` spells each reason in lower case with hyphens (`length-ratio-outlier`), except the two only a run can see (`provider-degraded`, `fuzzy-cache-reuse`), which never appear. It adds `icu-arms` with the missing or extra arms when a value's ICU arms do not fit the target language; the integrity gate refuses that value, so fix it before the row comes back. To clear a flag, fix the translation.

## From the SDK [#from-the-sdk]

The two commands wrap [`exportWorkbook`](https://verbatra.kreitz-webdev.de/docs/sdk/exchange.md#exportworkbook) and [`importWorkbook`](https://verbatra.kreitz-webdev.de/docs/sdk/exchange.md#importworkbook):

```ts
import { exportWorkbook, importWorkbook, loadConfig } from "@verbatra/sdk";

const config = await loadConfig();

await exportWorkbook({ config, format: "csv", out: "handoff" });

const summary = await importWorkbook({ config, workbook: "handoff", format: "csv" });
```

`importWorkbook` returns the same [`RunSummary`](https://verbatra.kreitz-webdev.de/docs/sdk/run-summary.md) as `translate`.

## Related [#related]

- [`verbatra export`](https://verbatra.kreitz-webdev.de/docs/cli/export.md) and [`verbatra import`](https://verbatra.kreitz-webdev.de/docs/cli/import.md): every flag, the output paths and the exit codes.
- [Translation safety](https://verbatra.kreitz-webdev.de/docs/translation-safety.md): the integrity gate behind every write.
- [Run without machine translation](https://verbatra.kreitz-webdev.de/docs/human-only-workflow.md): a project where every translation comes from a person.
