UNPKG

tify

Version:

A slim and mobile-friendly IIIF document viewer

218 lines (176 loc) 5.59 kB
import fs from 'node:fs'; import path from 'node:path'; import url from 'node:url'; import chalk from 'chalk'; import * as Diff from 'diff'; export const indention = '\t'; export const rootDir = url.fileURLToPath(new URL('..', import.meta.url)); const globPattern = `${rootDir}/src/**/*.vue`; const excludedGlobPatterns = ['**/demo']; const excludedKeys = ['$n/a']; const translationFunctionName = '$translate'; function sortTranslation(translation) { return Object.keys(translation) .sort((a, b) => { if (a === '$language') { return -1; } if (b === '$language') { return 1; } return a.toLowerCase().localeCompare(b.toLowerCase()); }) .reduce((acc, key) => { acc[key] = translation[key]; return acc; }, {}); } export function checkTranslationFiles( translationsDir, referenceKeys, options = { addMissing: false, removeUnused: false, sort: false }, ) { const filteredReferenceKeys = referenceKeys.filter((key) => !excludedKeys.includes(key)); const results = []; const translationFiles = fs.readdirSync(translationsDir); translationFiles.forEach((file) => { const result = { langCode: path.parse(file).name, notes: [], issues: [], }; const json = fs.readFileSync(`${translationsDir}/${file}`); const translation = JSON.parse(json); const keys = Object.keys(translation).filter((key) => !excludedKeys.includes(key)); result.langName = translation.$language; const emptyKeys = keys.filter((key) => !translation[key]); result.issues.push(...emptyKeys.map((key) => ({ type: 'empty', key }))); const missingKeys = filteredReferenceKeys.filter((key) => !keys.includes(key)); result.issues.push(...missingKeys.map((key) => ({ type: 'missing', key }))); const unusedKeys = keys.filter((key) => !filteredReferenceKeys.includes(key)); result.issues.push(...unusedKeys.map((key) => ({ type: 'unused', key }))); const keysStr = keys.join(', '); const referenceKeysStr = filteredReferenceKeys.join(', '); if (!missingKeys.length && !unusedKeys.length && keysStr !== referenceKeysStr ) { const diffs = Diff.diffChars(keysStr, referenceKeysStr); const difference = diffs.map((part) => { if (part.added) { return chalk.green(part.value); } if (part.removed) { return chalk.red(part.value); } return part.value; }).join(''); result.notes.push(`Keys are not properly sorted: ${chalk.reset(difference)}`); } results.push(result); if (options.addMissing && missingKeys.length) { missingKeys.forEach((key) => { translation[key] = ''; }); result.notes.push('Added missing keys, please add translations'); } if (options.removeUnused && unusedKeys.length) { unusedKeys.forEach((key) => { delete translation[key]; }); result.notes.push('Removed unused keys'); } const updatedTranslation = options.sort ? sortTranslation(translation) : translation; if (options.addMissing || options.removeUnused || options.sort) { const updatedJson = JSON.stringify(updatedTranslation, null, indention); fs.writeFileSync(`${translationsDir}/${file}`, `${updatedJson}\n`); } }); return results; } /** * Find all proper occurrences of $translate() * * $translate() should always fit into a single line and only have single-quoted * strings as parameters. The function params may contain ternary operators, but * translated strings must never be concatenated or otherwise "built" for the * automated translation tests to work. * * Good examples: * * $translate('String') * $translate(condition ? 'String' : 'Another string') * * Bad examples (i.e. would not be found): * * $translate('String' + ' ' + 'another string') * $translate( * 'String', * ) * $translate(1); * $translate( * '1', * `2`, * ) * $translate('', * '2', * ) */ export function findTranslatedStrings() { const filePaths = fs.globSync(globPattern, { exclude: excludedGlobPatterns }); const results = []; const translationFunctionRegexp = new RegExp( `${translationFunctionName.replace(/\$/g, '\\$')}\\s*\\(\\s*(.*)\\s*\\)?`, 'gs', ); filePaths.forEach((filePath) => { let lineNumber = 1; const fileContents = fs.readFileSync(filePath, 'utf8'); fileContents.split('\n').forEach((line) => { const matches = [...line.matchAll(translationFunctionRegexp)]; if (!matches) { return; } matches.forEach((match) => { const strings = match[1] .match(/'(.*?)'|"(.*?)"/g) ?.map((string) => string.slice(1, -1)); // remove quotes if (!strings) { console.warn(chalk.redBright( 'Found translation function, but could not determine parameters' + ` in ${chalk.dim('file://')}${chalk.bold(filePath)}, line ${lineNumber}: ${chalk.bold(match[0])}\n`, )); return; } if (strings.some(((string) => !string))) { console.warn(chalk.redBright( 'Translation function uses an empty value' + ` in ${chalk.dim('file://')}${chalk.bold(filePath)}, line ${lineNumber}: ${chalk.bold(match[0])}\n`, )); return; } const matchObject = { file: filePath, line: lineNumber, match: match[0], }; strings.forEach((string) => { const index = results.findIndex((result) => result.key === string); if (index >= 0) { results[index].matches.push(matchObject); } else { results.push({ key: string, matches: [matchObject], }); } }); }); lineNumber += 1; }); }); results.sort((a, b) => a.key.localeCompare(b.key)); return results; }