0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

feat(core,phrases): add GET /phrase route (#1959)

This commit is contained in:
IceHe 2022-09-27 15:36:03 +08:00 committed by GitHub
parent 505985802b
commit 7ce55a8458
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 619 additions and 36 deletions

View file

@ -25,9 +25,11 @@
"@logto/connector-kit": "^1.0.0-beta.13",
"@logto/core-kit": "^1.0.0-beta.13",
"@logto/phrases": "^1.0.0-beta.9",
"@logto/phrases-ui": "^1.0.0-beta.9",
"@logto/schemas": "^1.0.0-beta.9",
"@silverhand/essentials": "^1.2.1",
"chalk": "^4",
"clean-deep": "^3.4.0",
"dayjs": "^1.10.5",
"debug": "^4.3.4",
"decamelize": "^5.0.0",

View file

@ -0,0 +1,67 @@
import en from '@logto/phrases-ui/lib/locales/en';
export const enKey = 'en';
export const koKrKey = 'ko-KR';
export const trTrKey = 'tr-TR';
export const zhCnKey = 'zh-CN';
export const zhHkKey = 'zh-HK';
export const mockEnCustomPhrase = {
languageKey: enKey,
translation: {
input: {
username: 'Username 1',
password: 'Password 2',
email: 'Email 3',
phone_number: 'Phone number 4',
confirm_password: 'Confirm password 5',
},
},
};
export const mockEnPhrase = {
languageKey: enKey,
translation: {
...en.translation,
...mockEnCustomPhrase.translation,
},
};
export const mockTrTrCustomPhrase = {
languageKey: trTrKey,
translation: {
input: {
username: 'Kullanıcı Adı 1',
password: 'Şifre 2',
email: 'E-posta Adresi 3',
phone_number: 'Telefon Numarası 4',
confirm_password: 'Şifreyi Doğrula 5',
},
},
};
export const mockZhCnCustomPhrase = {
languageKey: zhCnKey,
translation: {
input: {
username: '用户名 1',
password: '密码 2',
email: '邮箱 3',
phone_number: '手机号 4',
confirm_password: '确认密码 5',
},
},
};
export const mockZhHkCustomPhrase = {
languageKey: zhHkKey,
translation: {
input: {
email: '郵箱 1',
password: '密碼 2',
username: '用戶名 3',
phone_number: '手機號 4',
confirm_password: '確認密碼 5',
},
},
};

View file

@ -0,0 +1,112 @@
import resource from '@logto/phrases-ui';
import { CustomPhrase } from '@logto/schemas';
import deepmerge from 'deepmerge';
import {
enKey,
mockEnCustomPhrase,
mockZhCnCustomPhrase,
mockZhHkCustomPhrase,
trTrKey,
zhCnKey,
zhHkKey,
} from '@/__mocks__/custom-phrase';
import RequestError from '@/errors/RequestError';
import { getPhrase } from '@/lib/phrase';
const englishBuiltInPhrase = resource[enKey];
const customOnlyLanguage = zhHkKey;
const customOnlyCustomPhrase = mockZhHkCustomPhrase;
const customizedLanguage = zhCnKey;
const customizedBuiltInPhrase = resource[zhCnKey];
const customizedCustomPhrase = mockZhCnCustomPhrase;
const mockCustomPhrases: Record<string, CustomPhrase> = {
[enKey]: mockEnCustomPhrase,
[customOnlyLanguage]: customOnlyCustomPhrase,
[customizedLanguage]: customizedCustomPhrase,
};
const findCustomPhraseByLanguageKey = jest.fn(async (languageKey: string) => {
const mockCustomPhrase = mockCustomPhrases[languageKey];
if (!mockCustomPhrase) {
throw new RequestError({ code: 'entity.not_found', status: 404 });
}
return mockCustomPhrase;
});
jest.mock('@/queries/custom-phrase', () => ({
findCustomPhraseByLanguageKey: async (key: string) => findCustomPhraseByLanguageKey(key),
}));
afterEach(() => {
jest.clearAllMocks();
});
it('should ignore empty string values from the custom phrase', async () => {
const mockTranslationInput = {
email: 'Email 3',
phone_number: 'Phone number 4',
confirm_password: 'Confirm password 5',
};
const mockEnCustomPhraseWithEmptyStringValues = {
languageKey: enKey,
translation: {
input: {
...resource.en.translation.input,
...mockTranslationInput,
username: '',
password: '',
},
},
};
findCustomPhraseByLanguageKey.mockResolvedValueOnce(mockEnCustomPhraseWithEmptyStringValues);
await expect(getPhrase(enKey, [enKey])).resolves.toEqual(
deepmerge(englishBuiltInPhrase, {
languageKey: enKey,
translation: {
input: {
...resource.en.translation.input,
...mockTranslationInput,
},
},
})
);
});
describe('when the language is English', () => {
it('should be English custom phrase merged with its built-in phrase when its custom phrase exists', async () => {
await expect(getPhrase(enKey, [enKey])).resolves.toEqual(
deepmerge(englishBuiltInPhrase, mockEnCustomPhrase)
);
});
it('should be English built-in phrase when its custom phrase does not exist', async () => {
await expect(getPhrase(enKey, [])).resolves.toEqual(englishBuiltInPhrase);
});
});
describe('when the language is not English', () => {
it('should be custom phrase merged with built-in phrase when both of them exist', async () => {
await expect(getPhrase(customizedLanguage, [customizedLanguage])).resolves.toEqual(
deepmerge(customizedBuiltInPhrase, customizedCustomPhrase)
);
});
it('should be built-in phrase when there is built-in phrase and no custom phrase', async () => {
const builtInOnlyLanguage = trTrKey;
const builtInOnlyPhrase = resource[trTrKey];
await expect(getPhrase(builtInOnlyLanguage, [])).resolves.toEqual(builtInOnlyPhrase);
});
it('should be built-in phrase when there is custom phrase and no built-in phrase', async () => {
await expect(getPhrase(customOnlyLanguage, [customOnlyLanguage])).resolves.toEqual(
deepmerge(englishBuiltInPhrase, customOnlyCustomPhrase)
);
});
});

View file

@ -0,0 +1,28 @@
import { LanguageKey } from '@logto/core-kit';
import resource, { LocalePhrase } from '@logto/phrases-ui';
import { CustomPhrase } from '@logto/schemas';
import cleanDeep from 'clean-deep';
import deepmerge from 'deepmerge';
import { findCustomPhraseByLanguageKey } from '@/queries/custom-phrase';
export const isBuiltInLanguage = (key: string): key is LanguageKey =>
Object.keys(resource).includes(key);
export const getPhrase = async (supportedLanguage: string, customLanguages: string[]) => {
if (!isBuiltInLanguage(supportedLanguage)) {
return deepmerge<LocalePhrase, CustomPhrase>(
resource.en,
cleanDeep(await findCustomPhraseByLanguageKey(supportedLanguage))
);
}
if (!customLanguages.includes(supportedLanguage)) {
return resource[supportedLanguage];
}
return deepmerge<LocalePhrase, CustomPhrase>(
resource[supportedLanguage],
cleanDeep(await findCustomPhraseByLanguageKey(supportedLanguage))
);
};

View file

@ -4,6 +4,7 @@ import koaBody from 'koa-body';
import { IMiddleware, IRouterParamContext } from 'koa-router';
import { ZodType } from 'zod';
import envSet from '@/env-set';
import RequestError from '@/errors/RequestError';
import ServerError from '@/errors/ServerError';
import assertThat from '@/utils/assert-that';
@ -114,7 +115,14 @@ export default function koaGuard<
}
if (response !== undefined) {
assertThat(response.safeParse(ctx.body).success, new ServerError());
const result = response.safeParse(ctx.body);
if (!result.success) {
if (!envSet.values.isProduction) {
console.error('Invalid response:', result.error);
}
throw new ServerError();
}
}
};

