mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-18 00:56:02 +00:00
feat: basic translations extract
This commit is contained in:
61
common/translations-cli/extract.js
Normal file
61
common/translations-cli/extract.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//@ts-check
|
||||||
|
const fs = require('fs');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
|
||||||
|
const { getFiles } = require('./helpers');
|
||||||
|
|
||||||
|
const readFilePromise = promisify(fs.readFile);
|
||||||
|
|
||||||
|
const translationRegex = /_t\(\s*['"]([^'"]+)['"]\s*,\s*\{\s*defaultMessage\s*:\s*['"]([^'"]+)['"]\s*\}/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} file
|
||||||
|
*
|
||||||
|
* @returns {Promise<Record<string, string>>}
|
||||||
|
*/
|
||||||
|
async function extractTranslationsFromFile(file) {
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
const translations = {};
|
||||||
|
const content = await readFilePromise(file, 'utf-8');
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = translationRegex.exec(content)) !== null) {
|
||||||
|
const [_, key, defaultText] = match;
|
||||||
|
translations[key] = defaultText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} directories
|
||||||
|
* @param {string[]} extensions
|
||||||
|
*
|
||||||
|
* @returns {Promise<Record<string, string>>}
|
||||||
|
*/
|
||||||
|
async function extractAllTranslations(directories, extensions) {
|
||||||
|
try {
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
const allTranslations = {};
|
||||||
|
|
||||||
|
for (const dir of directories) {
|
||||||
|
const files = await getFiles(dir, extensions);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fileTranslations = await extractTranslationsFromFile(file);
|
||||||
|
Object.assign(allTranslations, fileTranslations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Total translations found: ${Object.keys(allTranslations).length}`);
|
||||||
|
|
||||||
|
return allTranslations;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting translations:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = {
|
||||||
|
extractTranslationsFromFile,
|
||||||
|
extractAllTranslations,
|
||||||
|
};
|
||||||
134
common/translations-cli/helpers.js
Normal file
134
common/translations-cli/helpers.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
//@ts-check
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} file
|
||||||
|
* @param {string[]} extensions
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function hasValidExtension(file, extensions) {
|
||||||
|
return extensions.includes(path.extname(file).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} dir
|
||||||
|
* @param {string[]} extensions
|
||||||
|
*
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
|
async function getFiles(dir, extensions) {
|
||||||
|
const files = await fs.promises.readdir(dir);
|
||||||
|
const allFiles = await Promise.all(
|
||||||
|
files.map(async file => {
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
const stats = await fs.promises.stat(filePath);
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
return getFiles(filePath, extensions);
|
||||||
|
} else if (stats.isFile() && hasValidExtension(file, extensions)) {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validFiles = /** @type {string[]} */ (allFiles.flat().filter(file => file !== null));
|
||||||
|
|
||||||
|
return validFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | string[]} value
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatDefaultValue(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.join(', ');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptDir = getScriptDir();
|
||||||
|
/** @param {string} file
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function resolveFile(file) {
|
||||||
|
if (path.isAbsolute(file)) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(scriptDir, '..', '..', file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string[]} dirs
|
||||||
|
*
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function resolveDirs(dirs) {
|
||||||
|
return dirs.map(resolveFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} extensions
|
||||||
|
*
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function resolveExtensions(extensions) {
|
||||||
|
return extensions.map(ext => (ext.startsWith('.') ? ext : `.${ext}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptDir() {
|
||||||
|
if (require.main?.filename) {
|
||||||
|
return path.dirname(require.main.filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('pkg' in process && process.pkg) {
|
||||||
|
return path.dirname(process.execPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return __dirname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} file
|
||||||
|
*/
|
||||||
|
function ensureFileDirExists(file) {
|
||||||
|
const dir = path.dirname(file);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, string>} existingTranslations - Previously extracted translations
|
||||||
|
* @param {Record<string, string>} newTranslations - Newly extracted translations
|
||||||
|
* @returns {{ added: string[], removed: string[], updated: string[] }} Translation changes
|
||||||
|
*/
|
||||||
|
const getTranslationChanges = (existingTranslations, newTranslations) => {
|
||||||
|
const existingKeys = new Set(Object.keys(existingTranslations || {}));
|
||||||
|
const newKeys = new Set(Object.keys(newTranslations));
|
||||||
|
|
||||||
|
const added = [...newKeys].filter(key => !existingKeys.has(key));
|
||||||
|
const removed = [...existingKeys].filter(key => !newKeys.has(key));
|
||||||
|
const updated = [...newKeys].filter(
|
||||||
|
key => existingKeys.has(key) && existingTranslations[key] !== newTranslations[key]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { added, removed, updated };
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hasValidExtension,
|
||||||
|
getFiles,
|
||||||
|
formatDefaultValue,
|
||||||
|
resolveFile,
|
||||||
|
resolveDirs,
|
||||||
|
resolveExtensions,
|
||||||
|
ensureFileDirExists,
|
||||||
|
getTranslationChanges,
|
||||||
|
};
|
||||||
89
common/translations-cli/program.js
Normal file
89
common/translations-cli/program.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
//@ts-check
|
||||||
|
const fs = require('fs');
|
||||||
|
const { program } = require('commander');
|
||||||
|
const {
|
||||||
|
resolveDirs,
|
||||||
|
resolveExtensions,
|
||||||
|
resolveFile,
|
||||||
|
ensureFileDirExists,
|
||||||
|
getTranslationChanges,
|
||||||
|
} = require('./helpers');
|
||||||
|
const { extractAllTranslations } = require('./extract');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ extensions: string[], directories: string[], outputFile: string}} Config
|
||||||
|
* @typedef {Config & { verbose?: boolean }} Options
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Config} */
|
||||||
|
const defaultConfig = {
|
||||||
|
extensions: ['.js', '.ts', '.svelte'],
|
||||||
|
directories: ['app', 'packages/web'],
|
||||||
|
outputFile: './translations/en.json',
|
||||||
|
};
|
||||||
|
|
||||||
|
program.name('dbgate-translations-cli').description('CLI tool for managing translation').version('1.0.0');
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('extract')
|
||||||
|
.description('Extract translation keys from source files')
|
||||||
|
.option('-d, --directories <directories...>', 'directories to search', defaultConfig.directories)
|
||||||
|
.option('-e, --extensions <extensions...>', 'file extensions to process', defaultConfig.extensions)
|
||||||
|
.option('-o, --outputFile <file>', 'output file path', defaultConfig.outputFile)
|
||||||
|
.option('-v, --verbose', 'verbose mode')
|
||||||
|
.action(async (/** @type {Options} */ options) => {
|
||||||
|
try {
|
||||||
|
const { directories, extensions, outputFile, verbose } = options;
|
||||||
|
|
||||||
|
const resolvedRirectories = resolveDirs(directories);
|
||||||
|
const resolvedExtensions = resolveExtensions(extensions);
|
||||||
|
|
||||||
|
const translations = await extractAllTranslations(resolvedRirectories, resolvedExtensions);
|
||||||
|
|
||||||
|
const resolvedOutputFile = resolveFile(outputFile);
|
||||||
|
ensureFileDirExists(resolvedOutputFile);
|
||||||
|
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
let existingTranslations = {};
|
||||||
|
if (fs.existsSync(resolvedOutputFile)) {
|
||||||
|
existingTranslations = JSON.parse(fs.readFileSync(resolvedOutputFile, 'utf-8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { added, removed, updated } = getTranslationChanges(existingTranslations, translations);
|
||||||
|
|
||||||
|
console.log('\nTranslation changes:');
|
||||||
|
console.log(`- Added: ${added.length} keys`);
|
||||||
|
console.log(`- Removed: ${removed.length} keys`);
|
||||||
|
console.log(`- Updated: ${updated.length} keys`);
|
||||||
|
console.log(`- Total: ${Object.keys(translations).length} keys`);
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
if (added.length > 0) {
|
||||||
|
console.log('\nNew keys:');
|
||||||
|
added.forEach(key => console.log(` + ${key}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed.length > 0) {
|
||||||
|
console.log('\nRemoved keys:');
|
||||||
|
removed.forEach(key => console.log(` - ${key}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated.length > 0) {
|
||||||
|
console.log('\nUpdated keys:');
|
||||||
|
updated.forEach(key => {
|
||||||
|
console.log(` ~ ${key}`);
|
||||||
|
console.log(` Old: ${existingTranslations[key]}`);
|
||||||
|
console.log(` New: ${translations[key]}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(resolvedOutputFile, JSON.stringify(translations, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.error('Error during extraction:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse();
|
||||||
Reference in New Issue
Block a user