Step by step, let's build a sample app with Qwik Speak and a localized router using Qwik City features
See Quick Start
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
// }
};
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
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 />
</>
);
});
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.
We can translate the it-IT
files and start the app:
npm start
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.
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
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.
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>
))}
</>
);
});