Skip to content

Commit 226fc57

Browse files
authored
Merge pull request #6 from wuespace/pklaschka/issue5
Use AsyncLocalStorage for better flexibility
2 parents 26fa374 + bbe8c74 commit 226fc57

9 files changed

Lines changed: 72 additions & 103 deletions

File tree

README.md

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@ Create an `i18n.ts` file to configure your localization settings:
2121

2222
```typescript
2323
#!/usr/bin/env -S deno run --allow-read --allow-write=./locales/ --allow-env
24-
import { HonolateConfig, initHonolate, runCLI } from "@wuespace/honolate";
24+
import { initHonolate, InitHonolateOptions, runCLI } from "@wuespace/honolate";
2525

26-
const config: HonolateConfig = {
27-
defaultLocale: "en",
28-
supportedLocales: {
26+
const config = {
27+
defaultLanguage: "en",
28+
languages: {
2929
en: import.meta.resolve("./locales/en.json"),
3030
de: import.meta.resolve("./locales/de.json"),
3131
},
32-
};
32+
} satisfies InitHonolateOptions<string>;
3333

3434
// CLI
35-
import.meta.main && await runCLI(config, import.meta.dirname);
35+
import.meta.main && await runCLI(config, import.meta.dirname ?? Deno.cwd());
3636

3737
// Middleware for Hono
3838
export const i18n = await initHonolate(config);
@@ -73,31 +73,16 @@ Deno.serve(app);
7373
You can use eager strings for immediate translation:
7474

7575
```typescript
76-
import { asFC, t } from "@wuespace/honolate";
76+
import { t } from "@wuespace/honolate";
7777

78-
c.render(asFC(() => (
78+
c.render(
7979
<div>
8080
<h1>{t`Hello world!`}</h1>
8181
<p>{t`We can also use variables like ${new Date()}.`}</p>
82-
</div>
83-
)));
82+
</div>,
83+
);
8484
```
8585

86-
Note that the `t` function internally uses `useRequestContext` to access the
87-
current request context. For this to work, it can only be called inside a
88-
component rendered with the `jsxRenderer` middleware. Here, we use `asFC` to
89-
create a functional component out of an inline JSX expression.
90-
91-
Internally, all `asFC` does is to create a functional component:
92-
93-
```typescript
94-
function asFC<T>(Component: () => JSX.Element) {
95-
return <Component />;
96-
}
97-
```
98-
99-
With that, everything inside the function now has access to the request context.
100-
10186
### Lazy strings
10287

10388
Sometimes, you need to define strings outside of a component context. In this
@@ -116,11 +101,11 @@ function:
116101
import { t } from "@wuespace/honolate";
117102
import { greeting } from "./greeting.ts";
118103

119-
c.render(asFC(() => (
104+
c.render(
120105
<div>
121106
<h1>{t(greeting)}</h1>
122-
</div>
123-
)));
107+
</div>,
108+
);
124109
```
125110

126111
### Updating localization files

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 ${c.req.url}!`}
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: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ import type { LocalizedStringValue } from "./LocalizedStringValue.ts";
1313
*
1414
* Can be used as a template string tag or as a function with a LazyLocalizedString.
1515
*
16-
* Can only be called inside a functional component. To use this directly in route handlers,
17-
* use {@link asFC} to wrap the handler as a functional component.
18-
*
1916
* This cannot be used outside of Hono request context (e.g., in plain Deno scripts),
20-
* since it relies on the request context to provide the current language's localization map.
17+
* since it relies on the request's localization context,
18+
* set by {@link import("./initHonolate.ts").initHonolate},
19+
* to provide the current language's localization map.
2120
*
2221
* @example Example as template string tag:
2322
* ```ts
@@ -33,15 +32,14 @@ import type { LocalizedStringValue } from "./LocalizedStringValue.ts";
3332
* const greeting = t(lls);
3433
* ```
3534
*
36-
* @example Using with asFC in route handler:
35+
* @example Using in route handler:
3736
* ```ts
38-
* import { asFC, t } from '@wuespace/honolate';
37+
* import { t } from '@wuespace/honolate';
3938
*
4039
* app.get('/greet', c => {
41-
* return c.render(asFC(() => {
42-
* const greeting = t`Hello ${c.req.param('name')}!`;
43-
* return greeting;
44-
* }));
40+
* return c.render(
41+
* <p>{t`Hello ${c.req.param('name')}!`}</p>
42+
* );
4543
* });
4644
* ```
4745
*/
@@ -56,13 +54,6 @@ export function t(
5654
string: TemplateStringsArray | LazyLocalizedString | string,
5755
...values: LocalizedStringValue[]
5856
): string | HtmlEscapedString {
59-
if (typeof string === "string") {
60-
// simple string, return as is
61-
string = {
62-
localizationKey: string,
63-
values: [],
64-
};
65-
}
6657
const lls = ensureLazyLocalizedString(string, values);
6758
const localizationValues = getLocalizationMap();
6859

lib/hono/useLocale.ts

Lines changed: 7 additions & 19 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
/**
8-
* Returns the current locale code. Must be used within a Hono JSX Renderer context.
4+
* Returns the current locale code. Must be used within the scope of the `initHonolate` middleware.
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)