From 494c4ae483c3887b54ce17b08a50401a5850345d Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 7 Nov 2022 19:29:19 +0800 Subject: [PATCH] refactor(core): optimize translation strictly partial check (#2333) --- .../core/src/routes/custom-phrase.test.ts | 12 ++--- packages/core/src/routes/custom-phrase.ts | 4 +- packages/core/src/utils/translation.test.ts | 12 ++--- packages/core/src/utils/translation.ts | 45 +++++++------------ 4 files changed, 30 insertions(+), 43 deletions(-) diff --git a/packages/core/src/routes/custom-phrase.test.ts b/packages/core/src/routes/custom-phrase.test.ts index e137ea7b1..007df147b 100644 --- a/packages/core/src/routes/custom-phrase.test.ts +++ b/packages/core/src/routes/custom-phrase.test.ts @@ -40,13 +40,13 @@ jest.mock('@/queries/custom-phrase', () => ({ upsertCustomPhrase: async (customPhrase: CustomPhrase) => upsertCustomPhrase(customPhrase), })); -const isValidStructure = jest.fn( +const isStrictlyPartial = jest.fn( (fullTranslation: Translation, partialTranslation: Partial) => true ); jest.mock('@/utils/translation', () => ({ - isValidStructure: (fullTranslation: Translation, partialTranslation: Translation) => - isValidStructure(fullTranslation, partialTranslation), + isStrictlyPartial: (fullTranslation: Translation, partialTranslation: Translation) => + isStrictlyPartial(fullTranslation, partialTranslation), })); const mockFallbackLanguage = trTrTag; @@ -130,13 +130,13 @@ describe('customPhraseRoutes', () => { }); }); - it('should call isValidStructure', async () => { + it('should call isStrictlyPartial', async () => { await customPhraseRequest.put(`/custom-phrases/${mockLanguageTag}`).send(translation); - expect(isValidStructure).toBeCalledWith(en.translation, translation); + expect(isStrictlyPartial).toBeCalledWith(en.translation, translation); }); it('should fail when the input translation structure is invalid', async () => { - isValidStructure.mockReturnValueOnce(false); + isStrictlyPartial.mockReturnValueOnce(false); const response = await customPhraseRequest .put(`/custom-phrases/${mockLanguageTag}`) .send(translation); diff --git a/packages/core/src/routes/custom-phrase.ts b/packages/core/src/routes/custom-phrase.ts index c32404d89..95c344f94 100644 --- a/packages/core/src/routes/custom-phrase.ts +++ b/packages/core/src/routes/custom-phrase.ts @@ -15,7 +15,7 @@ import { } from '@/queries/custom-phrase'; import { findDefaultSignInExperience } from '@/queries/sign-in-experience'; import assertThat from '@/utils/assert-that'; -import { isValidStructure } from '@/utils/translation'; +import { isStrictlyPartial } from '@/utils/translation'; import type { AuthedRouter } from './types'; @@ -70,7 +70,7 @@ export default function customPhraseRoutes(router: T) { const translation = cleanDeepTranslation(body); assertThat( - isValidStructure(resource.en.translation, translation), + isStrictlyPartial(resource.en.translation, translation), new RequestError('localization.invalid_translation_structure') ); diff --git a/packages/core/src/utils/translation.test.ts b/packages/core/src/utils/translation.test.ts index c492cb099..d9bb27671 100644 --- a/packages/core/src/utils/translation.test.ts +++ b/packages/core/src/utils/translation.test.ts @@ -1,7 +1,7 @@ import en from '@logto/phrases-ui/lib/locales/en'; import fr from '@logto/phrases-ui/lib/locales/fr'; -import { isValidStructure } from '@/utils/translation'; +import { isStrictlyPartial } from '@/utils/translation'; const customizedFrTranslation = { secondary: { @@ -10,15 +10,15 @@ const customizedFrTranslation = { }, }; -describe('isValidStructure', () => { +describe('isStrictlyPartial', () => { it('should be true when its structure is valid', () => { - expect(isValidStructure(en.translation, fr.translation)).toBeTruthy(); - expect(isValidStructure(en.translation, customizedFrTranslation)).toBeTruthy(); + expect(isStrictlyPartial(en.translation, fr.translation)).toBeTruthy(); + expect(isStrictlyPartial(en.translation, customizedFrTranslation)).toBeTruthy(); }); it('should be true when the structure is partial and the existing key-value pairs are correct', () => { expect( - isValidStructure(en.translation, { + isStrictlyPartial(en.translation, { secondary: { sign_in_with: 'Se connecter avec {{methods, list(type: disjunction;)}}', // Missing 'secondary.social_bind_with' key-value pair @@ -29,7 +29,7 @@ describe('isValidStructure', () => { it('should be false when there is an unexpected key-value pair', () => { expect( - isValidStructure(en.translation, { + isStrictlyPartial(en.translation, { secondary: { sign_in_with: 'Se connecter avec {{methods, list(type: disjunction;)}}', social_bind_with: diff --git a/packages/core/src/utils/translation.ts b/packages/core/src/utils/translation.ts index 4801531d7..73932c99d 100644 --- a/packages/core/src/utils/translation.ts +++ b/packages/core/src/utils/translation.ts @@ -1,38 +1,25 @@ import type { Translation } from '@logto/schemas'; -// LOG-4385: Refactor me -// eslint-disable-next-line complexity -export const isValidStructure = (fullTranslation: Translation, partialTranslation: Translation) => { - const fullKeys = new Set(Object.keys(fullTranslation)); - const partialKeys = Object.keys(partialTranslation); - - if (fullKeys.size === 0 || partialKeys.length === 0) { - return true; - } - - if (partialKeys.some((key) => !fullKeys.has(key))) { - return false; - } - - for (const [key, value] of Object.entries(fullTranslation)) { - const targetValue = partialTranslation[key]; - - if (targetValue === undefined) { - continue; - } - - if (typeof value === 'string') { - if (typeof targetValue === 'string') { - continue; - } +/** + * @param fullTranslation The translation with full keys + * @param partialTranslation The translation to check + * @returns If the flatten keys of `partialTranslation` is a subset of `fullTranslation` + */ +export const isStrictlyPartial = ( + fullTranslation: Translation, + partialTranslation: Translation +): boolean => { + return Object.entries(partialTranslation).every(([key, value]) => { + const fullValue = fullTranslation[key]; + if (!fullValue) { return false; } - if (typeof targetValue === 'string' || !isValidStructure(value, targetValue)) { - return false; + if (typeof fullValue === 'object' && typeof value === 'object') { + return isStrictlyPartial(fullValue, value); } - } - return true; + return typeof fullValue === typeof value; + }); };