View file

@ -8,6 +8,18 @@ import { DeletionError } from '@/errors/SlonikError';
const { table, fields } = convertToIdentifiers(CustomPhrases);
export const findAllCustomLanguageKeys = async () => {
const rows = await manyRows<{ languageKey: string }>(
envSet.pool.query(sql`
select ${fields.languageKey}
from ${table}
order by ${fields.languageKey}
`)
);
return rows.map((row) => row.languageKey);
};
export const findAllCustomPhrases = async () =>
manyRows(
envSet.pool.query<CustomPhrase>(sql`

View file

@ -1,25 +1,15 @@
import { CustomPhrase } from '@logto/schemas';
import { CustomPhrase, SignInExperience } from '@logto/schemas';
import { mockSignInExperience } from '@/__mocks__';
import { mockZhCnCustomPhrase, trTrKey, zhCnKey } from '@/__mocks__/custom-phrase';
import RequestError from '@/errors/RequestError';
import customPhraseRoutes from '@/routes/custom-phrase';
import { createRequester } from '@/utils/test-utils';
const mockLanguageKey = 'en-US';
const mockLanguageKey = zhCnKey;
const mockPhrase = mockZhCnCustomPhrase;
const mockCustomPhrases: Record<string, CustomPhrase> = {
[mockLanguageKey]: {
languageKey: mockLanguageKey,
translation: {
input: {
username: 'Username',
password: 'Password',
email: 'Email',
phone_number: 'Phone number',
confirm_password: 'Confirm password',
},
},
},
[mockLanguageKey]: mockPhrase,
};
const deleteCustomPhraseByLanguageKey = jest.fn(async (languageKey: string) => {
@ -40,7 +30,7 @@ const findCustomPhraseByLanguageKey = jest.fn(async (languageKey: string) => {
const findAllCustomPhrases = jest.fn(async (): Promise<CustomPhrase[]> => []);
const upsertCustomPhrase = jest.fn(async (customPhrase: CustomPhrase) => customPhrase);
const upsertCustomPhrase = jest.fn(async (customPhrase: CustomPhrase) => mockPhrase);
jest.mock('@/queries/custom-phrase', () => ({
deleteCustomPhraseByLanguageKey: async (key: string) => deleteCustomPhraseByLanguageKey(key),
@ -49,7 +39,18 @@ jest.mock('@/queries/custom-phrase', () => ({
upsertCustomPhrase: async (customPhrase: CustomPhrase) => upsertCustomPhrase(customPhrase),
}));
const findDefaultSignInExperience = jest.fn(async () => mockSignInExperience);
const mockFallbackLanguage = trTrKey;
const findDefaultSignInExperience = jest.fn(
async (): Promise<SignInExperience> => ({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage: mockFallbackLanguage,
fixedLanguage: mockFallbackLanguage,
},
})
);
jest.mock('@/queries/sign-in-experience', () => ({
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
@ -108,7 +109,7 @@ describe('customPhraseRoutes', () => {
expect(upsertCustomPhrase).toBeCalledWith(mockCustomPhrases[mockLanguageKey]);
});
it('should return the custom phrase after upserting', async () => {
it('should return custom phrase after upserting', async () => {
const response = await customPhraseRequest
.put(`/custom-phrases/${mockLanguageKey}`)
.send(mockCustomPhrases[mockLanguageKey]?.translation);
@ -118,23 +119,23 @@ describe('customPhraseRoutes', () => {
});
describe('DELETE /custom-phrases/:languageKey', () => {
it('should call deleteCustomPhraseByLanguageKey', async () => {
it('should call deleteCustomPhraseByLanguageKey when custom phrase exists and is not fallback language in sign-in experience', async () => {
await customPhraseRequest.delete(`/custom-phrases/${mockLanguageKey}`);
expect(deleteCustomPhraseByLanguageKey).toBeCalledWith(mockLanguageKey);
});
it('should return 204 status code after deleting the specified custom phrase', async () => {
it('should return 204 status code after deleting specified custom phrase', async () => {
const response = await customPhraseRequest.delete(`/custom-phrases/${mockLanguageKey}`);
expect(response.status).toEqual(204);
});
it('should return 404 status code when the specified custom phrase does not exist before deleting', async () => {
const response = await customPhraseRequest.delete('/custom-phrases/en-UK');
it('should return 404 status code when specified custom phrase does not exist before deleting', async () => {
const response = await customPhraseRequest.delete('/custom-phrases/en-GB');
expect(response.status).toEqual(404);
});
it('should return 400 status code when the specified custom phrase is used as default language in sign-in experience', async () => {
const response = await customPhraseRequest.delete('/custom-phrases/en');
it('should return 400 status code when specified custom phrase is used as fallback language in sign-in experience', async () => {
const response = await customPhraseRequest.delete(`/custom-phrases/${mockFallbackLanguage}`);
expect(response.status).toEqual(400);
});
});

View file

@ -13,6 +13,7 @@ import connectorRoutes from '@/routes/connector';
import customPhraseRoutes from '@/routes/custom-phrase';
import dashboardRoutes from '@/routes/dashboard';
import logRoutes from '@/routes/log';
import phraseRoutes from '@/routes/phrase';
import resourceRoutes from '@/routes/resource';
import roleRoutes from '@/routes/role';
import sessionRoutes from '@/routes/session';
@ -43,6 +44,7 @@ const createRouters = (provider: Provider) => {
customPhraseRoutes(managementRouter);
const anonymousRouter: AnonymousRouter = new Router();
phraseRoutes(anonymousRouter, provider);
wellKnownRoutes(anonymousRouter, provider);
statusRoutes(anonymousRouter);
authnRoutes(anonymousRouter);

View file

@ -0,0 +1,152 @@
import en from '@logto/phrases-ui/lib/locales/en';
import { Provider } from 'oidc-provider';
import { mockSignInExperience } from '@/__mocks__';
import { trTrKey, zhCnKey, zhHkKey } from '@/__mocks__/custom-phrase';
import phraseRoutes from '@/routes/phrase';
import { createRequester } from '@/utils/test-utils';
const mockApplicationId = 'mockApplicationIdValue';
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({
params: { client_id: mockApplicationId },
}));
jest.mock('oidc-provider', () => ({
Provider: jest.fn(() => ({
interactionDetails,
})),
}));
const fallbackLanguage = trTrKey;
const unsupportedLanguageX = 'xx-XX';
const unsupportedLanguageY = 'yy-YY';
const findDefaultSignInExperience = jest.fn(async () => ({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage,
fixedLanguage: fallbackLanguage,
},
}));
jest.mock('@/queries/sign-in-experience', () => ({
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
}));
jest.mock('@/queries/custom-phrase', () => ({
findAllCustomLanguageKeys: async () => [trTrKey, zhCnKey],
}));
jest.mock('@/lib/phrase', () => ({
...jest.requireActual('@/lib/phrase'),
getPhrase: jest.fn().mockResolvedValue(en),
}));
const phraseRequest = createRequester({
anonymousRoutes: phraseRoutes,
provider: new Provider(''),
});
afterEach(() => {
jest.clearAllMocks();
});
describe('when auto-detect is not enabled', () => {
it('should be English when fallback language is unsupported', async () => {
findDefaultSignInExperience.mockResolvedValueOnce({
...mockSignInExperience,
languageInfo: {
autoDetect: false,
fallbackLanguage: unsupportedLanguageX,
fixedLanguage: unsupportedLanguageX,
},
});
const response = await phraseRequest
.get('/phrase')
.set('Accept-Language', `${zhCnKey},${zhHkKey}`);
expect(response.headers['content-language']).toEqual('en');
});
describe('should be fallback language', () => {
beforeEach(() => {
findDefaultSignInExperience.mockResolvedValueOnce({
...mockSignInExperience,
languageInfo: {
autoDetect: false,
fallbackLanguage,
fixedLanguage: fallbackLanguage,
},
});
});
it('when there is no detected language', async () => {
const response = await phraseRequest.get('/phrase');
expect(response.headers['content-language']).toEqual(fallbackLanguage);
});
it('when there are detected languages', async () => {
const response = await phraseRequest
.get('/phrase')
.set('Accept-Language', `${zhCnKey},${zhHkKey}`);
expect(response.headers['content-language']).toEqual(fallbackLanguage);
});
});
});
describe('when auto-detect is enabled', () => {
it('should be English when fallback language is unsupported', async () => {
findDefaultSignInExperience.mockResolvedValueOnce({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage: unsupportedLanguageX,
fixedLanguage: unsupportedLanguageX,
},
});
const response = await phraseRequest
.get('/phrase')
.set('Accept-Language', unsupportedLanguageY);
expect(response.headers['content-language']).toEqual('en');
});
describe('when fallback language is supported', () => {
beforeEach(() => {
findDefaultSignInExperience.mockResolvedValueOnce({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage,
fixedLanguage: fallbackLanguage,
},
});
});
describe('when there is no detected language', () => {
it('should be fallback language from sign-in experience', async () => {
const response = await phraseRequest.get('/phrase');
expect(response.headers['content-language']).toEqual(fallbackLanguage);
});
});
describe('when there are detected languages but all of them is unsupported', () => {
it('should be first supported detected language', async () => {
const response = await phraseRequest
.get('/phrase')
.set('Accept-Language', `${unsupportedLanguageX},${unsupportedLanguageY}`);
expect(response.headers['content-language']).toEqual(fallbackLanguage);
});
});
describe('when there are detected languages but some of them is unsupported', () => {
it('should be first supported detected language', async () => {
const firstSupportedLanguage = zhCnKey;
const response = await phraseRequest
.get('/phrase')
.set('Accept-Language', `${unsupportedLanguageX},${firstSupportedLanguage},${zhHkKey}`);
expect(response.headers['content-language']).toEqual(firstSupportedLanguage);
});
});
});
});

View file

@ -0,0 +1,157 @@
import zhCN from '@logto/phrases-ui/lib/locales/zh-cn';
import { SignInExperience } from '@logto/schemas';
import { adminConsoleApplicationId, adminConsoleSignInExperience } from '@logto/schemas/lib/seeds';
import { Provider } from 'oidc-provider';
import { mockSignInExperience } from '@/__mocks__';
import { zhCnKey } from '@/__mocks__/custom-phrase';
import * as detectLanguage from '@/i18n/detect-language';
import phraseRoutes from '@/routes/phrase';
import { createRequester } from '@/utils/test-utils';
const mockApplicationId = 'mockApplicationIdValue';
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({
params: { client_id: mockApplicationId },
}));
jest.mock('oidc-provider', () => ({
Provider: jest.fn(() => ({
interactionDetails,
})),
}));
const customizedLanguage = zhCnKey;
const findDefaultSignInExperience = jest.fn(
async (): Promise<SignInExperience> => ({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage: customizedLanguage,
fixedLanguage: customizedLanguage,
},
})
);
jest.mock('@/queries/sign-in-experience', () => ({
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
}));
const detectLanguageSpy = jest.spyOn(detectLanguage, 'default');
const findAllCustomLanguageKeys = jest.fn(async () => [customizedLanguage]);
const findCustomPhraseByLanguageKey = jest.fn(async (key: string) => ({}));
jest.mock('@/queries/custom-phrase', () => ({
findAllCustomLanguageKeys: async () => findAllCustomLanguageKeys(),
findCustomPhraseByLanguageKey: async (key: string) => findCustomPhraseByLanguageKey(key),
}));
const getPhrase = jest.fn(async (language: string, customLanguages: string[]) => zhCN);
jest.mock('@/lib/phrase', () => ({
...jest.requireActual('@/lib/phrase'),
getPhrase: async (language: string, customLanguages: string[]) =>
getPhrase(language, customLanguages),
}));
const phraseRequest = createRequester({
anonymousRoutes: phraseRoutes,
provider: new Provider(''),
});
afterEach(() => {
jest.clearAllMocks();
});
describe('when the application is admin-console', () => {
beforeEach(() => {
interactionDetails.mockResolvedValueOnce({
params: { client_id: adminConsoleApplicationId },
});
});
it('should call interactionDetails', async () => {
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(interactionDetails).toBeCalledTimes(1);
});
it('should not call findDefaultSignInExperience', async () => {
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(findDefaultSignInExperience).not.toBeCalled();
});
it('should call detectLanguage', async () => {
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(detectLanguageSpy).toBeCalledTimes(1);
});
it('should call findAllCustomLanguageKeys', async () => {
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(findAllCustomLanguageKeys).toBeCalledTimes(1);
});
it('should call getPhrase with fallback language from Admin Console sign-in experience', async () => {
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(getPhrase).toBeCalledTimes(1);
expect(getPhrase).toBeCalledWith(adminConsoleSignInExperience.languageInfo.fallbackLanguage, [
customizedLanguage,
]);
});
});
describe('when the application is not admin-console', () => {
it('should call interactionDetails', async () => {
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(interactionDetails).toBeCalledTimes(1);
});
it('should call findDefaultSignInExperience', async () => {
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(findDefaultSignInExperience).toBeCalledTimes(1);
});
it('should call detectLanguage when auto-detect is enabled', async () => {
findDefaultSignInExperience.mockResolvedValueOnce({
...mockSignInExperience,
languageInfo: {
...mockSignInExperience.languageInfo,
autoDetect: true,
},
});
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(detectLanguageSpy).toBeCalledTimes(1);
});
it('should not call detectLanguage when auto-detect is not enabled', async () => {
findDefaultSignInExperience.mockResolvedValueOnce({
...mockSignInExperience,
languageInfo: {
...mockSignInExperience.languageInfo,
autoDetect: false,
},
});
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(detectLanguageSpy).not.toBeCalled();
});
it('should call findAllCustomLanguageKeys', async () => {
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(findAllCustomLanguageKeys).toBeCalledTimes(1);
});
it('should call getPhrase with fallback language from default sign-in experience', async () => {
findDefaultSignInExperience.mockResolvedValueOnce({
...mockSignInExperience,
languageInfo: {
autoDetect: false,
fallbackLanguage: customizedLanguage,
fixedLanguage: customizedLanguage,
},
});
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(getPhrase).toBeCalledTimes(1);
expect(getPhrase).toBeCalledWith(customizedLanguage, [customizedLanguage]);
});
});

View file

@ -0,0 +1,43 @@
import { adminConsoleApplicationId, adminConsoleSignInExperience } from '@logto/schemas/lib/seeds';
import { Provider } from 'oidc-provider';
import detectLanguage from '@/i18n/detect-language';
import { isBuiltInLanguage, getPhrase } from '@/lib/phrase';
import { findAllCustomLanguageKeys } from '@/queries/custom-phrase';
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
import { AnonymousRouter } from './types';
const getLanguageInfo = async (applicationId: unknown) => {
if (applicationId === adminConsoleApplicationId) {
return adminConsoleSignInExperience.languageInfo;
}
const { languageInfo } = await findDefaultSignInExperience();
return languageInfo;
};
export default function phraseRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
router.get('/phrase', async (ctx, next) => {
const interaction = await provider
.interactionDetails(ctx.req, ctx.res)
// Should not block when failed to get interaction
.catch(() => null);
const applicationId = interaction?.params.client_id;
const { autoDetect, fallbackLanguage } = await getLanguageInfo(applicationId);
const detectedLanguages = autoDetect ? detectLanguage(ctx) : [];
const acceptableLanguages = [...detectedLanguages, fallbackLanguage];
const customLanguages = await findAllCustomLanguageKeys();
const language =
acceptableLanguages.find((key) => isBuiltInLanguage(key) || customLanguages.includes(key)) ??
'en';
ctx.set('Content-Language', language);
ctx.body = await getPhrase(language, customLanguages);
return next();
});
}

View file

@ -6,11 +6,13 @@ import koKR from './locales/ko-kr';
import ptPT from './locales/pt-pt';
import trTR from './locales/tr-tr';
import zhCN from './locales/zh-cn';
import { Resource, Translation } from './types';
import { Resource } from './types';
export * from './types';
export { languageCodeAndDisplayNameMappings, languageOptions } from './types';
export type I18nKey = NormalizeKeyPaths<Translation>;
export type { LocalePhrase } from './types';
export type I18nKey = NormalizeKeyPaths<typeof en.translation>;
const resource: Resource = {
en,

View file

@ -2,10 +2,7 @@ import { LanguageKey, languageKeyGuard } from '@logto/core-kit';
import en from './locales/en';
export type Translation = typeof en.translation;
export type LocalePhrase = { translation: Translation };
/* Copied from i18next/index.d.ts */
export type LocalePhrase = typeof en;
export type Resource = Record<LanguageKey, LocalePhrase>;
export const languageCodeAndDisplayNameMappings: Record<LanguageKey, string> = {

8
pnpm-lock.yaml generated
View file

@ -155,6 +155,7 @@ importers:
'@logto/connector-kit': ^1.0.0-beta.13
'@logto/core-kit': ^1.0.0-beta.13
'@logto/phrases': ^1.0.0-beta.9
'@logto/phrases-ui': ^1.0.0-beta.9
'@logto/schemas': ^1.0.0-beta.9
'@shopify/jest-koa-mocks': ^5.0.0
'@silverhand/eslint-config': 1.0.0
@ -179,6 +180,7 @@ importers:
'@types/supertest': ^2.0.11
'@types/tar': ^6.1.2
chalk: ^4
clean-deep: ^3.4.0
copyfiles: ^2.4.1
dayjs: ^1.10.5
debug: ^4.3.4
@ -231,9 +233,11 @@ importers:
'@logto/connector-kit': 1.0.0-beta.13
'@logto/core-kit': 1.0.0-beta.13
'@logto/phrases': link:../phrases
'@logto/phrases-ui': link:../phrases-ui
'@logto/schemas': link:../schemas
'@silverhand/essentials': 1.2.1
chalk: 4.1.2
clean-deep: 3.4.0
dayjs: 1.10.7
debug: 4.3.4
decamelize: 5.0.1
@ -5579,7 +5583,6 @@ packages:
lodash.isempty: 4.4.0
lodash.isplainobject: 4.0.6
lodash.transform: 4.6.0
dev: true
/clean-regexp/1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
@ -10225,7 +10228,6 @@ packages:
/lodash.isempty/4.4.0:
resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==}
dev: true
/lodash.ismatch/4.4.0:
resolution: {integrity: sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=}
@ -10233,7 +10235,6 @@ packages:
/lodash.isplainobject/4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: true
/lodash.kebabcase/4.1.1:
resolution: {integrity: sha1-hImxyw0p/4gZXM7KRI/21swpXDY=}
@ -10279,7 +10280,6 @@ packages:
/lodash.transform/4.6.0:
resolution: {integrity: sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==}
dev: true
/lodash.truncate/4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}