Skip to content

Commit 223462b

Browse files
committed
feat(core): add translation middleware
1 parent 50ef2ac commit 223462b

File tree

6 files changed

+120
-1
lines changed

6 files changed

+120
-1
lines changed

packages/root/src/core/core.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export {
88
StringParamsProvider,
99
useStringParams,
1010
} from './hooks/useStringParams.js';
11+
export {
12+
TranslationMiddlewareProvider,
13+
useTranslationMiddleware,
14+
TRANSLATION_MIDDLEWARE_CONTEXT,
15+
} from './hooks/useTranslationsMiddleware.js';
1116
export {
1217
I18nContext,
1318
useI18nContext,

packages/root/src/core/hooks/useTranslations.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {I18nContext, useI18nContext} from './useI18nContext.js';
22
import {useStringParams} from './useStringParams.js';
3+
import {useTranslationMiddleware} from './useTranslationsMiddleware.js';
34

45
/**
56
* A hook that returns a function that can be used to translate a string, and
@@ -23,14 +24,22 @@ export function useTranslations() {
2324
}
2425
const translations = i18nContext?.translations || {};
2526
const stringParams = useStringParams();
27+
const middleware = useTranslationMiddleware();
2628
const t = (str: string, params?: Record<string, string | number>) => {
27-
const key = normalizeString(str);
29+
let input = str;
30+
middleware.pre.forEach((fn) => {
31+
input = fn(input);
32+
});
33+
const key = normalizeString(input);
2834
let translation = translations[key] ?? key ?? '';
2935
const allParams = {...stringParams, ...params};
3036
for (const paramName of Object.keys(allParams)) {
3137
const paramValue = String(allParams[paramName] ?? '');
3238
translation = translation.replaceAll(`{${paramName}}`, paramValue);
3339
}
40+
middleware.post.forEach((fn) => {
41+
translation = fn(translation);
42+
});
3443
return translation;
3544
};
3645
return t;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {ComponentChildren, FunctionalComponent, createContext} from 'preact';
2+
import {useContext} from 'preact/hooks';
3+
4+
export interface TranslationMiddleware {
5+
/** Transform the source string before translation lookup. */
6+
pre?: (str: string) => string;
7+
/** Transform the translated string before returning it. */
8+
post?: (str: string) => string;
9+
}
10+
11+
export interface TranslationMiddlewareContext {
12+
pre: Array<(str: string) => string>;
13+
post: Array<(str: string) => string>;
14+
}
15+
16+
export const TRANSLATION_MIDDLEWARE_CONTEXT = createContext<TranslationMiddlewareContext | null>(null);
17+
18+
export interface TranslationMiddlewareProviderProps {
19+
value?: TranslationMiddleware;
20+
children?: ComponentChildren;
21+
}
22+
23+
export const TranslationMiddlewareProvider: FunctionalComponent<TranslationMiddlewareProviderProps> = (props) => {
24+
const parent = useContext(TRANSLATION_MIDDLEWARE_CONTEXT) || {pre: [], post: []};
25+
const merged: TranslationMiddlewareContext = {
26+
pre: [...parent.pre],
27+
post: [...parent.post],
28+
};
29+
if (props.value?.pre) {
30+
merged.pre.push(props.value.pre);
31+
}
32+
if (props.value?.post) {
33+
// Apply post transforms from child first then parent.
34+
merged.post.unshift(props.value.post);
35+
}
36+
return (
37+
<TRANSLATION_MIDDLEWARE_CONTEXT.Provider value={merged}>
38+
{props.children}
39+
</TRANSLATION_MIDDLEWARE_CONTEXT.Provider>
40+
);
41+
};
42+
43+
export function useTranslationMiddleware(): TranslationMiddlewareContext {
44+
return useContext(TRANSLATION_MIDDLEWARE_CONTEXT) || {pre: [], post: []};
45+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
prettyHtml: true,
3+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {
2+
TranslationMiddlewareProvider,
3+
useTranslations,
4+
} from '../../../../dist/core';
5+
6+
export default function Page() {
7+
return (
8+
<TranslationMiddlewareProvider
9+
value={{
10+
pre: (s) => s.toUpperCase(),
11+
post: (s) => s + '!'}}
12+
>
13+
<Content />
14+
</TranslationMiddlewareProvider>
15+
);
16+
}
17+
18+
function Content() {
19+
const t = useTranslations();
20+
return <p>{t('hello')}</p>;
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {promises as fs} from 'node:fs';
2+
import path from 'node:path';
3+
import {assert, beforeEach, test, expect, afterEach} from 'vitest';
4+
import {fileExists} from '../src/utils/fsutils.js';
5+
import {Fixture, loadFixture} from './testutils.js';
6+
7+
let fixture: Fixture;
8+
9+
beforeEach(async () => {
10+
fixture = await loadFixture('./fixtures/translation-middleware');
11+
});
12+
13+
afterEach(async () => {
14+
if (fixture) {
15+
await fixture.cleanup();
16+
}
17+
});
18+
19+
test('apply translation middleware', async () => {
20+
await fixture.build();
21+
const indexPath = path.join(fixture.distDir, 'html/index.html');
22+
assert.isTrue(await fileExists(indexPath));
23+
const html = await fs.readFile(indexPath, 'utf-8');
24+
expect(html).toMatchInlineSnapshot(`
25+
"<!doctype html>
26+
<html>
27+
<head>
28+
<meta charset=\"utf-8\">
29+
</head>
30+
<body>
31+
<p>HELLO!</p>
32+
</body>
33+
</html>
34+
"
35+
`);
36+
});

0 commit comments

Comments
 (0)