---
title: Translations at build time
description: Pull the project's composed translations from ops in build.rs and embed them in the binary.
order: 5
---

Feature: `i18n`.

A Rust service keeps its locale files in the repository, for example `locales/da.json`
and `locales/en.json`. At build time `i18n::Pull` asks ops for each locale's composed
tree, which is your project's own translations with the shared bundles and your overrides
applied, and writes it where your crate can embed it. The deployed binary carries its
translations and never fetches them while it runs.

For a Nuxt app the same pull is part of the Nuxt module: see
[Translations at build time](/nuxt-ops/translations).

Add the crate as a build dependency:

```toml
[build-dependencies]
ops-sdk = { version = "0.1", registry = "ops", features = ["i18n"] }
```

```rust
// build.rs
fn main() {
    ops_sdk::i18n::Pull::from_env("locales").run().expect("locales");
}
```

```rust
// in the crate
const DA: &str = include_str!(concat!(env!("OUT_DIR"), "/ops-i18n/da.json"));
```

## Configuration

| variable | what it is |
| --- | --- |
| `OPS_URL` | the address of ops. Falls back to `OPS_PUBLIC_URL` |
| `OPS_PROJECT` | your project's slug |
| `OPS_TMS_BUNDLE` | which of the project's own bundles the composition starts from. The default is the one named like the project |
| `OPS_API_TOKEN` | a token that may read the project's translations |

On an ops build none of these need setting: the build environment carries `OPS_URL`,
`OPS_PROJECT` and an `OPS_API_TOKEN` that reads your project's translations for as long as
the build runs.

Cargo reruns the build script when any of these or the locale directory changes. A
translation edited in ops ships with the next build.

## A pull never fails the build

The set of locales is the `.json` files in the directory: add a file to add a locale. For
each one the pull writes the tree from ops, ordered and newline terminated, to
`$OUT_DIR/ops-i18n/<locale>.json`. When there is no token (a contributor, a fork, CI
without ops credentials), when ops cannot be reached, or when it refuses, the checked in
file is copied instead and cargo prints a warning:

```text
warning: ops-sdk i18n: da built from the checked in file: no OPS_API_TOKEN set
```

A key the checked in file has and ops does not is kept in the embedded tree, and the build
names it, so a string added in the repository works before it reaches ops:

```text
warning: ops-sdk i18n: da kept 1 key(s) ops does not have: booking.cancel
```

Ops wins for every key it has.

Only two things are errors, because then there is nothing to embed: a locale directory
that does not exist, and an `OUT_DIR` that cannot be written.

The pull never writes your checked in files. To bring them up to date, run
`ops tms pull <locale> --project <slug>` and commit the diff.

## Only in a build script

`run()` refuses with `NotInBuildScript` unless `OUT_DIR` is set, which cargo does for a
build script and nothing else. The API is blocking on purpose: nothing in this feature can
sit in a request path. `write_to(dir)` does the same pull into any directory, for a test.
