Skip to content

Languages

ScreenCI supports multiple language versions of a video from a single script. You declare the languages once and ScreenCI records a separate pass per language, setting the browser locale automatically so a self-localizing app renders in the right language without extra work from you.

A plain video with no .languages(...) call records one round that stays language-agnostic (no [en] tag), pinned to the en-US browser locale.

Narration accepts a per-language object form: the same cue keys under each language code. The language set is inferred from the union of the narration keys (plus any .languages(...) call), so adding a language there is enough to produce a version. TypeScript validates that every language covers the same cues, which catches drift early. Overlays are shared across every language (see below); only narration (and per-language browser locales) vary by language.

One language per plan

Multiple languages are a Business feature. On the Free and Starter plans, your organization renders a single narration language across all of its videos:

  • An upload that declares more than one language is blocked.
  • Once your organization has rendered one language, an upload in a different language is blocked until you delete the videos and versions in the first language. This keeps the limit at one language in total, not one per video.
  • A per-cue language override (see Speak a cue in a different language) counts as a language too, so it cannot be used to slip a second spoken language past the limit.

Free and Starter narrate that one language with the built-in voice or your own self-recorded voice. See Voices and plans.

Upgrade to Business to render as many languages as you like. When an upload is blocked, the CLI prints the reason and a link back to this section.

Add languages

Add languages by keeping the same cue keys under each language code in video.narration(...):

video.narration({
en: { intro: 'Open the settings page.' },
fi: { intro: 'Avaa asetussivu.' },
})('Settings', async ({ page, narration }) => {
await narration.intro()
})

Use bare language keys such as en, fi, fr, and cmn. You can also add a default key as a shared fallback: any cue missing for a language falls back to the default value, for example video.narration({ default: { intro: 'Hi' }, fr: { intro: 'Salut' } }).

Overlays do not vary by language. video.overlays(...) takes a names-only array or a single shared name -> config object, and the same overlay is used in every language’s recording pass:

video.overlays({
badge: { path: 'assets/badge.png', x: 1382, y: 65, width: 384 },
})('Landing', async ({ page, overlays }) => {
await page.goto('/')
await overlays.badge() // the same badge in every language
})

If you need a language-specific asset, swap the overlay file per language in the web editor rather than in code.

Localized recordings (per-language capture)

By default a localized video records a separate pass per language, setting the browser locale from the language and exposing the active language to the body. That is ideal when the UI itself differs per language: the app renders translated text, you navigate to a localized route, or you want the browser locale set.

import { video, voices } from 'screenci'
video.narration({
en: { intro: 'Open the settings page.' },
fi: { intro: 'Avaa asetussivu.' },
})('Tutorial', async ({ page, language, narration }) => {
// `language` is the language being recorded in this pass ('en' or 'fi').
// The browser locale is set automatically (en -> en-US, fi -> fi-FI), so a
// self-localizing app renders in the right language. You can also navigate
// per language.
await page.goto('/' + language)
await narration.intro()
})

Each declared language becomes its own recording pass, and the passes group into a single video with one language version each. The declared languages are the single source of truth: the narration map must cover exactly those languages, so a forgotten translation fails loudly instead of silently drifting.

Choosing the locale

Locales default from the language (fi -> fi-FI). Override per language with locales on video.languages(...) when you need a specific region. The object form of video.languages(...) takes the explicit languages set alongside its options:

video
.narration({
en: { intro: 'Welcome.' },
pt: { intro: 'Bem-vindo.' },
})
.languages({
languages: ['en', 'pt'],
locales: { en: 'en-GB', pt: 'pt-BR' },
})('Pricing', async ({ page, language }) => {
await page.goto('/' + language + '/pricing')
})

To skip setting the browser locale entirely, pass browserLocale: false to video.languages(...).

Shared capture mode

To capture once and overdub narration per language at render (instead of a pass per language), pass mode: 'shared' to video.languages(...). This is ideal when the visible UI is identical across languages. The body’s language fixture is then undefined:

video
.narration({
en: { intro: 'Welcome.' },
fi: { intro: 'Tervetuloa.' },
})
.languages({ mode: 'shared' })('Tour', async ({ page, narration }) => {
await page.goto('/')
await narration.intro()
})

Recording only some languages

To record (and render) a subset, pass --languages to screenci export:

Terminal window
screenci export --languages fi
screenci export --languages fi,en

Per-language videos record only the requested languages, so a run never produces more than you asked for. A shared-mode recording is a single capture and is not split by this filter.

The filter only restricts which languages are recorded and rendered this run, not which languages your video declares. Every recording still reports the full code-defined language set, so the app keeps showing the languages you did not render this time (rather than treating them as removed from code).

Variants with each

video.each([...]) produces a separate video per variant, for cases like viewport or theme. Each variant has its own identity and history. It chains with the per-feature methods:

