import { mkdir, rm, writeFile } from 'node:fs/promises';
import { dirname, extname, join, resolve } from 'node:path';
import { isDeepStrictEqual } from 'node:util';
import {
getFilteredLocalesDictionary,
getPerLocaleDictionary,
} from '@intlayer/core';
import type {
Dictionary,
IntlayerConfig,
Locale,
LocalesValues,
} from '@intlayer/types';
import { getUnmergedDictionaries } from '@intlayer/unmerged-dictionaries-entry';
import {
type Extension,
getFormatFromExtension,
} from '../utils/getFormatFromExtension';
import type { DictionaryStatus } from './dictionaryStatus';
import { processContentDeclarationContent } from './processContentDeclarationContent';
import { writeJSFile } from './writeJSFile';
const formatContentDeclaration = async (
dictionary: Dictionary,
configuration: IntlayerConfig,
localeList?: LocalesValues[]
) => {
/**
* Clean Markdown, Insertion, File, etc. node metadata
*/
const processedDictionary =
await processContentDeclarationContent(dictionary);
let content = processedDictionary.content;
/**
* Filter locales content
*/
if (dictionary.locale) {
content = getPerLocaleDictionary(
processedDictionary,
dictionary.locale
).content;
} else if (localeList) {
content = getFilteredLocalesDictionary(
processedDictionary,
localeList
).content;
}
let pluginFormatResult: any = {
...dictionary,
content,
} satisfies Dictionary;
/**
* Format the dictionary with the plugins
*/
for await (const plugin of configuration.plugins ?? []) {
if (plugin.formatOutput) {
const formattedResult = await plugin.formatOutput?.({
dictionary: pluginFormatResult,
configuration,
});
if (formattedResult) {
pluginFormatResult = formattedResult;
}
}
}
const isDictionaryFormat =
pluginFormatResult.content && pluginFormatResult.key;
if (!isDictionaryFormat) return pluginFormatResult;
let result: Dictionary = {
key: dictionary.key,
id: dictionary.id,
title: dictionary.title,
description: dictionary.description,
tags: dictionary.tags,
locale: dictionary.locale,
fill: dictionary.fill,
filled: dictionary.filled,
priority: dictionary.priority,
live: dictionary.live,
version: dictionary.version,
content,
};
/**
* Add $schema to JSON dictionaries
*/
const extension = (
dictionary.filePath ? extname(dictionary.filePath) : '.json'
) as Extension;
const format = getFormatFromExtension(extension);
if (
format === 'json' &&
pluginFormatResult.content &&
pluginFormatResult.key
) {
result = {
$schema: 'https://intlayer.org/schema.json',
...result,
};
}
return result;
};
type WriteContentDeclarationOptions = {
newDictionariesPath?: string;
localeList?: LocalesValues[];
fallbackLocale?: Locale;
};
const defaultOptions = {
newDictionariesPath: 'intlayer-dictionaries',
} satisfies WriteContentDeclarationOptions;
export const writeContentDeclaration = async (
dictionary: Dictionary,
configuration: IntlayerConfig,
options?: WriteContentDeclarationOptions
): Promise<{ status: DictionaryStatus; path: string }> => {
const { content } = configuration;
const { baseDir } = content;
const { newDictionariesPath, localeList } = {
...defaultOptions,
...options,
};
const newDictionaryLocationPath = join(baseDir, newDictionariesPath);
const unmergedDictionariesRecord = getUnmergedDictionaries(configuration);
const unmergedDictionaries = unmergedDictionariesRecord[
dictionary.key
] as Dictionary[];
const existingDictionary = unmergedDictionaries?.find(
(el) => el.localId === dictionary.localId
);
const formattedContentDeclaration = await formatContentDeclaration(
dictionary,
configuration,
localeList
);
if (existingDictionary?.filePath) {
// Compare existing dictionary content with new dictionary content
const isSameContent = isDeepStrictEqual(existingDictionary, dictionary);
const filePath = resolve(
configuration.content.baseDir,
existingDictionary.filePath
);
// Up to date, nothing to do
if (isSameContent) {
return {
status: 'up-to-date',
path: filePath,
};
}
await writeFileWithDirectories(
filePath,
formattedContentDeclaration,
configuration
);
return { status: 'updated', path: filePath };
}
if (dictionary.filePath) {
const filePath = resolve(
configuration.content.baseDir,
dictionary.filePath
);
await writeFileWithDirectories(
filePath,
formattedContentDeclaration,
configuration
);
return { status: 'created', path: filePath };
}
// No existing dictionary, write to new location
const contentDeclarationPath = join(
newDictionaryLocationPath,
`${dictionary.key}.content.json`
);
await writeFileWithDirectories(
contentDeclarationPath,
formattedContentDeclaration,
configuration
);
return {
status: 'imported',
path: contentDeclarationPath,
};
};
const writeFileWithDirectories = async (
absoluteFilePath: string,
dictionary: Dictionary,
configuration: IntlayerConfig
): Promise<void> => {
// Extract the directory from the file path
const dir = dirname(absoluteFilePath);
// Create the directory recursively
await mkdir(dir, { recursive: true });
const extension = extname(absoluteFilePath);
const acceptedExtensions = configuration.content.fileExtensions.map(
(extension) => extname(extension)
);
if (!acceptedExtensions.includes(extension)) {
throw new Error(
`Invalid file extension: ${extension}, file: ${absoluteFilePath}`
);
}
if (extension === '.json') {
const jsonDictionary = JSON.stringify(dictionary, null, 2);
// Write the file
await writeFile(absoluteFilePath, `${jsonDictionary}\n`); // Add a new line at the end of the file to avoid formatting issues with VSCode
return;
}
await writeJSFile(absoluteFilePath, dictionary, configuration);
// remove the cache as content has changed
// Will force a new preparation of the intlayer on next build
try {
const sentinelPath = join(
configuration.content.cacheDir,
'intlayer-prepared.lock'
);
await rm(sentinelPath, { recursive: true });
} catch {}
};