Skip to content

Commit

Permalink
fix - icu - support nesting icu params
Browse files Browse the repository at this point in the history
Support nesting icu params
  • Loading branch information
varzager authored Jun 27, 2024
2 parents ad18e1c + 5534f85 commit 20dabaa
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
cache: yarn
registry-url: https://registry.npmjs.org/
- run: yarn install --frozen-lockfile
- run: yarn test
- run: yarn test
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# 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}}`
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 6 additions & 2 deletions src/BaseWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -162,11 +164,13 @@ export class BaseWriter {
}

private buildFunction(key: string, value: string): string {
const interpolationKeys = this.getInterpolationKeys(value);

let param = '';
let secondCallParam = '';
const icuCompatible = isSingleCurlyBraces(this.interpolation.prefix);

const interpolationKeys = icuCompatible
? getTypedParams(value).map((p) => p.name)
: this.getInterpolationKeys(value);
if (interpolationKeys.length) {
param = `data: Record<${interpolationKeys
.map((k) => `'${k}'`)
Expand Down
92 changes: 92 additions & 0 deletions src/icuParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import parse, { AST, Element, SubMessages } from 'format-message-parse';

interface Param {
name: string;
}

type Format =
| 'plural'
| 'selectordinal'
| 'date'
| 'time'
| 'select'
| 'number';

const isPlural = (format: Format): boolean => {
return format === 'plural' || format === 'selectordinal';
};

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 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 (isStringOnly(element as Format)) continue;
if (hasHashtagOnly(element)) continue;

const [name, format] = element!;

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);
}
}
return params.reverse();
};

export const getTypedParams = (text: string): Param[] => {
try {
return getParamsFromPatternAst(parse(text));
} catch (e) {
return [];
}
};
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions tests/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
3 changes: 3 additions & 0 deletions tests/generateFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export const Entries: Record<string, Partial<Config>> = {
icu: {
singleCurlyBraces: true,
},
'icu-nested': {
singleCurlyBraces: true,
},
nested: {},
flat: {},
'exotic-keys': {},
Expand Down
29 changes: 29 additions & 0 deletions tests/generator.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<typeof ICUNestedLocaleKeys>(),
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);
});
14 changes: 14 additions & 0 deletions tests/snapshot/icu-nested/LocaleKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable */
/* tslint:disable */
export function LocaleKeys<R extends string>(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<typeof LocaleKeys>;
6 changes: 6 additions & 0 deletions tests/snapshot/icu/LocaleKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export function LocaleKeys<R extends string>(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 */
},
},
};
Expand Down
8 changes: 8 additions & 0 deletions tests/sources/icu-nested.json
Original file line number Diff line number Diff line change
@@ -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.}}"
}
}
}
8 changes: 7 additions & 1 deletion tests/sources/icu.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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://registry.yarnpkg.com/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"
Expand Down

0 comments on commit 20dabaa

Please sign in to comment.