Tutorial: localized routing with the language

Step by step, let's build a sample app with Qwik Speak and a localized router using Qwik City features

Setup

See Quick Start

Routing

Let's assume that we want to create a navigation of this type:

  • default language (en-US): routes not localized http://127.0.0.1:4173/

  • other languages (it-IT): localized routes http://127.0.0.1:4173/it-IT/

In routes root level add [...lang] directory to catch all routes:

src/routes/

└───[...lang]/
        index.tsx
    layout.tsx

Now let's handle it. Update plugin.ts in the root of the src/routes directory:

src/routes/plugin.ts

import type { RequestHandler } from '@builder.io/qwik-city';
import { setSpeakContext, validateLocale } from 'qwik-speak';

import { config } from '../speak-config';

/**
 * This middleware function must only contain the logic to set the locale,
 * because it is invoked on every request to the server.
 * Avoid redirecting or throwing errors here, and prefer layouts or pages
 */
export const onRequest: RequestHandler = ({ params, locale }) => {
  let lang: string | undefined = undefined;

  if (params.lang && validateLocale(params.lang)) {
    // Check supported locales
    lang = config.supportedLocales.find(value => value.lang === params.lang)?.lang;
  } else {
    lang = config.defaultLocale.lang;
  }

  // Set Speak context (optional: set the configuration on the server)
  setSpeakContext(config);

  // Set Qwik locale
  locale(lang);
};

If you want to handle errors or redirects due to the locale, use layouts or pages. For example you could add in src/routes/layout.tsx:

export const onRequest: RequestHandler = ({ locale, error, redirect }) => {
  // E.g. 404 error page
  if (!locale()) throw error(404, 'Page not found for requested locale');

  // E.g. Redirect
  // if (!locale()) {
  //   const getPath = localizePath();
  //   throw redirect(302, getPath('/page', 'en-US')); // Let the server know the language to use
  // }
};

Usage

Add index.tsx with some translation, providing optional default values for each translation: key@@[default value]:

src/routes/[...lang]/index.tsx

import { inlineTranslate, useFormatDate, useFormatNumber } from 'qwik-speak';

export default component$(() => {
  const t = inlineTranslate();

  const fd = useFormatDate();
  const fn = useFormatNumber();

  return (
    <>
      <h1>{t('app.title@@{{name}} demo', { name: 'Qwik Speak' })}</h1>

      <h3>{t('dates@@Dates')}</h3>
      <p>{fd(Date.now(), { dateStyle: 'full', timeStyle: 'short' })}</p>

      <h3>{t('numbers@@Numbers')}</h3>
      <p>{fn(1000000, { style: 'currency' })}</p>
    </>
  );
});

export const head: DocumentHead = () => {
  const t = inlineTranslate();

  return {
    title: t('app.head.home.title@@{{name}}', { name: 'Qwik Speak' }),
    meta: [{ name: 'description', content: t('app.head.home.description@@Localized routing') }],
  };
};

Add a page/index.tsx to try the router:

src/routes/[...lang]/page/index.tsx

import { inlineTranslate } from 'qwik-speak';

export default component$(() => {
  const t = inlineTranslate();

  const key = 'dynamic';

  return (
    <>
      <h1>{t('app.title', { name: 'Qwik Speak' })}</h1>

      <p>{t(`runtime.${key}`)}</p>
    </>
  );
});

Note that it is not necessary to provide the default value in the key once again: it is sufficient and not mandatory to provide it once in the app

Note the use of a dynamic key (which will therefore only be available at runtime), which we assign to the runtime scope

Change locale

Now we want to change locale. Let's create a ChangeLocale component:

src/components/change-locale/change-locale.tsx

import { useLocation } from '@builder.io/qwik-city';
import { useSpeakLocale, useSpeakConfig, useDisplayName, inlineTranslate, localizePath } from 'qwik-speak';

export const ChangeLocale = component$(() => {
  const t = inlineTranslate();

  const pathname = useLocation().url.pathname;

  const locale = useSpeakLocale();
  const config = useSpeakConfig();
  const dn = useDisplayName();

  const getPath = localizePath();

  return (
    <>
      <h2>{t('app.changeLocale@@Change locale')}</h2>
      {config.supportedLocales.map(value => (
        <a key={value.lang} class={{ active: value.lang == locale.lang }} href={getPath(pathname, value.lang)}>
          {dn(value.lang, { type: 'language' })}
        </a>
      ))}
    </>
  );
});

