@inlang/paraglide-js 1.5.0 → 1.6.0

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/README.md CHANGED
@@ -85,7 +85,33 @@ m.hello() // Hello world!
85
85
  m.loginHeader({ name: "Samuel" }) // Hello Samuel, please login to continue.
86
86
  ```
87
87
 
88
- To choose between messages at runtime create a map of messages and index into it.
88
+ ## Working with the Inlang Message Format
89
+
90
+ Paraglide is part of the highly modular Inlang Ecosystem which supports many different Message Formats. By default, the [Inlang Message Format](https://inlang.com/m/reootnfj/plugin-inlang-messageFormat) is used.
91
+
92
+ It expects messages to be in `messages/{lang}.json` relative to your repo root.
93
+
94
+ ```json
95
+ //messages/en.json
96
+ {
97
+ //the $schema key is automatically ignored
98
+ "$schema": "https://inlang.com/schema/inlang-message-format",
99
+ "hello_world: "Hello World!",
100
+ "greeting": "Hello {name}!"
101
+ }
102
+ ```
103
+
104
+ The `messages/{lang}.json` file contains a flat map of message IDs and their translations. You can use curly braces to insert `{parameters}` into translations
105
+
106
+ **Nesting purposely isn't supported and likely won't be**. Nested messages are way harder to interact with from complementary tools like the [Sherlock IDE Extension](https://inlang.com/m/r7kp499g/app-inlang-ideExtension), the [Parrot Figma Plugin](https://inlang.com/m/gkrpgoir/app-parrot-figmaPlugin), or the [Fink Localization editor](https://inlang.com/m/tdozzpar/app-inlang-finkLocalizationEditor). Intellisense also becomes less helpful since it only shows the messages at the current level, not all messages. Additionally enforcing an organization-style side-steps organization discussions with other contributors.
107
+
108
+ ### Complex Formatting
109
+
110
+ The Message Format is still quite young, so advanced formats like plurals, param-formatting, and markup interpolation are currently not supported but are all on our roadmap.
111
+
112
+ If you need complex formatting, like plurals, dates, currency, or markup interpolation you can achieve them like so:
113
+
114
+ For a message with multiple cases, aka a _select message_, you can define a message for each case & then use a Map in JS to index into it.
89
115
 
90
116
  ```ts
91
117
  import * as m from "./paraglide/messages.js"
