Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parjs-361-localized-pathname-api #3383

Merged
merged 28 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
25dbe24
work in progress
samuelstroschein Feb 1, 2025
bdddfa0
half working de-localize path function
samuelstroschein Feb 3, 2025
804c83e
replace wildcards
samuelstroschein Feb 4, 2025
e3f3e03
wildcard working
samuelstroschein Feb 4, 2025
1f9bf2e
extract match compile pathname
samuelstroschein Feb 4, 2025
436e2d5
improve type
samuelstroschein Feb 4, 2025
797ed08
align api with path-to-regexp
samuelstroschein Feb 5, 2025
8b8d939
broken matching function
samuelstroschein Feb 5, 2025
021f4e3
fix optional wildcards
samuelstroschein Feb 5, 2025
50f3d51
add more tests
samuelstroschein Feb 5, 2025
53303b5
try re-export path-to-regexp
samuelstroschein Feb 5, 2025
c96eb2c
implement matching with path to regexp
samuelstroschein Feb 7, 2025
e481d6b
remove whitespace
samuelstroschein Feb 7, 2025
78c934a
add handle query parameters
samuelstroschein Feb 7, 2025
d785c52
fix: only export what is needed to avoid bundler issues
samuelstroschein Feb 7, 2025
eb6f1dd
fix matcher
samuelstroschein Feb 7, 2025
048bde3
fix: extract locale from pathname
samuelstroschein Feb 8, 2025
c9a3309
update test
samuelstroschein Feb 8, 2025
03b8bad
remove unused variables
samuelstroschein Feb 8, 2025
faeff2d
tree-shake default pathnames
samuelstroschein Feb 8, 2025
407f332
tree-shake unused strategies
samuelstroschein Feb 8, 2025
c0608de
fix: switching of locale
samuelstroschein Feb 8, 2025
05c1884
better message
samuelstroschein Feb 8, 2025
8bce0db
formatting
samuelstroschein Feb 8, 2025
3ba446e
fix test types
samuelstroschein Feb 8, 2025
daec298
update package json
samuelstroschein Feb 8, 2025
9c64bbb
add docs for strategy
samuelstroschein Feb 8, 2025
2b0bd44
rename `variable` strategy to `globalVariable` to increase clarity
samuelstroschein Feb 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 128 additions & 4 deletions inlang/packages/paraglide/paraglide-js/docs/strategy.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,132 @@
# Locale strategy

Paraglide JS comes with various strategies to determine the locale out of the box.

The strategy is defined with the `strategy` option. The order of the strategies in the array defines the priority. The first strategy that returns a locale will be used.

In the example below, the locale is first determined by the `cookie` strategy. If no cookie is found, the `baseLocale` is used.

```diff
compile({
project: "./project.inlang",
outdir: "./src/paraglide",
+ strategy: ["cookie", "baseLocale"]
})
```

## Built-in strategies

### cookie

The cookie strategy determines the locale from a cookie.

```diff
compile({
project: "./project.inlang",
outdir: "./src/paraglide",
+ strategy: ["cookie"]
})
```

### baseLocale

Returns the `baseLocale` defined in the settings.

Useful as fallback if no other strategy returned a locale. If a cookie has not been set yet, for example.

```diff
compile({
project: "./project.inlang",
outdir: "./src/paraglide",
+ strategy: ["cookie", "baseLocale"]
})
```

### globalVariable

Uses a global variable to determine the locale.

This strategy is only useful in testing environments, or to get started quickly. Setting a global variable can lead to cross request issues in server-side environments and the locale is not persisted between page reloads in client-side environments.

```diff
compile({
project: "./project.inlang",
outdir: "./src/paraglide",
+ strategy: ["globalVariable"]
})
```

### pathname

The pathname strategy determines the locale from the pathname.

