@quilted/preact-localize 0.0.0-preview-20240627021955
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +222 -0
- package/LICENSE.md +21 -0
- package/build/esm/Localization.mjs +26 -0
- package/build/esm/context.mjs +6 -0
- package/build/esm/hooks/formatting.mjs +5 -0
- package/build/esm/hooks/locale-from-environment.mjs +15 -0
- package/build/esm/hooks/locale.mjs +5 -0
- package/build/esm/index.mjs +13 -0
- package/build/esm/request-router.mjs +49 -0
- package/build/esm/routing/LocalizedLink.mjs +26 -0
- package/build/esm/routing/LocalizedNavigation.mjs +48 -0
- package/build/esm/routing/LocalizedRouter.mjs +34 -0
- package/build/esm/routing/context.mjs +6 -0
- package/build/esm/routing/localization/by-locale.mjs +50 -0
- package/build/esm/routing/localization/by-path.mjs +23 -0
- package/build/esm/routing/localization/by-subdomain.mjs +25 -0
- package/build/esnext/Localization.esnext +35 -0
- package/build/esnext/context.esnext +6 -0
- package/build/esnext/hooks/formatting.esnext +5 -0
- package/build/esnext/hooks/locale-from-environment.esnext +13 -0
- package/build/esnext/hooks/locale.esnext +5 -0
- package/build/esnext/index.esnext +13 -0
- package/build/esnext/request-router.esnext +48 -0
- package/build/esnext/routing/LocalizedLink.esnext +21 -0
- package/build/esnext/routing/LocalizedNavigation.esnext +36 -0
- package/build/esnext/routing/LocalizedRouter.esnext +32 -0
- package/build/esnext/routing/context.esnext +6 -0
- package/build/esnext/routing/localization/by-locale.esnext +54 -0
- package/build/esnext/routing/localization/by-path.esnext +20 -0
- package/build/esnext/routing/localization/by-subdomain.esnext +22 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/build/typescript/Localization.d.ts +7 -0
- package/build/typescript/Localization.d.ts.map +1 -0
- package/build/typescript/context.d.ts +4 -0
- package/build/typescript/context.d.ts.map +1 -0
- package/build/typescript/hooks/formatting.d.ts +2 -0
- package/build/typescript/hooks/formatting.d.ts.map +1 -0
- package/build/typescript/hooks/locale-from-environment.d.ts +2 -0
- package/build/typescript/hooks/locale-from-environment.d.ts.map +1 -0
- package/build/typescript/hooks/locale.d.ts +2 -0
- package/build/typescript/hooks/locale.d.ts.map +1 -0
- package/build/typescript/index.d.ts +9 -0
- package/build/typescript/index.d.ts.map +1 -0
- package/build/typescript/request-router.d.ts +16 -0
- package/build/typescript/request-router.d.ts.map +1 -0
- package/build/typescript/routing/LocalizedLink.d.ts +9 -0
- package/build/typescript/routing/LocalizedLink.d.ts.map +1 -0
- package/build/typescript/routing/LocalizedNavigation.d.ts +13 -0
- package/build/typescript/routing/LocalizedNavigation.d.ts.map +1 -0
- package/build/typescript/routing/LocalizedRouter.d.ts +9 -0
- package/build/typescript/routing/LocalizedRouter.d.ts.map +1 -0
- package/build/typescript/routing/LocalizedRouting.d.ts +9 -0
- package/build/typescript/routing/LocalizedRouting.d.ts.map +1 -0
- package/build/typescript/routing/context.d.ts +4 -0
- package/build/typescript/routing/context.d.ts.map +1 -0
- package/build/typescript/routing/localization/by-locale.d.ts +6 -0
- package/build/typescript/routing/localization/by-locale.d.ts.map +1 -0
- package/build/typescript/routing/localization/by-path.d.ts +6 -0
- package/build/typescript/routing/localization/by-path.d.ts.map +1 -0
- package/build/typescript/routing/localization/by-subdomain.d.ts +7 -0
- package/build/typescript/routing/localization/by-subdomain.d.ts.map +1 -0
- package/build/typescript/routing/types.d.ts +17 -0
- package/build/typescript/routing/types.d.ts.map +1 -0
- package/build/typescript/routing.d.ts +9 -0
- package/build/typescript/routing.d.ts.map +1 -0
- package/configuration/rollup.config.js +3 -0
- package/package.json +67 -0
- package/source/Localization.tsx +45 -0
- package/source/context.ts +7 -0
- package/source/hooks/formatting.ts +3 -0
- package/source/hooks/locale-from-environment.ts +15 -0
- package/source/hooks/locale.ts +3 -0
- package/source/index.ts +26 -0
- package/source/request-router.ts +98 -0
- package/source/routing/LocalizedLink.tsx +27 -0
- package/source/routing/LocalizedNavigation.tsx +64 -0
- package/source/routing/LocalizedRouter.ts +46 -0
- package/source/routing/context.ts +8 -0
- package/source/routing/localization/by-locale.ts +71 -0
- package/source/routing/localization/by-path.ts +31 -0
- package/source/routing/localization/by-subdomain.ts +34 -0
- package/source/routing/types.ts +16 -0
- package/source/routing.ts +12 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface RouteLocalization {
|
|
2
|
+
readonly locales: string[];
|
|
3
|
+
readonly defaultLocale: string;
|
|
4
|
+
matchLocale(locale: string): string | undefined;
|
|
5
|
+
redirectURL(from: URL, options: {
|
|
6
|
+
to: string;
|
|
7
|
+
}): URL;
|
|
8
|
+
localeFromURL(url: URL): string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolvedRouteLocalization extends RouteLocalization {
|
|
11
|
+
readonly locale: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DefaultLocaleDefinition {
|
|
14
|
+
locale: string;
|
|
15
|
+
nested?: boolean;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../source/routing/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAChD,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,GAAG,GAAG,CAAC;IACnD,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAC;CAC7C;AAED,MAAM,WAAW,yBAA0B,SAAQ,iBAAiB;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { LocalizedLink } from './routing/LocalizedLink.tsx';
|
|
2
|
+
export { LocalizedNavigation } from './routing/LocalizedNavigation.tsx';
|
|
3
|
+
export { LocalizedRouter } from './routing/LocalizedRouter.ts';
|
|
4
|
+
export { useRouteLocalization } from './routing/context.ts';
|
|
5
|
+
export { createRouteLocalization } from './routing/localization/by-locale.ts';
|
|
6
|
+
export { createRoutePathLocalization } from './routing/localization/by-path.ts';
|
|
7
|
+
export { createRouteSubdomainLocalization } from './routing/localization/by-subdomain.ts';
|
|
8
|
+
export type { RouteLocalization, ResolvedRouteLocalization, DefaultLocaleDefinition, } from './routing/types.ts';
|
|
9
|
+
//# sourceMappingURL=routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../source/routing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAC,mBAAmB,EAAC,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAC,eAAe,EAAC,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAC,uBAAuB,EAAC,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAC,2BAA2B,EAAC,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAC,gCAAgC,EAAC,MAAM,wCAAwC,CAAC;AACxF,YAAY,EACV,iBAAiB,EACjB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quilted/preact-localize",
|
|
3
|
+
"description": "Utilities for localizing React applications",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.0.0-preview-20240627021955",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=14.0.0"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/lemonmade/quilt",
|
|
13
|
+
"directory": "packages/preact-localize"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"@quilted:registry": "https://registry.npmjs.org"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./build/typescript/index.d.ts",
|
|
22
|
+
"quilt:source": "./source/index.ts",
|
|
23
|
+
"quilt:esnext": "./build/esnext/index.esnext",
|
|
24
|
+
"import": "./build/esm/index.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./request-router": {
|
|
27
|
+
"types": "./build/typescript/request-router.d.ts",
|
|
28
|
+
"quilt:source": "./source/request-router.ts",
|
|
29
|
+
"quilt:esnext": "./build/esnext/request-router.esnext",
|
|
30
|
+
"import": "./build/esm/request-router.mjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"types": "./build/typescript/index.d.ts",
|
|
34
|
+
"typesVersions": {
|
|
35
|
+
"*": {
|
|
36
|
+
"request-router": [
|
|
37
|
+
"./build/typescript/request-router.d.ts"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@quilted/localize": "^0.2.0",
|
|
44
|
+
"@quilted/preact-browser": "^0.1.0",
|
|
45
|
+
"@quilted/preact-context": "^0.1.0",
|
|
46
|
+
"@quilted/preact-router": "0.0.0-preview-20240627021955"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@quilted/request-router": "0.0.0-preview-20240627021955",
|
|
50
|
+
"preact": "^10.21.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@quilted/request-router": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"preact": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@quilted/request-router": "0.0.0-preview-20240627021955",
|
|
62
|
+
"preact": "^10.21.0"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "rollup --config configuration/rollup.config.js"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {RenderableProps} from 'preact';
|
|
2
|
+
import {useMemo} from 'preact/hooks';
|
|
3
|
+
import {createLocalizedFormatting} from '@quilted/localize';
|
|
4
|
+
import {HTMLAttributes} from '@quilted/preact-browser';
|
|
5
|
+
|
|
6
|
+
import {LocaleContext, LocalizedFormattingContext} from './context.ts';
|
|
7
|
+
|
|
8
|
+
export interface LocalizationProps {
|
|
9
|
+
locale: string;
|
|
10
|
+
direction?: 'ltr' | 'rtl';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RTL_LOCALES = new Set([
|
|
14
|
+
'ar',
|
|
15
|
+
'arc',
|
|
16
|
+
'ckb',
|
|
17
|
+
'dv',
|
|
18
|
+
'fa',
|
|
19
|
+
'ha',
|
|
20
|
+
'he',
|
|
21
|
+
'khw',
|
|
22
|
+
'ks',
|
|
23
|
+
'ps',
|
|
24
|
+
'sd',
|
|
25
|
+
'ur',
|
|
26
|
+
'uz-AF',
|
|
27
|
+
'yi',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function Localization({
|
|
31
|
+
locale,
|
|
32
|
+
direction = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr',
|
|
33
|
+
children,
|
|
34
|
+
}: RenderableProps<LocalizationProps>) {
|
|
35
|
+
const formatting = useMemo(() => createLocalizedFormatting(locale), [locale]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<LocaleContext.Provider value={locale}>
|
|
39
|
+
<LocalizedFormattingContext.Provider value={formatting}>
|
|
40
|
+
<HTMLAttributes lang={locale} dir={direction} />
|
|
41
|
+
{children}
|
|
42
|
+
</LocalizedFormattingContext.Provider>
|
|
43
|
+
</LocaleContext.Provider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {createOptionalContext} from '@quilted/preact-context';
|
|
2
|
+
import type {LocalizedFormatting} from '@quilted/localize';
|
|
3
|
+
|
|
4
|
+
export const LocalizedFormattingContext =
|
|
5
|
+
createOptionalContext<LocalizedFormatting>();
|
|
6
|
+
|
|
7
|
+
export const LocaleContext = createOptionalContext<string>();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {parseAcceptLanguageHeader} from '@quilted/localize';
|
|
2
|
+
import {useBrowserDetails} from '@quilted/preact-browser';
|
|
3
|
+
|
|
4
|
+
export function useLocaleFromEnvironment() {
|
|
5
|
+
if (typeof navigator === 'object' && navigator.language) {
|
|
6
|
+
return navigator.language;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const browserDetails = useBrowserDetails({optional: true});
|
|
10
|
+
|
|
11
|
+
const acceptLanguage =
|
|
12
|
+
browserDetails?.request.headers?.get('Accept-Language');
|
|
13
|
+
|
|
14
|
+
return acceptLanguage && parseAcceptLanguageHeader(acceptLanguage);
|
|
15
|
+
}
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createTranslate,
|
|
3
|
+
createLocalizedFormatting,
|
|
4
|
+
parseAcceptLanguageHeader,
|
|
5
|
+
MissingTranslationError,
|
|
6
|
+
MissingTranslationPlaceholderError,
|
|
7
|
+
} from '@quilted/localize';
|
|
8
|
+
export type {
|
|
9
|
+
Translate,
|
|
10
|
+
TranslationDictionary,
|
|
11
|
+
LocalizedFormatting,
|
|
12
|
+
LocalizedFormattingCache,
|
|
13
|
+
LocalizedNumberFormatOptions,
|
|
14
|
+
LocalizedDateTimeFormatOptions,
|
|
15
|
+
} from '@quilted/localize';
|
|
16
|
+
|
|
17
|
+
export {Localization} from './Localization.tsx';
|
|
18
|
+
export {useLocalizedFormatting} from './hooks/formatting.ts';
|
|
19
|
+
export {useLocale} from './hooks/locale.ts';
|
|
20
|
+
export {useLocaleFromEnvironment} from './hooks/locale-from-environment.ts';
|
|
21
|
+
export {LocalizedFormattingContext} from './context.ts';
|
|
22
|
+
|
|
23
|
+
export * from './routing.ts';
|
|
24
|
+
|
|
25
|
+
// export function useLocale(): string {}
|
|
26
|
+
// export function useTranslate(): Translate {}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {RedirectResponse, type RequestHandler} from '@quilted/request-router';
|
|
2
|
+
import {parseAcceptLanguageHeader} from '@quilted/localize';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
RouteLocalization,
|
|
6
|
+
ResolvedRouteLocalization,
|
|
7
|
+
DefaultLocaleDefinition,
|
|
8
|
+
} from './routing.ts';
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
RouteLocalization,
|
|
12
|
+
ResolvedRouteLocalization,
|
|
13
|
+
DefaultLocaleDefinition,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface RequestRouterLocalization {
|
|
17
|
+
redirect(
|
|
18
|
+
request: Request,
|
|
19
|
+
options: {to: string} & Pick<
|
|
20
|
+
NonNullable<ConstructorParameters<typeof RedirectResponse>[1]>,
|
|
21
|
+
'status' | 'headers'
|
|
22
|
+
>,
|
|
23
|
+
): Response;
|
|
24
|
+
localizedRequestHandler(
|
|
25
|
+
handler: RequestHandler,
|
|
26
|
+
options?: {include?(request: Request): boolean},
|
|
27
|
+
): RequestHandler;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createRequestRouterLocalization({
|
|
31
|
+
localization,
|
|
32
|
+
requestLocale: customRequestLocale,
|
|
33
|
+
}: {
|
|
34
|
+
localization: RouteLocalization;
|
|
35
|
+
requestLocale?(
|
|
36
|
+
request: Request,
|
|
37
|
+
getDefaultFromRequest: () => string | undefined,
|
|
38
|
+
): string | undefined;
|
|
39
|
+
}): RequestRouterLocalization {
|
|
40
|
+
const {matchLocale, redirectURL, defaultLocale, localeFromURL} = localization;
|
|
41
|
+
|
|
42
|
+
const getDefaultLocaleFromRequest = (request: Request) => {
|
|
43
|
+
const acceptLanguage = request.headers.get('Accept-Language');
|
|
44
|
+
return (
|
|
45
|
+
(acceptLanguage && parseAcceptLanguageHeader(acceptLanguage)) || undefined
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getLocaleForRequest =
|
|
50
|
+
customRequestLocale ?? getDefaultLocaleFromRequest;
|
|
51
|
+
|
|
52
|
+
function localeRedirect(
|
|
53
|
+
request: Request,
|
|
54
|
+
{
|
|
55
|
+
to,
|
|
56
|
+
...options
|
|
57
|
+
}: {to: string} & Pick<
|
|
58
|
+
NonNullable<ConstructorParameters<typeof RedirectResponse>[1]>,
|
|
59
|
+
'status' | 'headers'
|
|
60
|
+
>,
|
|
61
|
+
): Response {
|
|
62
|
+
return new RedirectResponse(
|
|
63
|
+
redirectURL(new URL(request.url), {to}),
|
|
64
|
+
options,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
redirect: localeRedirect,
|
|
70
|
+
localizedRequestHandler(
|
|
71
|
+
handler: RequestHandler,
|
|
72
|
+
{include = () => true}: {include?(request: Request): boolean} = {},
|
|
73
|
+
): RequestHandler {
|
|
74
|
+
return async (request, ...args) => {
|
|
75
|
+
if (!include(request)) return handler(request, ...args);
|
|
76
|
+
|
|
77
|
+
const url = new URL(request.url);
|
|
78
|
+
const urlLocale = localeFromURL(url);
|
|
79
|
+
const requestLocale = getLocaleForRequest(request, () =>
|
|
80
|
+
getDefaultLocaleFromRequest(request),
|
|
81
|
+
);
|
|
82
|
+
const matchedLocale =
|
|
83
|
+
(requestLocale == null ? undefined : matchLocale(requestLocale)) ??
|
|
84
|
+
defaultLocale;
|
|
85
|
+
|
|
86
|
+
if (urlLocale !== matchedLocale) {
|
|
87
|
+
return new RedirectResponse(
|
|
88
|
+
redirectURL(url, {
|
|
89
|
+
to: matchedLocale,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return handler(request, ...args);
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {ComponentProps} from 'preact';
|
|
2
|
+
import {useMemo} from 'preact/hooks';
|
|
3
|
+
import {useCurrentURL, Link, useRouter} from '@quilted/preact-router';
|
|
4
|
+
|
|
5
|
+
import {useRouteLocalization} from './context.ts';
|
|
6
|
+
|
|
7
|
+
type LinkProps = ComponentProps<typeof Link>;
|
|
8
|
+
|
|
9
|
+
export function LocalizedLink({
|
|
10
|
+
to,
|
|
11
|
+
locale,
|
|
12
|
+
...props
|
|
13
|
+
}: Omit<LinkProps, 'to' | 'hrefLang'> & {
|
|
14
|
+
locale: string;
|
|
15
|
+
to?: LinkProps['to'];
|
|
16
|
+
}) {
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const url = useCurrentURL();
|
|
19
|
+
const {redirectURL} = useRouteLocalization();
|
|
20
|
+
|
|
21
|
+
const resolvedURL = useMemo(
|
|
22
|
+
() => redirectURL(to ? router.resolve(to).url : url, {to: locale}),
|
|
23
|
+
[to, url, locale, redirectURL, router],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return <Link hrefLang={locale} to={resolvedURL} {...props} />;
|
|
27
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {RenderableProps} from 'preact';
|
|
2
|
+
import {useMemo} from 'preact/hooks';
|
|
3
|
+
import {useBrowserDetails} from '@quilted/preact-browser';
|
|
4
|
+
import {Navigation, type RouteDefinition} from '@quilted/preact-router';
|
|
5
|
+
|
|
6
|
+
import {Localization} from '../Localization.tsx';
|
|
7
|
+
import {useLocaleFromEnvironment} from '../hooks/locale-from-environment.ts';
|
|
8
|
+
|
|
9
|
+
import {LocalizedRouter} from './LocalizedRouter.ts';
|
|
10
|
+
import {RouteLocalizationContext} from './context.ts';
|
|
11
|
+
import type {RouteLocalization} from './types.ts';
|
|
12
|
+
|
|
13
|
+
export interface LocalizedNavigationProps<Context = unknown> {
|
|
14
|
+
locale?: string;
|
|
15
|
+
router?: LocalizedRouter;
|
|
16
|
+
localization?: RouteLocalization;
|
|
17
|
+
routes?: readonly RouteDefinition<any, any, Context>[];
|
|
18
|
+
context?: Context;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function LocalizedNavigation<Context = unknown>({
|
|
22
|
+
locale: explicitLocale,
|
|
23
|
+
router,
|
|
24
|
+
routes,
|
|
25
|
+
context,
|
|
26
|
+
localization,
|
|
27
|
+
children,
|
|
28
|
+
}: RenderableProps<LocalizedNavigationProps<Context>>) {
|
|
29
|
+
const browser = useBrowserDetails({optional: true});
|
|
30
|
+
const resolvedRouter = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
router ??
|
|
33
|
+
new LocalizedRouter(browser?.request.url, {localization: localization!}),
|
|
34
|
+
[router],
|
|
35
|
+
);
|
|
36
|
+
const resolvedLocalization = resolvedRouter.localization;
|
|
37
|
+
|
|
38
|
+
const localeFromEnvironment = useLocaleFromEnvironment();
|
|
39
|
+
|
|
40
|
+
let resolvedLocale: string;
|
|
41
|
+
|
|
42
|
+
if (explicitLocale) {
|
|
43
|
+
resolvedLocale = explicitLocale;
|
|
44
|
+
} else if (
|
|
45
|
+
localeFromEnvironment != null &&
|
|
46
|
+
localeFromEnvironment
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.startsWith(resolvedLocalization.locale.toLowerCase())
|
|
49
|
+
) {
|
|
50
|
+
resolvedLocale = localeFromEnvironment;
|
|
51
|
+
} else {
|
|
52
|
+
resolvedLocale = resolvedLocalization.locale;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Localization locale={resolvedLocale}>
|
|
57
|
+
<RouteLocalizationContext.Provider value={resolvedLocalization}>
|
|
58
|
+
<Navigation router={resolvedRouter} routes={routes} context={context}>
|
|
59
|
+
{children}
|
|
60
|
+
</Navigation>
|
|
61
|
+
</RouteLocalizationContext.Provider>
|
|
62
|
+
</Localization>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {Router, type RouterOptions} from '@quilted/preact-router';
|
|
2
|
+
|
|
3
|
+
import type {RouteLocalization, ResolvedRouteLocalization} from './types.ts';
|
|
4
|
+
|
|
5
|
+
export class LocalizedRouter extends Router {
|
|
6
|
+
readonly localization: ResolvedRouteLocalization;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
initial: ConstructorParameters<typeof Router>[0],
|
|
10
|
+
{
|
|
11
|
+
localization,
|
|
12
|
+
isExternal: explicitIsExternal,
|
|
13
|
+
}: {localization: RouteLocalization} & Pick<RouterOptions, 'isExternal'>,
|
|
14
|
+
) {
|
|
15
|
+
const {localeFromURL} = localization;
|
|
16
|
+
|
|
17
|
+
super(initial, {
|
|
18
|
+
isExternal(url, currentURL) {
|
|
19
|
+
return (
|
|
20
|
+
matchedLocale !== localeFromURL(url) ||
|
|
21
|
+
(explicitIsExternal?.(url, currentURL) ?? false)
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const currentURL = this.currentRequest.url;
|
|
27
|
+
|
|
28
|
+
const matchedLocale = localeFromURL(currentURL);
|
|
29
|
+
|
|
30
|
+
const resolvedLocalization: ResolvedRouteLocalization = {
|
|
31
|
+
locale: matchedLocale ?? localization.defaultLocale,
|
|
32
|
+
...localization,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this.localization = resolvedLocalization;
|
|
36
|
+
|
|
37
|
+
const {pathname: rootPath} = localization.redirectURL(
|
|
38
|
+
new URL('/', currentURL),
|
|
39
|
+
{
|
|
40
|
+
to: resolvedLocalization.locale,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (rootPath.length > 1) Object.assign(this, {base: rootPath});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {createOptionalContext} from '@quilted/preact-context';
|
|
2
|
+
|
|
3
|
+
import type {ResolvedRouteLocalization} from './types.ts';
|
|
4
|
+
|
|
5
|
+
export const RouteLocalizationContext =
|
|
6
|
+
createOptionalContext<ResolvedRouteLocalization>();
|
|
7
|
+
|
|
8
|
+
export const useRouteLocalization = RouteLocalizationContext.use;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type {RouteLocalization} from '../types.ts';
|
|
2
|
+
|
|
3
|
+
export function createRouteLocalization({
|
|
4
|
+
locales: localeMap,
|
|
5
|
+
default: defaultLocale,
|
|
6
|
+
}: {
|
|
7
|
+
locales: Map<string, string>;
|
|
8
|
+
default: string;
|
|
9
|
+
}): RouteLocalization {
|
|
10
|
+
const sortedLocaleMap = [...localeMap].sort(
|
|
11
|
+
([, a], [, b]) => b.length - a.length,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const locales = [...localeMap.keys()].sort((a, b) => b.length - a.length);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
locales,
|
|
18
|
+
redirectURL,
|
|
19
|
+
matchLocale,
|
|
20
|
+
localeFromURL,
|
|
21
|
+
defaultLocale,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function matchLocale(requestLocale: string) {
|
|
25
|
+
const language = requestLocale.split('-')[0];
|
|
26
|
+
|
|
27
|
+
return locales.find(
|
|
28
|
+
(locale) => locale === requestLocale || locale === language,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function localeFromURL(url: URL) {
|
|
33
|
+
const hostname = url.hostname.toLowerCase();
|
|
34
|
+
const pathname = normalizePath(url.pathname.toLowerCase());
|
|
35
|
+
|
|
36
|
+
for (const [locale, target] of sortedLocaleMap) {
|
|
37
|
+
if (target.startsWith('/')) {
|
|
38
|
+
if (pathname.startsWith(target)) return locale;
|
|
39
|
+
} else {
|
|
40
|
+
if (hostname === target) return locale;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function redirectURL(from: URL, {to: toLocale}: {to: string}) {
|
|
46
|
+
const fromLocale = localeFromURL(from);
|
|
47
|
+
const toUrl = new URL(from);
|
|
48
|
+
|
|
49
|
+
if (fromLocale === toLocale) return toUrl;
|
|
50
|
+
|
|
51
|
+
const target = localeMap.get(toLocale) ?? localeMap.get(defaultLocale)!;
|
|
52
|
+
|
|
53
|
+
if (target.startsWith('/')) {
|
|
54
|
+
const fromTarget =
|
|
55
|
+
fromLocale == null ? '/' : localeMap.get(fromLocale) ?? '/';
|
|
56
|
+
|
|
57
|
+
toUrl.pathname = normalizePath(
|
|
58
|
+
toUrl.pathname.replace(fromTarget, target),
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
toUrl.hostname = target;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return toUrl;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizePath(path: string) {
|
|
69
|
+
if (path.length === 0) return '/';
|
|
70
|
+
return path.endsWith('/') && path.length > 1 ? path.slice(0, -1) : path;
|
|
71
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {DefaultLocaleDefinition} from '../types.ts';
|
|
2
|
+
import {createRouteLocalization} from './by-locale.ts';
|
|
3
|
+
|
|
4
|
+
export function createRoutePathLocalization({
|
|
5
|
+
locales,
|
|
6
|
+
default: defaultLocaleDefinition,
|
|
7
|
+
}: {
|
|
8
|
+
locales: string[];
|
|
9
|
+
default: string | DefaultLocaleDefinition;
|
|
10
|
+
}) {
|
|
11
|
+
const localeMap = new Map<string, string>();
|
|
12
|
+
|
|
13
|
+
const defaultLocale =
|
|
14
|
+
typeof defaultLocaleDefinition === 'string'
|
|
15
|
+
? defaultLocaleDefinition
|
|
16
|
+
: defaultLocaleDefinition.locale;
|
|
17
|
+
|
|
18
|
+
const defaultLocaleNested =
|
|
19
|
+
typeof defaultLocaleDefinition === 'string' ||
|
|
20
|
+
(defaultLocaleDefinition.nested ?? true);
|
|
21
|
+
|
|
22
|
+
for (const locale of locales) {
|
|
23
|
+
if (!defaultLocaleNested && locale === defaultLocale) {
|
|
24
|
+
localeMap.set(locale, '/');
|
|
25
|
+
} else {
|
|
26
|
+
localeMap.set(locale, `/${locale.toLowerCase()}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return createRouteLocalization({locales: localeMap, default: defaultLocale});
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {DefaultLocaleDefinition} from '../types.ts';
|
|
2
|
+
import {createRouteLocalization} from './by-locale.ts';
|
|
3
|
+
|
|
4
|
+
export function createRouteSubdomainLocalization({
|
|
5
|
+
base,
|
|
6
|
+
locales,
|
|
7
|
+
default: defaultLocaleDefinition,
|
|
8
|
+
}: {
|
|
9
|
+
base: string;
|
|
10
|
+
locales: string[];
|
|
11
|
+
default: string | DefaultLocaleDefinition;
|
|
12
|
+
}) {
|
|
13
|
+
const localeMap = new Map<string, string>();
|
|
14
|
+
const normalizedBase = base.replace(/^https?:\/\//, '');
|
|
15
|
+
|
|
16
|
+
const defaultLocale =
|
|
17
|
+
typeof defaultLocaleDefinition === 'string'
|
|
18
|
+
? defaultLocaleDefinition
|
|
19
|
+
: defaultLocaleDefinition.locale;
|
|
20
|
+
|
|
21
|
+
const defaultLocaleNested =
|
|
22
|
+
typeof defaultLocaleDefinition === 'string' ||
|
|
23
|
+
(defaultLocaleDefinition.nested ?? true);
|
|
24
|
+
|
|
25
|
+
for (const locale of locales) {
|
|
26
|
+
if (!defaultLocaleNested && locale === defaultLocale) {
|
|
27
|
+
localeMap.set(locale, normalizedBase);
|
|
28
|
+
} else {
|
|
29
|
+
localeMap.set(locale, `${locale.toLowerCase()}.${normalizedBase}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return createRouteLocalization({locales: localeMap, default: defaultLocale});
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RouteLocalization {
|
|
2
|
+
readonly locales: string[];
|
|
3
|
+
readonly defaultLocale: string;
|
|
4
|
+
matchLocale(locale: string): string | undefined;
|
|
5
|
+
redirectURL(from: URL, options: {to: string}): URL;
|
|
6
|
+
localeFromURL(url: URL): string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResolvedRouteLocalization extends RouteLocalization {
|
|
10
|
+
readonly locale: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DefaultLocaleDefinition {
|
|
14
|
+
locale: string;
|
|
15
|
+
nested?: boolean;
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {LocalizedLink} from './routing/LocalizedLink.tsx';
|
|
2
|
+
export {LocalizedNavigation} from './routing/LocalizedNavigation.tsx';
|
|
3
|
+
export {LocalizedRouter} from './routing/LocalizedRouter.ts';
|
|
4
|
+
export {useRouteLocalization} from './routing/context.ts';
|
|
5
|
+
export {createRouteLocalization} from './routing/localization/by-locale.ts';
|
|
6
|
+
export {createRoutePathLocalization} from './routing/localization/by-path.ts';
|
|
7
|
+
export {createRouteSubdomainLocalization} from './routing/localization/by-subdomain.ts';
|
|
8
|
+
export type {
|
|
9
|
+
RouteLocalization,
|
|
10
|
+
ResolvedRouteLocalization,
|
|
11
|
+
DefaultLocaleDefinition,
|
|
12
|
+
} from './routing/types.ts';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@quilted/typescript/tsconfig.project.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "source",
|
|
5
|
+
"outDir": "build/typescript",
|
|
6
|
+
"jsxImportSource": "preact"
|
|
7
|
+
},
|
|
8
|
+
"include": ["source"],
|
|
9
|
+
"exclude": ["*.test.ts", "*.test.tsx"],
|
|
10
|
+
"references": [
|
|
11
|
+
{"path": "../localize"},
|
|
12
|
+
{"path": "../preact-browser"},
|
|
13
|
+
{"path": "../preact-context"},
|
|
14
|
+
{"path": "../preact-router"},
|
|
15
|
+
{"path": "../request-router"}
|
|
16
|
+
]
|
|
17
|
+
}
|