How we localized the Depot dashboard with General Translation banner
11 min read
engineering

Stay in the loop

Get notified when we ship new posts.

How we localized the Depot dashboard with General Translation

We partnered with General Translation to add Brazilian Portuguese, French, Japanese, Korean, and Simplified Chinese to the Depot dashboard in under three weeks.

That was quick. We implemented the first two languages on September 24. We added the General Translation integration that same day, and announced the translated dashboard on October 13. Nineteen days from the first implementation to the public launch.

I believe you should be able to check whether your build passed, find a failed job, or change your project settings in your own language. Those are normal things you need to do to use Depot. So we started with five languages that we knew we or our customers could test right away.

We're sharing how we got there because translating an existing app involves a bit more than adding a language menu. General Translation generates the translated text, and we connect it to the dashboard. That meant moving English copy out of our components, loading the right strings before the server renders a page, and making translation part of how we ship new features. Here's how that works in our app.

Why General Translation fit

We already had a React app with a lot of UI. Projects, builds, workflows, test results, billing, settings. The text was spread across all of it, including the bits you only see when something goes wrong. If you're opening the dashboard to figure out why a build failed, having to translate the interface is another thing getting in your way.

General Translation gave us a way to generate translations from the English copy in our repository. We use its CLI with TypeScript translation files, and i18next with react-i18next to display those strings in the app.

That arrangement fits how we ship. The English source, generated translations, and components live in the same repository. We can review them together and ship them together. The app reads local translation files, so opening a page doesn't need a request to General Translation to translate its text.

Our partnership with General Translation helped us get this shipped quickly. We handled the integration in Depot; their tooling handles generating the translations. That left us with the work specific to our app: finding the copy, choosing the right language for each request, and making sure the translated interface was usable.

Moving the dashboard over

We made English the source language and grouped the strings by the part of the product they belong to. Project strings live in projects.ts, workflow strings in workflows.ts, and shared things like buttons have a common namespace.

Here's a small excerpt from our English project strings:

export default {
  saveChanges: 'Save changes',
  savingChanges: 'Saving changes...',
  deleteProjectTitle: 'Delete {{projectName}}',
  deleteProjectConfirm: 'Are you sure you want to delete <0>{{projectName}}</0>?',
}

Each string has a key, like saveChanges in the example above. The component uses that key to look up the text in the user's selected language. Here's a minimal button example:

import {useTranslation} from 'react-i18next'

function SaveButton() {
  const {t} = useTranslation('projects')

  return <button type="submit">{t('saveChanges')}</button>
}

useTranslation('projects') selects the project namespace, and t('saveChanges') looks up the string within it. In English, the button says "Save changes." When the user changes language, the same key gets the text from that language's projects.ts file.

The app replaces {{projectName}} with the project’s name. The <0>...</0> tags let a React component wrap part of a translated sentence. We use react-i18next's Trans component for that. The whole sentence can be translated together, including moving the project name to wherever it belongs in that language.

We keep each sentence in one translation string instead of assembling it from separately translated fragments. English word order doesn't tell you where a name or a link belongs in Japanese.

The migration had to cover every part of the interface: headings, labels, loading states, empty tables, confirmation dialogs, tooltips, and validation errors. Otherwise, you get most of the way through a task in your language, submit a form, and get an error back in English.

We added linting to catch hardcoded UI text in the dashboard. It checks common places where we put copy, including JSX, display props, and toast messages. It doesn't replace reviewing a change, but it catches a lot of the easy ways to accidentally add untranslated text back in.

Generating the other languages

Our General Translation configuration points at the English TypeScript files and lists the languages we want. With the project identifier omitted, it looks like this:

{
  "defaultLocale": "en",
  "locales": ["pt-BR", "fr", "ja", "ko", "zh-CN"],
  "files": {
    "ts": {
      "include": ["app/locales/[locale]/*.ts"]
    }
  }
}

The same namespace exists under each language directory. app/locales/en/projects.ts is the source, and General Translation produces the matching files under pt-BR, fr, ja, ko, and zh-CN.

From English copy to a translated dashboardEnglish TypeScript files go to the General Translation CLI. Generated translations are checked for matching keys, variables, and component tags, then committed and shipped with Depot. Pages read the local files.Translate the files. Ship them with the app.General Translation generates the copy before it reaches a user.English sourceen/projects.tsThe copy we editGeneral Translationgt translateGenerate five languagesCheck the filesi18n:checkKeys, variables, tagsCommit and ship with DepotEnglish + pt-BR + fr + ja + ko + zh-CNPages render from local files. No translation API request when you open them.

We wrapped the translation command in a repository script:

pnpm run i18n:translate

That runs gt translate, checks the results, and formats the locale files. We commit the generated files and General Translation's lockfile with the source changes. Credentials are needed to generate translations; they aren't needed to build the app or render a page from the files we've already generated.

We started with French and Brazilian Portuguese on September 24, added Japanese the following day, then Korean and Simplified Chinese on October 5. Once the app could use translation files, adding another language used the same path through the app.

Checking what comes back

Translated strings still have to work as part of the application. A translation can read well and still break a confirmation dialog if it drops {{projectName}} or changes a component tag.

