Skip to content

Latest commit

 

History

History
342 lines (295 loc) · 9.96 KB

tutorial-routing.md

File metadata and controls

342 lines (295 loc) · 9.96 KB

Tutorial: localized routing

Step by step, let's build an app with Qwik Speak and a localized router

npm create qwik@latest
npm install qwik-speak --save-dev

Configuration

Let's create speak-config.ts and speak-functions.ts files in src:

src/speak-config.ts

export const config: SpeakConfig = {
  defaultLocale: { lang: 'en-US', currency: 'USD', timeZone: 'America/Los_Angeles' },
  supportedLocales: [
    { lang: 'it-IT', currency: 'EUR', timeZone: 'Europe/Rome' },
    { lang: 'en-US', currency: 'USD', timeZone: 'America/Los_Angeles' }
  ],
  assets: [
    'app' // Translations shared by the pages
  ]
};

src/speak-functions.ts

/**
 * Translation files are lazy-loaded via dynamic import and will be split into separate chunks during build.
 * Keys must be valid variable names
 */
const translationData = import.meta.glob<Translation>('/i18n/**/*.json');

/**
 * Using server$, translation data is always accessed on the server
 */
const loadTranslation$: LoadTranslationFn = server$(async (lang: string, asset: string) =>
  await translationData[`/i18n/${lang}/${asset}.json`]?.()
);

export const translationFn: TranslationFn = {
  loadTranslation$: loadTranslation$
};

We have added the Speak config and the implementation of the loadTranslation$ function. loadTranslation$ is a customizable function, with which you can load the translation files in the way you prefer.

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. Create plugin.ts in the root of the src/routes directory::

src/routes/plugin.ts

export const onRequest: RequestHandler = ({ params, locale }) => {
  const lang = params.lang;

  // Set Qwik locale
  locale(lang || config.defaultLocale.lang);
};

We assign the value of the lang parameter to Qwik locale. This way it will be immediately available to the library.

Adding Qwik Speak

Just wrap Qwik City provider with QwikSpeakProvider component in root.tsx and pass it the configuration and the translation functions:

src/root.tsx

export default component$(() => {
  return (
    <QwikSpeakProvider config={config} translationFn={translationFn}>
      <QwikCityProvider>
        <head>
          <meta charSet="utf-8" />
          <link rel="manifest" href="/manifest.json" />
          <RouterHead />
        </head>
        <body lang="en">
          <RouterOutlet />
          <ServiceWorkerRegister />
        </body>
      </QwikCityProvider>
    </QwikSpeakProvider>
  );
});

Finally we add an index.tsx with some translation:

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

import {
  $translate as t,
  formatDate as fd,
  formatNumber as fn,
} from 'qwik-speak';

export const Home = component$(() => {
  return (
    <>
      <h1>{t('app.title@@{{name}} demo', { name: 'Qwik Speak' })}</h1>

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

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

export default component$(() => {
  return (
    /**
     * Add Home translations (only available in child components)
     */
    <Speak assets={['home']}>
      <Home />
    </Speak>
  );
});

export const head: DocumentHead = {
  title: 'home.head.title@@Qwik Speak',
  meta: [{ name: 'description', content: 'home.head.description@@Qwik Speak with localized routing' }]
};

Here we have used the Speak component to add scoped translations to the home page. This means that in addition to the app asset that comes with the configuration, the home page will also use the home asset. To distinguish them, app asset keys start with app and home asset keys start with home.

We are also providing default values for each translation: key@@[default value].

Speak component is a Slot component: because Qwik renders Slot components and direct children in isolation, translations are not immediately available in direct children, and we need to use a component for the Home page. It is generally not necessary to use more than one Speak component per page

Head metas

You may have noticed, that in index.tsx we have provided the meta title and description with only the keys. Since the Qwik City DocumentHead is out of context, we need to do the translations directly in router-head.tsx:

src/components/router-head/router-head.tsx

<title>{t(head.title)}</title>

{head.meta.map((m) => (
  <meta key={m.key} name={m.name} content={m.name === 'description' ? t(m.content!) : m.content} />
))}

We can also pass the lang attribute in the html tag:

src/entry.ssr.tsx

export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    manifest,
    ...opts,
    // Use container attributes to set attributes on the html tag
    containerAttributes: {
      lang: opts.serverData?.locale || config.defaultLocale.lang,
      ...opts.containerAttributes,
    },
  });
}

