From e329645c35bd6f61efd59e23d0aaa8ad30037c3f Mon Sep 17 00:00:00 2001 From: Samuel Stroschein <35429197+samuelstroschein@users.noreply.github.com> Date: Fri, 7 Mar 2025 11:12:35 -0800 Subject: [PATCH] add `localStorage` strategy closes https://github.com/opral/inlang-paraglide-js/issues/431 --- .../paraglide/paraglide-js/CHANGELOG.md | 2 ++ .../paraglide-js/docs-api/compiler-options.md | 18 +++++++++++- .../docs-api/runtime/-internal-.md | 29 ++++++++++--------- .../paraglide/paraglide-js/docs/strategy.md | 14 +++++++++ .../src/compiler/compiler-options.ts | 7 +++++ .../output-structure/locale-modules.ts | 1 + .../output-structure/message-modules.ts | 1 + .../src/compiler/runtime/create-runtime.ts | 9 ++++++ .../runtime/extract-locale-from-request.js | 2 ++ .../extract-locale-from-request.test.ts | 14 +++++++++ .../src/compiler/runtime/get-locale.js | 9 ++++++ .../src/compiler/runtime/get-locale.test.ts | 22 +++++++++++++- .../src/compiler/runtime/set-locale.js | 10 +++++++ .../src/compiler/runtime/set-locale.test.ts | 26 +++++++++++++++++ .../src/compiler/runtime/variables.js | 7 ++++- 15 files changed, 154 insertions(+), 17 deletions(-) diff --git a/inlang/packages/paraglide/paraglide-js/CHANGELOG.md b/inlang/packages/paraglide/paraglide-js/CHANGELOG.md index 0209da81e1..bf5f8ebb7a 100644 --- a/inlang/packages/paraglide/paraglide-js/CHANGELOG.md +++ b/inlang/packages/paraglide/paraglide-js/CHANGELOG.md @@ -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 diff --git a/inlang/packages/paraglide/paraglide-js/docs-api/compiler-options.md b/inlang/packages/paraglide/paraglide-js/docs-api/compiler-options.md index 38a04a09f8..29ec4fe235 100644 --- a/inlang/packages/paraglide/paraglide-js/docs-api/compiler-options.md +++ b/inlang/packages/paraglide/paraglide-js/docs-api/compiler-options.md @@ -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 @@ -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` @@ -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"` diff --git a/inlang/packages/paraglide/paraglide-js/docs-api/runtime/-internal-.md b/inlang/packages/paraglide/paraglide-js/docs-api/runtime/-internal-.md index 411a677ba5..09583ef43a 100644 --- a/inlang/packages/paraglide/paraglide-js/docs-api/runtime/-internal-.md +++ b/inlang/packages/paraglide/paraglide-js/docs-api/runtime/-internal-.md @@ -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 @@ -82,7 +82,7 @@ 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) *** @@ -90,7 +90,7 @@ Defined in: [runtime/variables.js:58](https://github.com/opral/monorepo/tree/mai > `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) *** @@ -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()`. @@ -127,9 +127,9 @@ 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) *** @@ -137,7 +137,7 @@ Defined in: [runtime/variables.js:27](https://github.com/opral/monorepo/tree/mai > `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. @@ -322,7 +322,7 @@ 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. @@ -330,7 +330,8 @@ 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 @@ -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. @@ -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. @@ -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. @@ -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. @@ -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. diff --git a/inlang/packages/paraglide/paraglide-js/docs/strategy.md b/inlang/packages/paraglide/paraglide-js/docs/strategy.md index 12b9b923d1..029a4a9284 100644 --- a/inlang/packages/paraglide/paraglide-js/docs/strategy.md +++ b/inlang/packages/paraglide/paraglide-js/docs/strategy.md @@ -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. + +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. + +```diff +compile({ + project: "./project.inlang", + outdir: "./src/paraglide", ++ strategy: ["localStorage"] +}) +``` + ### url Determine the locale from the URL (pathname, domain, etc). diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/compiler-options.ts b/inlang/packages/paraglide/paraglide-js/src/compiler/compiler-options.ts index 47502943c3..4ffe4139fa 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/compiler-options.ts +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/compiler-options.ts @@ -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", @@ -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. * diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/output-structure/locale-modules.ts b/inlang/packages/paraglide/paraglide-js/src/compiler/output-structure/locale-modules.ts index 3b437ff11a..fc4937fe5b 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/output-structure/locale-modules.ts +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/output-structure/locale-modules.ts @@ -14,6 +14,7 @@ export function generateLocaleModules( strategy: NonNullable; cookieName: NonNullable; isServer: NonNullable; + localStorageKey: NonNullable; experimentalMiddlewareLocaleSplitting: NonNullable< CompilerOptions["experimentalMiddlewareLocaleSplitting"] >; diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/output-structure/message-modules.ts b/inlang/packages/paraglide/paraglide-js/src/compiler/output-structure/message-modules.ts index 4245c59f9e..c7a610f920 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/output-structure/message-modules.ts +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/output-structure/message-modules.ts @@ -15,6 +15,7 @@ export function generateMessageModules( strategy: NonNullable; cookieName: NonNullable; isServer: NonNullable; + localStorageKey: NonNullable; experimentalMiddlewareLocaleSplitting: NonNullable< CompilerOptions["experimentalMiddlewareLocaleSplitting"] >; diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/create-runtime.ts b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/create-runtime.ts index 1a17399490..7c50def81c 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/create-runtime.ts +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/create-runtime.ts @@ -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 ?? []; @@ -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 = {} diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.js b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.js index 2ded48e216..0eeab1f5e7 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.js +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.js @@ -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}`); } diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.test.ts b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.test.ts index c842b6c820..67e22905fa 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.test.ts +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/extract-locale-from-request.test.ts @@ -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"); }); diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.js b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.js index 0333108e2a..27a1c78d36 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.js +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.js @@ -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"; @@ -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) { diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.test.ts b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.test.ts index 07760b8f36..084b780fd8 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.test.ts +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/get-locale.test.ts @@ -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 () => { @@ -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"); +}); diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.js b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.js index 46ffb4728f..b954d5bf05 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.js +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.js @@ -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"; @@ -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) { diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.test.ts b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.test.ts index b3b60b577a..80b44b62af 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.test.ts +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/set-locale.test.ts @@ -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" + ); +}); diff --git a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js index e152060bb1..9cd63e6eab 100644 --- a/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js +++ b/inlang/packages/paraglide/paraglide-js/src/compiler/runtime/variables.js @@ -21,8 +21,11 @@ export const locales = /** @type {const} */ (["en", "de"]); /** @type {string} */ export const cookieName = ""; +/** @type {string} */ +export const localStorageKey = "PARAGLIDE_LOCALE"; + /** - * @type {Array<"cookie" | "baseLocale" | "globalVariable" | "url" | "preferredLanguage">} + * @type {Array<"cookie" | "baseLocale" | "globalVariable" | "url" | "preferredLanguage" | "localStorage">} */ export const strategy = ["globalVariable"]; @@ -82,3 +85,5 @@ export const TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED = false; export const TREE_SHAKE_PREFERRED_LANGUAGE_STRATEGY_USED = false; export const TREE_SHAKE_DEFAULT_URL_PATTERN_USED = false; + +export const TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED = false;