Skip to content

feat: enhance locale handling with AsyncLocalStorage support for server-side requests #7826

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

Open
wants to merge 6 commits into
base: build/v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/pretty-parents-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@qwik.dev/router': patch
'@qwik.dev/core': patch
---

enhance locale handling with AsyncLocalStorage support for server-side requests
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './request-event';
import { encoder } from './resolve-request-handlers';
import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types';
import { withLocale } from '@qwik.dev/core';
// Import separately to avoid duplicate imports in the vite dev server
import {
AbortMessage,
Expand Down Expand Up @@ -63,9 +64,12 @@ export function runQwikRouter<T>(
return {
response: responsePromise,
requestEv,
completion: asyncStore
? asyncStore.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!)
: runNext(requestEv, rebuildRouteInfo, resolve!),
completion: withLocale(
requestEv.locale(),
asyncStore
? () => asyncStore!.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!)
: () => runNext(requestEv, rebuildRouteInfo, resolve!)
),
};
}

Expand Down
7 changes: 5 additions & 2 deletions packages/qwik-router/src/runtime/src/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ export const resolveHead = (
}
return data;
}) as any as ResolveSyncValue;
const storeEv = (globalThis as any).qcAsyncRequestStore;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you rename this to hasAsyncStore and add a comment // Qwik Core will also be using the async store if this is present

Actually, it would be better to export the store from where it is made and to import it.

instead of globalThis, make it export let asyncRequestStore and then in the other places it's used just import it. That way, we can be sure it is initialized correctly, and we don't pollute globalThis.

const headProps: DocumentHeadProps = {
head,
withLocale: (fn) => withLocale(locale, fn),
withLocale: storeEv ? (fn) => fn() : (fn) => withLocale(locale, fn),
resolveValue: getData,
...routeLocation,
};
Expand All @@ -50,7 +51,9 @@ export const resolveHead = (
if (typeof contentModuleHead === 'function') {
resolveDocumentHead(
head,
withLocale(locale, () => contentModuleHead(headProps))
storeEv
? contentModuleHead(headProps)
: withLocale(locale, () => contentModuleHead(headProps))
);
} else if (typeof contentModuleHead === 'object') {
resolveDocumentHead(head, contentModuleHead);
Expand Down
47 changes: 47 additions & 0 deletions packages/qwik/src/core/use/use-locale.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { tryGetInvokeContext } from './use-core';
import { isServer } from '@qwik.dev/core/build';

let _locale: string | undefined = undefined;

type LocaleStore = { locale: string | undefined };

type LocaleAsyncStore = import('node:async_hooks').AsyncLocalStorage<LocaleStore>;

let localAsyncStore: LocaleAsyncStore | undefined;
Comment on lines +6 to +10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import type {AsyncLocalStorage} from 'node:async_hooks'

let asyncStore: AsyncLocalStorage<{locale?: string}> | undefined


if (isServer) {
import('node:async_hooks')
.then((module) => {
const AsyncLocalStorage = module.AsyncLocalStorage as unknown as new () => LocaleAsyncStore;
localAsyncStore = new AsyncLocalStorage();
})
.catch(() => {
// ignore if AsyncLocalStorage is not available
});
}

/**
* Retrieve the current locale.
*
Expand All @@ -11,6 +29,16 @@ let _locale: string | undefined = undefined;
* @public
*/
export function getLocale(defaultLocale?: string): string {
// Prefer per-request locale from local AsyncLocalStorage if available (server-side)
try {
const locale = localAsyncStore?.getStore?.()?.locale;
if (locale) {
return locale;
}
} catch {
// ignore and fallback
}

Comment on lines +33 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (localAsyncStore) {
  const locale = localAsyncStore.getStore()?.locale
  if (locale) {
    return locale
  }
}

don't add try/catch and conditionals when not needed, it has a runtime cost.

if (_locale === undefined) {
const ctx = tryGetInvokeContext();
if (ctx && ctx.$locale$) {
Expand All @@ -30,6 +58,15 @@ export function getLocale(defaultLocale?: string): string {
* @public
*/
export function withLocale<T>(locale: string, fn: () => T): T {
// If running on the server with AsyncLocalStorage, set locale for this async context
try {
if (localAsyncStore?.run) {
return localAsyncStore.run({ locale }, fn);
}
} catch {
// ignore and fallback
}
Comment on lines +62 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (localAsyncStore) {
  return localAsyncStore.run({ locale }, fn);
}


const previousLang = _locale;
try {
_locale = locale;
Expand All @@ -48,5 +85,15 @@ export function withLocale<T>(locale: string, fn: () => T): T {
* @public
*/
export function setLocale(locale: string): void {
// On the server, prefer setting the locale on the local per-request store
try {
const store = localAsyncStore?.getStore?.();
if (store) {
store.locale = locale;
return;
}
} catch {
// ignore and fallback
}
Comment on lines +88 to +97
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

_locale = locale;
}
Loading