0
Fork 0
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:
Gao Sun 2021-09-01 17:03:04 +08:00
parent 806e99de61
commit 43456aae9b
No known key found for this signature in database
GPG key ID: 0F0EFA2E36639F31
9 changed files with 131 additions and 41 deletions

View file

@ -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",

View file

@ -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);

View 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;

View file

@ -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,
});
}

View 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();
};
}

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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