Cloudflare Dashboard Adds Four Languages, Opens Up on i18n Work
Cloudflare’s dashboard now supports Spanish (with locales for Chile, Ecuador, Mexico, Peru, and Spain), Brazilian Portuguese, Korean, and Traditional Chinese. That brings the total count of languages available on the dashboard to eight, building on the earlier rollout of German at the end of 2019 and French, Japanese, and Simplified Chinese in March 2020. Users can switch languages from the top right of the dashboard, and the preference is saved across sessions.
Reaching more languages meant making internationalization a repeatable part of the engineering workflow rather than a one-off retrofit. The bulk of that work falls into a few buckets: externalizing user-facing strings, keeping sentences intact through translation, formatting locale-specific data correctly, and building a pipeline that moves text between developers and translators. Here is a look at how Cloudflare approached each of those pieces.
Strings Out of the Code
The first step was pulling every user-readable string out of the application code and into standalone files. That separation gives translators clean, standardized inputs (JSON, XML, MD, or CSV) that they can load into translation management tools, and it lets engineers tweak copy without recompiling or redeploying. For the React-based dashboard, that meant converting hard-coded text into a <Trans> component backed by dictionaries keyed by translation ID. Those dictionaries — one set per language — are the catalogs the component reads from at runtime.
<Button><Trans id="signup.cancel" /></Button>
<Button><Trans id="signup.next" /></Button>
// And in a separate catalog.json file for en_US:
{
"signup.cancel": "Cancel",
"signup.next": "Next",
// ...many more keys
}
The tricky part is dynamic data. Concatenating separately translated chunks around a variable is tempting but breaks quickly because word order and inflection vary by language. When Cloudflare’s translation teams were given the segments “You’ve selected” and “Page Rules” as standalone strings, the results rendered awkwardly in Japanese and German. Giving them the whole sentence as a single string with a placeholder yielded correct translations. To keep that context, the <Trans> component supports template injection and pluralization via a smart_count feature, with singular and plural variants delimited by ||||. Markup can also be injected into placeholders by passing components and props in alongside the string.
<span>
<Trans id="pageRules.selectedForDeletion" values={{ smart_count: totalSelected }} />
</span>
// English catalog.json
{
"pageRules.selected": "You've selected %{ smart_count } Page Rule. |||| You've selected %{ smart_count } Page Rules.",
}
// Japanese catalog.json
{
"pageRules.selected": "%{ smart_count } 件のページ ルールを選択しました。 |||| %{ smart_count } 件のページ ルールを選択しました。",
}
// German catalog.json
{
"pageRules.selected": "Sie haben %{ smart_count } Page Rule ausgewählt. |||| Sie haben %{ smart_count } Page Rules ausgewählt.",
}
// Portuguese (Brazil) catalog.json
{
"pageRules.selected": "Você selecionou %{ smart_count } Page Rule. |||| Você selecionou %{ smart_count } Page Rules.",
}
Finding Missed and Broken Strings
Externalizing strings exposes two classes of bugs. The first is hard-coded text hiding in plain sight; since the rest of the page is translated, it is easy to miss until someone happens to look at the page in another language. Cloudflare’s answer is a pseudo-localization mode that substitutes unicode lookalikes during development, which also lets engineers preview how longer content will fit. German words tend to run longer than English ones, so overflow in elements like an “Add” button was an early sign that layout and copy conventions needed more flexibility. There are few easy fixes for overflow that don’t compromise the user experience, so variable content width has to be part of the design from the start.

