Skip to content

Commit 20dabaa

Browse files
authored
fix - icu - support nesting icu params
Support nesting icu params
2 parents ad18e1c + 5534f85 commit 20dabaa

File tree

14 files changed

+182
-6
lines changed

14 files changed

+182
-6
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ jobs:
2121
cache: yarn
2222
registry-url: https://registry.npmjs.org/
2323
- run: yarn install --frozen-lockfile
24-
- run: yarn test
24+
- run: yarn test

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
5+
## [2.1.15] - 27-06-2024
6+
### Fixed
7+
- `icu` - add support for nested icu parameters. key format example: `Hello, {numPersons, plural, =0 {No one.} =1 {Mr. {personName}} other {# persons}}`
8+
49
## [2.1.4] - 29-01-2023
510
### Added
611
- `icu` - add support for icu format. key format example: ` {numPersons, plural, =0 {no persons} =1 {one person} other {# persons}}`

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"child-process-promise": "^2.2.1",
2020
"cosmiconfig": "^7.0.1",
2121
"flat": "^5.0.2",
22+
"format-message-parse": "^6.2.4",
2223
"handlebars": "^4.7.7",
2324
"ts-essentials": "^8.1.0",
2425
"ts-morph": "^12.0.0",
@@ -48,14 +49,14 @@
4849
"eslint-plugin-prettier": "^4.0.0",
4950
"fast-json-stable-stringify": "^2.1.0",
5051
"i18next": "^20.6.1",
52+
"jest": "^27.1.1",
5153
"lint-staged": "^11.2.0",
5254
"prettier": "2.4.1",
5355
"react": "^17.0.2",
5456
"react-dom": "^17.0.2",
5557
"rimraf": "^3.0.2",
5658
"simple-git-hooks": "^2.6.1",
57-
"ts-jest": "^27.0.5",
58-
"jest": "^27.1.1"
59+
"ts-jest": "^27.0.5"
5960
},
6061
"keywords": [
6162
"i18n",

src/BaseWriter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
NestedLocaleValues,
1212
} from './Generator';
1313
import { IMPORTED_TRANSLATION_FN_TYPE_NAME } from './constants';
14+
import { getTypedParams } from './icuParams';
15+
import { isSingleCurlyBraces } from './utils';
1416

