diff --git a/packages/react-intl-universal/examples/browser-example/pages/index.tsx b/packages/react-intl-universal/examples/browser-example/pages/index.tsx index 572e1e2..957131f 100644 --- a/packages/react-intl-universal/examples/browser-example/pages/index.tsx +++ b/packages/react-intl-universal/examples/browser-example/pages/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import intl from 'react-intl-universal'; +import intl, { createIntlCache } from 'react-intl-universal'; import useForceUpdate from 'use-force-update'; import enUS from 'locales/en-US.json'; import zhCN from 'locales/zh-CN.json'; @@ -73,11 +73,18 @@ const ReactIntlUniversalExample: React.FC = (props) => { setInitDone(true); } + // This is optional but highly recommended + // since it prevents memory leak + const cache = createIntlCache() + const setCurrentLocale = (currentLocale: string) => { - intl.init({ - currentLocale, - locales: LOCALE_DATA, - }); + intl.init( + { + currentLocale, + locales: LOCALE_DATA, + }, + cache + ); }; const onLocaleChange = (e: React.ChangeEvent) => { diff --git a/packages/react-intl-universal/examples/node-js-example/src/App.js b/packages/react-intl-universal/examples/node-js-example/src/App.js index 71899ed..75577e0 100644 --- a/packages/react-intl-universal/examples/node-js-example/src/App.js +++ b/packages/react-intl-universal/examples/node-js-example/src/App.js @@ -1,4 +1,4 @@ -import intl from 'react-intl-universal'; +import intl, { createIntlCache } from 'react-intl-universal'; import React, { Component } from "react"; import BasicComponent from "./Basic"; import PluralComponent from "./Plural"; @@ -44,12 +44,20 @@ class App extends Component { constructor(props) { super(props); const currentLocale = SUPPORTED_LOCALES[0].value; // Determine user's locale here - intl.init({ - currentLocale, - locales: { - [currentLocale]: require(`./locales/${currentLocale}`) - } - }); + + // This is optional but highly recommended + // since it prevents memory leak + const cache = createIntlCache() + + intl.init( + { + currentLocale, + locales: { + [currentLocale]: require(`./locales/${currentLocale}`) + } + }, + cache + ); } render() { diff --git a/packages/react-intl-universal/src/ReactIntlUniversal.js b/packages/react-intl-universal/src/ReactIntlUniversal.js index 0c6444a..19faa71 100644 --- a/packages/react-intl-universal/src/ReactIntlUniversal.js +++ b/packages/react-intl-universal/src/ReactIntlUniversal.js @@ -1,12 +1,12 @@ import "intl"; import React from "react"; -import IntlMessageFormat from "intl-messageformat"; import escapeHtml from "escape-html"; import cookie from "cookie"; import queryParser from "querystring"; import invariant from "invariant"; import * as constants from "./constants"; import merge from "lodash.merge"; +import { generateCacheKey, createFormattedMessage } from "./utils"; String.prototype.defaultMessage = String.prototype.d = function (msg) { return this || msg || ""; @@ -84,8 +84,14 @@ class ReactIntlUniversal { } try { - const msgFormatter = new IntlMessageFormat(msg, currentLocale, formats); - return msgFormatter.format(variables); + if (this.cache) { + const cacheKey = generateCacheKey(key, variables, currentLocale); + if (!this.cache[cacheKey]) { + this.cache[cacheKey] = createFormattedMessage(msg, currentLocale, formats, variables); + } + return this.cache[cacheKey]; + } + return createFormattedMessage(msg, currentLocale, formats, variables); } catch (err) { this.options.warningHandler( `react-intl-universal format message failed for key='${key}'.`, @@ -174,9 +180,10 @@ class ReactIntlUniversal { * @param {Object} options * @param {string} options.currentLocale Current locale such as 'en-US' * @param {string} options.locales App locale data like {"en-US":{"key1":"value1"},"zh-CN":{"key1":"值1"}} + * @param {Object} [cache] explicit cache to prevent leaking memory, Initialize using createIntlCache * @returns {Promise} */ - init(options = {}) { + init(options = {}, cache) { invariant(options.currentLocale, "options.currentLocale is required"); invariant(options.locales, "options.locales is required"); @@ -188,6 +195,8 @@ class ReactIntlUniversal { constants.defaultFormats ); + this.cache = cache || null; + return new Promise((resolve, reject) => { // init() will not load external common locale data anymore. // But, it still return a Promise for abckward compatibility. diff --git a/packages/react-intl-universal/src/index.js b/packages/react-intl-universal/src/index.js index 90b6e8a..9606227 100644 --- a/packages/react-intl-universal/src/index.js +++ b/packages/react-intl-universal/src/index.js @@ -1,4 +1,5 @@ import ReactIntlUniversal from './ReactIntlUniversal'; +import { createIntlCache } from './utils'; const defaultInstance = new ReactIntlUniversal(); // resolved by CommonJS module loader @@ -33,5 +34,6 @@ export { getLocaleFromURL, getDescendantProp, getLocaleFromBrowser, - defaultInstance as default + defaultInstance as default, + createIntlCache }; diff --git a/packages/react-intl-universal/src/utils.js b/packages/react-intl-universal/src/utils.js new file mode 100644 index 0000000..b2f634f --- /dev/null +++ b/packages/react-intl-universal/src/utils.js @@ -0,0 +1,39 @@ +import IntlMessageFormat from "intl-messageformat"; + +/** + * Create an object that acts as a cache for internationalized messages. + * This function uses Object.create(null) to ensure that the returned object + * does not inherit any properties or methods from the global Object prototype. + * + * @returns {Object} An empty object that can be used as a cache. + */ +export function createIntlCache() { + return Object.create(null) +} + +/** + * Get the formatted message by key + * @param {string} key The string representing key in locale data file + * @param {Object} variables Variables in message + * @param {string} currentLocale Current locale such as 'en-US' + * @returns {string} message + */ +export function generateCacheKey(key, variables, currentLocale) { + return key + JSON.stringify(variables) + currentLocale; +} + +/** + * Create and return a formatted message based on provided parameters. + * This function takes in a message, a locale, optional formats, and variables, + * and returns the message formatted accordingly using IntlMessageFormat. + * + * @param {string} msg The message string to be formatted + * @param {string} currentLocale The locale in which the message should be formatted + * @param {Object} formats formats to be applied to the message + * @param {Object} variables Variables to replace placeholders in the message + * @returns {string} The formatted message string + */ +export function createFormattedMessage(msg, currentLocale, formats, variables) { + const msgFormatter = new IntlMessageFormat(msg, currentLocale, formats); + return msgFormatter.format(variables); +} \ No newline at end of file diff --git a/packages/react-intl-universal/test/index.test.js b/packages/react-intl-universal/test/index.test.js index 0a878be..93f578b 100644 --- a/packages/react-intl-universal/test/index.test.js +++ b/packages/react-intl-universal/test/index.test.js @@ -1,6 +1,6 @@ import React from "react"; import cookie from "cookie"; -import intl from "../src/index"; +import intl, { createIntlCache } from "../src/index"; import zhCN from "./locales/zh-CN"; import enUS from "./locales/en-US"; import enUSMore from "./locales/en-US-more"; @@ -368,3 +368,16 @@ test("Resolve directly if the environment is not browser", async () => { }); expect(result).toBe(undefined); }); + +test("cache", () => { + const key = "HELLO"; + const variables = { name: 'World' }; + const currentLocale = "en-US" + const cacheKey = key + JSON.stringify(variables) + currentLocale; + const cache = createIntlCache() + + intl.init({ locales, currentLocale }, cache); + + expect(intl.get(key, variables)).toBe("Hello, World"); + expect(intl.cache[cacheKey]).toBe("Hello, World"); +}); \ No newline at end of file diff --git a/packages/react-intl-universal/typings/index.d.ts b/packages/react-intl-universal/typings/index.d.ts index a9ac081..317f23a 100644 --- a/packages/react-intl-universal/typings/index.d.ts +++ b/packages/react-intl-universal/typings/index.d.ts @@ -73,9 +73,10 @@ declare module "react-intl-universal" { * @param {Object} options.warningHandler Ability to accumulate missing messages using third party services like Sentry * @param {string} options.fallbackLocale Fallback locale such as 'zh-CN' to use if a key is not found in the current locale * @param {boolean} options.escapeHtml To escape html. Default value is true. + * @param {Object} [cache] explicit cache to prevent leaking memory, Initialize using createIntlCache * @returns {Promise} */ - export function init(options: ReactIntlUniversalOptions): Promise; + export function init(options: ReactIntlUniversalOptions, cache: { [key: string]: any }): Promise; /** * Load more locales after init