We use the <a> tag tag because it is mandatory to reload the page when changing the language

Add the ChangeLocale component in header.tsx along with localized navigation links:

import { Link, useLocation } from '@builder.io/qwik-city';
import { inlineTranslate, localizePath } from 'qwik-speak';

import { ChangeLocale } from '../../change-locale/change-locale';

export default component$(() => {
  const t = inlineTranslate();

  const pathname = useLocation().url.pathname;

  const getPath = localizePath();
  const [homePath, pagePath] = getPath(['/', '/page/']);

  return (
    <>
      <header>
        <ul>
          <li>
            <Link href={homePath} class={{ active: pathname === homePath }}>
              {t('app.nav.home@@Home')}
            </Link>
          </li>
          <li>
            <Link href={pagePath} class={{ active: pathname === pagePath }}>
              {t('app.nav.page@@Page')}
            </Link>
          </li>
        </ul>
      </header>

      <ChangeLocale />
    </>
  );
});

Extraction

We can now extract the translations and generate the assets as json. In package.json add the following command to the scripts:

"qwik-speak-extract": "qwik-speak-extract --supportedLangs=en-US,it-IT --assetsPath=i18n"
npm run qwik-speak-extract

The following files are generated:

i18n/en-US/app.json
i18n/it-IT/app.json
translations skipped due to dynamic keys: 1
extracted keys: 9

app asset for each language, initialized with the default values we provided.

translations skipped due to dynamic keys is runtime.${key}. During configuration, we provided in runtimeAssets a runtime file, which we can now create and populate with dynamic keys:

i18n/[lang]/runtime.json

{
  "runtime": {
    "dynamic": "I'm a dynamic value"
  }
}

See Qwik Speak Extract for more details.

Development

We can translate the it-IT files and start the app:

npm start

Production

Build the production app in preview mode:

npm run preview

and inspect the qwik-speak-inline.log file in root folder to see warnings for missing values or dynamic keys.

Domain-based routing

Prefix always

If you want to use different domains in production, update speak-config.ts with the domains supported by each locale, and set the prefix usage strategy:

export const config: SpeakConfig = {
  defaultLocale: { lang: 'en' },
  supportedLocales: [
    { domain: 'example.com', lang: 'en' },
    { domain: 'example.it', lang: 'it' },
    { withDomain: 'example.com', lang: 'de' }
  ],
  domainBasedRouting: {
    prefix: 'always'
  },
};

While in dev mode the navigation will only use the prefix, in production it will use the domain and the prefix:

https://example.com/
https://example.com/page
https://example.it/it
https://example.it/it/page
https://example.com/de
https://example.com/de/page

In SSG mode, you can only use always as prefix strategy

Prefix as needed

If in production you don't want the prefix for the default domains, change the prefix strategy to as-needed:

domainBasedRouting: {
  prefix: 'as-needed'
},

It will result in:

https://example.com/
https://example.com/page
https://example.it
https://example.it/page
https://example.com/de
https://example.com/de/page

Since the de language does not have a default domain, but we have associated another domain, it will automatically keep the prefix.

Usage

Update plugin.ts to get the language from the domain:

import type { RequestHandler } from '@builder.io/qwik-city';
import { extractFromDomain, setSpeakContext, validateLocale } from 'qwik-speak';

import { config } from '../speak-config';

export const onRequest: RequestHandler = ({ params, locale, url }) => {
  let lang: string | undefined = undefined;

  if (params.lang && validateLocale(params.lang)) {
    // Check supported locales
    lang = config.supportedLocales.find(value => value.lang === params.lang)?.lang;
   } else {
    // Extract from domain
    lang = extractFromDomain(url, config.supportedLocales) || config.defaultLocale.lang;
  }

  // Set Speak context (optional: set the configuration on the server)
  setSpeakContext(config);

  // Set Qwik locale
  locale(lang);
};

and in ChangeLocale component pass the URL instead of the pathname to getPath:

export const ChangeLocale = component$(() => {
  const url = useLocation().url;

  const config = useSpeakConfig();

  const getPath = localizePath();

  return (
    <>
      {config.supportedLocales.map(value => (
        <a key={value.lang} href={getPath(url, value.lang)}>
          {/*  */}
        </a>
      ))}
    </>
  );
});

Last updated