import fs from "fs"; import path from "path"; import deepmerge from "deepmerge"; import { Config } from "../config"; import { TranslationKey } from "../keys"; import { ConflictError, Exporter, ExportError } from "./commons"; import jsonv3Exporter from "./jsonv3"; import phpExporter from "./php"; export { ConflictError, ExportError }; /** * An instance of exporter cache. * * See createExporterCache for details. */ export interface ExporterCache { // eslint-disable-next-line @typescript-eslint/no-explicit-any originalTranslationFiles: { [path: string]: any }; // eslint-disable-next-line @typescript-eslint/no-explicit-any currentTranslationFiles: { [path: string]: any }; } /** * This creates a new empty cache for the exporter. * * The cache is required by the exporter and is used to merge the translations * from the original translation file. It will be mutated by the exporter * and the same instance must be given untouched across export calls. */ export function createExporterCache(): ExporterCache { return { originalTranslationFiles: {}, currentTranslationFiles: {}, }; } /** * Load a translation file. */ function loadTranslationFile( // eslint-disable-next-line @typescript-eslint/no-explicit-any exporter: Exporter, config: Config, filePath: string ): F { let content: string; try { content = fs.readFileSync(filePath, { encoding: "utf8" }); } catch (err) { if (err.code === "ENOENT") return exporter.init({ config }); throw err; } return exporter.parse({ config, content }); } /** * Get the default value for a key. */ function getDefaultValue( key: TranslationKey, locale: string, config: Config // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any { let defaultValue = config.defaultValue; const keyAsDefaultValueEnabled = config.keyAsDefaultValue === true || (Array.isArray(config.keyAsDefaultValue) && config.keyAsDefaultValue.includes(locale)); const keyAsDefaultValueForDerivedKeys = config.keyAsDefaultValueForDerivedKeys; if ( keyAsDefaultValueEnabled && (keyAsDefaultValueForDerivedKeys || !key.isDerivedKey) ) { defaultValue = key.cleanKey; } const useI18nextDefaultValueEnabled = config.useI18nextDefaultValue === true || (Array.isArray(config.useI18nextDefaultValue) && config.useI18nextDefaultValue.includes(locale)); const useI18nextDefaultValueForDerivedKeys = config.useI18nextDefaultValueForDerivedKeys; if ( useI18nextDefaultValueEnabled && key.parsedOptions.defaultValue !== null && (useI18nextDefaultValueForDerivedKeys || !key.isDerivedKey) ) { defaultValue = key.parsedOptions.defaultValue; } return defaultValue; } /** * Exports all given translation keys as JSON. * * @param keys: translation keys to export * @param locale: the locale to export * @param config: plugin configuration * @param cache: cache instance to use (see createExporterCache) */ export default function exportTranslationKeys( keys: TranslationKey[], locale: string, config: Config, cache: ExporterCache ): void { const keysPerFilepath: { [path: string]: TranslationKey[] } = {}; const exporter = jsonv3Exporter; for (const key of keys) { // Figure out in which path each key should go. const filePath = typeof config.outputPath === "function" ? config.outputPath(locale, key.ns) : config.outputPath .replace("{{locale}}", locale) .replace("{{ns}}", key.ns); keysPerFilepath[filePath] = [...(keysPerFilepath[filePath] || []), key]; } for (const [filePath, keysForFilepath] of Object.entries(keysPerFilepath)) { cache.originalTranslationFiles[filePath] = deepmerge( cache.originalTranslationFiles[filePath] ?? {}, loadTranslationFile(exporter, config, filePath), { // Overwrites the existing array values completely rather than concatenating them arrayMerge: (dest, source) => source, } ); const originalTranslationFile = cache.originalTranslationFiles[filePath]; let translationFile = cache.currentTranslationFiles[filePath] || (config.discardOldKeys ? exporter.init({ config }) : originalTranslationFile); for (const k of keysForFilepath) { const previousValue = exporter.getKey({ config, file: originalTranslationFile, keyPath: k.keyPath, cleanKey: k.cleanKey, }); translationFile = exporter.addKey({ config, file: translationFile, key: k, value: previousValue === undefined ? getDefaultValue(k, locale, config) : previousValue, }); } cache.currentTranslationFiles[filePath] = translationFile; // Finally do the export const directoryPath = path.dirname(filePath); fs.mkdirSync(directoryPath, { recursive: true }); if (config.__usePhp) { fs.writeFileSync( filePath, phpExporter.stringify({ config, file: translationFile, }), { encoding: "utf8", } ); } else { fs.writeFileSync( filePath, exporter.stringify({ config, file: translationFile, }), { encoding: "utf8", } ); } } }