# General Translation Knowledge Base: How do I translate Lottie animations?
URL: https://generaltranslation.com/en-US/kb/translate-lottie-animations.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Translate the strings in the animation's text layers, stored in the Lottie JSON at t.d.k[].s.t, then check that each translation fits its box, fonts cover the new script, and no text was exported as shapes. Re-export per language, swap text at run time, or script the JSON; General Translation's CLI translates dotLottie 1.0 .lottie files and refits the layout.
Category: Translation
Last updated: 2026-10-28
Authors: General Translation Team
Published: 2026-10-28

A Lottie animation is JSON, usually exported from After Effects with the Bodymovin plugin, and its words sit inside text layers, not in a separate string file. Translating one means replacing that text per language without breaking the animation. The words are the easy part; the hard part is the fixed box sizes, embedded fonts and glyphs, and right-to-left scripts around them.

## Where text lives in a Lottie file

Layers are typed by `ty`, and text layers are type `5`. Each text layer has a `t` object whose document, `t.d`, holds keyframes in `k`; each keyframe's `s` is a text document whose `t` is the string itself, with line breaks written as `\r`. A text document also names its font (`f`), size (`s`), and, for box text, the box size (`sz`) and position (`ps`). Text that changes mid-animation has one keyframe per change ([Lottie docs: Text](https://lottiefiles.github.io/lottie-docs/text/), [Layers](https://lottiefiles.github.io/lottie-docs/layers/)):

```json title="animation.json (excerpt)"
{
  "fonts": {
    "list": [
      { "fName": "Inter-Bold", "fFamily": "Inter", "fStyle": "Bold", "origin": 0 }
    ]
  },
  "layers": [
    {
      "ty": 5,
      "nm": "Headline",
      "t": {
        "d": {
          "k": [
            { "t": 0, "s": { "t": "Start your free trial", "f": "Inter-Bold", "s": 48, "sz": [600, 80], "ps": [-300, -40] } }
          ]
        }
      }
    }
  ]
}
```

Three other places matter:

- **Precomps.** Nested compositions keep their layers in `assets[].layers`, so text layers can sit there too, not only in the top-level `layers` array.
- **Fonts and glyphs.** `fonts.list` names each font. If the designer turned on Bodymovin's **Glyphs** setting, the file also has a `chars` array with an outline for every character it uses; with it off, the player needs the font file itself ([Lottie: After Effects export settings](https://lottie.airbnb.tech/#/after-effects)). Glyphs render on Android and the web but not on iOS, which needs fonts ([Lottie: Supported features](https://lottie.airbnb.tech/#/supported-features)).
- **Text converted to shapes.** Text a designer converted to shapes in After Effects becomes a shape layer (`ty` 4), with no string left to translate. It has to go back to a text layer in After Effects.