Change locale

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

src/components/change-locale.tsx

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

  const config = useSpeakConfig();

  // Replace the locale and navigate to the new URL
  const navigateByLocale$ = $((newLocale: SpeakLocale) => {
    const url = new URL(location.href);
    if (loc.params.lang) {
      if (newLocale.lang !== config.defaultLocale.lang) {
        url.pathname = url.pathname.replace(loc.params.lang, newLocale.lang);
      } else {
        url.pathname = url.pathname.replace(new RegExp(`(/${loc.params.lang}/)|(/${loc.params.lang}$)`), '/');
      }
    } else if (newLocale.lang !== config.defaultLocale.lang) {
      url.pathname = `/${newLocale.lang}${url.pathname}`;
    }

    location.href = url.toString();
  });

  return (
    <div>
      <h2>{t('app.changeLocale@@Change locale')}</h2>
      {config.supportedLocales.map(value => (
        <button key={value.lang} onClick$={async () => await navigateByLocale$(value)}>
          {value.lang}
        </button>
      ))}
    </div>
  );
});

and add the component in header.tsx:

export default component$(() => {
  return (
    <header>
      <ChangeLocale />
    </header>
  );
});

In navigateByLocale$ we replace the language in the URL, before navigating to the new localized URL.

Extraction: Qwik Speak Extract

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/en-US/home.json
i18n/it-IT/app.json
i18n/it-IT/home.json
translations skipped due to dynamic keys: 2
extracted keys: 4

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

translations skipped due to dynamic keys are meta title and description keys, because those keys are passed as dynamic parameters to the $translate function. We have to add them manually in a new file that we will call runtime:

public/i18n/[lang]/runtime.json

{
  "runtime": {
    "home": {
      "head": {
        "title": "Qwik Speak",
        "description": "Qwik Speak with localized routing"
      }
    }
  }
}

Update the keys in DocumentHead of index.tsx:

export const head: DocumentHead = {
  title: 'runtime.home.head.title@@Qwik Speak',
  meta: [{ name: 'description', content: 'runtime.home.head.description@@Qwik Speak with localized routing' }]
};

and add runtime asset in Speak config:

assets: [
  'app' // Translations shared by the pages
],
runtimeAssets: [
  'runtime' // Translations with dynamic keys or parameters
]

We can translate the it-IT files, and run the app:

npm start

In production mode, assets are loaded only during SSR, and to get the translations on the client as well it is required to inline the translations in chucks sent to the browser.

Add qwikSpeakInline Vite plugin in vite.config.ts:

import { qwikSpeakInline } from 'qwik-speak/inline';

export default defineConfig(() => {
  return {
    plugins: [
      qwikCity(),
      qwikVite(),
      qwikSpeakInline({
        supportedLangs: ['en-US', 'it-IT'],
        defaultLang: 'en-US',
        assetsPath: 'i18n'
      }),
      tsconfigPaths(),
    ],
  };
});

Set the base URL for loading the chunks in the browser in entry.ssr.tsx file:

export function extractBase({ serverData }: RenderOptions): string {
  if (!isDev && serverData?.locale) {
    return '/build/' + serverData.locale;
  } else {
    return '/build';
  }
}

export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    manifest,
    ...opts,
    // Determine the base URL for the client code
    base: extractBase,
    // Use container attributes to set attributes on the html tag
    containerAttributes: {
      lang: opts.serverData?.locale || config.defaultLocale.lang,
      ...opts.containerAttributes,
    },
  });
}

Build the production app in preview mode:

npm run preview

Inspect the qwik-speak-inline.log file in root folder:

client: root.tsx
dynamic key: t(head.title) - skip
dynamic key: t(m.content) - skip

It contains the non-inlined dynamic keys that we added in the runtime.json file.

The app will have the same behavior as you saw in dev mode, but now the translations are inlined as you can verify by inspecting the production files, reducing resource usage at runtime