diff --git a/README.md b/README.md
index 00a4854..13ed945 100644
--- a/README.md
+++ b/README.md
@@ -9,10 +9,11 @@ npm install qwik-speak --save-dev
## Getting Started
- [Quick Start](./docs/quick-start.md)
-- [Tutorial: localized routing with prefix only](./docs/tutorial-routing.md)
-- [Tutorial: localized routing with url rewriting](./docs/tutorial-routing-rewrite.md)
+- [Tutorial: localized routing with the language](./docs/tutorial-routing.md)
+- [Tutorial: translated routing with url rewriting](./docs/tutorial-routing-rewrite.md)
- [Translate](./docs/translate.md)
-- [Translation functions](./docs/translation-functions.md)
+- [Translation functions](./docs/translation-functions.md)
+- [Lazy loading translation](./docs/lazy-loading.md)
- [Qwik Speak and Adapters](./docs/adapters.md)
- [Testing](./docs/testing.md)
@@ -21,15 +22,15 @@ Live example on [Cloudflare pages](https://qwik-speak.pages.dev/) and playground
## Overview
### Getting the translation
```tsx
-import { useTranslate } from 'qwik-speak';
+import { inlineTranslate } from 'qwik-speak';
export default component$(() => {
- const t = useTranslate();
+ const t = inlineTranslate();
return (
<>
-
{t('app.title@@Qwik Speak')}
{/* Qwik Speak */}
-
{t('home.greeting@@Hi! I am {{name}}', { name: 'Qwik Speak' })}
{/* Hi! I am Qwik Speak */}
+
{t('title@@Qwik Speak')}
{/* Qwik Speak */}
+
{t('greeting@@Hi! I am {{name}}', { name: 'Qwik Speak' })}
{/* Hi! I am Qwik Speak */}
>
);
});
@@ -53,6 +54,11 @@ export default component$(() => {
});
```
+## Static translations
+Translation are loaded and inlined in chunks sent to the browser during the build.
+
+See [Qwik Speak Inline Vite plugin](./docs/inline.md) for more information on how it works and how to use it.
+
## Extraction of translations
To extract translations directly from the components, a command is available that automatically generates the files with the keys and default values.
@@ -63,11 +69,6 @@ To automatically translate files, an external command is available that uses Ope
See [GPT Translate JSON](./docs/gpt-translate-json.md) for more information on how to use it.
-## Production
-In production, translations are loaded and inlined during the build.
-
-See [Qwik Speak Inline Vite plugin](./docs/inline.md) for more information on how it works and how to use it.
-
## Speak context
```mermaid
stateDiagram-v2
@@ -93,16 +94,11 @@ stateDiagram-v2
- loadTranslation$
end note
note right of State5
- key-value pairs
- of translation data
+ runtime assets
end note
```
> `SpeakState` is immutable: it cannot be updated after it is created and is not reactive
-- `useSpeakContext()` Returns the Speak state
-- `useSpeakConfig()` Returns the configuration in Speak context
-- `useSpeakLocale()` Returns the locale in Speak context
-
### Speak config
- `defaultLocale` The default locale to use as fallback
- `supportedLocales` List of locales supported by the app
@@ -110,7 +106,7 @@ stateDiagram-v2
- `runtimeAssets` Assets available at runtime
- `keySeparator` Separator of nested keys. Default is `.`
- `keyValueSeparator` Key-value separator. Default is `@@`
-- `rewriteRoutes` Rewrite routes as specified in Vite config for qwikCity
+- `rewriteRoutes` Rewrite routes as specified in Vite config for `qwikCity` plugin
### SpeakLocale
The `SpeakLocale` object contains the `lang`, in the format `language[-script][-region]`, where:
@@ -129,36 +125,34 @@ and optionally contains:
`TranslationFn` interface can be implemented to change the behavior of the library:
- `loadTranslation$` QRL function to load translation data
+### Translation
+`Translation` contains only the key value pairs of the translation data provided with the `runtimeAssets`
+
## APIs
-### Components
-#### QwikSpeakProvider component
-`QwikSpeakProvider` component provides the Speak context to the app. `Props`:
+### Providers
+`useQwikSpeak(props: QwikSpeakProps)` provides the Speak context to the app. `QwikSpeakProps`:
- `config` Speak config
- `translationFn` Optional functions to use
- - `locale` Optional locale to use
- `langs` Optional additional languages to preload data for (multilingual)
-#### Speak component (scoped translations)
-`Speak` component can be used for scoped translations. `Props`:
+`useSpeak(props: SpeakProps) ` can be used for lazy loading translation. `SpeakProps`:
- `assets` Assets to load
- `runtimeAssets` Assets to load available at runtime
- `langs` Optional additional languages to preload data for (multilingual)
-### Functions
-#### Translate
-- `useTranslate: () => (keys: string | string[], params?: Record, lang?: string)`
-Translates a key or an array of keys. The syntax of the string is `key@@[default value]`
+### Context
+- `useSpeakContext()` Returns the Speak state
+- `useSpeakConfig()` Returns the configuration in Speak context
+- `useSpeakLocale()` Returns the locale in Speak context
-- `inlineTranslate(keys: string | string[], ctx: SpeakState, params?: Record, lang?: string)`
-Translates a key or an array of keys outside the `component$`. The syntax of the string is `key@@[default value]`
+### Translate
+- `inlineTranslate: () => (keys: string | string[], params?: Record, lang?: string)`
+Translates a key or an array of keys. The syntax of the string is `key@@[default value]`
-- `usePlural: () => (value: number | string, key?: string, params?: Record, options?: Intl.PluralRulesOptions, lang?: string)`
+- `inlinePlural: () => (value: number | string, key?: string, params?: Record, options?: Intl.PluralRulesOptions, lang?: string)`
Gets the plural by a number using [Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) API
-- `useTranslatePath: () => (paths: string | string[], lang?: string)`
-Translates a path or an array of paths. The translating string can be in any language. If not specified the target lang is the current one
-
-#### Localize
+### Localize
- `useFormatDate: () => (value: Date | number | string, options?: Intl.DateTimeFormatOptions, lang?: string, timeZone?: string)`
Formats a date using [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) API
@@ -171,6 +165,16 @@ Formats a number using [Intl.NumberFormat](https://developer.mozilla.org/en-US/d
- `useDisplayName: () => (code: string, options: Intl.DisplayNamesOptions, lang?: string)`
Returns the translation of language, region, script or currency display names using [Intl.DisplayNames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames) API
+### Routing
+- `localizePath: () => (route: (string | URL) | string[], lang?: string)`
+Localize a path, an URL or an array of paths with the language
+
+- `translatePath: () => (route: (string | URL) | string[], lang?: string)`
+Translates a path, an URL or an array of paths. The translating string can be in any language. If not specified the target lang is the current one
+
+### Testing
+- `QwikSpeakMockProvider` component provides the Speak context to test enviroments
+
## Development Builds
### Library & tools
#### Build
diff --git a/SUMMARY.md b/SUMMARY.md
index cbc89de..14eef82 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -3,10 +3,11 @@
## Library
* [Quick Start](docs/quick-start.md)
-* [Tutorial: localized routing with prefix only](docs/tutorial-routing.md)
-* [Tutorial: localized routing with url rewriting](docs/tutorial-routing-rewrite.md)
+* [Tutorial: localized routing with the language](docs/tutorial-routing.md)
+* [Tutorial: translated routing with url rewriting](docs/tutorial-routing-rewrite.md)
* [Translate](docs/translate.md)
* [Translation functions](docs/translation-functions.md)
+* [Lazy loading translation](docs/lazy-loading.md)
* [Qwik Speak and Adapters](docs/adapters.md)
* [Testing](docs/testing.md)
diff --git a/docs/adapters.md b/docs/adapters.md
index 289e405..00a9fe5 100644
--- a/docs/adapters.md
+++ b/docs/adapters.md
@@ -7,7 +7,7 @@ If your production environment doesn't support _dynamic import_, you can import
/**
* Translation files are imported directly as string
*/
-const translationData = import.meta.glob('/i18n/**/*.json', { as: 'raw', eager: true });
+const translationData = import.meta.glob('/i18n/**/*.json', { as: 'raw', eager: true });
const loadTranslation$: LoadTranslationFn = server$((lang: string, asset: string) =>
JSON.parse(translationData[`/i18n/${lang}/${asset}.json`])
diff --git a/docs/extract.md b/docs/extract.md
index 9b028e1..73e35f1 100644
--- a/docs/extract.md
+++ b/docs/extract.md
@@ -7,13 +7,13 @@
#### Get the code ready
Optionally, you can use a default value for the keys. The syntax is `key@@[default value]`:
```html
-
{t('app.title@@Qwik Speak'}
-
{t('home.greeting@@Hi! I am {{name}}', { name: 'Qwik Speak' })}
+
{t('title@@Qwik Speak'}
+
{t('greeting@@Hi! I am {{name}}', { name: 'Qwik Speak' })}
```
When you use a default value, it will be used as initial value for the key in every translation.
-> Note. A key will not be extracted when it is an identifier or contains an indentifier (dynamic)
+> Note that it is not necessary to provide the default value of a key every time: it is sufficient and not mandatory to provide it once in the app
#### Naming conventions
If you use scoped translations, the first property will be used as filename:
diff --git a/docs/inline.md b/docs/inline.md
index d9ae365..b28cae5 100644
--- a/docs/inline.md
+++ b/docs/inline.md
@@ -1,18 +1,19 @@
# Qwik Speak Inline Vite plugin
-> Inline Qwik Speak `useTranslate`, `inlineTranslate` and `usePlural` functions at compile time
+> Inline Qwik Speak `inlineTranslate` and `inlinePlural` functions at compile time
## How it works
-In development mode, translation happens _at runtime_: `assets` are loaded during SSR or on client, and the lookup also happens at runtime.
+O the server, translation happens _at runtime_: `assets` are loaded during SSR and the lookup also happens at runtime.
-In production mode, `assets` are loaded only during SSR, and to get the translations on the client as well you have to use _Qwik Speak Inline_ Vite plugin.
+On the client, translation happens _at compile-time_: `assets` are loaded and inlined in chunks sent to the browser during the build, reducing resource usage at runtime.
-Using the _Qwik Speak Inline_, translation happens _at compile-time_: `assets` are loaded and inlined in chunks sent to the browser during the build, reducing resource usage at runtime:
+`runtimeAssets` are always loaded at runtime, both on the server or on the client, allowing dynamic translations.
```mermaid
sequenceDiagram
participant Server
participant assets
+ participant runtimeAssets
participant Client
Server->>assets: loadTranslation$
activate assets
@@ -20,6 +21,12 @@ sequenceDiagram
deactivate assets
Server->>Client: SSR: no serialize data
Note over Client: inlined data
+ Server->>runtimeAssets: loadTranslation$
+ activate runtimeAssets
+ runtimeAssets-->>Server: data
+ deactivate runtimeAssets
+ Server->>Client: SSR: serialize data
+ Note over Client: runtime data
```
## Usage
@@ -104,32 +111,4 @@ _dist/build/it-IT/q-*.js_
At the end of the build, in root folder a `qwik-speak-inline.log` file is generated which contains:
- Missing values
-- Translations with dynamic keys
-- Translations with dynamic params
-
-## Qwik Speak Inline Vite plugin & runtime
-When there are translations with dynamic keys or params, you have to use separate files, and add them to `runtimeAssets`:
-
-```typescript
-export const config: SpeakConfig = {
- /* ... */
- assets: [
- 'app' // Translations shared by the pages
- ],
- runtimeAssets: [
- 'runtime' // Translations with dynamic keys or parameters
- ]
-};
-```
-Likewise, you can also create scoped runtime files for different pages and pass them to `Speak` components:
-```tsx
-export default component$(() => {
- return (
-
-
-
- );
-});
-```
-> `runtimeAssets` are serialized and sent to the client, and loaded when required
-
+- Translations with dynamic keys or params
diff --git a/docs/lazy-loading.md b/docs/lazy-loading.md
new file mode 100644
index 0000000..9500c98
--- /dev/null
+++ b/docs/lazy-loading.md
@@ -0,0 +1,43 @@
+# Lazy loading translation
+
+If you are developing a large app, you can consider using lazy loading translation: translations that are lazy loaded only when requested (when the user navigates to a specific section or page of the app):
+
+```mermaid
+C4Container
+ Container_Boundary(a, "App") {
+ Component(a0, "root", "useQwikSpeak", "Translations available in the whole app")
+ Container_Boundary(b1, "Site") {
+ Component(b10, "Page", "useSpeak", "Translations available in Page component")
+
+ }
+ Container_Boundary(b2, "Admin") {
+ Component(b20, "layout", "useSpeak", "Translations available in child components")
+ }
+ }
+```
+
+For lazy loading of files in a specific section, you need to add `useSpeak` to the layout:
+```tsx
+import { useSpeak } from 'qwik-speak';
+
+export default component$(() => {
+ useSpeak({assets:['admin'], runtimeAssets: ['runtimeAdmin']});
+
+ return (
+ <>
+
+
+
+ >
+ );
+});
+```
+or in a specific page:
+```tsx
+export default component$(() => {
+ useSpeak({runtimeAssets: ['runtimePage']});
+
+ return ;
+});
+```
+> Note that you must create a component for the page, because Qwik renders components in isolation, and translations are only available in child components
\ No newline at end of file
diff --git a/docs/quick-start.md b/docs/quick-start.md
index d23dec8..5f50107 100644
--- a/docs/quick-start.md
+++ b/docs/quick-start.md
@@ -1,14 +1,34 @@
# Quick Start
-> Step by step, let's build a sample app with Qwik Speak
+> Setup an app with Qwik Speak
-```shell
-npm create qwik@latest
+```shell
npm install qwik-speak --save-dev
```
+## Vite plugin
+Add [Qwik Speak Inline Vite plugin](./inline.md) in `vite.config.ts`:
+```typescript
+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(),
+ ],
+ };
+});
+```
+
## Configuration
-Let's create `speak-config.ts` and `speak-functions.ts` files in `src`:
+Let's create `speak-config.ts` and `speak-functions.ts` files in `src` folder:
_src/speak-config.ts_
```typescript
@@ -20,8 +40,13 @@ export const config: SpeakConfig = {
{ lang: 'it-IT', currency: 'EUR', timeZone: 'Europe/Rome' },
{ lang: 'en-US', currency: 'USD', timeZone: 'America/Los_Angeles' }
],
+ // Translations available in the whole app
assets: [
- 'app' // Translations shared by the pages
+ 'app'
+ ],
+ // Translations with dynamic keys available in the whole app
+ runtimeAssets: [
+ 'runtime'
]
};
```
@@ -47,168 +72,47 @@ export const translationFn: TranslationFn = {
loadTranslation$: loadTranslation$
};
```
-We have added the Speak config and the implementation of the `loadTranslation$` function to load translation files.
-
> `loadTranslation$` is a customizable QRL function: you can load the translation files in the way you prefer
-## Adding Qwik Speak
-Just wrap Qwik City provider with `QwikSpeakProvider` component in `root.tsx` and pass it the configuration and the translation functions:
+
+Add `useQwikSpeak` provider in `root.tsx` and pass it the configuration and the translation functions:
_src/root.tsx_
```tsx
-import { QwikSpeakProvider } from 'qwik-speak';
-
-import { config } from './speak-config';
-import { translationFn } from './speak-functions';
+import { useQwikSpeak } from 'qwik-speak';
+import { config } from "./speak-config";
+import { translationFn } from "./speak-functions";
export default component$(() => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-```
-
-Finally we add an `index.tsx` with some translation, providing optional default values for each translation: `key@@[default value]`:
-
-_src/routes/index.tsx_
-```tsx
-import {
- useTranslate,
- useFormatDate,
- useFormatNumber,
- Speak,
-} from 'qwik-speak';
-
-interface TitleProps {
- name: string;
-}
-
-export const Title = component$(props => {
- return (
- >
- );
-});
-
-export default component$(() => {
- return (
- /**
- * Add Home translations (only available in child components)
- */
-
-
-
- );
-});
-
-export const head: DocumentHead = {
- title: 'home.head.title@@Qwik Speak',
- meta: [{ name: 'description', content: 'home.head.description@@Quick start' }]
-};
-```
-
-## Scoped translation
-We have used the `Speak` component to add scoped translations to the `Home` component:
-- `Home` component will use the `home` asset, in addition to the `app` asset that comes with the configuration
-- Using the asset name `home` as the root property in each key is the best practice to avoid keys in different files being overwritten
-
-> `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 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_
-```tsx
-export const RouterHead = component$(() => {
- const t = useTranslate();
-
- const head = useDocumentHead();
-
- return (
- <>
- {t(head.title, { name: 'Qwik Speak' })}
-
- {head.meta.map((m) => (
-
- ))}
- >
+
+ {/* ... */}
+
);
});
```
-We can also pass the `lang` attribute in the html tag:
-
-_src/entry.ssr.tsx_
-```typescript
-import { config } from './speak-config';
-
-export default function (opts: RenderToStreamOptions) {
- return renderToStream(, {
- manifest,
- ...opts,
- // Use container attributes to set attributes on the html tag
- containerAttributes: {
- lang: opts.serverData?.locale || config.defaultLocale.lang,
- ...opts.containerAttributes,
- },
- });
-}
-```
-
## Resolve locale
-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:
+Create `plugin.ts` in the root of the `src/routes` directory:
_src/routes/plugin.ts_
```typescript
+import type { RequestHandler } from '@builder.io/qwik-city';
import { config } from '../speak-config';
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];
- }
+ if (acceptLanguage) {
+ lang = acceptLanguage.split(';')[0]?.split(',')[0];
}
// Check supported locales
@@ -218,155 +122,13 @@ export const onRequest: RequestHandler = ({ request, locale }) => {
locale(lang);
};
```
-Internally, Qwik Speak will try to use the Qwik `locale`, before falling back to default locale if it is not in `supportedLocales`.
+> We're on the server here, and you can get the language from `acceptLanguage`, a cookie, or a URL parameter, as you like. But is mandatory to set the Qwik locale
-## Change locale
-Now we want to change locale. Let's create a `ChangeLocale` component:
-
-_src/components/change-locale.tsx_
-```tsx
-import type { SpeakLocale } from 'qwik-speak';
-import { useSpeakConfig, useTranslate } from 'qwik-speak';
-
-export const ChangeLocale = component$(() => {
- const t = useTranslate();
-
- 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 (
-
- );
-});
-```
-and add the component in `header.tsx`:
-```tsx
-export default component$(() => {
- return (
-
-
-
- );
-});
-```
-In `changeLocale$` we set the locale in a cookie, before reloading the page.
-
-## Extraction
-We can now extract the translations and generate the `assets` as json. In `package.json` add the following command to the scripts:
-```json
-"qwik-speak-extract": "qwik-speak-extract --supportedLangs=en-US,it-IT --assetsPath=i18n"
-```
-
-```shell
-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. We have to add them manually in a new file that we will call `runtime`:
-
-_i18n/[lang]/runtime.json_
-```json
-{
- "runtime": {
- "home": {
- "head": {
- "title": "Qwik Speak",
- "description": "Quick start"
- }
- }
- }
-}
-```
-Update the keys in `DocumentHead` of `index.tsx`:
-```tsx
-export const head: DocumentHead = {
- title: 'runtime.home.head.title@@Qwik Speak',
- meta: [{ name: 'description', content: 'runtime.home.head.description@@Quick start' }]
-};
-```
-and add `runtime` asset in Speak config:
-```typescript
-assets: [
- 'app' // Translations shared by the pages
-],
-runtimeAssets: [
- 'runtime' // Translations with dynamic keys or parameters
-]
-```
-
-See [Qwik Speak Extract](./extract.md) for more details.
-
-## Translation
-We can translate the `it-IT` files.
-
-If you have an OpenAI API key, you could use `gpt-translate-json` package:
-```shell
-npm install gpt-translate-json --save-dev
-```
-In `package.json` add the following command to the scripts:
-```json
-"gpt-translate-json": "gpt-translate-json --apiKey=openai_api_key --model=gpt-3.5-turbo --maxTokens=3000 --langs=en-US,it-IT --originalLang=en-US"
-```
-
-```shell
-npm gpt-translate-json
-```
-
-Run the app:
-```shell
-npm start
-```
-
-See [GPT Translate JSON](./gpt-translate-json.md) for more details.
-
-## Production
-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`:
-```typescript
-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:
```typescript
import { isDev } from '@builder.io/qwik/build';
+import type { RenderOptions } from "@builder.io/qwik/server";
+import { config } from './speak-config';
/**
* Determine the base URL to use for loading the chunks in the browser.
@@ -387,22 +149,15 @@ export default function (opts: RenderToStreamOptions) {
...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:
-```shell
-npm run preview
-```
-Inspect the `qwik-speak-inline.log` file in root folder:
-
-```
-client: root.tsx
-dynamic key: t(head.title) - Make sure the keys are in 'runtimeAssets'
-dynamic key: t(m.content) - Make sure the keys are in 'runtimeAssets'
-```
-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
-See [Qwik Speak Inline Vite plugin](./inline.md) for more details.
+## Tutorials
+- [Tutorial: localized routing with the language](./tutorial-routing.md)
+- [Tutorial: translated routing with url rewriting](./tutorial-routing-rewrite.md)
diff --git a/docs/testing.md b/docs/testing.md
index ae97f72..99380aa 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -2,44 +2,23 @@
> Unit test a Qwik component using Qwik Speak
-To unit test a component which uses `qwik-speak`, you need to wrap it with `QwikSpeakProvider` component, so that it can pass the `SpeakContext` to the test component and its children.
+To unit test a component which uses `qwik-speak`, you need to wrap it with `QwikSpeakMockProvider` component, so that it can pass the `SpeakContext` to the test component and its children.
-Given the `config` object and a component to test like in [Quick Start](./quick-start.md):
+Given the `config` object and a component to test like:
_src/routes/index.tsx_
```tsx
-import {
- useTranslate,
- useFormatDate,
- useFormatNumber,
- Speak,
-} from 'qwik-speak';
+import { inlineTranslate, useFormatDate, useFormatNumber } from 'qwik-speak';
-export const Home = component$(() => {
- const t = useTranslate();
- const fd = useFormatDate();
- const fn = useFormatNumber();
+export default component$(() => {
+ const t = inlineTranslate();
return (
<>
>
);
});
-
-export default component$(() => {
- return (
-
-
-
- );
-});
```
We'll have the following unit test (using _Vitest_):
@@ -52,9 +31,9 @@ test(`[Home Component]: Should render the component`, async () => {
const { screen, render } = await createDOM();
await render(
-
+
-
+
);
expect(screen.outerHTML).toContain('Qwik Speak demo');
diff --git a/docs/translate.md b/docs/translate.md
index bd47a02..44ebbc0 100644
--- a/docs/translate.md
+++ b/docs/translate.md
@@ -1,12 +1,12 @@
# Translate
-> The return functions of `useTranslate`, `inlineTranslate` and `usePlural` are parsed and replaced with translated texts at compile time. For this reason, they expect _values_ or _identifiers_ as parameters, and no JavaScript _operators_
+> The return functions of `inlineTranslate` and `inlinePlural` are parsed and replaced with translated texts in chunks sent to the browser at compile time
-## useTranslate
-`useTranslate` returns a functions to get the translation using key-value pairs:
+## inlineTranslate
+`inlineTranslate` returns a functions to get the translation using key-value pairs:
```tsx
-const t = useTranslate();
+const t = inlineTranslate();
-t('home.title@@Qwik Speak')
+t('title@@Qwik Speak')
```
Value after `@@` is the optional default value:
```tsx
@@ -16,7 +16,7 @@ Value after `@@` is the optional default value:
### Params interpolation
`t` function accept params as well:
```tsx
-t('home.greeting@@Hi! I am {{name}}', { name: 'Qwik Speak' })
+t('greeting@@Hi! I am {{name}}', { name: 'Qwik Speak' })
```
`name` param is replaced at runtime or during the inlining:
```text
@@ -37,16 +37,14 @@ and returns an array of translated values:
`t` function can get arrays and objects directly from files:
```json
{
- "home": {
- "array": [
- "one",
- "two",
- "three"
- ],
- "obj": {
- "one": "one",
- "two": "two"
- }
+ "array": [
+ "one",
+ "two",
+ "three"
+ ],
+ "obj": {
+ "one": "one",
+ "two": "two"
}
}
```
@@ -54,115 +52,54 @@ just pass to the function the type parameter:
```tsx
import type { Translation } from 'qwik-speak';
-t('home.array')
-t('home.obj')
+t('array')
+t('obj')
```
You can also access by array position:
```tsx
-t('home.array.2@@three')
+t('array.2@@three')
```
Finally, it is possible to set arrays and objects passing a _valid stringified_ default value:
```tsx
-t('home.array@@["one","two","three"]')
-t('home.obj@@{"one":"one","two":"two"}')
+t('array@@["one","two","three"]')
+t('obj@@{"one":"one","two":"two"}')
```
### Html in translations
You can have Html in translations, like:
```json
{
- "home": {
- "text": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps"
- }
+ "description": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps"
}
```
but you have to use `dangerouslySetInnerHTML`:
```tsx
-
+
```
> On the client the text is _inlined_ during build, so there are no XSS risks
-## component$ props
-> A component can wake up independently from the parent component. If the component wakes up, it needs to be able to know its props
-
-Prefer translating inside components rather than on props:
-
-```tsx
-export const Title = component$((props) => {
- return (
{props.name}
)
-});
-
-export const Home = component$(() => {
- const t = useTranslate();
-
- const name = t('app.title');
- return (
-
- );
-});
-```
-or
-```tsx
-export const Title = component$((props) => {
- const t = useTranslate();
-
- return (
{t(props.name)}
)
-});
-
-export const Home = component$(() => {
- return (
-
- );
-});
-```
-In the latter case, `app.title` will have to be placed in the `runtimeAssets`, as a dynamic key is passed to the `t` function.
-
-## inlineTranslate
-`inlineTranslate` function has the same behavior as the function returned by `useTranslate`, but can be used outside the `component$`, for example in _Inline components_, passing the Speak context as second argument:
+## inlinePlural
+`inlinePlural` returns a functions that uses [Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) API:
```tsx
-import { inlineTranslate, useSpeakContext } from 'qwik-speak';
+const p = inlinePlural();
-export const TitleComponent = (props: { ctx: SpeakState }) => {
- return
- );
-});
+p(1, 'devs')
```
-
-## usePlural
-`usePlural` returns a functions that uses [Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) API:
-```tsx
-const p = usePlural();
-
-p(1, 'home.devs')
-```
-When you run the extraction tool, it creates a translation file with the Intl API plural rules for each language:
+When you run the extraction tool, it creates the Intl API plural rules for each language:
```json
{
- "home": {
- "devs": {
- "one": "",
- "other": ""
- }
+ "devs": {
+ "one": "",
+ "other": ""
}
}
```
-There is no default value for `usePlural` function, so you must add the translation in each language, keeping in mind that the counter is optionally interpolated with the `value` parameter:
+There is no default value for `inlinePlural` function, so you must add the translation in each language, keeping in mind that the counter is optionally interpolated with the `value` parameter:
```json
{
- "home": {
- "devs": {
- "one": "{{ value }} software developer",
- "other": "{{ value }} software developers"
- }
+ "devs": {
+ "one": "{{ value }} software developer",
+ "other": "{{ value }} software developers"
}
}
```
@@ -171,6 +108,54 @@ It is rendered as:
1 software developer
```
+## Runtime translation
+When you use a translation like this:
+```tsx
+const key = 'dynamic';
+
+t(key)
+```
+you are using a dynamic translation. It means that it is not possible to evaluate the translation at compile time but only at runtime based on the value that the key takes on.
+
+To instruct Qwik Speak to use dynamic translations, create a file with the values that these translations can take:
+
+_i18n/[lang]/runtime.json_
+```json
+{
+ "dynamic": "I'm a dynamic value"
+}
+```
+and add the `runtime` file to `runtimeAssets` in configuration or `useSpeak` provider.
+
+## Server translation
+`inlineTranslate` and `inlinePlural` work in `component$`, _Inline components_, QRL and functions if called by the components, but they might not work in functions invoked on the server, such as `routeLoader$` and _endpoints_.
+
+Functions like `routeLoader$` live on the server, which knows nothing about the context of the app, and depending on the case they can be invoked before the app runs. To translate on the server you need:
+- make sure translations are available
+- let the server know the current language of the user
+
+`server$` function can satisfy both conditions, since the function is executed only when invoked, and accepts parameters:
+
+```tsx
+export const serverFn = server$(function (lang: string) {
+ const t = inlineTranslate();
+
+ return t('title', { name: 'Qwik Speak' }, lang);
+});
+
+export default component$(() => {
+ const locale = useSpeakLocale();
+ const s = useSignal('');
+
+ useTask$(async () => {
+ s.value = await serverFn(locale.lang)
+ });
+
+ return (
{s.value}
);
+});
+```
+You can also extract the language directly into the function, through the request (cookies, params), instead of passing it as a parameter.
+
# Localize
## useFormatDate
`useFormatDate` returns a functions that uses [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) API to format dates:
@@ -251,13 +236,11 @@ American English
# Multilingual
Each of the translation and localization functions accepts a different language other than the current one as its last argument:
```tsx
-const t = useTranslate();
+const t = inlineTranslate();
-t('home.title@@Qwik Speak', undefined, 'it-IT')
+t('title@@Qwik Speak', undefined, 'it-IT')
```
-For the translation to occur in the language passed as an argument, you need to pass the additional language to `QwikSpeakProvider` or `Speak` components:
+For the translation to occur in the language passed as an argument, you need to set the additional language to `useQwikSpeak` or `useSpeak` providers:
```tsx
-
-
-
+useQwikSpeak({ config, translationFn, langs: ['it-IT'] });
```
diff --git a/docs/tutorial-routing-rewrite.md b/docs/tutorial-routing-rewrite.md
index 8b3cbdf..0576f30 100644
--- a/docs/tutorial-routing-rewrite.md
+++ b/docs/tutorial-routing-rewrite.md
@@ -1,12 +1,11 @@
-# Tutorial: localized routing with url rewriting
+# Tutorial: translated routing with url rewriting
> Step by step, let's build a sample app with Qwik Speak and translated paths using Qwik City features
-```shell
-npm create qwik@latest
-npm install qwik-speak --save-dev
-```
+## Setup
+See [Quick Start](./quick-start.md)
+## 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/`
@@ -18,8 +17,7 @@ Or:
But we DON'T want to have this url instead:
- other languages (it-IT): localized routes `http://127.0.0.1:4173/it-IT/page`
-## Configuration
-Let's create `speak-routes.ts` file in `src`:
+Now let's handle it. Create `speak-routes.ts` file in `src`:
_src/speak-routes.ts_
```typescript
@@ -29,32 +27,43 @@ import type { RewriteRouteOption } from 'qwik-speak';
* Translation paths
*/
export const rewriteRoutes: RewriteRouteOption[] = [
+ // No prefix for default locale
+ // {
+ // paths: {
+ // 'page': 'page'
+ // }
+ // },
{
prefix: 'it-IT',
paths: {
- 'page': 'pagina'
+ 'page': 'pagina'
}
}
-]
+];
```
-and update `qwikCity` Vite plugin in `vite.config.ts`:
+Add `rewriteRoutes` to `qwikCity` Vite plugin in `vite.config.ts`:
+
```typescript
+import { qwikSpeakInline } from 'qwik-speak/inline';
+
import { rewriteRoutes } from './src/speak-routes';
export default defineConfig(() => {
return {
plugins: [
- qwikCity(
- { rewriteRoutes }
- ),
+ qwikCity({ rewriteRoutes }),
qwikVite(),
+ qwikSpeakInline({
+ supportedLangs: ['en-US', 'it-IT'],
+ defaultLang: 'en-US',
+ assetsPath: 'i18n'
+ }),
tsconfigPaths(),
],
};
});
```
-
-Now create `speak-config.ts` and `speak-functions.ts` files in `src`:
+Add `rewriteRoutes` to `speak-config.ts` in `src`:
_src/speak-config.ts_
```typescript
@@ -69,39 +78,17 @@ export const config: SpeakConfig = {
{ lang: 'it-IT', currency: 'EUR', timeZone: 'Europe/Rome' },
{ lang: 'en-US', currency: 'USD', timeZone: 'America/Los_Angeles' }
],
+ // Translations available in the whole app
assets: [
- 'app' // Translations shared by the pages
+ 'app'
+ ],
+ // Translations with dynamic keys available in the whole app
+ runtimeAssets: [
+ 'runtime'
]
};
```
-_src/speak-functions.ts_
-```typescript
-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('/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 to load translation files.
-
-> `loadTranslation$` is a customizable QRL function: you can load the translation files in the way you prefer
-
-## Routing
-Now let's handle the routing. Create `plugin.ts` in the root of the `src/routes` directory:
+Update `plugin.ts` in the root of the `src/routes` directory:
_src/routes/plugin.ts_
```typescript
@@ -122,230 +109,133 @@ export const onRequest: RequestHandler = ({ url, locale }) => {
locale(lang || config.defaultLocale.lang);
};
```
-We assign the value of the `lang` parameter (from the url prefix) to Qwik `locale`. This way it will be immediately available to the library.
-## Adding Qwik Speak
-Just put `QwikSpeakProvider` inside Qwik City provider component in `root.tsx` and pass it the configuration and the translation functions:
+## Usage
+Add `index.tsx` with some translation, providing optional default values for each translation: `key@@[default value]`:
-_src/root.tsx_
+_src/routes//index.tsx_
```tsx
-import { QwikSpeakProvider } from 'qwik-speak';
-
-import { config } from './speak-config';
-import { translationFn } from './speak-functions';
+import { inlineTranslate, useFormatDate, useFormatNumber } from 'qwik-speak';
export default component$(() => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-```
+ const t = inlineTranslate();
-Now we add an `index.tsx` with some translation, providing optional default values for each translation: `key@@[default value]`:
-
-_src/routes/index.tsx_
-```tsx
-import {
- useTranslate,
- useFormatDate,
- useFormatNumber,
- Speak,
-} from 'qwik-speak';
-
-interface TitleProps {
- name: string;
-}
-
-export const Title = component$(props => {
- return (
{props.name}
)
-});
-
-export const Home = component$(() => {
- const t = useTranslate();
const fd = useFormatDate();
const fn = useFormatNumber();
- // Prefer translating inside components rather than on props
- const title = t('app.title@@{{name}} demo', { name: 'Qwik Speak' });
-
return (
<>
-
+
>
);
});
-export default component$(() => {
- return (
- /**
- * Add Home translations (only available in child components)
- */
-
-
-
- );
-});
+export const head: DocumentHead = () => {
+ const t = inlineTranslate();
-export const head: DocumentHead = {
- title: 'home.head.title@@Qwik Speak',
- meta: [{ name: 'description', content: 'home.head.description@@Qwik Speak with localized routing' }]
+ 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:
-Finally we add a `page/index.tsx` to try the router:
-
-_src/routes/page/index.tsx_
+_src/routes//page/index.tsx_
```tsx
-import { component$ } from '@builder.io/qwik';
-import { useTranslate } from 'qwik-speak';
+import { inlineTranslate } from 'qwik-speak';
export default component$(() => {
- const t = useTranslate();
+ const t = inlineTranslate();
- return (
- <>
-
{t('app.title')}
-
{t('app.subtitle')}
- >
- );
-});
-```
-
-## Scoped translation
-We have used the `Speak` component to add scoped translations to the `Home` component:
-- `Home` component will use the `home` asset, in addition to the `app` asset that comes with the configuration
-- Using the asset name `home` as the root property in each key is the best practice to avoid keys in different files being overwritten
-
-> `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 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_
-```tsx
-export const RouterHead = component$(() => {
- const t = useTranslate();
-
- const head = useDocumentHead();
+ const key = 'dynamic';
return (
<>
- {t(head.title, { name: 'Qwik Speak' })}
+
{t('app.title', { name: 'Qwik Speak' })}
- {head.meta.map((m) => (
-
- ))}
+
{t(`runtime.${key}`)}
>
);
});
```
+> 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
-We can also pass the `lang` attribute in the html tag:
-
-_src/entry.ssr.tsx_
-```typescript
-import { config } from './speak-config';
-
-export default function (opts: RenderToStreamOptions) {
- return renderToStream(, {
- manifest,
- ...opts,
- // Use container attributes to set attributes on the html tag
- containerAttributes: {
- lang: opts.serverData?.locale || config.defaultLocale.lang,
- ...opts.containerAttributes,
- },
- });
-}
-```
+> 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.tsx_
+_src/components/change-locale/change-locale.tsx_
```tsx
-import type { SpeakLocale } from 'qwik-speak';
-import { useSpeakLocale, useSpeakConfig, useDisplayName, useTranslate, useTranslatePath } from 'qwik-speak';
+import { useLocation } from '@builder.io/qwik-city';
+import { useSpeakLocale, useSpeakConfig, useDisplayName, inlineTranslate, translatePath } from 'qwik-speak';
export const ChangeLocale = component$(() => {
- const t = useTranslate();
- const tp = useTranslatePath();
- const dn = useDisplayName();
+ const t = inlineTranslate();
+
+ const url = useLocation().url;
- const loc = useLocation()
const locale = useSpeakLocale();
const config = useSpeakConfig();
+ const dn = useDisplayName();
- // Replace the locale and navigate to the new URL
- const getLocalePath = (newLocale: SpeakLocale) => {
- const url = new URL(loc.url)
- url.pathname = tp(url.pathname, newLocale.lang)
- return url.toString();
- };
+ const getPath = translatePath();
return (
-