Skip to content

Commit bd6c9b3

Browse files
committed
feat: add compiled-i18n feature
1 parent 40ae7fe commit bd6c9b3

File tree

12 files changed

+199
-305
lines changed

12 files changed

+199
-305
lines changed

.changeset/stupid-spoons-strive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik': patch
3+
---
4+
5+
Feat: `qwik add compiled-i18` now adds easy i18n to your app.

packages/docs/src/routes/docs/integrations/i18n/index.mdx

Lines changed: 89 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ contributors:
77
- tzdesign
88
- Benny-Nottonson
99
- mrhoodz
10-
updated_at: '2023-10-08T10:16:44Z'
10+
- wmertens
11+
updated_at: '2025-11-23T00:00:00Z'
1112
created_at: '2023-04-19T22:13:46Z'
1213
---
1314

@@ -71,117 +72,114 @@ Our recommendation is to use a tool that would provide a runtime approach on the
7172

7273
## Internationalization Libraries
7374

74-
### $localize
75+
### Paraglide JS
7576

76-
[$localize](https://angular.io/api/localize/init/$localize) the translation system is based on the $localize system from [Angular](https://angular.io/). The translations can be extracted in xmb, xlf, xlif, xliff, xlf2, xlif2, xliff2, and json formats.
77+
[Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) is a compile-time translation library that generates type-safe translation functions from standard message format messages.
7778

78-
>NOTE: The $localize system is a compile-time translation system and is completely removed from the final output. $localize is a sub-project of Angular, and including its usage does not mean that Angular is used for rendering of applications.
79+
#### Advantages
80+
- **Tiny Runtime Overhead**: Translations are compiled to trivial functions.
81+
- **Framework Agnostic**: Can be used with various frameworks, even within the same project.
82+
- **Type-Safe Translations**: Generates type-safe functions for translations, reducing runtime errors
83+
- **Lazy Loading**: Only the translations used by the application are bundled, optimizing bundle size.
84+
- **Standard Message Format**: Supports complex message formatting using the widely adopted ICU message format.
85+
- **Developed ecosystem**: Part of the inlang ecosystem, which provides additional tools and integrations for managing translations.
86+
#### Disadvantages
87+
- **Ships all languages**: The translations for all languages are included in the single build output, which may increase the overall bundle size.
88+
- **No runtime features**: Lacks the possibility to add messages at runtime.
7989

80-
The easiest way to add $localize to Qwik is using the Qwik CLI command. This will install the required dependencies and create a new public route `/src/routes/[locale]` showcasing i18n $localize integration.
90+
#### Installation
91+
The easiest way to add Paraglide JS to Qwik is by following the [official guide for Vite](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/vite).
8192

82-
<PackageManagerTabs>
83-
<span q:slot="pnpm">
84-
```shell
85-
pnpm run qwik add localize
86-
```
87-
</span>
88-
<span q:slot="npm">
89-
```shell
90-
npm run qwik add localize
91-
```
92-
</span>
93-
<span q:slot="yarn">
94-
```shell
95-
yarn run qwik add localize
96-
```
97-
</span>
98-
<span q:slot="bun">
99-
```shell
100-
bun run qwik add localize
93+
For further features like language switching and automatic translations, check the [documentation](https://inlang.com/m/gerre34r/library-inlang-paraglideJs).
94+
95+
#### Usage
96+
Use the generated functions in your code:
97+
98+
```tsx
99+
import * as m from './src/paraglide/messages'
100+
import { component$ } from '@builder.io/qwik';
101+
102+
export default component$(() => {
103+
return <p>{m.hello({name: 'World'})}</p>
104+
})
101105
```
102-
</span>
103-
</PackageManagerTabs>
104106

105-
For further reference, please check this [example repo](https://github.com/mhevery/qwik-i18n).
107+
### compiled-i18n
106108

107-
#### Extract translations
109+
[compiled-i18n](https://github.com/wmertens/compiled-i18n) is inspired by the $localize system from Angular. It only requires a plugin to be added to the Vite configuration.
108110

109-
When you are done with your changes, you can use the `i18n-extract` command to extract the translations from the code.
110-
This will update the file you see in `package.json`.
111+
It supports both runtime and compile-time translations.
111112

112-
<PackageManagerTabs>
113-
<span q:slot="pnpm">
114-
```shell
115-
pnpm run i18n-extract
116-
```
117-
</span>
118-
<span q:slot="npm">
119-
```shell
120-
npm run i18n-extract
121-
```
122-
</span>
123-
<span q:slot="yarn">
124-
```shell
125-
yarn run i18n-extract
126-
```
127-
</span>
128-
<span q:slot="bun">
129-
```shell
130-
bun run i18n-extract
131-
```
132-
</span>
133-
</PackageManagerTabs>
113+
#### Advantages
114+
- **Zero Runtime Overhead (Compile-time mode)**: Translations are inlined at build time, resulting in no runtime cost for lookups.
115+
- **Simplicity**: Minimal setup. Simple template string function API.
116+
- **Framework Agnostic**: Can be used with various frameworks, even within the same project.
117+
- **Flexible Approach**: Supports both runtime and compile-time modes, allowing developers to choose based on their needs.
118+
- **Per-locale builds**: Compile-time mode inlines translations, eliminating runtime lookups. Server code includes all languages, simplifying deployment.
119+
- **Automatic extraction**: Automatically adds new keys to translation files during build, and warns about missing and/or unused translations.
134120

135-
#### Auto translations for $localize with deepl
121+
#### Disadvantages
122+
- **Per-locale builds**: Compile-time mode requires separate builds for each locale, complicating deployment.
136123

137-
For auto translations, you can use the [deepl-localize](https://www.npmjs.com/package/deepl-localize) package. It will automatically translate your strings using the [deepl.com](https://deepl.com/) API.
124+
#### Installation
125+
Run `npx qwik add compiled-i18n` to add compiled-i18n to your Qwik app.
138126

139-
Use the `deepl-localize` command to translate your strings with:
127+
See the [Qwik-specific instructions](https://github.com/wmertens/compiled-i18n/blob/main/docs/qwik.md) for more details.
140128

141-
<PackageManagerTabs>
142-
<span q:slot="pnpm">
143-
```shell
144-
pnpm dlx deepl-localize translate -b src/locales/message.en.json -l de-DE fr-FR -a "YOUR-DEEPL-API-KEY"
145-
```
146-
</span>
147-
<span q:slot="npm">
148-
```shell
149-
npx deepl-localize translate -b src/locales/message.en.json -l de-DE fr-FR -a "YOUR-DEEPL-API-KEY"
150-
```
151-
</span>
152-
<span q:slot="yarn">
153-
```shell
154-
yarn dlx deepl-localize translate -b src/locales/message.en.json -l de-DE fr-FR -a "YOUR-DEEPL-API-KEY"
155-
```
156-
</span>
157-
<span q:slot="bun">
158-
```shell
159-
bunx deepl-localize translate -b src/locales/message.en.json -l de-DE fr-FR -a "YOUR-DEEPL-API-KEY"
160-
```
161-
</span>
162-
</PackageManagerTabs>
163-
164-
Alternatively, you can use the `deepl-localize` command to translate your strings within your script section:
165-
```json
166-
{
167-
"scripts":{
168-
"translate":"deepl-localize translate -b src/locales/message.en.json -l de-DE fr-FR -a 'your-deepl-api-key'"
169-
}
170-
}
171-
```
129+
Automatic translation is supported via [deepl-localize](https://github.com/tzdesign/deepl-localize).
172130

173-
### compiled-i18n
131+
For further explanation of the API and features like pluralization, check the [documentation](https://github.com/wmertens/compiled-i18n/blob/main/Readme.md).
174132

175-
[compiled-i18n](https://github.com/wmertens/compiled-i18n) is inspired by the $localize system from Angular. It only requires a plugin to be added to the Vite configuration.
133+
#### Usage
134+
Use the template string function anywhere in your code:
176135

177-
It supports both runtime and compile-time translations.
136+
```tsx
137+
import {_} from 'compiled-i18n'
178138

179-
See the [Qwik-specific instructions](https://github.com/wmertens/compiled-i18n/blob/main/docs/qwik.md).
139+
console.log(_`Logenv ${process.env.NODE_ENV}`)
180140

181-
Automatic translation is supported via [deepl-localize](https://github.com/tzdesign/deepl-localize).
141+
export const Count = ({count}) => (
142+
<div title={_`countTitle`}>{_`${count} items`}</div>
143+
)
144+
```
182145

183146
### qwik-speak
184147

185148
[qwik-speak](https://github.com/robisim74/qwik-speak) library to translate texts, dates and numbers in Qwik apps.
186149

150+
#### Advantages
151+
- **Runtime Flexibility**: Allows dynamic language switching without page reloads, improving user experience.
152+
- **Qwik-Optimized**: Deep integration with Qwik's reactivity system and serialization.
153+
- **Rich Features**: Supports advanced formatting including dates, numbers, Pluralization, and ICU message format.
154+
155+
#### Disadvantages
156+
- **Runtime Overhead**: Requires loading translation maps at runtime, which can increase bundle size and initial load time.
157+
- **Limited Compiler Benefits**: Doesn't provide the same compile-time optimizations and tree-shaking as pure compile-time solutions.
158+
159+
#### Installation
160+
187161
The easiest way to add qwik-speak to Qwik is following the official [guide](https://github.com/robisim74/qwik-speak/blob/main/docs/quick-start.md).
162+
163+
#### Usage
164+
165+
```tsx
166+
import { component$, useStore } from '@builder.io/qwik';
167+
import { Speak, useSpeakContext } from 'qwik-speak';
168+
169+
export default component$(() => {
170+
const speak = useSpeakContext();
171+
const state = useStore({ count: 0 });
172+
173+
return (
174+
<Speak>
175+
<div>
176+
<h1>{speak.t('helloWorld')}</h1>
177+
<p>{speak.t('itemCount', { count: state.count })}</p>
178+
<button onClick$={() => state.count++}>
179+
{speak.t('increment')}
180+
</button>
181+
</div>
182+
</Speak>
183+
);
184+
});
185+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"description": "Add compiled-i18n to your Qwik app",
3+
"__qwik__": {
4+
"displayName": "Integration: compiled-i18n (compile time translations)",
5+
"priority": -10,
6+
"viteConfig": {
7+
"imports": [
8+
{
9+
"namedImports": [
10+
"i18nPlugin"
11+
],
12+
"importPath": "compiled-i18n/vite"
13+
}
14+
],
15+
"vitePlugins": [
16+
"i18nPlugin({ locales: ['en'] })"
17+
]
18+
},
19+
"docs": [
20+
"https://qwik.dev/docs/integrations/i18n/#localize",
21+
"https://github.com/wmertens/compiled-i18n/blob/main/Readme.md"
22+
],
23+
"nextSteps": {
24+
"title": "Next Steps",
25+
"lines": [
26+
" - Change the `locales` array in the vite config to add more languages",
27+
" - Use the `src/components/locale-selector/locale-selector.tsx` component to switch languages",
28+
"",
29+
" Check out the compiled-i18n docs:",
30+
" - https://github.com/wmertens/compiled-i18n/blob/main/Readme.md"
31+
]
32+
}
33+
},
34+
"devDependencies": {
35+
"compiled-i18n": "latest"
36+
}
37+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { component$, getLocale } from "@builder.io/qwik";
2+
import { _, locales } from "compiled-i18n";
3+
4+
export const LocaleSelector = component$(() => {
5+
const currentLocale = getLocale();
6+
return (
7+
<>
8+
{locales.map((locale) => {
9+
const isCurrent = locale === currentLocale;
10+
return (
11+
// Note, you must use `<a>` and not `<Link>` so the page reloads
12+
<a
13+
key={locale}
14+
// When using route-based locale selection, build the URL here
15+
href={`?locale=${locale}`}
16+
aria-disabled={isCurrent}
17+
class={
18+
"btn btn-ghost btn-sm" +
19+
(isCurrent
20+
? " bg-neutralContent text-neutral pointer-events-none"
21+
: " bg-base-100 text-base-content")
22+
}
23+
>
24+
{locale}
25+
</a>
26+
);
27+
})}
28+
</>
29+
);
30+
});

starters/features/localize/src/entry.ssr.tsx renamed to starters/features/compiled-i18n/src/entry.ssr.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,24 @@ import {
1414
renderToStream,
1515
type RenderToStreamOptions,
1616
} from "@builder.io/qwik/server";
17+
import { extractBase, setSsrLocaleGetter } from "compiled-i18n/qwik";
1718
import Root from "./root";
18-
import { extractBase } from "./routes/[locale]/i18n-utils";
19+
20+
setSsrLocaleGetter();
1921

2022
export default function (opts: RenderToStreamOptions) {
2123
return renderToStream(<Root />, {
2224
...opts,
23-
base: extractBase, // determine the base URL for the client code
25+
26+
base: extractBase,
27+
2428
// Use container attributes to set attributes on the html tag.
2529
containerAttributes: {
26-
lang: opts.serverData?.locale ?? "en-us",
30+
lang: opts.serverData!.locale,
2731
...opts.containerAttributes,
2832
},
33+
serverData: {
34+
...opts.serverData,
35+
},
2936
});
3037
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// ... other imports
2+
import { guessLocale } from "compiled-i18n";
3+
import type { RequestHandler } from "@builder.io/qwik-city";
4+
5+
/**
6+
* Handle incoming requests to determine and set the appropriate locale.
7+
* This function checks for a 'locale' query parameter, then a `locale` cookie,
8+
* and finally falls back to the 'Accept-Language' header.
9+
*/
10+
export const onRequest: RequestHandler = async ({
11+
query,
12+
cookie,
13+
headers,
14+
locale,
15+
}) => {
16+
// Allow overriding locale with query param `locale`
17+
if (query.has("locale")) {
18+
const newLocale = guessLocale(query.get("locale"));
19+
cookie.delete("locale");
20+
cookie.set("locale", newLocale, {});
21+
locale(newLocale);
22+
} else {
23+
// Choose locale based on cookie or accept-language header
24+
const maybeLocale =
25+
cookie.get("locale")?.value || headers.get("accept-language");
26+
locale(guessLocale(maybeLocale));
27+
}
28+
};

starters/features/localize/package.json

Lines changed: 0 additions & 37 deletions
This file was deleted.

starters/features/localize/src/locales/message.en.json

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)