Skip to content

Commit

Permalink
fix: api changes and add schema
Browse files Browse the repository at this point in the history
  • Loading branch information
imranbarbhuiya committed Jul 16, 2023
1 parent 1fbab2c commit 41f3156
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 103 deletions.
79 changes: 79 additions & 0 deletions .github/i18n-validate.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"config": {
"type": "string",
"default": "./i18n-validate.json",
"description": "Path to the config file"
},
"exclude": {
"oneOf": [
{
"type": "array",
"items": { "type": "string" }
},
{ "type": "string" }
],
"default": "**/node_modules/**",
"description": "Exclude files from parsing"
},
"exitOnError": {
"type": "boolean",
"default": false,
"description": "Exit immediately if an error is found"
},
"functions": {
"type": "array",
"items": { "type": "string" },
"default": ["t", "i18next.t", "i18n.t"],
"description": "Names of the translation function"
},
"inputs": {
"oneOf": [
{
"type": "array",
"items": { "type": "string" }
},
{ "type": "string" }
],
"default": "**/*.{js,jsx,ts,tsx}",
"description": "Glob pattern to match input files"
},
"keySeparator": {
"type": "string",
"default": ".",
"description": "Key separator for nested translation keys"
},
"sourceLang": {
"type": "string",
"default": "en",
"description": "The source language of the translation keys"
},
"localeFolder": {
"type": "string",
"default": "i18n",
"description": "Path to translation files"
},
"localePath": {
"type": "string",
"default": "{{lng}}/{{ns}}.json",
"description": "Path to translation files. You can use `{{lng}}` for language and `{{ns}}` for namespace"
},
"logLevel": {
"type": "string",
"default": "info",
"description": "Log level"
},
"nsSeparator": {
"type": "string",
"default": ":",
"description": "Namespace separator for translation keys"
},
"nsFolderSeparator": {
"type": "string",
"default": "/",
"description": "Folder separator for translation keys"
}
}
}
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,48 @@

## Usage

```ts
```sh
npx i18n-validate
```

> **Note:** Currently, `i18n-validate` only supports `ts`, `tsx`, `js` and `jsx` source files and `json` translation files.
<!-- prettier-ignore-start -->
```sh
Usage: i18n-validate [options] <file ...>

Options:
-V, --version output the version number
-c, --config <config> Path to the config file (default:
"./i18n-validate.json")
--log-level <logLevel> Log level (default: "info")
--exclude <exclude...> Exclude files from parsing (default:
"**/node_modules/**")
--exitOnError Exit immediately if an error is found (default:
false)
-h, --help display help for command

Examples:

$ i18next-validate "/path/to/src/app.js"
$ i18next-validate --config i18n-validate-custom.json 'src/**/*.{js,jsx}'
$ i18next-validate --exclude "**/node_modules/**" "src/**/*.{js,jsx}"
```
<!-- prettier-ignore-end -->
You can disable the `i18n-validate` for a specific line by adding `// i18n-validate-disable-next-line` before the line.
> **Note**: Currently, `i18n-validate` only supports `ts`, `tsx`, `js` and `jsx` source files and `json` translation files.
## Configuration
You can customize the behavior of `i18n-validate` by adding a `i18n-validate.json` file to the root of your project.
<!-- Add info -->
```json
{
"$schema": "https://raw.githubusercontent.com/imranbarbhuiya/i18n-validate/.github/i18n-validate.schema.json"
}
```
> **Note:** You can also use `js`, `cjs` or `mjs` file and with any name you want. Just make sure to pass the path of the config file to `i18n-validate` using `--config` option.
> **Note**: You can also use `js`, `cjs` or `mjs` file and with any name you want. Just make sure to pass the path of the config file to `i18n-validate` using `--config` option.
## Buy me some doughnuts
Expand Down
13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
"i18next",
"i18next-lint"
],
"dependencies": {
"commander": "^11.0.0",
"glob": "^10.3.3",
"typescript": "^5.1.6"
},
"devDependencies": {
"@commitlint/cli": "^17.6.6",
"@commitlint/config-conventional": "^17.6.6",
Expand All @@ -48,7 +53,6 @@
"pinst": "^3.0.0",
"prettier": "^3.0.0",
"tsup": "^7.1.0",
"typescript": "^5.1.6",
"vitest": "^0.33.0"
},
"files": [
Expand Down Expand Up @@ -80,10 +84,5 @@
"ansi-regex": "^5.0.1",
"minimist": "^1.2.8"
},
"packageManager": "[email protected]",
"dependencies": {
"commander": "^11.0.0",
"glob": "^10.3.3",
"is-ci": "^3.0.1"
}
"packageManager": "[email protected]"
}
2 changes: 1 addition & 1 deletion src/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export class ValidationError extends Error {
) {
super(message);

this.stack = `ValidationError: ${message}\n at ${filePath}:${positions.start.line}:${positions.start.character}-${positions.end.line}:${positions.end.character}`;
this.stack = `\u001B[31mValidationError\u001B[0m: ${message}\n at ${filePath}:${positions.start.line}:${positions.start.character}-${positions.end.line}:${positions.end.character}`;
}
}
31 changes: 21 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import process from 'node:process';

import { Command } from 'commander';
import { Glob } from 'glob';
import isCI from 'is-ci';

import { ValidationError } from './Error.js';
import { log } from './logger.js';
Expand All @@ -18,17 +17,15 @@ const command = new Command()
.option('-c, --config <config>', 'Path to the config file', './i18n-validate.json')
.option('--log-level <logLevel>', 'Log level', 'info')
.option('--exclude <exclude...>', 'Exclude files from parsing', '**/node_modules/**')
.option('--error-on-invalid-key', 'Exit with error code 1 if invalid keys are found', isCI)
.option('--error-on-missing-variable', 'Exit with error code 1 if missing variables are found', isCI)
.option('--error-on-unused-variable', 'Exit with error code 1 if unused variables are found', false);
.option('--exit-on-error', 'Exit immediately if an error is found', false);

