diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..b107111
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,11 @@
+{
+ "presets": [
+ ["@babel/preset-env", {
+ "targets": {
+ "browsers":[
+ "defaults"
+ ]
+ }
+ }, "@babel/react"]
+ ]
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..edb4aba
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,10 @@
+# EditorConfig is awesome: http://EditorConfig.org
+root = true
+
+[*.{js,jsx,json,ts}]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..2a77c65
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,3 @@
+/dist
+# /types
+/examples
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..26399d6
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,21 @@
+{
+ "extends": [
+ "standard",
+ "plugin:require-path-exists/recommended"
+ ],
+ "plugins": [
+ "require-path-exists"
+ ],
+ "globals": {
+ "describe": false,
+ "it": false,
+ "before": false,
+ "after": false,
+ "beforeEach": false,
+ "afterEach": false
+ },
+ "rules": {
+ "array-bracket-spacing": 0,
+ "standard/no-callback-literal": 0
+ }
+}
\ No newline at end of file
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000..e1fa181
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,23 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 7
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - "discussion"
+ - "feature request"
+ - "bug"
+ - "breaking change"
+ - "doc"
+ - "issue"
+ - "help wanted"
+ - "good first issue"
+# Label to use when marking an issue as stale
+staleLabel: stale
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml
new file mode 100644
index 0000000..4c33775
--- /dev/null
+++ b/.github/workflows/node.yml
@@ -0,0 +1,24 @@
+name: node
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ test:
+ name: Test on node ${{ matrix.node }} and ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ node: [ '20.x', '18.x', '16.x' ]
+ # os: [ubuntu-latest, windows-latest, macOS-latest]
+ os: [ubuntu-latest]
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Node.js
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node }}
+ - run: npm install
+ - run: npm test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..25b4ef2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules
+.DS_Store
+npm-debug.log
+package-lock.json
+dist
+# types
+# !dist/
+# dist/*
+# !dist/deno/
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..ff19cdb
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,11 @@
+.DS_Store
+.eslintrc
+.gitignore
+.npmignore
+.editorconfig
+.vscode
+README.md
+.eslintignore
+test
+scripts
+examples
\ No newline at end of file
diff --git a/.ts.eslintrc b/.ts.eslintrc
new file mode 100644
index 0000000..72db1d0
--- /dev/null
+++ b/.ts.eslintrc
@@ -0,0 +1,37 @@
+{
+ "extends": [
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint"],
+ "env": { "node": true },
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module",
+ "project": "./tsconfig.json",
+ "createDefaultProgram": true
+ },
+ "rules": {
+ "semi": ["error", "never"],
+ "import/export": "off", // this errors on multiple exports (overload interfaces)
+ "require-path-exists/exists": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.d.ts","*.test-d.ts"],
+ "rules": {
+ "@typescript-eslint/no-explicit-any": "off"
+ }
+ },
+ {
+ "files": ["*.test-d.ts"],
+ "rules": {
+ "@typescript-eslint/no-empty-function": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-non-null-assertion": "off"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8859237
--- /dev/null
+++ b/README.md
@@ -0,0 +1,142 @@
+# Introduction
+
+[](https://github.com/i18next/i18next-resources-for-ts/actions?query=workflow%3Anode)
+[](https://www.npmjs.com/package/i18next-resources-for-ts)
+
+This package helps to transform resources to be used in a typesafe i18next project.
+
+# Getting started
+
+Source can be loaded via [npm](https://www.npmjs.com/package/i18next-resources-for-ts).
+
+```bash
+# npm package
+$ npm install i18next-resources-for-ts
+```
+
+## Usage via code (toc):
+
+```js
+import { tocForResources } from 'i18next-resources-for-ts'
+
+const nsA = {
+ name: 'nsA',
+ path: '/some/path/locales/en/nsA.json'
+}
+const nsB = {
+ name: 'nsB',
+ path: '/some/path/locales/en/nsB.json'
+}
+
+const toc = tocForResources([nsA, nsB], '/some/path')
+// import nsA from './locales/en/nsA.json';
+// import nsB from './locales/en/nsB.json';
+
+// export default {
+// nsA,
+// nsB
+// }
+```
+
+## Usage via code (merge):
+
+```js
+import { mergeResources } from 'i18next-resources-for-ts'
+
+const nsA = {
+ name: 'nsA',
+ path: '/some/path/locales/en/nsA.json',
+ resources: {
+ k1: 'v1',
+ k2: 'v2',
+ k3: {
+ d3: 'v3'
+ }
+ }
+}
+const nsB = {
+ name: 'nsB',
+ path: '/some/path/locales/en/nsB.json',
+ resources: {
+ k21: 'v21',
+ k22: 'v22',
+ k23: {
+ d23: 'v23'
+ }
+ }
+}
+
+const merged = mergeResources([nsA, nsB])
+// {
+// nsA: {
+// k1: 'v1',
+// k2: 'v2',
+// k3: {
+// d3: 'v3'
+// }
+// },
+// nsB: {
+// k21: 'v21',
+// k22: 'v22',
+// k23: {
+// d23: 'v23'
+// }
+// }
+// }
+```
+
+Usage via CLI:
+
+```sh
+# use it with npx
+npx i18next-resources-for-ts subcommand -i /Users/user/my/input -o /Users/user/my/output
+
+# or install it globally
+npm install i18next-resources-for-ts -g
+
+# subcommand is either toc or merge
+# -i is the input path
+# -o is the output path
+# if the output path is not provided, it will use the input path as base path for the result file
+
+i18next-resources-for-ts toc -i /Users/user/my/input -o /Users/user/my/output.ts
+i18next-resources-for-ts merge -i /Users/user/my/input -o /Users/user/my/output.json
+# i18next-resources-for-ts toc /Users/user/my/input -o /Users/user/my/output
+# i18next-resources-for-ts toc -o /Users/user/my/output
+# i18next-resources-for-ts toc -i /Users/user/my/input
+# i18next-resources-for-ts toc
+```
+
+*Make sure your folder structure contains all relevant namespaces (in your source/reference language):*
+
+```sh
+└── namespace.json
+```
+
+i.e.
+```sh
+├── translation.json
+└── common.json
+```
+
+---
+
+
Gold Sponsors
+
+
+
+
+
+
+
+---
+
+**From the creators of i18next: localization as a service - locize.com**
+
+A translation management system built around the i18next ecosystem - [locize.com](https://locize.com).
+
+
+
+With using [locize](http://locize.com/?utm_source=react_i18next_readme&utm_medium=github) you directly support the future of i18next.
+
+---
diff --git a/bin/getNamespaces.js b/bin/getNamespaces.js
new file mode 100755
index 0000000..cc52965
--- /dev/null
+++ b/bin/getNamespaces.js
@@ -0,0 +1,40 @@
+const fs = require('fs')
+const path = require('path')
+
+const getFiles = (srcpath) => {
+ return fs.readdirSync(srcpath).filter((file) => {
+ return !fs.statSync(path.join(srcpath, file)).isDirectory()
+ }).filter((file) => path.extname(file) === '.json').map((file) => path.join(srcpath, file))
+}
+
+const getDirectories = (srcpath) => {
+ return fs.readdirSync(srcpath).filter((file) => {
+ return fs.statSync(path.join(srcpath, file)).isDirectory()
+ }).map((dir) => path.join(srcpath, dir))
+}
+
+function getAllFiles (srcpath) {
+ let files = getFiles(srcpath)
+ const dirs = getDirectories(srcpath)
+ dirs.forEach((dir) => {
+ files = files.concat(getAllFiles(dir))
+ })
+ return files
+}
+
+module.exports = (p) => {
+ const allFiles = getAllFiles(p)
+
+ return allFiles.map((file) => {
+ const namespace = JSON.parse(fs.readFileSync(file, 'utf-8'))
+ const sepFile = file.split(path.sep)
+ const fileName = sepFile[sepFile.length - 1]
+ const name = path.parse(fileName).name
+
+ return {
+ name,
+ path: file,
+ resources: namespace
+ }
+ })
+}
diff --git a/bin/i18next-resources-for-ts.js b/bin/i18next-resources-for-ts.js
new file mode 100755
index 0000000..84f087a
--- /dev/null
+++ b/bin/i18next-resources-for-ts.js
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+
+const fs = require('fs')
+const path = require('path')
+const getNamespaces = require('./getNamespaces.js')
+const {
+ tocForResources,
+ mergeResources
+} = require('../')
+
+const cliArgs = process.argv.slice(2)
+
+const subCommands = [
+ 'toc',
+ 'merge'
+]
+
+if (cliArgs.length === 0 || subCommands.indexOf(cliArgs[0]) < 0) {
+ throw new Error(`Please define an appropriate subcommand: ${subCommands.join(', ')}!`)
+}
+
+const subCommand = cliArgs[0]
+
+let inputPath = process.cwd()
+let outputPath = inputPath
+if (cliArgs.length > 0 && cliArgs[1].indexOf('-') !== 0) {
+ inputPath = cliArgs[1]
+ outputPath = inputPath
+}
+
+const inputArgIndex = cliArgs.indexOf('-i')
+const outputArgIndex = cliArgs.indexOf('-o')
+if (inputArgIndex > -1 && cliArgs[inputArgIndex + 1]) inputPath = cliArgs[inputArgIndex + 1]
+if (outputArgIndex > -1 && cliArgs[outputArgIndex + 1]) outputPath = cliArgs[outputArgIndex + 1]
+
+const namespaces = getNamespaces(inputPath)
+
+if (subCommand === 'toc') {
+ let outputFile = outputPath
+ if (!outputFile.endsWith('.ts')) {
+ outputFile = path.join(outputFile, 'resources.ts')
+ }
+ const toc = tocForResources(namespaces, outputFile)
+ fs.writeFileSync(outputFile, toc, 'utf-8')
+ console.log(`created toc resources file for ${namespaces.length} namespaces: ${outputFile}`)
+}
+
+if (subCommand === 'merge') {
+ const merged = mergeResources(namespaces)
+ let outputFile = outputPath
+ if (!outputFile.endsWith('.json')) {
+ outputFile = path.join(outputFile, 'resources.json')
+ }
+ fs.writeFileSync(outputFile, JSON.stringify(merged, null, 2), 'utf-8')
+ console.log(`created merged resources file for ${namespaces.length} namespaces: ${outputFile}`)
+}
diff --git a/bin/package.json b/bin/package.json
new file mode 100644
index 0000000..729ac4d
--- /dev/null
+++ b/bin/package.json
@@ -0,0 +1 @@
+{"type":"commonjs"}
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..b20cbdd
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,23 @@
+export type Resources = {
+ [key: string]: any
+}
+
+export type NamespaceForToc = {
+ name: string;
+ path: string;
+ resources?: Resources;
+}
+export type NamespacesForToc = NamespaceForToc[]
+
+export type NamespaceForMerge = {
+ name: string;
+ path?: string;
+ resources: Resources;
+}
+export type NamespacesForMerge = NamespaceForMerge[]
+export type Merged = {
+ [ns: string]: Resources;
+}
+
+export function tocForResources (namespaces: NamespacesForToc, toPath: string, options?: { quotes?: 'single' | 'double' }): string
+export function mergeResources (namespaces: NamespacesForMerge): Merged
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..28ea284
--- /dev/null
+++ b/index.js
@@ -0,0 +1,9 @@
+import {
+ tocForResources,
+ mergeResources
+} from './src/index.js'
+
+export {
+ tocForResources,
+ mergeResources
+}
diff --git a/licence b/licence
new file mode 100644
index 0000000..d520788
--- /dev/null
+++ b/licence
@@ -0,0 +1,19 @@
+Copyright (c) 2023 i18next
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9043a12
--- /dev/null
+++ b/package.json
@@ -0,0 +1,95 @@
+{
+ "name": "i18next-resources-for-ts",
+ "version": "1.0.0",
+ "description": "This package helps to transform resources to be used in a typesafe i18next project.",
+ "keywords": [
+ "i18next",
+ "typescript"
+ ],
+ "homepage": "https://github.com/i18next/i18next-resources-for-ts",
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:i18next/i18next-resources-for-ts.git"
+ },
+ "bugs": {
+ "url": "https://github.com/i18next/i18next-resources-for-ts/issues"
+ },
+ "type": "module",
+ "main": "./dist/cjs/index.js",
+ "module": "./dist/esm/index.js",
+ "browser": "./dist/umd/i18nextResourcesForTS.js",
+ "types": "./index.d.ts",
+ "exports": {
+ "./package.json": "./package.json",
+ ".": {
+ "types": {
+ "require": "./dist/cjs/index.d.ts",
+ "import": "./dist/esm/index.d.ts"
+ },
+ "module": "./dist/esm/index.js",
+ "import": "./dist/esm/index.js",
+ "require": "./dist/cjs/index.js",
+ "default": "./dist/esm/index.js"
+ },
+ "./cjs": {
+ "types": "./dist/cjs/index.d.ts",
+ "default": "./dist/cjs/index.js"
+ },
+ "./esm": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ },
+ "./src": {
+ "default": "./src/index.js"
+ }
+ },
+ "bin": {
+ "i18next-resources-for-ts": "./bin/i18next-resources-for-ts.js"
+ },
+ "scripts": {
+ "lint:javascript": "eslint .",
+ "lint:typescript": "eslint -c .ts.eslintrc *.d.ts test/types/**/*.test-d.ts",
+ "lint": "npm run lint:javascript && npm run lint:typescript",
+ "build": "rm -rf dist && rollup -c && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json && cp index.d.ts dist/cjs/index.d.ts && cp index.d.ts dist/esm/index.d.ts",
+ "test:typescript": "tsd",
+ "test": "npm run lint && mocha --colors --reporter spec --recursive test/*.js",
+ "test:all": "npm run test && npm run test:typescript",
+ "preversion": "npm run test && npm run build && git push",
+ "postversion": "git push && git push --tags"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.5"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.22.5",
+ "@babel/plugin-transform-runtime": "^7.22.5",
+ "@babel/preset-env": "^7.22.5",
+ "@types/mocha": "^10.0.1",
+ "@typescript-eslint/eslint-plugin": "^5.59.9",
+ "@typescript-eslint/parser": "^5.59.9",
+ "babel-plugin-add-module-exports": "^1.0.4",
+ "eslint": "^8.42.0",
+ "eslint-config-standard": "^17.1.0",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-n": "^15.7.0",
+ "eslint-plugin-promise": "^6.1.1",
+ "eslint-plugin-require-path-exists": "^1.1.9",
+ "eslint-plugin-standard": "^5.0.0",
+ "i18next": "^22.5.1",
+ "i18next-chained-backend": "^4.3.0",
+ "mocha": "^10.2.0",
+ "rollup": "^2.79.1",
+ "@rollup/plugin-babel": "^6.0.3",
+ "@rollup/plugin-commonjs": "^25.0.1",
+ "@rollup/plugin-node-resolve": "^15.1.0",
+ "rollup-plugin-terser": "^7.0.2",
+ "should": "^13.2.3",
+ "sinon": "^15.1.0",
+ "tsd": "^0.28.1",
+ "typescript": "^5.1.3"
+ },
+ "tsd": {
+ "directory": "test/types"
+ }
+}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..66c3b8c
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,82 @@
+import { babel } from '@rollup/plugin-babel'
+import { nodeResolve } from '@rollup/plugin-node-resolve'
+import { terser } from 'rollup-plugin-terser'
+import commonjs from '@rollup/plugin-commonjs'
+import pkg from './package.json'
+
+const getBabelOptions = ({ useESModules, plugins = [] }) => ({
+ exclude: /node_modules/,
+ babelHelpers: 'runtime',
+ plugins: [['@babel/transform-runtime', { useESModules }]].concat(plugins),
+ comments: false
+})
+
+const input = './src/index.js'
+// check relative and absolute paths for windows and unix
+const external = id => !id.startsWith('.') && !id.startsWith('/') && !id.includes(':')
+
+export default [
+ {
+ input,
+ output: {
+ dir: 'dist/cjs',
+ preserveModules: true,
+ // file: pkg.main,
+ format: 'cjs'
+ },
+ external,
+ // external: [
+ // ...Object.keys(pkg.dependencies || {})
+ // ],
+ plugins: [babel(getBabelOptions({
+ useESModules: false,
+ plugins: [['add-module-exports']]
+ }))]
+ },
+ {
+ input,
+ output: {
+ dir: 'dist/esm',
+ preserveModules: true,
+ // file: pkg.module,
+ format: 'esm' // the preferred format
+ },
+ external,
+ // external: [
+ // ...Object.keys(pkg.dependencies || {})
+ // ],
+ plugins: [babel(getBabelOptions({ useESModules: true }))]
+ },
+ // this is not used, if we make sure every js file is imported with .js ending
+ // {
+ // input,
+ // output: {
+ // dir: 'dist/deno',
+ // preserveModules: true,
+ // // file: pkg.module,
+ // format: 'esm' // the preferred format
+ // },
+ // external
+ // // external: [
+ // // ...Object.keys(pkg.dependencies || {})
+ // // ]
+ // },
+ {
+ input,
+ output: {
+ file: pkg.browser,
+ format: 'umd',
+ name: 'i18nextResourcesForTS' // the global which can be used in a browser
+ },
+ plugins: [commonjs(), babel(getBabelOptions({ useESModules: true })), nodeResolve()]
+ },
+ {
+ input,
+ output: {
+ file: pkg.browser.replace('.js', '.min.js'),
+ format: 'umd',
+ name: 'i18nextResourcesForTS' // the global which can be used in a browser
+ },
+ plugins: [commonjs(), babel(getBabelOptions({ useESModules: true })), nodeResolve(), terser()]
+ }
+]
diff --git a/src/defaults.js b/src/defaults.js
new file mode 100644
index 0000000..85c7e1b
--- /dev/null
+++ b/src/defaults.js
@@ -0,0 +1,5 @@
+const defaults = {
+ quotes: 'single'
+}
+
+export default defaults
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..0aea10e
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,7 @@
+import tocForResources from './tocForResources.js'
+import mergeResources from './mergeResources.js'
+
+export {
+ tocForResources,
+ mergeResources
+}
diff --git a/src/mergeResources.js b/src/mergeResources.js
new file mode 100644
index 0000000..7d59624
--- /dev/null
+++ b/src/mergeResources.js
@@ -0,0 +1,8 @@
+function mergeResources (namespaces) {
+ return namespaces.reduce((prev, cur) => {
+ prev[cur.name] = cur.resources
+ return prev
+ }, {})
+}
+
+export default mergeResources
diff --git a/src/relative.js b/src/relative.js
new file mode 100644
index 0000000..3fa942c
--- /dev/null
+++ b/src/relative.js
@@ -0,0 +1,60 @@
+function trim (arr, startOnly) {
+ let start = 0
+ for (; start < arr.length; start++) {
+ if (arr[start] !== '') break
+ }
+
+ let end = arr.length - 1
+ for (; end >= 0; end--) {
+ if (arr[end] !== '') break
+ }
+
+ if (start > end) return []
+ if (startOnly) return arr.slice(start)
+ return arr.slice(start, end - start + 1)
+}
+
+const pathSeparatorWin = '\\'
+const pathSeparator = '/'
+
+function relative (from, to) {
+ let separator = pathSeparator
+ if (from.indexOf(pathSeparatorWin) > 0 || to.indexOf(pathSeparatorWin) > 0) {
+ separator = pathSeparatorWin
+ }
+
+ if (from.endsWith(separator)) from = from.substring(0, from.length - separator.length)
+
+ const toParts = trim(to.split(separator), true)
+
+ const lowerFromParts = trim(from.split(separator), from.indexOf('.') < 0)
+ const lowerToParts = trim(to.split(separator), true)
+
+ const length = Math.min(lowerFromParts.length, lowerToParts.length)
+ let samePartsLength = length
+
+ for (let i = 0; i < length; i++) {
+ if (lowerFromParts[i] !== lowerToParts[i]) {
+ samePartsLength = i
+ break
+ }
+ }
+
+ if (samePartsLength === 0) return to
+
+ let outputParts = []
+ for (let i = samePartsLength; i < lowerFromParts.length; i++) {
+ outputParts.push('..')
+ }
+
+ outputParts = outputParts.concat(toParts.slice(samePartsLength))
+
+ let ret = outputParts.join(separator)
+ if (!ret.startsWith(from.substring(0, 2)) && !ret.startsWith('.')) {
+ ret = `.${pathSeparator}${ret}`
+ }
+
+ return ret
+}
+
+export default relative
diff --git a/src/tocForResources.js b/src/tocForResources.js
new file mode 100644
index 0000000..38b0026
--- /dev/null
+++ b/src/tocForResources.js
@@ -0,0 +1,26 @@
+import defaults from './defaults.js'
+import relative from './relative.js'
+
+function tocForResources (namespaces, toPath, options = {}) {
+ const opt = { ...options, ...defaults }
+ const quoteChar = opt.quotes === 'single' ? '\'' : '"'
+
+ let toc = ''
+
+ namespaces.forEach((ns) => {
+ toc += `import ${ns.name} from ${quoteChar}${relative(toPath, ns.path)}${quoteChar};\n`
+ })
+
+ toc += '\nexport default {'
+ namespaces.forEach((ns, i) => {
+ toc += `\n ${ns.name}`
+ if (i < namespaces.length - 1) {
+ toc += ','
+ }
+ })
+ toc += '\n}\n'
+
+ return toc
+}
+
+export default tocForResources
diff --git a/test/basic.spec.js b/test/basic.spec.js
new file mode 100644
index 0000000..ec7df37
--- /dev/null
+++ b/test/basic.spec.js
@@ -0,0 +1,55 @@
+import {
+ tocForResources,
+ mergeResources
+} from '../index.js'
+import should from 'should'
+
+const nsA = {
+ name: 'nsA',
+ path: '/some/path/locales/en/nsA.json',
+ resources: {
+ k1: 'v1',
+ k2: 'v2',
+ k3: {
+ d3: 'v3'
+ }
+ }
+}
+const nsB = {
+ name: 'nsB',
+ path: '/some/path/locales/en/nsB.json',
+ resources: {
+ k21: 'v21',
+ k22: 'v22',
+ k23: {
+ d23: 'v23'
+ }
+ }
+}
+
+const allMerged = { nsA: nsA.resources, nsB: nsB.resources }
+
+const toc = `import nsA from './locales/en/nsA.json';
+import nsB from './locales/en/nsB.json';
+
+export default {
+ nsA,
+ nsB
+}
+`
+
+describe('tocForResources', () => {
+ it('should generate a toc file content from namespace resources', async () => {
+ const tocRet = tocForResources([nsA, nsB], '/some/path')
+ // console.log(tocRet)
+ should(tocRet).eql(toc)
+ })
+})
+
+describe('mergeResources', () => {
+ it('should generate a big json from namespace resources', async () => {
+ const merged = mergeResources([nsA, nsB])
+ // console.log(merged)
+ should(merged).eql(allMerged)
+ })
+})
diff --git a/test/types/basic.test-d.ts b/test/types/basic.test-d.ts
new file mode 100644
index 0000000..26f661f
--- /dev/null
+++ b/test/types/basic.test-d.ts
@@ -0,0 +1,9 @@
+import {
+ tocForResources,
+ mergeResources,
+ Merged
+} from '../../'
+import { expectType } from 'tsd'
+
+expectType(tocForResources([{ name: 'ns1', path: '/some/path' }], '/some'))
+expectType(mergeResources([{ name: 'ns1', resources: { key: 'val' } }]))
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0b29b93
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "es6",
+ "lib": [ "es2015" ],
+ "module": "commonjs",
+ "noEmit": true,
+ "strict": true
+ },
+ "include": [
+ "/test/types/*.test-d.ts",
+ "/*.d.ts"
+ ]
+}