@intlayer/docs 9.3.1 → 9.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blog/ar/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/de/nextjs-multilingual-seo-comparison.md +9 -9
- package/blog/en/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/en-GB/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/es/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/fr/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/hi/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/id/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/it/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/ja/nextjs-multilingual-seo-comparison.md +9 -9
- package/blog/ko/nextjs-multilingual-seo-comparison.md +9 -9
- package/blog/pl/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/pt/nextjs-multilingual-seo-comparison.md +9 -9
- package/blog/ru/nextjs-multilingual-seo-comparison.md +9 -9
- package/blog/tr/nextjs-multilingual-seo-comparison.md +9 -9
- package/blog/uk/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/vi/nextjs-multilingual-seo-comparison.md +10 -10
- package/blog/zh/nextjs-multilingual-seo-comparison.md +10 -10
- package/dist/cjs/generated/docs.entry.cjs +20 -0
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs +20 -0
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/types/generated/docs.entry.d.ts +1 -0
- package/dist/types/generated/docs.entry.d.ts.map +1 -1
- package/docs/ar/bundle_optimization.md +1 -1
- package/docs/ar/configuration.md +32 -9
- package/docs/ar/dictionary/content_file.md +0 -22
- package/docs/ar/dictionary/function_fetching.md +23 -0
- package/docs/ar/eslint.md +336 -0
- package/docs/ar/intlayer_with_react_router_v7_fs_routes.md +1 -1
- package/docs/bn/configuration.md +34 -9
- package/docs/bn/eslint.md +336 -0
- package/docs/cs/bundle_optimization.md +1 -1
- package/docs/cs/configuration.md +33 -9
- package/docs/cs/eslint.md +336 -0
- package/docs/de/bundle_optimization.md +1 -1
- package/docs/de/configuration.md +34 -9
- package/docs/de/dictionary/content_file.md +0 -22
- package/docs/de/dictionary/function_fetching.md +23 -0
- package/docs/de/eslint.md +336 -0
- package/docs/en/bundle_optimization.md +1 -1
- package/docs/en/configuration.md +34 -9
- package/docs/en/dictionary/content_file.md +0 -22
- package/docs/en/dictionary/function_fetching.md +23 -0
- package/docs/en/eslint.md +336 -0
- package/docs/en/packages/intlayer/getLocalizedPath.md +70 -19
- package/docs/en-GB/configuration.md +33 -9
- package/docs/en-GB/dictionary/content_file.md +0 -22
- package/docs/en-GB/dictionary/function_fetching.md +23 -0
- package/docs/en-GB/eslint.md +336 -0
- package/docs/en-GB/intlayer_with_create_react_app.md +32 -35
- package/docs/en-GB/intlayer_with_react_router_v7_fs_routes.md +1 -1
- package/docs/es/bundle_optimization.md +1 -1
- package/docs/es/configuration.md +35 -9
- package/docs/es/dictionary/content_file.md +0 -22
- package/docs/es/dictionary/function_fetching.md +23 -0
- package/docs/es/eslint.md +336 -0
- package/docs/fr/bundle_optimization.md +1 -1
- package/docs/fr/configuration.md +35 -9
- package/docs/fr/dictionary/content_file.md +0 -22
- package/docs/fr/dictionary/function_fetching.md +23 -0
- package/docs/fr/eslint.md +336 -0
- package/docs/hi/configuration.md +35 -9
- package/docs/hi/dictionary/content_file.md +0 -22
- package/docs/hi/dictionary/function_fetching.md +23 -0
- package/docs/hi/eslint.md +336 -0
- package/docs/hi/intlayer_with_react_router_v7_fs_routes.md +1 -1
- package/docs/hi/intlayer_with_vite+svelte.md +2 -2
- package/docs/id/configuration.md +35 -9
- package/docs/id/dictionary/content_file.md +0 -22
- package/docs/id/dictionary/function_fetching.md +23 -0
- package/docs/id/eslint.md +336 -0
- package/docs/it/bundle_optimization.md +1 -1
- package/docs/it/configuration.md +35 -9
- package/docs/it/dictionary/content_file.md +0 -22
- package/docs/it/dictionary/function_fetching.md +23 -0
- package/docs/it/eslint.md +336 -0
- package/docs/ja/configuration.md +30 -9
- package/docs/ja/dictionary/content_file.md +0 -22
- package/docs/ja/dictionary/function_fetching.md +23 -0
- package/docs/ja/eslint.md +336 -0
- package/docs/ja/intlayer_with_react_router_v7.md +1 -146
- package/docs/ja/intlayer_with_vite+react.md +5 -1
- package/docs/ko/configuration.md +31 -9
- package/docs/ko/dictionary/content_file.md +0 -22
- package/docs/ko/dictionary/function_fetching.md +23 -0
- package/docs/ko/eslint.md +336 -0
- package/docs/ko/intlayer_with_lynx+react.md +4 -0
- package/docs/ko/intlayer_with_react_router_v7_fs_routes.md +1 -1
- package/docs/ko/intlayer_with_storybook.md +5 -5
- package/docs/nl/configuration.md +33 -9
- package/docs/nl/eslint.md +336 -0
- package/docs/pl/bundle_optimization.md +1 -1
- package/docs/pl/configuration.md +34 -9
- package/docs/pl/dictionary/content_file.md +0 -22
- package/docs/pl/dictionary/function_fetching.md +23 -0
- package/docs/pl/eslint.md +336 -0
- package/docs/pl/intlayer_with_astro.md +1 -114
- package/docs/pl/migration_from_i18next_to_intlayer.md +4 -0
- package/docs/pl/migration_from_next-i18next_to_intlayer.md +8 -4
- package/docs/pl/migration_from_next-intl_to_intlayer.md +11 -5
- package/docs/pl/migration_from_nuxtjs_i18n_to_intlayer.md +8 -4
- package/docs/pl/migration_from_react-i18next_to_intlayer.md +8 -4
- package/docs/pl/migration_from_vue-i18n_to_intlayer.md +4 -0
- package/docs/pt/bundle_optimization.md +1 -1
- package/docs/pt/configuration.md +34 -9
- package/docs/pt/dictionary/content_file.md +0 -22
- package/docs/pt/dictionary/function_fetching.md +23 -0
- package/docs/pt/eslint.md +336 -0
- package/docs/pt/intlayer_with_astro.md +1 -114
- package/docs/ru/bundle_optimization.md +1 -1
- package/docs/ru/configuration.md +34 -9
- package/docs/ru/dictionary/content_file.md +0 -22
- package/docs/ru/dictionary/function_fetching.md +23 -0
- package/docs/ru/eslint.md +336 -0
- package/docs/tr/bundle_optimization.md +1 -1
- package/docs/tr/configuration.md +33 -9
- package/docs/tr/dictionary/content_file.md +0 -22
- package/docs/tr/dictionary/function_fetching.md +23 -0
- package/docs/tr/eslint.md +336 -0
- package/docs/uk/configuration.md +35 -9
- package/docs/uk/dictionary/content_file.md +0 -22
- package/docs/uk/dictionary/function_fetching.md +23 -0
- package/docs/uk/eslint.md +336 -0
- package/docs/uk/packages/angular-intlayer/exports.md +2 -2
- package/docs/ur/configuration.md +35 -9
- package/docs/ur/eslint.md +336 -0
- package/docs/vi/bundle_optimization.md +1 -1
- package/docs/vi/configuration.md +33 -9
- package/docs/vi/dictionary/content_file.md +0 -22
- package/docs/vi/dictionary/function_fetching.md +23 -0
- package/docs/vi/eslint.md +336 -0
- package/docs/zh/bundle_optimization.md +1 -1
- package/docs/zh/configuration.md +29 -9
- package/docs/zh/dictionary/content_file.md +0 -22
- package/docs/zh/dictionary/function_fetching.md +23 -0
- package/docs/zh/eslint.md +336 -0
- package/docs/zh/intlayer_with_create_react_app.md +4 -0
- package/docs/zh/intlayer_with_lynx+react.md +4 -0
- package/docs/zh/intlayer_with_nextjs_14.md +0 -2
- package/docs/zh/intlayer_with_nextjs_15.md +0 -2
- package/docs/zh/intlayer_with_nextjs_page_router.md +0 -2
- package/docs/zh/intlayer_with_nuxt.md +1 -1
- package/docs/zh/intlayer_with_react_router_v7.md +4 -0
- package/docs/zh/intlayer_with_react_router_v7_fs_routes.md +4 -0
- package/docs/zh/intlayer_with_solid_start.md +1 -1
- package/docs/zh/intlayer_with_vite+vue.md +0 -2
- package/docs/zh-TW/bundle_optimization.md +1 -1
- package/docs/zh-TW/eslint.md +336 -0
- package/package.json +7 -7
- package/src/generated/docs.entry.ts +20 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2026-08-12
|
|
3
|
+
updatedAt: 2026-08-13
|
|
4
|
+
title: ESLint Plugin | Lint rules for Intlayer
|
|
5
|
+
description: Catch hardcoded strings, dynamic calls the Intlayer compiler cannot optimize, and unused dictionary content, with eslint-plugin-intlayer. Works with ESLint and oxlint, across React, Vue, Svelte, Angular and Astro.
|
|
6
|
+
keywords:
|
|
7
|
+
- Intlayer
|
|
8
|
+
- ESLint
|
|
9
|
+
- oxlint
|
|
10
|
+
- Linting
|
|
11
|
+
- i18n
|
|
12
|
+
- Internationalization
|
|
13
|
+
- no-raw-text
|
|
14
|
+
- Hardcoded strings
|
|
15
|
+
- Unused translations
|
|
16
|
+
- Dead content
|
|
17
|
+
- React
|
|
18
|
+
- Vue
|
|
19
|
+
- Svelte
|
|
20
|
+
- Angular
|
|
21
|
+
slugs:
|
|
22
|
+
- doc
|
|
23
|
+
- eslint
|
|
24
|
+
history:
|
|
25
|
+
- version: 9.3.1
|
|
26
|
+
date: 2026-08-12
|
|
27
|
+
changes: "Init history"
|
|
28
|
+
author: aymericzip
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# ESLint x OXLint Plugin
|
|
32
|
+
|
|
33
|
+
`eslint-plugin-intlayer` catches the kinds of i18n mistake TypeScript cannot:
|
|
34
|
+
|
|
35
|
+
1. **Hardcoded text** that never made it into a dictionary.
|
|
36
|
+
2. **Dynamic calls** that type-check and run, but that the Intlayer compiler cannot optimize.
|
|
37
|
+
3. **Dead content** — dictionaries and fields nothing in the project reads (opt-in).
|
|
38
|
+
|
|
39
|
+
Unknown dictionary keys, unknown field paths and missing locales are already compile errors, so the plugin does not repeat them.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash packageManager="npm"
|
|
44
|
+
npm install --save-dev eslint-plugin-intlayer
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash packageManager="pnpm"
|
|
48
|
+
pnpm add --save-dev eslint-plugin-intlayer
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```bash packageManager="yarn"
|
|
52
|
+
yarn add --dev eslint-plugin-intlayer
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requires ESLint 9 or later (flat config). ESLint 10 is supported.
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
The plugin runs in both ESLint and [oxlint](https://oxc.rs) — the same rules, the same options.
|
|
60
|
+
|
|
61
|
+
<Tabs defaultTab="eslint">
|
|
62
|
+
<Tab label="ESLint" value="eslint">
|
|
63
|
+
|
|
64
|
+
```javascript fileName="eslint.config.mjs"
|
|
65
|
+
import intlayer from "eslint-plugin-intlayer";
|
|
66
|
+
|
|
67
|
+
export default [...intlayer.configs.recommended];
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or spread a config and set the severities yourself:
|
|
71
|
+
|
|
72
|
+
```javascript fileName="eslint.config.mjs"
|
|
73
|
+
import intlayer from "eslint-plugin-intlayer";
|
|
74
|
+
|
|
75
|
+
export default [
|
|
76
|
+
...intlayer.configs.recommended,
|
|
77
|
+
{
|
|
78
|
+
rules: {
|
|
79
|
+
"intlayer/no-raw-text": "warn",
|
|
80
|
+
"intlayer/static-dictionary-key": "error",
|
|
81
|
+
"intlayer/no-dynamic-field-access": "error",
|
|
82
|
+
"intlayer/enforce-adapter-import": "warn",
|
|
83
|
+
"intlayer/no-unused-content": "warn",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
</Tab>
|
|
90
|
+
<Tab label="oxlint" value="oxlint">
|
|
91
|
+
|
|
92
|
+
```json fileName=".oxlintrc.json"
|
|
93
|
+
{
|
|
94
|
+
"jsPlugins": ["eslint-plugin-intlayer"],
|
|
95
|
+
"rules": {
|
|
96
|
+
"intlayer/no-raw-text": "warn",
|
|
97
|
+
"intlayer/static-dictionary-key": "error",
|
|
98
|
+
"intlayer/no-dynamic-field-access": "error",
|
|
99
|
+
"intlayer/enforce-adapter-import": "warn"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Two caveats: oxlint's JS plugin support is still alpha, and oxlint does not support custom parsers — so `.vue`, `.svelte`, `.astro` and Angular templates are not linted there. Run oxlint over your JS/TS/JSX files and keep ESLint for the rest.
|
|
105
|
+
|
|
106
|
+
`no-unused-content` is left out above on purpose: it needs the working directory and the linted file path from the rule context, which the alpha JS plugin bridge does not guarantee. Run it under ESLint.
|
|
107
|
+
|
|
108
|
+
</Tab>
|
|
109
|
+
</Tabs>
|
|
110
|
+
|
|
111
|
+
### Configs
|
|
112
|
+
|
|
113
|
+
| Config | `no-raw-text` | `static-dictionary-key` | `no-dynamic-field-access` | `enforce-adapter-import` | `no-unused-content` |
|
|
114
|
+
| --------------- | -------------------------- | ----------------------- | ------------------------- | ------------------------ | ------------------- |
|
|
115
|
+
| `recommended` | warn | error | error | off | off |
|
|
116
|
+
| `strict` | error (+ non-JSX literals) | error | error | error | off |
|
|
117
|
+
| `contract-only` | off | error | error | off | off |
|
|
118
|
+
|
|
119
|
+
`recommended` keeps `no-raw-text` at `warn` on purpose: pointing it at an existing codebase surfaces every untranslated string at once, which should not break your build on day one.
|
|
120
|
+
|
|
121
|
+
`enforce-adapter-import` is off by default — enable it explicitly if you want it.
|
|
122
|
+
|
|
123
|
+
`no-unused-content` is off in every config, `strict` included. It is the one rule that reads your Intlayer configuration and walks your source files from disk, so turning it on should be a deliberate choice rather than something a preset does for you.
|
|
124
|
+
|
|
125
|
+
## Rules
|
|
126
|
+
|
|
127
|
+
### `no-raw-text`
|
|
128
|
+
|
|
129
|
+
Reports user-facing text that is not declared in a dictionary. It uses the same detection as `intlayer extract`, so brand names, CSS classes and technical identifiers are ignored.
|
|
130
|
+
|
|
131
|
+
```jsx
|
|
132
|
+
// ✗ Reported
|
|
133
|
+
<h1>Welcome to our documentation</h1>
|
|
134
|
+
<input placeholder="Enter your email address" />
|
|
135
|
+
|
|
136
|
+
// ✓ Fine
|
|
137
|
+
const { title } = useIntlayer("home");
|
|
138
|
+
<h1>{title}</h1>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Content declaration files (`*.content.ts`, …) are skipped.
|
|
142
|
+
|
|
143
|
+
To fix a whole file at once, run `npx intlayer extract` and let the compiler move the strings into a dictionary for you.
|
|
144
|
+
|
|
145
|
+
**Options**
|
|
146
|
+
|
|
147
|
+
```javascript fileName="eslint.config.mjs"
|
|
148
|
+
{
|
|
149
|
+
"intlayer/no-raw-text": [
|
|
150
|
+
"warn",
|
|
151
|
+
{
|
|
152
|
+
// Attributes whose value is user-facing text.
|
|
153
|
+
// Default: title, placeholder, alt, aria-label, label
|
|
154
|
+
attributes: ["title", "placeholder", "alt", "aria-label", "label"],
|
|
155
|
+
|
|
156
|
+
// Elements whose content is never user-facing text.
|
|
157
|
+
// Default: code, pre, script, style
|
|
158
|
+
ignoreElements: ["code", "pre", "script", "style"],
|
|
159
|
+
|
|
160
|
+
// Regular expressions for text to never report.
|
|
161
|
+
ignorePatterns: ["^Powered by"],
|
|
162
|
+
|
|
163
|
+
// Also report string literals outside markup. Default: false
|
|
164
|
+
includeStringLiterals: false,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `static-dictionary-key`
|
|
171
|
+
|
|
172
|
+
Requires the dictionary key to be a string literal.
|
|
173
|
+
|
|
174
|
+
The compiler can only pre-load a dictionary when it can read the key directly at the call site. With a computed key it silently skips the optimization and bundles every dictionary instead.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// ✗ Reported
|
|
178
|
+
useIntlayer(dictionaryKey);
|
|
179
|
+
useIntlayer(`home-${suffix}`);
|
|
180
|
+
getTranslations({ namespace: page });
|
|
181
|
+
|
|
182
|
+
// ✗ A variable is still not a literal
|
|
183
|
+
const key = "home";
|
|
184
|
+
useIntlayer(key);
|
|
185
|
+
|
|
186
|
+
// ✓ Fine
|
|
187
|
+
useIntlayer("home");
|
|
188
|
+
getTranslations({ namespace: "home" });
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This applies to `useIntlayer`, `getIntlayer` and every compat adapter (`useTranslation`, `useTranslations`, `formatMessage`, `<FormattedMessage id>`, `<Trans i18nKey>`, …).
|
|
192
|
+
|
|
193
|
+
### `no-dynamic-field-access`
|
|
194
|
+
|
|
195
|
+
Requires the field you read from a dictionary to be statically known.
|
|
196
|
+
|
|
197
|
+
The compiler removes fields it does not see used. A computed access is invisible to it, so the read can return `undefined` at runtime.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// ✗ Reported
|
|
201
|
+
const content = useIntlayer("home");
|
|
202
|
+
content[fieldName];
|
|
203
|
+
|
|
204
|
+
const t = useTranslations("home");
|
|
205
|
+
t(messageKey);
|
|
206
|
+
|
|
207
|
+
// ✓ Fine
|
|
208
|
+
content.title;
|
|
209
|
+
content["title"];
|
|
210
|
+
content.items[0];
|
|
211
|
+
t("hero.title");
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### `enforce-adapter-import`
|
|
215
|
+
|
|
216
|
+
Prefers the `@intlayer/*` compat adapter over the original package. The original only resolves to Intlayer when the bundler alias is configured; the adapter always does. Autofixable with `--fix`.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// ✗ Reported
|
|
220
|
+
import { useTranslation } from "react-i18next";
|
|
221
|
+
import { getTranslations } from "next-intl/server";
|
|
222
|
+
|
|
223
|
+
// ✓ Fine
|
|
224
|
+
import { useTranslation } from "@intlayer/react-i18next";
|
|
225
|
+
import { getTranslations } from "@intlayer/next-intl/server";
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `no-unused-content`
|
|
229
|
+
|
|
230
|
+
**Off by default.** Reports content nothing in your project reads, plus dictionary keys declared in more than one place.
|
|
231
|
+
|
|
232
|
+
```typescript fileName="src/home.content.ts"
|
|
233
|
+
export default {
|
|
234
|
+
key: "home", // ✗ Reported when no caller anywhere asks for "home"
|
|
235
|
+
content: {
|
|
236
|
+
title: t({ en: "Title" }),
|
|
237
|
+
|
|
238
|
+
// ✗ Reported when nothing reads `hero`
|
|
239
|
+
hero: {
|
|
240
|
+
subtitle: t({ en: "Subtitle" }),
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Unlike the other rules, this one cannot answer from the file in front of it — a field is unused only relative to the whole project. On the first content declaration of a lint run it loads your Intlayer configuration, globs the source files that configuration declares (`build.traversePattern`, `compiler.transformPattern`) and runs the same usage analyser that powers `@intlayer/lsp` and the "unused" strikethrough in the VS Code extension. The result is cached for `cacheTtl` milliseconds, so the scan happens once per run rather than once per file.
|
|
247
|
+
|
|
248
|
+
**Options**
|
|
249
|
+
|
|
250
|
+
```javascript fileName="eslint.config.mjs"
|
|
251
|
+
{
|
|
252
|
+
"intlayer/no-unused-content": [
|
|
253
|
+
"warn",
|
|
254
|
+
{
|
|
255
|
+
// Report dictionary keys nothing references. Default: true
|
|
256
|
+
reportUnusedDictionaries: true,
|
|
257
|
+
|
|
258
|
+
// Report content fields nothing reads. Default: true
|
|
259
|
+
reportUnusedFields: true,
|
|
260
|
+
|
|
261
|
+
// Report keys declared in more than one place. Default: true
|
|
262
|
+
reportDuplicateKeys: true,
|
|
263
|
+
|
|
264
|
+
// Regular expressions for field paths to never report.
|
|
265
|
+
ignoreFields: ["^meta"],
|
|
266
|
+
|
|
267
|
+
// Project root the scan starts from. Default: ESLint's working directory
|
|
268
|
+
baseDir: process.cwd(),
|
|
269
|
+
|
|
270
|
+
// How long one project scan is reused, in ms. Default: 30000
|
|
271
|
+
cacheTtl: 30000,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Lower `cacheTtl` when you lint from a long-lived editor server and want your edits reflected sooner; set `baseDir` when a single lint run spans several Intlayer projects in a monorepo.
|
|
278
|
+
|
|
279
|
+
> **It errs towards silence.** A false positive here deletes a translation, so nothing is reported when the dictionary is consumed in a way the analysis cannot follow: the content object passed on as a whole, a translator function bound from it (`const t = useTranslations("home")`), a declaration reached through a direct import (`useDictionary(myDictionary)`), a `nest()` from another dictionary, or a field list made non-exhaustive by a spread. Single-file components (`.vue`, `.svelte`, `.astro`) count as using every field of the dictionaries they mention, because their script blocks are not parsed here.
|
|
280
|
+
|
|
281
|
+
`reportDuplicateKeys` reads the unmerged dictionaries the build writes under `.intlayer/`, so it stays quiet until the project has been built at least once. Two declarations sharing a key are merged, which is a legitimate pattern — the report exists because a field defined on both sides silently keeps only one of the two values.
|
|
282
|
+
|
|
283
|
+
The analyser is loaded from `@intlayer/lsp`, which ships as ESM. The rule therefore needs a Node version that can `require()` an ES module — Node 20.19+ or 22.12+. On anything older it reports nothing rather than failing the lint run.
|
|
284
|
+
|
|
285
|
+
## Frameworks
|
|
286
|
+
|
|
287
|
+
Every rule works across all Intlayer integrations, including inside Vue, Svelte and Angular templates. You only need to tell ESLint which parser reads each file type.
|
|
288
|
+
|
|
289
|
+
| Framework | Files | Parser |
|
|
290
|
+
| ------------------------- | ----------------- | --------------------------------- |
|
|
291
|
+
| React, Preact, Solid, Lit | `.jsx` `.tsx` | `typescript-eslint` |
|
|
292
|
+
| Next.js | `.jsx` `.tsx` | `typescript-eslint` |
|
|
293
|
+
| Vue, Nuxt | `.vue` | `vue-eslint-parser` |
|
|
294
|
+
| Svelte, SvelteKit | `.svelte` | `svelte-eslint-parser` |
|
|
295
|
+
| Angular | `.ts` | `typescript-eslint` |
|
|
296
|
+
| Angular templates | `.component.html` | `@angular-eslint/template-parser` |
|
|
297
|
+
| Astro | `.astro` | `astro-eslint-parser` |
|
|
298
|
+
|
|
299
|
+
```javascript fileName="eslint.config.mjs"
|
|
300
|
+
import intlayer from "eslint-plugin-intlayer";
|
|
301
|
+
import tseslint from "typescript-eslint";
|
|
302
|
+
import vueParser from "vue-eslint-parser";
|
|
303
|
+
import svelteParser from "svelte-eslint-parser";
|
|
304
|
+
import angularTemplateParser from "@angular-eslint/template-parser";
|
|
305
|
+
|
|
306
|
+
export default [
|
|
307
|
+
...intlayer.configs.recommended,
|
|
308
|
+
|
|
309
|
+
{
|
|
310
|
+
files: ["**/*.{ts,tsx,jsx}"],
|
|
311
|
+
languageOptions: { parser: tseslint.parser },
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
files: ["**/*.vue"],
|
|
315
|
+
languageOptions: {
|
|
316
|
+
parser: vueParser,
|
|
317
|
+
parserOptions: { parser: tseslint.parser },
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
files: ["**/*.svelte"],
|
|
322
|
+
languageOptions: {
|
|
323
|
+
parser: svelteParser,
|
|
324
|
+
parserOptions: { parser: tseslint.parser },
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
files: ["**/*.component.html"],
|
|
329
|
+
languageOptions: { parser: angularTemplateParser },
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Install only the parsers your project needs.
|
|
335
|
+
|
|
336
|
+
> **Known limitation.** In Vue and Angular templates, an expression such as `{{ content[key] }}` is not checked by `no-dynamic-field-access`. Dynamic reads written in the script block are caught normally.
|
|
@@ -19,6 +19,9 @@ slugs:
|
|
|
19
19
|
- intlayer
|
|
20
20
|
- getLocalizedPath
|
|
21
21
|
history:
|
|
22
|
+
- version: 8.0.0
|
|
23
|
+
date: 2026-08-19
|
|
24
|
+
changes: "Apply the locale prefix, and narrow the return type from the declared rewrite rules"
|
|
22
25
|
- version: 8.0.0
|
|
23
26
|
date: 2026-01-22
|
|
24
27
|
changes: "Implement custom URL rewrites"
|
|
@@ -29,13 +32,17 @@ author: aymericzip
|
|
|
29
32
|
|
|
30
33
|
## Description
|
|
31
34
|
|
|
32
|
-
The `getLocalizedPath` function
|
|
35
|
+
The `getLocalizedPath` function localizes a canonical path (internal application path): it resolves the custom rewrite rules, then applies the locale prefix of your routing mode. It is particularly useful for generating SEO-friendly URLs that vary by language.
|
|
36
|
+
|
|
37
|
+
It is the relative counterpart of [`getLocalizedUrl`](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en/packages/intlayer/getLocalizedUrl.md) — for a relative input both return the same value. Unlike `getLocalizedUrl`, it never returns an absolute URL: the `domains` configuration is ignored, so a locale served from its own domain still yields a path. An absolute input is accepted, but its origin is dropped — only its path, query string and hash are kept.
|
|
33
38
|
|
|
34
39
|
**Key Features:**
|
|
35
40
|
|
|
36
41
|
- Supports dynamic route parameters using the `[param]` syntax.
|
|
37
42
|
- Resolves paths according to custom rewrite rules defined in your configuration.
|
|
43
|
+
- Applies the locale prefix of the configured routing mode (`prefix-no-default`, `prefix-all`, …).
|
|
38
44
|
- Automatically handles fallback to the canonical path if no rewrite rule is found for the specified locale.
|
|
45
|
+
- Narrows its return type: a literal path resolves to the localized string literal at compile time.
|
|
39
46
|
|
|
40
47
|
---
|
|
41
48
|
|
|
@@ -43,9 +50,14 @@ The `getLocalizedPath` function resolves a canonical path (internal application
|
|
|
43
50
|
|
|
44
51
|
```typescript
|
|
45
52
|
getLocalizedPath(
|
|
46
|
-
canonicalPath: string,
|
|
47
|
-
locale
|
|
48
|
-
|
|
53
|
+
canonicalPath: string, // Required
|
|
54
|
+
locale?: Locales, // Optional
|
|
55
|
+
options?: { // Optional
|
|
56
|
+
locales?: Locales[];
|
|
57
|
+
defaultLocale?: Locales;
|
|
58
|
+
mode?: 'prefix-no-default' | 'prefix-all' | 'no-prefix' | 'search-params';
|
|
59
|
+
rewrite?: RoutingConfig['rewrite'];
|
|
60
|
+
}
|
|
49
61
|
): string
|
|
50
62
|
```
|
|
51
63
|
|
|
@@ -60,17 +72,21 @@ getLocalizedPath(
|
|
|
60
72
|
- **Type**: `string`
|
|
61
73
|
- **Required**: Yes
|
|
62
74
|
|
|
63
|
-
|
|
75
|
+
### Optional Parameters
|
|
76
|
+
|
|
77
|
+
- `locale?: Locales`
|
|
64
78
|
- **Description**: The target locale for which the path should be localized.
|
|
65
79
|
- **Type**: `Locales`
|
|
66
|
-
- **
|
|
80
|
+
- **Default**: The default locale of your project's configuration.
|
|
67
81
|
|
|
68
|
-
|
|
82
|
+
- `options?: object`
|
|
83
|
+
- **Description**: Routing overrides. Every entry defaults to your project's configuration.
|
|
84
|
+
- **Type**: `object`
|
|
69
85
|
|
|
70
|
-
- `
|
|
71
|
-
-
|
|
72
|
-
- **
|
|
73
|
-
- **Default**: `configuration.routing.rewrite`
|
|
86
|
+
- `options.locales?: Locales[]` — supported locales. **Default**: `configuration.internationalization.locales`
|
|
87
|
+
- `options.defaultLocale?: Locales` — the default locale. **Default**: `configuration.internationalization.defaultLocale`
|
|
88
|
+
- `options.mode?: 'prefix-no-default' | 'prefix-all' | 'no-prefix' | 'search-params'` — how the locale appears in the path. **Default**: `configuration.routing.mode`
|
|
89
|
+
- `options.rewrite?: RoutingConfig['rewrite']` — custom rewrite rules. **Default**: `configuration.routing.rewrite`
|
|
74
90
|
|
|
75
91
|
---
|
|
76
92
|
|
|
@@ -79,6 +95,28 @@ getLocalizedPath(
|
|
|
79
95
|
- **Type**: `string`
|
|
80
96
|
- **Description**: The localized path for the specified locale.
|
|
81
97
|
|
|
98
|
+
The type is narrowed from the rewrite rules declared in your configuration, so the editor shows the resolved path rather than a bare `string`:
|
|
99
|
+
|
|
100
|
+
```typescript codeFormat="typescript"
|
|
101
|
+
// Configuration: mode 'prefix-no-default', defaultLocale 'en',
|
|
102
|
+
// { '/about': { fr: '/a-propos' }, '/product/[id]': { fr: '/produit/[id]' } }
|
|
103
|
+
const about = getLocalizedPath("/about", Locales.FRENCH);
|
|
104
|
+
// ^? '/fr/a-propos'
|
|
105
|
+
const product = getLocalizedPath("/product/123", Locales.FRENCH);
|
|
106
|
+
// ^? '/fr/produit/123'
|
|
107
|
+
const contact = getLocalizedPath("/contact", Locales.FRENCH);
|
|
108
|
+
// ^? '/fr/contact' (no rewrite rule matches, only the prefix is applied)
|
|
109
|
+
const home = getLocalizedPath("/", Locales.FRENCH);
|
|
110
|
+
// ^? '/fr'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The same narrowing flows into [`getLocalizedUrl`](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en/packages/intlayer/getLocalizedUrl.md), which applies the rewrite rules before prefixing the locale.
|
|
114
|
+
|
|
115
|
+
Two cases stay widened to `string`, because they cannot be resolved at compile time:
|
|
116
|
+
|
|
117
|
+
- a path that is not a string literal (e.g. one built from a variable);
|
|
118
|
+
- a path matched by a rule using a multi-segment or optional parameter (`[...slug]`, `[[...slug]]`, `:param?`).
|
|
119
|
+
|
|
82
120
|
---
|
|
83
121
|
|
|
84
122
|
## Example Usage
|
|
@@ -90,12 +128,13 @@ If you have configured custom rewrites in your `intlayer.config.ts`:
|
|
|
90
128
|
```typescript codeFormat="typescript"
|
|
91
129
|
import { getLocalizedPath, Locales } from "intlayer";
|
|
92
130
|
|
|
93
|
-
// Configuration:
|
|
131
|
+
// Configuration: mode 'prefix-no-default', defaultLocale 'en',
|
|
132
|
+
// { '/about': { en: '/about', fr: '/a-propos' } }
|
|
94
133
|
getLocalizedPath("/about", Locales.FRENCH);
|
|
95
|
-
// Output: "/a-propos"
|
|
134
|
+
// Output: "/fr/a-propos"
|
|
96
135
|
|
|
97
136
|
getLocalizedPath("/about", Locales.ENGLISH);
|
|
98
|
-
// Output: "/about"
|
|
137
|
+
// Output: "/about" (default locale, no prefix)
|
|
99
138
|
```
|
|
100
139
|
|
|
101
140
|
### Usage with Dynamic Routes
|
|
@@ -105,7 +144,7 @@ import { getLocalizedPath, Locales } from "intlayer";
|
|
|
105
144
|
|
|
106
145
|
// Configuration: { '/product/[id]': { en: '/product/[id]', fr: '/produit/[id]' } }
|
|
107
146
|
getLocalizedPath("/product/123", Locales.FRENCH);
|
|
108
|
-
// Output: "/produit/123"
|
|
147
|
+
// Output: "/fr/produit/123"
|
|
109
148
|
```
|
|
110
149
|
|
|
111
150
|
### Manual Rewrite Rules
|
|
@@ -122,13 +161,25 @@ const manualRules = {
|
|
|
122
161
|
},
|
|
123
162
|
};
|
|
124
163
|
|
|
125
|
-
getLocalizedPath("/contact", Locales.FRENCH, manualRules);
|
|
126
|
-
// Output: "/contactez-nous"
|
|
164
|
+
getLocalizedPath("/contact", Locales.FRENCH, { rewrite: manualRules });
|
|
165
|
+
// Output: "/fr/contactez-nous"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Omitting the Locale
|
|
169
|
+
|
|
170
|
+
When no locale is given, the path is localized for the configured default locale:
|
|
171
|
+
|
|
172
|
+
```typescript codeFormat="typescript"
|
|
173
|
+
import { getLocalizedPath } from "intlayer";
|
|
174
|
+
|
|
175
|
+
// Configuration: defaultLocale = Locales.ENGLISH, { '/about': { en: '/about', fr: '/a-propos' } }
|
|
176
|
+
getLocalizedPath("/about");
|
|
177
|
+
// Output: "/about"
|
|
127
178
|
```
|
|
128
179
|
|
|
129
180
|
---
|
|
130
181
|
|
|
131
182
|
## Related Functions
|
|
132
183
|
|
|
133
|
-
- [`getCanonicalPath`](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en/packages/intlayer/getCanonicalPath.md): Resolves a localized path back to its internal canonical path.
|
|
134
|
-
- [`getLocalizedUrl`](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en/packages/intlayer/getLocalizedUrl.md):
|
|
184
|
+
- [`getCanonicalPath`](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en/packages/intlayer/getCanonicalPath.md): Resolves a localized path back to its internal canonical path. Note that it undoes the rewrite rules only — strip the locale prefix with [`getPathWithoutLocale`](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en/packages/intlayer/getPathWithoutLocale.md) first.
|
|
185
|
+
- [`getLocalizedUrl`](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en/packages/intlayer/getLocalizedUrl.md): Same localization, but able to return an absolute URL (protocol, host, domain routing).
|
|
@@ -479,6 +479,28 @@ const config: IntlayerConfig = {
|
|
|
479
479
|
*/
|
|
480
480
|
purge: true,
|
|
481
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Group the per-locale dictionary chunks by the code-split boundary that uses
|
|
484
|
+
* them, so a lazily loaded page fetches its content in one request.
|
|
485
|
+
* Default: true
|
|
486
|
+
*
|
|
487
|
+
* Note:
|
|
488
|
+
* - Only applies to dictionaries using `importMode: 'dynamic'`.
|
|
489
|
+
*/
|
|
490
|
+
chunkGrouping: true,
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Load a dictionary together with the chunk that uses it, instead of fetching
|
|
494
|
+
* it once that chunk renders. Readers render synchronously instead of
|
|
495
|
+
* suspending, so navigating no longer flashes a loading state.
|
|
496
|
+
* Default: true
|
|
497
|
+
*
|
|
498
|
+
* Note:
|
|
499
|
+
* - Only the resolved locale is awaited, so a page still downloads only the
|
|
500
|
+
* language it renders.
|
|
501
|
+
*/
|
|
502
|
+
dictionariesPreload: true,
|
|
503
|
+
|
|
482
504
|
/**
|
|
483
505
|
* Output format for generated dictionary files.
|
|
484
506
|
* Default: ['cjs', 'esm']
|
|
@@ -1060,15 +1082,17 @@ Build options apply to the `@intlayer/babel` and `@intlayer/swc` plugins.
|
|
|
1060
1082
|
|
|
1061
1083
|
> When optimized, Intlayer will replace dictionary calls to optimize chunking, so the final bundle only imports dictionaries that are actually used.
|
|
1062
1084
|
|
|
1063
|
-
| Field
|
|
1064
|
-
|
|
|
1065
|
-
| `mode`
|
|
1066
|
-
| `optimize`
|
|
1067
|
-
| `minify`
|
|
1068
|
-
| `prune`
|
|
1069
|
-
| `checkTypes`
|
|
1070
|
-
| `
|
|
1071
|
-
| `
|
|
1085
|
+
| Field | Description | Type | Default | Example | Note |
|
|
1086
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1087
|
+
| `mode` | Controls the mode of the build. | `'auto'` | <br/> `'manual'` | `'auto'` | `'manual'` | • `'auto'`: build enabled automatically when the application is built.<br/>• `'manual'`: only runs when the build command is executed.<br/>• Can be used to disable dictionary builds (e.g. to avoid running in Node.js environments). |
|
|
1088
|
+
| `optimize` | Controls whether the build should be optimized. | `boolean` | `undefined` | `process.env.NODE_ENV === 'production'` | • If unset, optimization is triggered on framework build (Vite/Next.js).<br/>• `true` forces optimization including dev mode.<br/>• `false` disables it.<br/>• When enabled, replaces dictionary calls to optimize chunking - only used dictionaries are imported.<br/>• Relies on `@intlayer/babel` and `@intlayer/swc` plugins.<br/>• Keys must be declared statically. |
|
|
1089
|
+
| `minify` | Defines if the dictionaries should be minified to reduce the bundle size. | `boolean` | `false` | | • Defines if the bundle should be minified.<br/>• Default: `false` in production.<br/>• This option will be ignored if `optimize` is disabled.<br/>• This option will be ignored if `editor.enabled` is true. |
|
|
1090
|
+
| `prune` | Defines if the unused keys in dictionaries should be purged. | `boolean` | `true` | | • Defines if the bundle should be pruned.<br/>• Default: `true` in production.<br/>• This option will be ignored if `optimize` is disabled. |
|
|
1091
|
+
| `checkTypes` | Indicates if the build should check TypeScript types and log errors. | `boolean` | `false` | | Can slow down the build. |
|
|
1092
|
+
| `chunkGrouping` | Whether to group the per-locale dictionary chunks by the code-split boundary that uses them. | `boolean` | `true` | | • Without grouping, a page assembled from many components issues one request per dictionary.<br/>• Dictionaries reached from several boundaries move to a shared chunk, so no page ships another page's content.<br/>• Only applies to dictionaries using `importMode: 'dynamic'`.<br/>• Only applies to the client build, and only when bundling (not in dev). |
|
|
1093
|
+
| `dictionariesPreload` | Whether a dictionary should load together with the chunk that uses it, instead of being fetched once that chunk renders. | `boolean` | `true` | | • The generated entry point awaits the browsing locale at the top level, so a lazily loaded route is not considered loaded until its content is there.<br/>• Readers render synchronously instead of suspending, so navigating no longer flashes a loading state.<br/>• Only the resolved locale is awaited, so a page still downloads only the language it renders.<br/>• Only applies to dictionaries using `importMode: 'dynamic'`, on the client build.<br/>• Requires a bundler supporting top-level await (Vite, esbuild). |
|
|
1094
|
+
| `outputFormat` | Controls the output format of the dictionaries. | `('esm' | 'cjs')[]` | `['esm', 'cjs']` | `['cjs']` | |
|
|
1095
|
+
| `traversePattern` | Patterns defining which files to traverse during optimization. | `string[]` | `['**/*.{tsx,ts,js,mjs,cjs,jsx,vue,svelte,svte}', '!**/node_modules/**', '!**/dist/**', '!**/.intlayer/**', '!**/*.config.*', '!**/*.test.*', '!**/*.spec.*', '!**/*.stories.*']` | `['src/**/*.{ts,tsx}', '../ui-library/**/*.{ts,tsx}', '!**/node_modules/**']` | • Limit optimization to relevant files to improve build performance.<br/>• Ignored if `optimize` is disabled.<br/>• Uses glob pattern. |
|
|
1072
1096
|
|
|
1073
1097
|
---
|
|
1074
1098
|
|
|
@@ -532,28 +532,6 @@ Used in conjunction with Variants, this field defines named content alternatives
|
|
|
532
532
|
|
|
533
533
|
> See [Variants](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en-GB/dynamic_dictionaries/variants.md) for more information.
|
|
534
534
|
|
|
535
|
-
#### `meta` (`Record<string, string | number | boolean>`)
|
|
536
|
-
|
|
537
|
-
Used in conjunction with Dynamic Records, this field allows declaring CMS-managed records or arbitrary data fetched at runtime by an opaque ID. The dictionary identity is defined by the arbitrary set of key-value pairs declared in this `meta` field.
|
|
538
|
-
|
|
539
|
-
**Example:**
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
{
|
|
543
|
-
key: "product-copy",
|
|
544
|
-
meta: {
|
|
545
|
-
id: "prod_abc",
|
|
546
|
-
userId: "user_123"
|
|
547
|
-
},
|
|
548
|
-
content: {
|
|
549
|
-
name: "Widget Pro",
|
|
550
|
-
description: "The best widget."
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
> See [Dynamic Records](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en-GB/dynamic_dictionaries/dynamic_content.md) for more information.
|
|
556
|
-
|
|
557
535
|
### CMS Properties
|
|
558
536
|
|
|
559
537
|
##### `version` (string)
|
|
@@ -85,6 +85,29 @@ No way to fetch content from a JSON file, use a .ts or .js file instead
|
|
|
85
85
|
|
|
86
86
|
In this case, the `fakeFetch` function mimics a delay to simulate server response time. Intlayer executes the asynchronous function and uses the result as the content for the `text` key.
|
|
87
87
|
|
|
88
|
+
## Fetching Remote Content
|
|
89
|
+
|
|
90
|
+
You can also assign a promise directly to a content field. Intlayer awaits it while building the dictionaries and inlines the resolved value:
|
|
91
|
+
|
|
92
|
+
```typescript fileName="**/*.content.ts" contentDeclarationFormat={["typescript", "esm", "commonjs"]}
|
|
93
|
+
import type { Dictionary } from "intlayer";
|
|
94
|
+
|
|
95
|
+
const remoteContent = {
|
|
96
|
+
key: "remote_content",
|
|
97
|
+
content: {
|
|
98
|
+
externalContent: fetch("https://example.com").then((res) => res.json()),
|
|
99
|
+
},
|
|
100
|
+
} satisfies Dictionary;
|
|
101
|
+
|
|
102
|
+
export default remoteContent;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```plaintext fileName="**/*.content.json" contentDeclarationFormat="json"
|
|
106
|
+
No way to fetch content from a JSON file, use a .ts or .js file instead
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> The request runs at build time, so the fetched data is a snapshot embedded in the dictionary. Rebuild your dictionaries to refresh it.
|
|
110
|
+
|
|
88
111
|
## Using Function-Based Content in React Components
|
|
89
112
|
|
|
90
113
|
To use function-based content in a React component, you need to import `useIntlayer` from `react-intlayer` and call it with the content ID to retrieve the content. Here's an example:
|