Resource Loading

The plugin supports three resource loading methods: HTTP backend, File System backend (FS Backend), and SDK backend. Additionally, the plugin supports chained backend, which allows combining multiple backends.

HTTP Backend

HTTP backend loads resource files through HTTP requests, suitable for client-side rendering (CSR) scenarios.

Configuration

i18nPlugin({
  backend: {
    enabled: true,
    loadPath: '/locales/{{lng}}/{{ns}}.json',
  },
});

Resource File Structure

Resource files need to be placed in the config/public directory or the directory configured through server.publicDir:

config/public/
└── locales/
    ├── en/
    │   └── translation.json
    └── zh/
        └── translation.json

Or configure a custom directory through server.publicDir:

export default defineConfig({
  server: {
    publicDir: './locales', // Specify resource file directory
  },
  plugins: [
    i18nPlugin({
      backend: {
        enabled: true,
        loadPath: '/locales/{{lng}}/{{ns}}.json',
      },
    }),
  ],
});

Resource File Format:

{
  "key1": "value1",
  "key2": "value2",
  "nested": {
    "key": "value"
  }
}

Path Variables

loadPath supports the following variables:

  • {{lng}}: Language code (e.g., en, zh)
  • {{ns}}: Namespace (e.g., translation, common)

Examples:

// Default path format
loadPath: '/locales/{{lng}}/{{ns}}.json';
// Actual loading paths:
// /locales/en/translation.json
// /locales/zh/translation.json

// Custom path format
loadPath: '/i18n/{{lng}}/{{ns}}.json';
// Actual loading paths:
// /i18n/en/translation.json
// /i18n/zh/translation.json

File System Backend (FS Backend)

File System backend reads resource files directly from the file system, suitable for server-side rendering (SSR) scenarios.

Configuration

In SSR scenarios, the plugin will automatically use the file system backend. Resource files need to be placed in the project directory:

Project Root/
└── locales/
    ├── en/
    │   └── translation.json
    └── zh/
        └── translation.json

Resource File Path

The default path format for file system backend is a relative path:

./locales/{{lng}}/{{ns}}.json

You can customize the path through loadPath:

i18nPlugin({
  backend: {
    enabled: true,
    loadPath: '/locales/{{lng}}/{{ns}}.json', // Use absolute path (recommended)
  },
});
Warning

The loadPath configuration is used for both HTTP backend (frontend) and file system backend (server-side). If configured as an absolute path starting with / (e.g., /locales/{{lng}}/{{ns}}.json), the file system backend will automatically convert it to a relative path (./locales/{{lng}}/{{ns}}.json). Therefore, it's recommended to use absolute paths in the configuration, which can meet both frontend and backend requirements.

SDK Backend

SDK backend allows loading resources through custom functions, suitable for scenarios where translation resources need to be loaded from external services, databases, or other custom sources.

Enable SDK Mode

Step 1: Enable SDK mode in modern.config.ts

i18nPlugin({
  backend: {
    enabled: true,
    sdk: true, // Enable SDK mode
  },
});

Step 2: Implement SDK function in modern.runtime.ts

import { defineRuntimeConfig } from '@modern-js/runtime';
import type { I18nSdkLoader, Resources } from '@modern-js/plugin-i18n/runtime';

const mySdkLoader: I18nSdkLoader = async options => {
  // Implement resource loading logic
  if (options.all) {
    // Load all resources
    return await loadAllResources();
  }

  if (options.lng && options.ns) {
    // Load single resource
    return await loadSingleResource(options.lng, options.ns);
  }

  return {};
};

export default defineRuntimeConfig({
  i18n: {
    initOptions: {
      backend: {
        sdk: mySdkLoader,
      },
    },
  },
});

Implement SDK Loader Function

The SDK function receives an I18nSdkLoadOptions parameter and needs to return data in Resources format:

interface I18nSdkLoadOptions {
  /** Single language code */
  lng?: string;
  /** Single namespace */
  ns?: string;
  /** Multiple language codes */
  lngs?: string[];
  /** Multiple namespaces */
  nss?: string[];
  /** Load all resources */
  all?: boolean;
}

type Resources = {
  [lng: string]: {
    [ns: string]: Record<string, string>;
  };
};

Batch Loading Examples

SDK backend supports multiple loading modes:

1. Load single resource:

const sdkLoader: I18nSdkLoader = async options => {
  if (options.lng && options.ns) {
    const response = await fetch(`/api/i18n/${options.lng}/${options.ns}`);
    const data = await response.json();
    return {
      [options.lng]: {
        [options.ns]: data,
      },
    };
  }
  return {};
};

2. Batch load multiple languages:

const sdkLoader: I18nSdkLoader = async options => {
  if (options.lngs && options.ns) {
    const resources: Resources = {};
    for (const lng of options.lngs) {
      const response = await fetch(`/api/i18n/${lng}/${options.ns}`);
      resources[lng] = {
        [options.ns]: await response.json(),
      };
    }
    return resources;
  }
  return {};
};