video
.each([
{ key: 'mobile', recordOptions: { aspectRatio: '9:16' } },
{ key: 'desktop', recordOptions: { aspectRatio: '16:9' } },
])
.narration({
en: { intro: 'Welcome.' },
fi: { intro: 'Tervetuloa.' },
})('Landing', async ({ page, language, narration }) => {
await page.goto('/' + language)
await narration.intro()
})

This records Landing mobile and Landing desktop as separate videos, each with en and fi language versions.

Run modifiers

A localized video builder supports the usual run modifiers, chained before the call: .only(...), .skip, .fixme, and .fail. The in-body conditional video.skip(condition, reason) still exists separately for skipping mid-test.

Managing languages from Editor

The recorded language set is the union of the code set declared with video.languages([...]) and any language keys used by per-language features. Code is the single source of truth.

import { video } from 'screenci'
video.narration({ en: { intro: 'Hi' } }).languages(['en', 'fi'])(
'Product tour',
async ({ page, narration }) => {
await narration.intro()
await page.goto('/dashboard')
}
)

To set the capture options too, pass a config object, for example video.languages({ languages: ['en', 'fi'], mode: 'shared' }). With no video.languages(...) declaration the set is inferred from the per-feature language keys, falling back to the implicit en default for a plain video.

The language menu on the Editor page lists the current languages and lets you add one. Adding a language auto-translates the existing narration from the language you are viewing and writes the new language into your video.languages([...]) declaration in code (a new .languages([...]) call is added when the video has none). The code edit applies through the connected screenci edit machine; with no machine connected it is queued and applies automatically the next time one connects. The new language is watchable in the live preview immediately (it borrows another language’s footage until its first record) and you can edit every translated line afterwards.

Deleting a language from the same menu removes its exports and narrations and removes the language from the video.languages([...]) declaration the same way (queued when no machine is connected). The code edit also removes the language’s entries from language-major video.narration({...}) and video.values({...}) declarations, so its cue texts do not linger in your sources. The default language cannot be deleted.

Available languages

The language-major forms (such as video.narration(...)) and video.languages(...) accept the supported language keys below.

For the built-in voices, narration coverage depends on the voice’s modelType (see Narration). The consistent model (the default) and the expressive model cover different language sets: most languages work with either, some are available only with the expressive model, and Cantonese (yue) is available only with the consistent model. Narrating a language with a model that does not cover it fails at record time with a message telling you which modelType to use.

The per-model split applies only to the built-in voices. Your own ElevenLabs voices (including a voice you record and clone from a sample) are multilingual and cover every key in either table, so any language below works with them regardless of modelType.

(Overlays and other non-narration features also work for every key regardless of model, since they carry no synthesized speech.)

One built-in-voice exception is worth calling out: Russian (ru) currently supports the built-in names Ava, Daniel, Emma, Leo, Lily, Max, Miles, and Nora. When a shared default also needs to cover Russian, use one of those names; the built-in fallback voice is Ava.

Available with any model

These narrate with both the consistent (default) and expressive models:

Language Key
Arabic ar
Bengali bn
Bulgarian bg
Croatian hr
Czech cs
Danish da
Dutch nl
English en
Estonian et
Finnish fi
French fr
German de
Greek el
Gujarati gu
Hebrew he
Hindi hi
Hungarian hu
Indonesian id
Italian it
Japanese ja
Kannada kn
Korean ko
Latvian lv
Lithuanian lt
Malayalam ml
Mandarin cmn
Marathi mr
Norwegian Bokmal nb
Polish pl
Portuguese pt
Punjabi pa
Romanian ro
Russian ru
Serbian sr
Slovak sk
Slovenian sl
Spanish es
Swahili sw
Swedish sv
Tamil ta
Telugu te
Thai th
Turkish tr
Ukrainian uk
Urdu ur
Vietnamese vi

Expressive model only

With the built-in voices these narrate only with the expressive model, which is their only built-in voice, so it is selected automatically on every plan: you do not need to set modelType, and Free and Starter can narrate one of these languages just like any other. They also work with your own ElevenLabs or sample-cloned voice, and for non-narration features (such as locale selection) they behave like any other key.

Language Key
Afrikaans af
Albanian sq
Amharic am
Armenian hy
Azerbaijani az
Basque eu
Belarusian be
Burmese my
Catalan ca
Cebuano ceb
Filipino fil
Galician gl
Georgian ka
Haitian Creole ht
Icelandic is
Javanese jv
Konkani kok
Lao lo
Latin la
Luxembourgish lb
Macedonian mk
Maithili mai
Malagasy mg
Malay ms
Mongolian mn
Nepali ne
Norwegian Nynorsk nn
Odia or
Pashto ps
Persian fa
Sindhi sd
Sinhala si

Consistent model only

Cantonese narrates only with the consistent (default) model. Selecting modelType: 'expressive' for it fails at record time. It also works with your own ElevenLabs or sample-cloned voice.

Language Key
Cantonese yue