npm create qwik@latest
npm install qwik-speak --save-devLet's create speak-config.ts and speak-functions.ts files in src:
src/speak-config.ts
import type { SpeakConfig } from 'qwik-speak';
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
import { server$ } from '@builder.io/qwik-city';
import type { LoadTranslationFn, Translation, TranslationFn } from 'qwik-speak';
/**
* 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.
Just wrap Qwik City provider with QwikSpeakProvider component in root.tsx and pass it the configuration and the translation functions:
src/root.tsx
import { QwikSpeakProvider } from 'qwik-speak';
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/index.tsx
import {
$translate as t,
formatDate as fd,
formatNumber as fn,
Speak,
} 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>
);
});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].
Speakcomponent is aSlotcomponent: because Qwik rendersSlotcomponents and direct children in isolation, translations are not immediately available in direct children, and we need to use a component for theHomepage. It is generally not necessary to use more than oneSpeakcomponent per page
We can resolve the locale to use in two ways: passing the locale parameter to the QwikSpeakProvider component, or assigning it to the locale handled by Qwik. Create plugin.ts in the root of the src/routes directory:
src/routes/plugin.ts
export const onRequest: RequestHandler = ({ request, locale }) => {
const cookie = request.headers?.get('cookie');
const acceptLanguage = request.headers?.get('accept-language');
let lang: string | null = null;
// Try whether the language is stored in a cookie
if (cookie) {
const result = new RegExp('(?:^|; )' + encodeURIComponent('locale') + '=([^;]*)').exec(cookie);
if (result) {
lang = JSON.parse(result[1])['lang'];
}
}
// Try to use user language
if (!lang) {
if (acceptLanguage) {
lang = acceptLanguage.split(';')[0]?.split(',')[0];
}
}
// Set Qwik locale
locale(lang || config.defaultLocale.lang);
};Internally, Qwik Speak will try to take the Qwik locale, before falling back to default locale if it is not in supportedLocales.
Now we want to change locale. Let's create a ChangeLocale component:
src/components/change-locale.tsx
import { $translate as t, useSpeakConfig, SpeakLocale } from 'qwik-speak';
export const ChangeLocale = component$(() => {
const config = useSpeakConfig();
const changeLocale$ = $((newLocale: SpeakLocale) => {
// Store locale in a cookie
document.cookie = `locale=${JSON.stringify(newLocale)};max-age=86400;path=/`;
location.reload();
});
return (
<div>
<h2>{t('app.changeLocale@@Change locale')}</h2>
{config.supportedLocales.map(value => (
<button key={value.lang} onClick$={async () => await changeLocale$(value)}>
{value.lang}
</button>
))}
</div>
);
});and add the component in header.tsx:
export default component$(() => {
return (
<header>
<ChangeLocale />
</header>
);
});In changeLocale$ we set the locale in a cookie, before reloading the page.
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-extractThe following files are generated:
i18n/en-US/app.json
i18n/en-US/home.json
i18n/it-IT/app.json
i18n/it-IT/home.json
extracted keys: 4
app asset and home asset for each language, initialized with the default values we provided.
We can translate the it-IT files, and run the app:
npm startProduction: Qwik Speak Inline Vite plugin
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,
});
}Build the production app in preview mode:
npm run previewThe 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