3. Batch load multiple namespaces:

const sdkLoader: I18nSdkLoader = async options => {
  if (options.lng && options.nss) {
    const resources: Resources = {
      [options.lng]: {},
    };
    for (const ns of options.nss) {
      const response = await fetch(`/api/i18n/${options.lng}/${ns}`);
      resources[options.lng][ns] = await response.json();
    }
    return resources;
  }
  return {};
};

4. Load all resources:

const sdkLoader: I18nSdkLoader = async options => {
  if (options.all) {
    const response = await fetch('/api/i18n/all');
    return await response.json();
  }
  return {};
};

Check Resource Loading State

When using SDK backend, you can check if resources are loaded using isResourcesReady:

import { useModernI18n } from '@modern-js/plugin-i18n/runtime';

function MyComponent() {
  const { isResourcesReady } = useModernI18n();

  if (!isResourcesReady) {
    return <div>Loading translation resources...</div>;
  }

  return <div>Resources are ready!</div>;
}

This is particularly useful when resources are loaded asynchronously, as it ensures all required namespaces for the current language are loaded before rendering content that depends on translations.

Chained Backend

Chained backend allows using multiple backends simultaneously, enabling progressive resource loading and updates. When both loadPath (or FS backend) and sdk are configured, the plugin automatically uses i18next-chained-backend to chain resource loading.

How It Works

The chained backend workflow:

  1. Initial Load: First load resources from HTTP/FS backend and display immediately (quick display of basic translations)
  2. Async Update: Then asynchronously load resources from SDK backend and update the i18next store (update/supplement translations)

This ensures users see page content quickly while the latest translation resources are loaded in the background.

Configuration

Step 1: Configure chained backend in modern.config.ts

i18nPlugin({
  backend: {
    enabled: true,
    loadPath: '/locales/{{lng}}/{{ns}}.json', // HTTP/FS backend
    sdk: true, // SDK backend
    // cacheHitMode: 'refreshAndUpdateStore', // Default value, can be omitted
  },
});

Step 2: Implement SDK function in modern.runtime.ts

import { defineRuntimeConfig } from '@modern-js/runtime';

export default defineRuntimeConfig({
  i18n: {
    initOptions: {
      backend: {
        sdk: async options => {
          // SDK implementation
          if (options.lng && options.ns) {
            return await mySdk.getResource(options.lng, options.ns);
          }
        },
      },
    },
  },
});

Cache Hit Mode (cacheHitMode)

The cacheHitMode option controls the behavior of chained backend:

  • 'none' (default, only when chained backend is not configured): If the first backend returns resources, stop and don't try the next backend
  • 'refresh': Try to refresh the cache by loading from the next backend and update the cache
  • 'refreshAndUpdateStore' (default for chained backend): Try to refresh the cache by loading from the next backend, update the cache and also update the i18next resource store. This allows FS/HTTP resources to be displayed first, then SDK resources will update them asynchronously.

Configuration example:

i18nPlugin({
  backend: {
    enabled: true,
    loadPath: '/locales/{{lng}}/{{ns}}.json',
    sdk: true,
    cacheHitMode: 'refreshAndUpdateStore', // Explicitly specify (default value)
  },
});

Use Cases

Chained backend is particularly suitable for the following scenarios:

  1. Progressive Loading: Display local/static resources first, then load latest translations from remote services
  2. Offline Support: Local resources as offline fallback, SDK resources provide online updates
  3. Performance Optimization: Quickly display basic translations, load complete translation content in the background
  4. A/B Testing: Local resources as default values, SDK provides dynamic translation variants

Complete Example

// modern.config.ts
i18nPlugin({
  backend: {
    enabled: true,
    loadPath: '/locales/{{lng}}/{{ns}}.json', // Local resources
    sdk: true, // Remote SDK resources
    cacheHitMode: 'refreshAndUpdateStore',
  },
});

// modern.runtime.ts
import { defineRuntimeConfig } from '@modern-js/runtime';

export default defineRuntimeConfig({
  i18n: {
    initOptions: {
      backend: {
        sdk: async options => {
          if (options.lng && options.ns) {
            // Load latest translations from remote service
            const response = await fetch(
              `https://api.example.com/i18n/${options.lng}/${options.ns}`,
            );
            return {
              [options.lng]: {
                [options.ns]: await response.json(),
              },
            };
          }
          return {};
        },
      },
    },
  },
});

In this example:

  • When the page loads, resources are first loaded from /locales/{{lng}}/{{ns}}.json and displayed immediately
  • Latest translations are loaded asynchronously from https://api.example.com/i18n/... in the background
  • After SDK resources are loaded, the i18next store is automatically updated, and the UI text is automatically updated