Skip to content

Commit

Permalink
Merge pull request #3479 from opral/parjs-433-localstorage-strategy
Browse files Browse the repository at this point in the history
add `localStorage` strategy
  • Loading branch information
samuelstroschein authored Mar 7, 2025
2 parents 85f3fbd + e329645 commit 0b1b5af
Show file tree
Hide file tree
Showing 15 changed files with 154 additions and 17 deletions.
2 changes: 2 additions & 0 deletions inlang/packages/paraglide/paraglide-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

- fix [serverMiddleware() throws when cookie contains invalid locale](https://github.com/opral/inlang-paraglide-js/issues/442)

- add `localStorage` strategy [#431](https://github.com/opral/inlang-paraglide-js/issues/431)

## 2.0.0-beta.26

- replace `node:crypto` with the Web Crypto API https://github.com/opral/inlang-paraglide-js/issues/424
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **CompilerOptions**: `object`
Defined in: [compiler-options.ts:15](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/compiler-options.ts)
Defined in: [compiler-options.ts:16](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/compiler-options.ts)

### Type declaration

Expand Down Expand Up @@ -162,6 +162,18 @@ enable tree-shaking.
typeof window === "undefined"
```

#### localStorageKey?

> `optional` **localStorageKey**: `string`
The name of the localStorage key to use for the localStorage strategy.

##### Default

```ts
'PARAGLIDE_LOCALE'
```

#### outdir

> **outdir**: `string`
Expand Down Expand Up @@ -311,6 +323,10 @@ Defined in: [compiler-options.ts:3](https://github.com/opral/monorepo/tree/main/

> `readonly` **isServer**: `"typeof window === 'undefined'"` = `"typeof window === 'undefined'"`
#### localStorageKey

> `readonly` **localStorageKey**: `"PARAGLIDE_LOCALE"` = `"PARAGLIDE_LOCALE"`
#### outputStructure

> `readonly` **outputStructure**: `"message-modules"` = `"message-modules"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Defined in: [runtime/ambient.d.ts:10](https://github.com/opral/monorepo/tree/mai

> **ParaglideAsyncLocalStorage**\<\>: `object`
Defined in: [runtime/variables.js:45](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)
Defined in: [runtime/variables.js:48](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)

### Type Parameters

Expand Down Expand Up @@ -82,15 +82,15 @@ Defined in: [runtime/variables.js:22](https://github.com/opral/monorepo/tree/mai

> `const` **experimentalMiddlewareLocaleSplitting**: `false` = `false`
Defined in: [runtime/variables.js:58](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)
Defined in: [runtime/variables.js:61](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)

***

## isServer

> `const` **isServer**: `boolean`
Defined in: [runtime/variables.js:60](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)
Defined in: [runtime/variables.js:63](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)

***

Expand All @@ -116,7 +116,7 @@ if (locales.includes(userSelectedLocale) === false) {

> **serverAsyncLocalStorage**: `undefined` \| [`ParaglideAsyncLocalStorage`](-internal-.md#paraglideasynclocalstorage) = `undefined`
Defined in: [runtime/variables.js:56](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)
Defined in: [runtime/variables.js:59](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)

Server side async local storage that is set by `serverMiddleware()`.

Expand All @@ -127,17 +127,17 @@ rendering context without effecting other requests.

## strategy

> `const` **strategy**: (`"cookie"` \| `"baseLocale"` \| `"globalVariable"` \| `"url"` \| `"preferredLanguage"`)[]
> `const` **strategy**: (`"cookie"` \| `"baseLocale"` \| `"globalVariable"` \| `"url"` \| `"preferredLanguage"` \| `"localStorage"`)[]
Defined in: [runtime/variables.js:27](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)
Defined in: [runtime/variables.js:30](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)

***

## urlPatterns

> `const` **urlPatterns**: `object`[] = `[]`
Defined in: [runtime/variables.js:34](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)
Defined in: [runtime/variables.js:37](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)

The used URL patterns.

Expand Down Expand Up @@ -322,15 +322,16 @@ The `document` object is not available in server-side rendering, so this functio

> **extractLocaleFromRequest**(`request`): `any`
Defined in: [runtime/extract-locale-from-request.js:27](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.js)
Defined in: [runtime/extract-locale-from-request.js:28](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.js)

Extracts a locale from a request.

Use the function on the server to extract the locale
from a request.

The function goes through the strategies in the order
they are defined.
they are defined. If a strategy returns an invalid locale,
it will fall back to the next strategy.

### Parameters

Expand Down Expand Up @@ -378,7 +379,7 @@ The extracted locale, or undefined if no locale is found.

> **getLocale**(): `any`
Defined in: [runtime/get-locale.js:38](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.js)
Defined in: [runtime/get-locale.js:41](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.js)

Get the current locale.

Expand Down Expand Up @@ -583,7 +584,7 @@ localizeUrl(url, { locale: "de" });

> **overwriteGetLocale**(`fn`): `void`
Defined in: [runtime/get-locale.js:127](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.js)
Defined in: [runtime/get-locale.js:136](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.js)

Overwrite the `getLocale()` function.

Expand Down Expand Up @@ -639,7 +640,7 @@ define how the URL origin is resolved.
> **overwriteServerAsyncLocalStorage**(`value`): `void`
Defined in: [runtime/variables.js:72](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)
Defined in: [runtime/variables.js:75](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js)
Sets the server side async local storage.
Expand All @@ -664,7 +665,7 @@ avoid a circular import between `runtime.js` and
> **overwriteSetLocale**(`fn`): `void`
Defined in: [runtime/set-locale.js:94](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.js)
Defined in: [runtime/set-locale.js:104](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.js)
Overwrite the `setLocale()` function.
Expand Down Expand Up @@ -696,7 +697,7 @@ overwriteSetLocale((newLocale) => {
> **setLocale**(`newLocale`): `void`
Defined in: [runtime/set-locale.js:20](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.js)
Defined in: [runtime/set-locale.js:22](https://github.com/opral/monorepo/tree/main/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.js)
Set the locale.
Expand Down
14 changes: 14 additions & 0 deletions inlang/packages/paraglide/paraglide-js/docs/strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ For example:
- If user prefers `fr-FR,fr;q=0.9,en;q=0.7` and your app supports `["en", "fr"]`, it will use `fr`
- If user prefers `en-US` and your app only supports `["en", "de"]`, it will use `en`

### localStorage

Determine the locale from the user's local storage.

<doc-callout type="warning">If you use this stragety in combination with url, make sure that a strategy like `cookie` is used as well to resolve the locale in a request. The server has no access to localStorage.</doc-callout>

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

### url

Determine the locale from the URL (pathname, domain, etc).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const defaultCompilerOptions = {
emitPrettierIgnore: true,
cleanOutdir: true,
experimentalMiddlewareLocaleSplitting: false,
localStorageKey: "PARAGLIDE_LOCALE",
isServer: "typeof window === 'undefined'",
strategy: ["cookie", "globalVariable", "baseLocale"],
cookieName: "PARAGLIDE_LOCALE",
Expand Down Expand Up @@ -69,6 +70,12 @@ export type CompilerOptions = {
* @default false
*/
experimentalMiddlewareLocaleSplitting?: boolean;
/**
* The name of the localStorage key to use for the localStorage strategy.
*
* @default 'PARAGLIDE_LOCALE'
*/
localStorageKey?: string;
/**
* Tree-shaking flag if the code is running on the server.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function generateLocaleModules(
strategy: NonNullable<CompilerOptions["strategy"]>;
cookieName: NonNullable<CompilerOptions["cookieName"]>;
isServer: NonNullable<CompilerOptions["isServer"]>;
localStorageKey: NonNullable<CompilerOptions["localStorageKey"]>;
experimentalMiddlewareLocaleSplitting: NonNullable<
CompilerOptions["experimentalMiddlewareLocaleSplitting"]
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function generateMessageModules(
strategy: NonNullable<CompilerOptions["strategy"]>;
cookieName: NonNullable<CompilerOptions["cookieName"]>;
isServer: NonNullable<CompilerOptions["isServer"]>;
localStorageKey: NonNullable<CompilerOptions["localStorageKey"]>;
experimentalMiddlewareLocaleSplitting: NonNullable<
CompilerOptions["experimentalMiddlewareLocaleSplitting"]
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function createRuntimeFile(args: {
urlPatterns?: CompilerOptions["urlPatterns"];
experimentalMiddlewareLocaleSplitting: CompilerOptions["experimentalMiddlewareLocaleSplitting"];
isServer: CompilerOptions["isServer"];
localStorageKey: CompilerOptions["localStorageKey"];
};
}): string {
const urlPatterns = args.compilerOptions.urlPatterns ?? [];
Expand Down Expand Up @@ -87,6 +88,14 @@ ${injectCode("./variables.js")
.replace(
`export const isServer = typeof window === "undefined";`,
`export const isServer = ${args.compilerOptions.isServer};`
)
.replace(
`export const localStorageKey = "PARAGLIDE_LOCALE";`,
`export const localStorageKey = "${args.compilerOptions.localStorageKey}";`
)
.replace(
`export const TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED = false;`,
`const TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED = ${args.compilerOptions.strategy.includes("localStorage")};`
)}
globalThis.__paraglide = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const extractLocaleFromRequest = (request) => {
locale = _locale;
} else if (strat === "baseLocale") {
return baseLocale;
} else if (strat === "localStorage") {
continue;
} else {
throw new Error(`Unsupported strategy: ${strat}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,19 @@ test("should fall back to next strategy when cookie contains invalid locale", as
},
});
expect(runtime.extractLocaleFromRequest(request2)).toBe("en");
});

test("skips over localStorage strategy as it is not supported on the server", async () => {
const runtime = await createRuntimeForTesting({
baseLocale: "en",
locales: ["en"],
compilerOptions: {
strategy: ["localStorage", "baseLocale"],
},
});

const request = new Request("http://example.com");

// expecting baseLocale
expect(runtime.extractLocaleFromRequest(request)).toBe("en");
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED,
TREE_SHAKE_PREFERRED_LANGUAGE_STRATEGY_USED,
TREE_SHAKE_URL_STRATEGY_USED,
TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED,
localStorageKey,
isServer,
} from "./variables.js";
import { extractLocaleFromCookie } from "./extract-locale-from-cookie.js";
import { extractLocaleFromUrl } from "./extract-locale-from-url.js";
Expand Down Expand Up @@ -71,6 +74,12 @@ export let getLocale = () => {
typeof window !== "undefined"
) {
locale = negotiatePreferredLanguageFromNavigator();
} else if (
TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED &&
strat === "localStorage" &&
!isServer
) {
locale = localStorage.getItem(localStorageKey) ?? undefined;
}
// check if match, else continue loop
if (locale !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from "vitest";
import { test, expect, vi } from "vitest";
import { createRuntimeForTesting } from "./create-runtime.js";

test("matching by strategy works", async () => {
Expand Down Expand Up @@ -137,3 +137,23 @@ test("returns the preferred locale from navigator.languages", async () => {
configurable: true,
});
});

test("returns the locale from local storage", async () => {
const runtime = await createRuntimeForTesting({
baseLocale: "en",
locales: ["en", "de"],
compilerOptions: {
strategy: ["localStorage"],
localStorageKey: "PARAGLIDE_LOCALE",
isServer: "false",
},
});

// @ts-expect-error - global variable definition
globalThis.localStorage = {
setItem: vi.fn(),
getItem: vi.fn(() => "de"),
};

expect(runtime.getLocale()).toBe("de");
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
cookieName,
isServer,
localStorageKey,
strategy,
TREE_SHAKE_COOKIE_STRATEGY_USED,
TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED,
TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED,
TREE_SHAKE_URL_STRATEGY_USED,
} from "./variables.js";
import { localizeUrl } from "./localize-url.js";
Expand Down Expand Up @@ -63,6 +65,14 @@ export let setLocale = (newLocale) => {
}).href;
// just in case return. the browser reloads the page by setting href
return;
} else if (
TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED &&
strat === "localStorage" &&
!isServer
) {
// set the localStorage
localStorage.setItem(localStorageKey, newLocale);
localeHasBeenSet = true;
}
}
if (localeHasBeenSet === false) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,29 @@ test("should not reload when setting locale to current locale", async () => {
expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=de; path=/");
expect(globalThis.window.location.reload).toBeCalled();
});

test("sets the locale to localStorage", async () => {
// @ts-expect-error - global variable definition
globalThis.localStorage = {
setItem: vi.fn(),
getItem: () => "en",
};

// @ts-expect-error - global variable definition
globalThis.window = {};

const runtime = await createRuntimeForTesting({
baseLocale: "en",
locales: ["en", "de"],
compilerOptions: {
strategy: ["localStorage"],
},
});

runtime.setLocale("de");

expect(globalThis.localStorage.setItem).toHaveBeenCalledWith(
"PARAGLIDE_LOCALE",
"de"
);
});
Loading

0 comments on commit 0b1b5af

Please sign in to comment.