├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── i18nextAsyncStorageBackend.js ├── i18nextAsyncStorageBackend.min.js ├── index.js ├── package.json ├── rollup.config.js └── src ├── index.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ "es2015", "stage-0" ] 5 | }, 6 | "rollup": { 7 | "presets": [ ["es2015", { "modules": false }], "stage-0" ] 8 | }, 9 | "jsnext": { 10 | "presets": [ ["es2015", { "modules": false }], "stage-0" ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/* 2 | **/node_modules/* 3 | **/*.min.* 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | parser: babel-eslint 2 | extends: airbnb 3 | 4 | rules: 5 | max-len: [0, 100] 6 | no-constant-condition: 0 7 | arrow-body-style: [1, "as-needed"] 8 | comma-dangle: [2, "never"] 9 | padded-blocks: [0, "never"] 10 | no-unused-vars: [2, {vars: all, args: none}] 11 | react/prop-types: 12 | - 0 13 | - ignore: #coming from hoc 14 | - location 15 | - fields 16 | - handleSubmit 17 | 18 | globals: 19 | expect: false 20 | -------------------------------------------------------------------------------- /.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 | package-lock.json 10 | 11 | # Ignore various temporary files 12 | *~ 13 | *.swp 14 | 15 | 16 | # Ignore various Node.js related directories and files 17 | node_modules 18 | node_modules/**/* 19 | coverage/**/* 20 | dist/**/* 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | src/ 3 | coverage/ 4 | .babelrc 5 | .editorconfig 6 | .eslintignore 7 | .eslintrc 8 | .gitignore 9 | bower.json 10 | gulpfile.js 11 | karma.conf.js 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.1.4 2 | - fixes cache save call 3 | 4 | ### 1.1.1 5 | 6 | - initial version 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is a i18next cache layer to be used in the browser. It will load and cache resources from AsyncStorage and can be used in combination with the [chained backend](https://github.com/i18next/i18next-chained-backend). 4 | 5 | # Getting started 6 | 7 | Source can be loaded via [npm](https://www.npmjs.com/package/i18next-async-storage-cache) or [downloaded](https://github.com/timbrandin/i18next-async-storage-cache/blob/master/i18nextAsyncStorageCache.min.js) from this repo. 8 | 9 | ``` 10 | # npm package 11 | $ npm install i18next-async-storage-backend 12 | ``` 13 | 14 | Wiring up with the chained backend: 15 | 16 | ```js 17 | import i18next from 'i18next'; 18 | import Backend from 'i18next-chained-backend'; 19 | import AsyncStorageBackend from 'i18next-async-storage-backend'; // primary use cache 20 | import XHR from 'i18next-xhr-backend'; // fallback xhr load 21 | 22 | i18next 23 | .use(Backend) 24 | .init({ 25 | backend: { 26 | backends: [ 27 | AsyncStorageBackend, // primary 28 | XHR // fallback 29 | ], 30 | backendOptions: [{ 31 | /* below options */ 32 | }, { 33 | loadPath: '/locales/{{lng}}/{{ns}}.json' // xhr load path for my own fallback 34 | }] 35 | } 36 | }); 37 | ``` 38 | 39 | ## Cache Backend Options 40 | 41 | 42 | ```js 43 | { 44 | // prefix for stored languages 45 | prefix: 'i18next_res_', 46 | 47 | // expiration 48 | expirationTime: 7*24*60*60*1000, 49 | 50 | // language versions 51 | versions: {} 52 | }; 53 | ``` 54 | 55 | - Contrary to cookies behavior, the cache will respect updates to `expirationTime`. If you set 7 days and later update to 10 days, the cache will persist for 10 days 56 | 57 | - Passing in a `versions` object (ex.: `versions: { en: 'v1.2', fr: 'v1.1' }`) will give you control over the cache based on translations version. This setting works along `expirationTime`, so a cached translation will still expire even though the version did not change. You can still set `expirationTime` far into the future to avoid this 58 | 59 | Wiring up a service backend with the chained backend: 60 | 61 | ```js 62 | import i18next from 'i18next'; 63 | import Backend from 'i18next-chained-backend'; 64 | import AsyncStorageBackend from 'i18next-async-storage-backend'; // primary use cache 65 | import ServiceBackend from 'i18next-service-backend'; // fallback service backend 66 | 67 | const TRANSLATION_BACKEND = 'https://api.spacetranslate.com'; 68 | const TRANSLATION_BACKEND_PROJECTID = '[projectId]'; // i.e. [projectId].spacetranslate.com 69 | 70 | i18next 71 | .use(Backend) 72 | .init({ 73 | backend: { 74 | backends: [ 75 | AsyncStorageBackend, // primary 76 | ServiceBackend // fallback 77 | ], 78 | backendOptions: [{ 79 | // prefix for stored languages 80 | prefix: 'i18next_res_', 81 | 82 | // expiration 83 | expirationTime: 7*24*60*60*1000, 84 | 85 | // language versions 86 | versions: {} 87 | }, { 88 | service: TRANSLATION_BACKEND, 89 | projectId: TRANSLATION_BACKEND_PROJECTID, 90 | referenceLng: 'en', 91 | version: 'latest' 92 | }] 93 | } 94 | }); 95 | ``` 96 | -------------------------------------------------------------------------------- /i18nextAsyncStorageBackend.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.i18nextAsyncStorageBackend = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var arr = []; 8 | var each = arr.forEach; 9 | var slice = arr.slice; 10 | 11 | function defaults(obj) { 12 | each.call(slice.call(arguments, 1), function (source) { 13 | if (source) { 14 | for (var prop in source) { 15 | if (obj[prop] === undefined) obj[prop] = source[prop]; 16 | } 17 | } 18 | }); 19 | return obj; 20 | } 21 | 22 | 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; }; }(); 23 | 24 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 25 | 26 | // get from whatever version of react native that is being used. 27 | var AsyncStorage = (require('react-native') || {}).AsyncStorage; 28 | 29 | var storage = { 30 | setItem: function setItem(key, value) { 31 | if (AsyncStorage) { 32 | return AsyncStorage.setItem(key, value); 33 | } 34 | }, 35 | getItem: function getItem(key, value) { 36 | if (AsyncStorage) { 37 | return AsyncStorage.getItem(key, value); 38 | } 39 | return undefined; 40 | } 41 | }; 42 | 43 | function getDefaults() { 44 | return { 45 | prefix: 'i18next_res_', 46 | expirationTime: 7 * 24 * 60 * 60 * 1000, 47 | versions: {} 48 | }; 49 | } 50 | 51 | var Cache = function () { 52 | function Cache(services) { 53 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 54 | 55 | _classCallCheck(this, Cache); 56 | 57 | this.init(services, options); 58 | 59 | this.type = 'backend'; 60 | } 61 | 62 | _createClass(Cache, [{ 63 | key: 'init', 64 | value: function init(services) { 65 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 66 | 67 | this.services = services; 68 | this.options = defaults(options, this.options || {}, getDefaults()); 69 | } 70 | }, { 71 | key: 'read', 72 | value: function read(language, namespace, callback) { 73 | var _this = this; 74 | 75 | var store = {}; 76 | var nowMS = new Date().getTime(); 77 | 78 | if (!AsyncStorage) { 79 | return callback(null, null); 80 | } 81 | 82 | storage.getItem('' + this.options.prefix + language + '-' + namespace).then(function (local) { 83 | if (local) { 84 | local = JSON.parse(local); 85 | if ( 86 | // expiration field is mandatory, and should not be expired 87 | local.i18nStamp && local.i18nStamp + _this.options.expirationTime > nowMS && 88 | 89 | // there should be no language version set, or if it is, it should match the one in translation 90 | _this.options.versions[language] === local.i18nVersion) { 91 | delete local.i18nVersion; 92 | delete local.i18nStamp; 93 | return callback(null, local); 94 | } 95 | } 96 | 97 | callback(null, null); 98 | }).catch(function (err) { 99 | console.warn(err); 100 | callback(null, null); 101 | }); 102 | } 103 | }, { 104 | key: 'save', 105 | value: function save(language, namespace, data) { 106 | if (AsyncStorage) { 107 | data.i18nStamp = new Date().getTime(); 108 | 109 | // language version (if set) 110 | if (this.options.versions[language]) { 111 | data.i18nVersion = this.options.versions[language]; 112 | } 113 | 114 | // save 115 | storage.setItem('' + this.options.prefix + language + '-' + namespace, JSON.stringify(data)); 116 | } 117 | } 118 | }]); 119 | 120 | return Cache; 121 | }(); 122 | 123 | Cache.type = 'backend'; 124 | 125 | return Cache; 126 | 127 | }))); 128 | -------------------------------------------------------------------------------- /i18nextAsyncStorageBackend.min.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):e.i18nextAsyncStorageBackend=n()}(this,function(){"use strict";function e(e){return o.call(r.call(arguments,1),function(n){if(n)for(var t in n)void 0===e[t]&&(e[t]=n[t])}),e}function n(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}function t(){return{prefix:"i18next_res_",expirationTime:6048e5,versions:{}}}var i=[],o=i.forEach,r=i.slice,s=function(){function e(e,n){for(var t=0;t1&&void 0!==arguments[1]?arguments[1]:{};n(this,i),this.init(e,t),this.type="backend"}return s(i,[{key:"init",value:function(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.services=n,this.options=e(i,this.options||{},t())}},{key:"read",value:function(e,n,t){var i=this,o=(new Date).getTime();if(!u)return t(null,null);a.getItem(""+this.options.prefix+e+"-"+n).then(function(n){if(n&&(n=JSON.parse(n),n.i18nStamp&&n.i18nStamp+i.options.expirationTime>o&&i.options.versions[e]===n.i18nVersion))return delete n.i18nVersion,delete n.i18nStamp,t(null,n);t(null,null)}).catch(function(e){console.warn(e),t(null,null)})}},{key:"save",value:function(e,n,t){u&&(t.i18nStamp=(new Date).getTime(),this.options.versions[e]&&(t.i18nVersion=this.options.versions[e]),a.setItem(""+this.options.prefix+e+"-"+n,JSON.stringify(t)))}}]),i}();return f.type="backend",f}); 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/commonjs/index.js').default; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "i18next-async-storage-backend", 3 | "version": "1.0.2", 4 | "description": "caching layer backend for i18next using react native async storage", 5 | "main": "./index.js", 6 | "jsnext:main": "dist/es/index.js", 7 | "keywords": [ 8 | "i18next", 9 | "i18next-backend" 10 | ], 11 | "homepage": "https://github.com/timbrandin/i18next-async-storage-backend", 12 | "bugs": "https://github.com/timbrandin/i18next-async-storage-backend/issues", 13 | "dependencies": {}, 14 | "devDependencies": { 15 | "babel-cli": "6.23.0", 16 | "babel-core": "6.23.1", 17 | "babel-eslint": "7.1.1", 18 | "babel-preset-es2015-native-modules": "^6.9.4", 19 | "babel-preset-es2015": "6.22.0", 20 | "babel-preset-es2015-rollup": "3.0.0", 21 | "babel-preset-stage-0": "^6.22.0", 22 | "eslint": "3.16.1", 23 | "eslint-config-airbnb": "14.1.0", 24 | "mkdirp": "0.5.1", 25 | "rimraf": "2.6.1", 26 | "rollup": "0.41.4", 27 | "rollup-plugin-babel": "2.7.1", 28 | "rollup-plugin-node-resolve": "2.0.0", 29 | "rollup-plugin-uglify": "1.0.1", 30 | "yargs": "6.6.0" 31 | }, 32 | "peerDependencies": { 33 | "@react-native-community/async-storage": "*" 34 | }, 35 | "scripts": { 36 | "clean": "rimraf dist && mkdirp dist", 37 | "copy": "cp ./dist/umd/i18nextAsyncStorageBackend.min.js ./i18nextAsyncStorageBackend.min.js && cp ./dist/umd/i18nextAsyncStorageBackend.js ./i18nextAsyncStorageBackend.js", 38 | "build:es": "BABEL_ENV=jsnext babel src --out-dir dist/es", 39 | "build:cjs": "babel src --out-dir dist/commonjs", 40 | "build:umd": "rollup -c rollup.config.js --format umd && rollup -c rollup.config.js --format umd --uglify", 41 | "build:amd": "rollup -c rollup.config.js --format amd && rollup -c rollup.config.js --format umd --uglify", 42 | "build:iife": "rollup -c rollup.config.js --format iife && rollup -c rollup.config.js --format iife --uglify", 43 | "build": "npm run clean && npm run build:cjs && npm run build:es && npm run build:umd && npm run copy", 44 | "preversion": "npm run build && git push", 45 | "postversion": "git push && git push --tags" 46 | }, 47 | "author": [ 48 | "Jan Mühlemann (https://github.com/jamuhl)", 49 | "Tim Brandin (https://github.com/timbrandin)" 50 | ], 51 | "license": "MIT" 52 | } 53 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import uglify from 'rollup-plugin-uglify'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | import { argv } from 'yargs'; 5 | 6 | const format = argv.format || argv.f || 'iife'; 7 | const compress = argv.uglify; 8 | 9 | const babelOptions = { 10 | exclude: 'node_modules/**', 11 | presets: [['es2015', { modules: false }], 'stage-0'], 12 | babelrc: false 13 | }; 14 | 15 | const dest = { 16 | amd: `dist/amd/i18nextAsyncStorageBackend${compress ? '.min' : ''}.js`, 17 | umd: `dist/umd/i18nextAsyncStorageBackend${compress ? '.min' : ''}.js`, 18 | iife: `dist/iife/i18nextAsyncStorageBackend${compress ? '.min' : ''}.js` 19 | }[format]; 20 | 21 | export default { 22 | entry: 'src/index.js', 23 | format, 24 | plugins: [ 25 | babel(babelOptions), 26 | nodeResolve({ jsnext: true }) 27 | ].concat(compress ? uglify() : []), 28 | moduleName: 'i18nextAsyncStorageBackend', 29 | // moduleId: 'i18nextAsyncStorageCache', 30 | dest 31 | }; 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as utils from './utils'; 2 | 3 | // get from whatever version of react native that is being used. 4 | const AsyncStorage = require('@react-native-community/async-storage') || {}; 5 | 6 | const storage = { 7 | setItem(key, value) { 8 | if (AsyncStorage) { 9 | return AsyncStorage.setItem(key, value); 10 | } 11 | }, 12 | getItem(key, value) { 13 | if (AsyncStorage) { 14 | return AsyncStorage.getItem(key, value); 15 | } 16 | return undefined; 17 | } 18 | }; 19 | 20 | function getDefaults() { 21 | return { 22 | prefix: 'i18next_res_', 23 | expirationTime: 7 * 24 * 60 * 60 * 1000, 24 | versions: {} 25 | }; 26 | } 27 | 28 | class Cache { 29 | constructor(services, options = {}) { 30 | this.init(services, options); 31 | 32 | this.type = 'backend'; 33 | } 34 | 35 | init(services, options = {}) { 36 | this.services = services; 37 | this.options = utils.defaults(options, this.options || {}, getDefaults()); 38 | } 39 | 40 | read(language, namespace, callback) { 41 | const store = {}; 42 | const nowMS = new Date().getTime(); 43 | 44 | if (!AsyncStorage) { 45 | return callback(null, null); 46 | } 47 | 48 | storage.getItem(`${this.options.prefix}${language}-${namespace}`) 49 | .then(local => { 50 | if (local) { 51 | local = JSON.parse(local); 52 | if ( 53 | // expiration field is mandatory, and should not be expired 54 | local.i18nStamp && local.i18nStamp + this.options.expirationTime > nowMS && 55 | 56 | // there should be no language version set, or if it is, it should match the one in translation 57 | this.options.versions[language] === local.i18nVersion 58 | ) { 59 | delete local.i18nVersion; 60 | delete local.i18nStamp; 61 | return callback(null, local); 62 | } 63 | } 64 | 65 | callback(null, null); 66 | }) 67 | .catch(err => { 68 | console.warn(err); 69 | callback(null, null); 70 | }); 71 | } 72 | 73 | save(language, namespace, data) { 74 | if (AsyncStorage) { 75 | data.i18nStamp = new Date().getTime(); 76 | 77 | // language version (if set) 78 | if (this.options.versions[language]) { 79 | data.i18nVersion = this.options.versions[language]; 80 | } 81 | 82 | // save 83 | storage.setItem(`${this.options.prefix}${language}-${namespace}`, JSON.stringify(data)); 84 | } 85 | } 86 | } 87 | 88 | Cache.type = 'backend'; 89 | 90 | export default Cache; 91 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | let arr = []; 2 | let each = arr.forEach; 3 | let slice = arr.slice; 4 | 5 | export function defaults(obj) { 6 | each.call(slice.call(arguments, 1), function(source) { 7 | if (source) { 8 | for (var prop in source) { 9 | if (obj[prop] === undefined) obj[prop] = source[prop]; 10 | } 11 | } 12 | }); 13 | return obj; 14 | } 15 | 16 | export function extend(obj) { 17 | each.call(slice.call(arguments, 1), function(source) { 18 | if (source) { 19 | for (var prop in source) { 20 | obj[prop] = source[prop]; 21 | } 22 | } 23 | }); 24 | return obj; 25 | } 26 | 27 | export function debounce(func, wait, immediate) { 28 | var timeout; 29 | return function() { 30 | var context = this, args = arguments; 31 | var later = function() { 32 | timeout = null; 33 | if (!immediate) func.apply(context, args); 34 | }; 35 | var callNow = immediate && !timeout; 36 | clearTimeout(timeout); 37 | timeout = setTimeout(later, wait); 38 | if (callNow) func.apply(context, args); 39 | }; 40 | }; 41 | --------------------------------------------------------------------------------