mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): detect language from querystring and header
This commit is contained in:
parent
806e99de61
commit
43456aae9b
9 changed files with 131 additions and 41 deletions
|
@ -42,8 +42,8 @@
|
|||
"zod": "^3.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/eslint-config": "^0.1.2",
|
||||
"@logto/ts-config": "^0.1.2",
|
||||
"@logto/eslint-config": "^0.1.3",
|
||||
"@logto/ts-config": "^0.1.3",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/koa": "^2.13.3",
|
||||
"@types/koa-logger": "^3.1.1",
|
||||
|
|
|
@ -6,6 +6,7 @@ import koaLogger from 'koa-logger';
|
|||
|
||||
import { port } from '@/env/consts';
|
||||
import koaErrorHandler from '@/middleware/koa-error-handler';
|
||||
import koaI18next from '@/middleware/koa-i18next';
|
||||
import koaUIProxy from '@/middleware/koa-ui-proxy';
|
||||
import initOidc from '@/oidc/init';
|
||||
import initRouter from '@/routes/init';
|
||||
|
@ -13,6 +14,7 @@ import initRouter from '@/routes/init';
|
|||
export default async function initApp(app: Koa): Promise<void> {
|
||||
app.use(koaErrorHandler());
|
||||
app.use(koaLogger());
|
||||
app.use(koaI18next());
|
||||
|
||||
const provider = await initOidc(app);
|
||||
initRouter(app, provider);
|
||||
|
|
53
packages/core/src/i18n/detect-language.ts
Normal file
53
packages/core/src/i18n/detect-language.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
import { Optional } from '@logto/essentials';
|
||||
import { ParameterizedContext } from 'koa';
|
||||
import { IRouterParamContext } from 'koa-router';
|
||||
|
||||
/**
|
||||
* Resolve language and its q value from string.
|
||||
* @param languageString The language string in header, e.g. 'en-GB;q=0.8', 'zh-CN'
|
||||
* @returns `[language, q]`, e.g. `['en-GB', 0.8]`; `undefined` if no language is detected.
|
||||
*/
|
||||
const resolveLanguage = (languageString: string): Optional<[string, number]> => {
|
||||
// Edited from https://github.com/lxzxl/koa-i18next-detector/blob/master/src/lookups/header.js
|
||||
const [language, ...rest] = languageString.split(';');
|
||||
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of rest) {
|
||||
const [key, value] = item.split('=');
|
||||
if (key === 'q' && !Number.isNaN(Number(value))) {
|
||||
return [language, Number(value)];
|
||||
}
|
||||
}
|
||||
|
||||
return [language, 1];
|
||||
};
|
||||
|
||||
const normalizeValueToStringArray = (value?: string | string[]): string[] => {
|
||||
if (value) {
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const detectLanguageFromHeaders = (headers: IncomingHttpHeaders): string[] =>
|
||||
headers['accept-language']
|
||||
?.split(',')
|
||||
.map((string) => resolveLanguage(string))
|
||||
.filter((value): value is NonNullable<typeof value> => Boolean(value))
|
||||
.sort((a, b) => b[1] - a[1]) // LOG-81: `.sort()` is a mutation, consider ban it later
|
||||
.map(([locale]) => locale) ?? [];
|
||||
|
||||
const detectLanguage = <StateT, ContextT extends IRouterParamContext, ResponseBodyT>(
|
||||
ctx: ParameterizedContext<StateT, ContextT, ResponseBodyT>
|
||||
): string[] => [
|
||||
...normalizeValueToStringArray(ctx.query.locale),
|
||||
...detectLanguageFromHeaders(ctx.headers),
|
||||
];
|
||||
|
||||
export default detectLanguage;
|
|
@ -3,7 +3,8 @@ import i18next from 'i18next';
|
|||
|
||||
export default async function initI18n() {
|
||||
await i18next.init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: Object.keys(resources),
|
||||
resources,
|
||||
});
|
||||
}
|
||||
|
|
34
packages/core/src/middleware/koa-i18next.ts
Normal file
34
packages/core/src/middleware/koa-i18next.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import i18next from 'i18next';
|
||||
import { MiddlewareType } from 'koa';
|
||||
import { IRouterParamContext } from 'koa-router';
|
||||
|
||||
import detectLanguage from '@/i18n/detect-language';
|
||||
|
||||
interface LanguageUtils {
|
||||
formatLanguageCode(code: string): string;
|
||||
isSupportedCode(code: string): boolean;
|
||||
}
|
||||
|
||||
export type WithI18nContext<ContextT extends IRouterParamContext = IRouterParamContext> =
|
||||
ContextT & {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function koaI18next<
|
||||
StateT,
|
||||
ContextT extends IRouterParamContext,
|
||||
ResponseBodyT
|
||||
>(): MiddlewareType<StateT, WithI18nContext<ContextT>, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
const languages = detectLanguage(ctx);
|
||||
// Cannot patch type def directly, see https://github.com/microsoft/TypeScript/issues/36146
|
||||
const languageUtils = i18next.services.languageUtils as LanguageUtils;
|
||||
const found = languages
|
||||
.map((code) => languageUtils.formatLanguageCode(code))
|
||||
.find((code) => languageUtils.isSupportedCode(code));
|
||||
|
||||
await i18next.changeLanguage(found);
|
||||
ctx.locale = i18next.language;
|
||||
return next();
|
||||
};
|
||||
}
|
|
@ -25,8 +25,8 @@
|
|||
"url": "https://github.com/logto-io/logto/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/eslint-config": "^0.1.2",
|
||||
"@logto/ts-config": "^0.1.2",
|
||||
"@logto/eslint-config": "^0.1.3",
|
||||
"@logto/ts-config": "^0.1.3",
|
||||
"eslint": "^7.32.0",
|
||||
"lint-staged": "^11.1.1",
|
||||
"prettier": "^2.3.2",
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
"node": ">=14.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/eslint-config": "^0.1.2",
|
||||
"@logto/eslint-config": "^0.1.3",
|
||||
"@logto/essentials": "^1.1.0-rc.2",
|
||||
"@logto/ts-config": "^0.1.2",
|
||||
"@logto/ts-config": "^0.1.3",
|
||||
"@types/lodash.uniq": "^4.5.6",
|
||||
"@types/node": "14",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
|
|
|
@ -31,10 +31,10 @@
|
|||
"devDependencies": {
|
||||
"@babel/core": "^7.14.6",
|
||||
"@jest/types": "^27.0.6",
|
||||
"@logto/eslint-config": "^0.1.2",
|
||||
"@logto/eslint-config-react": "^0.1.2",
|
||||
"@logto/ts-config": "^0.1.2",
|
||||
"@logto/ts-config-react": "^0.1.2",
|
||||
"@logto/eslint-config": "^0.1.3",
|
||||
"@logto/eslint-config-react": "^0.1.3",
|
||||
"@logto/ts-config": "^0.1.3",
|
||||
"@logto/ts-config-react": "^0.1.3",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/react": "^17.0.14",
|
||||
|
|
|
@ -20,11 +20,11 @@ importers:
|
|||
|
||||
packages/core:
|
||||
specifiers:
|
||||
'@logto/eslint-config': ^0.1.2
|
||||
'@logto/eslint-config': ^0.1.3
|
||||
'@logto/essentials': ^1.1.0-rc.2
|
||||
'@logto/phrases': ^0.1.0
|
||||
'@logto/schemas': ^0.1.0
|
||||
'@logto/ts-config': ^0.1.2
|
||||
'@logto/ts-config': ^0.1.3
|
||||
'@types/jest': ^27.0.1
|
||||
'@types/koa': ^2.13.3
|
||||
'@types/koa-logger': ^3.1.1
|
||||
|
@ -90,8 +90,8 @@ importers:
|
|||
slonik-interceptor-preset: 1.2.10
|
||||
zod: 3.8.1
|
||||
devDependencies:
|
||||
'@logto/eslint-config': 0.1.2_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/ts-config': 0.1.2_typescript@4.3.5
|
||||
'@logto/eslint-config': 0.1.3_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/ts-config': 0.1.3_typescript@4.3.5
|
||||
'@types/jest': 27.0.1
|
||||
'@types/koa': 2.13.4
|
||||
'@types/koa-logger': 3.1.1
|
||||
|
@ -113,15 +113,15 @@ importers:
|
|||
|
||||
packages/phrases:
|
||||
specifiers:
|
||||
'@logto/eslint-config': ^0.1.2
|
||||
'@logto/ts-config': ^0.1.2
|
||||
'@logto/eslint-config': ^0.1.3
|
||||
'@logto/ts-config': ^0.1.3
|
||||
eslint: ^7.32.0
|
||||
lint-staged: ^11.1.1
|
||||
prettier: ^2.3.2
|
||||
typescript: ^4.3.5
|
||||
devDependencies:
|
||||
'@logto/eslint-config': 0.1.2_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/ts-config': 0.1.2_typescript@4.3.5
|
||||
'@logto/eslint-config': 0.1.3_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/ts-config': 0.1.3_typescript@4.3.5
|
||||
eslint: 7.32.0
|
||||
lint-staged: 11.1.1
|
||||
prettier: 2.3.2
|
||||
|
@ -129,10 +129,10 @@ importers:
|
|||
|
||||
packages/schemas:
|
||||
specifiers:
|
||||
'@logto/eslint-config': ^0.1.2
|
||||
'@logto/eslint-config': ^0.1.3
|
||||
'@logto/essentials': ^1.1.0-rc.2
|
||||
'@logto/phrases': ^0.1.0
|
||||
'@logto/ts-config': ^0.1.2
|
||||
'@logto/ts-config': ^0.1.3
|
||||
'@types/lodash.uniq': ^4.5.6
|
||||
'@types/node': '14'
|
||||
'@types/pluralize': ^0.0.29
|
||||
|
@ -148,9 +148,9 @@ importers:
|
|||
dependencies:
|
||||
'@logto/phrases': link:../phrases
|
||||
devDependencies:
|
||||
'@logto/eslint-config': 0.1.2_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/eslint-config': 0.1.3_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/essentials': 1.1.0-rc.2
|
||||
'@logto/ts-config': 0.1.2_typescript@4.3.5
|
||||
'@logto/ts-config': 0.1.3_typescript@4.3.5
|
||||
'@types/lodash.uniq': 4.5.6
|
||||
'@types/node': 14.17.6
|
||||
'@types/pluralize': 0.0.29
|
||||
|
@ -168,12 +168,12 @@ importers:
|
|||
specifiers:
|
||||
'@babel/core': ^7.14.6
|
||||
'@jest/types': ^27.0.6
|
||||
'@logto/eslint-config': ^0.1.2
|
||||
'@logto/eslint-config-react': ^0.1.2
|
||||
'@logto/eslint-config': ^0.1.3
|
||||
'@logto/eslint-config-react': ^0.1.3
|
||||
'@logto/phrases': ^0.1.0
|
||||
'@logto/schemas': ^0.1.0
|
||||
'@logto/ts-config': ^0.1.2
|
||||
'@logto/ts-config-react': ^0.1.2
|
||||
'@logto/ts-config': ^0.1.3
|
||||
'@logto/ts-config-react': ^0.1.3
|
||||
'@testing-library/react': ^12.0.0
|
||||
'@types/jest': ^26.0.24
|
||||
'@types/react': ^17.0.14
|
||||
|
@ -218,10 +218,10 @@ importers:
|
|||
devDependencies:
|
||||
'@babel/core': 7.14.8
|
||||
'@jest/types': 27.0.6
|
||||
'@logto/eslint-config': 0.1.2_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/eslint-config-react': 0.1.2_8e322dd0e62beacbfb7b944fe3d15c43
|
||||
'@logto/ts-config': 0.1.2_typescript@4.3.5
|
||||
'@logto/ts-config-react': 0.1.2_typescript@4.3.5
|
||||
'@logto/eslint-config': 0.1.3_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/eslint-config-react': 0.1.3_8e322dd0e62beacbfb7b944fe3d15c43
|
||||
'@logto/ts-config': 0.1.3_typescript@4.3.5
|
||||
'@logto/ts-config-react': 0.1.3_typescript@4.3.5
|
||||
'@testing-library/react': 12.0.0_react-dom@17.0.2+react@17.0.2
|
||||
'@types/jest': 26.0.24
|
||||
'@types/react': 17.0.15
|
||||
|
@ -2792,12 +2792,12 @@ packages:
|
|||
write-file-atomic: 3.0.3
|
||||
dev: true
|
||||
|
||||
/@logto/eslint-config-react/0.1.2_8e322dd0e62beacbfb7b944fe3d15c43:
|
||||
resolution: {integrity: sha512-8bBRmfv6zg+W9vFLhBidX5qTFxtz2eKaCNCz5+Ax3KGPKQ+aBNOjULZ8ZO5Xt28+EvQktEMUBEm6P5Pq5vlboQ==}
|
||||
/@logto/eslint-config-react/0.1.3_8e322dd0e62beacbfb7b944fe3d15c43:
|
||||
resolution: {integrity: sha512-TGQ10/SPpT18KgL9oqX1YDeS3JPnaUdAVViXps+CaDNfuoIkStytnLKKeNqt4Jb9YeEo69345zsk0pdFoERoMw==}
|
||||
peerDependencies:
|
||||
stylelint: ^13.13.1
|
||||
dependencies:
|
||||
'@logto/eslint-config': 0.1.2_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
'@logto/eslint-config': 0.1.3_aff669e8eb0d21fc4e2068e6112ef4d0
|
||||
eslint-config-xo-react: 0.25.0_34cd3168eeae4de23db8343b5dfd9fdd
|
||||
eslint-plugin-react: 7.25.0_eslint@7.32.0
|
||||
eslint-plugin-react-hooks: 4.2.0_eslint@7.32.0
|
||||
|
@ -2810,8 +2810,8 @@ packages:
|
|||
- typescript
|
||||
dev: true
|
||||
|
||||
/@logto/eslint-config/0.1.2_aff669e8eb0d21fc4e2068e6112ef4d0:
|
||||
resolution: {integrity: sha512-RnyBvzZjm6osGkKPMwZ30mZCd0jsOKCg43dMeoQsrnZIQAbdNKn0TiKx5VHQITOyq2gwVnugWIKtKU7vK5Vt7g==}
|
||||
/@logto/eslint-config/0.1.3_aff669e8eb0d21fc4e2068e6112ef4d0:
|
||||
resolution: {integrity: sha512-Pv7sZHX/blbACaCzeg5ZxD6QNPg0J8wirwhyONVvAtiVPvIhCuIuygJF4xqhsuXUTl4ebEycbHPhbJHQGZRTTQ==}
|
||||
engines: {node: '>=14.15.0'}
|
||||
peerDependencies:
|
||||
eslint: ^7.32.0
|
||||
|
@ -2846,18 +2846,18 @@ packages:
|
|||
lodash.orderby: 4.6.0
|
||||
lodash.pick: 4.4.0
|
||||
|
||||
/@logto/ts-config-react/0.1.2_typescript@4.3.5:
|
||||
resolution: {integrity: sha512-Ivwce+4w5mOi1FdfUdgVElAd19iieD6r9LKUqgePipuCc9jJpVxeLc9xHVxxhaAohJdSd0rLoRICJhCzqZBwlA==}
|
||||
/@logto/ts-config-react/0.1.3_typescript@4.3.5:
|
||||
resolution: {integrity: sha512-whl0l8jRwSSBfoJ6kV1Kx0zvxvkPlf3BhHlC3u68HsxmK2Z0VJi+aIM/uaJ6pQfsMTTayEVmVAE2mZBHV1ez3g==}
|
||||
engines: {node: '>=14.15.0'}
|
||||
peerDependencies:
|
||||
typescript: ^4.3.5
|
||||
dependencies:
|
||||
'@logto/ts-config': 0.1.2_typescript@4.3.5
|
||||
'@logto/ts-config': 0.1.3_typescript@4.3.5
|
||||
typescript: 4.3.5
|
||||
dev: true
|
||||
|
||||
/@logto/ts-config/0.1.2_typescript@4.3.5:
|
||||
resolution: {integrity: sha512-aJZAhaQIVKxG1Ixexj/ksUTNhJVcE10EZHczF47bwsNGlRYR4FBHATca3S5bgEVnfRIJnR3uPwpbKnJu7TvM+w==}
|
||||
/@logto/ts-config/0.1.3_typescript@4.3.5:
|
||||
resolution: {integrity: sha512-AN3RBa2P0zVtRhwvLfHbUdco1II421iwHYAnshlqpKSbnu++lnYGfA+EYg6p5F6bKaQ8beYRakKqBUlVDtyGug==}
|
||||
engines: {node: '>=14.15.0'}
|
||||
peerDependencies:
|
||||
typescript: ^4.3.5
|
||||
|
|
Loading…
Reference in a new issue