From 49d2cbee09aeafb97f8ac4f83adf4a8b03ef7fa8 Mon Sep 17 00:00:00 2001 From: Boris Polak <18208654+BorisTB@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:57:26 +0100 Subject: [PATCH 1/3] support keyPrefix --- src/extractors/getFixedTFunction.ts | 6 ++++++ src/extractors/useTranslationHook.ts | 10 +++++++++ src/keys.ts | 15 ++++++++++++- .../testGetFixedTFunction/keyPrefix.js | 18 ++++++++++++++++ .../testGetFixedTFunction/keyPrefix.json | 9 ++++++++ .../testUseTranslationHook/keyPrefix.js | 21 +++++++++++++++++++ .../testUseTranslationHook/keyPrefix.json | 10 +++++++++ .../testUseTranslationHook/simple.js | 9 ++++++-- .../testUseTranslationHook/simple.json | 1 + 9 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 tests/__fixtures__/testGetFixedTFunction/keyPrefix.js create mode 100644 tests/__fixtures__/testGetFixedTFunction/keyPrefix.json create mode 100644 tests/__fixtures__/testUseTranslationHook/keyPrefix.js create mode 100644 tests/__fixtures__/testUseTranslationHook/keyPrefix.json diff --git a/src/extractors/getFixedTFunction.ts b/src/extractors/getFixedTFunction.ts index 8654679..d0cdb4a 100644 --- a/src/extractors/getFixedTFunction.ts +++ b/src/extractors/getFixedTFunction.ts @@ -67,6 +67,11 @@ export default function extractGetFixedTFunction( const tBinding = id.scope.bindings[id.node.name]; if (!tBinding) return []; + const keyPrefixArgument = path.get('arguments')[2]; + const keyPrefix: string | null = getFirstOrNull( + evaluateIfConfident(keyPrefixArgument), + ) + let keys = Array(); for (const reference of tBinding.referencePaths) { if ( @@ -94,6 +99,7 @@ export default function extractGetFixedTFunction( return keys.map((k) => ({ ...k, + keyPrefix: keyPrefix || undefined, sourceNodes: [path.node, ...k.sourceNodes], extractorName: extractGetFixedTFunction.name, })); diff --git a/src/extractors/useTranslationHook.ts b/src/extractors/useTranslationHook.ts index 831b795..b44d744 100644 --- a/src/extractors/useTranslationHook.ts +++ b/src/extractors/useTranslationHook.ts @@ -59,6 +59,15 @@ export default function extractUseTranslationHook( const tBinding = id.scope.bindings['t']; if (!tBinding) return []; + let keyPrefix: string | undefined; + + const optionsArgument = path.get('arguments')[1]; + const options = getFirstOrNull(evaluateIfConfident(optionsArgument)) + + if (options) { + keyPrefix = options.keyPrefix || keyPrefix + } + let keys = Array(); for (const reference of tBinding.referencePaths) { if ( @@ -86,6 +95,7 @@ export default function extractUseTranslationHook( return keys.map((k) => ({ ...k, + keyPrefix: keyPrefix, sourceNodes: [path.node, ...k.sourceNodes], extractorName: extractUseTranslationHook.name, })); diff --git a/src/keys.ts b/src/keys.ts index e863863..f3781a9 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -18,6 +18,7 @@ interface I18NextParsedOptions { */ export interface ExtractedKey { key: string; + keyPrefix?: string; parsedOptions: I18NextParsedOptions; // Nodes (not node paths) from which the key was extracted. @@ -54,6 +55,14 @@ function parseExtractedKey(key: ExtractedKey, config: Config): TranslationKey { const nsSeparatorPos = cleanKey.indexOf(config.nsSeparator); if (nsSeparatorPos !== -1) { + if (key.keyPrefix) { + throw new Error( + `Do not use the keyPrefix option if you want to use keys with prefixed namespace notation. + key: ${cleanKey} + keyPrefix: ${key.keyPrefix}`, + ) + } + ns = cleanKey.slice(0, nsSeparatorPos); cleanKey = cleanKey.slice(nsSeparatorPos + 1); } @@ -61,8 +70,12 @@ function parseExtractedKey(key: ExtractedKey, config: Config): TranslationKey { let keyPath = Array(); if (config.keySeparator) { + if (key.keyPrefix) { + keyPath = key.keyPrefix.split(config.keySeparator); + } + const fullPath = cleanKey.split(config.keySeparator); - keyPath = fullPath.slice(0, fullPath.length - 1); + keyPath = [...keyPath, ...fullPath.slice(0, fullPath.length - 1)]; cleanKey = fullPath[fullPath.length - 1]; } diff --git a/tests/__fixtures__/testGetFixedTFunction/keyPrefix.js b/tests/__fixtures__/testGetFixedTFunction/keyPrefix.js new file mode 100644 index 0000000..00fcba4 --- /dev/null +++ b/tests/__fixtures__/testGetFixedTFunction/keyPrefix.js @@ -0,0 +1,18 @@ +import i18next from "i18next"; + +export function anyJSFunction1() { + const t = i18next.getFixedT(null, 'ns0', 'deep0'); + t('key0'); +} + +export function anyJSFunction2() { + const t = i18next.getFixedT(null, 'ns1', 'deep1.deep2'); + + t('key1'); +} + +export function anyJSFunction3() { + const t = i18next.getFixedT(null, 'ns2', 'deep3.deep4'); + + t('key2.key3'); +} diff --git a/tests/__fixtures__/testGetFixedTFunction/keyPrefix.json b/tests/__fixtures__/testGetFixedTFunction/keyPrefix.json new file mode 100644 index 0000000..1f118c9 --- /dev/null +++ b/tests/__fixtures__/testGetFixedTFunction/keyPrefix.json @@ -0,0 +1,9 @@ +{ + "description": "test keyPrefix usage of getFixedT TFunction", + "pluginOptions": {}, + "expectValues": [ + [{"deep0": { "key0": "" }}, {"ns": "ns0"}], + [{"deep1": { "deep2": { "key1": "" }}}, {"ns": "ns1"}], + [{"deep3": { "deep4": { "key2": { "key3": "" } }}}, {"ns": "ns2"}] + ] +} diff --git a/tests/__fixtures__/testUseTranslationHook/keyPrefix.js b/tests/__fixtures__/testUseTranslationHook/keyPrefix.js new file mode 100644 index 0000000..9c90bae --- /dev/null +++ b/tests/__fixtures__/testUseTranslationHook/keyPrefix.js @@ -0,0 +1,21 @@ +import { useTranslation } from 'react-i18next'; + +export function MyComponent1() { + const [t] = useTranslation('ns0', { keyPrefix: 'deep0' }); + return

{t('key0')}{t('key1')}

+} + +export function MyComponent2() { + const [t] = useTranslation('ns1', { keyPrefix: 'deep1.deep2' }); + return

{t('key2')}{t('key3')}

+} + +export function MyComponent3() { + const [t] = useTranslation('ns2', { keyPrefix: 'deep3.deep4' }); + return

{t('key4.key5')}{t('key4.key6')}

+} + +export function MyComponent4() { + const [t] = useTranslation('ns3', { keyPrefix: 'deep5.deep6' }); + return

{t('key7.key8')}{t('key9.key10')}

+} diff --git a/tests/__fixtures__/testUseTranslationHook/keyPrefix.json b/tests/__fixtures__/testUseTranslationHook/keyPrefix.json new file mode 100644 index 0000000..9f029cb --- /dev/null +++ b/tests/__fixtures__/testUseTranslationHook/keyPrefix.json @@ -0,0 +1,10 @@ +{ + "description": "test keyPrefix extraction of useTranslation hook", + "pluginOptions": {}, + "expectValues": [ + [{ "deep0": {"key0": "", "key1": ""}}, {"ns": "ns0"}], + [{ "deep1": {"deep2": {"key2": "", "key3": ""}}}, {"ns": "ns1"}], + [{ "deep3": {"deep4": {"key4": { "key5": "", "key6": "" }}}}, {"ns": "ns2"}], + [{ "deep5": {"deep6": {"key7": { "key8": "" }, "key9": {"key10": ""}}}}, {"ns": "ns3"}] + ] +} diff --git a/tests/__fixtures__/testUseTranslationHook/simple.js b/tests/__fixtures__/testUseTranslationHook/simple.js index 48af0fc..ef5d94c 100644 --- a/tests/__fixtures__/testUseTranslationHook/simple.js +++ b/tests/__fixtures__/testUseTranslationHook/simple.js @@ -18,12 +18,17 @@ const MyComponent2 = () => { return

{t('key2')}

} -export function MyComponent3() { +const MyComponent3 = () => { + const { t, i18n } = useTranslation(); + return

{t('key3.key4')}

+} + +export function MyComponent4() { const {i18n} = useTranslation(); return

Shouldn't crash

} -export function MyComponent4() { +export function MyComponent5() { const { t } = ReactI18Next.useTranslation(); t('from wildcard import'); } diff --git a/tests/__fixtures__/testUseTranslationHook/simple.json b/tests/__fixtures__/testUseTranslationHook/simple.json index 6958643..4d2ed0a 100644 --- a/tests/__fixtures__/testUseTranslationHook/simple.json +++ b/tests/__fixtures__/testUseTranslationHook/simple.json @@ -5,6 +5,7 @@ "key0": "", "key1": "", "key2": "", + "key3": { "key4": "" }, "from wildcard import": "" } } From 59fd6fb5221aad9b60eeceb01d22eb8bc3bc1e8c Mon Sep 17 00:00:00 2001 From: Boris Polak <18208654+BorisTB@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:08:18 +0100 Subject: [PATCH 2/3] review fixes - move keyPrefix to parsedOptions - don't let user know he's about to break translations --- src/extractors/getFixedTFunction.ts | 2 +- src/extractors/tFunction.ts | 7 ++++++ src/extractors/useTranslationHook.ts | 4 ++-- src/keys.ts | 22 ++++++++----------- .../testUseTranslationHook/keyPrefix.js | 5 +++++ .../testUseTranslationHook/keyPrefix.json | 3 ++- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/extractors/getFixedTFunction.ts b/src/extractors/getFixedTFunction.ts index d0cdb4a..a97e677 100644 --- a/src/extractors/getFixedTFunction.ts +++ b/src/extractors/getFixedTFunction.ts @@ -90,6 +90,7 @@ export default function extractGetFixedTFunction( ...k, parsedOptions: { ...k.parsedOptions, + keyPrefix: k.parsedOptions.keyPrefix || keyPrefix, ns: k.parsedOptions.ns || ns, }, })), @@ -99,7 +100,6 @@ export default function extractGetFixedTFunction( return keys.map((k) => ({ ...k, - keyPrefix: keyPrefix || undefined, sourceNodes: [path.node, ...k.sourceNodes], extractorName: extractGetFixedTFunction.name, })); diff --git a/src/extractors/tFunction.ts b/src/extractors/tFunction.ts index 19004be..18a4770 100644 --- a/src/extractors/tFunction.ts +++ b/src/extractors/tFunction.ts @@ -50,6 +50,7 @@ function parseTCallOptions( contexts: false, hasCount: false, ns: null, + keyPrefix: null, defaultValue: null, }; @@ -76,6 +77,12 @@ function parseTCallOptions( const defaultValueNodeValue = defaultValueNode.get('value'); res.defaultValue = evaluateIfConfident(defaultValueNodeValue); } + + const keyPrefixNode = findKeyInObjectExpression(path, 'keyPrefix'); + if (keyPrefixNode !== null && keyPrefixNode.isObjectProperty()) { + const keyPrefixNodeValue = keyPrefixNode.get('value'); + res.keyPrefix = evaluateIfConfident(keyPrefixNodeValue); + } } return res; diff --git a/src/extractors/useTranslationHook.ts b/src/extractors/useTranslationHook.ts index b44d744..8f4ac91 100644 --- a/src/extractors/useTranslationHook.ts +++ b/src/extractors/useTranslationHook.ts @@ -59,7 +59,7 @@ export default function extractUseTranslationHook( const tBinding = id.scope.bindings['t']; if (!tBinding) return []; - let keyPrefix: string | undefined; + let keyPrefix: string | null = null; const optionsArgument = path.get('arguments')[1]; const options = getFirstOrNull(evaluateIfConfident(optionsArgument)) @@ -86,6 +86,7 @@ export default function extractUseTranslationHook( ...k, parsedOptions: { ...k.parsedOptions, + keyPrefix: k.parsedOptions.keyPrefix || keyPrefix, ns: k.parsedOptions.ns || ns, }, })), @@ -95,7 +96,6 @@ export default function extractUseTranslationHook( return keys.map((k) => ({ ...k, - keyPrefix: keyPrefix, sourceNodes: [path.node, ...k.sourceNodes], extractorName: extractUseTranslationHook.name, })); diff --git a/src/keys.ts b/src/keys.ts index f3781a9..ee06bf1 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -10,6 +10,7 @@ interface I18NextParsedOptions { contexts: string[] | boolean; hasCount: boolean; ns: string | null; + keyPrefix: string | null; defaultValue: string | null; } @@ -18,7 +19,6 @@ interface I18NextParsedOptions { */ export interface ExtractedKey { key: string; - keyPrefix?: string; parsedOptions: I18NextParsedOptions; // Nodes (not node paths) from which the key was extracted. @@ -50,30 +50,26 @@ export interface TranslationKey extends ExtractedKey { function parseExtractedKey(key: ExtractedKey, config: Config): TranslationKey { let cleanKey = key.key; + const keyPrefix = key.parsedOptions.keyPrefix; + if (keyPrefix) { + // Imitate behavior of i18next and just connect prefix with key before any other action + const keySeparator = config.keySeparator || '.'; + cleanKey = `${keyPrefix}${keySeparator}${cleanKey}`; + } + let ns: string = key.parsedOptions.ns || config.defaultNS; if (config.nsSeparator) { const nsSeparatorPos = cleanKey.indexOf(config.nsSeparator); if (nsSeparatorPos !== -1) { - if (key.keyPrefix) { - throw new Error( - `Do not use the keyPrefix option if you want to use keys with prefixed namespace notation. - key: ${cleanKey} - keyPrefix: ${key.keyPrefix}`, - ) - } - ns = cleanKey.slice(0, nsSeparatorPos); cleanKey = cleanKey.slice(nsSeparatorPos + 1); } } let keyPath = Array(); - if (config.keySeparator) { - if (key.keyPrefix) { - keyPath = key.keyPrefix.split(config.keySeparator); - } + if (config.keySeparator) { const fullPath = cleanKey.split(config.keySeparator); keyPath = [...keyPath, ...fullPath.slice(0, fullPath.length - 1)]; cleanKey = fullPath[fullPath.length - 1]; diff --git a/tests/__fixtures__/testUseTranslationHook/keyPrefix.js b/tests/__fixtures__/testUseTranslationHook/keyPrefix.js index 9c90bae..c6704ed 100644 --- a/tests/__fixtures__/testUseTranslationHook/keyPrefix.js +++ b/tests/__fixtures__/testUseTranslationHook/keyPrefix.js @@ -19,3 +19,8 @@ export function MyComponent4() { const [t] = useTranslation('ns3', { keyPrefix: 'deep5.deep6' }); return

{t('key7.key8')}{t('key9.key10')}

} + +export function MyComponent5() { + const [t] = useTranslation('ns4', { keyPrefix: 'deep7.deep8' }); + return

{t('ns5:key11')}

+} diff --git a/tests/__fixtures__/testUseTranslationHook/keyPrefix.json b/tests/__fixtures__/testUseTranslationHook/keyPrefix.json index 9f029cb..e30ef4f 100644 --- a/tests/__fixtures__/testUseTranslationHook/keyPrefix.json +++ b/tests/__fixtures__/testUseTranslationHook/keyPrefix.json @@ -5,6 +5,7 @@ [{ "deep0": {"key0": "", "key1": ""}}, {"ns": "ns0"}], [{ "deep1": {"deep2": {"key2": "", "key3": ""}}}, {"ns": "ns1"}], [{ "deep3": {"deep4": {"key4": { "key5": "", "key6": "" }}}}, {"ns": "ns2"}], - [{ "deep5": {"deep6": {"key7": { "key8": "" }, "key9": {"key10": ""}}}}, {"ns": "ns3"}] + [{ "deep5": {"deep6": {"key7": { "key8": "" }, "key9": {"key10": ""}}}}, {"ns": "ns3"}], + [{ "key11": ""}, {"ns": "deep7.deep8.ns5"}] ] } From 866d257f4de380747549a4222856ecd52106ede8 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Sat, 30 Dec 2023 09:25:59 +0100 Subject: [PATCH 3/3] fix lint and typing --- src/extractors/getFixedTFunction.ts | 2 +- src/extractors/transComponent.ts | 1 + src/extractors/useTranslationHook.ts | 4 ++-- tests/helpers.ts | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/extractors/getFixedTFunction.ts b/src/extractors/getFixedTFunction.ts index a97e677..878b968 100644 --- a/src/extractors/getFixedTFunction.ts +++ b/src/extractors/getFixedTFunction.ts @@ -70,7 +70,7 @@ export default function extractGetFixedTFunction( const keyPrefixArgument = path.get('arguments')[2]; const keyPrefix: string | null = getFirstOrNull( evaluateIfConfident(keyPrefixArgument), - ) + ); let keys = Array(); for (const reference of tBinding.referencePaths) { diff --git a/src/extractors/transComponent.ts b/src/extractors/transComponent.ts index 7991e49..2a0d0c4 100644 --- a/src/extractors/transComponent.ts +++ b/src/extractors/transComponent.ts @@ -50,6 +50,7 @@ function parseTransComponentOptions( const res: ExtractedKey['parsedOptions'] = { contexts: false, hasCount: false, + keyPrefix: null, ns: null, defaultValue: null, }; diff --git a/src/extractors/useTranslationHook.ts b/src/extractors/useTranslationHook.ts index 8f4ac91..262d73c 100644 --- a/src/extractors/useTranslationHook.ts +++ b/src/extractors/useTranslationHook.ts @@ -62,10 +62,10 @@ export default function extractUseTranslationHook( let keyPrefix: string | null = null; const optionsArgument = path.get('arguments')[1]; - const options = getFirstOrNull(evaluateIfConfident(optionsArgument)) + const options = getFirstOrNull(evaluateIfConfident(optionsArgument)); if (options) { - keyPrefix = options.keyPrefix || keyPrefix + keyPrefix = options.keyPrefix || keyPrefix; } let keys = Array(); diff --git a/tests/helpers.ts b/tests/helpers.ts index 40ad9ab..8702a40 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -13,6 +13,7 @@ export function createTranslationKey( parsedOptions: { contexts: false, hasCount: false, + keyPrefix: null, ns: null, defaultValue: null, },