
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Sun, 05 Apr 2026 05:28:36 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Internationalization and localization: bringing Cloudflare Radar to a global audience]]></title>
            <link>https://blog.cloudflare.com/cloudflare-radar-localization-journey/</link>
            <pubDate>Mon, 16 Dec 2024 14:00:00 GMT</pubDate>
            <description><![CDATA[ Internationalization and localization require more than translation: tone, images, date/time and number formatting, among other items, need to be considered. ]]></description>
            <content:encoded><![CDATA[ <p><a href="https://radar.cloudflare.com/">Cloudflare Radar</a> celebrated its fourth birthday in September 2024. As we’ve expanded Radar’s scope over the last four years, the value that it provides as a resource for the <b>global</b> Internet has grown over time, and with Radar data and graphs often appearing in publications and social media around the world, we knew that we needed to make it available in languages beyond English.</p><p>Localization is important because most Internet users do not speak English as a first language. According to <a href="https://w3techs.com/technologies/history_overview/content_language/ms/y"><u>W3Techs</u></a>, English usage on the Internet has dropped 8.3 points (57.7% to 49.4%) since January 2023, whereas usage of other languages like Spanish, German, Japanese, Italian, Portuguese and Dutch is steadily increasing. Furthermore, a <a href="https://csa-research.com/Featured-Content/For-Global-Enterprises/Global-Growth/CRWB-Series/CRWB-B2C"><u>CSA Research study</u></a> determined that 65% of Internet users prefer content in their language.</p><p>To successfully (and painlessly) localize any product, it must be internationalized first.  Internationalization is the process of making a product ready to be translated and adapted into multiple languages and cultures, and it sets the foundation to enable your product to be localized later on at a much faster pace (and at a lower cost, both in time and budget). Below, we review how Cloudflare’s Radar and Globalization teams worked together to deliver a Radar experience spanning twelve languages.</p>
    <div>
      <h2>What is localization?</h2>
      <a href="#what-is-localization">
        
      </a>
    </div>
    <p>Localization (l10n) is the process of adapting content for a region, including translation, associated imagery, and cultural elements that influence how your content will be perceived. The goal, ideally, is to make the content sound like it was originally written with the region in mind, incorporating relevant cultural nuances instead of merely replacing English with translated text.</p><p>Localization includes, among others:</p><ul><li><p><b>Language</b>: Translation, obviously, but it’s just the beginning.</p></li><li><p><b>Tone and message</b>: Localization considers what will resonate with your target audience, not just what’s accurate.</p></li><li><p><b>Images</b>: What may be appropriate in one country can be problematic in another (maps, for instance, that tend to include disputed territories). </p></li><li><p><b>Date, time, measurement, and number formats</b>: Formats change based on location and may differ even within the same language. In the U.S., the date follows this format: “December 15, 2018.” But in the U.K., that same date would be written like this: “15 December 2018.” Not to mention a constant source of confusion: the month/day/year vs.day/month/year difference:</p></li></ul>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/ChruPHkbCpKkaDNfnd3wK/13a299038c2fc70e71fc7e4e71fb8671/image9.png" />
          </figure><p><sup><i>Image: XKCD, </i></sup><a href="https://xkcd.com/2562/"><sup><i><u>https://xkcd.com/2562/</u></i></sup></a></p><p>Pixar movies are a great example of localization<b><i>. </i></b>Pixar takes great care to internationalize their movie production process, so they can replace or insert scenes that will resonate with watchers all over the world, not just the US. Let’s consider <i>Inside Out</i> (2015). During the movie, Riley reminisces about playing ice hockey back in Minnesota. Most of the world is not as familiar with ice hockey as in the US, so Pixar wisely decided that they would use soccer elsewhere, allowing a more direct emotional connection with those audiences. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2vmuz0H3YXa6sAh0EJpj0E/5ed37b80f24932c6082a079a587c1ac4/Screenshot_2024-12-14_at_16.15.15.png" />
          </figure><p><sup><i>Images: scene from Inside Out (2015), produced by Pixar Animation Studios and Walt Disney Pictures. Copyright Pixar Animation Studios and Walt Disney Pictures. Images used under fair use.</i></sup></p><p>And you don’t have to go to computer animated movies. Here’s an example from The Shining (1980) where the famous “All work and no play makes Jack a dull boy” typewriter scene was localized into all languages differently. <i>The producers, in a pre-Information Technology example of internationalization, shot and cut the localized scene into the local versions of the movie.</i></p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3GNqgdcxlk9fLll5OQgilJ/5bfe683547230280ea84741a7c7c0405/Screenshot_2024-12-14_at_16.16.28.png" />
          </figure><p><sup><i>Images: scene from The Shining (1980), directed by Stanley Kubrick. Copyright Warner Bros. Pictures. Images used under fair use.</i></sup></p>
    <div>
      <h2>Internationalization</h2>
      <a href="#internationalization">
        
      </a>
    </div>
    <p>Localization is hard, and no one in the business will tell you otherwise. Fortunately there’s a playbook: the first step to localization is <b>internationalization</b> (i18n). <b>Internationalization</b> is the process of making a product ready to be translated and adapted into multiple languages and cultures. It's a preparatory step that helps with translation and localization. The more you internationalize your code and the more you take into account language and cultural nuances, the easier the localization will be.</p>
    <div>
      <h3>Hard-coding and externalization</h3>
      <a href="#hard-coding-and-externalization">
        
      </a>
    </div>
    <p>The first step to internationalize Radar was to assess how many of the localizable strings were hard-coded. Hard coding is the practice of embedding data directly into the source code of a program. Although a convenient and fast way to write your code, it makes it more difficult to change or localize the code later.</p><p>Most of the strings that make up the Radar pages used to be hard-coded, so before we could begin translating, <b>externalization</b> had to be done, which is the process of extracting any text that needs to be localized from the code and moving it into separate files.</p><p>Hard-coded strings:</p>
            <pre><code>import Card from “~/components/Card”;
import Chart from “~/components/Chart”;

export default function TrafficChart() {
  return (
    &lt;Card
      title="Traffic"
      description="Share of HTTP requests"
    &gt;
      &lt;Chart /&gt;
    &lt;/Card&gt;
  );
}</code></pre>
            <p>Externalized key placeholders:</p>
            <pre><code>import { useTranslation } from "react-i18next";
import Card from “~/components/Card”;
import Chart from “~/components/Chart”;

export default function TrafficChart() {
  const { t } = useTranslation();
  return (
    &lt;Card
      title={t("traffic.chart.title")}
      description={t("traffic.chart.description")}
    &gt;
      &lt;Chart /&gt;
    &lt;/Card&gt;
  );
}</code></pre>
            <p>There are several benefits to externalizing strings:</p><ul><li><p>It allows translators to work on separate, isolated files that contain only localizable strings </p></li><li><p>It prevents accidental changes to the code </p></li><li><p>It allows developers to deploy updates, changes, and fixes without having to recompile or redeploy code for each language every time</p></li></ul><p>If you look at the example below, when the code is compiled or deployed, upon reaching line 10 (on the left), it will find a key named <code>traffic.chart.title</code>. It will then proceed to match that key within the JSON file on the right, finding it on line 1090 and resolving it to “<code>Traffic</code>” for English, "<code>Tráfego</code>" for Portuguese and "<code>トラフィック</code>" for Japanese, doing this for every localized JSON file present in the code.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/14pNiwO9UTdrwbFzq2VUvi/c1a163bc98915195a0cb8ccb29639365/image1.png" />
          </figure>
    <div>
      <h3>Pseudo translation</h3>
      <a href="#pseudo-translation">
        
      </a>
    </div>
    <p>Not all strings are easily found and some are buried deep in the code, sometimes in legacy, inherited code or APIs. Fortunately, there are some strategies that help detect hard-coded strings. This is where <b><i>pseudo translation</i></b> comes into play.</p><p><i>Pseudo translation</i> is a process that replaces all characters in a string with similar-looking ones; pseudo translated strings are enclosed within [ ] characters, and some extra characters are added to them to simulate text expansion (<a href="#text-expansion"><u>more on that later</u></a>). It is an invaluable tool to help us find any hard coded strings, and to stress test the UI for language readiness and length variability, while still keeping the content mostly readable. For example, this string:</p><p><code>Routing Information</code></p><p>looks like this once pseudo translated:</p><p><code>[R~óútíñg Í~ñfó~rmát~íóñ]</code></p><p>Once pseudo translation is done, any English strings left intact are most likely hard coded or come from other sources. In the screenshot below you can see how <code>ASN</code>, <code>Country</code>, <code>Name</code> and <code>Prefix Count</code> did not get pseudo translated and had to be externalized by the Radar developers. The Globalization team collaborated with the Radar team to report and fix hard-coded text issues, as well as the issues that are mentioned in the next few sections.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/16SjYR099rxBLw0YuptB9n/7ca430aa1544349c3e2eff45cdbdfe60/image4.png" />
          </figure>
    <div>
      <h3>Text expansion</h3>
      <a href="#text-expansion">
        
      </a>
    </div>
    <p>Text expansion occurs when translated content from one language to another takes up more space than the original. Sometimes this expansion is horizontal, as English to German can <a href="https://abctranslations.com/text-expansion-and-reduction-in-translation-services"><u>expand</u></a> up to an average of 35%, Spanish 30%, and French 20%). Asian languages might contract from the English but expand vertically. Interestingly, the fewer characters English has, the more the localized languages tend to expand.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3P2FszOadUDhhc1oRzjjra/250d2425f09423907359914bf628e29c/image10.png" />
          </figure><p><sup><i>Data source: </i></sup><a href="https://www.ibm.com/docs/en/i/7.3?topic=interfaces-text-translation-design"><sup><i><u>IBM</u></i></sup></a></p><p>UI designers and developers need to keep this in mind when creating their applications. Thus, one important consideration is to test the design mock-ups with larger texts and plan the UI to accommodate for text expansion. If some English content barely fits within its container, it will most likely not fit in other languages and possibly break the layout.</p><p>Here’s an example of the same button in different languages in Radar’s fixed-width sidebar. Since it’s the main navigation, truncating the text is not appropriate and the only viable option is wrapping, which means localized buttons can end up having different heights. Sometimes it’s necessary to trade visual consistency for usability.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/oByQcEAMDwh4gv1GtxOQG/0de2878728f293a49e03a9b90fbae536/image3.png" />
          </figure>
    <div>
      <h3>String concatenation</h3>
      <a href="#string-concatenation">
        
      </a>
    </div>
    <p>In English, you can easily chain-connect words because most words lack <a href="https://en.wikipedia.org/wiki/Inflection"><u>inflections</u></a>. Almost all programming languages are designed using the English language in mind. An old linguist joke goes like: <i>an English teacher: a teacher of English or a teacher from England? </i>Case in point, it would be nightmarish to translate this example:</p><p><code>A lovely little old rectangular green French silver whittling knife</code> </p><p>Most Western languages need to connect words with some <i>glue</i>: prepositions, articles, or inflections. This is why, in general, string concatenation (putting together sentences or sentence parts by combining two or more strings) is a terrible practice for localization, even though it seems efficient from a development point of view. You can’t assume that all languages follow the same sentence structure as English. Most languages don’t.</p><p>Sentences may need to be completely reversed for them to sound grammatically correct in other languages. This becomes a particularly severe problem when a string doesn’t include a placeholder because it’s assumed to be concatenated at the beginning or the end of the string, such as this:</p><p><code>"is currently categorized as:"</code></p><p>Developers need to make sure to include any placeholders within the string itself, so that translators can easily move them as needed, for instance:</p><p><code>"Distribution of {{botClass}} traffic by IP version"</code></p><p>would look like this in Simplified Chinese (notice how the <code>{{botClass}}</code> placeholder got moved)</p><p><code>"{{botClass}} 流量分布（按 IP 版本）"</code></p>
    <div>
      <h3>String reuse</h3>
      <a href="#string-reuse">
        
      </a>
    </div>
    <p>As with string concatenation, string reuse (using the same string in more than one place and just swapping out the contents of a placeholder) seems efficient if you’re a developer. A problem arises when translating this into gendered languages, such as most European languages. In Spanish, depending on its position and context, a word as simple as “open” standing by itself, could have all these different translations:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3rcU3cH7ZXfsn5bxmx11UG/60e9ebfbec19caed3553e8d3b63828fd/image7.png" />
          </figure><p>Other examples are <code>Custom</code>, <code>Detected</code>, or <code>Disabled</code>, which can have different translations depending on their position within a sentence, their location in the UI, depending on whether they accompany a singular, plural, masculine or feminine noun, so <a href="https://www.i18next.com/translation-function/plurals#singular-plural"><u>extra entries</u></a> for these may need to be created in the language files.</p><p>Translators will also need to know what will replace the placeholders in strings like the one below, because the surrounding wording may refer to a term that is masculine, feminine, or neutral (for languages that have those, such as German). If a placeholder could be more than one of these (a masculine noun but also a feminine noun), the translation will become grammatically incorrect in at least some of the cases. In the following example, translators would need to know what <code>{link1}</code> and <code>{link2}</code> will be replaced with, so they know which grammatically correct wording to use around them.</p><p><code>Your use of the URL Scanner is subject to our {{link1}}. Any personal data in a submitted URL will be handled in accordance with our {{link2}}</code>.</p><p>A better way to do this is to have component placeholders and include the text to be translated for context:</p><p><code>Your use of the URL Scanner is subject to our &lt;link1&gt;Online Service Terms of Use&lt;/link1&gt;. Any personal data in a submitted URL will be handled in accordance with our &lt;link2&gt;Privacy Policy&lt;/link2&gt;</code>.</p>
    <div>
      <h2>Regional considerations</h2>
      <a href="#regional-considerations">
        
      </a>
    </div>
    
    <div>
      <h3>Date formats</h3>
      <a href="#date-formats">
        
      </a>
    </div>
    <p>Date formats vary greatly from country to country. Not only can’t you assume that all countries use a month/day/year format, but even the day that the week starts may be different based on the country or culture.

Here’s a comparison of Radar’s date picker in American English against European Spanish (which has weeks starting on Mondays instead of Sundays), and against Simplified Chinese (which uses a completely different format for dates).</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/10dcc7wLFMwNmIeMtHeezb/863c9e61d174f9f647379a146b0d4912/image2.png" />
          </figure><p>Thankfully, developers don’t need to know all the country-specific details, as they can use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat"><u>Intl.DateTimeFormat</u></a> or <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString"><u>Date.toLocaleString()</u></a> for this.</p><p>Intl.DateTimeFormat receives a locale and formatting <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#date-time_component_options"><u>options</u></a> that differ from string tokens commonly found on date libraries such as <a href="http://day.js"><u>Day.js</u></a> or <a href="https://momentjs.com/"><u>Moment.js</u></a>. Unless you specifically use the <a href="https://day.js.org/docs/en/display/format#localized-formats"><u>localized string tokens</u></a> on those libraries, the order of the tokens is fixed, along with any characters or delimiters you might add to the format, which poses a problem because the date format parts order should change according to the locale. 

Intl.DateTimeFormat handles all that and saves you the trouble of having to add a date formatting dependency to your project and loading library-specific locale resources.</p><p>Here’s an example of a generic React component using Intl.DateTimeFormat and react-i18next. The code below will render the date as “Tue, Oct 1, 2024” for American English (en-US) and as “2024年10月1日(火)” for Japanese (ja-JP).</p>
            <pre><code>import { useTranslation } from "react-i18next";

export default function SomeComponent() {
  const { i18n } = useTranslation();
  const date = new Date("2024-10-01");
  return (
    &lt;time dateTime={date.toISOString()}&gt;
      {new Intl.DateTimeFormat(i18n.language, {
        weekday: "short",
        month: "short",
        year: "numeric",
        day: "numeric",
      }).format(date)}
    &lt;/time&gt;
  );
}</code></pre>
            
    <div>
      <h3>Number notations</h3>
      <a href="#number-notations">
        
      </a>
    </div>
    <p>Similarly, different locales use different notations for numbers. In the US and the UK, a period is used as the decimal separator, and a comma as the thousands separator. Instead, other countries use a comma as the decimal separator and a period (or a space) as the thousands separator. Again, it’s not necessary for developers to know all the odds and ends for this, as they can use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat"><u>Intl.NumberFormat</u></a>.</p><p>Here’s an example of a generic React component using Intl.NumberFormat and react-i18next. The code below will render the number as “12,345,678.90” for American English (en-US) and as “12 345 678,90” for Portuguese (pt-PT). Intl.NumberFormat <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options"><u>options</u></a> can be passed to format numbers as decimals, percentages, currencies, etc, and specify things like number of decimal places and rounding strategies.</p>
            <pre><code>import { useTranslation } from "react-i18next";

export default function SomeComponent() {
  const { i18n } = useTranslation();
  return (
    &lt;span&gt;{new Intl.NumberFormat(i18n.language, {
    style: "decimal",
    minimumFractionDigits: 2,
  }).format(12345678.9)}&lt;/span&gt;);
}</code></pre>
            <p>As of mid-December, regionalized number formatting is not fully implemented on Radar. We expect this to be complete by the end of Q1 2025.</p>
    <div>
      <h3>List sorting</h3>
      <a href="#list-sorting">
        
      </a>
    </div>
    <p>When you have a list of items that appears sorted, such as a country list in a dropdown, it’s not enough to simply translate the items. For instance, when translated into Portuguese, “<code>South Africa</code>” becomes “<code>África do Sul</code>”, which means it should then go near the top of the list. Besides that, each language has different sorting requirements, and those go way beyond the A-Z alphabet. For instance, several Asian languages don’t use Latin characters at all, and may get sorted by stroke or <a href="https://en.wikipedia.org/wiki/Chinese_character_radicals"><u>character radical</u></a> order instead.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5uOOTu2U3txnxjcVkhI4VI/37af8b29d9537fc56c4c5e23e3504640/image13.png" />
          </figure><p>Here’s an example of a generic React country selector component using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare"><u>String.localeCompare</u></a> and react-i18next. The code below imports a list of countries with name and <a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2"><u>alpha-2</u></a> code and sorts the options according to the translated country name for the active locale. <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#options"><u>Intl.Collator options</u></a> can be passed to localeCompare() for specific sorting needs.</p>
            <pre><code>import { useTranslation } from "react-i18next";

import Select from "~/components/Select";
import COUNTRIES from "~/constants/geo";

export default function CountrySelector() {
  const { t, i18n } = useTranslation();
  const options = COUNTRIES.map(({ name, code }) =&gt; ({
    label: t(name, { ns: "countries" }),
    value: code,
  })).sort((a, b) =&gt; a.label.localeCompare(b.label, i18n.language));
  return &lt;Select options={options} onChange={(option) =&gt; { /* do something */ }} /&gt;;
}</code></pre>
            
    <div>
      <h2>API localization</h2>
      <a href="#api-localization">
        
      </a>
    </div>
    <p>Many of the Radar screens and reports include output from Cloudflare or third-party APIs. Unfortunately, the vast majority of these APIs only output English content. When combining that with the translated part of the site, it may give the impression of a poorly localized site.</p><p>To solve this, we take API outputs and map everything into <a href="https://react.i18next.com/guides/multiple-translation-files"><u>separate files</u></a>, translate all possible messages, and then display that instead of the original output. But as APIs evolve over time and new messages are added, or existing ones get changed, keeping up with these translations becomes an endless game of “<a href="https://en.wikipedia.org/wiki/Whac-A-Mole"><u>whac-a-mole</u></a>".</p>
            <pre><code>"Address unreachable error when attempting to load page": "Error de dirección inaccesible al intentar cargar la página",
"Authentication failed": "Autenticación fallida",
"Browser did not fully start before timeout": "El navegador no se ha iniciado por completo antes de agotar el tiempo de espera.",
"Certificate and/or SSL error when attempting to load page": "Error de certificado y/o SSL al intentar cargar la página",
"Crawl took too long to finish": "El rastreo ha tardado demasiado en completarse.",
"DNS resolution failed": "Error de resolución de DNS",
"Network connection aborted.": "Conexión de red cancelada"</code></pre>
            <p>It should be a best practice for APIs to accept a locale parameter or header, and for engineers to have multiple languages in mind when building these APIs, even if it’s just the error messages. That could save time and resources for any number of clients they might have.</p>
    <div>
      <h2>Project setup</h2>
      <a href="#project-setup">
        
      </a>
    </div>
    <p>Radar is a <a href="https://remix.run/"><u>Remix</u></a> project running on <a href="https://pages.cloudflare.com/"><u>Cloudflare Pages</u></a>. While researching ways to implement internationalization, we came across this Remix <a href="https://remix.run/blog/remix-i18n"><u>blog post</u></a>, and after some experimenting, we decided to go with Sergio Xalambrí’s <a href="https://github.com/sergiodxa/remix-i18next"><u>remix-i18next</u></a>. We mostly followed the installation instructions found on the repo, with some changes.</p><p>We have multiple translation files on every locale folder, one for each data source, to help us maintain strings that come from APIs. Each file can be used to create a namespace for translations, to avoid key collisions, and also to be loaded separately as needed for each route or component.</p><p>On remix-i18next’s instructions, you’ll find the concept of backend plugins to achieve the loading of these files, except that you cannot use <a href="https://github.com/i18next/i18next-fs-backend"><u>i18next-fs-backend</u></a> with Cloudflare Pages because there’s no access to the filesystem. To solve that we used the <i>resources</i> approach, similar to what can be found on this sample <a href="https://github.com/sergiodxa/remix-vite-i18next"><u>remix-i18next with vite</u></a> setup, but we didn’t want to have to maintain the resources dictionary each time we add new namespaces, so <a href="https://vite.dev/guide/features#glob-import"><u>vite’s Glob imports</u></a> came in handy:</p>
            <pre><code>import { serverOnly$ } from "vite-env-only/macros";

export const resources = serverOnly$(
  Object.entries(import.meta.glob("./*/*.json", { import: "default" })).reduce(
    (acc, [path, module]) =&gt; {
      const parts = path.split("/").slice(1);
      const locale = parts[0];
      const namespace = parts[1].split(".")[0];
      if (!acc[locale]) acc[locale] = {};
      if (!acc[locale][namespace]) acc[locale][namespace] = {};
      module().then((value) =&gt; (acc[locale][namespace] = value));
      return acc;
    },
    {},
  ),
)!;</code></pre>
            <p>This creates a <a href="https://github.com/pcattori/vite-env-only?tab=readme-ov-file#serveronly"><u>server-side only</u></a> resources dictionary by importing all JSON files in the locales folder to be passed as the resources property for the i18next configuration in Remix’s <a href="https://remix.run/docs/en/main/file-conventions/entry.server"><u>entry.server.tsx</u></a>.</p><p>To load namespaces on the client side, we created a Remix <a href="https://remix.run/docs/en/main/guides/resource-routes#creating-resource-routes"><u>resource route</u></a> that uses the resources dictionary and responds with the namespace object of the requested locale:</p>
            <pre><code>import { LoaderFunctionArgs } from "@remix-run/server-runtime";

import { resources } from "~/i18n";

export async function loader({ params }: LoaderFunctionArgs) {
  const { locale, namespace } = params;
  return resources[locale]?.[namespace] || {};
}</code></pre>
            <p>You can then use the <a href="https://github.com/i18next/i18next-http-backend"><u>i18next-http-backend</u></a> or <a href="https://github.com/dotcore64/i18next-fetch-backend"><u>i18next-fetch-backend</u></a> backend plugins for the i18next configuration in Remix’s <a href="https://remix.run/docs/en/main/file-conventions/entry.client"><u>entry.client.tsx</u></a>:</p>
            <pre><code>import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next/client";
import { options } from “~/i18n”;
  
await i18next
    .use(initReactI18next)
    .use(i18nextHttpBackend)
    .init({
      ...options,
      ns: getInitialNamespaces(),
      backend: { loadPath: "/api/i18n/{{lng}}/{{ns}}" },
    });</code></pre>
            <p>Default namespaces are defined with the <i>defaultNS</i> config property:</p>
            <pre><code>export const defaultNS = ["main", "countries"];</code></pre>
            <p>Additional namespaces to be used for each route can be defined through Remix’s <a href="https://remix.run/docs/en/main/route/handle"><u>handle export</u></a>:</p>
            <pre><code>export const handle = {
  i18n: ["url-scanner", "domain-categories"],
};</code></pre>
            <p>Namespaces on the server side get picked up by the <i>getRouteNamespaces</i> function on entry.server.tsx:</p>
            <pre><code>const ns = i18n.getRouteNamespaces(remixContext);</code></pre>
            <p>On the client-side, examples suggested that you’d have to declare the namespaces on each <i>useTranslation()</i> hook instance, but we worked around that in Remix’s <a href="https://remix.run/docs/en/main/file-conventions/root"><u>root.tsx</u></a> file:</p>
            <pre><code>import { useLocation, useMatches } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import { defaultNS } from “~/i18n”;

export function Layout({ children }) {
  const location = useLocation();
  const matches = useMatches();
  const handle = matches?.find((m) =&gt; m.pathname === location.pathname)?.handle || {};
  useTranslation([...new Set([...defaultNS, ...(handle.i18n || [])])]);
  ...
}</code></pre>
            <p>This causes the client-side plugin to make calls to the resource route and load the required namespaces.</p><p>We also wanted to have the locale in the URL pathname, but not for the default language, so Remix’s <a href="https://remix.run/docs/en/main/file-conventions/routes#optional-segments"><u>optional segments</u></a> allowed us to do just that. remix-i18next does not have URL locale detection by default, but you can provide your own <a href="https://github.com/sergiodxa/remix-i18next?tab=readme-ov-file#finding-the-locale-from-the-request-url-pathname"><u>findLocale function</u></a> that will receive the request as an argument, and you can then parse the request URL to extract the locale.</p>
    <div>
      <h2>Search engine optimization</h2>
      <a href="#search-engine-optimization">
        
      </a>
    </div>
    <p>Once you set up your project for internationalization, you can inform search engines of <a href="https://developers.google.com/search/docs/specialty/international/localized-versions"><u>localized versions of your pages</u></a>. This allows search engines to display localized results of your website in the same language that is being searched.</p>
            <pre><code>&lt;head&gt;
  ...
  &lt;link rel="alternate" href="https://radar.cloudflare.com/" hreflang="en-US"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/de-de" hreflang="de-DE"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/es-es" hreflang="es-ES"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/es-la" hreflang="es-LA"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/fr-fr" hreflang="fr-FR"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/it-it" hreflang="it-IT"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/ja-jp" hreflang="ja-JP"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/ko-kr" hreflang="ko-KR"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/pt-br" hreflang="pt-BR"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/pt-pt" hreflang="pt-PT"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/zh-cn" hreflang="zh-CN"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/zh-tw" hreflang="zh-TW"&gt;
  &lt;link rel="alternate" href="https://radar.cloudflare.com/" hreflang="x-default"&gt;
  &lt;link rel="canonical" href="https://radar.cloudflare.com/"&gt;
  ...
&lt;/head&gt;</code></pre>
            <p>You should also localize page titles, descriptions, and relevant <a href="https://ogp.me/"><u>Open Graph</u></a> metadata. To achieve this with Remix and remix-i18next, you use the <a href="https://github.com/sergiodxa/remix-i18next?tab=readme-ov-file#translating-text-inside-loaders-or-actions"><u>getFixedT</u></a> method in route loaders to resolve the translations and return data for the <a href="https://remix.run/docs/en/main/route/meta"><u>meta export</u></a>:</p>
            <pre><code>import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/server-runtime";
import i18n from "~/i18next.server";

export async function loader({ request }: LoaderFunctionArgs) {
  const t = await i18n.getFixedT(request);
  return {
    meta: {
      title: t("meta.about.title"),
      description: t("meta.about.description"),
      url: request.url,
    },
  };
}

export const meta = ({ data }: MetaArgs&lt;typeof loader&gt;) =&gt; data.meta;</code></pre>
            <p>If you are defining default meta tags in parent routes you may also need to <a href="https://remix.run/docs/ru/main/route/meta#merging-with-parent-meta"><u>merge the meta objects</u></a>.</p>
    <div>
      <h2>Conclusion</h2>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>There is hardly ever an absolute when dealing with languages. Your Spanish, your French, your Japanese will be different from someone else’s, even if you grew up next door to each other. Family, education, environment, relationships will season and give color to your language. It is like a family recipe — it’s unique, it feels like home and it’s not negotiable. It does not make it better or worse, it just makes it yours. And yours will always be different from other languages.</p><p>Localization is hard. We have seen that there are many things that can and will go sideways, and there are many unknowns that bubble up to the surface in the process. It can also make a product better, as it stress tests the product’s code and design. A tight relationship between the globalization and Radar teams helped make our efforts go more smoothly. In addition, our translators stepped up to the challenge, familiarizing themselves with Radar, analyzing the English content, finding the right translation that will not only resonate with the audience but also fit in the space allotted, constantly checking for context, previous translations, consistency, industry standards, adapting to style guides, tone, and messaging, and after all of that, ultimately acknowledging the fact that there will be people who will disagree (to varying levels of zeal) with their choice of words.</p><p>If you haven’t done so already, we encourage you to explore the localized versions of <a href="https://radar.cloudflare.com/">Cloudflare Radar</a>. Click the language drop-down in the upper right corner of the Radar interface and select your language of choice — Radar will be presented in that language until a new selection is made. Have comments or suggestions about the translations? Let us know at radar_localization@cloudflare.com.</p> ]]></content:encoded>
            <category><![CDATA[Radar]]></category>
            <category><![CDATA[JavaScript]]></category>
            <category><![CDATA[Radar Maps]]></category>
            <category><![CDATA[Localization]]></category>
            <guid isPermaLink="false">6mJPdu3FUGFiIg7e0UMXr6</guid>
            <dc:creator>Alejandro Diaz-Garcia</dc:creator>
            <dc:creator>David Fidalgo</dc:creator>
            <dc:creator>Nuno Pereira</dc:creator>
        </item>
        <item>
            <title><![CDATA[How we built it: the technology behind Cloudflare Radar 2.0]]></title>
            <link>https://blog.cloudflare.com/technology-behind-radar2/</link>
            <pubDate>Thu, 17 Nov 2022 14:00:00 GMT</pubDate>
            <description><![CDATA[ Radar 2.0 was launched last month during Cloudflare's Birthday Week as a complete product revamp. This blog explains how we built it technically. Hopefully, it will inspire other developers to build complex web apps using Cloudflare products. ]]></description>
            <content:encoded><![CDATA[ 
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1Tbyq3gfFHHRXwc4Uny8RH/a7d2558532a5b33ce1ffa285c950afb2/image11-1.png" />
            
            </figure><p><a href="/radar2/">Radar 2.0</a> was built on the learnings of Radar 1.0 and was launched last month during Cloudflare's Birthday Week as a complete product revamp. We wanted to make it easier for our users to find insights and navigate our data, and overall provide a better and faster user experience.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/34b5YuwtsM5h8WwqSABuXw/dc314ea8e1a3a6b8db68bae7010e64ed/image16.png" />
            
            </figure><p>We're building a <a href="/welcome-to-the-supercloud-and-developer-week-2022/">Supercloud</a>. Cloudflare's products now include hundreds of features in networking, security, access controls, computing, storage, and more.</p><p>This blog will explain how we built the new Radar from an engineering perspective. We wanted to do this to demonstrate that anyone could build a somewhat complex website that involves demanding requirements and multiple architectural layers, do it on top of our stack, and how easy it can be.</p><p>Hopefully, this will inspire other developers to switch from traditional software architectures and build their applications using modern, more efficient technologies.</p>
    <div>
      <h2>High level architecture</h2>
      <a href="#high-level-architecture">
        
      </a>
    </div>
    <p>The following diagram is a birds-eye view of the Radar 2.0 architecture. As you can see, it's divided into three main layers:</p><ul><li><p>The Core layer is where we keep our data lake, data exploration tools, and backend API.</p></li><li><p>The Cloudflare network layer is where we host and run Radar and serve the public APIs.</p></li><li><p>The Client layer is essentially everything else that runs in your browser. We call it the Radar Web app.</p></li></ul>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7eBe8tSbh0Uocpgw1abbZ8/5b30a247f945240f2f9254f16c0a021a/image3-31.png" />
            
            </figure><p>As you can see, there are Cloudflare products <i>everywhere</i>. They provide the foundational resources to host and securely run our code at scale, but also other building blocks necessary to run the application end to end.</p><p>By having these features readily available and tightly integrated into our ecosystem and tools, at the distance of a click and a few lines of code, engineering teams don't have to reinvent the wheel constantly and can use their time on what is essential: their app logic.</p><p>Let's dig in.</p>
    <div>
      <h2>Cloudflare Pages</h2>
      <a href="#cloudflare-pages">
        
      </a>
    </div>
    <p>Radar 2.0 is deployed using <a href="https://pages.cloudflare.com/">Cloudflare Pages</a>, our <a href="https://www.cloudflare.com/developer-platform/solutions/hosting/">developer-focused website hosting platform</a>. In the early days, you could only host static assets on Pages, which was helpful for many use cases, including integrating with static site generators like <a href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-hugo-site/">Hugo</a>, <a href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-jekyll-site/">Jekyll</a>, or <a href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-gatsby-site/">Gatsby</a>. Still, it wouldn't solve situations where your application needs some sort of server-side computing or advanced logic using a single deployment.</p><p>Luckily Pages recently added support to run custom Workers scripts. With <a href="https://developers.cloudflare.com/pages/platform/functions/">Functions</a>, you can now run server-side code and enable any kind of dynamic functionality you'd typically implement using a separate Worker.</p><p>Cloudflare Pages Functions also allow you to use <a href="https://developers.cloudflare.com/workers/learning/using-durable-objects/">Durable Objects</a>, <a href="https://developers.cloudflare.com/workers/runtime-apis/kv/">KV</a>, <a href="https://developers.cloudflare.com/r2/">R2</a>, or <a href="https://developers.cloudflare.com/d1">D1</a>, just like a regular Worker would. We provide <a href="https://developers.cloudflare.com/pages/platform/functions/">excellent documentation</a> on how to do this and more in our Developer Documentation. Furthermore, the team wrote a blog on <a href="/building-full-stack-with-pages/">how to build a full-stack application</a> that describes all the steps in detail.</p><p>Radar 2.0 needs server-side functions for two reasons:</p><ul><li><p>To render Radar and run the server side of Remix.</p></li><li><p>To implement and serve our frontend API.</p></li></ul>
    <div>
      <h2>Remix and Server-side Rendering</h2>
      <a href="#remix-and-server-side-rendering">
        
      </a>
    </div>
    <p>We use Remix with Cloudflare Pages on Radar 2.0.</p><p><a href="https://remix.run/">Remix</a> follows a server/client model and works under the premise that you can't control the user's network, so web apps must reduce the amount of Javascript, CSS, and JSON they send through the wire. To do this, they move some of the logic to the server.</p><p>In this case, the client browser will get pre-rendered DOM components and the result of pre-fetched API calls with just the right amount of JSON, Javascript, and CSS code, rightfully adjusted to the UI needs. Here’s the <a href="https://remix.run/docs/en/v1/pages/technical-explanation">technical explanation</a> with more detail.</p><p>Typically, Remix would need a Node.js server to do all of this, but guess what: <a href="https://developers.cloudflare.com/pages/framework-guides/remix/">It can also run</a> on Cloudflare Workers and Pages.</p><p>Here’s the code to get the Remix server running on Workers, using Cloudflare Pages:</p>
            <pre><code>import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
import * as build from "@remix-run/dev/server-build";

const handleRequest = createPagesFunctionHandler({
  build: {
    ...build,
    publicPath: "/build/",
    assetsBuildDirectory: "public/build",
  },
  mode: process.env.NODE_ENV,
  getLoadContext: (context) =&gt; ({
    ...context.env,
    CF: (context.request as any).cf as IncomingRequestCfProperties | undefined,
  }),
});

const handler: ExportedHandler&lt;Env&gt; = {
  fetch: async (req, env, ctx) =&gt; {
    const r = new Request(req);
    return handleRequest({
      env,
      params: {},
      request: r,
      waitUntil: ctx.waitUntil,
      next: () =&gt; {
        throw new Error("next() called in Worker");
      },
      functionPath: "",
      data: undefined,
    });
  },
};</code></pre>
            <p>In Remix, <a href="https://remix.run/docs/en/v1/guides/api-routes">routes</a> handle changes when a user interacts with the app and changes it (clicking on a menu option, for example). A Remix route can have a <a href="https://remix.run/docs/en/v1/guides/data-loading"><i>loader</i></a>, an <a href="https://remix.run/docs/en/v1/guides/data-writes"><i>action</i></a> and a <a href="https://remix.run/docs/en/v1/api/conventions#root-layout-route"><i>default</i></a> export. The <i>loader</i> handles API calls for fetching data (GET method). The <i>action</i> handles submissions to the server (POST, PUT, PATCH, DELETE methods) and returns the response. The <i>default</i> export handles the UI code in React that’s returned for that route. A route without a <i>default</i> export returns only data.</p><p>Because Remix runs both on the server and the client, it can get smart and know what can be pre-fetched and computed server-side and what must go through the network connection, optimizing everything for performance and responsiveness.</p><p>Here’s an example of a Radar route, simplified for readability, for the <a href="https://radar.cloudflare.com/outage-center">Outage Center</a> page.</p>
            <pre><code>import type { MetaFunction } from "@remix-run/cloudflare";
import { useLoaderData } from "@remix-run/react";
import { type LoaderArgs } from "@remix-run/server-runtime";

export async function loader(args: LoaderArgs) {
  const ssr = await initialFetch(SSR_CHARTS, args);
  return { ssr, };
}

export default function Outages() {
  const { ssr } = useLoaderData&lt;typeof loader&gt;();

  return (
    &lt;Page
      filters={["timerange"]}
      title={
        &lt;&gt;
          &lt;Svg use="icon-outages" /&gt;
          {t("nav.main.outage-center")}
        &lt;/&gt;
      }
    &gt;
      &lt;Grid columns={[1, 1, 1, 1]}&gt;
        &lt;Card.Article colspan={[1, 1, 1, 1]} rowspan={[1, 1, 1, 1]}&gt;
          &lt;Card.Section&gt;
            &lt;Components.InternetOutagesChoropleth ssr={ssr} /&gt;
          &lt;/Card.Section&gt;
          &lt;Divider /&gt;
          &lt;Card.Section&gt;
            &lt;Components.InternetOutagesTable ssr={ssr} /&gt;
          &lt;/Card.Section&gt;
        &lt;/Card.Article&gt;
      &lt;/Grid&gt;
    &lt;/Page&gt;
  );
}</code></pre>
            <p>And here’s what it produces:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7wmqtv1VV0kSOU3UTlwXiz/fcbaf883e8f975e679069737e6750251/image18.png" />
            
            </figure><p>Remix and SSR can also help you with your <a href="https://developer.chrome.com/docs/lighthouse/overview/">Lighthouse</a> scores and SEO. It can drastically improve metrics like <a href="https://web.dev/cls/">Cumulative Layout Shift</a>, <a href="https://web.dev/fcp/">First Contentful Paint</a> and <a href="https://web.dev/lcp/">Largest Contentful Paint</a> by reducing the number of fetches and information traveling from the server to the browser and pre-rendering the DOM.</p><p>Another project porting their app to Remix is <a href="https://cloudflare.tv/">Cloudflare TV</a>. This is how their metrics looked before and after the changes.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/oR1Rqf8Mp1fYJRpQMBrgE/aa49405daef020536bfe82c22e42c5d1/image12.png" />
            
            </figure><p>Radar’s Desktop Lighthouse score is now nearly 100% on Performance, Accessibility, Best Practices, and SEO.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3fe4weO7U21a7ZNGW969su/e5c4250fe87be0cfa8bd6f879a721e03/image14.png" />
            
            </figure><p>Another Cloudflare product that we use extensively on Radar 2.0 is <a href="https://www.cloudflare.com/website-optimization/">Speed</a>. In particular, we want to mention the <a href="/early-hints/">Early Hints</a> feature. Early Hints is a new web <a href="https://developer.mozilla.org/docs/Web/HTTP/Status/103">standard</a> that defines a new HTTP 103 header the server can use to inform the browser which assets will likely be needed to render the web page while it's still being requested, resulting in dramatic load times improvements.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2OSrsfjFevZ4qx1TT4jYWb/a012f0b129b260e0f75a62742e9d9df4/image2-42.png" />
            
            </figure><p>You can use <a href="/early-hints-on-cloudflare-pages/">Cloudflare Pages with Early Hints</a>.</p>
    <div>
      <h2>APIs</h2>
      <a href="#apis">
        
      </a>
    </div>
    <p>Radar has two APIs. The backend which has direct access to our data sources, and the frontend, which is available on the Internet.</p>
    <div>
      <h3>Backend API</h3>
      <a href="#backend-api">
        
      </a>
    </div>
    <p>The backend API was written using <a href="https://www.python.org/">Python</a>, <a href="https://pandas.pydata.org/">Pandas</a> and <a href="https://fastapi.tiangolo.com/">FastAPI</a> and is protected by <a href="https://developers.cloudflare.com/cloudflare-one/policies/access/">Cloudflare Access</a>, <a href="https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json/">JWT tokens</a> and an <a href="https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/set-up/">authenticated origin pull</a> (AOP) configuration. Using Python allows anyone on the team, engineers or data scientists, to collaborate easily and contribute to improving and expanding the API, which is great. Our data science team uses <a href="https://jupyter.org/hub">JupyterHub</a> and <a href="https://docs.jupyter.org/en/latest/start/index.html">Jupyter Notebooks</a> as part of their data exploration workflows, which makes prototyping and reusing code, algorithms and models particularly easy and fast.</p><p>It then talks to the upstream frontend API via a <a href="https://strawberry.rocks/">Strawberry</a> based GraphQL server. Using <a href="https://graphql.org/">GraphQL</a> makes it easy to create complex queries, giving internal users and analysts the flexibility they need when building reports from our vast collection of data.</p>
    <div>
      <h3>Frontend API</h3>
      <a href="#frontend-api">
        
      </a>
    </div>
    <p>We built Radar's frontend API on top of Cloudflare <a href="https://developers.cloudflare.com/workers/">Workers</a>. This worker has two main functions:</p><ul><li><p>It fetches data from the backend API using GraphQL, and then transforms it.</p></li><li><p>It provides a public <a href="https://developers.cloudflare.com/radar">REST API</a> that anyone can use, including Radar.</p></li></ul><p>Using a worker in front of our core API allows us to easily add and separate microservices, and also adds notable features like:</p><ul><li><p>Cloudflare's <a href="https://developers.cloudflare.com/workers/runtime-apis/cache/">Cache API</a> allows finer control over what to cache and for how long and supports POST requests and customizable cache control headers, which we use.</p></li><li><p>Stale responses using <a href="https://developers.cloudflare.com/r2/">R2</a>. When the backend API cannot serve a request for some reason, and there’s a stale response cached, it’ll be served directly from R2, giving end users a better experience.</p></li><li><p><a href="https://en.wikipedia.org/wiki/Comma-separated_values">CSV</a> and <a href="https://en.wikipedia.org/wiki/JSON">JSON</a> output formats. The CSV format is convenient and makes it easier for data scientists, analysts, and others to use the API and consume our API data directly from other tools.</p></li></ul>
    <div>
      <h3>Open sourcing our OpenAPI 3 schema generator and validator</h3>
      <a href="#open-sourcing-our-openapi-3-schema-generator-and-validator">
        
      </a>
    </div>
    <p>One last feature on the frontend API is <a href="https://spec.openapis.org/oas/latest.html">OpenAPI 3</a> support. We automatically generate an OpenAPI schema and validate user input with it. This is done through a custom library that we built on top of <a href="https://github.com/kwhitley/itty-router">itty-router</a>, which we also use. Today we’re open sourcing this work.</p><p><a href="https://github.com/cloudflare/itty-router-openapi">itty-router-openapi</a> provides an easy and compact OpenAPI 3 schema generator and validator for Cloudflare Workers. Check our <a href="https://github.com/cloudflare/itty-router-openapi">GitHub repository</a> for more information and details on how to use it.</p>
    <div>
      <h3>Developer’s Documentation</h3>
      <a href="#developers-documentation">
        
      </a>
    </div>
    <p>Today we’re also launching our developer’s <a href="https://developers.cloudflare.com/radar">documentation pages for the Radar API</a> where you can find more information about our data license, basic concepts, how to get started and the available API methods. Cloudflare Radar's API is free, allowing academics, data sleuths and other web enthusiasts to investigate Internet usage across the globe, based on data from our global network.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1GbwacvF3M7mVj6kovY0x3/c9e8c3f3b7d2ef7b364f6740bfec2760/image6-7.png" />
            
            </figure><p>To facilitate using our API, we also put together a <a href="https://colab.research.google.com/github/cloudflare/radar-notebooks/blob/main/notebooks/example.ipynb">Colab Notebook template</a> that you can play with, copy and expand to your use case.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5IQocyWiVv1U9ecurP6AOg/1f6336488129f9f3ae6e804d15526d39/image7-4.png" />
            
            </figure>
    <div>
      <h2>The Radar App</h2>
      <a href="#the-radar-app">
        
      </a>
    </div>
    <p>The Radar App is the code that runs in your browser. We've talked about Remix, but what else do we use?</p><p>Radar relies on a lot of <b>data visualizations</b>. Things like charts and maps are essential to us. We decided to build our reusable library of visualization components on top of two other frameworks: <a href="https://airbnb.io/visx/">visx</a>, a "collection of expressive, low-level visualization primitives for React," <a href="https://d3js.org/">D3</a>, a powerful JavaScript library for manipulating the DOM based on data, and <a href="https://maplibre.org/">MapLibre</a>, an open-source map visualization stack.</p><p>Here’s one of our visualization components in action. We call it the “PewPew map”.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1ENMTCsc23Vq5Of80vVqBP/fd8b4bab134c536c73f34c9ced3db670/image5-12.png" />
            
            </figure><p>And here’s the Remix React code for it, whenever we need to use it in a page:</p>
            <pre><code>&lt;Card.Section
    title={t("card.attacks.title")}
    description={t("card.attacks.description")}
  &gt;
    &lt;Flex gap={spacing.medium} align="center" justify="flex-end"&gt;
      &lt;SegmentedControl
        label="Sort order:"
        name="attacksDirection"
        value={attacksDirection}
        options={[
          { label: t("common.source"), value: "ORIGIN" },
          { label: t("common.target"), value: "TARGET" },
        ]}
      onChange={({ target }: any) =&gt; setAttacksDirection(target.value)}
      /&gt;
    &lt;/Flex&gt;

    &lt;Components.AttacksCombinedChart
      ssr={ssr}
      height={400}
      direction={attacksDirection}
    /&gt;
  &lt;/Card.Section&gt;</code></pre>
            
    <div>
      <h3>SVGs</h3>
      <a href="#svgs">
        
      </a>
    </div>
    <p>Another change we made to Radar was switching our images and graphical assets to <a href="https://en.wikipedia.org/wiki/Scalable_Vector_Graphics">Scalable Vector Graphics</a>. SVGs are great because they're essentially a declarative graphics language. They're XML text files with vectorial information. And so, they can be easily manipulated, transformed, stored, or indexed, and of course, they can be rendered at any size, producing beautiful, crisp results on any device and resolution.</p><p>SVGs are also extremely small and efficient in size compared to bitmap formats and support <a href="https://www.w3.org/TR/SVGTiny12/i18n.html">internationalization</a>, making them easier to translate to other languages (localization), thus providing better <a href="https://www.a11yproject.com/">accessibility</a>.</p><p>Here’s an example of a Radar Bubble Chart, inspected, where you can see the SVG code and the  strings embedded.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1PJgCboffVUKXDjtRTPFov/b07a3c91bc2e3ad91bbfb150ace899bc/image17.png" />
            
            </figure>
    <div>
      <h3>Cosmos</h3>
      <a href="#cosmos">
        
      </a>
    </div>
    <p><a href="https://reactcosmos.org/">React Cosmos</a> is a "sandbox for developing and testing UI components in isolation." We wanted to use Cosmos with Radar 2.0 because it's the perfect project for it:</p><ol><li><p>It has a lot of visual components; some are complex and have many configuration options and features.</p></li><li><p>The components are highly reusable across multiple pages in different contexts with different data.</p></li><li><p>We have a multidisciplinary team; everyone can send a pull request and add or change code in the frontend.</p></li></ol><p>Cosmos acts as a component library where you can see our palette of ready-to-use visualizations and widgets, from simple buttons to complex charts, and you play with their options in real-time and see what happens. Anyone can do it, not only designers or engineers but also other project stakeholders. This effectively improves team communications and makes contributing and iterating quickly.</p><p>Here’s a screenshot of our Cosmos in action:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5LBKvrUOyalqcEkuie59dB/c571741695acf093204103d94ce6ebd5/image1-57.png" />
            
            </figure>
    <div>
      <h2>Continuous integration and development</h2>
      <a href="#continuous-integration-and-development">
        
      </a>
    </div>
    <p>Continuous integration is important for any team doing modern software. Cloudflare Pages provides multiple options to work with CI tools using direct uploads, out of the box. The team has put up <a href="https://developers.cloudflare.com/pages/how-to/use-direct-upload-with-continuous-integration/">documentation and examples</a> on how to do that with GitHub Actions, CircleCI, and Travis, but you can use others.</p><p>In our case, we use BitBucket and TeamCity internally to build and deploy our releases. Our workflow automatically builds, tests, and deploys Radar 2.0 within minutes on an approved PR and follow-up merge.</p><p>Unit tests are done with <a href="https://vitest.dev/">Vitest</a> and E2E tests with <a href="https://playwright.dev/">Playwright</a>. Visual Regression testing is planned and <a href="https://playwright.dev/docs/test-snapshots">Playwright can also help with that</a>.</p><p>Furthermore, we have multiple environments to stage and test our releases before they go live to production. Our <a href="https://www.cloudflare.com/learning/serverless/glossary/what-is-ci-cd/">CI/CD</a> setup makes it easy to switch from one environment to the other or quickly roll back any undesired deployment.</p><p>Again Cloudflare Pages makes it easy to do this using <a href="https://developers.cloudflare.com/pages/platform/preview-deployments/">Preview deployments</a>, aliases, or <a href="https://developers.cloudflare.com/pages/platform/branch-build-controls/">Branch build controls</a>. The same is true for regular Workers using <a href="https://developers.cloudflare.com/workers/platform/environments/">Environments</a>.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2Q9hXo3Iz04CtXpmaALbzI/0c40d53fd560b42f08098a00d00157e5/image19.png" />
            
            </figure>
    <div>
      <h3>Fast previews and notifications</h3>
      <a href="#fast-previews-and-notifications">
        
      </a>
    </div>
    <p>Radar 1.0 wasn't particularly fast doing CI/CD, we confess. We had a few episodes when a quick fix could take some good 30 minutes from committing the code to deployment, and we felt frustrated about it.</p><p>So we invested a lot in ensuring that the new CI would be fast, efficient, and furious.</p><p>One cool thing we ended up doing was fast preview links on any commit pushed to the code repository. Using a combination of intelligent caching during builds and doing asynchronous tests when the commit is outside the normal release branches, we were able to shorten the deployment time to seconds.</p><p>This is the notification we get in our chat when anyone pushes code to any branch:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5moqSeGzEAlNHnEJVoxwbO/900325ae89a90528c250ffd3fa4c4e0e/image8-2.png" />
            
            </figure><p>Anyone can follow a thread for a specific branch in the chat and get notified on new changes when they happen.</p><p>Blazing-fast builds, preview links and notifications are game-changers. An engineer can go from an idea or a quick fix to showing the result on a link to a product manager or another team member. Anyone can quickly click the link to see the changes on a fully working end-to-end version of Radar.</p>
    <div>
      <h2>Accessibility and localization</h2>
      <a href="#accessibility-and-localization">
        
      </a>
    </div>
    <p>Cloudflare is committed to web accessibility. Recently we announced how we upgraded Cloudflare’s Dashboard to <a href="/project-a11y/">adhere to industry accessibility standards</a>, but this premise is valid for all our properties. The same is true for localization. In 2020, we <a href="/internationalizing-the-cloudflare-dashboard/">internationalized</a> our Dashboard and added support for new languages and locales.</p><p>Accessibility and localization go hand in hand and are both important, but they are also different. The <a href="https://www.w3.org/TR/WCAG21/">Web Content Accessibility Guidelines</a> define many best practices around accessibility, including using <a href="https://color.cloudflare.design/">color</a> and contrast, tags, SVGs, shortcuts, gestures, and many others. The <a href="https://www.a11yproject.com/">A11Y project page</a> is an excellent resource for learning more.</p><p>Localization, though, also known as <a href="https://en.wikipedia.org/wiki/Internationalization_and_localization">L10n</a>, is more of a technical requirement when you start a new project. It's about making sure you choose the right set of libraries and frameworks that will make it easier to add new translations without engineering dependencies or code rewrites.</p><p>We wanted Radar to perform well on both fronts. Our design system takes Cloudflare's design and brand <a href="https://cloudflare.design/">guidelines</a> seriously and adds as many A11Y good practices as possible, and the app is fully aware of localization strings across its pages and UI components.</p><p>Adding a new language is as easy as translating a single JSON file. Here's a snippet of the en-US.json file with the default American English strings:</p>
            <pre><code>{
  "abbr.asn": "Autonomous System Number",
  "actions.chart.download.csv": "Download chart data in CSV",
  "actions.chart.download.png": "Download chart in PNG Format",
  "actions.chart.download.svg": "Download chart in SVG Format",
  "actions.chart.download": "Download chart",
  "actions.chart.maximize": "Maximize chart",
  "actions.chart.minimize": "Minimize chart",
  "actions.chart.share": "Share chart",
  "actions.download.csv": "Download CSV",
  "actions.download.png": "Download PNG",
  "actions.download.svg": "Download SVG",
  "actions.share": "Share",
  "alert.beta.link": "Radar Classic",
  "alert.beta.message": "Radar 2.0 is currently in Beta. You can still use {link} during the transition period.",
  "card.about.cloudflare.p1": "Cloudflare, Inc. ({website} / {twitter}) is on a mission to help build a better Internet. Cloudflare's suite of products protects and accelerates any Internet application online without adding hardware, installing software, or changing a line of code. Internet properties powered by Cloudflare have all web traffic routed through its intelligent global network, which gets smarter with every request. As a result, they see significant improvement in performance and a decrease in spam and other attacks. Cloudflare was named to Entrepreneur Magazine's Top Company Cultures 2018 list and ranked among the World's Most Innovative Companies by Fast Company in 2019.",
  "card.about.cloudflare.p2": "Headquartered in San Francisco, CA, Cloudflare has offices in Austin, TX, Champaign, IL, New York, NY, San Jose, CA, Seattle, WA, Washington, D.C., Toronto, Dubai, Lisbon, London, Munich, Paris, Beijing, Singapore, Sydney, and Tokyo.",
  "card.about.cloudflare.title": "About Cloudflare",
...</code></pre>
            <p>You can expect us to release Radar in other languages soon.</p>
    <div>
      <h2>Radar Reports and Jupyter notebooks</h2>
      <a href="#radar-reports-and-jupyter-notebooks">
        
      </a>
    </div>
    <p><a href="https://radar.cloudflare.com/reports">Radar Reports</a> are documents that use data exploration and storytelling to analyze a particular theme in-depth. Some reports tend to get updates from time to time. Examples of Radar Reports are our quarterly <a href="https://radar.cloudflare.com/reports/ddos-2022-q3">DDoS Attack Trends</a>, or the <a href="https://radar.cloudflare.com/reports/ipv6">IPv6 adoption</a>.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1YdlIovSNvHy7YITYdxiCf/50804dd483563fd236d7b8ab6f05b8b1/image4-23.png" />
            
            </figure><p>The source of these Reports is <a href="https://jupyter.org/">Jupyter Notebooks</a>. Our Data Science team works on some use-case or themes with other stakeholders using our internal Jupyter Hub tool. After all the iteration and exploration are done, and the work is signed off, a notebook is produced.</p><p>A Jupyter Notebook is a <a href="https://ipython.org/ipython-doc/3/notebook/nbformat.html">JSON document</a> containing text, source code, rich media such as images or charts, and other metadata. It is the de facto standard for presenting data science projects, and every data scientist uses it.</p><p>With Radar 1.0, converting a Jupyter Notebook to a Radar page was a lengthy and manual process implicating many engineering and design resources and causing much frustration to everyone involved. Even updating an already-published notebook would frequently cause trouble for us.</p><p>Radar 2.0 changed all of this. We now have a fully automated process that takes a Jupyter Notebook and, as long as it's designed using a list of simple rules and internal guidelines, converts it automatically, hosts the resulting HTML and assets in an R2 bucket, and publishes it on the <a href="https://radar.cloudflare.com/reports">Reports</a> page.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5iS9T8mn23CDjg4nFfpqWT/efdeaa9d9b3ee645cd21b5014af13da1/image9-2.png" />
            
            </figure><p>The conversion to HTML takes into account our design system and UI components, and the result is a <a href="https://radar.cloudflare.com/reports/ddos-2022-q3">beautiful document</a>, usually long-form, perfectly matching Radar's look and feel.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4oXmxPoDH1NDJqsBaIRKK4/9cdd9229bd835c6722fb06c6350fbf0f/image13.png" />
            
            </figure><p>We will eventually open-source this tool so that anyone can use it.</p>
    <div>
      <h2>More Cloudflare, less to worry about</h2>
      <a href="#more-cloudflare-less-to-worry-about">
        
      </a>
    </div>
    <p>We gave examples of using Cloudflare's products and features to build your next-gen app without worrying too much about things that aren't core to your business or logic. A few are missing, though.</p><p>Once the app is up and running, you must protect it from bad traffic and malicious actors. Cloudflare offers you <a href="https://www.cloudflare.com/ddos/">DDoS</a>, <a href="https://www.cloudflare.com/waf/">WAF</a>, and <a href="https://www.cloudflare.com/products/bot-management/">Bot Management</a> protection out of the box at a click's distance.</p><p>For example, here are some of our security rules. This is traffic we don't have to worry about in our app because Cloudflare detects it and acts on it according to our rules.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/14D17IdfhuHOyzPeFPePyP/faa894184818241d101551d1815bf0d7/image10-1.png" />
            
            </figure><p>Another thing we don't need to worry about is redirects from the old site to the new one. Cloudflare has a feature called <a href="https://developers.cloudflare.com/rules/url-forwarding/bulk-redirects/create-dashboard/">Bulk Redirects</a>, where you can easily create redirect lists directly on the dashboard.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3ieVLmpoynt1H7lDWykfs8/ca4136845ea8fa4f66166a3be7fa57b5/image15.png" />
            
            </figure><p>It's also important to mention that every time we talk about what you can do using our Dashboard, we're, in fact, also saying you can do precisely the same using <a href="https://api.cloudflare.com/">Cloudflare's APIs</a>. Our Dashboard is built entirely on top of them. And if you're the infrastructure as code kind of person, we have you covered, too; you can use the <a href="https://developers.cloudflare.com/terraform/tutorial/">Cloudflare Terraform provider</a>.</p><p>Deploying and managing Workers, R2 buckets, or Pages sites is obviously scriptable too. <a href="https://github.com/cloudflare/wrangler">Wrangler</a> is the command-line tool to do this and more, and it goes the extra mile to allow you to run your full app <a href="https://developers.cloudflare.com/workers/wrangler/commands/#dev">locally</a>, emulating our stack, on your computer, before deploying.</p>
    <div>
      <h2>Final words</h2>
      <a href="#final-words">
        
      </a>
    </div>
    <p>We hope you enjoyed this Radar team write-up and were inspired to build your next app on top of our <a href="/welcome-to-the-supercloud-and-developer-week-2022/">Supercloud</a>. We will continue improving and innovating on Radar 2.0 with new features, share our findings and open-sourcing our tools with you.</p><p>In the meantime, we opened a <a href="https://discord.gg/cloudflaredev">Radar room</a> on our Developers Discord Server. Feel free to <a href="https://discord.gg/cloudflaredev">join</a> it and ask us questions; the team is eager to receive feedback and discuss web technology with you.</p><p>You can also follow us <a href="https://twitter.com/cloudflareradar">on Twitter</a> for more Radar updates.</p> ]]></content:encoded>
            <category><![CDATA[Developer Week]]></category>
            <category><![CDATA[Radar]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Cloudflare Pages]]></category>
            <category><![CDATA[Developers]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <guid isPermaLink="false">2H0o8Ld6ebN4hs7uhm1ELW</guid>
            <dc:creator>Celso Martinho</dc:creator>
            <dc:creator>Nuno Pereira</dc:creator>
            <dc:creator>Sofia Cardita</dc:creator>
            <dc:creator>Gabriel Massadas</dc:creator>
        </item>
    </channel>
</rss>