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

feat: add locales generator #544

Merged
merged 9 commits into from
Feb 2, 2024
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
22 changes: 22 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions generators/locales/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# LOCALES-GENERATOR

Скрипт для автоматической генерации переводов `react-inl`.

Создаёт на уровне энтрипоинта `locales/[localName].json` файл, содержащий все переводы энтрипоинта на основе `id` и `defaultMessages`

Для работы обязательно указывать в `formatMessage` или `FormattedMessage` уникальный `id` и текст перевода в `defaultMessages`.

Пример валидного компонента:

```html
<Hello
hello={formatMessage({ id: 'app.home_page.hello', defaultMessage: 'HELLO FROM HOME' })}
/>
<Text>
<FormattedMessage id='app.home_page.content' defaultMessage='CONTENT' />
</Text>
```

---

## Установка

1. В каждый энтрипоинт, где используются переводы, устанавливаем пакет: `yarn add -D @atls-ui-generators/locales`
2. Добавляем в корневой `package.json` проекта следующий скрипт:
```json
{
"scripts": {
"postinstall": "yarn workspaces changed foreach --parallel run generate-locales"
}
}
```
3. Вносим изменения в энтрипоинты и вызываем `yarn postinstall` из корня проекта.

_\* достаточно изменить хотя бы один компонент, например фрагмент, в каждом энтрипоинте_

---

## Запуск

Вызов `yarn postinstall` из корня проекта вызовет генерацию переводов для энтрипоинтов, если в них произошли изменения.

Вызов `yarn generate-locales` из энтрипоинта вызовет генерацию для исходного энтрипоинта.

После успешной генерации в проекте должны сгенерироваться папки `locales`, содержащие актуальные переводы.

### Входные аргументы

`generate-locales` принимает следующие аргументы:

1. **componentPaths** - Относительный список путей от `package.json` энтрипоинта по которым будет производится поиск переводов \*_необязательный_.
- Принимается неограниченное кол-во аргументов. "generate-locales path1 path2 ...pathN"
- Если не указать **componentPaths** , скрипт будет проходить по фрагментам и страницам текущего энтрипоинта
2. **localName** - Название локали \*_необязательный_.
- Указывается после `-out=`
- Если не указывать **localName**, то умолчанию берётся локаль `ru`

Если необходимо вызвать скрипт с параметрами отличными от исходных, то в энтрипоинт необходимо добавить новый скрипт.

Пример расширенного скрипта:

```json
{
"scripts": {
"generate-locales": "generate-locales ../test-folder --out=en"
}
}
```

---

# Changelog

---
34 changes: 34 additions & 0 deletions generators/locales/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@atls-ui-generators/locales",
"version": "0.0.0",
"license": "BSD-3-Clause",
"main": "src/index.ts",
"bin": {
"generate-locales": "dist/generator.js"
},
"files": [
"dist"
],
"scripts": {
"build": "yarn library build",
"prepack": "yarn run build",
"postpack": "rm -rf dist"
},
"dependencies": {
"@atls/config-prettier": "0.0.5",
"@atls/prettier-plugin": "0.0.7",
"@babel/standalone": "7.22.20",
"camelcase": "6.3.0",
"commander": "9.5.0",
"prettier": "2.8.8"
},
"devDependencies": {
"@types/babel__standalone": "7.1.6",
"@types/prettier": "2.7.3"
},
"publishConfig": {
Nelfimov marked this conversation as resolved.
Show resolved Hide resolved
"access": "public",
"main": "dist/index.js",
"typings": "dist/index.d.ts"
}
}
22 changes: 22 additions & 0 deletions generators/locales/src/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defaultPaths } from './locales-generator.constants'
import { mergeLocales } from './merge-locales'
import { processDirectory } from './process-directory'

const allLocales = []
let outputFile = 'ru'
const argPaths: string[] = []

// @ts-ignore
process.argv.slice(2).forEach((arg) => {
if (!arg.startsWith('--out=')) {
argPaths.push(arg)
} else {
// eslint-disable-next-line prefer-destructuring
outputFile = arg.split('=')[1]
}
})
const paths = argPaths.length ? argPaths : defaultPaths

paths.forEach((path) => processDirectory(path, 'locales', allLocales, outputFile))

mergeLocales(allLocales, `./locales/${outputFile}.json`)
1 change: 1 addition & 0 deletions generators/locales/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './generator'
1 change: 1 addition & 0 deletions generators/locales/src/locales-generator.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const defaultPaths = ['../../fragments', '../../pages']
1 change: 1 addition & 0 deletions generators/locales/src/merge-locales/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './merge-locales'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type MergeLocalesType = (files: string[], outputPath: string) => void
26 changes: 26 additions & 0 deletions generators/locales/src/merge-locales/merge-locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { existsSync } from 'fs'
import { readFileSync } from 'fs'
import { mkdirSync } from 'fs'
import { writeFileSync } from 'fs'
import { dirname } from 'path'

import { MergeLocalesType } from './merge-locales.interfaces'

