Skip to content
Merged
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ scaleway-lib is a set of NPM packages used at Scaleway.
![npm bundle size](https://img.shields.io/bundlephobia/min/@scaleway/outdated-browser)
![npm](https://img.shields.io/npm/v/@scaleway/outdated-browser)

- [`@scaleway/validate-icu-locales`](./packages/validate-icu-locales/README.md): A small cli to check ICU locales error

![npm](https://img.shields.io/npm/dm/@scaleway/validate-icu-locales)
![npm bundle size](https://img.shields.io/bundlephobia/min/@scaleway/validate-icu-locales)
![npm](https://img.shields.io/npm/v/@scaleway/validate-icu-locales)

## Development

### Locally
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"packages/*"
],
"type": "module",
"packageManager": "pnpm@7.14.0",
"devDependencies": {
"@babel/core": "7.19.6",
"@babel/eslint-parser": "7.19.1",
Expand Down Expand Up @@ -42,6 +43,7 @@
"read-pkg": "7.1.0",
"rollup": "3.2.3",
"rollup-plugin-dts": "5.0.0",
"rollup-plugin-preserve-shebangs": "^0.2.0",
"rollup-plugin-visualizer": "5.8.3",
"tsd-lite": "0.6.0",
"typescript": "4.8.4",
Expand Down Expand Up @@ -117,6 +119,5 @@
}
]
]
},
"packageManager": "pnpm@7.14.0"
}
}
10 changes: 10 additions & 0 deletions packages/validate-icu-locales/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const { join } = require('path')

module.exports = {
rules: {
'import/no-extraneous-dependencies': [
'error',
{ packageDir: [__dirname, join(__dirname, '../../')] },
],
},
}
49 changes: 49 additions & 0 deletions packages/validate-icu-locales/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# `@scaleway/validate-icu-locales`

## A tiny cli to handle ICU errors on locales files.
## Install

Requirements
- Node should be installed

```bash
$ pnpm add -D @scaleway/validate-icu-locales
```
## Usage

We can parse JSON, TS and JS if theses files use default export.


```
validate-icu "../**/en.json"
```

If there is an error on a local, the CLI will throw an Error and print all errors.


## Error
```
export default from: ../src/__tests__/locales/en-1.js is not an object
{
errors: [
{
message: 'EXPECT_ARGUMENT_CLOSING_BRACE',
value: '{count, plural, =0 {Minute} =1 {Minute} other {Minutes',
key: 'units.minutes.label',
filePath: '../../packages/validate-icu-locales/src/__tests__/locales/en.js'
},
{
message: 'EXPECT_ARGUMENT_CLOSING_BRACE',
value: '{count, plural, =0 {Minute} =1 {Minute} other {Minutes',
key: 'units.minutes.label',
filePath: '../../packages/validate-icu-locales/src/__tests__/locales/en.json'
},
{
message: 'EXPECT_ARGUMENT_CLOSING_BRACE',
value: '{count, plural, =0 {Minute} =1 {Minute} other {Minutes',
key: 'units.minutes.label',
filePath: '../../packages/validate-icu-locales/src/__tests__/locales/en.ts'
}
]
}
```
28 changes: 28 additions & 0 deletions packages/validate-icu-locales/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@scaleway/validate-icu-locales",
"version": "1.0.0",
"description": "A small cli to handle icu errors on locales files",
"keywords": [
"icu",
"i18n",
"cli"
],
"type": "module",
"bin": {
"validate-icu": "dist/index.js"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/scaleway/scaleway-lib",
"directory": "packages/validate-icu-locales"
},
"license": "MIT",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.1.10",
"globby": "^13.1.2",
"module-from-string": "^3.3.0"
}
}
9 changes: 9 additions & 0 deletions packages/validate-icu-locales/src/__tests__/locales/en-1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const locales = {
// error on this one missing bracket

'units.minutes.label':
'{count, plural, =0 {Minute} =1 {Minute} other {Minutes',
'units.hours.label': '{count, plural, =0 {Hour} =1 {Hour} other {Hours}}',
'units.days.label': '{count, plural, =0 {Day} =1 {Day} other {Days}}',
'units.months.label': '{count, plural, =0 {Month} =1 {Month} other {Months}}',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const toto = {
// error on this one missing bracket

'units.minutes.label':
'{count, plural, =0 {Minute} =1 {Minute} other {Minutes',
'units.hours.label': '{count, plural, =0 {Hour} =1 {Hour} other {Hours}}',
'units.days.label': '{count, plural, =0 {Day} =1 {Day} other {Days}}',
'units.months.label': '{count, plural, =0 {Month} =1 {Month} other {Months}}',
} as const
9 changes: 9 additions & 0 deletions packages/validate-icu-locales/src/__tests__/locales/en-ts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
// error on this one missing bracket

'units.minutes.label':
'{count, plural, =0 {Minute} =1 {Minute} other {Minutes',
'units.hours.label': '{count, plural, =0 {Hour} =1 {Hour} other {Hours}}',
'units.days.label': '{count, plural, =0 {Day} =1 {Day} other {Days}}',
'units.months.label': '{count, plural, =0 {Month} =1 {Month} other {Months}}',
} as const
9 changes: 9 additions & 0 deletions packages/validate-icu-locales/src/__tests__/locales/en.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
// error on this one missing bracket

