Skip to content

Commit

Permalink
Merge pull request #16 from rawand-faraidun/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
rawand-faraidun authored Nov 27, 2023
2 parents 0859d6e + 8161a2e commit 584c5e6
Show file tree
Hide file tree
Showing 18 changed files with 1,757 additions and 1,109 deletions.
7 changes: 7 additions & 0 deletions .changeset/early-eagles-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'linguify': minor
---

Use single translation file:
* allow to have one translation file for each locale instead of a file for each namespace
* `useSingleFile` config option
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ Linguify translation files manager

Via npm
```bash
$ npm install linguify --save-dev
npm install linguify --save-dev
```

Via pnpm
```bash
$ pnpm add -D linguify
pnpm add -D linguify
```

Via yarn
```bash
$ yarn add -D linguify
yarn add -D linguify
```

### Usage

1. Initiate Linguify with the `init` command

```bash
$ linguify init
linguify init
```

* This will create `linguify.config.json` file at the root of your project.
Expand All @@ -43,24 +43,26 @@ $ linguify init

* `defaultLocale`: default locale to your application.

* `useSingleFile`: determines to use one translation file for each locale or not.

<br />

3. Start linguify.

```bash
$ linguify
linguify
```

or

```bash
$ linguify start
linguify start
```

* Linguify server port can be changed using `-p` or `--port` option following the desired port

```bash
$ linguify -p 3000
linguify -p 3000
```

* Note: Updating `linguify.config.json` while Linguify runs requires restarting it before affecting it.
Expand All @@ -74,7 +76,7 @@ It uses `defaultLocale` as the base of translations and namespaces, and copies m
the `sync` operation happenes everytime Linguify starts, to sync translations manually you can use `sync` command.

```bash
$ linguify sync
linguify sync
```

<hr />
Expand Down
8 changes: 7 additions & 1 deletion assets/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@
"description": "default locale to your application",
"default": "en",
"$comment": "default locale to your application"
},
"useSingleFile": {
"type": "boolean",
"description": "determines to use single file translations or not",
"default": false,
"$comment": "determines to use single file translations or not"
}
},
"required": ["localesPath", "locales", "defaultLocale"]
"required": ["localesPath", "locales", "defaultLocale", "useSingleFile"]
}
12 changes: 10 additions & 2 deletions lib/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { Config } from './types'

/**
* default config props
*/
type DefaultConfig = Config & {
$schema: string
}

