{/* Wednesday, July 20, 2022 at 7:09 AM */}
@@ -45,16 +70,22 @@ export default component$(() => {
);
});
```
+See [Localize](./docs/translate.md#localize) for more details.
+
+## 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.
See [Qwik Speak Extract](./docs/extract.md) for more information on how to use it.
-## Production
-Using _Qwik Speak Inline_ Vite plugin, 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.
+## Automatic translation
+To automatically translate files, the following external packages are available:
+- [GPT Translate JSON](https://github.com/robisim74/gpt-translate-json)
+- [Qwik Speak DeepL](https://www.npmjs.com/package/@tegonal/qwik-speak-deepl)
## Speak context
```mermaid
@@ -75,28 +106,29 @@ stateDiagram-v2
- currency
- timezone
- unit
+ - dir
+ - domain
end note
note right of State4
- 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
+> `SpeakState` is immutable: it cannot be updated after it is created and is not reactive
### Speak config
- `defaultLocale` The default locale to use as fallback
- `supportedLocales` List of locales supported by the app
-- `assets` An array of strings: each asset is passed to the `loadTranslation$` function to obtain data according to the language
+- `assets` Translation file names. Each asset is passed to the `loadTranslation$` function to obtain data according to the language
- `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` plugin
+- `domainBasedRouting` Domain-based routing options
+- `basePath` Base path of the application (e.g. '/reponame' for GitHub Pages)
+- `showDebugMessagesLocally` Whether to enable local debugging mode. Default is true
### SpeakLocale
The `SpeakLocale` object contains the `lang`, in the format `language[-script][-region]`, where:
@@ -109,58 +141,81 @@ and optionally contains:
- `currency` ISO 4217 three-letter code
- `timeZone` From the IANA time zone database
- `units` Key value pairs of unit identifiers
+- `dir` Text direction: `'ltr' | 'rtl' | 'auto'`
+- `domain` In domain-based routing, set the default domain for the locale
+- `withDomain` In domain-based routing, set another domain for the locale
### Translation functions
`TranslationFn` interface can be implemented to change the behavior of the library:
-- `loadTranslation$` Function to load translation data
+- `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)
+ - `currency` Optional currency if different from the current one
+ - `timeZone` Optional time zone if different from the current one
-#### 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
-- `$translate(keys: string | string[], params?: any, lang?: string)`
-Translates a key or an array of keys. The syntax of the string is `key@@[default value]`
-
-- `$inlineTranslate(keys: string | string[], ctx: SpeakState, params?: any, lang?: string)`
-Translates a key or an array of keys outside the component$. 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
-- `useTranslate$()`
-Returns the translate functions as QRL
+### 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]`
-- `$plural(value: number | string, key?: string, params?: any, 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
-#### Localize
-- `formatDate(value: Date | number | string, options?: Intl.DateTimeFormatOptions, lang?: string, timeZone?: string)`
+### 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
-- `relativeTime(value: number | string, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions, lang?: string)`
+- `useRelativeTime: () => (value: number | string, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions, lang?: string)`
Formats a relative time using [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) API
-- `formatNumber(value: number | string, options?: Intl.NumberFormatOptions, lang?: string, currency?: string)`
+- `useFormatNumber: () => (value: number | string, options?: Intl.NumberFormatOptions, lang?: string, currency?: string)`
Formats a number using [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) API
-- `displayName(code: string, options: Intl.DisplayNamesOptions, lang?: string)`
+- `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
+
+- `validateLocale(lang: string)`
+Validate `language[-script][-region]`
+
+- `extractFromUrl(route: URL)`
+Extract prefix from url
+
+- `extractFromDomain(route: URL, domains: SpeakLocale[] | RewriteRouteOption[])`
+Extract lang/prefix from domain
+
+### Testing
+- `QwikSpeakMockProvider` component provides the Speak context to test enviroments
+
## Development Builds
### Library & tools
#### Build
```shell
cd packages/qwik-speak
+npm install
npm run build
```
#### Test
@@ -170,6 +225,7 @@ npm test
### Sample app
#### Run
```shell
+npm install
npm start
```
#### Preview
@@ -181,6 +237,10 @@ npm run preview
npm test
npm run test.e2e
```
+### Watch mode
+```shell
+npm run dev
+```
## License
MIT
diff --git a/SUMMARY.md b/SUMMARY.md
index d2890ac..2c36b55 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -2,12 +2,14 @@
## Library
-* [Quick Start](docs/quick-start.md)
-* [Tutorial: localized routing](docs/tutorial-routing.md)
-* [Translate](docs/translate.md)
-* [Translation functions](docs/translation-functions.md)
+* [Quick Start](docs/quick-start.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)
+* [Testing](docs/testing.md)
## Tools
diff --git a/adapters/static/vite.config.ts b/adapters/static/vite.config.ts
new file mode 100644
index 0000000..fa9bc3a
--- /dev/null
+++ b/adapters/static/vite.config.ts
@@ -0,0 +1,19 @@
+import { staticAdapter } from "@builder.io/qwik-city/adapters/static/vite";
+import { extendConfig } from "@builder.io/qwik-city/vite";
+import baseConfig from "../../vite.config";
+
+export default extendConfig(baseConfig, () => {
+ return {
+ build: {
+ ssr: true,
+ rollupOptions: {
+ input: ["@qwik-city-plan"],
+ },
+ },
+ plugins: [
+ staticAdapter({
+ origin: "http://127.0.0.1:8080",
+ }),
+ ],
+ };
+});
diff --git a/docs/adapters.md b/docs/adapters.md
index 09057d3..9139f83 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`])
@@ -15,17 +15,27 @@ const loadTranslation$: LoadTranslationFn = server$((lang: string, asset: string
```
## Static Site Generation (SSG)
-If you want to use Static Site Generation with the localized router, it is necessary to manage the dynamic language parameter, and you need to add the values it can take to the pages that will be pre-rendered:
+If you want to use Static Site Generation, you need to generate for each supported language an `index.html` of each page you will include in SSG.
+### Get the code ready
+- Bundle the translation files (see [Translation functions](./translation-functions.md)) or provide a running server during the build if you are fetching the files
+- Configure a localized router with a `lang` parameter
+- Handle the dynamic `lang` parameter, adding the values it can take to each page included in SSG, e.g.:
+
+_src/routes/[...lang]/index.tsx_
```typescript
-/**
- * Dynamic SSG route
- */
export const onStaticGenerate: StaticGenerateHandler = () => {
return {
params: config.supportedLocales.map(locale => {
- return { lang: locale.lang !== config.defaultLocale.lang ? locale.lang : '' };
+ return { lang: locale.lang !== config.defaultLocale.lang ? locale.lang : '.' };
})
};
};
```
+> See [Dynamic SSG routes](https://qwik.builder.io/docs/guides/static-site-generation/#dynamic-ssg-routes) in official Qwik docs for more details
+
+### Building
+```shell
+npm run build
+```
+Inspect the `dist` folder: you should have for each language an `index.html` of each page you have included in SSG.
diff --git a/docs/extract.md b/docs/extract.md
index c06e922..814086f 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:
@@ -49,6 +49,10 @@ Available options:
- `assetsPath` Path to translation files: `[basePath]/[assetsPath]/[lang]/*.json`. Default to `'i18n'`
- `format` The format of the translation files. Default to `'json'`
- `filename` Filename for not scoped translations. Default is `'app'`
+- `fallback` Optional function to implement a fallback strategy
+- `autoKeys` Automatically handle keys for each string. Default is false
+- `unusedKeys` Automatically remove unused keys from assets, except in runtime assets
+- `runtimeAssets` Comma-separated list of runtime assets to preserve
- `keySeparator` Separator of nested keys. Default is `'.'`
- `keyValueSeparator` Key-value separator. Default is `'@@'`
@@ -64,10 +68,54 @@ If you add new translations in the components, or a new language, they will be m
### Using it programmatically
Rather than using the command, you can invoke `qwikSpeakExtract` function:
-```typescript
+```javascript
import { qwikSpeakExtract } from 'qwik-speak/extract';
await qwikSpeakExtract({
supportedLangs: ['en-US', 'it-IT']
});
```
+
+### Translations fallback
+By default, the extract command uses the default value of translations as the initial value when provided.
+You can extend this behavior, to complete the missing translations by taking them from another language, by implementing a custom function with this signature:
+
+```typescript
+(translation: Translation) => Translation
+```
+and pass the function to `fallback` option:
+
+```typescript
+await qwikSpeakExtract({
+ supportedLangs: supportedLangs,
+ fallback: fallback
+});
+```
+For example, if you want to use the default language translations for missing values in other languages:
+```typescript
+import { qwikSpeakExtract, deepMergeMissing } from 'qwik-speak/extract';
+
+const defaultLang = 'en-US';
+const supportedLangs = ['en-US', 'it-IT', 'de-DE'];
+
+/**
+ * Fallback missing values to default lang
+ */
+const fallback = (translation) => {
+ const defaultTranslation = translation[defaultLang];
+ for (const lang of supportedLangs) {
+ if (lang !== defaultLang) {
+ deepMergeMissing(translation[lang], defaultTranslation);
+ }
+ }
+ return translation;
+};
+```
+
+### Automatic removal of unused keys
+To remove unused keys from json files, you need to enable the option and provide a comma-separated list of runtime file names:
+```json
+"scripts": {
+ "qwik-speak-extract": "qwik-speak-extract --supportedLangs=en-US,it-IT --unusedKeys=true --runtimeAssets=runtime"
+}
+```
\ No newline at end of file
diff --git a/docs/inline.md b/docs/inline.md
index 5b4f420..495c0e2 100644
--- a/docs/inline.md
+++ b/docs/inline.md
@@ -1,13 +1,13 @@
# Qwik Speak Inline Vite plugin
-> Inline Qwik Speak `$translate`, `$inlineTranslate` and `$plural` 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.
+On 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 it is required to inline the translations in chucks sent to the browser.
+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 _Qwik Speak Inline_ Vite plugin, translation happens _at compile-time_: `assets` are loaded and inlined in chunks sent to the browser during the build, and only `runtimeAssets` are loaded and translated on the client:
+`runtimeAssets` are always loaded at runtime, both on the server or on the client, allowing dynamic translations.
```mermaid
sequenceDiagram
@@ -20,25 +20,21 @@ sequenceDiagram
assets-->>Server: data
deactivate assets
Server->>Client: SSR: no serialize data
- Note over Client: inlined data
+ Note over Client: inlined data
Server->>runtimeAssets: loadTranslation$
activate runtimeAssets
- runtimeAssets-->>Server: runtime data
+ runtimeAssets-->>Server: data
deactivate runtimeAssets
- Server->>Client: SSR: serialize runtime data
- Client->>runtimeAssets: loadTranslation$ in SPA mode
- activate runtimeAssets
- runtimeAssets-->>Client: runtime data
- deactivate runtimeAssets
- Note over Client: $translate
+ Server->>Client: SSR: serialize data
+ Note over Client: runtime data
```
## Usage
-### Build using Qwik Speak Inline Vite plugin
-Translations are loaded only during SSR and inlined in chunks sent to the browser during the build.
-#### Get the code ready
+### Get the code ready
Qwik uses the `q:base` attribute to determine the base URL for loading the chunks in the browser, so you have to set it in `entry.ssr.tsx` file:
```typescript
+import { isDev } from '@builder.io/qwik/build';
+
export function extractBase({ serverData }: RenderOptions): string {
if (!isDev && serverData?.locale) {
return '/build/' + serverData.locale;
@@ -56,7 +52,9 @@ export default function (opts: RenderToStreamOptions) {
});
}
```
-#### Configure
+> Note. The value set through Qwik `locale()` in `plugin.ts` is saved by Qwik in `serverData.locale` directly. Make sure the locale is among the `supportedLocales`
+
+### Configure
Add `qwikSpeakInline` Vite plugin in `vite.config.ts`:
```typescript
import { qwikSpeakInline } from 'qwik-speak/inline';
@@ -76,7 +74,20 @@ export default defineConfig(() => {
};
});
```
-and build the app:
+Available options:
+- `supportedLangs` Supported langs. Required
+- `defaultLang` Default lang. Required
+- `basePath` The base path. Default to `'./'`
+- `assetsPath` Path to translation files: `[basePath]/[assetsPath]/[lang]/*.json`. Default to `'i18n'`
+- `outDir` The build output directory. Default to `'dist'`
+- `loadAssets` Optional function to load asset by lang
+- `autoKeys` Automatically handle keys for each string. Default is false
+- `keySeparator` Separator of nested keys. Default is `'.'`
+- `keyValueSeparator` Key-value separator. Default is `'@@'`
+
+> Note. Currently, only `json` is supported as format
+
+Now build the app:
```shell
npm run preview
```
@@ -93,32 +104,37 @@ Each contains only its own translation:
_dist/build/en-US/q-*.js_
```javascript
-/* @__PURE__ */ pr("h2", null, null, `Translate your Qwik apps into any language`, 1, null)
+/* @__PURE__ */ Nr("h2", null, null, `Translate your Qwik apps into any language`, 1, null)
```
_dist/build/it-IT/q-*.js_
```javascript
-/* @__PURE__ */ pr("h2", null, null, `Traduci le tue app Qwik in qualsiasi lingua`, 1, null)
+/* @__PURE__ */ Nr("h2", null, null, `Traduci le tue app Qwik in qualsiasi lingua`, 1, null)
```
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
+- Translations with dynamic keys or params
-> Note. Currently, only `json` files are supported as assets
+### Load assets
+If you need to load translation data from an external source, such as a db, you can implement a custom function with this signature:
-### Build using Qwik Speak Inline Vite plugin & runtime
-When there are translations with dynamic keys or params, you can manage them in 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
- ]
+(lang: string) => Promise
+```
+For example:
+```typescript
+export const loadAssets = async (lang: string) => {
+ const response = await fetch('https://...');
+ return response.json();
};
```
-Likewise, you can also create scoped runtime files for different pages and pass them to `Speak` components.
+and pass the function to `loadAssets` option:
+
+```typescript
+qwikSpeakInline({
+ supportedLangs: ['en-US', 'it-IT'],
+ defaultLang: 'en-US',
+ loadAssets: loadAssets
+}),
+```
+The function will be called during the build for each supported language, and must return all translations for that language.
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 8c0d928..dfdec83 100644
--- a/docs/quick-start.md
+++ b/docs/quick-start.md
@@ -1,12 +1,34 @@
# Quick Start
-```shell
-npm create qwik@latest
+> Setup an app with Qwik Speak
+
+```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
@@ -18,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'
]
};
```
@@ -30,7 +57,7 @@ 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
+ * Assets names and keys must be valid variable names
*/
const translationData = import.meta.glob('/i18n/**/*.json');
@@ -45,197 +72,76 @@ 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.
+> `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:
+For more details, see [Translation functions](./translation-functions.md)
-_src/root.tsx_
-```tsx
-import { QwikSpeakProvider } from 'qwik-speak';
-export default component$(() => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-```
+Add `useQwikSpeak` provider in `root.tsx` and pass it the configuration and the translation functions:
-Finally we add an `index.tsx` with some translation:
-
-_src/routes/index.tsx_
+_src/root.tsx_
```tsx
-import {
- $translate as t,
- formatDate as fd,
- formatNumber as fn,
- Speak,
-} from 'qwik-speak';
-
-export const Home = component$(() => {
- return (
- <>
-
- >
- );
-});
+import { useQwikSpeak } from 'qwik-speak';
+import { config } from "./speak-config";
+import { translationFn } from "./speak-functions";
export default component$(() => {
+ /**
+ * Init Qwik Speak
+ */
+ useQwikSpeak({ config, translationFn });
+
return (
- /**
- * Add Home translations (only available in child components)
- */
-
-
-
+
+ {/* ... */}
+
);
});
```
-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
-
## 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';
+
+/**
+ * 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 = ({ 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
+ lang = config.supportedLocales.find(value => value.lang === lang)?.lang || config.defaultLocale.lang;
+
// Set Qwik locale
- locale(lang || config.defaultLocale.lang);
+ locale(lang);
};
```
-Internally, Qwik Speak will try to take the Qwik `locale`, before falling back to default locale if it is not in `supportedLocales`.
-
-## Change locale
-Now we want to change locale. Let's create a `ChangeLocale` component:
-
-_src/components/change-locale.tsx_
-```tsx
-import { $translate as t, useSpeakConfig, SpeakLocale } from 'qwik-speak';
+> 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
-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 (
-
- );
-});
-```
-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: [Qwik Speak Extract](./extract.md)
-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
-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:
-```shell
-npm start
-```
-
-## Production: [Qwik Speak Inline Vite plugin](./inline.md)
-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.
+ * The value set through Qwik 'locale()' in 'plugin.ts' is saved by Qwik in 'serverData.locale' directly.
+ * Make sure the locale is among the 'supportedLocales'
+ */
export function extractBase({ serverData }: RenderOptions): string {
if (!isDev && serverData?.locale) {
return '/build/' + serverData.locale;
@@ -250,13 +156,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
-```
-
-> 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
+## 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 191ccb9..99380aa 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -2,40 +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 {
- $translate as t,
- formatDate as fd,
- formatNumber as fn,
- Speak,
-} from 'qwik-speak';
+import { inlineTranslate, useFormatDate, useFormatNumber } from 'qwik-speak';
+
+export default component$(() => {
+ const t = inlineTranslate();
-export const Home = component$(() => {
return (
<>
>
);
});
-
-export default component$(() => {
- return (
-
-
-
- );
-});
```
We'll have the following unit test (using _Vitest_):
@@ -48,37 +31,23 @@ test(`[Home Component]: Should render the component`, async () => {
const { screen, render } = await createDOM();
await render(
-
+
-
+
);
expect(screen.outerHTML).toContain('Qwik Speak demo');
});
```
-Optionally, if you need to test the translated texts in different languages, you have to provide a stub `loadTranslation$` to ensure translations are loaded in test environment. For example you could load the json files of translations as follows:
-```typescript
-const loadTranslationStub$: LoadTranslationFn = $((lang: string, asset: string) =>
- JSON.parse(
- import.meta.glob('/public/i18n/**/*.json', { as: 'raw', eager: true })[
- `/public/i18n/${lang}/${asset}.json`
- ]
- )
-);
-
-const translationFnStub: TranslationFn = {
- loadTranslation$: loadTranslationStub$
-};
-```
-and pass it with the language you want to `QwikSpeakProvider`:
+Optionally, if you need to test the translated texts in different languages, you have to provide `loadTranslation$` to ensure translations are loaded in test environment, and the locale to use:
```tsx
test(`[Home Component]: Should render translated texts in Italian`, async () => {
const { screen, render } = await createDOM();
await render(
-
+
);
diff --git a/docs/translate.md b/docs/translate.md
index 9798e59..ef7e919 100644
--- a/docs/translate.md
+++ b/docs/translate.md
@@ -1,32 +1,22 @@
# Translate
-> All translation functions, except `$inlineTranslate`, use the Speak context internally: they must be used within the Qwik components
+> The return functions of `inlineTranslate` and `inlinePlural` are parsed and replaced with translated texts in chunks sent to the browser at compile time
-## $translate
-The `$translate` function is responsible for translating, extracting to external files, and inlining during the build, using key-value pairs:
+## inlineTranslate
+`inlineTranslate` returns a functions to get the translation using key-value pairs:
```tsx
-$translate('home.title@@Qwik Speak')
-```
-- In dev mode, the function returns the default value (value after `@@`) if provided
-- When extracting translations, it creates scoped translation files:
+const t = inlineTranslate();
- _home.json_
- ```json
- {
- "home": {
- "title": "Qwik Speak"
- }
- }
- ```
-- After extraction, it returns the value in files
-- In prod mod, using _Qwik Speak Inline_ Vite plugin, `$translate` function is replaced by its translation in chunks sent to the browser:
- ```tsx
- `Qwik Speak`
- ```
+t('title@@Qwik Speak')
+```
+Value after `@@` is the optional default value:
+```tsx
+`Qwik Speak`
+```
### Params interpolation
-`$translate` function accept params as well:
+`t` function accept params as well:
```tsx
-$translate('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
@@ -34,9 +24,9 @@ Hi! I am Qwik Speak
```
### Array of keys
-`$translate` function accepts array of keys:
+`t` function accepts array of keys:
```tsx
-$translate(['value1@@Value 1', 'value2@@Value 2'])
+t(['value1@@Value 1', 'value2@@Value 2'])
```
and returns an array of translated values:
```tsx
@@ -44,19 +34,17 @@ and returns an array of translated values:
```
### Arrays and objects as values
-It can get arrays and objects directly from files:
+`t` function can get arrays and objects directly from files:
```json
{
- "home": {
- "array": [
- "one",
- "two",
- "three"
- ],
- "obj": {
- "one": "1",
- "two": "2"
- }
+ "array": [
+ "one",
+ "two",
+ "three"
+ ],
+ "obj": {
+ "one": "one",
+ "two": "two"
}
}
```
@@ -64,64 +52,58 @@ just pass to the function the type parameter:
```tsx
import type { Translation } from 'qwik-speak';
-$translate('home.array')
-$translate('home.obj')
+t('array')
+t('obj')
```
-Finally, it is possible to set arrays and objects passing a _valid stringified_ default value:
+You can also access by array position:
```tsx
-$translate('home.array@@["one","two","three"]')
-$translate('home.obj@@{"one":"1","two":"2"}')
+t('array.2@@three')
```
-You can also access by array position:
+Finally, it is possible to set arrays and objects passing a _valid stringified_ default value:
```tsx
-$translate('home.array.2@@three')
+t('array@@["one","two","three"]')
+t('obj@@{"one":"one","two":"two"}')
```
-> To reduce complexity (arrays and objects are _inlined_ during build) it is recommended to use objects with _a depth not greater than 1_. For the same reason, `params` interpolation is not supported when you return an array or an object
-## $inlineTranslate
-The `$inlineTranslate` function has the same behavior as `$translate`, but can be used outside the `component$`, for example in _Inline components_, passing the Speak context as second argument:
+### Html in translations
+You can have Html in translations, like:
+```json
+{
+ "description": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps"
+}
+```
+but you have to use `dangerouslySetInnerHTML`:
```tsx
-import { $inlineTranslate, useSpeakContext } from 'qwik-speak';
-
-export const MyComponent = (props: { ctx: SpeakState }) => {
- return
;
-};
-
-export const Home = component$(() => {
- const ctx = useSpeakContext();
-
- return (
- <>
-
- >
- );
-});
+
```
+> On the client the text is _inlined_ during build, so there are no XSS risks
-## $plural
-The `$plural` function uses the [Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) API:
+## 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
-p(1, 'home.devs')
+const p = inlinePlural();
+
+p(1, '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 the `$plural` function, so you must add the translation in each language, keeping in mind that the counter is optionally interpolated with the `value` parameter:
+It is possible to set the default value passing a _valid stringified_ json, keeping in mind that the counter is optionally interpolated with the `value` parameter:
+```tsx
+p(1, 'devs@@{"one": "{{ value }} software developer","other": "{{ value }} software developers"}')
+```
+Will result in:
```json
{
- "home": {
- "devs": {
- "one": "{{ value }} software developer",
- "other": "{{ value }} software developers"
- }
+ "devs": {
+ "one": "{{ value }} software developer",
+ "other": "{{ value }} software developers"
}
}
```
@@ -130,31 +112,115 @@ It is rendered as:
1 software developer
```
-## useTranslate$
-The `useTranslate$` closure returns the translate function as QRL. This means that it is serializable and it can be passed to other QRL functions characterized by lazy loading:
+## Runtime translation
+When you use a translation like this:
```tsx
-import { useTranslate$ } from 'qwik-speak';
+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:
-const MyComponent = component$(() => {
- const t$ = useTranslate$();
+_i18n/[lang]/runtime.json_
+```json
+{
+ "dynamic": "I'm a dynamic value"
+}
+```
+and add the `runtime` file to `runtimeAssets` in configuration or `useSpeak` provider.
+## QRL functions and lifecycle hooks
+QRL functions `$` and lifecycle hooks like `useTask$` create js chunks that will be lazy loaded. So you need to re-invoke `inlineTranslate` inside them:
+```tsx
+const fn$ = $(() => {
+ const t = inlineTranslate();
+ console.log(t('title@@Qwik Speak'));
+});
+```
+
+## 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.
+
+## Automatic key generation
+If you don't want to handle the keys inside the translation functions, but only the default values, you can enable automatic key generation:
+- Extraction tool: add `--autoKeys=true` to the script
+- Inline Vite plugin: add `autoKeys: true` to the options
+
+> Note. You can enable this option, even if you use the syntax `key@@[default value]`.
+
+If you enable this option, you can pass only the default values to the translation functions:
+```tsx
+export default component$(() => {
+ const t = inlineTranslate();
+ const p = inlinePlural();
+
return (
-
+ <>
+
{p(
+ 1,
+ '{"one": "{{ value }} {{ color }} zebra","other": "{{ value }} {{ color }} zebras"}',
+ {
+ color: t('black and white')
+ }
+ )}
+ >
);
});
```
-> The translation keys passed into its must be provided in `runtimeAssets` and will not be inlined
-
+If you run the extractor, you will get json files like this:
+```json
+{
+ "app": {
+ "title": "Qwik Speak demo"
+ },
+ "autoKey_3c909eb27a10640be9495cff142f601c": {
+ "one": "{{ value }} {{ color }} zebra",
+ "other": "{{ value }} {{ color }} zebras"
+ },
+ "autoKey_8e4c0598319b3b04541df2fc36cb6fc5": "New strings without existing keys",
+ "autoKey_cbe370e60f10f92d4dd8b3e9c267b1fa": "black and white"
+}
+```
+Then the Inline plugin will manage the self-assigned keys.
# Localize
-> All localization functions use the Speak context internally: they must be used within the Qwik components
-
-## formatDate
-The `formatDate` function uses the [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) API:
+## 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:
```tsx
-formatDate(Date.now(), { dateStyle: 'full', timeStyle: 'short' })
+const fd = useFormatDate();
+
+fd(Date.now(), { dateStyle: 'full', timeStyle: 'short' })
```
The second param in the signature is an Intl `DateTimeFormatOptions` object, which allows you to customize the format:
```text
@@ -162,21 +228,24 @@ Monday, March 6, 2023 at 12:20 PM
```
Optionally it uses the time zone set in `timeZone` property of the `SpeakLocale`.
-## relativeTime
-The `relativeTime` function uses the [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) API:
+## useRelativeTime
+`useRelativeTime` returns a functions that uses [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) API to format relative times:
```tsx
-relativeTime(-1, 'second')
+const rt = useRelativeTime();
+
+rt(-1, 'second')
```
The second param in the signature is an Intl `RelativeTimeFormatUnit` string:
```text
1 second ago
```
-
-## formatNumber
-The `formatNumber` function uses the [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) API:
+## useFormatNumber
+`useFormatNumber` returns a functions that uses [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) API to format numbers:
```tsx
-formatNumber(1000000)
+const fn = useFormatNumber();
+
+fn(1000000)
```
```text
1,000,000
@@ -185,7 +254,7 @@ formatNumber(1000000)
### Currency
To format as currency, you have to set the `style` property of the second param, an Intl `NumberFormatOptions` object:
```tsx
-formatNumber(1000000, { style: 'currency' })
+fn(1000000, { style: 'currency' })
```
```text
$1,000,000.00
@@ -198,7 +267,7 @@ To format as unit, you have to set the `style` and `unit` properties of the seco
const locale = useSpeakLocale();
const units = locale.units!;
-formatNumber(1, { style: 'unit', unit: units['length'] })
+fn(1, { style: 'unit', unit: units['length'] })
```
```text
1 mi
@@ -208,26 +277,28 @@ It uses the unit set in optional `units` property of the `SpeakLocale`:
units: { 'length': 'mile' }
```
-## displayName
-The `displayName` function uses the [Intl.DisplayNames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames) API:
+## useDisplayName
+`useDisplayName` returns a functions that uses [Intl.DisplayNames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames) API to translate language, region, script or currency display names:
```tsx
-displayName('en-US', { type: 'language' })
+const dn = useDisplayName();
+
+dn('en-US', { type: 'language' })
```
```text
American English
```
-> The locale used by `formatDate`, `relativeTime`, `formatNumber` and `displayName` is primarily the `extension` property of the `SpeakLocale` if provided, otherwise the `lang` property. `extension` is the language with Intl extensions, in the format `language[-script][-region][-extensions]` like `en-US-u-ca-gregory-nu-latn`
+> The locale used by `useFormatDate`, `useRelativeTime`, `useFormatNumber` and `useDisplayName` is primarily the `extension` property of the `SpeakLocale` if provided, otherwise the `lang` property. `extension` is the language with Intl extensions, in the format `language[-script][-region][-extensions]` like `en-US-u-ca-gregory-nu-latn`
# Multilingual
Each of the translation and localization functions accepts a different language other than the current one as its last argument:
```tsx
-$translate('home.title@@Qwik Speak', undefined, 'it-IT')
+const t = inlineTranslate();
+
+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/translation-functions.md b/docs/translation-functions.md
index 6334bfa..73114b0 100644
--- a/docs/translation-functions.md
+++ b/docs/translation-functions.md
@@ -3,31 +3,7 @@
> Note. It is recommended to put these functions in a separate file from the configuration, to allow the Qwik compiler to respect the initialization order of the functions
## loadTranslation$
-`loadTranslation$` is the core function of the library. It is a customizable function, with which you can load the translation files in the way you prefer.
-
-### Fetching files
-A simple implementation is to fetch files from the public folder:
-
-```typescript
-export const loadTranslation$: LoadTranslationFn = $(async (lang: string, asset: string, origin?: string) => {
- let url = '';
- // Absolute urls on server
- if (isServer && origin) {
- url = origin;
- }
- url += `/public/i18n/${lang}/${asset}.json`;
- const response = await fetch(url);
-
- if (response.ok) {
- return response.json();
- }
- else {
- console.error(`loadTranslation$: ${url}`, response);
- }
-});
-```
-
-> The function must be able to load files to both the server and the client
+`loadTranslation$` is the core function of the library. It is a customizable QRL function, with which you can load the translation files in the way you prefer.
### Bundling files
#### Dynamic import
@@ -35,7 +11,7 @@ To improve performance, it is recommended to bundle translation files instead of
```typescript
/**
* Translation files are lazy-loaded via dynamic import and will be split into separate chunks during build.
- * Keys must be valid variable names
+ * Assets names and keys must be valid variable names
*/
const translationData = import.meta.glob('/i18n/**/*.json');
@@ -70,4 +46,24 @@ const loadTranslation$: LoadTranslationFn = server$((lang: string, asset: string
JSON.parse(translationData[`/i18n/${lang}/${asset}.json`])
);
```
-Refer to _Vite_ documentation for more information on [Glob import](https://vitejs.dev/guide/features.html#glob-import)
\ No newline at end of file
+Refer to _Vite_ documentation for more information on [Glob import](https://vitejs.dev/guide/features.html#glob-import)
+
+
+### Fetching files
+You can fetch files from the _public_ folder:
+
+```typescript
+const loadTranslation$ = server$(async function (lang: string, asset: string) {
+ // Absolute urls on server
+ const url = `${this.url.origin}/i18n/${lang}/${asset}.json`;
+ const response = await fetch(url);
+
+ if (response.ok) {
+ return response.json();
+ }
+ else {
+ console.error(`loadTranslation$: ${url}`, response);
+ }
+});
+```
+or from a db: since the `server$` function is always executed on server, it is safe to use secrets as well.
diff --git a/docs/tutorial-routing-rewrite.md b/docs/tutorial-routing-rewrite.md
new file mode 100644
index 0000000..f6e6fdf
--- /dev/null
+++ b/docs/tutorial-routing-rewrite.md
@@ -0,0 +1,468 @@
+# 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
+
+## 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/`
+
+Or:
+- default language (en-US): routes not localized `http://127.0.0.1:4173/page`
+- other languages (it-IT): localized routes `http://127.0.0.1:4173/it-IT/pagina`
+
+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`
+
+Now let's handle it. Create `speak-routes.ts` file in `src`:
+
+_src/speak-routes.ts_
+```typescript
+import type { RewriteRouteOption } from 'qwik-speak';
+
+/**
+ * Translation paths
+ */
+export const rewriteRoutes: RewriteRouteOption[] = [
+ // No prefix/paths for default locale
+ {
+ prefix: 'it-IT',
+ paths: {
+ 'page': 'pagina'
+ }
+ }
+];
+```
+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 }),
+ qwikVite(),
+ qwikSpeakInline({
+ supportedLangs: ['en-US', 'it-IT'],
+ defaultLang: 'en-US',
+ assetsPath: 'i18n'
+ }),
+ tsconfigPaths(),
+ ],
+ };
+});
+```
+Add `rewriteRoutes` to `speak-config.ts` in `src`:
+
+_src/speak-config.ts_
+```typescript
+import type { SpeakConfig } from 'qwik-speak';
+
+import { rewriteRoutes } from './speak-routes';
+
+export const config: SpeakConfig = {
+ rewriteRoutes,
+ 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' }
+ ],
+ // Translations available in the whole app
+ assets: [
+ 'app'
+ ],
+ // Translations with dynamic keys available in the whole app
+ runtimeAssets: [
+ 'runtime'
+ ]
+};
+```
+Update `plugin.ts` in the root of the `src/routes` directory:
+
+_src/routes/plugin.ts_
+```typescript
+import type { RequestHandler } from "@builder.io/qwik-city";
+import { extractFromUrl, 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 = ({ locale, url }) => {
+ let lang: string | undefined = undefined;
+
+ const prefix = extractFromUrl(url);
+
+ if (prefix && validateLocale(prefix)) {
+ // Check supported locales
+ lang = config.supportedLocales.find(value => value.lang === prefix)?.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`:
+```typescript
+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 = translatePath();
+ // 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//index.tsx_
+```tsx
+import { inlineTranslate, useFormatDate, useFormatNumber } from 'qwik-speak';
+
+export default component$(() => {
+ const t = inlineTranslate();
+
+ const fd = useFormatDate();
+ const fn = useFormatNumber();
+
+ return (
+ <>
+
+ >
+ );
+});
+```
+> 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_
+```tsx
+import { useLocation } from '@builder.io/qwik-city';
+import { useSpeakLocale, useSpeakConfig, useDisplayName, inlineTranslate, translatePath } 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 = translatePath();
+
+ 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') }],
+ };
};
```
-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`.
+Add a `page/index.tsx` to try the router:
-We are also providing default values for each translation: `key@@[default value]`.
+_src/routes/[...lang]/page/index.tsx_
+```tsx
+import { inlineTranslate } from 'qwik-speak';
-> `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
+export default component$(() => {
+ const t = inlineTranslate();
-## 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`:
+ const key = 'dynamic';
-_src/components/router-head/router-head.tsx_
-```tsx
-{t(head.title)}
+ return (
+ <>
+
{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
-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 { useLocation } from '@builder.io/qwik-city';
+import { useSpeakLocale, useSpeakConfig, useDisplayName, inlineTranslate, localizePath } from 'qwik-speak';
+
export const ChangeLocale = component$(() => {
- const loc = useLocation();
+ const t = inlineTranslate();
+ const pathname = useLocation().url.pathname;
+
+ const locale = useSpeakLocale();
const config = useSpeakConfig();
+ const dn = useDisplayName();
- // 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();
- });
+ const getPath = localizePath();
return (
-