The second bug class is typos in long translation ID keys. A missing letter can silently fall back to the base locale text, and if the string is buried in a help popover it may go unnoticed entirely. The shift to TypeScript helped here: because translation calls are typed, the editor shows a red underline for invalid keys, and the build fails when a violation is committed.
Scaling Catalogs
Cloudflare divides its translation files into catalogs that map roughly to product verticals, such as Firewall or Workers. That keeps file sizes manageable for translators — whose job units should be a single feature area — and gives developers a predictable home for new strings. A shared “common” catalog holds the strings reused throughout the app, which keeps IDs short and reduces duplication.
In total, Cloudflare’s translated copy weighs in at about 50,000 words across all languages, roughly the length of Slaughterhouse Five. Files that get too large overwhelm translation tools and editors, and this breakdown by feature keeps line counts reasonable.
Library Choices: Less Reinvention, More Control
For the base i18n functionality, Cloudflare stuck with Airbnb’s Polyglot despite looking at purpose-built React i18n libraries like react-intl and i18n-next. The reasoning was practical: Polyglot already served legacy Backbone parts of the application, and migrating those to another scheme just to unify the stack wasn’t worth the cost. So Cloudflare built its own <Trans> component on top of Polyglot and shaped its interface to suit the development team.
The chosen design does have one obvious annoyance: strings live in separate catalog files from the components that use them, and those files are often far away in the directory tree. Extraction-based libraries like jslingui sidestep that by letting developers keep strings inline in components and generating catalogs at build time. That also removes the need to type-check translation IDs. But Cloudflare found pros and cons there: translators often draw context from key names, and the approach trades typos in IDs for subtle copy mismatches — “Verify your email” versus “Verify your e-mail” — which are near-duplicates that are hard to detect and cost money to translate twice.
Dates, Times, and Numbers Need Locale Logic
Formatting injected data also deserves attention. Passing a raw number like 300,000.03 into a sentence renders incorrectly in many cultures, where digit grouping uses different separators. Dates can be ordered day-month-year versus month-day-year, with different separators and zero padding. Time formatting carries its own variations.
For dates and times, Cloudflare relied on Moment.js, which it already used widely and which shipped locale support that required little work to turn on. Bloat criticisms of Moment are valid, but the cost of swapping it out for the date handling already in place didn’t add up. Numbers were another matter entirely — with thousands of raw numbers scattered across the dashboard, each one needed to be hunted down and explicit formatting applied. The formatting itself came from the standard Intl API, whose browser support and performance have both improved significantly in recent engines. Older browsers like IE10 on Windows 8 may fall short, and polyfills are the remedy there.
var number = 300000.03;
var formatted = number.toLocaleString('hi-IN'); // 3,00,000.03
// This probably works in the browser you're using right now!
Shipping Through the Translation Pipeline
Internationalization is the engineering task — making the app easy to localize. Localization is the human process that follows. Cloudflare’s automated scripts package snapshot catalogs into JSON, create placeholder files for unsupported languages when new English strings are added, and upload the lot to the company’s translation management system. A translation memory pre-processes the new strings against previously translated ones, avoiding duplicate billing and keeping terminology consistent across releases.
Handoff timelines vary from hours to weeks, depending on job size and translator availability. When translated files come back, automated checks validate them before optional in-context review — where reviewers see how the text renders inside the product and catch errors that look fine standalone. This step relies on staff fluent both in the product and in the target language.
The process repeats continuously. Because Cloudflare releases daily and typical translation work takes two to five days, features ship in English first and get covered in other locales soon after — a tradeoff that keeps developer velocity up and accepts missing coverage. When a translation doesn’t exist for a user’s configured language, the dashboard falls back to its base locale (en_US) rather than showing raw keys.
Polish and Roadmap
Performance considerations pushed translation catalogs out of the initial page load; they are bundled per feature and fetched lazily via dynamic imports. Language preferences also carry across Cloudflare’s marketing site, support portal, and dashboard, so picking a language in one place persists everywhere.
A few known gaps remain. Collation differs by language, so string ordering logic can misbehave for users of Chinese, Japanese, or other languages that don’t use alphabets. Right-to-left languages like Arabic and Hebrew are not supported yet. Localizing API responses requires coordinated changes across microservice teams, map-based visualizations still need translation coverage, and machine translation quality has advanced but isn’t yet production-ready without human review.