For example, if the pathname is `/en-US/about`, the locale will be `en-US`. You can adjust the pathnames with the `pathnames` option. The syntax uses [path-to-regexp](https://github.com/pillarjs/path-to-regexp).

<doc-callout type="info">If you use wildcards (*), be aware of the matching order. Use wildcards after static and parameterized paths.</doc-callout>

```diff
compile({
project: "./project.inlang",
outdir: "./src/paraglide",
+ strategy: ["pathname"]
+ pathnames: {
// define static paths
"/about": {
en: "/about",
de: "/ueber-uns",
},
// parameterized paths
"/shop/:id": {
en: "/shop/:id",
de: "/einkaufen/:id",
}
// wildcard paths
"/{*path}": {
de: "/de{/*path}",
en: "/{*path}",
},
})
```

Pathnames default to prefixing every locale other than the base locale in the path. If you want another behaviour, you can define it with the `pathnames` option.

#### Prefix every locale

```
/en/about
/de/about
```

```json
{
"/{*path}": {
"de": "/de{/*path}",
"en": "/en{/*path}"
}
}
```

#### Aliases for locales

```
/deutsch/about
/english/about
```

```json
{
"/{*path}": {
"de": "/deutsch{/*path}",
"en": "/english{/*path}"
}
}
```

## Write your own strategy

Write your own cookie, http header, or i18n routing based locale strategy to integrate Paraglide into any framework or app.

## Basics
### Basics

Every time a message is rendered, Paraglide calls the `getLocale()` function under the hood to determine which locale to apply. By default, this will be the `baseLocale` defined in your settings. Calling `setLocale(locale)` anywhere in your code will update the locale stored by the runtime. Any calls to `getLocale()` after that (eg: when a new message is rendered) will return the newly set locale.

Expand Down Expand Up @@ -159,9 +283,9 @@ defineSetLocale((newLocale) => {
{/key}
```

## Examples
### Examples

### Cookie based strategy
#### Cookie based strategy

The example uses React for demonstration purposes. You can replicate the same pattern in any other framework or vanilla JS.

Expand Down Expand Up @@ -199,7 +323,7 @@ function App() {
}
```

### Server-side rendering
#### Server-side rendering

1. Detect the locale from the request.
2. Make sure that `defineGetLocale()` is cross-request safe on the server.
Expand Down
14 changes: 9 additions & 5 deletions inlang/packages/paraglide/paraglide-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
"./dist",
"./bin"
],
"exports": {
".": "./dist/index.js",
"./path-to-regexp": "./dist/path-to-regexp/index.js"
},
"_comment": "Required for tree-shaking https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free",
"sideEffects": false,
"scripts": {
"dev": "tsc --watch",
"bench": "vitest bench --run",
Expand All @@ -37,6 +43,7 @@
"commander": "11.1.0",
"consola": "3.4.0",
"json5": "2.2.3",
"path-to-regexp": "^8.2.0",
"unplugin": "^2.1.2"
},
"devDependencies": {
Expand All @@ -51,13 +58,10 @@
"memfs": "4.17.0",
"prettier": "^3.4.2",
"rolldown": "1.0.0-beta.1",
"typescript-eslint": "^8.20.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0",
"vitest": "2.1.8"
},
"exports": {
".": "./dist/index.js"
},
"keywords": [
"inlang",
"paraglide",
Expand All @@ -81,4 +85,4 @@
"vite-plugin",
"rollup-plugin"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const unpluginFactory: UnpluginFactory<CompilerOptions> = (args) => ({
const shouldCompile = readFiles.has(path) && !path.includes("cache");
if (shouldCompile) {
readFiles.clear();
logger.info("Re-compiling inlang project...");
logger.info(`Re-compiling inlang project... File "${path}" has changed.`);
compilationResult = await compile(
{
fs: wrappedFs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ describe.each([
// useTsImports must be true to test emitTs. Otherwise, rolldown can't resolve the imports
{
outputStructure: "locale-modules",
strategy: ["variable", "baseLocale"],
strategy: ["globalVariable", "baseLocale"],
},
{
outputStructure: "message-modules",
strategy: ["variable", "baseLocale"],
strategy: ["globalVariable", "baseLocale"],
},
] satisfies Array<Parameters<typeof compileProject>["0"]["compilerOptions"]>)(
"options",
Expand All @@ -110,7 +110,7 @@ describe.each([
output,
`import * as m from "./paraglide/messages.js"

console.log(m.sad_penguin_bundle())`
console.log(m.sad_penguin_bundle())`
);
const log = vi.spyOn(console, "log").mockImplementation(() => {});
// all required code for the message to be rendered is included like sourceLanguageTag.
Expand Down Expand Up @@ -404,63 +404,6 @@ describe.each([
strict: true,
};

// remove with v3 of paraglide js
test("./runtime.js types", async () => {
const project = await typescriptProject({
useInMemoryFileSystem: true,
compilerOptions: superStrictRuleOutAnyErrorTsSettings,
});

for (const [fileName, code] of Object.entries(output)) {
if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
project.createSourceFile(fileName, code);
}
}
project.createSourceFile(
"test.ts",
`
import * as runtime from "./runtime.js"

// --------- RUNTIME ---------

// getLocale() should return type should be a union of language tags, not a generic string
runtime.getLocale() satisfies "de" | "en" | "en-US"

// locales should have a narrow type, not a generic string
runtime.locales satisfies Readonly<Array<"de" | "en" | "en-US">>

// setLocale() should fail if the given language tag is not included in locales
// @ts-expect-error - invalid locale
runtime.setLocale("fr")

// setLocale() should not fail if the given language tag is included in locales
runtime.setLocale("de")

// isLocale should narrow the type of it's argument
const thing = 5;

let a: "de" | "en" | "en-US";

if(runtime.isLocale(thing)) {
a = thing
} else {
// @ts-expect-error - thing is not a language tag
a = thing
}

// to make ts not complain about unused variables
console.log(a)
`
);

const program = project.createProgram();
const diagnostics = ts.getPreEmitDiagnostics(program);
for (const diagnostic of diagnostics) {
console.error(diagnostic.messageText, diagnostic.file?.fileName);
}
expect(diagnostics.length).toEqual(0);
});

test("./messages.js types", async () => {
const project = await typescriptProject({
useInMemoryFileSystem: true,
Expand Down Expand Up @@ -505,7 +448,10 @@ describe.each([
);

const program = project.createProgram();
const diagnostics = ts.getPreEmitDiagnostics(program);
const diagnostics = ts
.getPreEmitDiagnostics(program)
// runtime type here makes issues because of the path-to-regexp import
.filter((d) => !d.file?.fileName.includes("runtime.js"));
for (const diagnostic of diagnostics) {
console.error(diagnostic.messageText, diagnostic.file?.fileName);
}
Expand All @@ -515,6 +461,11 @@ describe.each([
);

async function bundleCode(output: Record<string, string>, file: string) {
output["runtime.js"] = output["runtime.js"]!.replace(
'import * as pathToRegexp from "@inlang/paraglide-js/path-to-regexp";',
"/** @type {any} */const pathToRegexp = {};"
);

const bundle = await rolldown({
input: ["main.js"],
plugins: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,14 @@ test("multiple compile calls do not interfere with each other", async () => {

const runtime = (await import(
"data:text/javascript;base64," +
Buffer.from(runtimeFile, "utf-8").toString("base64")
Buffer.from(
// replace the
runtimeFile.replace(
`import * as pathToRegexp from "@inlang/paraglide-js/path-to-regexp";`,
""
),
"utf-8"
).toString("base64")
)) as Runtime;

// expecting the second compile step to overwrite the first
Expand Down Expand Up @@ -286,6 +293,6 @@ test("default compiler options should include variable and baseLocale to ensure
// paraglide js is the right tool for them, they can then define their own strategy.

expect(defaultCompilerOptions.strategy).toEqual(
expect.arrayContaining(["variable", "baseLocale"])
expect.arrayContaining(["globalVariable", "baseLocale"])
);
});
36 changes: 21 additions & 15 deletions inlang/packages/paraglide/paraglide-js/src/compiler/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ export const defaultCompilerOptions = {
emitGitIgnore: true,
includeEslintDisableComment: true,
emitPrettierIgnore: true,
strategy: ["cookie", "variable", "baseLocale"],
strategy: ["cookie", "globalVariable", "baseLocale"],
cookieName: "PARAGLIDE_LOCALE",
pathnamePrefixDefaultLocale: false,
} as const satisfies Partial<CompilerOptions>;

export type CompilerOptions = {
Expand Down Expand Up @@ -71,6 +70,26 @@ export type CompilerOptions = {
* @default true
*/
emitPrettierIgnore?: boolean;
/**
* The localized pathname patterns.
*
* The syntax is based on path-to-regexp v8
* https://github.com/pillarjs/path-to-regexp.
*
* @example
* pathnames: {
* "/about": {
* "en": "/about",
* "de": "/ueber-uns",
* },
* // catch all
* "/{*path}": {
* "de": "/de/{*path}",
* "en": "/{*path}",
* }
* }
*/
pathnames?: Record<string, Record<string, string>>;
/**
* Whether to include an eslint-disable comment at the top of each .js file.
*
Expand All @@ -83,19 +102,6 @@ export type CompilerOptions = {
* @default true
*/
emitGitIgnore?: boolean;
/**
* Whether to prefix the default locale to the pathname.
*
* For incremental adoption reasons, the base locale is not
* prefixed by default. A path like `/page` will be served by
* is matched as base locale.
*
* Setting the option to `true` will prefix the default locale to the
* pathname. `/page` will become `/en/page` (if the base locale is en).
*
* @default false
*/
pathnamePrefixDefaultLocale?: boolean;
/**
* The file-structure of the compiled output.
*
Expand Down
Loading