├── .babelrc ├── test ├── locales │ └── en │ │ ├── bad.js │ │ └── test.js └── backend.js ├── .coveralls.yml ├── .eslintignore ├── index.js ├── .npmignore ├── .editorconfig ├── .travis.yml ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── package.json ├── src ├── utils.js └── index.js ├── lib ├── utils.js └── index.js ├── .eslintrc └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /test/locales/en/bad.js: -------------------------------------------------------------------------------- 1 | module.exports = 'randomString'; 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: hJ5JGN2FbTz0AF22Dm6Ki2diXuB8qVxYI 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/* 2 | **/node_modules/* 3 | **/*.min.* 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index.js').default; 2 | -------------------------------------------------------------------------------- /test/locales/en/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | key: 'passing', 3 | evaluated: 1 + 1 4 | }; 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | src/ 3 | coverage/ 4 | .babelrc 5 | .editorconfig 6 | .eslintignore 7 | .eslintrc 8 | .gitignore 9 | gulpfile.js 10 | karma.conf.js 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*.{js,jsx,json}] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | before_script: 5 | - npm install -g gulp 6 | - npm install -g mocha 7 | after_script: NODE_ENV=test istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore specific files 2 | .settings.xml 3 | .monitor 4 | .DS_Store 5 | *.orig 6 | npm-debug.log 7 | npm-debug.log.* 8 | *.dat 9 | 10 | # Ignore various temporary files 11 | *~ 12 | *.swp 13 | 14 | 15 | # Ignore various Node.js related directories and files 16 | node_modules 17 | node_modules/**/* 18 | coverage/**/* 19 | package-lock.json 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 i18next 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v2.1.3 2 | 3 | - Update js-yaml to a non vulnerable version [238](https://github.com/i18next/i18next-node-fs-backend/pull/238) 4 | 5 | ### v2.1.2 6 | 7 | - Bump dep versions due to DOS vulnerability [237](https://github.com/i18next/i18next-node-fs-backend/pull/237) 8 | 9 | ### v2.1.1 10 | 11 | - fixes loadPath [230](https://github.com/i18next/i18next-node-fs-backend/pull/230) 12 | 13 | ### v2.1.0 14 | 15 | - optional pass in loadPath, addPath as function 16 | 17 | ### v2.0.0 18 | 19 | - remove cson parser 20 | - adds option.parse to add custom parser 21 | 22 | ### v1.2.1 23 | 24 | - fix missing break in cson parser 25 | 26 | ### v1.2.0 27 | 28 | - support cson parser [224](https://github.com/i18next/i18next-node-fs-backend/pull/224) 29 | 30 | ### v1.1.0 31 | 32 | - support for keySeparator = false [222](https://github.com/i18next/i18next-node-fs-backend/issues/222) 33 | 34 | ### v1.0.0 35 | 36 | - support for reading js files (no write supported!) [PR216](https://github.com/i18next/i18next-node-fs-backend/pull/216) 37 | - update build to no longer depend on gulp 38 | 39 | ### v0.1.x 40 | 41 | - json support (read/write) 42 | - yaml support (read) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "i18next-node-fs-backend", 3 | "version": "2.1.3", 4 | "description": "node.js backend layer for i18next using fs module to load resources from filesystem", 5 | "main": "./index.js", 6 | "keywords": [ 7 | "i18next", 8 | "i18next-backend" 9 | ], 10 | "homepage": "https://github.com/i18next/i18next-node-fs-backend", 11 | "bugs": "https://github.com/i18next/i18next-node-fs-backend/issues", 12 | "dependencies": { 13 | "js-yaml": "3.13.1", 14 | "json5": "2.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-cli": "6.26.0", 18 | "babel-core": "6.26.3", 19 | "babel-eslint": "8.2.6", 20 | "babel-preset-es2015": "6.24.1", 21 | "babel-preset-stage-0": "6.24.1", 22 | "chai": "^4.1.2", 23 | "coveralls": "3.0.2", 24 | "eslint": "5.3.0", 25 | "i18next": "11.6.0", 26 | "istanbul": "0.4.5", 27 | "mocha": "5.2.0", 28 | "mockery": "2.1.0", 29 | "sinon": "6.1.5", 30 | "watchify": "3.11.1", 31 | "yargs": "12.0.1" 32 | }, 33 | "scripts": { 34 | "test": "mocha", 35 | "transpile": "babel src -d lib", 36 | "build": "npm run transpile", 37 | "version": "npm run build", 38 | "postversion": "git push && git push --tags && rm -rf build/temp" 39 | }, 40 | "author": "Jan Mühlemann (https://github.com/jamuhl)", 41 | "license": "MIT", 42 | "repository": "https://github.com/i18next/i18next-node-fs-backend.git" 43 | } 44 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function debounce(func, wait, immediate) { 2 | var timeout; 3 | return function() { 4 | var context = this, args = arguments; 5 | var later = function() { 6 | timeout = null; 7 | if (!immediate) func.apply(context, args); 8 | }; 9 | var callNow = immediate && !timeout; 10 | clearTimeout(timeout); 11 | timeout = setTimeout(later, wait); 12 | if (callNow) func.apply(context, args); 13 | }; 14 | }; 15 | 16 | function getLastOfPath(object, path, Empty) { 17 | function cleanKey(key) { 18 | return (key && key.indexOf('###') > -1) ? key.replace(/###/g, '.') : key; 19 | } 20 | 21 | let stack = (typeof path !== 'string') ? [].concat(path) : path.split('.'); 22 | while(stack.length > 1) { 23 | if (!object) return {}; 24 | 25 | let key = cleanKey(stack.shift()); 26 | if (!object[key] && Empty) object[key] = new Empty(); 27 | object = object[key]; 28 | } 29 | 30 | if (!object) return {}; 31 | return { 32 | obj: object, 33 | k: cleanKey(stack.shift()) 34 | }; 35 | } 36 | 37 | export function setPath(object, path, newValue) { 38 | let { obj, k } = getLastOfPath(object, path, Object); 39 | 40 | obj[k] = newValue; 41 | } 42 | 43 | export function pushPath(object, path, newValue, concat) { 44 | let { obj, k } = getLastOfPath(object, path, Object); 45 | 46 | obj[k] = obj[k] || []; 47 | if (concat) obj[k] = obj[k].concat(newValue); 48 | if (!concat) obj[k].push(newValue); 49 | } 50 | 51 | export function getPath(object, path) { 52 | let { obj, k } = getLastOfPath(object, path); 53 | 54 | if (!obj) return undefined; 55 | return obj[k]; 56 | } 57 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.debounce = debounce; 7 | exports.setPath = setPath; 8 | exports.pushPath = pushPath; 9 | exports.getPath = getPath; 10 | function debounce(func, wait, immediate) { 11 | var timeout; 12 | return function () { 13 | var context = this, 14 | args = arguments; 15 | var later = function later() { 16 | timeout = null; 17 | if (!immediate) func.apply(context, args); 18 | }; 19 | var callNow = immediate && !timeout; 20 | clearTimeout(timeout); 21 | timeout = setTimeout(later, wait); 22 | if (callNow) func.apply(context, args); 23 | }; 24 | }; 25 | 26 | function getLastOfPath(object, path, Empty) { 27 | function cleanKey(key) { 28 | return key && key.indexOf('###') > -1 ? key.replace(/###/g, '.') : key; 29 | } 30 | 31 | var stack = typeof path !== 'string' ? [].concat(path) : path.split('.'); 32 | while (stack.length > 1) { 33 | if (!object) return {}; 34 | 35 | var key = cleanKey(stack.shift()); 36 | if (!object[key] && Empty) object[key] = new Empty(); 37 | object = object[key]; 38 | } 39 | 40 | if (!object) return {}; 41 | return { 42 | obj: object, 43 | k: cleanKey(stack.shift()) 44 | }; 45 | } 46 | 47 | function setPath(object, path, newValue) { 48 | var _getLastOfPath = getLastOfPath(object, path, Object), 49 | obj = _getLastOfPath.obj, 50 | k = _getLastOfPath.k; 51 | 52 | obj[k] = newValue; 53 | } 54 | 55 | function pushPath(object, path, newValue, concat) { 56 | var _getLastOfPath2 = getLastOfPath(object, path, Object), 57 | obj = _getLastOfPath2.obj, 58 | k = _getLastOfPath2.k; 59 | 60 | obj[k] = obj[k] || []; 61 | if (concat) obj[k] = obj[k].concat(newValue); 62 | if (!concat) obj[k].push(newValue); 63 | } 64 | 65 | function getPath(object, path) { 66 | var _getLastOfPath3 = getLastOfPath(object, path), 67 | obj = _getLastOfPath3.obj, 68 | k = _getLastOfPath3.k; 69 | 70 | if (!obj) return undefined; 71 | return obj[k]; 72 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | parser: babel-eslint 3 | 4 | ecmaFeatures: 5 | arrowFunctions: true 6 | blockBindings: true 7 | classes: true 8 | defaultParams: true 9 | destructuring: true 10 | forOf: true 11 | generators: false 12 | modules: true 13 | objectLiteralComputedProperties: true 14 | objectLiteralDuplicateProperties: false 15 | objectLiteralShorthandMethods: true 16 | objectLiteralShorthandProperties: true 17 | spread: true 18 | superInFunctions: true 19 | templateStrings: true 20 | 21 | env: 22 | browser: true 23 | node: true 24 | es6: true 25 | 26 | globals: 27 | __resourceQuery: false 28 | bootstrap: false 29 | describe: false 30 | describeSaga: false 31 | describeEvent: false 32 | describeCommand: false 33 | describeScene: false 34 | before: false 35 | it: false 36 | xit: false 37 | window : false 38 | beforeEach : false 39 | afterEach : false 40 | after : false 41 | before : false 42 | beforeEachChapter: false 43 | describeScenario: false 44 | describeChapter: false 45 | describeStep: false 46 | document : false 47 | window: false 48 | File : false 49 | FormData: false 50 | QCodeDecoder: false 51 | $: false 52 | L: false 53 | btoa: false 54 | escape: false 55 | angular: false 56 | jQuery: false 57 | ga: false 58 | 59 | settings: 60 | jsx: true 61 | 62 | ecmaFeatures: 63 | jsx: true 64 | 65 | rules: 66 | 67 | # ERRORS 68 | curly: [2, "multi-line"] 69 | 70 | # WARNINGS 71 | no-unused-vars: [1, {vars: all, args: none}] 72 | semi-spacing: 1 73 | no-empty: 1 74 | handle-callback-err: 1 75 | eqeqeq: 1 76 | quotes: [1, 'single'] 77 | no-unused-expressions: 1 78 | no-throw-literal: 1 79 | semi: 1 80 | block-scoped-var: 1 81 | no-alert: 1 82 | no-console: 1 83 | new-cap: 1 84 | space-unary-ops: 1 85 | 86 | # DISABLED 87 | space-after-keywords: 0 88 | dot-notation: 0 89 | consistent-return: 0 90 | brace-style: 0 91 | no-multi-spaces: 0 92 | no-underscore-dangle: 0 93 | key-spacing: 0 94 | comma-spacing: 0 95 | no-shadow: 0 96 | no-mixed-requires: 0 97 | space-infix-ops: 0 98 | strict: 0 99 | camelcase: 0 100 | no-wrap-func: 0 101 | comma-dangle: 0 102 | no-extra-semi: 0 103 | no-use-before-define: [0, "nofunc"] 104 | 105 | # AUTOMATED BY EDITORCONFIG 106 | eol-last: 0 107 | no-trailing-spaces: 0 108 | indent: 0 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | Can be replaced with: [i18next-fs-backend](https://github.com/i18next/i18next-fs-backend) 4 | 5 | --- 6 | 7 | # Introduction 8 | 9 | [![Travis](https://img.shields.io/travis/i18next/i18next-node-fs-backend/master.svg?style=flat-square)](https://travis-ci.org/i18next/i18next-node-fs-backend) 10 | [![Coveralls](https://img.shields.io/coveralls/i18next/i18next-node-fs-backend/master.svg?style=flat-square)](https://coveralls.io/github/i18next/i18next-node-fs-backend) 11 | [![npm version](https://img.shields.io/npm/v/i18next-node-fs-backend.svg?style=flat-square)](https://www.npmjs.com/package/i18next-node-fs-backend) 12 | [![David](https://img.shields.io/david/i18next/i18next-node-fs-backend.svg?style=flat-square)](https://david-dm.org/i18next/i18next-node-fs-backend) 13 | 14 | This is a i18next backend to be used node.js. It will load resources from filesystem. Right now it supports following filetypes: 15 | 16 | - .json 17 | - .json5 18 | - .yml 19 | - .cson 20 | 21 | # Getting started 22 | 23 | Source can be loaded via [npm](https://www.npmjs.com/package/i18next-node-fs-backend). 24 | 25 | ``` 26 | $ npm install i18next-node-fs-backend 27 | ``` 28 | 29 | Wiring up: 30 | 31 | ```js 32 | var i18next = require("i18next"); 33 | var Backend = require("i18next-node-fs-backend"); 34 | 35 | i18next.use(Backend).init(i18nextOptions); 36 | ``` 37 | 38 | As with all modules you can either pass the constructor function (class) to the i18next.use or a concrete instance. 39 | 40 | ## Backend Options 41 | 42 | ```js 43 | { 44 | // path where resources get loaded from 45 | loadPath: '/locales/{{lng}}/{{ns}}.json', 46 | 47 | // path to post missing resources 48 | addPath: '/locales/{{lng}}/{{ns}}.missing.json', 49 | 50 | // jsonIndent to use when storing json files 51 | jsonIndent: 2, 52 | 53 | // custom parser 54 | parse: function(data) { return data; } 55 | } 56 | ``` 57 | 58 | **hint** {{lng}}, {{ns}} use the same prefix, suffix you define in interpolation for translations!!! 59 | 60 | Options can be passed in: 61 | 62 | **preferred** - by setting options.backend in i18next.init: 63 | 64 | ```js 65 | var i18next = require("i18next"); 66 | var Backend = require("i18next-node-fs-backend"); 67 | 68 | i18next.use(Backend).init({ 69 | backend: options 70 | }); 71 | ``` 72 | 73 | on construction: 74 | 75 | ```js 76 | var Backend = require("i18next-node-fs-backend"); 77 | var backend = new Backend(null, options); 78 | ``` 79 | 80 | by calling init: 81 | 82 | ```js 83 | var Backend = require("i18next-node-fs-backend"); 84 | var backend = new Backend(); 85 | backend.init(options); 86 | ``` 87 | 88 | --- 89 | 90 |