command.on('--help', () => {
console.log('');
console.log(' Examples:');
console.log('');
console.log(' $ i18next-validate "/path/to/src/app.js"');
console.log(" $ i18next-validate --config i18n-validate-custom.json 'src/**/*.{js,jsx}'");
console.log(' $ i18next-validate --exclude "**/node_modules/**" "src/**/*.{js,jsx}"');
console.log(' $ i18next-validate --exclude "**/node_modules/**" "src/**/*.{js,jsx}"');
console.log('');
});

Expand All @@ -51,6 +48,8 @@ options.inputs = options.inputs
})
.filter(Boolean);

log(`Parsed options:\n${JSON.stringify(options, null, 2)}`, 'debug', options);

if (options.inputs.length === 0) {
program.help();
process.exit(1);
Expand All @@ -60,15 +59,27 @@ const glob = new Glob(options.inputs, {
ignore: options.exclude
});

let errorCount = 0;

for await (const file of glob) {
log(`Parsing ${file}`, 'debug', options);
const translationNodes = parseFile(file, options);
console.log(translationNodes);

for (const node of translationNodes) {
if (!node.key || !node.namespace)
log(new ValidationError('Missing translation key or namespace', node.path, node.positions), 'invalidKey', options);

await validateKey(node, options);
if (!node.key || !node.namespace) {
log(new ValidationError('Missing translation key or namespace', node.path, node.positions), 'error', options);
errorCount++;
} else {
const valid = await validateKey(node, options);
if (!valid) errorCount++;
}
}
}

if (errorCount > 0) {
log(`Found ${errorCount} errors`, 'error', options);
process.exit(1);
} else {
log(`Found ${errorCount} errors`, 'info', options);
process.exit(0);
}
30 changes: 3 additions & 27 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,16 @@ import type { LogLevel, OptionsWithDefault } from './parseOptionsFile.js';

const logLevels = ['debug', 'info', 'warn', 'error'] as const;

const errorPrefix = '\u001B[31m[ERROR]\u001B[0m';
const warnPrefix = '\u001B[33m[WARN]\u001B[0m';
const infoPrefix = '\u001B[34m[INFO]\u001B[0m';
const debugPrefix = '[DEBUG]';

export const log = (
message: any,
_type: 'debug' | 'error' | 'info' | 'invalidKey' | 'missingVariable' | 'unusedVariable' | 'warn',
options: OptionsWithDefault
) => {
let type = _type as LogLevel;

if (_type === 'invalidKey')
if (options.errorOnInvalidKey) type = 'error';
else type = 'warn';

if (_type === 'missingVariable')
if (options.errorOnMissingVariable) type = 'error';
else type = 'warn';

if (_type === 'unusedVariable')
if (options.errorOnUnusedVariable) type = 'error';
else type = 'warn';

export const log = (message: any, type: LogLevel, options: OptionsWithDefault) => {
if (logLevels.indexOf(type) < logLevels.indexOf(options.logLevel)) return;

if (type === 'error') {
console.error(
errorPrefix,
message
// ['invalidKey', 'missingVariable'].includes(_type)
// ? '\n\nIf you want to ignore this error, add the following comment in your code:\n\u001B[33m// i18n-validate-disable-next-line\u001B[0m'
// : ''
);
if (options.exitOnError) throw message;
else console.error(message);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/parseFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const parseFile = (filePath: string, options: OptionsWithDefault) => {
const ignoreFunction = node
.getFullText(sourceFile)
.split('\n')
.find((line) => line.includes('i18n-validate-disable-next-line'));
.find((line) => line.startsWith('// i18n-validate-disable-next-line'));

if (ignoreFunction) return;

Expand Down
39 changes: 5 additions & 34 deletions src/parseOptionsFile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { URL } from 'node:url';

import isCI from 'is-ci';

export type LogLevel = 'debug' | 'error' | 'info' | 'warn';

const defaultOption = {
Expand All @@ -13,39 +11,18 @@ const defaultOption = {
config: './i18n-validate.json',

/**
* Throw an error if invalid keys are found
*
* @defaultValue {isCI}
*
* @remarks
* For CI/CD environments, it's default value is `true` else `false`
*/
errorOnInvalidKey: isCI,

/**
* Throw an error if variables are missing in the source code
*
* @defaultValue {isCI}
* Exclude files from parsing
*
* @remarks
* For CI/CD environments, it's default value is `true` else `false`
* @defaultValue '**\/node_modules/**'
*/
errorOnMissingVariable: isCI,
exclude: '**/node_modules/**' as string[] | string,

/**
* Throw an error if variables are unused in the source code
* Exit immediately if an error is found
*
* @defaultValue false
*
*/
errorOnUnusedVariable: false,

/**
* Exclude files from parsing
*
* @defaultValue '**\/node_modules/**'
*/
exclude: '**/node_modules/**' as string[] | string,
exitOnError: false,

/**
* names of the translation function
Expand Down Expand Up @@ -122,12 +99,6 @@ export async function parseOptionsFile(cliOptions: OptionsWithDefault): Promise<
const configUrl = new URL(config, import.meta.url);
const options = await import(configUrl.toString()).catch(() => ({}));

console.log(cliOptions, {
...defaultOption,
...options,
...cliOptions
});

return {
...defaultOption,
...options,
Expand Down
Loading

0 comments on commit 41f3156

Please sign in to comment.