export const mergeLocales: MergeLocalesType = (files, outputPath) => {
if (!files.length) return

const mergedLocales = {}
files.forEach((file) => {
if (existsSync(file)) {
const content = JSON.parse(readFileSync(file, 'utf8'))
Object.assign(mergedLocales, content)
}
})
const directory = dirname(outputPath)

if (!existsSync(directory)) {
mkdirSync(directory, { recursive: true })
}

writeFileSync(outputPath, JSON.stringify(mergedLocales, null, 2), 'utf8')
}
1 change: 1 addition & 0 deletions generators/locales/src/process-directory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './process-directory'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type ProcessDirectoryType = (
startPath: string,
folderName: string,
allLocales: string[],
outputLocale: string
) => void
37 changes: 37 additions & 0 deletions generators/locales/src/process-directory/process-directory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable no-console */

import { execSync } from 'child_process'
import { existsSync } from 'fs'
import { readdirSync } from 'fs'
import { join } from 'path'

import { ProcessDirectoryType } from './process-directory.interfaces'
import { removeEmptyLocale } from '../remove-empty-locale'

export const processDirectory: ProcessDirectoryType = (
startPath,
folderName,
allLocales,
outputLocale
) => {
if (!existsSync(startPath)) {
console.error(new Error(`No directory ${startPath}`))
return
}

const directories = readdirSync(startPath, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name)

directories.forEach((dir) => {
const localePath = join(startPath, dir)
if (existsSync(localePath)) {
const outputFilePath = `${localePath}/${folderName}/${outputLocale}.json`
const command = `formatjs extract "${localePath}/**/*.tsx" --out-file "${outputFilePath}" --format simple`
console.log(`Running: ${command}`)
execSync(command, { stdio: 'inherit' })

removeEmptyLocale(outputFilePath, localePath, folderName, allLocales)
}
})
}
1 change: 1 addition & 0 deletions generators/locales/src/remove-empty-locale/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './remove-empty-locale'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type RemoveEmptyLocaleType = (
outputFilePath: string,
localePath: string,
folderName: string,
allLocales: string[]
) => void
28 changes: 28 additions & 0 deletions generators/locales/src/remove-empty-locale/remove-empty-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { existsSync } from 'fs'
import { readFileSync } from 'fs'
import { readdirSync } from 'fs'
import { rmSync } from 'fs'
import { unlinkSync } from 'fs'
import { join } from 'path'

import { RemoveEmptyLocaleType } from './remove-empty-locale.interfaces'

export const removeEmptyLocale: RemoveEmptyLocaleType = (
outputFilePath,
localePath,
folderName,
allLocales
) => {
if (existsSync(outputFilePath)) {
const content = readFileSync(outputFilePath, 'utf8').trim()
if (content === '{}' || content === '{\n}') {
unlinkSync(outputFilePath)

if (readdirSync(join(localePath, folderName)).length === 0) {
rmSync(join(localePath, folderName), { recursive: true, force: true })
}
} else {
allLocales.push(outputFilePath)
}
}
}
42 changes: 42 additions & 0 deletions generators/locales/src/unit/locales-generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { existsSync } from 'fs'
import { readFileSync } from 'fs'

import { mergeLocales } from '../merge-locales'
import { removeEmptyLocale } from '../remove-empty-locale'

describe('Locale processing script', () => {
describe('mergeLocales', () => {
const outputFileName = 'merged'
const outputPath = `generators/locales/src/unit/locales/${outputFileName}.json`

it('should merge locale files into one', () => {
mergeLocales(
[
'generators/locales/src/unit/mocks/test.json',
'generators/locales/src/unit/mocks/test1.json',
],
outputPath
)

const mergedContent = JSON.parse(readFileSync(outputPath, 'utf8'))
expect(mergedContent).toEqual({
test_id: 'test_message',
test_id1: 'test_message1',
})
})
it('should remove empty locale file', () => {
mergeLocales([''], outputPath)

const allLocales = []
const mergedContent = JSON.parse(readFileSync(outputPath, 'utf8'))
expect(mergedContent).toEqual({})
removeEmptyLocale(
'generators/locales/src/unit/locales/merged.json',
'generators/locales/src/unit',
'locales',
allLocales
)
expect(existsSync(outputPath)).toEqual(false)
})
})
})
3 changes: 3 additions & 0 deletions generators/locales/src/unit/mocks/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"test_id": "test_message"
}
3 changes: 3 additions & 0 deletions generators/locales/src/unit/mocks/test1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"test_id1": "test_message1"
}
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,23 @@ __metadata:
languageName: unknown
linkType: soft

"@atls-ui-generators/locales@workspace:generators/locales":
version: 0.0.0-use.local
resolution: "@atls-ui-generators/locales@workspace:generators/locales"
dependencies:
"@atls/config-prettier": "npm:0.0.5"
"@atls/prettier-plugin": "npm:0.0.7"
"@babel/standalone": "npm:7.22.20"
"@types/babel__standalone": "npm:7.1.6"
"@types/prettier": "npm:2.7.3"
camelcase: "npm:6.3.0"
commander: "npm:9.5.0"
prettier: "npm:2.8.8"
bin:
generate-locales: dist/generator.js
languageName: unknown
linkType: soft

"@atls-ui-generators/utils@workspace:*, @atls-ui-generators/utils@workspace:generators/utils":
version: 0.0.0-use.local
resolution: "@atls-ui-generators/utils@workspace:generators/utils"
Expand Down