Gold Sponsors

91 | 92 |

93 | 94 | 95 | 96 |

97 | -------------------------------------------------------------------------------- /test/backend.js: -------------------------------------------------------------------------------- 1 | var mockery = require('mockery'); 2 | var expect = require('chai').expect; 3 | var path = require('path'); 4 | 5 | var Interpolator = require('i18next/dist/commonjs/Interpolator').default; 6 | 7 | var test3Save = 0; 8 | var test4Save = 0; 9 | 10 | var fsMock = { 11 | readFile: function (path, encoding, cb) { 12 | if (path.indexOf('test.json') > -1) return cb(null, '{"key": "passing"}'); 13 | if (path.indexOf('test3.missing.json') > -1 && test3Save > 0) return cb(null, JSON.stringify({ key1: '1', key2: '2' }, null, 2)); 14 | 15 | cb(null, '{}'); 16 | }, 17 | 18 | writeFile: function(path, data, cb) { 19 | if (path.indexOf('test.missing.json') > -1) { 20 | expect(data).to.be.eql(JSON.stringify({some: { key: 'myDefault' }}, null, 2)); 21 | } 22 | else if (path.indexOf('test2.missing.json') > -1) { 23 | expect(data).to.be.eql(JSON.stringify({ key1: '1', key2: '2', key3: '3', key4: '4' }, null, 2)); 24 | } 25 | else if (path.indexOf('test3.missing.json') > -1 && test3Save === 0) { 26 | test3Save = test3Save + 1; 27 | expect(data).to.be.eql(JSON.stringify({ key1: '1', key2: '2' }, null, 2)); 28 | } 29 | else if (path.indexOf('test3.missing.json') > -1 && test3Save > 0) { 30 | expect(data).to.be.eql(JSON.stringify({ key1: '1', key2: '2', key3: '3', key4: '4' }, null, 2)); 31 | } 32 | else if (path.indexOf('test4.missing.json') > -1) { 33 | test4Save = test4Save + 1; 34 | expect(data).to.be.eql(JSON.stringify({ key1: '1', key2: '2', key3: '3', key4: '4' }, null, 2)); 35 | } 36 | 37 | cb(null); 38 | } 39 | }; 40 | 41 | 42 | describe('backend', function() { 43 | var Backend; 44 | var backend; 45 | 46 | before(function() { 47 | mockery.enable(); 48 | mockery.registerMock('fs', fsMock); 49 | 50 | Backend = require('../lib').default; 51 | backend = new Backend({ 52 | interpolator: new Interpolator() 53 | }, { 54 | loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json' 55 | }); 56 | }); 57 | 58 | after(function() { 59 | mockery.disable(); 60 | }); 61 | 62 | it('read', function(done) { 63 | backend.read('en', 'test', function(err, data) { 64 | expect(err).to.be.not.ok; 65 | expect(data).to.eql({key: 'passing'}); 66 | done(); 67 | }); 68 | }); 69 | 70 | it('read javascript files', function(done) { 71 | backend = new Backend({ 72 | interpolator: new Interpolator() 73 | }, { 74 | loadPath: path.join(__dirname, '/locales/{{lng}}/{{ns}}.js') 75 | }); 76 | 77 | backend.read('en', 'test', function(err, data) { 78 | expect(err).to.be.not.ok; 79 | expect(data).to.eql({key: 'passing', evaluated: 2}); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('fail if a bad js file is provided', function(done) { 85 | backend = new Backend({ 86 | interpolator: new Interpolator() 87 | }, { 88 | loadPath: path.join(__dirname, '/locales/{{lng}}/{{ns}}.js') 89 | }); 90 | 91 | backend.read('en', 'bad', function(err, data) { 92 | expect(err).to.be.ok; 93 | expect(data).to.eql(false); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('create simple', function(done) { 99 | backend.create('en', 'test', 'some.key', 'myDefault', function() { 100 | done(); 101 | }); 102 | }); 103 | 104 | it('create multiple', function(done) { 105 | backend.create('en', 'test2', 'key1', '1') 106 | backend.create('en', 'test2', 'key2', '2') 107 | backend.create('en', 'test2', 'key3', '3') 108 | backend.create('en', 'test2', 'key4', '4', function() { 109 | done(); 110 | }); 111 | }); 112 | 113 | it('create multiple - with pause', function(done) { 114 | backend.create('en', 'test3', 'key1', '1') 115 | backend.create('en', 'test3', 'key2', '2', function() { 116 | setTimeout(function () { 117 | backend.create('en', 'test3', 'key3', '3') 118 | backend.create('en', 'test3', 'key4', '4', function() { 119 | done(); 120 | }); 121 | }, 200); 122 | }); 123 | }); 124 | 125 | it('create multiple with multiple languages to write to (saveMissingTo=all)', function(done) { 126 | backend.create(['en', 'de'], 'test4', 'key1', '1') 127 | backend.create(['en', 'de'], 'test4', 'key2', '2') 128 | backend.create(['en', 'de'], 'test4', 'key3', '3') 129 | backend.create(['en', 'de'], 'test4', 'key4', '4', function() { 130 | expect(test4Save).to.equal(2); 131 | done(); 132 | }); 133 | }); 134 | 135 | }); 136 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as utils from './utils'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import JSON5 from 'json5'; 5 | import YAML from 'js-yaml'; 6 | 7 | function getDefaults() { 8 | return { 9 | loadPath: '/locales/{{lng}}/{{ns}}.json', 10 | addPath: '/locales/{{lng}}/{{ns}}.missing.json', 11 | jsonIndent: 2, 12 | parse: JSON.parse 13 | }; 14 | } 15 | 16 | function readFile(filename, options, callback) { 17 | const extension = path.extname(filename); 18 | let result; 19 | 20 | if (/^\.(js|ts)$/.test(extension)) { 21 | try { 22 | const file = require(filename); 23 | result = file.default ? file.default : file; 24 | 25 | if (typeof result !== 'object') { 26 | return callback(new Error('A resource file must export an object.')); 27 | } 28 | 29 | callback(null, result); 30 | } catch (err) { 31 | callback(err); 32 | } 33 | } else { 34 | fs.readFile(filename, 'utf8', function(err, data) { 35 | if (err) { 36 | callback(err); 37 | } else { 38 | try { 39 | data = data.replace(/^\uFEFF/, ''); 40 | switch(extension) { 41 | case '.json5': 42 | result = JSON5.parse(data); 43 | break; 44 | case '.yml': 45 | case '.yaml': 46 | result = YAML.safeLoad(data); 47 | break; 48 | default: 49 | result = options.parse(data); 50 | } 51 | } catch (err) { 52 | err.message = 'error parsing ' + filename + ': ' + err.message; 53 | return callback(err); 54 | } 55 | callback(null, result); 56 | } 57 | }); 58 | } 59 | } 60 | 61 | class Backend { 62 | constructor(services, options = {}) { 63 | this.init(services, options); 64 | 65 | this.type = 'backend'; 66 | } 67 | 68 | init(services, options = {}, coreOptions = {}) { 69 | this.services = services; 70 | this.options = this.options || {}; 71 | this.options = {...getDefaults(), ...this.options, ...options}; 72 | this.coreOptions = coreOptions; 73 | this.queuedWrites = {}; 74 | 75 | this.debouncedWrite = utils.debounce(this.write, 250); 76 | } 77 | 78 | read(language, namespace, callback) { 79 | let loadPath = this.options.loadPath; 80 | if (typeof this.options.loadPath === 'function') { 81 | loadPath = this.options.loadPath(language, namespace); 82 | } 83 | 84 | let filename = this.services.interpolator.interpolate(loadPath, { lng: language, ns: namespace }); 85 | 86 | readFile(filename, this.options, (err, resources) => { 87 | if (err) return callback(err, false); // no retry 88 | callback(null, resources); 89 | }); 90 | } 91 | 92 | create(languages, namespace, key, fallbackValue, callback) { 93 | if (!callback) callback = () => {}; 94 | if (typeof languages === 'string') languages = [languages]; 95 | 96 | let todo = languages.length; 97 | function done() { 98 | if (!--todo) callback && callback(); 99 | } 100 | 101 | languages.forEach(lng => { 102 | this.queue.call(this, lng, namespace, key, fallbackValue, done); 103 | }); 104 | } 105 | 106 | // write queue 107 | write() { 108 | for (let lng in this.queuedWrites) { 109 | const namespaces = this.queuedWrites[lng]; 110 | if (lng !== 'locks') { 111 | for (let ns in namespaces) { 112 | this.writeFile(lng, ns); 113 | } 114 | } 115 | } 116 | } 117 | 118 | writeFile(lng, namespace) { 119 | let lock = utils.getPath(this.queuedWrites, ['locks', lng, namespace]); 120 | if (lock) return; 121 | 122 | let addPath = this.options.addPath; 123 | if (typeof this.options.addPath === 'function') { 124 | addPath = this.options.addPath(lng, namespace); 125 | } 126 | 127 | let filename = this.services.interpolator.interpolate(addPath, { lng: lng, ns: namespace }); 128 | 129 | let missings = utils.getPath(this.queuedWrites, [lng, namespace]); 130 | utils.setPath(this.queuedWrites, [lng, namespace], []); 131 | 132 | if (missings.length) { 133 | // lock 134 | utils.setPath(this.queuedWrites, ['locks', lng, namespace], true); 135 | 136 | readFile(filename, this.options, (err, resources) => { 137 | if (err) resources = {}; 138 | 139 | missings.forEach((missing) => { 140 | const path = this.coreOptions.keySeparator === false ? [missing.key] : (missing.key.split(this.coreOptions.keySeparator || '.')); 141 | utils.setPath(resources, path, missing.fallbackValue); 142 | }); 143 | 144 | fs.writeFile(filename, JSON.stringify(resources, null, this.options.jsonIndent), (err) => { 145 | // unlock 146 | utils.setPath(this.queuedWrites, ['locks', lng, namespace], false); 147 | 148 | missings.forEach((missing) => { 149 | if (missing.callback) missing.callback(); 150 | }); 151 | 152 | // rerun 153 | this.debouncedWrite(); 154 | }); 155 | }); 156 | } 157 | } 158 | 159 | queue(lng, namespace, key, fallbackValue, callback) { 160 | utils.pushPath(this.queuedWrites, [lng, namespace], {key: key, fallbackValue: fallbackValue || '', callback: callback}); 161 | 162 | this.debouncedWrite(); 163 | } 164 | 165 | } 166 | 167 | Backend.type = 'backend'; 168 | 169 | 170 | export default Backend; 171 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 12 | 13 | var _utils = require('./utils'); 14 | 15 | var utils = _interopRequireWildcard(_utils); 16 | 17 | var _fs = require('fs'); 18 | 19 | var _fs2 = _interopRequireDefault(_fs); 20 | 21 | var _path = require('path'); 22 | 23 | var _path2 = _interopRequireDefault(_path); 24 | 25 | var _json = require('json5'); 26 | 27 | var _json2 = _interopRequireDefault(_json); 28 | 29 | var _jsYaml = require('js-yaml'); 30 | 31 | var _jsYaml2 = _interopRequireDefault(_jsYaml); 32 | 33 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 34 | 35 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 36 | 37 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 38 | 39 | function getDefaults() { 40 | return { 41 | loadPath: '/locales/{{lng}}/{{ns}}.json', 42 | addPath: '/locales/{{lng}}/{{ns}}.missing.json', 43 | jsonIndent: 2, 44 | parse: JSON.parse 45 | }; 46 | } 47 | 48 | function readFile(filename, options, callback) { 49 | var extension = _path2.default.extname(filename); 50 | var result = void 0; 51 | 52 | if (/^\.(js|ts)$/.test(extension)) { 53 | try { 54 | var file = require(filename); 55 | result = file.default ? file.default : file; 56 | 57 | if ((typeof result === 'undefined' ? 'undefined' : _typeof(result)) !== 'object') { 58 | return callback(new Error('A resource file must export an object.')); 59 | } 60 | 61 | callback(null, result); 62 | } catch (err) { 63 | callback(err); 64 | } 65 | } else { 66 | _fs2.default.readFile(filename, 'utf8', function (err, data) { 67 | if (err) { 68 | callback(err); 69 | } else { 70 | try { 71 | data = data.replace(/^\uFEFF/, ''); 72 | switch (extension) { 73 | case '.json5': 74 | result = _json2.default.parse(data); 75 | break; 76 | case '.yml': 77 | case '.yaml': 78 | result = _jsYaml2.default.safeLoad(data); 79 | break; 80 | default: 81 | result = options.parse(data); 82 | } 83 | } catch (err) { 84 | err.message = 'error parsing ' + filename + ': ' + err.message; 85 | return callback(err); 86 | } 87 | callback(null, result); 88 | } 89 | }); 90 | } 91 | } 92 | 93 | var Backend = function () { 94 | function Backend(services) { 95 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 96 | 97 | _classCallCheck(this, Backend); 98 | 99 | this.init(services, options); 100 | 101 | this.type = 'backend'; 102 | } 103 | 104 | _createClass(Backend, [{ 105 | key: 'init', 106 | value: function init(services) { 107 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 108 | var coreOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 109 | 110 | this.services = services; 111 | this.options = this.options || {}; 112 | this.options = _extends({}, getDefaults(), this.options, options); 113 | this.coreOptions = coreOptions; 114 | this.queuedWrites = {}; 115 | 116 | this.debouncedWrite = utils.debounce(this.write, 250); 117 | } 118 | }, { 119 | key: 'read', 120 | value: function read(language, namespace, callback) { 121 | var loadPath = this.options.loadPath; 122 | if (typeof this.options.loadPath === 'function') { 123 | loadPath = this.options.loadPath(language, namespace); 124 | } 125 | 126 | var filename = this.services.interpolator.interpolate(loadPath, { lng: language, ns: namespace }); 127 | 128 | readFile(filename, this.options, function (err, resources) { 129 | if (err) return callback(err, false); // no retry 130 | callback(null, resources); 131 | }); 132 | } 133 | }, { 134 | key: 'create', 135 | value: function create(languages, namespace, key, fallbackValue, callback) { 136 | var _this = this; 137 | 138 | if (!callback) callback = function callback() {}; 139 | if (typeof languages === 'string') languages = [languages]; 140 | 141 | var todo = languages.length; 142 | function done() { 143 | if (! --todo) callback && callback(); 144 | } 145 | 146 | languages.forEach(function (lng) { 147 | _this.queue.call(_this, lng, namespace, key, fallbackValue, done); 148 | }); 149 | } 150 | 151 | // write queue 152 | 153 | }, { 154 | key: 'write', 155 | value: function write() { 156 | for (var lng in this.queuedWrites) { 157 | var namespaces = this.queuedWrites[lng]; 158 | if (lng !== 'locks') { 159 | for (var ns in namespaces) { 160 | this.writeFile(lng, ns); 161 | } 162 | } 163 | } 164 | } 165 | }, { 166 | key: 'writeFile', 167 | value: function writeFile(lng, namespace) { 168 | var _this2 = this; 169 | 170 | var lock = utils.getPath(this.queuedWrites, ['locks', lng, namespace]); 171 | if (lock) return; 172 | 173 | var addPath = this.options.addPath; 174 | if (typeof this.options.addPath === 'function') { 175 | addPath = this.options.addPath(lng, namespace); 176 | } 177 | 178 | var filename = this.services.interpolator.interpolate(addPath, { lng: lng, ns: namespace }); 179 | 180 | var missings = utils.getPath(this.queuedWrites, [lng, namespace]); 181 | utils.setPath(this.queuedWrites, [lng, namespace], []); 182 | 183 | if (missings.length) { 184 | // lock 185 | utils.setPath(this.queuedWrites, ['locks', lng, namespace], true); 186 | 187 | readFile(filename, this.options, function (err, resources) { 188 | if (err) resources = {}; 189 | 190 | missings.forEach(function (missing) { 191 | var path = _this2.coreOptions.keySeparator === false ? [missing.key] : missing.key.split(_this2.coreOptions.keySeparator || '.'); 192 | utils.setPath(resources, path, missing.fallbackValue); 193 | }); 194 | 195 | _fs2.default.writeFile(filename, JSON.stringify(resources, null, _this2.options.jsonIndent), function (err) { 196 | // unlock 197 | utils.setPath(_this2.queuedWrites, ['locks', lng, namespace], false); 198 | 199 | missings.forEach(function (missing) { 200 | if (missing.callback) missing.callback(); 201 | }); 202 | 203 | // rerun 204 | _this2.debouncedWrite(); 205 | }); 206 | }); 207 | } 208 | } 209 | }, { 210 | key: 'queue', 211 | value: function queue(lng, namespace, key, fallbackValue, callback) { 212 | utils.pushPath(this.queuedWrites, [lng, namespace], { key: key, fallbackValue: fallbackValue || '', callback: callback }); 213 | 214 | this.debouncedWrite(); 215 | } 216 | }]); 217 | 218 | return Backend; 219 | }(); 220 | 221 | Backend.type = 'backend'; 222 | 223 | exports.default = Backend; --------------------------------------------------------------------------------