Our translation check compares every configured language against the English source. It checks that the keys match, that the strings aren't empty, and that variables and component tags are preserved.

For a confirmation like “Are you sure you want to delete <0>{{projectName}}</0>?” from our earlier example, the words can change and the project name can move. But the translation still needs the same {{projectName}} variable and the same component wrapper. We check balanced tags and their nesting while allowing complete sibling components to change order.

We also parse the generated TypeScript files as data. They have to contain a static object of strings; the check doesn't execute code from them to find out what's inside.

These checks are part of pnpm check, which runs in CI. They tell us whether the files have the structure the app expects. They can't tell us whether a sentence sounds natural or whether a technical term is right in context. That still needs review and feedback from people using the language.

Getting the first render right

The dashboard is rendered on the server, so choosing the language only after the browser loads would be too late. We need to know it before generating the page.

We first check for a language the user has chosen. If there isn't one, we use their browser's language preferences to choose a supported language. If none match, we use English.

We save the choice in a cookie in that browser. Language isn't an organization-wide setting, so you can use Japanese while a teammate in the same org uses French, and you can choose differently on another device.

The server then loads only the namespaces needed for the page. Other namespaces can load when they're needed.

We pass the selected language and those strings to the browser with the page, so React starts with the same translations the server used. When you navigate, we merge in the destination page's strings and keep namespaces already loaded for that language. Switching languages starts with a separate resource store.

We also create separate translation instances for server-side requests. Changing the language on a shared instance would be a problem when two people request different languages at the same time. The same rule applies to form validation: we pass the language into validation for that request, rather than changing a global error language.

When everything works, you just open the page and it's in the language you expected.

Finding the bits that don't fit

English labels can be short. Translating them can make buttons wider, push navigation items onto another line, or expose a dialog that was only ever tested with a few words in it.

We added a pseudo locale, a fake language for testing the interface. It takes our English strings, accents the characters, wraps the result in brackets, and adds roughly 40% more text. That helps us spot untranslated labels and layouts that don't leave enough room. Variables and component tags stay intact.

Making short English labels take up more spaceIllustrative buttons compare Save changes in English with its accented, bracketed pseudo translation, padded by 40 percent. The second button expands to fit the longer label. Variables and component tags are preserved.Does the longer label still fit?Our pseudo locale makes English labels longer.EnglishSave changesPseudo locale[Šàṽé çĥàñğéš one ]Same key.More room for the text.Spot untranslated text and cramped layouts. Example button for internal layout testing.

The pseudo locale makes two things easy to spot. If a label still looks like ordinary English, it probably hasn't gone through the translation system. If the expanded text gets cut off, we've found a layout that needs more room.

The pseudo locale is an internal testing tool. It doesn't tell us whether Japanese or Portuguese reads well, and it doesn't reproduce every way those languages affect layout. It gives us a quick way to find problems before reviewing the actual translations.

Keeping it translated as we ship

The dashboard keeps changing, so localization has to be part of adding a feature. We write the English copy in the relevant namespace, use those keys in the UI, generate the translations, and run the checks. The translated files ship with the feature.

For a new project setting, that means adding the label, help text, and validation messages to the project namespace as part of the same change. We run pnpm run i18n:translate and review the generated diff alongside the component. If we introduce a new namespace, we also add it to the server's preload list for the routes that use it, so those strings are ready on the first render.

This is easy to miss when you're just changing some wording. You update the English, the checks pass, but everyone using another language still sees the old copy. We have to regenerate those translations too. CI won't tell us we've forgotten if the keys and placeholders still match.

Parallel development adds another detail: different branches can update the translation files and gt-lock.json at the same time. We use General Translation's Git merge driver for the lockfile, then regenerate translations against the merged English source.

Our CI check fails if a supported language is missing a translation file or key. While we're developing a feature, i18next can fall back to English for copy we haven't generated translations for yet. That keeps the page usable while we're working, but the translations still need to be complete before the change passes CI.

We also have to keep looking at the rendered pages. Passing the string checks doesn't tell you whether a button is clipped, an error message is confusing, or a sentence reads awkwardly. If you find something like that while using Depot, please tell us.

Use Depot in your language

You can try this in the Depot dashboard. We follow your browser's language preferences, or you can open the organization menu in the sidebar and choose Language yourself.

We don't touch your source code, build logs, or tool output. We localize only the dashboard interface, so the controls can now be in a language you choose.

Under three weeks got us from an English-only dashboard to five more languages, with a way to keep them up to date. People shouldn't have to learn English just to use our dashboard.

FAQ

Does the dashboard request translations from General Translation when a page loads?

No. General Translation generates the translations before we ship. We commit the translated files with the app, and pages read those local files.

How does Depot choose the dashboard language?

We use the language you've selected, saved in a cookie in that browser. If you haven't selected one, we check your browser's language preferences for a supported language, then fall back to English. You can change your selection under Language in the organization menu.

What does Depot's pseudo locale check?

It changes the English characters, adds brackets, and makes the text roughly 40% longer. Unchanged English labels help us spot untranslated copy, and text that gets cut off reveals layouts that need more room. The pseudo locale doesn't tell us whether a real translation reads naturally.

Start building with Depot
in minutes