A `.lottie` file is a dotLottie package: a ZIP archive with a `manifest.json` and one or more animation JSON files, plus optional images and fonts ([dotLottie 2.0 spec](https://dotlottie.io/spec/2.0/)). The text sits in the same place inside each animation JSON.

## What goes wrong when you translate an animation

- **Text expansion.** Translations rarely match the source length. On [General Translation's site](https://generaltranslation.com), "Save changes" measures 63% wider in German than in English. A box text layer has a fixed `sz`, so longer text wraps into extra lines the design never planned for, and point text (no box) can run past the frame.
- **Missing characters.** A Latin-only font has no Japanese, Chinese, or Arabic characters. With glyphs exported, the file only has outlines for the characters the English text used; lottie-web's maintainers note that all glyphs for new text "must have been exported from AE" ([lottie-web wiki](https://github.com/airbnb/lottie-web/wiki/TextLayer.updateDocumentData)).
- **Right-to-left text.** Player support is uneven. lottie-android has open issues for right-to-left text ([#1145](https://github.com/airbnb/lottie-android/issues/1145)) and for Arabic letters that render unjoined ([#2279](https://github.com/airbnb/lottie-android/issues/2279)). (See [How do I support RTL (right-to-left) languages in CSS?](/kb/right-to-left-css) for the page around the animation.)
- **Text driven by expressions.** After Effects expressions are JavaScript stored in the file (the `x` property). A text layer whose content comes from an expression is computed as the animation plays, so there's no fixed string to translate.

Test the source animation with longer, accented text before translating. (See [What is pseudo-localization?](/kb/pseudo-localization).)

## Ways to translate a Lottie animation

| Approach | Where the text changes | Handles fit and fonts | Repeatable on every release | What it leaves to you |
| --- | --- | --- | --- | --- |
| Re-export per language from After Effects | A designer duplicates the composition, edits each text layer, and exports one file per language | A designer adjusts each version by eye | No; every copy change repeats per language | One file per language and designer time for each |
| Swap text at run time | The player replaces strings as the animation renders | Partly; lottie-web can shrink box text to fit, but the font, glyphs, and surrounding layers stay as they are | Yes; strings come from your app's translations | Fit, font coverage, and glyphs in every language, plus stable keys (source text or layer names) |
| Script edits to the JSON | A script replaces each `s.t` value, often with machine translation or an LLM | No | Yes | Line breaks, fit, fonts, glyphs, and re-zipping `.lottie` files |
| General Translation CLI | [`gt stage`](/docs/cli/reference/commands/stage) and [`gt download`](/docs/cli/reference/commands/download) produce a translated `.lottie` file per language | Layout processing refits text and nearby layers using fonts you upload | Yes | Reviewing each animation, removing expressions, and Layout Processing usage |

Runtime swapping is built into the players. In lottie-android, a `TextDelegate` maps source text to replacement text, and `LottieProperty.TEXT` sets it through dynamic properties ([Lottie for Android](https://lottie.airbnb.tech/#/android)):

```kotlin
// Replace the animation's English text with the localized string resource.
val textDelegate = TextDelegate(animationView)
textDelegate.setText("Start your free trial", getString(R.string.trial_cta))
animationView.setTextDelegate(textDelegate)
```

In lottie-ios, set `animationView.textProvider` to a `DictionaryTextProvider` keyed by the text layer's keypath ([lottie-ios source](https://github.com/airbnb/lottie-ios/blob/master/Sources/Public/TextProvider/AnimationTextProvider.swift)). In lottie-web, call `updateDocumentData` on the text layer element. The dotLottie web player's `setTextSlot` replaces text the designer exposed as a slot ([dotLottie slots](https://docs.lottiefiles.com/en/runtimes/distributions/js/v0.x/core-concepts/slots)). lottie-web can also shrink box text to fit: [`canResizeFont(true)`](https://github.com/airbnb/lottie-web/wiki/TextLayer.canResizeFont) reduces a text layer's font size until the string fits its box, down to a floor you set with [`setMinimumFontSize`](https://github.com/airbnb/lottie-web/wiki/TextLayer.setMinimumFontSize). None of these APIs moves or resizes the layers around the text, such as a button shape behind a label.

Re-export from After Effects when an animation ships in one or two languages and a designer wants to tune every frame. Swap text at run time when the words come from your app, such as a user's name, and the box has room for the longest translation. Choose General Translation when animations ship in many languages, change with releases, and need layouts refit for each language. Script the JSON yourself only if you'll own the fit and font checks.

## How to translate Lottie files with General Translation

General Translation's CLI, `gt`, translates dotLottie files with the `lottie` file key (`gt` 2.16.0 or later). Add a `lottie` entry with a `[locale]` placeholder in the path:

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["es", "ja"],
  "files": {
    "lottie": {
      "include": ["animations/[locale]/**/*.lottie"]
    }
  },
  "fonts": {
    "include": ["assets/fonts/**/*.{ttf,otf}"]
  }
}
```

Source animations come from `animations/en/`, and translations land in `animations/es/` and `animations/ja/`. Lottie translation runs asynchronously, so submit with [`npx gt stage`](/docs/cli/reference/commands/stage), then run [`npx gt download`](/docs/cli/reference/commands/download) until every locale is done; locales still processing are skipped and fetched on a later run. [`gt translate`](/docs/cli/reference/commands/translate) can't finish an unstaged Lottie job in one run and tells you to stage it. Downloads are complete `.lottie` files; they aren't published to the CDN.

General Translation reads the animation JSON from the package's `animations/` folder, the dotLottie 1.0 layout. dotLottie 2.0 packages keep it in `a/`, and a plain `.json` animation isn't a package at all. Convert either with [`@dotlottie/dotlottie-js`](https://github.com/dotlottie/dotlottie-js), whose `toDotLottieV1` and `DotLottieV1` produce the 1.0 layout.

After translation, layout processing keeps the animation's font sizes, reflows text, and moves nearby layers to fit each label. When a label still doesn't fit, it may shorten or rephrase it in the target language while keeping the meaning, and an ellipsis is the last resort; monospaced code tokens are never shortened. If the layout changes can't be rendered and verified, you get the plain translated animation instead. Watch each download before publishing it. (See the [Lottie reference](/docs/cli/reference/formats/lottie-files).)

The top-level [`fonts`](/docs/cli/reference/config#fonts) setting uploads your `.ttf` and `.otf` files so layout processing can use the animation's real fonts; when the animation embeds glyphs, General Translation adds outlines for the characters each translation needs, drawn from your uploaded fonts where they cover them. Fonts are Organization assets, so uploading needs a saved login or an Organization API key with `org:assets:write`; a project API key can't upload them, and translation continues with fallback fonts and a warning. Files with After Effects expressions are rejected, because expressions are executable code; re-export with expressions removed or baked into keyframes.

### What layout processing changed in a test

We built a 480 × 160 animation of a pill button: an 18px icon, then the label "Download invoice" in 22px Arial Bold, with 16 to 18px of padding. We translated it into German, French, and Japanese with `gt` 2.26.11 and a project API key, so no fonts were uploaded and layout processing used its fallback fonts. We measured each label in lottie-web 5.13's SVG renderer with the browser's fonts. The full-translation rows are General Translation's translations of each label as a plain string, outside the animation, placed in the original button for comparison:

| Version | Label text | Label width | Button width | Result |
| --- | --- | --- | --- | --- |
| English source | Download invoice | 187px | 251px | Fits, 11px from the icon |
| German, full translation | Rechnung herunterladen | 260px | 251px | Covers the icon by 25px and runs 18px past the edge |
| German, General Translation | Rechnung laden | 171px | 251px | Fits |
| French, full translation | Télécharger la facture | 229px | 251px | Covers the icon by 9px and runs 2px past the edge |
| French, General Translation | Télécharger facture | 204px | 273px | Fits; the button grew 22px to the right, and the label sits 3px from the icon |
| Japanese, full translation | 請求書をダウンロード | 220px | 251px | Covers the icon by 5px |
| Japanese, General Translation | 請求書取得 | 111px | 251px | Fits |

Every label kept its 22px size and fit. German and Japanese got shorter wordings instead of a wider button: _Rechnung laden_ ("load invoice") and 請求書取得 ("get invoice") are terser than the usual labels. French kept its verb, dropped the article, and got a wider button, but the label ended up closer to the icon than in the source. Whether a shorter wording or a tighter gap is acceptable is a reviewer's call, and results can differ between runs.

A second animation in the same test had a 180px "Save changes" button. It came back as _Speichern_, _Enregistrer_, and 保存, the standard short labels, which fit without any layout change. The same run translated its heading, "Your changes are ready," into Japanese as 変更内容を保存できます ("You can save your changes"), a change in meaning that only a person reading the result would catch.

Lottie translation adds a Layout Processing charge, priced separately from the translation itself and based on the animation's number of animated properties ([usage metering](/docs/platform/dashboard/guides/managing-billing#usage-metering), [pricing](https://generaltranslation.com/pricing)). Glossary terms and Custom Prompts from your [Context Group](/docs/platform/dashboard/guides/defining-context-for-translations) apply to animation text too, so a feature name in an onboarding animation matches your app and your [JSON locale files](/kb/translate-json-files).

## Common questions

### Why does my translated animation show empty boxes or missing letters?

The font or the embedded glyphs don't cover the new characters. Ship a font that covers the script. With a glyph export, the file needs an outline for every character the translation uses, so re-export that language from After Effects with the translated text in place.

### Do I need a separate file for each language?

With runtime swapping, no: one file serves every language. Re-exporting and the General Translation CLI produce one file per language, which you load by locale.

### Can I translate text that was converted to shapes?

No. Shapes have no string to replace, so recreate the text layer in After Effects and export again.

## Key takeaways

- Lottie text lives in text layers (`ty` 5) at `t.d.k[].s.t`, including text layers inside precomps under `assets`.
- Glyph exports contain outlines only for the source text's characters, and iOS doesn't render glyphs; check fonts for every script you add.
- Runtime APIs (`TextDelegate`, `DictionaryTextProvider`, `updateDocumentData`, `setTextSlot`) swap strings; lottie-web's `canResizeFont` can shrink box text to fit, but none of them moves the surrounding layers.
- General Translation's CLI translates dotLottie 1.0 `.lottie` files through [`gt stage`](/docs/cli/reference/commands/stage) and [`gt download`](/docs/cli/reference/commands/download), refits layouts with your uploaded fonts, and rejects files with expressions.
- In our test, layout processing kept font sizes and fit every label, by widening a button or by shortening the wording; review each download for wording and spacing.
- Layout Processing for Lottie is priced separately, based on the animation's animated properties.

## Related questions

- [How do I translate i18n JSON files?](https://generaltranslation.com/en-US/kb/translate-json-files.mdx): Translate only the string values in a JSON locale file. Keep nonplural keys, placeholders such as {{name}}, and tags unchanged, but expect plural keys to differ by language: Polish needs inbox_few and inbox_many. Use a format-aware tool or API, then parse and validate the output before review.
- [How do I support RTL (right-to-left) languages in CSS?](https://generaltranslation.com/en-US/kb/right-to-left-css.mdx): Put dir="rtl" and lang on the html element, because lang alone doesn't change direction, then replace physical CSS such as margin-left with logical properties such as margin-inline-start. Flex rows, grid columns, and default text alignment flip on their own; translateX(), left, and right don't.
- [What is pseudo-localization?](https://generaltranslation.com/en-US/kb/pseudo-localization.mdx): Pseudo-localization is a test that swaps your source text for an altered version, accented, padded about 40% longer, and wrapped in brackets, so hardcoded strings, truncation, broken fonts, and concatenated sentences show up before any translation exists. Android ships the en-XA and ar-XB pseudo-locales for it.
- [How do I translate Google Slides?](https://generaltranslation.com/en-US/kb/translate-google-slides.mdx): Copy the deck once per language, replace the text in every text box, table cell, and speaker note, and resize text that overflows. General Translation's Google Drive integration writes each language to its own copy next to the source and runs a formatting pass to fit the new text to each slide.
- [How do I translate SRT subtitles without breaking timing?](https://generaltranslation.com/en-US/kb/translate-srt-subtitles.mdx): Translate only the cue text in an SRT file; keep cue numbers, timecodes, blank-line separators, and formatting tags as in the source. Then check line length and reading speed against your destination's requirements: Netflix's English guide, for example, allows 42 characters per line and 20 characters per second for adult programs.

## Previous and next questions

- Previous question: [How do I translate content in Payload CMS?](https://generaltranslation.com/en-US/kb/translate-payload-content.mdx)
- Next question: [How do I translate PO and POT files?](https://generaltranslation.com/en-US/kb/translate-po-files.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/en-US/kb/sitemap.md) for every article.
