Bỏ qua

I18n Hot Reload - Updated new

Our current implementation faces two critical challenges:

  1. Inefficient Updates

    Key changes trigger full page reloads regardless of locale selection, disrupting user workflows and increasing development friction.

  2. Security Vulnerabilities

    Manual key-value input exposes three systemic risks:

    • Inconsistent localization formats (no sorting, mixed quotes for key-value)
    • XSS injection vulnerabilities through unprocessed JS execution
    • Maintenance overhead in multi-lingual environments

Explanation

Before v9.3, vue-i18n message compiler precompiled locale messages like AOT (Ahead Of Time). However, it had the following issues:

  • CSP issues: hard to work on service/web workers, edge-side runtimes of CDNs and etc.
  • Back-end integration: hard to get messages from back-end such as database via API and localize them dynamically

Solution

To solve these issues, JIT (Just In Time) style compilation is supported message compiler. Each time localization is performed in an application using $t or t functions, message resources will be compiled on message compiler.

  • Since vue-i18n@v9.3.x, the locale messages are handled with message compiler, which transform them to javascript functions or AST objects after compiling, so these can improve the performance of the application.

Building an i18n Hot-Reload System

How it works

This solution combines a custom Vite plugin with Vue's Hot Module Replacement (HMR) system: I18n Hot Reload.webp

The system operates in three main steps:

  1. Watch: The Vite plugin monitors JSON files in specified locale directories for changes.
  2. Intercept: When a change is detected, the plugin intercepts the update and sends a custom HMR event instead of triggering a full page reload.
  3. Update: The client-side handler receives the event, loads the updated translations, and updates the i18n instance in real-time

Implementation Guide

Prerequisites
  • Override @intlify/unplugin-vue-i18n@4.0.0 (optional for jit compilation)
  • Minor version update @vitejs/plugin-vue from 3.1.0 to 3.2.0

1. The Vite Plugin

First, we created a plugin that watches for changes in locale files:

// src/lib/plugins/hmr.ts
function hmrReload(options: { 
  localesDir?: string, 
  localesDirs?: string[] 
} = {}): Plugin {
  let absoluteLocaleDirs: string[] = [];
  
  return {
	name: 'i18n-hmr',
	enforce: 'post',
	configResolved(config) {
	  const localeDirs = [
		options.localesDir || 'src/i18n',
		...(options.localesDirs || [])
	  ].filter(Boolean);
	  
	  absoluteLocaleDirs = localeDirs.map((dir) => 
		path.resolve(config.root, dir)
	  );
	},
	handleHotUpdate({ file, server }) {
	  if (file.endsWith('.json')) {
		const isLocaleFile = absoluteLocaleDirs.some(dir => 
		  file.startsWith(dir) || file.includes('/i18n/')
		);
		
		if (isLocaleFile) {
		  server.ws.send({
			type: "custom",
			event: "locales-updated",
			data: { file }
		  });
		  
		  return [];
		}
	  }
	}
  };
}
// src/lib/plugins/plugin.d.ts
interface VitePlugin {
  name: string;
}
export declare function i18HMRPlugin(
  options: { localesDir: string } | { localesDirs: string[] }
): VitePlugin;

2. Client-Side HMR Handler

Next, we implemented the client-side handler that responds to HMR events:

src/i18n/index.ts
if (import.meta.hot) {
  import.meta.hot.on('locales-updated', async ({ file }) => {
    try {
      const locale = file?.split('/').pop()?.split('.')[0];
      
      if (!locale || !SUPPORT_LOCALES.includes(locale)) {
        return;
      }

      // Load updated translations
      const xhr = new XMLHttpRequest();
      const url = `${file}?nocache=${Date.now()}`;
      
      const loadJson = new Promise<Record<string, any>>((resolve, reject) => {
        xhr.onreadystatechange = function() {
          if (xhr.readyState === 4) {
            if (xhr.status === 200) {
              resolve(JSON.parse(xhr.responseText));
            } else {
              reject(new Error(`XHR failed: ${xhr.status}`));
            }
          }
        };
        
        xhr.open('GET', url, true);
        xhr.setRequestHeader('Cache-Control', 'no-cache');
        xhr.send();
      });
      
      // Update i18n instance
      const newResources = await loadJson;
      i18n.global.setLocaleMessage(locale, newResources);
      
    } catch (err) {
      console.error('Error updating translations:', err);
    }
  });
}

Update vite.config.js to use the plugin:

vite.config.js
import hmrReload from './src/lib/plugins/hmr';

export default defineConfig(({ command, mode }) => {
  return {
    server: {
      watch: {
        usePolling: true,
      },
    },
    plugins: [
      // ...
      hmrReload({
        localesDir: 'src/i18n/locales',
      })
      // ...
    ],
  };
});

Technical Considerations

  • The system uses XMLHttpRequest instead of fetch to ensure no caching of translation files
  • It validates locales against a supported list to prevent unwanted updates
  • Error handling ensures the application remains stable even if updates fail
  • The implementation is framework-agnostic and can be adapted for other i18n libraries