1517
export interface Options extends GeneratorOptions {
1618
project: Project;
@@ -162,11 +164,13 @@ export class BaseWriter {
162164
}
163165

164166
private buildFunction(key: string, value: string): string {
165-
const interpolationKeys = this.getInterpolationKeys(value);
166-
167167
let param = '';
168168
let secondCallParam = '';
169+
const icuCompatible = isSingleCurlyBraces(this.interpolation.prefix);
169170

171+
const interpolationKeys = icuCompatible
172+
? getTypedParams(value).map((p) => p.name)
173+
: this.getInterpolationKeys(value);
170174
if (interpolationKeys.length) {
171175
param = `data: Record<${interpolationKeys
172176
.map((k) => `'${k}'`)

src/icuParams.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import parse, { AST, Element, SubMessages } from 'format-message-parse';
2+
3+
interface Param {
4+
name: string;
5+
}
6+
7+
type Format =
8+
| 'plural'
9+
| 'selectordinal'
10+
| 'date'
11+
| 'time'
12+
| 'select'
13+
| 'number';
14+
15+
const isPlural = (format: Format): boolean => {
16+
return format === 'plural' || format === 'selectordinal';
17+
};
18+
19+
const isSelect = (format: Format): boolean => {
20+
return format === 'select';
21+
};
22+
23+
const hasHashtagOnly = (element: Element | undefined): boolean => {
24+
return element!.length === 1 && element![0] === '#';
25+
};
26+
27+
const isStringOnly = (element: Format | undefined): boolean => {
28+
return typeof element === 'string';
29+
};
30+
31+
const isValidSubMessages = (subMessages: {} | undefined): boolean => {
32+
return typeof subMessages === 'object';
33+
};
34+
35+
const getSubMessages = (
36+
element: Element | undefined,
37+
format: Format
38+
): SubMessages | undefined => {
39+
if (element) {
40+
let subMessages: SubMessages | undefined;
41+
if (isPlural(format)) {
42+
subMessages = element[3] as SubMessages;
43+
} else if (isSelect(format)) {
44+
subMessages = element[2] as SubMessages;
45+
}
46+
if (isValidSubMessages(subMessages)) {
47+
return subMessages as SubMessages;
48+
}
49+
}
50+
return undefined;
51+
};
52+
53+
const stackWithSubMessages = (
54+
stack: Element[],
55+
subMessages: SubMessages
56+
): Element[] => {
57+
// eslint-disable-next-line prefer-spread
58+
return stack.concat.apply(stack, Object.values(subMessages));
59+
};
60+
61+
const getParamsFromPatternAst = (parsedArray: AST): Param[] => {
62+
if (!parsedArray || !parsedArray.slice) return [];
63+
let stack = parsedArray.slice();
64+
const params: Param[] = [];
65+
const used = new Set();
66+
while (stack.length) {
67+
const element = stack.pop();
68+
if (isStringOnly(element as Format)) continue;
69+
if (hasHashtagOnly(element)) continue;
70+
71+
const [name, format] = element!;
72+
73+
if (!used.has(name)) {
74+
params.push({ name: name as string });
75+
used.add(name);
76+
}
77+
78+
const subMessages = getSubMessages(element, format as Format);
79+
if (subMessages) {
80+
stack = stackWithSubMessages(stack, subMessages);
81+
}
82+
}
83+
return params.reverse();
84+
};
85+
86+
export const getTypedParams = (text: string): Param[] => {
87+
try {
88+
return getParamsFromPatternAst(parse(text));
89+
} catch (e) {
90+
return [];
91+
}
92+
};

src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ export const getInterpolationPrefix = (singleCurlyBraces?: boolean) =>
99
export const getInterpolationSuffix = (singleCurlyBraces?: boolean) =>
1010
singleCurlyBraces ? '}' : '}}';
1111

12+
export const isSingleCurlyBraces = (prefix: string) => prefix === '{';
13+
1214
export const getFileExtension = (isReactFile?: boolean): string =>
1315
isReactFile ? 'tsx' : 'ts';

tests/config.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ test('should override params in package.json with cli args', async () => {
8484
output: 'dist',
8585
reactHook: false,
8686
showTranslations: false,
87+
singleCurlyBraces: false,
8788
});
8889

8990
const { useLocaleKeys, LocaleKeys } = await driver.get.generatedResults<

tests/generateFiles.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export const Entries: Record<string, Partial<Config>> = {
2828
icu: {
2929
singleCurlyBraces: true,
3030
},
31+
'icu-nested': {
32+
singleCurlyBraces: true,
33+
},
3134
nested: {},
3235
flat: {},
3336
'exotic-keys': {},

tests/generator.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Driver } from './driver';
22
import * as InterpolationComplexLocaleKeys from './__generated__/pregenerated/interpolation-complex/LocaleKeys';
33
import * as ICULocaleKeys from './__generated__/pregenerated/icu/LocaleKeys';
4+
import * as ICUNestedLocaleKeys from './__generated__/pregenerated/icu-nested/LocaleKeys';
45
import * as CustomFnNameLocaleKeys from './__generated__/pregenerated/fn-name/customFnName';
56
import * as ExoticKeysLocaleKeys from './__generated__/pregenerated/exotic-keys/LocaleKeys';
67
import * as FlatLocaleKeys from './__generated__/pregenerated/flat/LocaleKeys';
@@ -406,3 +407,31 @@ test('data interpolation icu', async () => {
406407
);
407408
expect(generatedResultsAsStr).toBe(generatedSnapShotAsStr);
408409
});
410+
411+
test('data interpolation icu with nested params', async () => {
412+
driver.given.namespace('icu-nested');
413+
await driver.when.runsCodegenCommand({
414+
singleCurlyBraces: true,
415+
});
416+
417+
const [{ LocaleKeys }, generatedResultsAsStr, generatedSnapShotAsStr] =
418+
await Promise.all([
419+
driver.get.generatedResults<typeof ICUNestedLocaleKeys>(),
420+
driver.get.generatedResultsAsStr(),
421+
driver.get.generatedSnapShotAsStr(),
422+
]);
423+
424+
const result = LocaleKeys(driver.get.defaultTranslationFn());
425+
expect(
426+
result.common.people.messageNestedParams({
427+
numPersons: 2,
428+
name: 'something',
429+
})
430+
).toBe(
431+
driver.get.expectedTranslationOf('common.people.messageNestedParams', {
432+
numPersons: 2,
433+
name: 'something',
434+
})
435+
);
436+
expect(generatedResultsAsStr).toBe(generatedSnapShotAsStr);
437+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* eslint-disable */
2+
/* tslint:disable */
3+
export function LocaleKeys<R extends string>(t: (...args: unknown[]) => R) {
4+
return {
5+
common: {
6+
people: {
7+
message: (data: Record<'numPersons', unknown>) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */
8+
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.}} */
9+
},
10+
},
11+
};
12+
}
13+
14+
export type ILocaleKeys = ReturnType<typeof LocaleKeys>;

tests/snapshot/icu/LocaleKeys.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ export function LocaleKeys<R extends string>(t: (...args: unknown[]) => R) {
66
people: {
77
message: (data: Record<'numPersons', unknown>) => t('common.people.message', data), /* Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}} */
88
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}} */
9+
pluralMessage: (data: Record<'numPeople', unknown>) => t('common.people.pluralMessage', data), /* {numPeople, plural, =0 {No one is} =1 {One person is} other {# people are}} interested */
10+
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}} */
11+
dateMessage: (data: Record<'currentDate', unknown>) => t('common.people.dateMessage', data), /* Today is {currentDate, date, long} */
12+
timeMessage: (data: Record<'currentTime', unknown>) => t('common.people.timeMessage', data), /* The current time is {currentTime, time, short} */
13+
selectMessage: (data: Record<'gender', unknown>) => t('common.people.selectMessage', data), /* {gender, select, male {He is} female {She is} other {They are} } interested */
14+
numberMessage: (data: Record<'numApples', unknown>) => t('common.people.numberMessage', data), /* You have {numApples, number} apples */
915
},
1016
},
1117
};

tests/sources/icu-nested.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"common": {
3+
"people": {
4+
"message": "Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}}",
5+
"messageNestedParams": "Hey, {numPersons, plural, =0 {No one here.} one {{name}. You are the only person here.} other {{name} and # other persons are here.}}"
6+
}
7+
}
8+
}

tests/sources/icu.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
"common": {
33
"people": {
44
"message": "Hey, {numPersons, plural, =0 {no one} =1 {one person} other {# persons}}",
5-
"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}}"
5+
"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}}",
6+
"pluralMessage": "{numPeople, plural, =0 {No one is} =1 {One person is} other {# people are}} interested",
7+
"ordinalMessage": "{position, selectordinal, one {You're 1st} two {You're 2nd} few {You're 3rd} other {You're #th}}",
8+
"dateMessage": "Today is {currentDate, date, long}",
9+
"timeMessage": "The current time is {currentTime, time, short}",
10+
"selectMessage": "{gender, select, male {He is} female {She is} other {They are} } interested",
11+
"numberMessage": "You have {numApples, number} apples"
612
}
713
}
814
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,6 +1877,11 @@ form-data@^3.0.0:
18771877
combined-stream "^1.0.8"
18781878
mime-types "^2.1.12"
18791879

1880+
format-message-parse@^6.2.4:
1881+
version "6.2.4"
1882+
resolved "https://registry.yarnpkg.com/format-message-parse/-/format-message-parse-6.2.4.tgz#2c9b39a32665bd247cb1c31ba2723932d9edf3f9"
1883+
integrity sha512-k7WqXkEzgXkW4wkHdS6Cv2Ou0rIFtiDelZjgoe1saW4p7FT7zS8OeAUpAekhormqzpeecR97e4vBft1zMsfFOQ==
1884+
18801885
fs.realpath@^1.0.0:
18811886
version "1.0.0"
18821887
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"

0 commit comments

Comments
 (0)