@@ -100,9 +126,46 @@ const season = {
100
126
  const msg = season["spring"]() // Hello spring!
101
127
  ```
102
128
 
129
+ For date & currency formatting use the `.toLocaleString` method on the `Date` or `Number`.
130
+
131
+ ```ts
132
+ import * as m from "./paraglide/messages.js"
133
+ import { languageTag } from "./paraglide/runtime.js"
134
+
135
+ const todaysDate = new Date();
136
+ m.today_is_the({
137
+ date: todaysDate.toLocaleString(languageTag())
138
+ })
139
+
140
+ const price = 100;
141
+ m.the_price_is({
142
+ price: price.toLocaleString(languageTag(), {
143
+ style: "currency",
144
+ currency: "EUR",
145
+ })
146
+ })
147
+ ```
148
+
149
+ You can put HTML into the messages. This is useful for links and images.
150
+
151
+ ```json
152
+ // messages/en.json
153
+ {
154
+ "you_must_agree_to_the_tos": "You must agree to the <a href='/en/tos'>Terms of Service</a>."
155
+ }
156
+ ```
157
+ ```json
158
+ // messages/de.json
159
+ {
160
+ you_must_agree_to_the_tos": "Sie müssen den <a href='/de/agb'>Nutzungsbedingungen</a> zustimmen."
161
+ }
162
+ ```
163
+
164
+ There is currently no way to interpolate full-blown components into the messages. If you require components mid-message you will need to create a one-off component for that bit of text.
165
+
103
166
  ## Setting the language
104
167
 
105
- You can set the [language tag](https://www.inlang.com/m/8y8sxj09/library-inlang-languageTag) by calling `setLanguageTag()`. Any subsequent calls to either `languageTag()` or a message function will use the new language tag.
168
+ You can set the [language tag](https://www.inlang.com/m/8y8sxj09/library-inlang-languageTag) by calling `setLanguageTag()` with the desired language, or a getter function. Any subsequent calls to either `languageTag()` or a message function will use the new language tag.
106
169
 
107
170
  ```js
108
171
  import { setLanguageTag } from "./paraglide/runtime.js"
@@ -111,11 +174,11 @@ import * as m from "./paraglide/messages.js"
111
174
  setLanguageTag("de")
112
175
  m.hello() // Hallo Welt!
113
176
 
114
- setLanguageTag("en")
177
+ setLanguageTag(()=>document.documentElement.lang /* en */ )
115
178
  m.hello() // Hello world!
116
179
  ```
117
180
 
118
- The [language tag](https://www.inlang.com/m/8y8sxj09/library-inlang-languageTag) is global, so you need to be careful with it on the server to make sure multiple requests don't interfere with each other.
181
+ The [language tag](https://www.inlang.com/m/8y8sxj09/library-inlang-languageTag) is global, so you need to be careful with it on the server to make sure multiple requests don't interfere with each other. Always use a getter-function that returns the current language tag _for the current request_.
119
182
 
120
183
  You will need to call `setLanguageTag` on both the server and the client since they run in separate processes.
121
184
 
@@ -137,7 +200,7 @@ setLanguageTag("de") // The language changed to de
137
200
  setLanguageTag("en") // The language changed to en
138
201
  ```
139
202
 
140
- A few things to know about `onSetLanguageTag()`:
203
+ Things to know about `onSetLanguageTag()`:
141
204
 
142
205
  - You can only register one listener. If you register a second listener it will throw an error.
143
206
  - `onSetLanguageTag` shouldn't be used on the server.
@@ -160,10 +223,9 @@ const msg = m.hello({ name: "Samuel" }, { languageTag: "de" }) // Hallo Samuel!
160
223
 
161
224
  ## Lazy-Loading
162
225
 
163
- Paraglide consciously discourages lazy-loading translations since it seriously hurts
164
- your Web Vitals. Learn more about why lazy-loading is bad & what to do instead in [our blog post on lazy-loading](https://inlang.com/g/mqlyfa7l/guide-lorissigrist-dontlazyload).
226
+ Paraglide consciously discourages lazy-loading translations since it causes a render-fetch waterfall which seriously hurts your Web Vitals. Learn more about why lazy-loading is bad & what to do instead in [our blog post on lazy-loading](https://inlang.com/g/mqlyfa7l/guide-lorissigrist-dontlazyload).
165
227
 
166
- If you want to do it anyway, lazily import the language-specific message files. Be careful with this.
228
+ If you want to do it anyway, lazily import the language-specific message files.
167
229
 
168
230
  ```ts
169
231
  const lazyGerman = await import("./paraglide/messages/de.js")
@@ -203,18 +265,14 @@ If you want your language files to be in a different location you can change the
203
265
 
204
266
  ```diff
205
267
  // project.inlang/settings.json
206
- "plugin.inlang.messageFormat": {
207
- - "pathPattern": "./messages/{languageTag}.json"
208
- + "pathPattern": "./i18n/{languageTag}.json"
209
- },
268
+ {
269
+ "plugin.inlang.messageFormat": {
270
+ - "pathPattern": "./messages/{languageTag}.json"
271
+ + "pathPattern": "./i18n/{languageTag}.json"
272
+ }
273
+ }
210
274
  ```
211
275
 
212
- ### Lint Rules
213
-
214
- If you're using the [Sherlock VS Code extension](https://inlang.com/m/r7kp499g/app-inlang-ideExtension) you might see warnings about certain messages. Perhaps they're duplicates, perhaps they're missing in one language.
215
-
216
- You can configure which lint-rules are active in `./project.inlang/settings.json`. Simply add or remove them from the `modules` array.
217
-
218
276
  # Playground
219
277
 
220
278
  Find examples of how to use Paraglide on CodeSandbox or in [our GitHub repository](https://github.com/opral/monorepo/tree/main/inlang/source-code/paraglide).
@@ -227,11 +285,11 @@ Find examples of how to use Paraglide on CodeSandbox or in [our GitHub repositor
227
285
 
228
286
  # Architecture
229
287
 
230
- ParaglideJS leverages a compiler to generate vanilla JavaScript functions from your messages. We call these "message functions".
288
+ Paraglide uses a compiler to generate JS functions from your messages. We call these "message functions".
231
289
 
232
- Message Functions are fully typed using JSDoc. They are exported individually from the `messages.js` file making them tree-shakable. They aren't reactive, they just return a string.
290
+ Message Functions are fully typed using JSDoc. They are exported individually from the `messages.js` file making them tree-shakable. When called, they return a translated string. Message functions aren't reactive in any way, if you want a translation in another language you will need to re-call them.
233
291
 
234
- This avoids many edge cases associated with reactivity, lazy-loading, and namespacing that other i18n libraries have to work around.
292
+ This design avoids many edge cases with reactivity, lazy-loading, and namespacing that other i18n libraries have to work around.
235
293
 
236
294
  In addition to the message functions, ParaglideJS also emits a runtime. The runtime is used to set the language tag. It contains less than 50 LOC (lines of code) and is less than 300 bytes minified & gzipped.
237
295
 
@@ -271,16 +329,6 @@ The compiler loads an Inlang project and compiles the messages into tree-shakabl
271
329
  export const hello = (params) => `Hello ${params.name}!`
272
330
  ```
273
331
 
274
- ## Messages
275
-
276
- By convention, we import the compiled functions with a wildcard import.
277
-
278
- ```js
279
- import * as m from "../paraglide/messages.js"
280
- ```
281
-
282
- Bundlers like Rollup, Webpack, or Turbopack tree-shake the messages that are not used, so using a wildcard import is perfectly fine.
283
-
284
332
  # Writing an Adapter
285
333
 
286
334
  An "Adapter" is a library that integrates with a framework's lifecycle and does two things:
@@ -288,7 +336,9 @@ An "Adapter" is a library that integrates with a framework's lifecycle and does
288
336
  1. Calls `setLanguageTag()` at appropriate times to set the language
289
337
  2. Reacts to `onSetLanguageTag()`, usually by navigating or relading the page.
290
338
 
291
- This example adapts Paraglide to a fictitious full-stack framework.
339
+ Many popular frameworks already have adapters available, check out the [list of available adapters](#use-it-with-your-favorite-framework).
340
+
341
+ If there isn't one for your framework, you can write your own. This example adapts Paraglide to a fictitious full-stack framework.
292
342
 
293
343
  ```tsx
294
344
  import {
@@ -316,7 +366,7 @@ if (isClient) {
316
366
 
317
367
  // When the language changes we want to re-render the page in the new language
318
368
  // Here we just navigate to the new route
319
- //
369
+
320
370
  // Make sure to call `onSetLanguageTag` after `setLanguageTag` to avoid an infinite loop.
321
371
  onSetLanguageTag((newLanguageTag) => {
322
372
  window.location.pathname = `/${newLanguageTag}${window.location.pathname}`
@@ -338,6 +388,8 @@ Of course, we're not done yet! We plan on adding the following features to Parag
338
388
  - [ ] Pluralization ([Join the Discussion](https://github.com/opral/monorepo/discussions/2025))
339
389
  - [ ] Formatting of numbers and dates ([Join the Discussion](https://github.com/opral/monorepo/discussions/992))
340
390
  - [ ] Markup Placeholders ([Join the Discussion](https://github.com/opral/monorepo/discussions/913))
391
+ - [ ] Component Interpolation
392
+ - [ ] Per-Language Splitting without Lazy-Loading
341
393
  - [ ] Even Smaller Output
342
394
 
343
395
  # Talks
@@ -347,7 +399,7 @@ Of course, we're not done yet! We plan on adding the following features to Parag
347
399
  - Web Zurich December 2023
348
400
  - [Svelte London January 2024](https://www.youtube.com/watch?v=eswNQiq4T2w&t=646s)
349
401
 
350
- # Tooling
402
+ # Complementary Tooling
351
403
 
352
404
  Paraglide JS is part of the Inlang ecosystem and integrates nicely with all the other Inlang-compatible tools.
353
405
 
@@ -0,0 +1,4 @@
1
+ export { negotiateLanguagePreferences } from './negotiation/language.js';
2
+ export { detectLanguageFromPath } from './routing/detectLanguage.js';
3
+ export { bestMatch, resolveRoute, parseRouteDefinition, exec, type PathDefinitionTranslations, type ParamMatcher, type RouteParam, } from './routing/routeDefinitions.js';
4
+ export { validatePathTranslations, prettyPrintIssues } from './routing/validatePathTranslations.js';
@@ -0,0 +1,375 @@
1
+ function negotiateLanguagePreferences(accept, availableLanguageTags) {
2
+ if (availableLanguageTags.length === 0)
3
+ return [];
4
+ accept ||= "*";
5
+ const acceptLanguageSpecs = parseAcceptLanguageHeader(accept);
6
+ const priorities = availableLanguageTags.map(
7
+ (languageTag, index) => getHighestLanguagePriority(languageTag, acceptLanguageSpecs, index)
8
+ );
9
+ return priorities.filter((prio) => prio.quality > 0).sort(comparePriorities).map((priority) => priority.languageTag);
10
+ }
11
+ function parseAcceptLanguageHeader(acceptLanguage) {
12
+ const acceptableLanguageDefinitions = acceptLanguage.split(",");
13
+ const specs = acceptableLanguageDefinitions.map((dfn) => dfn.trim()).map((dfn, index) => parseLanguage(dfn, index)).filter((maybeSpec) => Boolean(maybeSpec));
14
+ return specs;
15
+ }
16
+ function parseLanguage(str, index) {
17
+ const LANGUAGE_REGEXP = /^\s*([^\s\-;]+)(?:-([^\s;]+))?\s*(?:;(.*))?$/;
18
+ const match = LANGUAGE_REGEXP.exec(str);
19
+ if (!match)
20
+ return void 0;
21
+ const [, prefix, suffix, qualityMatch] = match;
22
+ if (!prefix)
23
+ throw new Error(`Invalid language tag: ${str}`);
24
+ const full = suffix ? `${prefix}-${suffix}` : prefix;
25
+ const quality = qualityMatch ? parseQuality(qualityMatch) ?? 1 : 1;
26
+ return {
27
+ prefix,
28
+ suffix,
29
+ quality,
30
+ index,
31
+ full
32
+ };
33
+ }
34
+ function parseQuality(qualityMatch) {
35
+ const params = qualityMatch.split(";");
36
+ for (const param of params) {
37
+ const [key, value] = param.split("=");
38
+ if (key === "q" && value)
39
+ return parseFloat(value);
40
+ }
41
+ return void 0;
42
+ }
43
+ function getHighestLanguagePriority(availableLanguageTag, acceptableLanguages, index) {
44
+ let highestPriority = {
45
+ languageTag: availableLanguageTag,
46
+ index: 0,
47
+ order: -1,
48
+ quality: 0,
49
+ specificity: 0
50
+ };
51
+ for (const acceptableLanguage of acceptableLanguages) {
52
+ const priority = calculatePriority(availableLanguageTag, acceptableLanguage, index);
53
+ if (!priority)
54
+ continue;
55
+ if (
56
+ //compare the calculated priority to the highest priority ignoring quality.
57
+ (highestPriority.specificity - priority.specificity || highestPriority.quality - priority.quality || highestPriority.order - priority.order) < 0
58
+ ) {
59
+ highestPriority = priority;
60
+ }
61
+ }
62
+ return highestPriority;
63
+ }
64
+ function calculatePriority(language, spec, index) {
65
+ const parsed = parseLanguage(language, 0);
66
+ if (!parsed)
67
+ return void 0;
68
+ let specificity = 0;
69
+ if (spec.full.toLowerCase() === parsed.full.toLowerCase()) {
70
+ specificity |= 4;
71
+ } else if (spec.prefix.toLowerCase() === parsed.full.toLowerCase()) {
72
+ specificity |= 2;
73
+ } else if (spec.full.toLowerCase() === parsed.prefix.toLowerCase()) {
74
+ specificity |= 1;
75
+ }
76
+ if (specificity === 0 && spec.full !== "*") {
77
+ return void 0;
78
+ }
79
+ return {
80
+ languageTag: language,
81
+ index,
82
+ order: spec.index,
83
+ quality: spec.quality,
84
+ specificity
85
+ };
86
+ }
87
+ function comparePriorities(a, b) {
88
+ return b.quality - a.quality || b.specificity - a.specificity || a.order - b.order || a.index - b.index || 0;
89
+ }
90
+ function detectLanguageFromPath({
91
+ path,
92
+ availableLanguageTags,
93
+ base
94
+ }) {
95
+ base ??= "";
96
+ if (base === "/")
97
+ base = "";
98
+ if (!path.startsWith(base)) {
99
+ return void 0;
100
+ }
101
+ const pathWithoutBase = path.replace(base, "");
102
+ const maybeLang = pathWithoutBase.split("/").at(1);
103
+ if (!maybeLang)
104
+ return void 0;
105
+ for (const lang of availableLanguageTags) {
106
+ if (lang.toLowerCase() === maybeLang.toLowerCase()) {
107
+ return lang;
108
+ }
109
+ }
110
+ return void 0;
111
+ }
112
+ const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;
113
+ function parseRouteDefinition(id) {
114
+ const params = [];
115
+ const pattern = id === "/" ? /^\/$/ : new RegExp(
116
+ `^${get_route_segments(id).map((segment) => {
117
+ const rest_match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
118
+ if (rest_match) {
119
+ params.push({
120
+ name: rest_match[1],
121
+ matcher: rest_match[2],
122
+ optional: false,
123
+ rest: true,
124
+ chained: true
125
+ });
126
+ return "(?:/(.*))?";
127
+ }
128
+ const optional_match = /^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(segment);
129
+ if (optional_match) {
130
+ params.push({
131
+ name: optional_match[1],
132
+ matcher: optional_match[2],
133
+ optional: true,
134
+ rest: false,
135
+ chained: true
136
+ });
137
+ return "(?:/([^/]+))?";
138
+ }
139
+ if (!segment) {
140
+ return;
141
+ }
142
+ const parts = segment.split(/\[(.+?)\](?!\])/);
143
+ const result = parts.map((content, i) => {
144
+ if (i % 2) {
145
+ if (content.startsWith("x+")) {
146
+ return escape(String.fromCharCode(parseInt(content.slice(2), 16)));
147
+ }
148
+ if (content.startsWith("u+")) {
149
+ return escape(
150
+ String.fromCharCode(
151
+ ...content.slice(2).split("-").map((code) => parseInt(code, 16))
152
+ )
153
+ );
154
+ }
155
+ const match = (
156
+ /** @type {RegExpExecArray} */
157
+ param_pattern.exec(content)
158
+ );
159
+ if (!match) {
160
+ throw new Error(
161
+ `Invalid param: ${content}. Params and matcher names can only have underscores and alphanumeric characters.`
162
+ );
163
+ }
164
+ const [, is_optional, is_rest, name, matcher] = match;
165
+ params.push({
166
+ name,
167
+ matcher,
168
+ optional: !!is_optional,
169
+ rest: !!is_rest,
170
+ chained: is_rest ? i === 1 && parts[0] === "" : false
171
+ });
172
+ return is_rest ? "(.*?)" : is_optional ? "([^/]*)?" : "([^/]+?)";
173
+ }
174
+ return escape(content);
175
+ }).join("");
176
+ return "/" + result;
177
+ }).join("")}/?$`
178
+ );
179
+ return { pattern, params };
180
+ }
181
+ function exec(match, params, matchers) {
182
+ const result = {};
183
+ const values = match.slice(1);
184
+ const values_needing_match = values.filter((value) => value !== void 0);
185
+ let buffered = 0;
186
+ for (const [i, param] of params.entries()) {
187
+ let value = values[i - buffered];
188
+ if (param.chained && param.rest && buffered) {
189
+ value = values.slice(i - buffered, i + 1).filter((s) => s).join("/");
190
+ buffered = 0;
191
+ }
192
+ if (value === void 0) {
193
+ if (param.rest)
194
+ result[param.name] = "";
195
+ continue;
196
+ }
197
+ if (param.matcher && !matchers[param.matcher]) {
198
+ return void 0;
199
+ }
200
+ const matcher = matchers[param.matcher] ?? (() => true);
201
+ if (matcher(value)) {
202
+ result[param.name] = value;
203
+ const next_param = params[i + 1];
204
+ const next_value = values[i + 1];
205
+ if (next_param && !next_param.rest && next_param.optional && next_value && param.chained) {
206
+ buffered = 0;
207
+ }
208
+ if (!next_param && !next_value && Object.keys(result).length === values_needing_match.length) {
209
+ buffered = 0;
210
+ }
211
+ continue;
212
+ }
213
+ if (param.optional && param.chained) {
214
+ buffered++;
215
+ continue;
216
+ }
217
+ return;
218
+ }
219
+ if (buffered)
220
+ return;
221
+ return result;
222
+ }
223
+ function escape(str) {
224
+ return str.normalize().replace(/[[\]]/g, "\\$&").replace(/%/g, "%25").replace(/\//g, "%2[Ff]").replace(/\?/g, "%3[Ff]").replace(/#/g, "%23").replace(/[.*+?^${}()|\\]/g, "\\$&");
225
+ }
226
+ const basic_param_pattern = /\[(\[)?(\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g;
227
+ function resolveRoute(id, params) {
228
+ const segments = get_route_segments(id);
229
+ return "/" + segments.map(
230
+ (segment) => segment.replace(basic_param_pattern, (_, optional, rest, name) => {
231
+ const param_value = params[name];
232
+ if (!param_value) {
233
+ if (optional)
234
+ return "";
235
+ if (rest && param_value !== void 0)
236
+ return "";
237
+ throw new Error(`Missing parameter '${name}' in route ${id}`);
238
+ }
239
+ if (param_value.startsWith("/") || param_value.endsWith("/"))
240
+ throw new Error(
241
+ `Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar`
242
+ );
243
+ return param_value;
244
+ })
245
+ ).filter(Boolean).join("/");
246
+ }
247
+ function bestMatch(canonicalPath, pathDefinitions, matchers) {
248
+ let bestMatch2 = void 0;
249
+ for (const pathDefinition of pathDefinitions) {
250
+ const route = parseRouteDefinition(pathDefinition);
251
+ const match = route.pattern.exec(removeTrailingSlash(canonicalPath));
252
+ if (!match)
253
+ continue;
254
+ const params = exec(match, route.params, matchers);
255
+ if (!params)
256
+ continue;
257
+ if (!bestMatch2) {
258
+ bestMatch2 = { params, route, id: pathDefinition };
259
+ continue;
260
+ }
261
+ const bestMatchNumParams = Object.keys(bestMatch2.route.params).length;
262
+ const currentMatchNumParams = Object.keys(route.params).length;
263
+ if (bestMatchNumParams < currentMatchNumParams) {
264
+ continue;
265
+ }
266
+ if (bestMatchNumParams > currentMatchNumParams) {
267
+ bestMatch2 = { params, route, id: pathDefinition };
268
+ continue;
269
+ }
270
+ if (bestMatchNumParams === currentMatchNumParams && route.pattern.source.length < bestMatch2.route.pattern.source.length) {
271
+ bestMatch2 = { params, route, id: pathDefinition };
272
+ }
273
+ }
274
+ return bestMatch2 ? {
275
+ id: bestMatch2.id,
276
+ params: bestMatch2.params
277
+ } : void 0;
278
+ }
279
+ function removeTrailingSlash(path) {
280
+ return path.endsWith("/") ? path.slice(0, -1) : path;
281
+ }
282
+ function get_route_segments(route) {
283
+ return route.slice(1).split("/");
284
+ }
285
+ function validatePathTranslations(pathTranslations, availableLanguageTags, matchers) {
286
+ const issues = [];
287
+ const expectedLanguages = new Set(availableLanguageTags);
288
+ const availableMatchers = new Set(Object.keys(matchers));
289
+ for (const path in pathTranslations) {
290
+ if (!isValidPath(path)) {
291
+ issues.push({
292
+ path,
293
+ message: "Path must start with a slash."
294
+ });
295
+ continue;
296
+ }
297
+ const { params: expectedParams } = parseRouteDefinition(path);
298
+ const expectedMatchers = expectedParams.map((param) => param.matcher).filter(Boolean);
299
+ for (const matcher of expectedMatchers) {
300
+ if (!availableMatchers.has(matcher)) {
301
+ issues.push({
302
+ path,
303
+ message: `Matcher ${matcher} is used but not available. Did you forget to pass it to createI18n?`
304
+ });
305
+ }
306
+ }
307
+ const translations = pathTranslations[path];
308
+ if (!translations)
309
+ continue;
310
+ for (const [lang, translatedPath] of Object.entries(translations)) {
311
+ if (!isValidPath(translatedPath)) {
312
+ issues.push({
313
+ path,
314
+ message: `The translation for language ${lang} must start with a slash.`
315
+ });
316
+ }
317
+ const { params: actualParams } = parseRouteDefinition(translatedPath);
318
+ let paramsDontMatch = false;
319
+ for (const param of expectedParams) {
320
+ if (!actualParams.some((actualParam) => paramsAreEqual(param, actualParam))) {
321
+ paramsDontMatch = true;
322
+ }
323
+ }
324
+ if (expectedParams.length !== actualParams.length) {
325
+ paramsDontMatch = true;
326
+ }
327
+ if (paramsDontMatch) {
328
+ issues.push({
329
+ path,
330
+ message: `The translation for language ${lang} must have the same parameters as the canonical path.`
331
+ });
332
+ }
333
+ }
334
+ const translatedLanguages = new Set(Object.keys(translations));
335
+ if (!isSubset(expectedLanguages, translatedLanguages)) {
336
+ const missingLanguages = new Set(expectedLanguages);
337
+ for (const lang of translatedLanguages) {
338
+ missingLanguages.delete(lang);
339
+ }
340
+ issues.push({
341
+ path,
342
+ message: `The following languages are missing translations: ${[...missingLanguages].join(
343
+ ", "
344
+ )}`
345
+ });
346
+ }
347
+ }
348
+ return issues;
349
+ }
350
+ function paramsAreEqual(param1, param2) {
351
+ return param1.chained == param2.chained && param1.matcher == param2.matcher && param1.name == param2.name && param1.optional == param2.optional && param1.rest == param2.rest;
352
+ }
353
+ function isValidPath(maybePath) {
354
+ return maybePath.startsWith("/");
355
+ }
356
+ function isSubset(a, b) {
357
+ for (const value of a) {
358
+ if (!b.has(value))
359
+ return false;
360
+ }
361
+ return true;
362
+ }
363
+ function prettyPrintIssues(issues) {
364
+ return issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n");
365
+ }
366
+ export {
367
+ bestMatch,
368
+ detectLanguageFromPath,
369
+ exec,
370
+ negotiateLanguagePreferences,
371
+ parseRouteDefinition,
372
+ prettyPrintIssues,
373
+ resolveRoute,
374
+ validatePathTranslations
375
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Negotiates which of the provided language tags is preferred from an Accept-Language header
3
+ *
4
+ * @param accept The value of the Accept-Language header. If it's missing, it defaults to "*" as per RFC 2616 sec 14.4
5
+ * @param availableLanguageTags The BCP 47 language tags that are available
6
+ *
7
+ * @returns The acceptable available language tags in descending order of preference
8
+ */
9
+ export declare function negotiateLanguagePreferences<T extends string = string>(accept: string | undefined | null, availableLanguageTags: readonly T[]): T[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Detects the language tag from the path if present
3
+ *
4
+ * If the path is not in the base path, no language will be detected
5
+ * If the language is not available, no language will be detected
6
+ *
7
+ * The language is not case sensitive, eg /de-ch/ and /DE_CH/ will both return "de-CH" if present
8
+ *
9
+ * @example
10
+ * /base/de/ueber-uns -> de
11
+ * @returns the language tag if present
12
+ *
13
+ */
14
+ export declare function detectLanguageFromPath<T extends string>({ path, availableLanguageTags, base, }: {
15
+ /** The absolute path including the base */
16
+ path: string;
17
+ availableLanguageTags: readonly T[];
18
+ /** The base path */
19
+ base?: string;
20
+ }): T | undefined;
@@ -0,0 +1,45 @@
1
+ export type PathDefinitionTranslations<T extends string = string> = {
2
+ [canonicalPath: `/${string}`]: Record<T, `/${string}`>;
3
+ };
4
+ export type RouteParam = {
5
+ name: string;
6
+ matcher: string;
7
+ optional: boolean;
8
+ rest: boolean;
9
+ chained: boolean;
10
+ };
11
+ export type ParamMatcher = (segment: string) => boolean;
12
+ /**
13
+ * Creates the regex pattern, extracts parameter names, and generates types for a route
14
+ */
15
+ export declare function parseRouteDefinition(id: string): {
16
+ params: RouteParam[];
17
+ pattern: RegExp;
18
+ };
19
+ export declare function exec(match: RegExpMatchArray, params: RouteParam[], matchers: Record<string, ParamMatcher>): Record<string, string> | undefined;
20
+ /**
21
+ * Populate a route ID with params to resolve a pathname.
22
+ * @example
23
+ * ```js
24
+ * resolveRoute(
25
+ * `/blog/[slug]/[...somethingElse]`,
26
+ * {
27
+ * slug: 'hello-world',
28
+ * somethingElse: 'something/else'
29
+ * }
30
+ * ); // `/blog/hello-world/something/else`
31
+ * ```
32
+ */
33
+ export declare function resolveRoute(id: string, params: Record<string, string | undefined>): string;
34
+ /**
35
+ * Returns the id and params for the route that best matches the given path.
36
+ * @param canonicalPath Canonical pathname excluding the base and language e.g. /foo/bar
37
+ * @param pathDefinitions An array of pathDefinitions
38
+ * @param matchers A map of param matcher functions
39
+ *
40
+ * @returns undefined if no route matches.
41
+ */
42
+ export declare function bestMatch(canonicalPath: string, pathDefinitions: string[], matchers: Record<string, ParamMatcher>): {
43
+ params: Record<string, string>;
44
+ id: string;
45
+ } | undefined;
@@ -0,0 +1,15 @@
1
+ import { ParamMatcher, PathDefinitionTranslations } from './routeDefinitions.js';
2
+
3
+ export type PathTranslationIssue = {
4
+ path: string;
5
+ message: string;
6
+ };
7
+ /**
8
+ * Check that the path translations are valid.
9
+ * Should only be called in development, this is a waste of time in production.
10
+ */
11
+ export declare function validatePathTranslations<T extends string>(pathTranslations: PathDefinitionTranslations<T>, availableLanguageTags: readonly T[], matchers: Record<string, ParamMatcher>): PathTranslationIssue[];
12
+ /**
13
+ * Formats the issues into a nice-looking string that can be logged
14
+ */
15
+ export declare function prettyPrintIssues(issues: PathTranslationIssue[]): string;
@@ -6,11 +6,11 @@
6
6
  * >> compiled === "`Hello ${params.name}`"
7
7
  */
8
8
  export declare const compilePattern: (pattern: ({
9
- type: "Text";
10
9
  value: string;
10
+ type: "Text";
11
11
  } | {
12
- type: "VariableReference";
13
12
  name: string;
13
+ type: "VariableReference";
14
14
  })[]) => {
15
15
  params: Record<string, "NonNullable<unknown>">;
16
16
  compiled: string;
package/dist/index.d.ts CHANGED
@@ -2,3 +2,7 @@ export { cli } from './cli/main.js';
2
2
  export { compile } from './compiler/compile.js';
3
3
  export { writeOutput } from './services/file-handling/write-output.js';
4
4
  export { Logger, type LoggerOptions } from './services/logger/index.js';
5
+ export type MessageIndexFunction<T extends string> = (params?: Record<string, never>, options?: {
6
+ languageTag: T;
7
+ }) => string;
8
+ export type MessageFunction = (params?: Record<string, never>) => string;
package/dist/index.js CHANGED
@@ -10051,7 +10051,7 @@ function withDefaults$2(oldDefaults, newDefaults) {
10051
10051
  });
10052
10052
  }
10053
10053
  const endpoint = withDefaults$2(null, DEFAULTS);
10054
- const VERSION$c = "8.3.1";
10054
+ const VERSION$c = "8.4.0";
10055
10055
  function isPlainObject(value2) {
10056
10056
  if (typeof value2 !== "object" || value2 === null)
10057
10057
  return false;
@@ -10194,7 +10194,7 @@ function getBufferResponse(response) {
10194
10194
  return response.arrayBuffer();
10195
10195
  }
10196
10196
  function fetchWrapper(requestOptions) {
10197
- var _a2, _b2, _c;
10197
+ var _a2, _b2, _c, _d;
10198
10198
  const log2 = requestOptions.request && requestOptions.request.log ? requestOptions.request.log : console;
10199
10199
  const parseSuccessResponseBody = ((_a2 = requestOptions.request) == null ? void 0 : _a2.parseSuccessResponseBody) !== false;
10200
10200
  if (isPlainObject(requestOptions.body) || Array.isArray(requestOptions.body)) {
@@ -10215,8 +10215,9 @@ function fetchWrapper(requestOptions) {
10215
10215
  return fetch2(requestOptions.url, {
10216
10216
  method: requestOptions.method,
10217
10217
  body: requestOptions.body,
10218
+ redirect: (_c = requestOptions.request) == null ? void 0 : _c.redirect,
10218
10219
  headers: requestOptions.headers,
10219
- signal: (_c = requestOptions.request) == null ? void 0 : _c.signal,
10220
+ signal: (_d = requestOptions.request) == null ? void 0 : _d.signal,
10220
10221
  // duplex must be set if request.body is ReadableStream or Async Iterables.
10221
10222
  // See https://fetch.spec.whatwg.org/#dom-requestinit-duplex.
10222
10223
  ...requestOptions.body && { duplex: "half" }
@@ -47083,7 +47084,7 @@ const addParaglideJsToDevDependencies = async (ctx) => {
47083
47084
  if (pkg2.devDependencies === void 0) {
47084
47085
  pkg2.devDependencies = {};
47085
47086
  }
47086
- pkg2.devDependencies["@inlang/paraglide-js"] = "1.5.0";
47087
+ pkg2.devDependencies["@inlang/paraglide-js"] = "1.6.0";
47087
47088
  await ctx.repo.nodeishFs.writeFile("./package.json", stringify2(pkg2));
47088
47089
  ctx.logger.success("Added @inlang/paraglide-js to the devDependencies in package.json.");
47089
47090
  return ctx;
@@ -47437,7 +47438,7 @@ const prompt = async (message, options) => {
47437
47438
  }
47438
47439
  return response;
47439
47440
  };
47440
- const cli = new Command().name("paraglide-js").addCommand(compileCommand).addCommand(initCommand).showHelpAfterError().version("1.5.0");
47441
+ const cli = new Command().name("paraglide-js").addCommand(compileCommand).addCommand(initCommand).showHelpAfterError().version("1.6.0");
47441
47442
  export {
47442
47443
  Logger,
47443
47444
  getBasename as a,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/paraglide-js",
3
3
  "type": "module",
4
- "version": "1.5.0",
4
+ "version": "1.6.0",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -62,14 +62,14 @@
62
62
  "vite-plugin-dts": "^3.8.1",
63
63
  "vite-tsconfig-paths": "^4.3.2",
64
64
  "vitest": "0.34.3",
65
- "@inlang/env-variables": "0.2.0",
66
65
  "@inlang/cross-sell-sherlock": "0.0.4",
66
+ "@inlang/env-variables": "0.2.0",
67
67
  "@inlang/language-tag": "1.5.1",
68
68
  "@inlang/plugin-message-format": "2.1.1",
69
- "@inlang/sdk": "0.31.0",
70
- "@inlang/telemetry": "0.3.21",
69
+ "@inlang/telemetry": "0.3.22",
71
70
  "@lix-js/client": "1.2.0",
72
- "@lix-js/fs": "1.0.0"
71
+ "@lix-js/fs": "1.0.0",
72
+ "@inlang/sdk": "0.32.0"
73
73
  },
74
74
  "exports": {
75
75
  ".": {
@@ -79,6 +79,10 @@
79
79
  "./internal": {
80
80
  "import": "./dist/index.js",
81
81
  "types": "./dist/index.d.ts"
82
+ },
83
+ "./internal/adapter-utils": {
84
+ "import": "./dist/adapter-utils/index.js",
85
+ "types": "./dist/adapter-utils/index.d.ts"
82
86
  }
83
87
  },
84
88
  "scripts": {