Skip to content

Commit f5e1ea9

Browse files
committed
Move localization context to AsyncLocalStorage
This makes a lot of things easier, including async contexts,[^1] `c.render()` calls,[^2] etc. With this, the `asFC` helper function is no longer necessary. Closes #5 [^1]: Cf. honojs/hono#3812 for details on why async contexts were hard with `useRequestContext()` [^2]: Cf. https://jsr.io/@wuespace/honolate@0.1.0/doc/~/asFC for why this was more difficult, before.
1 parent 26fa374 commit f5e1ea9

8 files changed

Lines changed: 50 additions & 64 deletions

File tree

example/main.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Hono } from "@hono/hono";
2-
import { jsxRenderer, useRequestContext } from "@hono/hono/jsx-renderer";
2+
import { jsxRenderer } from "@hono/hono/jsx-renderer";
33
// import { languageDetector } from '@hono/hono/language';
4-
import { asFC, t } from "@wuespace/honolate";
4+
import { t } from "@wuespace/honolate";
55
import { withI18n } from "./i18n.ts";
66

77
const app = new Hono();
@@ -18,14 +18,12 @@ app.use(
1818

1919
app.get("/", (c) => {
2020
return c.render(
21-
asFC(() => (
22-
<div>
23-
{t`Welcome to the {{}}}}{{ {0} {1} {{0}} {{1}} homepage ${useRequestContext().req.url}!`}
24-
{t`Hello world!`}
25-
{t`Test: ${<code>{t`Hello!`}</code>}`}
26-
<LocalePrinter />
27-
</div>
28-
)),
21+
<div>
22+
{t`Welcome to the {{}}}}{{ {0} {1} {{0}} {{1}} homepage ${3}!`}
23+
{t`Hello world!`}
24+
{t`Test: ${<code>{t`Hello!`}</code>}`}
25+
<LocalePrinter />
26+
</div>,
2927
);
3028
});
3129

lib/hono/LazyLocalizedString.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { LocalizedStringValue } from "./LocalizedStringValue.ts";
99
*/
1010
export type LazyLocalizedString = {
1111
/**
12-
* The localization key used to look up the localized string.
12+
* The escaped localization key used to look up the localized string.
1313
*/
1414
localizationKey: string;
1515
/**

lib/hono/asFC.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { FC } from "@hono/hono/jsx";
22

33
/**
4+
* @deprecated This is no longer necessary as of Honolate v0.2.0, since `t` can now be used directly in route handlers.
5+
* Previously, `t` used the request context, which was only available inside functional components.
6+
* Now, `t` uses AsyncLocalStorage to access the localization context, making it usable directly in route handlers.
7+
*
8+
* @remarks
49
* Converts a JSX element into a functional component.
510
*
611
* This is necessary to add support for hooks like `useRequestContext` within the element,

lib/hono/ensureLazyLocalizedString.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { escapeKey } from "../common/escapeKey.ts";
12
import type { LazyLocalizedString } from "./LazyLocalizedString.ts";
23
import type { LocalizedStringValue } from "./LocalizedStringValue.ts";
34
import { lt } from "./lt.ts";
@@ -10,11 +11,13 @@ import { lt } from "./lt.ts";
1011
* @returns the input as a LazyLocalizedString
1112
*/
1213
export function ensureLazyLocalizedString(
13-
input: TemplateStringsArray | LazyLocalizedString,
14-
values: LocalizedStringValue[],
14+
input: TemplateStringsArray | LazyLocalizedString | string,
15+
values?: LocalizedStringValue[],
1516
): LazyLocalizedString {
16-
if (Array.isArray(input)) {
17-
return lt(input as TemplateStringsArray, ...values);
17+
if (typeof input === "string") {
18+
return { localizationKey: escapeKey(input), values: [] };
19+
} else if (Array.isArray(input)) {
20+
return lt(input as TemplateStringsArray, ...(values ?? []));
1821
} else {
1922
return input as LazyLocalizedString;
2023
}

lib/hono/getLocalizationMap.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,18 @@
1-
import { useRequestContext } from "@hono/hono/jsx-renderer";
2-
import { neverThrow } from "../common/neverThrow.ts";
1+
import { currentLocalizedValuesStorage } from "./initHonolate.ts";
32

43
/**
54
* @returns the current locale's localization map, mapping localization keys to their respective translations
65
*/
76
export function getLocalizationMap(): Record<string, string> {
8-
const ctx = neverThrow(() => useRequestContext());
9-
if (ctx instanceof Error) {
7+
const localizationMap = currentLocalizedValuesStorage.getStore();
8+
if (!localizationMap) {
109
console.warn(
1110
new Error(
12-
"t was called outside of a request context. Using default locale 'default'." +
13-
"\nMake sure to wrap t calls within a Hono JSX Renderer context." +
14-
"\nIf you're not in a functional component, use asFC() to wrap the parameter of c.render():" +
15-
"\nc.render(asFC(() => <>{t`...`}</>))" +
16-
"\ninstead of c.render(<>{t`...`}</>)",
17-
{ cause: ctx },
11+
"t was called outside of a request context." +
12+
"\nMake sure to only call t within routes that use the middleware returned by initHonolate.",
1813
),
1914
);
2015
return {};
2116
}
22-
23-
const map = ctx.get("localizedValues");
24-
if (!map || typeof map !== "object") {
25-
console.warn(
26-
new Error(
27-
"Localized values not found in request context. Using empty localization map." +
28-
"\nMake sure to update the localization files and initialize Honolate middleware correctly.",
29-
{ cause: ctx },
30-
),
31-
);
32-
return {};
33-
}
34-
return map as Record<string, string>;
17+
return localizationMap;
3518
}

lib/hono/initHonolate.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import type { HonolateContext } from "./HonolateContext.ts";
55
import type { InitHonolateOptions } from "./InitHonolateOptions.ts";
66
import type { LocalizedValue } from "./LocalizedValue.ts";
77
import { ensureRequestLanguage } from "./ensureRequestLanguage.ts";
8+
import { AsyncLocalStorage } from "node:async_hooks";
9+
10+
export const currentLocaleStorage = new AsyncLocalStorage<string>();
11+
export const currentLocalizedValuesStorage = new AsyncLocalStorage<
12+
Record<
13+
string,
14+
LocalizedValue
15+
>
16+
>();
817

918
/**
1019
* Initializes Honolate with the given options.
@@ -54,7 +63,14 @@ export const initHonolate: <T extends string>(
5463

5564
c.set("localizedValues", languages.get(language) || {});
5665

57-
return next();
66+
return currentLocaleStorage.run(
67+
language,
68+
() =>
69+
currentLocalizedValuesStorage.run(
70+
c.get("localizedValues") || {},
71+
next,
72+
),
73+
);
5874
});
5975
};
6076

lib/hono/t.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,6 @@ export function t(
5656
string: TemplateStringsArray | LazyLocalizedString | string,
5757
...values: LocalizedStringValue[]
5858
): string | HtmlEscapedString {
59-
if (typeof string === "string") {
60-
// simple string, return as is
61-
string = {
62-
localizationKey: string,
63-
values: [],
64-
};
65-
}
6659
const lls = ensureLazyLocalizedString(string, values);
6760
const localizationValues = getLocalizationMap();
6861

lib/hono/useLocale.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,15 @@
1-
import { useRequestContext } from "@hono/hono/jsx-renderer";
2-
import { neverThrow } from "../common/neverThrow.ts";
3-
4-
export class LocaleNotFoundError extends Error {}
5-
export class NoRequestContextError extends Error {}
1+
import { currentLocaleStorage } from "./initHonolate.ts";
62

73
/**
84
* Returns the current locale code. Must be used within a Hono JSX Renderer context.
95
* @returns the current locale code
106
*/
117
export function useLocale(): string {
12-
const ctx = neverThrow(() => useRequestContext());
13-
if (ctx instanceof Error) {
14-
throw new NoRequestContextError(
15-
"useLocale must be used within a Hono JSX Renderer context.",
16-
{ cause: ctx },
8+
const locale = currentLocaleStorage.getStore();
9+
if (!locale) {
10+
throw new Error(
11+
"No locale found in AsyncLocalStorage. Make sure to use the initHonolate middleware.",
1712
);
1813
}
19-
const locale = ctx.get("language");
20-
if (typeof locale === "string") {
21-
return locale;
22-
}
23-
throw new LocaleNotFoundError(
24-
'Language ("language") not found in request context. Make sure to set it before using useLocale.' +
25-
"\nOne option to do so is to use the languageDetector middleware provided by hono.",
26-
);
14+
return locale;
2715
}

0 commit comments

Comments
 (0)