'units.minutes.label':
'{count, plural, =0 {Minute} =1 {Minute} other {Minutes',
'units.hours.label': '{count, plural, =0 {Hour} =1 {Hour} other {Hours}}',
'units.days.label': '{count, plural, =0 {Day} =1 {Day} other {Days}}',
'units.months.label': '{count, plural, =0 {Month} =1 {Month} other {Months}}',
}
6 changes: 6 additions & 0 deletions packages/validate-icu-locales/src/__tests__/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"units.minutes.label": "{count, plural, =0 {Minute} =1 {Minute} other {Minutes",
"units.hours.label": "{count, plural, =0 {Hour} =1 {Hour} other {Hours}}",
"units.days.label": "{count, plural, =0 {Day} =1 {Day} other {Days}}",
"units.months.label": "{count, plural, =0 {Month} =1 {Month} other {Months}}"
}
9 changes: 9 additions & 0 deletions packages/validate-icu-locales/src/__tests__/locales/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
// error on this one missing bracket

'units.minutes.label':
'{count, plural, =0 {Minute} =1 {Minute} other {Minutes',
'units.hours.label': '{count, plural, =0 {Hour} =1 {Hour} other {Hours}}',
'units.days.label': '{count, plural, =0 {Day} =1 {Day} other {Days}}',
'units.months.label': '{count, plural, =0 {Month} =1 {Month} other {Months}}',
} as const
122 changes: 122 additions & 0 deletions packages/validate-icu-locales/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env node

import { parse } from '@formatjs/icu-messageformat-parser'
import { ParserError } from '@formatjs/icu-messageformat-parser/error'
import { readFile } from 'fs/promises'
import { globby } from 'globby'
import { importFromString } from 'module-from-string'

const args = process.argv.slice(2)
const pattern = args[0]
const { error, table } = console

type Locales = Record<string, string>
type ErrorICU = {
message: ParserError['message']
value: string
key: string
filePath: string
}

type ErrorsICU = (ErrorICU | undefined)[]

const isObject = (obj: unknown): obj is Record<string, unknown> =>
obj === Object(obj)

const findICUErrors = (
locales: { [key: string]: string },
filePath: string,
): ErrorsICU => {
const keys = Object.keys(locales)

const errors = keys
.map(key => {
const value = locales[key]

try {
parse(value)

return undefined
} catch (err) {
const { message } = err as ParserError

return {
message,
value,
key,
filePath,
}
}
})
.filter(Boolean)

return errors
}

const readFiles = async (files: string[]): Promise<ErrorsICU> => {
const errors = []

for await (const file of files) {
const extension = file.split('.').pop()

if (extension === 'json') {
try {
const data = await readFile(file)
const jsonFile = data.toString()

const locales = JSON.parse(jsonFile) as Locales

const ICUErrors = findICUErrors(locales, file)
errors.push(...ICUErrors)
} catch (err) {
error({ file, err })
}
}

if (extension === 'ts' || extension === 'js') {
try {
const data = await readFile(file)
const javascriptFile = data.toString()

const mod: unknown = await importFromString(javascriptFile, {
transformOptions: { loader: 'ts' },
})

if (isObject(mod)) {
if ('default' in mod) {
const { default: locales } = mod as { default: Locales }

const ICUErrors = findICUErrors(locales, file)
errors.push(...ICUErrors)
} else {
error('export default from: ', file, ' is not an object')
}
} else {
error(file, ' is not an object')
}
} catch (err) {
error({ err, file })
}
}
}

return errors
}

const files = await globby(pattern)

if (files.length === 0) {
error('There is no files matching this pattern', pattern)
process.exit(1)
}

table(files)

const errors = await readFiles(files)

if (errors.length > 0) {
error({
errors,
})
process.exit(1)
}
Loading