From af0144ca6ba1eb6934db614a474b0b55838267d3 Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Sun, 26 May 2024 18:45:45 +0300 Subject: [PATCH 01/14] fix: support nesting ICU params bug & add accurate types instead of unknown --- package.json | 5 +- src/BaseWriter.ts | 28 ++++++--- src/icuParams.ts | 57 +++++++++++++++++++ src/utils.ts | 2 + tests/config.spec.ts | 1 + tests/snapshot/icu/LocaleKeys.ts | 5 +- .../interpolation-single/LocaleKeys.ts | 4 +- tests/sources/icu.json | 3 +- yarn.lock | 5 ++ 9 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 src/icuParams.ts diff --git a/package.json b/package.json index df1b316..f209db7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "child-process-promise": "^2.2.1", "cosmiconfig": "^7.0.1", "flat": "^5.0.2", + "format-message-parse": "^6.2.4", "handlebars": "^4.7.7", "ts-essentials": "^8.1.0", "ts-morph": "^12.0.0", @@ -48,14 +49,14 @@ "eslint-plugin-prettier": "^4.0.0", "fast-json-stable-stringify": "^2.1.0", "i18next": "^20.6.1", + "jest": "^27.1.1", "lint-staged": "^11.2.0", "prettier": "2.4.1", "react": "^17.0.2", "react-dom": "^17.0.2", "rimraf": "^3.0.2", "simple-git-hooks": "^2.6.1", - "ts-jest": "^27.0.5", - "jest": "^27.1.1" + "ts-jest": "^27.0.5" }, "keywords": [ "i18n", diff --git a/src/BaseWriter.ts b/src/BaseWriter.ts index 3867850..e315118 100644 --- a/src/BaseWriter.ts +++ b/src/BaseWriter.ts @@ -11,6 +11,8 @@ import type { NestedLocaleValues, } from './Generator'; import { IMPORTED_TRANSLATION_FN_TYPE_NAME } from './constants'; +import { getTypedParams } from './icuParams'; +import { isSingleCurlyBraces } from './utils'; export interface Options extends GeneratorOptions { project: Project; @@ -162,16 +164,26 @@ export class BaseWriter { } private buildFunction(key: string, value: string): string { - const interpolationKeys = this.getInterpolationKeys(value); - let param = ''; let secondCallParam = ''; - - if (interpolationKeys.length) { - param = `data: Record<${interpolationKeys - .map((k) => `'${k}'`) - .join(' | ')}, unknown>`; - secondCallParam = ', data'; + const icuCompatible = isSingleCurlyBraces(this.interpolation.prefix); + + if (icuCompatible) { + const params = getTypedParams(value); + if (params.length) { + param = `data: { ${params + .map(({ name, type }) => `${name}: ${type}`) + .join('; ')} }`; + secondCallParam = ', data'; + } + } else { + const interpolationKeys = this.getInterpolationKeys(value); + if (interpolationKeys.length) { + param = `data: Record<${interpolationKeys + .map((k) => `'${k}'`) + .join(' | ')}, unknown>`; + secondCallParam = ', data'; + } } return `(${param}) => ${this.translationFnName}('${key}'${secondCallParam})`; diff --git a/src/icuParams.ts b/src/icuParams.ts new file mode 100644 index 0000000..0b1b8d6 --- /dev/null +++ b/src/icuParams.ts @@ -0,0 +1,57 @@ +import parse, { AST, Element } from 'format-message-parse'; + +interface Param { + name: string; + type: string; +} + +const formatToType = ( + format?: Element, + children?: Record | string +): string => { + if (format === 'plural' || format === 'selectordinal') { + return 'number'; + } else if (format === 'date' || format === 'time') { + return 'Date'; + } else if (format === 'select') { + return Object.keys(children!) + .map((c) => `'${c}'`) + .join(' | '); + } + return 'string'; +}; + +const getParamsFromPatternAst = (ast: AST): Param[] => { + if (!ast || !ast.slice) return []; + let stack = ast.slice(); + const params: Param[] = []; + const used = new Set(); + while (stack.length) { + const element = stack.pop(); + if (typeof element === 'string') continue; + if (element!.length === 1 && element![0] === '#') continue; + + const [name, format] = element!; + const children = (element![3] || element![2]) as unknown as {}; + const type = formatToType(format, children); + + if (!used.has(name)) { + params.push({ name, type }); + used.add(name); + } + + if (typeof children === 'object') { + // eslint-disable-next-line prefer-spread + stack = stack.concat.apply(stack, Object.values(children)); + } + } + return params.reverse(); +}; + +export const getTypedParams = (text: string): Param[] => { + try { + return getParamsFromPatternAst(parse(text)); + } catch (e) { + return []; + } +}; diff --git a/src/utils.ts b/src/utils.ts index 8f02839..0382b63 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,5 +9,7 @@ export const getInterpolationPrefix = (singleCurlyBraces?: boolean) => export const getInterpolationSuffix = (singleCurlyBraces?: boolean) => singleCurlyBraces ? '}' : '}}'; +export const isSingleCurlyBraces = (prefix: string) => prefix === '{'; + export const getFileExtension = (isReactFile?: boolean): string => isReactFile ? 'tsx' : 'ts'; diff --git a/tests/config.spec.ts b/tests/config.spec.ts index c924853..40def4a 100644 --- a/tests/config.spec.ts +++ b/tests/config.spec.ts @@ -84,6 +84,7 @@ test('should override params in package.json with cli args', async () => { output: 'dist', reactHook: false, showTranslations: false, + singleCurlyBraces: false, }); const { useLocaleKeys, LocaleKeys } = await driver.get.generatedResults< diff --git a/tests/snapshot/icu/LocaleKeys.ts b/tests/snapshot/icu/LocaleKeys.ts index 99206c7..03cf79e 100644 --- a/tests/snapshot/icu/LocaleKeys.ts +++ b/tests/snapshot/icu/LocaleKeys.ts @@ -4,8 +4,9 @@ export function LocaleKeys(t: (...args: unknown[]) => R) { return { common: { people: { - message: (data: Record<'numPersons', unknown>) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ - messageComplex: (data: Record<'name' | 'numPersons' | 'productsAmount', unknown>) => t('common.people.messageComplex', data), /* Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}} */ + message: (data: { numPersons: number }) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ + messageComplex: (data: { name: string; numPersons: number; productsAmount: number }) => t('common.people.messageComplex', data), /* Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}} */ + messageNestingParams: (data: { name: string; numPersons: number }) => t('common.people.messageNestingParams', data), /* Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}} */ }, }, }; diff --git a/tests/snapshot/interpolation-single/LocaleKeys.ts b/tests/snapshot/interpolation-single/LocaleKeys.ts index 341d6f5..a81fae4 100644 --- a/tests/snapshot/interpolation-single/LocaleKeys.ts +++ b/tests/snapshot/interpolation-single/LocaleKeys.ts @@ -4,10 +4,10 @@ export function LocaleKeys(t: (...args: unknown[]) => R) { return { common: { loggedIn: { - message: (data: Record<'username', unknown>) => t('common.loggedIn.message', data), /* Hey, {username}, you have successfully logged in! */ + message: (data: { username: string }) => t('common.loggedIn.message', data), /* Hey, {username}, you have successfully logged in! */ }, }, - readingWarning: (data: Record<'reader' | 'writer', unknown>) => t('readingWarning', data), /* {reader} reads message from {writer} */ + readingWarning: (data: { reader: string; writer: string }) => t('readingWarning', data), /* {reader} reads message from {writer} */ }; } diff --git a/tests/sources/icu.json b/tests/sources/icu.json index ec7af2c..80ff9f3 100644 --- a/tests/sources/icu.json +++ b/tests/sources/icu.json @@ -2,7 +2,8 @@ "common": { "people": { "message": "Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}}", - "messageComplex": "Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}}" + "messageComplex": "Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}}", + "messageNestingParams": "Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}}" } } } diff --git a/yarn.lock b/yarn.lock index ee88f88..09334a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1877,6 +1877,11 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +format-message-parse@^6.2.4: + version "6.2.4" + resolved "https://npm.dev.wixpress.com/api/npm/npm-repos/format-message-parse/-/format-message-parse-6.2.4.tgz#2c9b39a32665bd247cb1c31ba2723932d9edf3f9" + integrity sha512-k7WqXkEzgXkW4wkHdS6Cv2Ou0rIFtiDelZjgoe1saW4p7FT7zS8OeAUpAekhormqzpeecR97e4vBft1zMsfFOQ== + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" From bea783e99d95b6aefd48446bb4cb4c93f65b92eb Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Sun, 26 May 2024 19:01:41 +0300 Subject: [PATCH 02/14] fix: makes params optional in order not to be breaking change --- src/BaseWriter.ts | 2 +- tests/snapshot/icu/LocaleKeys.ts | 6 +++--- tests/snapshot/interpolation-single/LocaleKeys.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/BaseWriter.ts b/src/BaseWriter.ts index e315118..74c0099 100644 --- a/src/BaseWriter.ts +++ b/src/BaseWriter.ts @@ -172,7 +172,7 @@ export class BaseWriter { const params = getTypedParams(value); if (params.length) { param = `data: { ${params - .map(({ name, type }) => `${name}: ${type}`) + .map(({ name, type }) => `${name}?: ${type}`) .join('; ')} }`; secondCallParam = ', data'; } diff --git a/tests/snapshot/icu/LocaleKeys.ts b/tests/snapshot/icu/LocaleKeys.ts index 03cf79e..a56b278 100644 --- a/tests/snapshot/icu/LocaleKeys.ts +++ b/tests/snapshot/icu/LocaleKeys.ts @@ -4,9 +4,9 @@ export function LocaleKeys(t: (...args: unknown[]) => R) { return { common: { people: { - message: (data: { numPersons: number }) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ - messageComplex: (data: { name: string; numPersons: number; productsAmount: number }) => t('common.people.messageComplex', data), /* Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}} */ - messageNestingParams: (data: { name: string; numPersons: number }) => t('common.people.messageNestingParams', data), /* Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}} */ + message: (data: { numPersons?: number }) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ + messageComplex: (data: { name?: string; numPersons?: number; productsAmount?: number }) => t('common.people.messageComplex', data), /* Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}} */ + messageNestingParams: (data: { name?: string; numPersons?: number }) => t('common.people.messageNestingParams', data), /* Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}} */ }, }, }; diff --git a/tests/snapshot/interpolation-single/LocaleKeys.ts b/tests/snapshot/interpolation-single/LocaleKeys.ts index a81fae4..0506006 100644 --- a/tests/snapshot/interpolation-single/LocaleKeys.ts +++ b/tests/snapshot/interpolation-single/LocaleKeys.ts @@ -4,10 +4,10 @@ export function LocaleKeys(t: (...args: unknown[]) => R) { return { common: { loggedIn: { - message: (data: { username: string }) => t('common.loggedIn.message', data), /* Hey, {username}, you have successfully logged in! */ + message: (data: { username?: string }) => t('common.loggedIn.message', data), /* Hey, {username}, you have successfully logged in! */ }, }, - readingWarning: (data: { reader: string; writer: string }) => t('readingWarning', data), /* {reader} reads message from {writer} */ + readingWarning: (data: { reader?: string; writer?: string }) => t('readingWarning', data), /* {reader} reads message from {writer} */ }; } From 329a58ea1d667511a9230c947cdb40b1fb226199 Mon Sep 17 00:00:00 2001 From: Yuval Zamir <73066192+yuval-zamir@users.noreply.github.com> Date: Sun, 26 May 2024 19:12:55 +0300 Subject: [PATCH 03/14] Allow updating lock file due to new package --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d825895..a15d332 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,5 +20,5 @@ jobs: node-version: 16 cache: yarn registry-url: https://registry.npmjs.org/ - - run: yarn install --frozen-lockfile - - run: yarn test \ No newline at end of file + - run: yarn install + - run: yarn test From ed82e03998e46496606937fd985a569be65e6aed Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Sun, 26 May 2024 19:29:54 +0300 Subject: [PATCH 04/14] fix: update yarn.lock to use open-source npm registry --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 09334a1..791cb4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1879,7 +1879,7 @@ form-data@^3.0.0: format-message-parse@^6.2.4: version "6.2.4" - resolved "https://npm.dev.wixpress.com/api/npm/npm-repos/format-message-parse/-/format-message-parse-6.2.4.tgz#2c9b39a32665bd247cb1c31ba2723932d9edf3f9" + resolved "https://registry.yarnpkg.com/format-message-parse/-/format-message-parse-6.2.4.tgz#2c9b39a32665bd247cb1c31ba2723932d9edf3f9" integrity sha512-k7WqXkEzgXkW4wkHdS6Cv2Ou0rIFtiDelZjgoe1saW4p7FT7zS8OeAUpAekhormqzpeecR97e4vBft1zMsfFOQ== fs.realpath@^1.0.0: From 9fd2e0838ceee14db5d6eef00f2fce8480ede121 Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Sun, 2 Jun 2024 19:14:37 +0300 Subject: [PATCH 05/14] refactor: add helper methods to describe the logic better --- src/icuParams.ts | 89 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/src/icuParams.ts b/src/icuParams.ts index 0b1b8d6..8b629ef 100644 --- a/src/icuParams.ts +++ b/src/icuParams.ts @@ -1,48 +1,99 @@ -import parse, { AST, Element } from 'format-message-parse'; +import parse, { AST, Element, SubMessages } from 'format-message-parse'; interface Param { name: string; type: string; } +type Format = 'plural' | 'selectordinal' | 'date' | 'time' | 'select'; + +const isPlural = (format: Format): boolean => { + return format === 'plural' || format === 'selectordinal'; +}; + +const isDateOrTime = (format: Format): boolean => { + return format === 'date' || format === 'time'; +}; + +const isSelect = (format: Format): boolean => { + return format === 'select'; +}; + +const hasHashtagOnly = (element: Element | undefined): boolean => { + return element!.length === 1 && element![0] === '#'; +}; + +const isStringOnly = (element: Format | undefined): boolean => { + return typeof element === 'string'; +}; + +const isValidSubMessages = (subMessages: {} | undefined): boolean => { + return typeof subMessages === 'object'; +}; + +const getSubMessages = ( + element: Element | undefined, + format: Format +): SubMessages | undefined => { + if (element) { + let subMessages: SubMessages | undefined; + if (isPlural(format)) { + subMessages = element[3] as SubMessages; + } else if (isSelect(format)) { + subMessages = element[2] as SubMessages; + } + if (isValidSubMessages(subMessages)) { + return subMessages as SubMessages; + } + } + return undefined; +}; + const formatToType = ( - format?: Element, - children?: Record | string + format?: Format, + subMessages?: Record | string ): string => { - if (format === 'plural' || format === 'selectordinal') { + if (isPlural(format!)) { return 'number'; - } else if (format === 'date' || format === 'time') { + } else if (isDateOrTime(format!)) { return 'Date'; - } else if (format === 'select') { - return Object.keys(children!) - .map((c) => `'${c}'`) + } else if (isSelect(format!)) { + return Object.keys(subMessages!) + .map((selectValue) => `'${selectValue}'`) .join(' | '); } return 'string'; }; -const getParamsFromPatternAst = (ast: AST): Param[] => { - if (!ast || !ast.slice) return []; - let stack = ast.slice(); +const stackWithSubMessages = ( + stack: Element[], + subMessages: SubMessages +): Element[] => { + // eslint-disable-next-line prefer-spread + return stack.concat.apply(stack, Object.values(subMessages)); +}; + +const getParamsFromPatternAst = (parsedArray: AST): Param[] => { + if (!parsedArray || !parsedArray.slice) return []; + let stack = parsedArray.slice(); const params: Param[] = []; const used = new Set(); while (stack.length) { const element = stack.pop(); - if (typeof element === 'string') continue; - if (element!.length === 1 && element![0] === '#') continue; + if (isStringOnly(element as Format)) continue; + if (hasHashtagOnly(element)) continue; const [name, format] = element!; - const children = (element![3] || element![2]) as unknown as {}; - const type = formatToType(format, children); + const subMessages = getSubMessages(element, format as Format); + const type = formatToType(format as Format, subMessages); if (!used.has(name)) { - params.push({ name, type }); + params.push({ name: name as string, type }); used.add(name); } - if (typeof children === 'object') { - // eslint-disable-next-line prefer-spread - stack = stack.concat.apply(stack, Object.values(children)); + if (subMessages) { + stack = stackWithSubMessages(stack, subMessages); } } return params.reverse(); From a67b02a57ea1bbf63263bea9acd817915d0c6212 Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Sun, 2 Jun 2024 19:16:18 +0300 Subject: [PATCH 06/14] fix: suport number format --- src/icuParams.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/icuParams.ts b/src/icuParams.ts index 8b629ef..1b7656e 100644 --- a/src/icuParams.ts +++ b/src/icuParams.ts @@ -5,12 +5,22 @@ interface Param { type: string; } -type Format = 'plural' | 'selectordinal' | 'date' | 'time' | 'select'; +type Format = + | 'plural' + | 'selectordinal' + | 'date' + | 'time' + | 'select' + | 'number'; const isPlural = (format: Format): boolean => { return format === 'plural' || format === 'selectordinal'; }; +const isNumber = (format: Format): boolean => { + return format === 'number'; +}; + const isDateOrTime = (format: Format): boolean => { return format === 'date' || format === 'time'; }; @@ -53,7 +63,7 @@ const formatToType = ( format?: Format, subMessages?: Record | string ): string => { - if (isPlural(format!)) { + if (isPlural(format!) || isNumber(format!)) { return 'number'; } else if (isDateOrTime(format!)) { return 'Date'; From 9e8502eb804b6f6c5c9f450c5b09132a424a15de Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Wed, 19 Jun 2024 23:22:31 +0300 Subject: [PATCH 07/14] revert: forzen-lockfile --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a15d332..c6c6f7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,5 +20,5 @@ jobs: node-version: 16 cache: yarn registry-url: https://registry.npmjs.org/ - - run: yarn install + - run: yarn install --frozen-lockfile - run: yarn test From bcb28afb63dbc50226a58564ed7f51fb286325f9 Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Wed, 19 Jun 2024 23:34:06 +0300 Subject: [PATCH 08/14] revert: remove new types for tests --- src/BaseWriter.ts | 24 ++++++---------- src/icuParams.ts | 28 +------------------ tests/snapshot/icu/LocaleKeys.ts | 6 ++-- .../interpolation-single/LocaleKeys.ts | 4 +-- 4 files changed, 14 insertions(+), 48 deletions(-) diff --git a/src/BaseWriter.ts b/src/BaseWriter.ts index 74c0099..e19f98f 100644 --- a/src/BaseWriter.ts +++ b/src/BaseWriter.ts @@ -168,22 +168,14 @@ export class BaseWriter { let secondCallParam = ''; const icuCompatible = isSingleCurlyBraces(this.interpolation.prefix); - if (icuCompatible) { - const params = getTypedParams(value); - if (params.length) { - param = `data: { ${params - .map(({ name, type }) => `${name}?: ${type}`) - .join('; ')} }`; - secondCallParam = ', data'; - } - } else { - const interpolationKeys = this.getInterpolationKeys(value); - if (interpolationKeys.length) { - param = `data: Record<${interpolationKeys - .map((k) => `'${k}'`) - .join(' | ')}, unknown>`; - secondCallParam = ', data'; - } + const interpolationKeys = icuCompatible + ? getTypedParams(value).map((p) => p.name) + : this.getInterpolationKeys(value); + if (interpolationKeys.length) { + param = `data: Record<${interpolationKeys + .map((k) => `'${k}'`) + .join(' | ')}, unknown>`; + secondCallParam = ', data'; } return `(${param}) => ${this.translationFnName}('${key}'${secondCallParam})`; diff --git a/src/icuParams.ts b/src/icuParams.ts index 1b7656e..5da0e43 100644 --- a/src/icuParams.ts +++ b/src/icuParams.ts @@ -2,7 +2,6 @@ import parse, { AST, Element, SubMessages } from 'format-message-parse'; interface Param { name: string; - type: string; } type Format = @@ -17,14 +16,6 @@ const isPlural = (format: Format): boolean => { return format === 'plural' || format === 'selectordinal'; }; -const isNumber = (format: Format): boolean => { - return format === 'number'; -}; - -const isDateOrTime = (format: Format): boolean => { - return format === 'date' || format === 'time'; -}; - const isSelect = (format: Format): boolean => { return format === 'select'; }; @@ -59,22 +50,6 @@ const getSubMessages = ( return undefined; }; -const formatToType = ( - format?: Format, - subMessages?: Record | string -): string => { - if (isPlural(format!) || isNumber(format!)) { - return 'number'; - } else if (isDateOrTime(format!)) { - return 'Date'; - } else if (isSelect(format!)) { - return Object.keys(subMessages!) - .map((selectValue) => `'${selectValue}'`) - .join(' | '); - } - return 'string'; -}; - const stackWithSubMessages = ( stack: Element[], subMessages: SubMessages @@ -95,10 +70,9 @@ const getParamsFromPatternAst = (parsedArray: AST): Param[] => { const [name, format] = element!; const subMessages = getSubMessages(element, format as Format); - const type = formatToType(format as Format, subMessages); if (!used.has(name)) { - params.push({ name: name as string, type }); + params.push({ name: name as string }); used.add(name); } diff --git a/tests/snapshot/icu/LocaleKeys.ts b/tests/snapshot/icu/LocaleKeys.ts index a56b278..2ccc33d 100644 --- a/tests/snapshot/icu/LocaleKeys.ts +++ b/tests/snapshot/icu/LocaleKeys.ts @@ -4,9 +4,9 @@ export function LocaleKeys(t: (...args: unknown[]) => R) { return { common: { people: { - message: (data: { numPersons?: number }) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ - messageComplex: (data: { name?: string; numPersons?: number; productsAmount?: number }) => t('common.people.messageComplex', data), /* Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}} */ - messageNestingParams: (data: { name?: string; numPersons?: number }) => t('common.people.messageNestingParams', data), /* Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}} */ + message: (data: Record<'numPersons', unknown>) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ + messageComplex: (data: Record<'name' | 'numPersons' | 'productsAmount', unknown>) => t('common.people.messageComplex', data), /* Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}} */ + messageNestingParams: (data: Record<'name' | 'numPersons', unknown>) => t('common.people.messageNestingParams', data), /* Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}} */ }, }, }; diff --git a/tests/snapshot/interpolation-single/LocaleKeys.ts b/tests/snapshot/interpolation-single/LocaleKeys.ts index 0506006..341d6f5 100644 --- a/tests/snapshot/interpolation-single/LocaleKeys.ts +++ b/tests/snapshot/interpolation-single/LocaleKeys.ts @@ -4,10 +4,10 @@ export function LocaleKeys(t: (...args: unknown[]) => R) { return { common: { loggedIn: { - message: (data: { username?: string }) => t('common.loggedIn.message', data), /* Hey, {username}, you have successfully logged in! */ + message: (data: Record<'username', unknown>) => t('common.loggedIn.message', data), /* Hey, {username}, you have successfully logged in! */ }, }, - readingWarning: (data: { reader?: string; writer?: string }) => t('readingWarning', data), /* {reader} reads message from {writer} */ + readingWarning: (data: Record<'reader' | 'writer', unknown>) => t('readingWarning', data), /* {reader} reads message from {writer} */ }; } From 64b09fd4ded2f879ac778addfe7812bfbf04da5e Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Wed, 19 Jun 2024 23:48:51 +0300 Subject: [PATCH 09/14] fix: create separated test for nested ICU params --- tests/generateFiles.ts | 3 +++ tests/generator.spec.ts | 29 +++++++++++++++++++++++++ tests/snapshot/icu-nested/LocaleKeys.ts | 14 ++++++++++++ tests/snapshot/icu/LocaleKeys.ts | 1 - tests/sources/icu-nested.json | 8 +++++++ tests/sources/icu.json | 3 +-- 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 tests/snapshot/icu-nested/LocaleKeys.ts create mode 100644 tests/sources/icu-nested.json diff --git a/tests/generateFiles.ts b/tests/generateFiles.ts index 6ede5c3..0145701 100644 --- a/tests/generateFiles.ts +++ b/tests/generateFiles.ts @@ -28,6 +28,9 @@ export const Entries: Record> = { icu: { singleCurlyBraces: true, }, + 'icu-nested': { + singleCurlyBraces: true, + }, nested: {}, flat: {}, 'exotic-keys': {}, diff --git a/tests/generator.spec.ts b/tests/generator.spec.ts index 6d32b78..3fe2e6d 100644 --- a/tests/generator.spec.ts +++ b/tests/generator.spec.ts @@ -1,6 +1,7 @@ import { Driver } from './driver'; import * as InterpolationComplexLocaleKeys from './__generated__/pregenerated/interpolation-complex/LocaleKeys'; import * as ICULocaleKeys from './__generated__/pregenerated/icu/LocaleKeys'; +import * as ICUNestedLocaleKeys from './__generated__/pregenerated/icu-nested/LocaleKeys'; import * as CustomFnNameLocaleKeys from './__generated__/pregenerated/fn-name/customFnName'; import * as ExoticKeysLocaleKeys from './__generated__/pregenerated/exotic-keys/LocaleKeys'; import * as FlatLocaleKeys from './__generated__/pregenerated/flat/LocaleKeys'; @@ -406,3 +407,31 @@ test('data interpolation icu', async () => { ); expect(generatedResultsAsStr).toBe(generatedSnapShotAsStr); }); + +test('data interpolation icu with nested params', async () => { + driver.given.namespace('icu-nested'); + await driver.when.runsCodegenCommand({ + singleCurlyBraces: true, + }); + + const [{ LocaleKeys }, generatedResultsAsStr, generatedSnapShotAsStr] = + await Promise.all([ + driver.get.generatedResults(), + driver.get.generatedResultsAsStr(), + driver.get.generatedSnapShotAsStr(), + ]); + + const result = LocaleKeys(driver.get.defaultTranslationFn()); + expect( + result.common.people.messageNestedParams({ + numPersons: 2, + name: 'something', + }) + ).toBe( + driver.get.expectedTranslationOf('common.people.messageNestedParams', { + numPersons: 2, + name: 'something', + }) + ); + expect(generatedResultsAsStr).toBe(generatedSnapShotAsStr); +}); diff --git a/tests/snapshot/icu-nested/LocaleKeys.ts b/tests/snapshot/icu-nested/LocaleKeys.ts new file mode 100644 index 0000000..d05d4b2 --- /dev/null +++ b/tests/snapshot/icu-nested/LocaleKeys.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +/* tslint:disable */ +export function LocaleKeys(t: (...args: unknown[]) => R) { + return { + common: { + people: { + message: (data: Record<'numPersons', unknown>) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ + messageNestedParams: (data: Record<'name' | 'numPersons', unknown>) => t('common.people.messageNestedParams', data), /* Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}} */ + }, + }, + }; +} + +export type ILocaleKeys = ReturnType; diff --git a/tests/snapshot/icu/LocaleKeys.ts b/tests/snapshot/icu/LocaleKeys.ts index 2ccc33d..99206c7 100644 --- a/tests/snapshot/icu/LocaleKeys.ts +++ b/tests/snapshot/icu/LocaleKeys.ts @@ -6,7 +6,6 @@ export function LocaleKeys(t: (...args: unknown[]) => R) { people: { message: (data: Record<'numPersons', unknown>) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ messageComplex: (data: Record<'name' | 'numPersons' | 'productsAmount', unknown>) => t('common.people.messageComplex', data), /* Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}} */ - messageNestingParams: (data: Record<'name' | 'numPersons', unknown>) => t('common.people.messageNestingParams', data), /* Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}} */ }, }, }; diff --git a/tests/sources/icu-nested.json b/tests/sources/icu-nested.json new file mode 100644 index 0000000..6793db1 --- /dev/null +++ b/tests/sources/icu-nested.json @@ -0,0 +1,8 @@ +{ + "common": { + "people": { + "message": "Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}}", + "messageNestedParams": "Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}}" + } + } +} diff --git a/tests/sources/icu.json b/tests/sources/icu.json index 80ff9f3..ec7af2c 100644 --- a/tests/sources/icu.json +++ b/tests/sources/icu.json @@ -2,8 +2,7 @@ "common": { "people": { "message": "Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}}", - "messageComplex": "Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}}", - "messageNestingParams": "Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}}" + "messageComplex": "Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}}" } } } From ca7b20dd49c1dc35eff0b3eb00a7fa0f1ceb8bc4 Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Tue, 25 Jun 2024 19:34:53 +0300 Subject: [PATCH 10/14] refactor: add tests for ICU format types --- tests/snapshot/icu/LocaleKeys.ts | 6 ++++++ tests/sources/icu.json | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/snapshot/icu/LocaleKeys.ts b/tests/snapshot/icu/LocaleKeys.ts index 99206c7..301b9c9 100644 --- a/tests/snapshot/icu/LocaleKeys.ts +++ b/tests/snapshot/icu/LocaleKeys.ts @@ -6,6 +6,12 @@ export function LocaleKeys(t: (...args: unknown[]) => R) { people: { message: (data: Record<'numPersons', unknown>) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */ messageComplex: (data: Record<'name' | 'numPersons' | 'productsAmount', unknown>) => t('common.people.messageComplex', data), /* Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}} */ + pluralMessage: (data: Record<'numPeople', unknown>) => t('common.people.pluralMessage', data), /* {numPeople, plural, =0 {No one is} =1 {One person is} other {# people are}} interested */ + ordinalMessage: (data: Record<'position', unknown>) => t('common.people.ordinalMessage', data), /* {position, selectordinal, one {You're 1st} two {You're 2nd} few {You're 3rd} other {You're #th}} */ + dateMessage: (data: Record<'currentDate', unknown>) => t('common.people.dateMessage', data), /* Today is {currentDate, date, long} */ + timeMessage: (data: Record<'currentTime', unknown>) => t('common.people.timeMessage', data), /* The current time is {currentTime, time, short} */ + selectMessage: (data: Record<'gender', unknown>) => t('common.people.selectMessage', data), /* {gender, select, male {He is} female {She is} other {They are} } interested */ + numberMessage: (data: Record<'numApples', unknown>) => t('common.people.numberMessage', data), /* You have {numApples, number} apples */ }, }, }; diff --git a/tests/sources/icu.json b/tests/sources/icu.json index ec7af2c..d953053 100644 --- a/tests/sources/icu.json +++ b/tests/sources/icu.json @@ -2,7 +2,13 @@ "common": { "people": { "message": "Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}}", - "messageComplex": "Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}}" + "messageComplex": "Hey {name}, There are {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} that want to change the {productsAmount, plural, =1 {price of 1 product} other {prices of # products}}", + "pluralMessage": "{numPeople, plural, =0 {No one is} =1 {One person is} other {# people are}} interested", + "ordinalMessage": "{position, selectordinal, one {You're 1st} two {You're 2nd} few {You're 3rd} other {You're #th}}", + "dateMessage": "Today is {currentDate, date, long}", + "timeMessage": "The current time is {currentTime, time, short}", + "selectMessage": "{gender, select, male {He is} female {She is} other {They are} } interested", + "numberMessage": "You have {numApples, number} apples" } } } From ca8cac834377df8fb92af30ac32817d9658bd96e Mon Sep 17 00:00:00 2001 From: yuvalzamir Date: Tue, 25 Jun 2024 19:36:15 +0300 Subject: [PATCH 11/14] refactor: move getSubMessages next to its usage --- src/icuParams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icuParams.ts b/src/icuParams.ts index 5da0e43..6a78e72 100644 --- a/src/icuParams.ts +++ b/src/icuParams.ts @@ -69,13 +69,13 @@ const getParamsFromPatternAst = (parsedArray: AST): Param[] => { if (hasHashtagOnly(element)) continue; const [name, format] = element!; - const subMessages = getSubMessages(element, format as Format); if (!used.has(name)) { params.push({ name: name as string }); used.add(name); } + const subMessages = getSubMessages(element, format as Format); if (subMessages) { stack = stackWithSubMessages(stack, subMessages); } From 65fb0c13bab35fa5f142507195a4f9d9c9ce4410 Mon Sep 17 00:00:00 2001 From: Yuval Zamir <73066192+yuval-zamir@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:51:42 +0300 Subject: [PATCH 12/14] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb6c18..a6f7bff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,8 @@ All notable changes to this project will be documented in this file. ## [2.1.4] - 29-01-2023 ### Added - `icu` - add support for icu format. key format example: ` {numPersons, plural, =0 {no persons} =1 {one person} other {# persons}}` + +## [2.1.15] - 27-06-2024 +### Added +- `icu` - add support for nested icu parameters. key format example: `Hello, {numPersons, plural, =0 {No one.} =1 {Mr. {personName}} other {# persons}}` + From 0838cc7145acd4a400d1843f430a496e98ee114b Mon Sep 17 00:00:00 2001 From: Guy Warzager Date: Thu, 27 Jun 2024 11:58:01 +0300 Subject: [PATCH 13/14] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f7bff..0bdc561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. + +## [2.1.15] - 27-06-2024 +### Fixed +- `icu` - add support for nested icu parameters. key format example: `Hello, {numPersons, plural, =0 {No one.} =1 {Mr. {personName}} other {# persons}}` + ## [2.1.4] - 29-01-2023 ### Added - `icu` - add support for icu format. key format example: ` {numPersons, plural, =0 {no persons} =1 {one person} other {# persons}}` - -## [2.1.15] - 27-06-2024 -### Added -- `icu` - add support for nested icu parameters. key format example: `Hello, {numPersons, plural, =0 {No one.} =1 {Mr. {personName}} other {# persons}}` + From 5534f859e7df050f7d8185c686da0386d466f35c Mon Sep 17 00:00:00 2001 From: Guy Warzager Date: Thu, 27 Jun 2024 11:59:07 +0300 Subject: [PATCH 14/14] Update CHANGELOG.md --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bdc561..fce76cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,5 +9,3 @@ All notable changes to this project will be documented in this file. ## [2.1.4] - 29-01-2023 ### Added - `icu` - add support for icu format. key format example: ` {numPersons, plural, =0 {no persons} =1 {one person} other {# persons}}` - -