/**
* configuration file name
*/
Expand All @@ -13,11 +20,12 @@ export const configSchemaPath = 'https://raw.githubusercontent.com/rawand-faraid
/**
* linguify default values
*/
export const defaultConfig = {
export const defaultConfig: DefaultConfig = {
$schema: configSchemaPath,
localesPath: './public/locales',
locales: ['en'],
defaultLocale: 'en'
defaultLocale: 'en',
useSingleFile: false
}

/**
Expand Down
29 changes: 27 additions & 2 deletions lib/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,20 @@ export const getPath = (...paths: string[]) => resolve(rootPath, config.localesP
*
* @returns array of namespaces
*/
export const getNamespaces = () => readdirSync(getPath(config.defaultLocale)).filter(file => extname(file) == '.json')
export const getNamespaces = () =>
(config.useSingleFile
? Object.keys(JSON.parse(readFileSync(getPath(`${config.defaultLocale}.json`), 'utf-8')))
: readdirSync(getPath(config.defaultLocale)).filter(file => extname(file) == '.json')
).sort()

/**
* gets content of a file as json
*
* @param paths - path to namespace
*
* @returns file json content
*/
export const getFileJson = (...paths: string[]) => JSON.parse(readFileSync(getPath(...paths), 'utf-8'))

/**
* gets content of a namespace as json
Expand All @@ -42,7 +55,19 @@ export const getNamespaces = () => readdirSync(getPath(config.defaultLocale)).fi
*
* @returns namespace json content
*/
export const getNamespaceJson = (...paths: string[]) => JSON.parse(readFileSync(getPath(...paths), 'utf-8'))
export const getNamespaceJson = (...paths: string[]) => {
if (config.useSingleFile) {
const path = paths.slice(0, paths.length - 1)
if (!path[path.length - 1]?.toLowerCase().endsWith('.json')) {
path[path.length - 1] = `${path[path.length - 1]}.json`
}
const namespace = paths[paths.length - 1]

return getFileJson(...path)[namespace!]
} else {
return getFileJson(...paths)
}
}

/**
* checks if a namespace excists
Expand Down
8 changes: 8 additions & 0 deletions lib/linguifyValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ export const linguifyValidation = () => {
chalk.yellow(`Provided 'defaultLocale' is not included in in 'locales', please change it before starting`)
)
}

// checking useSingleFile
if (typeof config.useSingleFile == 'undefined') {
throw new Error(chalk.yellow(`Linguify config file misses 'useSingleFile' key, please add it before starting`))
}
if (typeof config.useSingleFile != 'boolean') {
throw new Error(chalk.yellow(`Provided 'useSingleFile' is not boolean, please change it before starting`))
}
} catch (error: any) {
console.error(chalk.red(error.message))
process.exit(0)
Expand Down
27 changes: 24 additions & 3 deletions lib/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ interface Options {
separator?: string
}

/**
* clear options props
*/
interface ClearOptions {
skipFirstDepth?: boolean
}

/**
* flatten object into single-depth object
*
Expand All @@ -19,6 +26,8 @@ interface Options {
export const flatten = (object: DynamicObject, { separator = '.' }: Options = {}) => {
const flattened: DynamicObject = {}

if (!_.isObject(object)) throw new Error("'object' parameter must be a valid object")

for (const key in object) {
if (!object.hasOwnProperty(key)) continue

Expand All @@ -33,6 +42,7 @@ export const flatten = (object: DynamicObject, { separator = '.' }: Options = {}
flattened[key] = object[key]
}
}

return flattened
}

Expand All @@ -47,6 +57,8 @@ export const flatten = (object: DynamicObject, { separator = '.' }: Options = {}
export const unflatten = (flatObject: DynamicObject, { separator = '.' }: Options = {}) => {
const nested: DynamicObject = {}

if (!_.isObject(flatObject)) throw new Error("'object' parameter must be a valid object")

for (const key in flatObject) {
if (!flatObject.hasOwnProperty(key)) continue

Expand Down Expand Up @@ -75,12 +87,21 @@ export const unflatten = (flatObject: DynamicObject, { separator = '.' }: Option
* clears object from empty nested objects
*
* @param object - object to clear
* @param options - options
*
* @note if `skipFirstDepth` is true it will reset any first depth key that is not object
*
* @returns clear object
*/
export const clear = (object: DynamicObject): DynamicObject => {
return _(object).pickBy(_.isObject).mapValues(clear).omitBy(_.isEmpty).assign(_.omitBy(object, _.isObject)).value()
}
export const clear = (object: DynamicObject, { skipFirstDepth = false }: ClearOptions = {}): DynamicObject =>
skipFirstDepth
? _(object)
.pickBy(_.isObject)
.mapValues(clear)
.assign(_.omitBy(object, _.isObject))
.mapValues(v => (_.isObject(v) ? v : {}))
.value()
: _(object).pickBy(_.isObject).mapValues(clear).omitBy(_.isEmpty).assign(_.omitBy(object, _.isObject)).value()

/**
* check if a path is assignable to the object, returns unvalid path if not
Expand Down
73 changes: 51 additions & 22 deletions lib/syncNamespaces.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'
import chalk from 'chalk'
import _ from 'lodash'
import { getNamespaceJson, getNamespaces, getPath } from './functions'
import { getFileJson, getNamespaceJson, getNamespaces, getPath } from './functions'
import { clear } from './object'
import type { DynamicObject } from './types'
import { config, otherLocales } from '@lib/utils'
Expand All @@ -15,11 +15,17 @@ export const syncNamespaces = () => {
try {
// checking or creating locale files
config.locales.forEach(locale => {
const path = getPath(locale)
const path = config.useSingleFile ? getPath(`${locale}.json`) : getPath(locale)
if (!existsSync(path)) {
mkdirSync(path)
config.useSingleFile ? writeFileSync(path, '{}') : mkdirSync(path)
} else {
if (!statSync(path).isDirectory()) {
if (config.useSingleFile && !statSync(path).isFile()) {
throw new Error(
chalk.yellow(
`Provided locale '${locale}' is not a valid json file '${locale}.json', please check if a directory exists with the same name, please change it before starting`
)
)
} else if (!config.useSingleFile && !statSync(path).isDirectory()) {
throw new Error(
chalk.yellow(
`Provided locale '${locale}' is not a valid directory name, please check if a file exists with the same name, please change it before starting`
Expand All @@ -29,40 +35,63 @@ export const syncNamespaces = () => {
}
})

// default locale namespaces
const defaultNSs = getNamespaces()

// each namespace keys
const nsKeys: DynamicObject = {}
let nsKeys: DynamicObject = {}

// getting default namespaces and keys
defaultNSs.forEach(ns => {
const path = getPath(config.defaultLocale, ns)
if (config.useSingleFile) {
const path = getPath(`${config.defaultLocale}.json`)
const file = readFileSync(path, 'utf-8')
let json: DynamicObject = {}
try {
json = clear(JSON.parse(file))
json = clear(JSON.parse(file), { skipFirstDepth: true })
writeFileSync(path, JSON.stringify(json))
} catch {
writeFileSync(path, '{}')
}
nsKeys[ns] = json
})
nsKeys = json
} else {
// default locale namespaces
const defaultNSs = getNamespaces()

defaultNSs.forEach(ns => {
const path = getPath(config.defaultLocale, ns)
const file = readFileSync(path, 'utf-8')
let json: DynamicObject = {}
try {
json = clear(JSON.parse(file))
writeFileSync(path, JSON.stringify(json))
} catch {
writeFileSync(path, '{}')
}
nsKeys[ns] = json
})
}

// syncing keys with other files
otherLocales.forEach(locale => {
Object.keys(nsKeys).forEach(ns => {
const path = getPath(locale, ns)
if (!existsSync(path)) {
return writeFileSync(path, JSON.stringify({ ...nsKeys[ns] }))
}
if (config.useSingleFile) {
const path = getPath(`${locale}.json`)
try {
const json = clear(getNamespaceJson(locale, ns))
writeFileSync(path, JSON.stringify(_.defaultsDeep(json, { ...nsKeys[ns] })))
const json = clear(getFileJson(`${locale}.json`), { skipFirstDepth: true })
writeFileSync(path, JSON.stringify(_.defaultsDeep(json, nsKeys)))
} catch {
writeFileSync(path, JSON.stringify({ ...nsKeys[ns] }))
writeFileSync(path, JSON.stringify(nsKeys))
}
})
} else {
Object.keys(nsKeys).forEach(ns => {
const path = getPath(locale, ns)
if (!existsSync(path)) {
return writeFileSync(path, JSON.stringify({ ...nsKeys[ns] }))
}
try {
const json = clear(getNamespaceJson(locale, ns))
writeFileSync(path, JSON.stringify(_.defaultsDeep(json, { ...nsKeys[ns] })))
} catch {
writeFileSync(path, JSON.stringify({ ...nsKeys[ns] }))
}
})
}
})
} catch (error: any) {
console.error(chalk.red(error.message))
Expand Down
9 changes: 9 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,13 @@ export type Config = {
* @default 'en'
*/
defaultLocale: string

/**
* determines to use single file translations or not
*
* instead of having a file for each namespace, it allows to have one file for each locale
*
* @default false
*/
useSingleFile: boolean
}
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,19 @@
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
"@types/cors": "^2.8.16",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.17.41",
"@types/findup-sync": "^4.0.4",
"@types/lodash": "^4.14.201",
"@types/node": "^20.9.0",
"@types/prompts": "^2.4.8",
"@types/lodash": "^4.14.202",
"@types/node": "^20.10.0",
"@types/prompts": "^2.4.9",
"concurrently": "^8.2.2",
"ncp": "^2.0.0",
"nodemon": "^3.0.1",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"typescript": "^5.2.2"
"tsup": "^8.0.1",
"typescript": "^5.3.2"
},
"files": [
"dist/**/*",
Expand Down
Loading

0 comments on commit 584c5e6

Please sign in to comment.