0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

refactor(core): migrate phrase library to factory mode

This commit is contained in:
Gao Sun 2023-01-10 00:28:41 +08:00
parent 836f3c101d
commit 1102e70226
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
7 changed files with 106 additions and 85 deletions

View file

@ -13,6 +13,7 @@ import {
zhHkTag,
} from '#src/__mocks__/custom-phrase.js';
import RequestError from '#src/errors/RequestError/index.js';
import { MockQueries } from '#src/test-utils/tenant.js';
const { jest } = import.meta;
@ -43,11 +44,10 @@ const findCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => {
return mockCustomPhrase;
});
mockEsm('#src/queries/custom-phrase.js', () => ({
findCustomPhraseByLanguageTag,
}));
const { getPhrase } = await import('#src/libraries/phrase.js');
const { createPhraseLibrary } = await import('#src/libraries/phrase.js');
const { getPhrases } = createPhraseLibrary(
new MockQueries({ customPhrases: { findCustomPhraseByLanguageTag } })
);
afterEach(() => {
jest.clearAllMocks();
@ -72,7 +72,7 @@ it('should ignore empty string values from the custom phrase', async () => {
};
findCustomPhraseByLanguageTag.mockResolvedValueOnce(mockEnCustomPhraseWithEmptyStringValues);
await expect(getPhrase(enTag, [enTag])).resolves.toEqual(
await expect(getPhrases(enTag, [enTag])).resolves.toEqual(
deepmerge(englishBuiltInPhrase, {
languageTag: enTag,
translation: {
@ -87,19 +87,19 @@ it('should ignore empty string values from the custom phrase', async () => {
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(enTag, [enTag])).resolves.toEqual(
await expect(getPhrases(enTag, [enTag])).resolves.toEqual(
deepmerge(englishBuiltInPhrase, mockEnCustomPhrase)
);
});
it('should be English built-in phrase when its custom phrase does not exist', async () => {
await expect(getPhrase(enTag, [])).resolves.toEqual(englishBuiltInPhrase);
await expect(getPhrases(enTag, [])).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(
await expect(getPhrases(customizedLanguage, [customizedLanguage])).resolves.toEqual(
deepmerge(customizedBuiltInPhrase, customizedCustomPhrase)
);
});
@ -107,11 +107,11 @@ describe('when the language is not English', () => {
it('should be built-in phrase when there is built-in phrase and no custom phrase', async () => {
const builtInOnlyLanguage = trTrTag;
const builtInOnlyPhrase = resource[trTrTag];
await expect(getPhrase(builtInOnlyLanguage, [])).resolves.toEqual(builtInOnlyPhrase);
await expect(getPhrases(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(
await expect(getPhrases(customOnlyLanguage, [customOnlyLanguage])).resolves.toEqual(
deepmerge(englishBuiltInPhrase, customOnlyCustomPhrase)
);
});

View file

@ -4,22 +4,28 @@ import type { CustomPhrase } from '@logto/schemas';
import cleanDeep from 'clean-deep';
import deepmerge from 'deepmerge';
import { findCustomPhraseByLanguageTag } from '#src/queries/custom-phrase.js';
import type Queries from '#src/tenants/Queries.js';
export const createPhraseLibrary = (queries: Queries) => {
const { findCustomPhraseByLanguageTag } = queries.customPhrases;
const getPhrases = async (supportedLanguage: string, customLanguages: string[]) => {
if (!isBuiltInLanguageTag(supportedLanguage)) {
return deepmerge<LocalePhrase, CustomPhrase>(
resource.en,
cleanDeep(await findCustomPhraseByLanguageTag(supportedLanguage))
);
}
if (!customLanguages.includes(supportedLanguage)) {
return resource[supportedLanguage];
}
export const getPhrase = async (supportedLanguage: string, customLanguages: string[]) => {
if (!isBuiltInLanguageTag(supportedLanguage)) {
return deepmerge<LocalePhrase, CustomPhrase>(
resource.en,
resource[supportedLanguage],
cleanDeep(await findCustomPhraseByLanguageTag(supportedLanguage))
);
}
};
if (!customLanguages.includes(supportedLanguage)) {
return resource[supportedLanguage];
}
return deepmerge<LocalePhrase, CustomPhrase>(
resource[supportedLanguage],
cleanDeep(await findCustomPhraseByLanguageTag(supportedLanguage))
);
return { getPhrases };
};

View file

@ -26,7 +26,7 @@ import type { LogtoConnector } from '#src/utils/connectors/types.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
const { mockEsm } = createMockUtils(jest);
mockEsm('#src/utils/connectors/platform.js', () => ({
checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(),

View file

@ -1,41 +1,42 @@
import en from '@logto/phrases-ui/lib/locales/en.js';
import { pickDefault, createMockUtils } from '@logto/shared/esm';
import type { SignInExperience } from '@logto/schemas';
import { pickDefault } from '@logto/shared/esm';
import { trTrTag, zhCnTag, zhHkTag } from '#src/__mocks__/custom-phrase.js';
import { mockSignInExperience } from '#src/__mocks__/index.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
const { mockEsmWithActual } = createMockUtils(jest);
const fallbackLanguage = trTrTag;
const unsupportedLanguageX = 'xx-XX';
const unsupportedLanguageY = 'yy-YY';
const { findDefaultSignInExperience } = await mockEsmWithActual(
'#src/queries/sign-in-experience.js',
() => ({
findDefaultSignInExperience: jest.fn(async () => ({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage,
},
})),
const findDefaultSignInExperience = jest.fn(
async (): Promise<SignInExperience> => ({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage,
},
})
);
await mockEsmWithActual('#src/queries/custom-phrase.js', () => ({
findAllCustomLanguageTags: async () => [trTrTag, zhCnTag],
}));
const tenantContext = new MockTenant(
undefined,
{
customPhrases: { findAllCustomLanguageTags: async () => [trTrTag, zhCnTag] },
signInExperiences: { findDefaultSignInExperience },
},
{ phrases: { getPhrases: jest.fn().mockResolvedValue(en) } }
);
await mockEsmWithActual('#src/libraries/phrase.js', () => ({
getPhrase: jest.fn().mockResolvedValue(en),
}));
const phraseRoutes = await pickDefault(import('./phrase.js'));
const phraseRequest = createRequester({
anonymousRoutes: phraseRoutes,
tenantContext,
});
afterEach(() => {
@ -48,6 +49,7 @@ describe('when auto-detect is not enabled', () => {
...mockSignInExperience,
languageInfo: {
autoDetect: false,
// @ts-expect-error
fallbackLanguage: unsupportedLanguageX,
},
});
@ -88,6 +90,7 @@ describe('when auto-detect is enabled', () => {
...mockSignInExperience,
languageInfo: {
autoDetect: true,
// @ts-expect-error
fallbackLanguage: unsupportedLanguageX,
},
});

View file

@ -1,50 +1,57 @@
import zhCN from '@logto/phrases-ui/lib/locales/zh-cn.js';
import type { SignInExperience } from '@logto/schemas';
import type { CustomPhrase, SignInExperience } from '@logto/schemas';
import { adminConsoleApplicationId, adminConsoleSignInExperience } from '@logto/schemas';
import { pickDefault, createMockUtils } from '@logto/shared/esm';
import { zhCnTag } from '#src/__mocks__/custom-phrase.js';
import { mockSignInExperience } from '#src/__mocks__/index.js';
import { createMockTenantWithInteraction } from '#src/test-utils/tenant.js';
import Queries from '#src/tenants/Queries.js';
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { MockTenant } from '#src/test-utils/tenant.js';
const { jest } = import.meta;
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
const { mockEsm } = createMockUtils(jest);
const customizedLanguage = zhCnTag;
const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({
findDefaultSignInExperience: jest.fn(
async (): Promise<SignInExperience> => ({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage: customizedLanguage,
},
})
),
}));
const findDefaultSignInExperience = jest.fn(
async (): Promise<SignInExperience> => ({
...mockSignInExperience,
languageInfo: {
autoDetect: true,
fallbackLanguage: customizedLanguage,
},
})
);
const { default: detectLanguageSpy } = mockEsm('#src/i18n/detect-language.js', () => ({
default: jest.fn().mockReturnValue([]),
}));
const { findAllCustomLanguageTags } = mockEsm('#src/queries/custom-phrase.js', () => ({
const customPhrases = {
findAllCustomLanguageTags: jest.fn(async () => [customizedLanguage]),
findCustomPhraseByLanguageTag: jest.fn(async (tag: string) => ({})),
}));
findCustomPhraseByLanguageTag: jest.fn(
async (tag: string): Promise<CustomPhrase> => ({ languageTag: tag, translation: {} })
),
} satisfies Partial<Queries['customPhrases']>;
const { findAllCustomLanguageTags } = customPhrases;
const { getPhrase } = await mockEsmWithActual('#src/libraries/phrase.js', () => ({
getPhrase: jest.fn(async () => zhCN),
}));
const getPhrases = jest.fn(async () => zhCN);
const interactionDetails = jest.fn();
const tenantContext = new MockTenant(
createMockProvider(interactionDetails),
{ customPhrases, signInExperiences: { findDefaultSignInExperience } },
{ phrases: { getPhrases } }
);
const phraseRoutes = await pickDefault(import('./phrase.js'));
const { createRequester } = await import('#src/utils/test-utils.js');
const phraseRequest = createRequester({
anonymousRoutes: phraseRoutes,
tenantContext: createMockTenantWithInteraction(interactionDetails),
tenantContext,
});
describe('when the application is admin-console', () => {
@ -78,10 +85,10 @@ describe('when the application is admin-console', () => {
expect(findAllCustomLanguageTags).toBeCalledTimes(1);
});
it('should call getPhrase with fallback language from Admin Console sign-in experience', async () => {
it('should call getPhrases 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, [
expect(getPhrases).toBeCalledTimes(1);
expect(getPhrases).toBeCalledWith(adminConsoleSignInExperience.languageInfo.fallbackLanguage, [
customizedLanguage,
]);
});
@ -139,7 +146,7 @@ describe('when the application is not admin-console', () => {
expect(findAllCustomLanguageTags).toBeCalledTimes(1);
});
it('should call getPhrase with fallback language from default sign-in experience', async () => {
it('should call getPhrases with fallback language from default sign-in experience', async () => {
findDefaultSignInExperience.mockResolvedValueOnce({
...mockSignInExperience,
languageInfo: {
@ -148,7 +155,7 @@ describe('when the application is not admin-console', () => {
},
});
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
expect(getPhrase).toBeCalledTimes(1);
expect(getPhrase).toBeCalledWith(customizedLanguage, [customizedLanguage]);
expect(getPhrases).toBeCalledTimes(1);
expect(getPhrases).toBeCalledWith(customizedLanguage, [customizedLanguage]);
});
});

View file

@ -2,25 +2,28 @@ import { isBuiltInLanguageTag } from '@logto/phrases-ui';
import { adminConsoleApplicationId, adminConsoleSignInExperience } from '@logto/schemas';
import detectLanguage from '#src/i18n/detect-language.js';
import { getPhrase } from '#src/libraries/phrase.js';
import { findAllCustomLanguageTags } from '#src/queries/custom-phrase.js';
import { findDefaultSignInExperience } from '#src/queries/sign-in-experience.js';
import type { AnonymousRouter, RouterInitArgs } from './types.js';
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, { provider }]: RouterInitArgs<T>
...[router, { provider, queries, libraries }]: RouterInitArgs<T>
) {
const {
customPhrases: { findAllCustomLanguageTags },
signInExperiences: { findDefaultSignInExperience },
} = queries;
const { getPhrases } = libraries.phrases;
const getLanguageInfo = async (applicationId: unknown) => {
if (applicationId === adminConsoleApplicationId) {
return adminConsoleSignInExperience.languageInfo;
}
const { languageInfo } = await findDefaultSignInExperience();
return languageInfo;
};
router.get('/phrase', async (ctx, next) => {
const interaction = await provider
.interactionDetails(ctx.req, ctx.res)
@ -39,7 +42,7 @@ export default function phraseRoutes<T extends AnonymousRouter>(
) ?? 'en';
ctx.set('Content-Language', language);
ctx.body = await getPhrase(language, customLanguages);
ctx.body = await getPhrases(language, customLanguages);
return next();
});

View file

@ -1,4 +1,5 @@
import { createConnectorLibrary } from '#src/libraries/connector.js';
import { createPhraseLibrary } from '#src/libraries/phrase.js';
import { createSignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js';
import { createUserLibrary } from '#src/libraries/user.js';
@ -8,6 +9,7 @@ export default class Libraries {
connectors = createConnectorLibrary(this.queries);
users = createUserLibrary(this.queries);
signInExperiences = createSignInExperienceLibrary(this.queries, this.connectors);
phrases = createPhraseLibrary(this.queries);
constructor(public readonly queries: Queries) {}
}