| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- import {
- createCompilationErrorMessage,
- createCompiledCatalog,
- getCatalogForFile,
- getCatalogs,
- } from '@lingui/cli/api';
- import { getConfig, LinguiConfigNormalized } from '@lingui/conf';
- import glob from 'fast-glob';
- import * as fs from 'fs';
- import * as path from 'path';
- import type { Plugin } from 'vite';
- import { PluginInfo } from './types.js';
- import { CompileResult } from './utils/compiler.js';
- import { getDashboardPaths } from './utils/get-dashboard-paths.js';
- import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
- type Catalog = Awaited<ReturnType<typeof getCatalogs>>[number];
- export interface TranslationsPluginOptions {
- /**
- * Array of paths to .po files to merge with built-in translations
- */
- externalPoFiles?: string[];
- /**
- * Path to the built-in locales directory
- */
- localesDir?: string;
- /**
- * Output path for merged translations within the build output (e.g., 'i18n')
- */
- outputPath?: string;
- packageRoot: string;
- }
- type PluginTranslation = {
- pluginRootPath: string;
- translations: string[];
- };
- const virtualModuleId = 'virtual:plugin-translations';
- const resolvedVirtualModuleId = `\0${virtualModuleId}`;
- /**
- * @description
- * This Vite plugin compiles the source .po files into JS bundles that can be loaded statically.
- *
- * It handles 2 modes: dev and build.
- *
- * - The dev case is handled in the `load` function using Vite virtual
- * modules to compile and return translations from plugins _only_, which then get merged with the built-in
- * translations in the `loadI18nMessages` function
- * - The build case loads both built-in and plugin translations, merges them, and outputs the compiled
- * files as .js files that can be statically consumed by the built app.
- *
- * @param options
- */
- export function translationsPlugin(options: TranslationsPluginOptions): Plugin {
- let configLoaderApi: ConfigLoaderApi;
- let loadVendureConfigResult: CompileResult;
- return {
- name: 'vendure:compile-translations',
- configResolved({ plugins }) {
- configLoaderApi = getConfigLoaderApi(plugins);
- },
- resolveId(id) {
- if (id === virtualModuleId) {
- return resolvedVirtualModuleId;
- }
- },
- async load(id) {
- if (id === resolvedVirtualModuleId) {
- this.debug('Loading plugin translations...');
- if (!loadVendureConfigResult) {
- loadVendureConfigResult = await configLoaderApi.getVendureConfig();
- }
- const { pluginInfo } = loadVendureConfigResult;
- const pluginTranslations = await getPluginTranslations(pluginInfo);
- const linguiConfig = getConfig({
- configPath: path.join(options.packageRoot, 'lingui.config.js'),
- });
- const catalogs = await getLinguiCatalogs(linguiConfig, pluginTranslations);
- const pluginFiles = pluginTranslations.flatMap(translation => translation.translations);
- const mergedMessageMap = await createMergedMessageMap({
- files: pluginFiles,
- packageRoot: options.packageRoot,
- catalogs,
- sourceLocale: linguiConfig.sourceLocale,
- });
- return `
- const translations = {
- ${[...mergedMessageMap.entries()]
- .map(([locale, messages]) => {
- const safeLocale = locale.replace(/-/g, '_');
- return `${safeLocale}: ${JSON.stringify(messages)}`;
- })
- .join(',\n')}
- };
- export default translations;
- `;
- }
- },
- // This runs at build-time only
- async generateBundle() {
- // This runs during the bundle generation phase - emit files directly to build output
- try {
- const { pluginInfo } = await configLoaderApi.getVendureConfig();
- // Get any plugin-provided .po files
- const pluginTranslations = await getPluginTranslations(pluginInfo);
- const pluginTranslationFiles = pluginTranslations.flatMap(p => p.translations);
- this.info(`Found ${pluginTranslationFiles.length} translation files from plugins`);
- this.debug(pluginTranslationFiles.join('\n'));
- await compileTranslations(options, pluginTranslations, this.emitFile);
- } catch (error) {
- this.error(
- `Translation plugin error: ${error instanceof Error ? error.message : String(error)}`,
- );
- }
- },
- };
- }
- async function getPluginTranslations(pluginInfo: PluginInfo[]): Promise<PluginTranslation[]> {
- const dashboardPaths = getDashboardPaths(pluginInfo);
- const pluginTranslations: PluginTranslation[] = [];
- for (const dashboardPath of dashboardPaths) {
- const poPatterns = path.join(dashboardPath, '**/*.po');
- const translations = await glob(poPatterns, {
- ignore: [
- // Standard test & doc files
- '**/node_modules/**/node_modules/**',
- '**/*.spec.js',
- '**/*.test.js',
- ],
- onlyFiles: true,
- absolute: true,
- followSymbolicLinks: false,
- stats: false,
- });
- pluginTranslations.push({
- pluginRootPath: dashboardPath,
- translations,
- });
- }
- return pluginTranslations;
- }
- async function compileTranslations(
- options: TranslationsPluginOptions,
- pluginTranslations: PluginTranslation[],
- emitFile: any,
- ) {
- const { localesDir = 'src/i18n/locales', outputPath = 'assets/i18n' } = options;
- const linguiConfig = getConfig({ configPath: path.join(options.packageRoot, 'lingui.config.js') });
- const resolvedLocalesDir = path.resolve(options.packageRoot, localesDir);
- const catalogs = await getLinguiCatalogs(linguiConfig, pluginTranslations);
- // Get all built-in .po files
- const builtInFiles = fs
- .readdirSync(resolvedLocalesDir)
- .filter(file => file.endsWith('.po'))
- .map(file => path.join(resolvedLocalesDir, file));
- const pluginFiles = pluginTranslations.flatMap(translation => translation.translations);
- const mergedMessageMap = await createMergedMessageMap({
- files: [...builtInFiles, ...pluginFiles],
- packageRoot: options.packageRoot,
- catalogs,
- sourceLocale: linguiConfig.sourceLocale,
- });
- for (const [locale, messages] of mergedMessageMap.entries()) {
- const { source: code, errors } = createCompiledCatalog(locale, messages, {
- namespace: 'es',
- pseudoLocale: linguiConfig.pseudoLocale,
- });
- if (errors.length) {
- const message = createCompilationErrorMessage(locale, errors);
- throw new Error(
- message +
- `These errors fail build because \`failOnCompileError=true\` in Lingui Vite plugin configuration.`,
- );
- }
- // Emit the compiled JavaScript file to the build output
- const outputFileName = path.posix.join(outputPath, `${locale}.js`);
- emitFile({
- type: 'asset',
- fileName: outputFileName,
- source: code,
- });
- }
- }
- async function getLinguiCatalogs(
- linguiConfig: LinguiConfigNormalized,
- pluginTranslations: PluginTranslation[],
- ) {
- for (const pluginTranslation of pluginTranslations) {
- if (pluginTranslation.translations.length === 0) {
- continue;
- }
- linguiConfig.catalogs?.push({
- path: pluginTranslation.translations[0]?.replace(/[a-z_-]+\.po$/, '{locale}') ?? '',
- include: [],
- });
- }
- return getCatalogs(linguiConfig);
- }
- async function createMergedMessageMap({
- files,
- packageRoot,
- catalogs,
- sourceLocale,
- }: {
- files: string[];
- packageRoot: string;
- catalogs: Catalog[];
- sourceLocale?: string;
- }): Promise<Map<string, Record<string, string>>> {
- const mergedMessageMap = new Map<string, Record<string, string>>();
- for (const file of files) {
- const catalogRelativePath = path.relative(packageRoot, file);
- const fileCatalog = getCatalogForFile(catalogRelativePath, catalogs);
- const { locale, catalog } = fileCatalog;
- const { messages } = await catalog.getTranslations(locale, {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- fallbackLocales: { default: sourceLocale! },
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- sourceLocale: sourceLocale!,
- });
- const mergedMessages = mergedMessageMap.get(locale) ?? {};
- mergedMessageMap.set(locale, { ...mergedMessages, ...messages });
- }
- return mergedMessageMap;
- }
|