Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(#621) Plural translation starting and ending with a variables fails #640

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/docs/getting-started/config-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ translocoConfig({
```

### `interpolation`
The start and end markings for parameters: (defaults to `['{{', '}}']`)
The start and end markings for parameters and forbidden characters for parameter names: (defaults to `['{{', '}}', '{}']`)
```ts
translocoConfig({
// This will enable you to specify parameters as such: `"Hello <<<value>>>"`
interpolation: ['<<<', '>>>']
interpolation: ['<<<', '>>>', '<>']
})
```
13 changes: 12 additions & 1 deletion docs/docs/tools/validator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@ This package provides validation for translation files. It validates that the JS
}
},
"lint-staged": {
"src/assets/i18n/*.json": ["transloco-validator"]
"src/assets/i18n/*.json": ["transloco-validator [options]"]
}
}
```

This will make sure no one accidentally pushes an invalid translation file.


### Options

#### Interpolation forbidden characters for parameter names.

- `--interpolationForbiddenChars`

`type`: `string`

`default`: `{}`
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ function assertParser(config: MessageformatConfig) {
expect(parsedMale).toEqual('The smart boy named Henkie won his race');
});

it('should translate simple param and interpolate params inside messageformat string with joined braces', () => {
const parsedMale = parser.transpile(
'{count, plural, =1 {1 person} other {{count} people}}',
{ count: 10 },
{},
'key'
);
expect(parsedMale).toEqual('10 people');
});

it('should translate simple param and interpolate params inside messageformat string using custom interpolation markers', () => {
const parsedMale = parserWithCustomInterpolation.transpile(
'The <<< value >>> { gender, select, male {boy named <<< name >>> won his} female {girl named <<< name >>> won her} other {person named <<< name >>> won their}} race',
Expand Down
15 changes: 15 additions & 0 deletions libs/transloco-validator/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
displayName: 'transloco-validator',

globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/transloco-validator',
preset: '../../jest.preset.js',
};
16 changes: 0 additions & 16 deletions libs/transloco-validator/karma.conf.js

This file was deleted.

8 changes: 4 additions & 4 deletions libs/transloco-validator/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@angular-devkit/build-angular:karma",
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/libs/transloco-schematics"],
"options": {
"main": "libs/transloco-validator/src/test-setup.ts",
"tsConfig": "libs/transloco-validator/tsconfig.spec.json",
"karmaConfig": "libs/transloco-validator/karma.conf.js"
"jestConfig": "libs/transloco-validator/jest.config.ts",
"passWithNoTests": true
}
},
"version": {
Expand Down
12 changes: 10 additions & 2 deletions libs/transloco-validator/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
#!/usr/bin/env node
import commandLineArgs from 'command-line-args';

import validator from './lib/transloco-validator';

const translationFilePaths = process.argv.slice(2);
validator(translationFilePaths);

const optionDefinitions: commandLineArgs.OptionDefinition[] = [
{ name: 'interpolationForbiddenChars', type: String, defaultValue: '{}' },
{ name: 'file', alias: 'f', type: String, multiple: true, defaultValue: [], defaultOption: true },
];

const { interpolationForbiddenChars, file } = commandLineArgs(optionDefinitions);
validator(interpolationForbiddenChars, file);
37 changes: 37 additions & 0 deletions libs/transloco-validator/src/lib/transloco-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import fs from 'fs';

import validator from './transloco-validator';

jest.mock('fs');

describe('transloco-validator', () => {

it('should find duplicated keys', () => {
jest.mocked(fs.readFileSync).mockImplementation(() => '{"test":{"dupl1":"data","dupl1": "data","test": [{"dupl2":"data","dupl2": "data"}]}}');

const callValidator = () => validator('', ['mytest.json']);
expect(callValidator).toThrowError(new Error("Found duplicate keys: <instance>.test.dupl1,<instance>.test.test[0].dupl2 (mytest.json)"));
})

it('should find forbidden keys', () => {
jest.mocked(fs.readFileSync).mockImplementation(() => '{"test":{"forbidden{":"data","forbidden}": "data","test": [{"for{bidden}":"data"}]}}');

const callValidator = () => validator('{}', ['mytest.json']);
expect(callValidator).toThrowError(new Error("Found forbidden characters [{}] in keys: <instance>.test.forbidden{,<instance>.test.forbidden},<instance>.test.test[0].for{bidden} (mytest.json)"));
})

it('should find syntax error', () => {
jest.mocked(fs.readFileSync).mockImplementation(() => '{"test":{"erreur"}}');

const callValidator = () => validator('', ['mytest.json']);
expect(callValidator).toThrowError(SyntaxError);
})

it('should return success', () => {
jest.mocked(fs.readFileSync).mockImplementation(() => '{"test":{"erreur":123}}');

const callValidator = () => validator('', ['mytest.json']);
expect(callValidator).not.toThrowError();
})

})
46 changes: 41 additions & 5 deletions libs/transloco-validator/src/lib/transloco-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,55 @@ import fs from 'fs';

import findDuplicatedPropertyKeys from 'find-duplicated-property-keys';

export default function (translationFilePaths: string[]) {
export default function (interpolationForbiddenChars: string, translationFilePaths: string[]) {
translationFilePaths.forEach((path) => {
const translation = fs.readFileSync(path, 'utf-8');

// Verify that we can parse the JSON
JSON.parse(translation);
let parsedTranslation;
try {
parsedTranslation = JSON.parse(translation);
} catch(error) {
throw new SyntaxError(
`${error.message} (${path})`
);
}

// Verify that we don't have any duplicate keys
const result = findDuplicatedPropertyKeys(translation);
if (result.length) {
const duplicatedKeys = findDuplicatedPropertyKeys(translation);
if (duplicatedKeys.length) {
throw new Error(
`Found duplicate keys: ${result.map(({ key }) => key)} (${path})`
`Found duplicate keys: ${duplicatedKeys.map(dupl => dupl.toString())} (${path})`
);
}

const forbiddenKeys = findPropertyKeysContaining(parsedTranslation, interpolationForbiddenChars);
if (forbiddenKeys.length) {
throw new Error(
`Found forbidden characters [${interpolationForbiddenChars}] in keys: ${forbiddenKeys} (${path})`
);
}
});
}

function findPropertyKeysContaining(object: unknown, chars: string, parent = '<instance>') {
const found = [];
if (Array.isArray(object)) {
for(let i = 0; i < object.length; i++) {
const value = object[i];
found.push(...findPropertyKeysContaining(value, chars, `${parent}[${i}]`));
}
} else if (typeof object === 'object') {
for(const key in object) {
const value = object[key];
for (const char of chars) {
if (key.includes(char)) {
found.push(parent + '.' + key);
break;
}
}
found.push(...findPropertyKeysContaining(value, chars, `${parent}.${key}`));
}
}
return found;
}
3 changes: 2 additions & 1 deletion libs/transloco-validator/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"esModuleInterop": true,
"module": "commonjs",
"types": ["jasmine", "node"]
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
Expand Down
16 changes: 15 additions & 1 deletion libs/transloco/src/lib/tests/transpiler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,26 @@ describe('TranslocoTranspiler', () => {

function testDefaultBehaviour(
parser: TranslocoTranspiler,
[start, end]: [string, string] = defaultConfig.interpolation
[start, end, forbiddenChars]: [string, string, string?] = defaultConfig.interpolation
) {
function wrapParam(param: string) {
return `${start} ${param} ${end}`;
}

it('should skip if forbidden chars are used', () => {
if (forbiddenChars?.length) {
for (const char of forbiddenChars) {
const parsed = parser.transpile(
`Hello ${wrapParam('value ' + char)}`,
{ value: 'World' },
{},
'key'
);
expect(parsed).toEqual(`Hello ${wrapParam('value ' + char)}`);
}
}
});

it('should translate simple string from params', () => {
const parsed = parser.transpile(
`Hello ${wrapParam('value')}`,
Expand Down
4 changes: 2 additions & 2 deletions libs/transloco/src/lib/transloco.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
useFallbackTranslation: boolean;
allowEmpty: boolean;
};
interpolation: [string, string];
interpolation: [start: string, end: string, forbiddenChars?: string];
}

export const TRANSLOCO_CONFIG = new InjectionToken<TranslocoConfig>(
Expand All @@ -43,10 +43,10 @@
flatten: {
aot: false,
},
interpolation: ['{{', '}}'],
interpolation: ['{{', '}}', '{}'],
};

type DeepPartial<T> = T extends Array<any>

Check warning on line 49 in libs/transloco/src/lib/transloco.config.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
? T
: T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
Expand Down
11 changes: 9 additions & 2 deletions libs/transloco/src/lib/transloco.transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,16 @@ export class DefaultTranspiler implements TranslocoTranspiler {
}

function resolveMatcher(config: TranslocoConfig): RegExp {
const [start, end] = config.interpolation;
const [start, end, forbiddenChars] = config.interpolation;
const matchingParamName = forbiddenChars != undefined ? `[^${escapeForRegExp(forbiddenChars)}]` : '.';
return new RegExp(
`${escapeForRegExp(start)}(${matchingParamName}*?)${escapeForRegExp(end)}`,
'g'
);
}

return new RegExp(`${start}(.*?)${end}`, 'g');
function escapeForRegExp(text: string) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}

export interface TranslocoTranspilerFunction {
Expand Down
Loading