<?xml version="1.0" encoding="utf-8"?>
    <feed xmlns="http://www.w3.org/2005/Atom">
     <title>BigBinary Blog</title>
     <link href="https://www.bigbinary.com/feed.xml" rel="self"/>
     <link href="https://www.bigbinary.com/"/>
     <updated>2026-05-19T04:01:16+00:00</updated>
     <id>https://www.bigbinary.com/</id>
     <entry>
       <title><![CDATA[Streamlining translation resource loading in React apps with babel-plugin-preval]]></title>
       <author><name>Mohit Harshan</name></author>
      <link href="https://www.bigbinary.com/blog/simplifying-loading-translation-resources-using-babel-plugin-preval"/>
      <updated>2024-02-27T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/simplifying-loading-translation-resources-using-babel-plugin-preval</id>
      <content type="html"><![CDATA[<p>At Neeto, our product development involves reusing common components, utilities,and initializers across various projects. To maintain a cohesive andstandardized codebase, we've created specialized packages, or &quot;<code>nanos</code>&quot; such as<code>neeto-commons-frontend</code>, <code>neeto-fields-nano</code>, and <code>neeto-team-members-nano</code>.</p><p><code>neeto-commons-frontend</code> houses utility functions, components, hooks,configuration settings, etc. <code>neeto-fields-nano</code> manages dynamic fieldcomponents, while <code>neeto-team-members-nano</code> handles team member managementfunctionalities.</p><p>These <code>nanos</code>, along with others, reduce redundancy and promote consistencyacross our products.</p><h2>Translation Challenges</h2><p>Many of our packages export components with text that requires internalization,maintaining their own translation files. We encountered an issue with the<code>withT</code> higher-order component (HOC) using <code>react-i18next</code> inside<code>neeto-commons-frontend</code>. Upon investigation, we found discrepancies in howpackages handled dependencies.</p><p><code>withT</code> is an HOC which provides the <code>t</code> function from <code>react-i18next</code> to thewrapped component as a prop.</p><pre><code class="language-js">import { withTranslation } from &quot;react-i18next&quot;;const withT = (Component, options, namespace = undefined) =&gt;  withTranslation(namespace, options)(Component);export default withT;</code></pre><pre><code class="language-jsx">// Example usage of withT:const ComponentWithTranslation = withT(({ t }) =&gt; &lt;div&gt;{t(&quot;some.key&quot;)}&lt;/div&gt;);</code></pre><p>Let us first understand the difference between <code>dependencies</code> and<code>peerDependencies</code>. <code>dependencies</code> are external packages a library relies on,automatically installed with the library. <code>peerDependencies</code> suggest that usersshould explicitly install these dependencies in their application if they wantto use this library. If not installed, we will get warnings during installationof this library to prompt us to install the peer dependencies.</p><p><code>react-i18next</code> and <code>i18next</code> were listed as <code>peerDependencies</code> in<code>neeto-commons-frontend</code>'s <code>package.json</code>. So, it will be using the instances ofthese libraries from the host application.</p><p>Examining <code>neeto-fields-nano</code>, we found that it listed <code>react-i18next</code> and<code>i18next</code> as <code>dependencies</code> rather than <code>peerDependencies</code>. This meant it hadits own instances of these libraries, leading to initialization discrepancies.</p><p>Contrastingly, <code>neeto-team-members-frontend</code> listed <code>react-i18next</code> and<code>i18next</code> as <code>peerDependencies</code>, relying on the host application'sinitialization of these libraries.</p><p>The initialization logic, which is common among all the products, is placedinside <code>neeto-commons-frontend</code>. To ensure translations from all packages,including <code>neeto-commons-frontend</code> are merged with that of the host application,we crafted a custom <code>initializeI18n</code> function:</p><pre><code class="language-js">import DOMPurify from &quot;dompurify&quot;;import i18n from &quot;i18next&quot;;import { curry, mergeAll, mergeDeepLeft } from &quot;ramda&quot;;import { initReactI18next } from &quot;react-i18next&quot;;import commonsEn from &quot;../translations/en.json&quot;;const packageNames = [  &quot;neeto-molecules&quot;,  &quot;neeto-integrations-frontend&quot;,  &quot;neeto-team-members-frontend&quot;,  &quot;neeto-tags-frontend&quot;,];const getPackageTranslations = (language, packageNames) =&gt; {  const loadTranslations = curry((language, packageName) =&gt; {    try {      return require(`../${packageName}/src/translations/${language}.json`);    } catch {      return {};    }  });  const allTranslations = packageNames.map(loadTranslations(language));  return mergeAll(allTranslations);};const packageEnTranslations = getPackageTranslations(&quot;en&quot;, packageNames);const en = mergeDeepLeft(commonsEn, packageEnTranslations);const initializeI18n = resources =&gt; {  i18n.use(initReactI18next).init({    resources: mergeDeepLeft(resources, { en: { translation: en } }),    lng: &quot;en&quot;,    fallbackLng: &quot;en&quot;,    interpolation: { escapeValue: false, skipOnVariables: false },  });};export default initializeI18n;</code></pre><p>Here we are looping through all the packages mentioned in <code>packageNames</code> andmerging with the translation keys inside <code>neeto-commons-frontend</code>, along withthe translation keys from the host app are passed as an argument to <code>initializeI18n</code>function.</p><p>While this approach successfully merges translations, it introduced complexity.As our project expanded with the inclusion of more packages, we found the needto regularly update the <code>neeto-commons-frontend</code> code, manually adding newpackages to the <code>packageNames</code> array. This prompted us to seek an automatedsolution to streamline this process.</p><p>Given that all our packages are under the <code>@bigbinary</code> namespace in <code>npm</code>, weexplored the possibility of dynamically handling this. An initial thought was toiterate through packages under <code>node_modules/@bigbinary</code> and merge theirtranslation keys. However, executing this in the browser was not possible sincethe browser does not have access to its build environment's file system.</p><h2>Enter babel-plugin-preval:</h2><p>To automate our translation aggregation process, we turned to<a href="https://www.npmjs.com/package/babel-plugin-preval"><code>babel-plugin-preval</code></a>. Thisplugin allows us to execute dynamic tasks during build time.</p><p><code>babel-plugin-preval</code> allows us to specify some code that runs in <code>Node</code> andwhatever we <code>module.exports</code> in there will be swapped.</p><p>Let us look at an example:</p><pre><code class="language-js">const x = preval`module.exports = 1`;</code></pre><p>will be transpiled to:</p><pre><code class="language-js">const x = 1;</code></pre><p>With <code>preval.require</code>, the following code:</p><pre><code class="language-js">const fileLastModifiedDate = preval.require(&quot;./get-last-modified-date&quot;);</code></pre><p>will be transpiled to:</p><pre><code class="language-js">const fileLastModifiedDate = &quot;2018-07-05&quot;;</code></pre><p>Here is the content of <code>get-last-modified-date.js</code>:</p><pre><code class="language-js">module.exports = &quot;2018-07-05&quot;;</code></pre><p>Here, the <code>2018-07-05</code> date is read from the file and replaced in the code.</p><p>In order to use this plugin we just need to install it and add <code>preval</code> to the<code>plugins</code> array in <code>.babelrc</code> or <code>.babel.config.js</code></p><h2>Streamlining Translations with preval:</h2><p>We revamped the <code>initializeI18n</code> function using <code>preval.require</code> to dynamicallyfetch translations from all <code>@bigbinary</code>-namespaced packages. This eliminatedthe need for manual updates in <code>neeto-commons-frontend</code> whenever a new packagewas added.</p><p>With preval, our <code>initializeI18n</code> function was refactored as follows:</p><pre><code class="language-js">const initializeI18n = hostTranslations =&gt; {  // eslint-disable-next-line no-undef  const packageTranslations = preval.require(    &quot;../configs/scripts/getPkgTranslations.js&quot;  );  const commonsTranslations = { en: { translation: commonsEn } };  const resources = [    hostTranslations,    commonsTranslations,    packageTranslations,  ].reduce(mergeDeepLeft);};</code></pre><p>The code for <code>getPackageTranslations.js</code>:</p><pre><code class="language-js">const fs = require(&quot;fs&quot;);const path = require(&quot;path&quot;);const { mergeDeepLeft } = require(&quot;ramda&quot;);const packageDir = path.join(__dirname, &quot;../../&quot;);const getPkgTransPath = pkg =&gt; {  const basePath = path.join(packageDir, pkg);  const transPath1 = path.join(basePath, &quot;app/javascript/src/translations&quot;);  const transPath2 = path.join(basePath, &quot;src/translations&quot;);  return fs.existsSync(transPath1) ? transPath1 : transPath2;};const packages = fs.readdirSync(packageDir);const loadTranslations = translationsDir =&gt; {  try {    const jsonFiles = fs      .readdirSync(translationsDir)      .filter(file =&gt; file.endsWith(&quot;.json&quot;))      .map(file =&gt; path.join(translationsDir, file));    const translations = {};    jsonFiles.forEach(jsonFile =&gt; {      const content = fs.readFileSync(jsonFile, &quot;utf8&quot;);      const basename = path.basename(jsonFile, &quot;.json&quot;);      translations[basename] = { translation: JSON.parse(content) };    });    return translations;  } catch {    return {};  }};const packageTranslations = packages  .map(pkg =&gt; loadTranslations(getPkgTransPath(pkg)))  .reduce(mergeDeepLeft);module.exports = packageTranslations;</code></pre><p>In this workflow, we iterate through all the packages to retrieve theirtranslation files and subsequently merge them. We are able to access thetranslation files of our packages since we have exposed those files in the<code>package.json</code> of all our packages.</p><p><code>files</code> property in <code>package.json</code> is an allowlist of all files that should beincluded in an <code>npm</code> release.</p><p>Inside <code>package.json</code> of our nanos, we have added the translations folder to the<code>files</code> property:</p><pre><code class="language-js">{  // other properties  files: [&quot;app/javascript/src/translations&quot;];}</code></pre><p>It's worth noting that we won't run <code>preval</code> at the time of bundling<code>neeto-commons-frontend</code>. Our objective is to merge the translation keys of allinstalled dependencies of the host project with those of the host projectitself. Since <code>neeto-commons-frontend</code> is one of the dependencies of the hostprojects, executing preval within <code>neeto-commons-frontend</code> is not what weneeded.</p><p>Consequently, we've manually excluded the <code>preval</code> plugin from the Babelconfiguration specific to <code>neeto-commons-frontend</code>:</p><pre><code class="language-js">module.exports = function (api) {  const config = defaultConfigurations(api);  config.plugins = config.plugins.filter(plugin =&gt; plugin !== &quot;preval&quot;);  config.sourceMaps = true;};</code></pre><p>With this change, the Babel compiler simply skips the code for <code>preval</code> duringbuild time and the <code>preval</code> related code will be kept as it is after compilationfor <code>neeto-commons-frontend</code>.</p><p>Another challenge arises from the default behavior of <code>webpack</code>, which does nottranspile the <code>node_modules</code> folder by default. However, it's necessary for ourhost application to perform this transpilation. To address this, we wrote acustom rule for <code>webpack</code>. The webpack rules are also placed in<code>neeto-commons-frontend</code> and shared across all the products.</p><pre><code class="language-js">  {    test: /\.js$/,    include:      /node_modules\/@bigbinary\/neeto-commons-frontend\/initializers\/i18n/,    use: { loader: &quot;babel-loader&quot;, options: { plugins: [&quot;preval&quot;] } },  },</code></pre><p>This configuration ensures that Babel applies the necessary transformations tothe code located in<code>node_modules/@bigbinary/neeto-commons-frontend/initializers/i18n/</code> within thehost application.</p><p>Upon transpilation, our system consolidates all translations from each package,including those from the <code>neeto-commons-frontend</code> package, and incorporates theminto the host application.</p><p>To mitigate potential conflicts arising from overlapping keys, we've implementeda namespacing strategy for translations originating from various packages. Thisensures that translations from our packages carry a distinctive key, uniquelyidentifying their source.</p><p>For example, consider the <code>neeto-filters-nano</code> package. In its Englishtranslation file (en.json), the translations are organized within a dedicatednamespace:</p><pre><code class="language-json">neetoFilters: {    &quot;common&quot;: { }}</code></pre><h2>Conclusion:</h2><p>Leveraging <code>babel-plugin-preval</code> significantly simplified our translationresource loading process. The automation introduced not only streamlined ourworkflow but also ensured that our applications stay consistent and easilyadaptable to future package additions.</p>]]></content>
    </entry>
     </feed>