├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── i18nextLocalStorageBackend.js
├── i18nextLocalStorageBackend.min.js
├── index.d.ts
├── index.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
└── index.js
├── test
└── typescript
│ └── basic.test-d.ts
├── tsconfig.json
└── tslint.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "@babel/env" ]
3 | }
4 |
--------------------------------------------------------------------------------
/.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 | {
2 | "extends": [
3 | "standard",
4 | "plugin:require-path-exists/recommended"
5 | ],
6 | "plugins": [
7 | "require-path-exists"
8 | ],
9 | "globals": {
10 | "describe": false,
11 | "it": false,
12 | "before": false,
13 | "after": false,
14 | "beforeEach": false,
15 | "afterEach": false
16 | },
17 | "rules": {
18 | "array-bracket-spacing": 0,
19 | "standard/no-callback-literal": 0
20 | }
21 | }
--------------------------------------------------------------------------------
/.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 | dist/**/*
20 |
21 | # JetBrains IDE's
22 | .idea
23 |
--------------------------------------------------------------------------------
/.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 | ### 4.2.0
2 |
3 | - return timestamp when reading, can be useful for chained-backend
4 |
5 | ### 4.1.1
6 |
7 | - fix: localStorage SecutiryError [46](https://github.com/i18next/i18next-localstorage-backend/pull/46)
8 |
9 | ### 4.1.0
10 |
11 | - typescript: export the backend options type
12 |
13 | ### 4.0.1
14 |
15 | - typescript: static type prop
16 |
17 | ### 4.0.0
18 |
19 | - typescript fix for i18next v22
20 |
21 | ### 3.1.3
22 |
23 | - SSR fix [31](https://github.com/i18next/i18next-localstorage-backend/pull/31)
24 |
25 | ### 3.1.2
26 |
27 | - typescript: Add defaultVersion to typings of BackendOptions [25](https://github.com/i18next/i18next-localstorage-backend/pull/25)
28 |
29 | ### 3.1.1
30 |
31 | - fix(options): make defaultVersion undefined [19](https://github.com/i18next/i18next-localstorage-backend/pull/19)
32 |
33 | ### 3.1.0
34 |
35 | - feat(version): add a defautversion option to apply a version for all languages in once [15](https://github.com/i18next/i18next-localstorage-backend/pull/15)
36 |
37 | ### 3.0.0
38 |
39 | - removes deprecated jsnext:main from package.json
40 | - Bundle all entry points with rollup [11](https://github.com/i18next/i18next-localstorage-backend/pull/11)
41 | - **note:** dist/es -> dist/esm, dist/commonjs -> dist/cjs (individual files -> one bundled file)
42 |
43 | ### 2.1.2
44 |
45 | - typescript: fix types [9](https://github.com/i18next/i18next-localstorage-backend/pull/9)
46 |
47 | ### 2.1.1
48 |
49 | - typescript: Fixing a TypeScript error complaining of missing default export [8](https://github.com/i18next/i18next-localstorage-backend/pull/8)
50 |
51 | ### 2.1.0
52 |
53 | - optionally set store to use [7](https://github.com/i18next/i18next-localstorage-backend/pull/7)
54 |
55 | ### 2.0.0
56 |
57 | - typescript: add types [5](https://github.com/i18next/i18next-localstorage-backend/pull/5)
58 |
59 | ### 1.1.4
60 |
61 | - fixes cache save call
62 |
63 | ### 1.1.1
64 |
65 | - initial version
66 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 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 localStorage 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-localstorage-backend) or [downloaded](https://github.com/i18next/i18next-localstorage-backend/blob/master/i18nextLocalStorageBackend.min.js) from this repo.
8 |
9 | - If you don't use a module loader it will be added to window.i18nextLocalStorageBackend
10 |
11 | ```
12 | # npm package
13 | $ npm install i18next-localstorage-backend
14 | ```
15 |
16 | Wiring up with the chained backend:
17 |
18 | ```js
19 | import i18next from 'i18next';
20 | import Backend from 'i18next-chained-backend';
21 | import LocalStorageBackend from 'i18next-localstorage-backend'; // primary use cache
22 | import HttpApi from 'i18next-http-backend'; // fallback http load
23 |
24 | i18next
25 | .use(Backend)
26 | .init({
27 | backend: {
28 | backends: [
29 | LocalStorageBackend, // primary backend
30 | HttpApi // fallback backend
31 | ],
32 | backendOptions: [{
33 | /* options for primary backend */
34 | }, {
35 | /* options for secondary backend */
36 | loadPath: '/locales/{{lng}}/{{ns}}.json' // http load path for my own fallback
37 | }]
38 | }
39 | });
40 | ```
41 |
42 | ## Cache Backend Options
43 |
44 |
45 | ```js
46 | {
47 | // prefix for stored languages
48 | prefix: 'i18next_res_',
49 |
50 | // expiration
51 | expirationTime: 7*24*60*60*1000,
52 |
53 | // Version applied to all languages, can be overriden using the option `versions`
54 | defaultVersion: '',
55 |
56 | // language versions
57 | versions: {},
58 |
59 | // can be either window.localStorage or window.sessionStorage. Default: window.localStorage
60 | store: typeof window !== 'undefined' ? window.localStorage : null
61 | };
62 | ```
63 |
64 | - 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
65 |
66 | - 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
67 |
68 | - Passing in a `defaultVersion` string (ex.: `version: 'v1.2'`) will act as if you applied a version to all languages using `versions` option.
69 |
70 | - The test on window makes this package available for SSR environments like NextJS
71 |
72 | ## IMPORTANT ADVICE for the usage in combination with saveMissing/updateMissing
73 |
74 | We suggest not to use a caching layer in combination with saveMissing or updateMissing, because it may happen, that the trigger for this is based on stale data.
75 |
76 |
77 | --------------
78 |
79 |
Gold Sponsors
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | ---
88 |
89 | **localization as a service - locize.com**
90 |
91 | Needing a translation management? Want to edit your translations with an InContext Editor? Use the orginal provided to you by the maintainers of i18next!
92 |
93 | 
94 |
95 | With using [locize](http://locize.com/?utm_source=react_i18next_readme&utm_medium=github) you directly support the future of i18next and react-i18next.
96 |
97 | ---
98 |
--------------------------------------------------------------------------------
/i18nextLocalStorageBackend.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 = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextLocalStorageBackend = factory());
5 | })(this, (function () { 'use strict';
6 |
7 | function _typeof(o) {
8 | "@babel/helpers - typeof";
9 |
10 | return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
11 | return typeof o;
12 | } : function (o) {
13 | return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
14 | }, _typeof(o);
15 | }
16 |
17 | function _toPrimitive(input, hint) {
18 | if (_typeof(input) !== "object" || input === null) return input;
19 | var prim = input[Symbol.toPrimitive];
20 | if (prim !== undefined) {
21 | var res = prim.call(input, hint || "default");
22 | if (_typeof(res) !== "object") return res;
23 | throw new TypeError("@@toPrimitive must return a primitive value.");
24 | }
25 | return (hint === "string" ? String : Number)(input);
26 | }
27 |
28 | function _toPropertyKey(arg) {
29 | var key = _toPrimitive(arg, "string");
30 | return _typeof(key) === "symbol" ? key : String(key);
31 | }
32 |
33 | function _defineProperty(obj, key, value) {
34 | key = _toPropertyKey(key);
35 | if (key in obj) {
36 | Object.defineProperty(obj, key, {
37 | value: value,
38 | enumerable: true,
39 | configurable: true,
40 | writable: true
41 | });
42 | } else {
43 | obj[key] = value;
44 | }
45 | return obj;
46 | }
47 |
48 | function _classCallCheck(instance, Constructor) {
49 | if (!(instance instanceof Constructor)) {
50 | throw new TypeError("Cannot call a class as a function");
51 | }
52 | }
53 |
54 | function _defineProperties(target, props) {
55 | for (var i = 0; i < props.length; i++) {
56 | var descriptor = props[i];
57 | descriptor.enumerable = descriptor.enumerable || false;
58 | descriptor.configurable = true;
59 | if ("value" in descriptor) descriptor.writable = true;
60 | Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
61 | }
62 | }
63 | function _createClass(Constructor, protoProps, staticProps) {
64 | if (protoProps) _defineProperties(Constructor.prototype, protoProps);
65 | if (staticProps) _defineProperties(Constructor, staticProps);
66 | Object.defineProperty(Constructor, "prototype", {
67 | writable: false
68 | });
69 | return Constructor;
70 | }
71 |
72 | function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
73 | function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
74 | /* eslint-disable max-classes-per-file */
75 | var Storage = /*#__PURE__*/function () {
76 | function Storage(options) {
77 | _classCallCheck(this, Storage);
78 | this.store = options.store;
79 | }
80 | _createClass(Storage, [{
81 | key: "setItem",
82 | value: function setItem(key, value) {
83 | if (this.store) {
84 | try {
85 | this.store.setItem(key, value);
86 | } catch (e) {
87 | // f.log('failed to set value for key "' + key + '" to localStorage.');
88 | }
89 | }
90 | }
91 | }, {
92 | key: "getItem",
93 | value: function getItem(key, value) {
94 | if (this.store) {
95 | try {
96 | return this.store.getItem(key, value);
97 | } catch (e) {
98 | // f.log('failed to get value for key "' + key + '" from localStorage.');
99 | }
100 | }
101 | return undefined;
102 | }
103 | }]);
104 | return Storage;
105 | }();
106 | function getDefaults() {
107 | var store = null;
108 | try {
109 | store = window.localStorage;
110 | } catch (e) {
111 | if (typeof window !== 'undefined') {
112 | console.log('Failed to load local storage.', e);
113 | }
114 | }
115 | return {
116 | prefix: 'i18next_res_',
117 | expirationTime: 7 * 24 * 60 * 60 * 1000,
118 | defaultVersion: undefined,
119 | versions: {},
120 | store: store
121 | };
122 | }
123 | var Cache = /*#__PURE__*/function () {
124 | function Cache(services) {
125 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
126 | _classCallCheck(this, Cache);
127 | this.init(services, options);
128 | this.type = 'backend';
129 | }
130 | _createClass(Cache, [{
131 | key: "init",
132 | value: function init(services) {
133 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
134 | this.services = services;
135 | this.options = _objectSpread(_objectSpread(_objectSpread({}, getDefaults()), this.options), options);
136 | this.storage = new Storage(this.options);
137 | }
138 | }, {
139 | key: "read",
140 | value: function read(language, namespace, callback) {
141 | var nowMS = Date.now();
142 | if (!this.storage.store) {
143 | return callback(null, null);
144 | }
145 | var local = this.storage.getItem("".concat(this.options.prefix).concat(language, "-").concat(namespace));
146 | if (local) {
147 | local = JSON.parse(local);
148 | var version = this.getVersion(language);
149 | if (
150 | // expiration field is mandatory, and should not be expired
151 | local.i18nStamp && local.i18nStamp + this.options.expirationTime > nowMS &&
152 | // there should be no language version set, or if it is, it should match the one in translation
153 | version === local.i18nVersion) {
154 | var i18nStamp = local.i18nStamp;
155 | delete local.i18nVersion;
156 | delete local.i18nStamp;
157 | return callback(null, local, i18nStamp);
158 | }
159 | }
160 | return callback(null, null);
161 | }
162 | }, {
163 | key: "save",
164 | value: function save(language, namespace, data) {
165 | if (this.storage.store) {
166 | data.i18nStamp = Date.now();
167 |
168 | // language version (if set)
169 | var version = this.getVersion(language);
170 | if (version) {
171 | data.i18nVersion = version;
172 | }
173 |
174 | // save
175 | this.storage.setItem("".concat(this.options.prefix).concat(language, "-").concat(namespace), JSON.stringify(data));
176 | }
177 | }
178 | }, {
179 | key: "getVersion",
180 | value: function getVersion(language) {
181 | return this.options.versions[language] || this.options.defaultVersion;
182 | }
183 | }]);
184 | return Cache;
185 | }();
186 | Cache.type = 'backend';
187 |
188 | return Cache;
189 |
190 | }));
191 |
--------------------------------------------------------------------------------
/i18nextLocalStorageBackend.min.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).i18nextLocalStorageBackend=e()}(this,(function(){"use strict";function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}function e(e){var n=function(e,n){if("object"!==t(e)||null===e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var o=r.call(e,n||"default");if("object"!==t(o))return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===n?String:Number)(e)}(e,"string");return"symbol"===t(n)?n:String(n)}function n(t,n,r){return(n=e(n))in t?Object.defineProperty(t,n,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[n]=r,t}function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,n){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:{};r(this,t),this.init(e,n),this.type="backend"}return i(t,[{key:"init",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.services=t,this.options=a(a(a({},u()),this.options),e),this.storage=new c(this.options)}},{key:"read",value:function(t,e,n){var r=Date.now();if(!this.storage.store)return n(null,null);var o=this.storage.getItem("".concat(this.options.prefix).concat(t,"-").concat(e));if(o){o=JSON.parse(o);var i=this.getVersion(t);if(o.i18nStamp&&o.i18nStamp+this.options.expirationTime>r&&i===o.i18nVersion){var s=o.i18nStamp;return delete o.i18nVersion,delete o.i18nStamp,n(null,o,s)}}return n(null,null)}},{key:"save",value:function(t,e,n){if(this.storage.store){n.i18nStamp=Date.now();var r=this.getVersion(t);r&&(n.i18nVersion=r),this.storage.setItem("".concat(this.options.prefix).concat(t,"-").concat(e),JSON.stringify(n))}}},{key:"getVersion",value:function(t){return this.options.versions[t]||this.options.defaultVersion}}]),t}();return f.type="backend",f}));
2 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import { BackendModule, ReadCallback } from "i18next";
2 |
3 | export interface LocalStorageBackendOptions {
4 | prefix?: string;
5 | expirationTime?: number;
6 | versions?: { [key: string]: string };
7 | defaultVersion?: string;
8 | store?: any;
9 | }
10 |
11 | export default class I18NextLocalStorageBackend
12 | implements BackendModule
13 | {
14 | static type: "backend";
15 | constructor(services?: any, options?: LocalStorageBackendOptions);
16 | init(services?: any, options?: LocalStorageBackendOptions): void;
17 | read(language: string, namespace: string, callback: ReadCallback): void;
18 | save(language: string, namespace: string, data: any): void;
19 | type: "backend";
20 | services: any;
21 | options: LocalStorageBackendOptions;
22 | }
23 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-var: 0 */
2 | var main = require('./dist/cjs/i18nextLocalStorageBackend.js').default;
3 |
4 | module.exports = main;
5 | module.exports.default = main;
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-localstorage-backend",
3 | "version": "4.2.0",
4 | "description": "caching layer backend for i18next using browsers localStorage",
5 | "main": "./dist/cjs/i18nextLocalStorageBackend.js",
6 | "module": "./dist/esm/i18nextLocalStorageBackend.js",
7 | "types": "./index.d.ts",
8 | "keywords": [
9 | "i18next",
10 | "i18next-backend"
11 | ],
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/i18next/i18next-localStorage-backend.git"
15 | },
16 | "homepage": "https://github.com/i18next/i18next-localStorage-backend",
17 | "bugs": "https://github.com/i18next/i18next-localStorage-backend/issues",
18 | "dependencies": {
19 | "@babel/runtime": "^7.22.15"
20 | },
21 | "devDependencies": {
22 | "@babel/core": "^7.22.19",
23 | "@babel/plugin-transform-runtime": "^7.22.15",
24 | "@babel/preset-env": "^7.22.15",
25 | "babel-eslint": "^10.0.1",
26 | "cpy-cli": "^5.0.0",
27 | "dtslint": "4.2.1",
28 | "escope": "^4.0.0",
29 | "eslint": "8.49.0",
30 | "eslint-config-standard": "17.1.0",
31 | "eslint-plugin-import": "2.28.1",
32 | "eslint-plugin-n": "16.1.0",
33 | "eslint-plugin-promise": "6.1.1",
34 | "eslint-plugin-require-path-exists": "1.1.9",
35 | "eslint-plugin-standard": "5.0.0",
36 | "i18next": "23.7.1",
37 | "rimraf": "5.0.1",
38 | "rollup": "^2.78.1",
39 | "rollup-plugin-babel": "^4.4.0",
40 | "rollup-plugin-node-resolve": "^5.2.0",
41 | "rollup-plugin-terser": "^7.0.2",
42 | "tsd": "0.29.0",
43 | "tslint": "5.20.1",
44 | "typescript": "5.2.2"
45 | },
46 | "scripts": {
47 | "lint": "eslint ./src/*",
48 | "pretest": "npm run test:typescript",
49 | "test": "npm run lint",
50 | "test:typescript": "tslint --project tsconfig.json && tsd",
51 | "build": "rimraf dist && rollup -c && cpy \"./dist/umd/*.js\" ./",
52 | "preversion": "npm run build && git push",
53 | "postversion": "git push && git push --tags"
54 | },
55 | "tsd": {
56 | "directory": "test/typescript"
57 | },
58 | "author": "Jan Mühlemann (https://github.com/jamuhl)",
59 | "license": "MIT"
60 | }
61 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import nodeResolve from 'rollup-plugin-node-resolve';
3 | import { terser } from 'rollup-plugin-terser';
4 | import pkg from './package.json';
5 |
6 | const getBabelOptions = ({ useESModules }) => ({
7 | exclude: /node_modules/,
8 | runtimeHelpers: true,
9 | plugins: [
10 | ['@babel/transform-runtime', { useESModules }]
11 | ]
12 | });
13 |
14 | const input = './src/index.js';
15 | const name = 'i18nextLocalStorageBackend'
16 | // check relative and absolute paths for windows and unix
17 | const external = id => !id.startsWith('.') && !id.startsWith('/') && !id.includes(':');
18 |
19 | export default [
20 | {
21 | input,
22 | output: { format: 'cjs', file: pkg.main },
23 | external,
24 | plugins: [
25 | babel(getBabelOptions({ useESModules: false }))
26 | ]
27 | },
28 |
29 | {
30 | input,
31 | output: { format: 'esm', file: pkg.module },
32 | external,
33 | plugins: [
34 | babel(getBabelOptions({ useESModules: true }))
35 | ]
36 | },
37 |
38 | {
39 | input,
40 | output: { format: 'umd', name, file: `dist/umd/${name}.js` },
41 | plugins: [
42 | babel(getBabelOptions({ useESModules: true })),
43 | nodeResolve()
44 | ],
45 | },
46 | {
47 | input,
48 | output: { format: 'umd', name, file: `dist/umd/${name}.min.js` },
49 | plugins: [
50 | babel(getBabelOptions({ useESModules: true })),
51 | nodeResolve(),
52 | terser()
53 | ],
54 | }
55 | ]
56 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | class Storage {
3 | constructor (options) {
4 | this.store = options.store
5 | }
6 |
7 | setItem (key, value) {
8 | if (this.store) {
9 | try {
10 | this.store.setItem(key, value)
11 | } catch (e) {
12 | // f.log('failed to set value for key "' + key + '" to localStorage.');
13 | }
14 | }
15 | }
16 |
17 | getItem (key, value) {
18 | if (this.store) {
19 | try {
20 | return this.store.getItem(key, value)
21 | } catch (e) {
22 | // f.log('failed to get value for key "' + key + '" from localStorage.');
23 | }
24 | }
25 | return undefined
26 | }
27 | }
28 |
29 | function getDefaults () {
30 | let store = null
31 | try {
32 | store = window.localStorage
33 | } catch (e) {
34 | if (typeof window !== 'undefined') {
35 | console.log('Failed to load local storage.', e)
36 | }
37 | }
38 | return {
39 | prefix: 'i18next_res_',
40 | expirationTime: 7 * 24 * 60 * 60 * 1000,
41 | defaultVersion: undefined,
42 | versions: {},
43 | store
44 | }
45 | }
46 |
47 | class Cache {
48 | constructor (services, options = {}) {
49 | this.init(services, options)
50 |
51 | this.type = 'backend'
52 | }
53 |
54 | init (services, options = {}) {
55 | this.services = services
56 | this.options = { ...getDefaults(), ...this.options, ...options }
57 | this.storage = new Storage(this.options)
58 | }
59 |
60 | read (language, namespace, callback) {
61 | const nowMS = Date.now()
62 |
63 | if (!this.storage.store) {
64 | return callback(null, null)
65 | }
66 |
67 | let local = this.storage.getItem(`${this.options.prefix}${language}-${namespace}`)
68 |
69 | if (local) {
70 | local = JSON.parse(local)
71 | const version = this.getVersion(language)
72 | if (
73 | // expiration field is mandatory, and should not be expired
74 | local.i18nStamp && local.i18nStamp + this.options.expirationTime > nowMS &&
75 |
76 | // there should be no language version set, or if it is, it should match the one in translation
77 | version === local.i18nVersion
78 | ) {
79 | const i18nStamp = local.i18nStamp
80 | delete local.i18nVersion
81 | delete local.i18nStamp
82 | return callback(null, local, i18nStamp)
83 | }
84 | }
85 |
86 | return callback(null, null)
87 | }
88 |
89 | save (language, namespace, data) {
90 | if (this.storage.store) {
91 | data.i18nStamp = Date.now()
92 |
93 | // language version (if set)
94 | const version = this.getVersion(language)
95 | if (version) {
96 | data.i18nVersion = version
97 | }
98 |
99 | // save
100 | this.storage.setItem(`${this.options.prefix}${language}-${namespace}`, JSON.stringify(data))
101 | }
102 | }
103 |
104 | getVersion (language) {
105 | return this.options.versions[language] || this.options.defaultVersion
106 | }
107 | }
108 |
109 | Cache.type = 'backend'
110 |
111 | export default Cache
112 |
--------------------------------------------------------------------------------
/test/typescript/basic.test-d.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 | import Backend, { LocalStorageBackendOptions } from 'i18next-localstorage-backend';
3 |
4 | i18next.use(Backend).init({
5 | backend: {
6 | prefix: 'asdf'
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5",
5 | "lib": ["es6", "dom"],
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "forceConsistentCasingInFileNames": true,
9 | "strict": true,
10 | "noEmit": true,
11 | "baseUrl": ".",
12 | "paths": { "i18next-localstorage-backend": ["./index.d.ts"] },
13 |
14 | "esModuleInterop": true,
15 | "allowSyntheticDefaultImports": true
16 | },
17 | "include": ["./index.d.ts", "./test/**/*"],
18 | "exclude": []
19 | }
20 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": "dtslint/dtslint.json",
4 | "rules": {
5 | "semicolon": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------