Screenshots
A translator who sees only the word "Open" can't tell whether it is a button or a status badge, or how much room it has. Screenshots fix that: each one is linked to the keys it shows, and the label editor shows it with the text marked. Add them by hand on any plan, or capture them from your Playwright tests and upload them from CI.
What translators see
Open a label from the labels table. The Screenshots panel in the editor's Context & Info section shows every screenshot linked to that key and outlines where the text appears – automatically for captures, and wherever someone marked it on an upload. Click a thumbnail to see it larger, open it at full size, and see how it was linked: by hand, by an exact marker, or by matching the text.
Two badges can appear under a thumbnail:
- Possible match – other keys have the same text on this screen, so the link may belong to one of them. Markers make these links exact.
- Outdated – a newer capture of the same screen exists. Outdated captures are deleted after a while (see Clean-up).
Every project member can see screenshots. Uploading, linking, unlinking and deleting need the developer, admin or owner role. A screenshot belongs to the key, not to a branch, so it shows for that key on every branch.
Add a screenshot by hand
Manual upload works on every plan, Free included.
- Open the label in the editor and scroll to the Screenshots panel.
- Click Upload, drop an image on the panel, or paste one – a paste works anywhere while the editor is open, so a screenshot on your clipboard is one paste away.
- Optionally drag across the image to mark where the text is. Clear box removes the mark.
- Click Save screenshot, or simply save the label: the label's Save uploads a pending screenshot first. If that upload fails, the label isn't saved yet and the panel shows why, so nothing is lost.
If you close the editor (Cancel, the X, Esc or a click outside) while a picked or pasted screenshot hasn't been saved, Translatize asks first: Save the screenshot, Discard it or Keep editing.
Uploading an image the project already has doesn't store it twice – the existing screenshot is linked to the key instead.
Link a screenshot the project already has
One screen usually shows a dozen keys. Upload it once, then open each of the other labels and click Link existing. Pick the screenshot, mark where this key's text is, and click Link to this key.
Unlink or delete
In the larger view, Unlink from this key removes the screenshot from this key only. Delete screenshot removes it from every key it is linked to and can't be undone.
Automated capture with Playwright
Automated capture is part of the Starter, Professional and Agency plans. The @translatize/playwright package takes screenshots during the Playwright tests you already run and records where every visible piece of text is. After the tests, translatize screenshots upload sends them to Translatize, which links each text to its key.
Capturing makes no network calls, so it works in parallel workers and nothing leaves your machine until you upload.
Install
The package needs @playwright/test 1.40 or newer and Node.js 18.17 or newer. Install the CLI next to it for the upload step.
npm install --save-dev @playwright/test @translatize/playwright @translatize/cliThere is nothing to configure in Translatize for the capture step. A normal Playwright config is enough – this one starts your app and points the tests at it:
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
use: { baseURL: "http://localhost:3000" },
webServer: {
command: "npm run start",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});Capture screens in a test
Import test from @translatize/playwright instead of @playwright/test. It is the regular Playwright test with one extra fixture, translatize, whose capture(name, options) takes the screenshot.
import { expect } from "@playwright/test";
import { test } from "@translatize/playwright";
test.describe("screens for translators", () => {
test("checkout", async ({ page, translatize }) => {
await page.goto("/checkout");
await expect(page.getByRole("heading", { name: "Checkout" })).toBeVisible();
await translatize.capture("checkout");
});
test("checkout in German", async ({ page, translatize }) => {
await page.goto("/de/checkout");
// only needed when the page has no <html lang>
await translatize.capture("checkout-de", { locale: "de" });
});
test("account settings", async ({ page, translatize }) => {
await page.goto("/settings/account");
await translatize.capture("settings-account", {
fullPage: true,
mask: [page.locator(".avatar"), page.getByTestId("email")],
});
});
});Each capture writes an image and a JSON file with the text positions, both named after the capture in lower case:
.translatize/captures/checkout.png
.translatize/captures/checkout.json
.translatize/captures/checkout-de.png
.translatize/captures/checkout-de.json
...Give every screen and language its own name, or one capture overwrites another. Keep names stable between runs too: a newer capture with the same name, viewport size and language replaces the older one in Translatize. Add .translatize/ to your .gitignore.
Before the screenshot, capture turns off animations and transitions, hides the text cursor and waits for web fonts, so a screenshot never catches a screen halfway through a fade. It records visible text plus the placeholder, title, aria-label and alt texts. Hidden, zero-size and aria-hidden content is skipped.
Options
| Option | Default | What it does |
|---|---|---|
fullPage | false | Capture the whole scrollable page instead of the visible viewport. |
mask | – | Playwright locators painted over in the image – use it for avatars, emails and other personal data. |
locale | the page's <html lang> | Language of the captured page. Texts are matched against the translations in this language. |
outDir | .translatize/captures | Where the capture files are written. |
Already have your own test setup?
If your tests import a test that already extends Playwright's, add the fixture to it:
import { test as base } from "@playwright/test";
import { translatize, type TranslatizeFixture } from "@translatize/playwright";
export const test = base.extend<{ translatize: TranslatizeFixture }>({ translatize });
export { expect } from "@playwright/test";Or call the function directly with any Playwright page:
import { test } from "@playwright/test";
import { capture } from "@translatize/playwright";
test("checkout", async ({ page }) => {
await page.goto("/checkout");
await capture(page, "checkout", { fullPage: true });
});Upload captures from CI
translatize screenshots upload sends every image and JSON pair in the capture directory as one capture run. It needs an API token with the Developer role (or higher): create one under Project Settings → Integrations and store it as a CI secret named TRANSLATIZE_API_TOKEN. The command works without a translatize.config.json.
export TRANSLATIZE_API_TOKEN=mcni_xxx
npx translatize screenshots uploadOnly images Translatize doesn't have yet are uploaded; the text positions of every screen are always sent. When it's done, the command prints a summary:
Capture run "main 2026-12-14 09:12 UTC" – 3 of 3 screen(s) sent
2 image(s) uploaded, 1 already on the server
run completed: 3 screen(s), 41 key link(s), 2 ambiguous, 3 supersededThis GitHub Actions workflow runs the tests on every push to main and uploads the captures. The run is named after the branch and time, and the commit is taken from GITHUB_SHA:
name: screenshots
on:
push:
branches: [main]
jobs:
capture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
- run: npx translatize screenshots upload
env:
TRANSLATIZE_API_TOKEN: ${{ secrets.TRANSLATIZE_API_TOKEN }}On GitLab CI, run the same commands in a job and set TRANSLATIZE_API_TOKEN as a masked CI/CD variable; the commit is read from CI_COMMIT_SHA.
| Flag | Description |
|---|---|
[dir] | Capture directory. Default .translatize/captures. |
--branch <name> | Branch whose translations the screens are matched against. Defaults to the config file's branch, then the token's branch. Must be a branch the token may use. |
--run-name <name> | Name of the run. Defaults to the CI branch and the time. |
--commit-sha <sha> | Commit the screens come from. Defaults to GITHUB_SHA or CI_COMMIT_SHA. |
--json | Print the result as JSON. |
A run holds up to 500 screens – with more, the command stops before uploading and asks you to split them into several directories. A project can upload 30 runs a day. The command exits with code 1 if any screen failed, so CI shows it. The full flag and exit-code reference is in the CLI docs, and the endpoints behind it are in the Integration API.
How captures are linked to keys
Translatize compares the text on each screen with your translations on the run's branch, in the language of the captured page (the source language when the page has none), and links every match to its key. Text that several keys share is linked to each of them and shown as a Possible match.
When a run completes, a newer capture replaces the older one with the same name, viewport size and language; the older one is marked Outdated. Links made by hand are never changed by a capture run.
Project Settings → Screenshots (for developers, admins and owners) lists the recent capture runs with their branch, commit and counts: screens, links, possible matches and replaced screens. The same tab repeats the setup steps for your project.
Exact links with markers
Matching by text can't tell two keys apart when both say "Open". Markers can. In test builds, your app appends an invisible code for its key to every translated string, and each text is linked to exactly that key. For i18next, add the post-processor from @translatize/playwright/markers:
import i18next from "i18next";
import { postProcessor } from "@translatize/playwright/markers";
i18next.use(postProcessor).init({
postProcess: ["translatizeMarkers"],
// link fallback keys to the key that was actually used
postProcessPassResolved: true,
// ...your existing options
});It does nothing unless TRANSLATIZE_MARKERS=1 is set for the build under test or the page URL contains ?translatize-markers, so normal use is untouched. For other libraries, call wrapWithMarker(key, value) from the same entry point. Keys must match your Translatize keys as they are: i18next namespace prefixes such as common: are dropped, so if your Translatize keys include the namespace, those strings fall back to text matching.
Storage and plans
| Plan | Screenshot storage | Manual upload | Automated capture |
|---|---|---|---|
| Free | 20 MB | Yes | – |
| Starter | 1 GB | Yes | Yes |
| Professional | 5 GB | Yes | Yes |
| Agency | 25 GB | Yes | Yes |
Storage belongs to the account that pays for the project – the project owner, or the workspace owner for a project in a workspace – and is shared by every project that account pays for. Uploads by hand and captures from CI both count. You can see how much is used under Project Settings → Screenshots and in the editor's Screenshots panel, and the paying account also sees it on its dashboard and subscription page.
When storage is full, new screenshots are refused until some are deleted or the plan is upgraded. Existing screenshots stay, and the account owner gets a notification in the app. In CI, the upload stops at that point, completes the run with what was already sent, and exits with code 1.
Clean-up
- Outdated captures are deleted after 7 days.
- Captures that no key links to are deleted after 30 days.
- Screenshots you uploaded by hand stay until someone deletes them, and so does a capture that someone linked to a key by hand.
- Deleting a project deletes its screenshots.
Privacy
Screenshots are visible only to members of the project, through short-lived links. They are never sent to AI – AI translation uses the key's description, character limit and the other context you choose, not the image.
A screenshot shows whatever was on the screen. Capture with test data, and use mask for anything personal that still appears.
