From a84cbee9dbf2e616a1ff6dbd5c460020bf770cd1 Mon Sep 17 00:00:00 2001 From: Nybkox Date: Tue, 18 Feb 2025 23:49:13 +0100 Subject: [PATCH 01/19] feat: basic translations extract --- common/translations-cli/extract.js | 61 +++++++++++++ common/translations-cli/helpers.js | 134 +++++++++++++++++++++++++++++ common/translations-cli/program.js | 89 +++++++++++++++++++ 3 files changed, 284 insertions(+) create mode 100644 common/translations-cli/extract.js create mode 100644 common/translations-cli/helpers.js create mode 100644 common/translations-cli/program.js diff --git a/common/translations-cli/extract.js b/common/translations-cli/extract.js new file mode 100644 index 000000000..1fa2f67f4 --- /dev/null +++ b/common/translations-cli/extract.js @@ -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>} + */ +async function extractTranslationsFromFile(file) { + /** @type {Record} */ + 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>} + */ +async function extractAllTranslations(directories, extensions) { + try { + /** @type {Record} */ + 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, +}; diff --git a/common/translations-cli/helpers.js b/common/translations-cli/helpers.js new file mode 100644 index 000000000..512e330be --- /dev/null +++ b/common/translations-cli/helpers.js @@ -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} + */ +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} existingTranslations - Previously extracted translations + * @param {Record} 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, +}; diff --git a/common/translations-cli/program.js b/common/translations-cli/program.js new file mode 100644 index 000000000..1ff90b119 --- /dev/null +++ b/common/translations-cli/program.js @@ -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 to search', defaultConfig.directories) + .option('-e, --extensions ', 'file extensions to process', defaultConfig.extensions) + .option('-o, --outputFile ', '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} */ + 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(); From 84bd81e525ec04094f66b9d44f6424da369472a7 Mon Sep 17 00:00:00 2001 From: Nybkox Date: Wed, 19 Feb 2025 12:14:23 +0100 Subject: [PATCH 02/19] feat: throw when found the same translation key with different default values --- common/translations-cli/extract.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/common/translations-cli/extract.js b/common/translations-cli/extract.js index 1fa2f67f4..5131336fd 100644 --- a/common/translations-cli/extract.js +++ b/common/translations-cli/extract.js @@ -37,13 +37,33 @@ async function extractAllTranslations(directories, extensions) { try { /** @type {Record} */ const allTranslations = {}; + /** @type {Record} */ + const translationKeyToFiles = {}; 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); + + for (const key in fileTranslations) { + if (!translationKeyToFiles[key]) { + translationKeyToFiles[key] = []; + } + + translationKeyToFiles[key].push(file); + + if (allTranslations[key] && allTranslations[key] !== fileTranslations[key]) { + console.error( + `Different translations for the same key [${key}] found. ${file}: ${ + fileTranslations[key] + }. Previous value: ${allTranslations[key]} was found in ${translationKeyToFiles[key].join(', ')}` + ); + throw new Error(`Duplicate translation key found: ${key}`); + } + + allTranslations[key] = fileTranslations[key]; + } } } From 1c6ec0f8e34e73a05dd14ea833576ee972084448 Mon Sep 17 00:00:00 2001 From: Nybkox Date: Wed, 19 Feb 2025 12:19:18 +0100 Subject: [PATCH 03/19] refactor: add index.js to translations-cli, add translations:extract to package.json --- common/translations-cli/index.js | 3 +++ common/translations-cli/program.js | 4 ++-- package.json | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 common/translations-cli/index.js diff --git a/common/translations-cli/index.js b/common/translations-cli/index.js new file mode 100644 index 000000000..db840a088 --- /dev/null +++ b/common/translations-cli/index.js @@ -0,0 +1,3 @@ +const { program } = require('./program'); + +program.parse(); diff --git a/common/translations-cli/program.js b/common/translations-cli/program.js index 1ff90b119..15ba3a2ac 100644 --- a/common/translations-cli/program.js +++ b/common/translations-cli/program.js @@ -19,7 +19,7 @@ const { extractAllTranslations } = require('./extract'); const defaultConfig = { extensions: ['.js', '.ts', '.svelte'], directories: ['app', 'packages/web'], - outputFile: './translations/en.json', + outputFile: './translations/en-US.json', }; program.name('dbgate-translations-cli').description('CLI tool for managing translation').version('1.0.0'); @@ -86,4 +86,4 @@ program } }); -program.parse(); +module.exports = { program }; diff --git a/package.json b/package.json index b57953ab0..85f3d8e2c 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "dbgate-serve": "node packages/dbgate/bin/dbgate-serve.js", "workflows": "node common/processWorkflows.js", "cy:open": "cd e2e-tests && yarn cy:open" + "translations:extract": "node common/translations-cli/index.js extract" }, "dependencies": { "concurrently": "^5.1.0", From e9779a3d2fda07eda47a3210f5d137f32ba91495 Mon Sep 17 00:00:00 2001 From: Nybkox Date: Thu, 20 Feb 2025 11:22:01 +0100 Subject: [PATCH 04/19] feat: add add-missing command to translations cli --- common/translations-cli/addMissing.js | 43 +++++++++++++++++++++++ common/translations-cli/helpers.js | 45 ++++++++++++++++++++++++ common/translations-cli/program.js | 49 ++++++++++++++++++++++++--- package.json | 5 +-- 4 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 common/translations-cli/addMissing.js diff --git a/common/translations-cli/addMissing.js b/common/translations-cli/addMissing.js new file mode 100644 index 000000000..f3e2fbf29 --- /dev/null +++ b/common/translations-cli/addMissing.js @@ -0,0 +1,43 @@ +//@ts-check +const { getDefaultTranslations, getLanguageTranslations } = require('./helpers'); + +/** + * @param {string} language + */ +function getMissingTranslations(language) { + const source = getDefaultTranslations(); + /** @type {Record} */ + let target; + + try { + target = getLanguageTranslations(language); + } catch { + console.log(`Language ${language} not found, creating a new one`); + target = {}; + } + + let added = 0; + let removed = 0; + + for (const key in source) { + if (!target[key]) { + target[key] = `*** ${source[key]}`; + added++; + } + } + + for (const key in target) { + if (!source[key]) { + delete target[key]; + removed++; + } + } + + const newLength = Object.keys(target).length; + + return { result: target, stats: { added, removed, newLength } }; +} + +module.exports = { + getMissingTranslations, +}; diff --git a/common/translations-cli/helpers.js b/common/translations-cli/helpers.js index 512e330be..3bcc9180f 100644 --- a/common/translations-cli/helpers.js +++ b/common/translations-cli/helpers.js @@ -122,6 +122,46 @@ const getTranslationChanges = (existingTranslations, newTranslations) => { return { added, removed, updated }; }; +const defaultLanguage = 'en-US'; + +function getDefaultTranslations() { + return getLanguageTranslations(defaultLanguage); +} + +/** + * @param {string} language + * + * @returns {Record} + */ +function getLanguageTranslations(language) { + const file = resolveFile(`translations/${language}.json`); + const content = fs.readFileSync(file, 'utf-8'); + + return JSON.parse(content); +} + +/** + * @param {string} language + * @param {Record} translations + */ +function setLanguageTranslations(language, translations) { + const file = resolveFile(`translations/${language}.json`); + fs.writeFileSync(file, JSON.stringify(translations, null, 2)); +} + +function getAllLanguages() { + const dir = resolveFile('translations'); + + const files = fs.readdirSync(dir); + const languages = files.filter(file => file.endsWith('.json')).map(file => file.replace('.json', '')); + + return languages; +} + +function getAllNonDefaultLanguages() { + return getAllLanguages().filter(language => language !== defaultLanguage); +} + module.exports = { hasValidExtension, getFiles, @@ -131,4 +171,9 @@ module.exports = { resolveExtensions, ensureFileDirExists, getTranslationChanges, + getDefaultTranslations, + getLanguageTranslations, + setLanguageTranslations, + getAllLanguages, + getAllNonDefaultLanguages, }; diff --git a/common/translations-cli/program.js b/common/translations-cli/program.js index 15ba3a2ac..c12532abf 100644 --- a/common/translations-cli/program.js +++ b/common/translations-cli/program.js @@ -7,15 +7,18 @@ const { resolveFile, ensureFileDirExists, getTranslationChanges, + setLanguageTranslations, + getAllNonDefaultLanguages, } = require('./helpers'); const { extractAllTranslations } = require('./extract'); +const { getMissingTranslations } = require('./addMissing'); /** - * @typedef {{ extensions: string[], directories: string[], outputFile: string}} Config - * @typedef {Config & { verbose?: boolean }} Options + * @typedef {{ extensions: string[], directories: string[], outputFile: string}} ExtractConfig + * @typedef {ExtractConfig & { verbose?: boolean }} ExtractOptions */ -/** @type {Config} */ +/** @type {ExtractConfig} */ const defaultConfig = { extensions: ['.js', '.ts', '.svelte'], directories: ['app', 'packages/web'], @@ -31,7 +34,7 @@ program .option('-e, --extensions ', 'file extensions to process', defaultConfig.extensions) .option('-o, --outputFile ', 'output file path', defaultConfig.outputFile) .option('-v, --verbose', 'verbose mode') - .action(async (/** @type {Options} */ options) => { + .action(async (/** @type {ExtractOptions} */ options) => { try { const { directories, extensions, outputFile, verbose } = options; @@ -86,4 +89,42 @@ program } }); +const ALL_LANGUAGES = 'all'; + +/** + * @param {string} target + */ +function addMissingTranslations(target) { + console.log(`Adding missing keys for language: ${target}`); + const { result, stats } = getMissingTranslations(target); + console.log(`Added: ${stats.added}, Removed: ${stats.removed}, Total: ${stats.newLength}`); + setLanguageTranslations(target, result); + console.log(`New translations for ${target} were saved.`); +} + +program + .command('add-missing') + .description('Add missing keys for a langauge to the translation file') + .option('-t, --target ', 'language to add missing translations to', ALL_LANGUAGES) + .action(options => { + try { + const { target } = options; + const languages = getAllNonDefaultLanguages(); + + if (target === ALL_LANGUAGES) { + console.log('Adding missing keys for all languages\n'); + for (const language of languages) { + addMissingTranslations(language); + console.log(); + } + } else { + addMissingTranslations(target); + } + } catch (error) { + console.error(error); + console.error('Error during add-missing:', error.message); + process.exit(1); + } + }); + module.exports = { program }; diff --git a/package.json b/package.json index 85f3d8e2c..4671457a4 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,9 @@ "postinstall": "yarn resetPackagedPlugins && yarn build:lib && patch-package && yarn build:plugins:frontend", "dbgate-serve": "node packages/dbgate/bin/dbgate-serve.js", "workflows": "node common/processWorkflows.js", - "cy:open": "cd e2e-tests && yarn cy:open" - "translations:extract": "node common/translations-cli/index.js extract" + "cy:open": "cd e2e-tests && yarn cy:open", + "translations:extract": "node common/translations-cli/index.js extract", + "translations:add-missing": "node common/translations-cli/index.js add-missing" }, "dependencies": { "concurrently": "^5.1.0", From ea5e2f660bd3b8f79a86bcbaac5a5dc6968bb34f Mon Sep 17 00:00:00 2001 From: Nybkox Date: Thu, 20 Feb 2025 15:09:12 +0100 Subject: [PATCH 05/19] feat: add --removeUnused flag to extract translations --- common/translations-cli/constants.js | 5 +++ common/translations-cli/extract.js | 9 +++-- common/translations-cli/helpers.js | 15 +++++++-- common/translations-cli/program.js | 49 ++++++++++++++++------------ 4 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 common/translations-cli/constants.js diff --git a/common/translations-cli/constants.js b/common/translations-cli/constants.js new file mode 100644 index 000000000..b37a3e98e --- /dev/null +++ b/common/translations-cli/constants.js @@ -0,0 +1,5 @@ +const defaultLanguage = 'en-US'; + +module.exports = { + defaultLanguage, +}; diff --git a/common/translations-cli/extract.js b/common/translations-cli/extract.js index 5131336fd..99ad0633c 100644 --- a/common/translations-cli/extract.js +++ b/common/translations-cli/extract.js @@ -27,13 +27,18 @@ async function extractTranslationsFromFile(file) { return translations; } +/** @typedef {{ ignoreDuplicates?: boolean }} ExtractOptions */ + /** * @param {string[]} directories * @param {string[]} extensions + * @param {ExtractOptions} options * * @returns {Promise>} */ -async function extractAllTranslations(directories, extensions) { +async function extractAllTranslations(directories, extensions, options = {}) { + const { ignoreDuplicates } = options; + try { /** @type {Record} */ const allTranslations = {}; @@ -53,7 +58,7 @@ async function extractAllTranslations(directories, extensions) { translationKeyToFiles[key].push(file); - if (allTranslations[key] && allTranslations[key] !== fileTranslations[key]) { + if (!ignoreDuplicates && allTranslations[key] && allTranslations[key] !== fileTranslations[key]) { console.error( `Different translations for the same key [${key}] found. ${file}: ${ fileTranslations[key] diff --git a/common/translations-cli/helpers.js b/common/translations-cli/helpers.js index 3bcc9180f..c5217bdea 100644 --- a/common/translations-cli/helpers.js +++ b/common/translations-cli/helpers.js @@ -1,6 +1,7 @@ //@ts-check const path = require('path'); const fs = require('fs'); +const { defaultLanguage } = require('./constants'); /** * @param {string} file @@ -122,8 +123,6 @@ const getTranslationChanges = (existingTranslations, newTranslations) => { return { added, removed, updated }; }; -const defaultLanguage = 'en-US'; - function getDefaultTranslations() { return getLanguageTranslations(defaultLanguage); } @@ -149,6 +148,17 @@ function setLanguageTranslations(language, translations) { fs.writeFileSync(file, JSON.stringify(translations, null, 2)); } +/** + * @param {string} language + * @param {Record} newTranslations + */ +function updateLanguageTranslations(language, newTranslations) { + const translations = getLanguageTranslations(language); + const updatedTranslations = { ...translations, ...newTranslations }; + + setLanguageTranslations(language, updatedTranslations); +} + function getAllLanguages() { const dir = resolveFile('translations'); @@ -174,6 +184,7 @@ module.exports = { getDefaultTranslations, getLanguageTranslations, setLanguageTranslations, + updateLanguageTranslations, getAllLanguages, getAllNonDefaultLanguages, }; diff --git a/common/translations-cli/program.js b/common/translations-cli/program.js index c12532abf..3fed3d244 100644 --- a/common/translations-cli/program.js +++ b/common/translations-cli/program.js @@ -9,20 +9,22 @@ const { getTranslationChanges, setLanguageTranslations, getAllNonDefaultLanguages, + updateLanguageTranslations, + getDefaultTranslations, } = require('./helpers'); const { extractAllTranslations } = require('./extract'); const { getMissingTranslations } = require('./addMissing'); +const { defaultLanguage } = require('./constants'); /** - * @typedef {{ extensions: string[], directories: string[], outputFile: string}} ExtractConfig - * @typedef {ExtractConfig & { verbose?: boolean }} ExtractOptions + * @typedef {{ extensions: string[], directories: string[]}} ExtractConfig + * @typedef {ExtractConfig & { verbose?: boolean, removeUnused?: boolean }} ExtractOptions */ /** @type {ExtractConfig} */ const defaultConfig = { extensions: ['.js', '.ts', '.svelte'], directories: ['app', 'packages/web'], - outputFile: './translations/en-US.json', }; program.name('dbgate-translations-cli').description('CLI tool for managing translation').version('1.0.0'); @@ -32,33 +34,25 @@ program .description('Extract translation keys from source files') .option('-d, --directories ', 'directories to search', defaultConfig.directories) .option('-e, --extensions ', 'file extensions to process', defaultConfig.extensions) - .option('-o, --outputFile ', 'output file path', defaultConfig.outputFile) + .option('-r, --removeUnused', 'Remove unused keys from the output file') .option('-v, --verbose', 'verbose mode') .action(async (/** @type {ExtractOptions} */ options) => { try { - const { directories, extensions, outputFile, verbose } = options; + const { directories, extensions, verbose, removeUnused } = options; const resolvedRirectories = resolveDirs(directories); const resolvedExtensions = resolveExtensions(extensions); - const translations = await extractAllTranslations(resolvedRirectories, resolvedExtensions); + const extractedTranslations = await extractAllTranslations(resolvedRirectories, resolvedExtensions); + const defaultTranslations = getDefaultTranslations(); - const resolvedOutputFile = resolveFile(outputFile); - ensureFileDirExists(resolvedOutputFile); - - /** @type {Record} */ - let existingTranslations = {}; - if (fs.existsSync(resolvedOutputFile)) { - existingTranslations = JSON.parse(fs.readFileSync(resolvedOutputFile, 'utf-8')); - } - - const { added, removed, updated } = getTranslationChanges(existingTranslations, translations); + const { added, removed, updated } = getTranslationChanges(defaultTranslations, extractedTranslations); console.log('\nTranslation changes:'); console.log(`- Added: ${added.length} keys`); - console.log(`- Removed: ${removed.length} keys`); + console.log(`- ${removeUnused ? 'Removed' : 'Unused'}: ${removed.length} keys`); console.log(`- Updated: ${updated.length} keys`); - console.log(`- Total: ${Object.keys(translations).length} keys`); + console.log(`- Total: ${Object.keys(extractedTranslations).length} keys`); if (verbose) { if (added.length > 0) { @@ -75,13 +69,26 @@ program console.log('\nUpdated keys:'); updated.forEach(key => { console.log(` ~ ${key}`); - console.log(` Old: ${existingTranslations[key]}`); - console.log(` New: ${translations[key]}`); + console.log(` Old: ${defaultLanguage[key]}`); + console.log(` New: ${extractedTranslations[key]}`); }); } } - fs.writeFileSync(resolvedOutputFile, JSON.stringify(translations, null, 2)); + if (removeUnused) { + console.log('Unused keys were removed.\n'); + setLanguageTranslations(defaultLanguage, extractedTranslations); + } else { + console.log('New translations were saved. Unused keys are kept.\n'); + updateLanguageTranslations(defaultLanguage, extractedTranslations); + + if (verbose) { + console.log('\nUnused keys:'); + for (const key of removed) { + console.log(`${key}: "${defaultTranslations[key]}"`); + } + } + } } catch (error) { console.error(error); console.error('Error during extraction:', error.message); From b7044248cb171c4627e8632d055c00fa871107df Mon Sep 17 00:00:00 2001 From: Nybkox Date: Thu, 20 Feb 2025 15:10:22 +0100 Subject: [PATCH 06/19] feat: add translations api for fe --- packages/web/package.json | 1 + packages/web/rollup.config.js | 2 ++ packages/web/src/translations.ts | 43 ++++++++++++++++++++++++++++++++ packages/web/tsconfig.json | 3 ++- translations/cs-CZ.json | 4 +++ translations/en-US.json | 3 +++ yarn.lock | 28 ++++++++++++++++++++- 7 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 packages/web/src/translations.ts create mode 100644 translations/cs-CZ.json create mode 100644 translations/en-US.json diff --git a/packages/web/package.json b/packages/web/package.json index 8b9bc916e..21212e0f2 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -16,6 +16,7 @@ "@ant-design/colors": "^5.0.0", "@mdi/font": "^7.1.96", "@rollup/plugin-commonjs": "^20.0.0", + "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^13.0.5", "@rollup/plugin-replace": "^3.0.0", "@rollup/plugin-typescript": "^8.2.5", diff --git a/packages/web/rollup.config.js b/packages/web/rollup.config.js index 599ee5632..e0f1c33bf 100644 --- a/packages/web/rollup.config.js +++ b/packages/web/rollup.config.js @@ -8,6 +8,7 @@ import sveltePreprocess from 'svelte-preprocess'; import typescript from '@rollup/plugin-typescript'; import replace from '@rollup/plugin-replace'; import css from 'rollup-plugin-css-only'; +import json from '@rollup/plugin-json'; const production = !process.env.ROLLUP_WATCH; @@ -121,6 +122,7 @@ export default [ sourceMap: !production, inlineSources: !production, }), + json(), // In dev mode, call `npm run start` once // the bundle has been generated diff --git a/packages/web/src/translations.ts b/packages/web/src/translations.ts new file mode 100644 index 000000000..aa8d39143 --- /dev/null +++ b/packages/web/src/translations.ts @@ -0,0 +1,43 @@ +import enUS from '../../../translations/en-US.json'; +import csCZ from '../../../translations/cs-CZ.json'; +import { getStringSettingsValue } from './settings/settingsTools'; + +const translations = { + 'en-US': enUS, + 'cs-CZ': csCZ, +}; + +export function getSelectedLanguage(): string { + const borwserLanguage = getBrowserLanguage(); + + const selectedLanguage = getStringSettingsValue('localization.language', borwserLanguage); + return selectedLanguage; +} + +export function getBrowserLanguage(): string { + if (typeof window !== 'undefined') { + return (navigator.languages && navigator.languages[0]) || navigator.language || 'en-US'; + } + return 'en-US'; +} + +type TranslateOptions = { + defaultMessage: string; + values?: Record; +}; + +export function _t(key: string, options: TranslateOptions): string { + const { defaultMessage } = options; + + const selectedLanguage = getSelectedLanguage(); + const selectedTranslations = translations[selectedLanguage] ?? enUS; + + const translation = selectedTranslations[key]; + + if (!translation) { + console.warn(`Translation not found for key: ${key}. For language: ${selectedLanguage}`); + return defaultMessage; + } + + return translation; +} diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json index 3b82116b5..8ca17f193 100644 --- a/packages/web/tsconfig.json +++ b/packages/web/tsconfig.json @@ -5,6 +5,7 @@ "exclude": ["node_modules/*", "public/*"], "compilerOptions": { + "resolveJsonModule": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, "esModuleInterop": true, @@ -12,7 +13,7 @@ "noImplicitAny": false, "strictNullChecks": false, "strict": false, - "target": "es6", + "target": "es6" // "allowJs": true, // "checkJs": true, } diff --git a/translations/cs-CZ.json b/translations/cs-CZ.json new file mode 100644 index 000000000..c7a1758ff --- /dev/null +++ b/translations/cs-CZ.json @@ -0,0 +1,4 @@ +{ + "settings.localization": "Localizace" +} + diff --git a/translations/en-US.json b/translations/en-US.json new file mode 100644 index 000000000..85edc84d9 --- /dev/null +++ b/translations/en-US.json @@ -0,0 +1,3 @@ +{ + "settings.localization": "Localization" +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8bc12a4dc..74b163fd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1827,6 +1827,13 @@ magic-string "^0.25.7" resolve "^1.17.0" +"@rollup/plugin-json@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-6.1.0.tgz#fbe784e29682e9bb6dee28ea75a1a83702e7b805" + integrity sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA== + dependencies: + "@rollup/pluginutils" "^5.1.0" + "@rollup/plugin-node-resolve@^13.0.5": version "13.3.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c" @@ -1872,6 +1879,15 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@rollup/pluginutils@^5.1.0": + version "5.1.4" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz#bb94f1f9eaaac944da237767cdfee6c5b2262d4a" + integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -2365,6 +2381,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/estree@^1.0.0": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + "@types/fs-extra@^8.0.1": version "8.1.5" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.5.tgz#33aae2962d3b3ec9219b5aca2555ee00274f5927" @@ -4979,7 +5000,7 @@ estree-walker@^1.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== -estree-walker@^2.0.1: +estree-walker@^2.0.1, estree-walker@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== @@ -9602,6 +9623,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatc resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" From 93a1c593feddb82714bcdee903d2fbfa1e78201f Mon Sep 17 00:00:00 2001 From: Nybkox Date: Thu, 20 Feb 2025 15:15:40 +0100 Subject: [PATCH 07/19] feat: add basic language switch to settings --- packages/web/src/settings/SettingsModal.svelte | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte index 1b013dc5d..7e9e609c1 100644 --- a/packages/web/src/settings/SettingsModal.svelte +++ b/packages/web/src/settings/SettingsModal.svelte @@ -39,6 +39,8 @@ import { derived } from 'svelte/store'; import { safeFormatDate } from 'dbgate-tools'; import FormDefaultActionField from './FormDefaultActionField.svelte'; + import { _t } from '../translations'; + import { internalRedirectTo } from '../clientAuth'; const electron = getElectron(); let restartWarning = false; @@ -122,6 +124,21 @@ ORDER BY {/if} +
{_t('settings.localization', { defaultMessage: 'Localization' })}
+ { + setTimeout(() => { + internalRedirectTo('/'); + }, 100); + }} + />
Data grid
Date: Thu, 20 Feb 2025 16:19:46 +0100 Subject: [PATCH 08/19] feat: separate remove-unused command --- common/translations-cli/constants.js | 9 +++++ common/translations-cli/extract.js | 2 -- common/translations-cli/program.js | 38 +++++++++++++------- common/translations-cli/removeUnused.js | 46 +++++++++++++++++++++++++ package.json | 3 +- 5 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 common/translations-cli/removeUnused.js diff --git a/common/translations-cli/constants.js b/common/translations-cli/constants.js index b37a3e98e..388ea082c 100644 --- a/common/translations-cli/constants.js +++ b/common/translations-cli/constants.js @@ -1,5 +1,14 @@ const defaultLanguage = 'en-US'; +/** @typedef {{ extensions: string[], directories: string[]}} ExtractConfig + +/** @type {ExtractConfig} */ +const defaultExtractConfig = { + extensions: ['.js', '.ts', '.svelte'], + directories: ['app', 'packages/web'], +}; + module.exports = { defaultLanguage, + defaultExtractConfig, }; diff --git a/common/translations-cli/extract.js b/common/translations-cli/extract.js index 99ad0633c..9c6374594 100644 --- a/common/translations-cli/extract.js +++ b/common/translations-cli/extract.js @@ -72,8 +72,6 @@ async function extractAllTranslations(directories, extensions, options = {}) { } } - console.log(`Total translations found: ${Object.keys(allTranslations).length}`); - return allTranslations; } catch (error) { console.error('Error extracting translations:', error); diff --git a/common/translations-cli/program.js b/common/translations-cli/program.js index 3fed3d244..532450c5d 100644 --- a/common/translations-cli/program.js +++ b/common/translations-cli/program.js @@ -4,8 +4,6 @@ const { program } = require('commander'); const { resolveDirs, resolveExtensions, - resolveFile, - ensureFileDirExists, getTranslationChanges, setLanguageTranslations, getAllNonDefaultLanguages, @@ -14,26 +12,20 @@ const { } = require('./helpers'); const { extractAllTranslations } = require('./extract'); const { getMissingTranslations } = require('./addMissing'); -const { defaultLanguage } = require('./constants'); +const { defaultLanguage, defaultExtractConfig } = require('./constants'); +const { removeUnusedAllTranslations, removeUnusedForSignelLanguage } = require('./removeUnused'); /** - * @typedef {{ extensions: string[], directories: string[]}} ExtractConfig - * @typedef {ExtractConfig & { verbose?: boolean, removeUnused?: boolean }} ExtractOptions + * @typedef {import('./constants').ExtractConfig & { verbose?: boolean, removeUnused?: boolean }} ExtractOptions */ -/** @type {ExtractConfig} */ -const defaultConfig = { - extensions: ['.js', '.ts', '.svelte'], - directories: ['app', 'packages/web'], -}; - 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 to search', defaultConfig.directories) - .option('-e, --extensions ', 'file extensions to process', defaultConfig.extensions) + .option('-d, --directories ', 'directories to search', defaultExtractConfig.directories) + .option('-e, --extensions ', 'file extensions to process', defaultExtractConfig.extensions) .option('-r, --removeUnused', 'Remove unused keys from the output file') .option('-v, --verbose', 'verbose mode') .action(async (/** @type {ExtractOptions} */ options) => { @@ -134,4 +126,24 @@ program } }); +program + .command('remove-unused') + .description('Remove unused keys from the translation files') + .option('-t, --target ', 'language to add missing translations to', ALL_LANGUAGES) + .action(async options => { + try { + const { target } = options; + if (target === ALL_LANGUAGES) { + console.log('Removing unused keys from all languages\n'); + await removeUnusedAllTranslations(); + } else { + await removeUnusedForSignelLanguage(target); + } + } catch (error) { + console.error(error); + console.error('Error during add-missing:', error.message); + process.exit(1); + } + }); + module.exports = { program }; diff --git a/common/translations-cli/removeUnused.js b/common/translations-cli/removeUnused.js new file mode 100644 index 000000000..0894fccea --- /dev/null +++ b/common/translations-cli/removeUnused.js @@ -0,0 +1,46 @@ +// @ts-check +const { defaultExtractConfig } = require('./constants'); +const { extractAllTranslations } = require('./extract'); +const { getLanguageTranslations, getAllLanguages, setLanguageTranslations } = require('./helpers'); + +const { directories, extensions } = defaultExtractConfig; + +/** + * @param {string} language + * @param {Record} source + */ +function getUsedTranslations(language, source) { + const languageTranslations = getLanguageTranslations(language); + + for (const key in languageTranslations) { + if (!(key in source)) { + delete languageTranslations[key]; + } + } + + return languageTranslations; +} + +async function removeUnusedAllTranslations() { + const source = await extractAllTranslations(directories, extensions); + const languages = getAllLanguages(); + + for (const language of languages) { + const newTranslations = getUsedTranslations(language, source); + setLanguageTranslations(language, newTranslations); + } +} + +/** + * @param {string} language + */ +async function removeUnusedForSignelLanguage(language) { + const source = await extractAllTranslations(directories, extensions); + const newTranslations = getUsedTranslations(language, source); + setLanguageTranslations(language, newTranslations); +} + +module.exports = { + removeUnusedAllTranslations, + removeUnusedForSignelLanguage, +}; diff --git a/package.json b/package.json index 4671457a4..c0d33953c 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "workflows": "node common/processWorkflows.js", "cy:open": "cd e2e-tests && yarn cy:open", "translations:extract": "node common/translations-cli/index.js extract", - "translations:add-missing": "node common/translations-cli/index.js add-missing" + "translations:add-missing": "node common/translations-cli/index.js add-missing", + "translations:remove-unused": "node common/translations-cli/index.js remove-unused" }, "dependencies": { "concurrently": "^5.1.0", From 5396b3f1fbd75577b60575dccdc6cc0e0a142cc1 Mon Sep 17 00:00:00 2001 From: Nybkox Date: Thu, 20 Feb 2025 22:15:09 +0100 Subject: [PATCH 09/19] feat: add translations:check command --- common/translations-cli/program.js | 13 +++++++++++++ package.json | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/common/translations-cli/program.js b/common/translations-cli/program.js index 532450c5d..d814c296e 100644 --- a/common/translations-cli/program.js +++ b/common/translations-cli/program.js @@ -146,4 +146,17 @@ program } }); +program + .command('check') + .description('Check if there are multiple default values for the same key') + .action(async () => { + try { + await extractAllTranslations(defaultExtractConfig.directories, defaultExtractConfig.extensions); + } catch (error) { + console.error(error); + console.error('Error during check:', error.message); + process.exit(1); + } + }); + module.exports = { program }; diff --git a/package.json b/package.json index c0d33953c..7314acb9f 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "cy:open": "cd e2e-tests && yarn cy:open", "translations:extract": "node common/translations-cli/index.js extract", "translations:add-missing": "node common/translations-cli/index.js add-missing", - "translations:remove-unused": "node common/translations-cli/index.js remove-unused" + "translations:remove-unused": "node common/translations-cli/index.js remove-unused", + "translations:check": "node common/translations-cli/index.js check" }, "dependencies": { "concurrently": "^5.1.0", From 3e6aab6b00789106e0c7ef2121f87ff6047c281b Mon Sep 17 00:00:00 2001 From: Nybkox Date: Thu, 20 Feb 2025 22:16:27 +0100 Subject: [PATCH 10/19] feat: basic translations to ui --- packages/web/index.html.tpl | 1 - packages/web/src/App.svelte | 10 ++- .../web/src/appobj/ConnectionAppObject.svelte | 3 +- packages/web/src/datagrid/DataGridCore.svelte | 70 ++++++++----------- .../src/elements/ObjectFieldsEditor.svelte | 3 +- packages/web/src/formview/FormView.svelte | 3 +- .../web/src/impexp/SourceTargetConfig.svelte | 27 +++++-- packages/web/src/modals/EditJsonModal.svelte | 3 +- .../web/src/modals/SaveArchiveModal.svelte | 3 +- packages/web/src/modals/SaveFileModal.svelte | 3 +- .../src/tableeditor/ColumnEditorModal.svelte | 3 +- .../tableeditor/ForeignKeyEditorModal.svelte | 3 +- packages/web/src/tabpanel/TabsPanel.svelte | 22 +++--- packages/web/src/tabs/ConnectionTab.svelte | 7 +- packages/web/src/tabs/DbKeyDetailTab.svelte | 9 ++- packages/web/src/tabs/ServerSummaryTab.svelte | 3 +- .../web/src/widgets/DatabaseWidget.svelte | 29 +++++--- packages/web/src/widgets/FilesWidget.svelte | 1 + .../web/src/widgets/SchemaSelector.svelte | 26 +++++-- translations/cs-CZ.json | 60 +++++++++++++++- translations/en-US.json | 57 ++++++++++++++- 21 files changed, 253 insertions(+), 93 deletions(-) diff --git a/packages/web/index.html.tpl b/packages/web/index.html.tpl index b188f34f9..1dfe12c64 100644 --- a/packages/web/index.html.tpl +++ b/packages/web/index.html.tpl @@ -115,7 +115,6 @@
-
Loading DbGate App
diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 66d95e176..7d9ffc07f 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -22,6 +22,7 @@ import SettingsListener from './utility/SettingsListener.svelte'; import { handleAuthOnStartup } from './clientAuth'; import { initializeAppUpdates } from './utility/appUpdate'; + import { _t } from './translations'; export let isAdminPage = false; @@ -95,10 +96,13 @@ {:else} {/if} {:else} - + {/if} diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte index a49e3d553..2c7aa23be 100644 --- a/packages/web/src/appobj/ConnectionAppObject.svelte +++ b/packages/web/src/appobj/ConnectionAppObject.svelte @@ -137,6 +137,7 @@ import hasPermission from '../utility/hasPermission'; import { switchCurrentDatabase } from '../utility/common'; import { getConnectionClickActionSetting } from '../settings/settingsTools'; + import { _t } from '../translations'; export let data; export let passProps; @@ -324,7 +325,7 @@ hasPermission(`dbops/query`) && { onClick: handleNewQuery, text: 'New Query (server)', isNewQuery: true }, $openedConnections.includes(data._id) && data.status && { - text: 'Refresh', + text: _t('common.refresh', { defaultMessage: 'Refresh' }), onClick: handleRefresh, }, hasPermission(`dbops/createdb`) && diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index c4d457cff..f3bc18c46 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -4,7 +4,7 @@ registerCommand({ id: 'dataGrid.refresh', category: 'Data grid', - name: 'Refresh', + name: _t('common.refresh', { defaultMessage: 'Refresh' }), keyText: 'F5 | CtrlOrCommand+R', toolbar: true, isRelatedToTab: true, @@ -28,7 +28,7 @@ registerCommand({ id: 'dataGrid.revertRowChanges', category: 'Data grid', - name: 'Revert row changes', + name: _t('command.data_grid.revert_row_changes', { defaultMessage: 'Revert row changes' }), keyText: 'CtrlOrCommand+U', testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges, onClick: () => getCurrentDataGrid().revertRowChanges(), @@ -37,8 +37,8 @@ registerCommand({ id: 'dataGrid.revertAllChanges', category: 'Data grid', - name: 'Revert all changes', - toolbarName: 'Revert all', + name: _t('command.data_grid.revert_all_changes.name', { defaultMessage: 'Revert all changes' }), + toolbarName: _t('command.data_grid.revert_all_changes.toolbar_name', { defaultMessage: 'Revert all' }), icon: 'icon undo', testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges, onClick: () => getCurrentDataGrid().revertAllChanges(), @@ -47,8 +47,8 @@ registerCommand({ id: 'dataGrid.deleteSelectedRows', category: 'Data grid', - name: 'Delete selected rows', - toolbarName: 'Delete row(s)', + name: _t('command.datagrid.delete_selected_rows.name', { defaultMessage: 'Delete selected rows' }), + toolbarName: _t('command.datagrid.delete_selected_rows.toolbar_name', { defaultMessage: 'Delete row(s)' }), keyText: isMac() ? 'Command+Backspace' : 'CtrlOrCommand+Delete', icon: 'icon minus', testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable, @@ -58,8 +58,8 @@ registerCommand({ id: 'dataGrid.insertNewRow', category: 'Data grid', - name: 'Insert new row', - toolbarName: 'New row', + name: _t('command.datagrid.insert_new_row.name', { defaultMessage: 'Insert new row' }), + toolbarName: _t('command.datagrid.insert_new_row.toolbar_name', { defaultMessage: 'New row' }), icon: 'icon add', keyText: isMac() ? 'Command+I' : 'Insert', testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable, @@ -69,8 +69,8 @@ registerCommand({ id: 'dataGrid.addNewColumn', category: 'Data grid', - name: 'Add new column', - toolbarName: 'New column', + name: _t('command.datagrid.add_new_column.name', { defaultMessage: 'Add new column' }), + toolbarName: _t('command.datagrid.add_new_column.toolbar_name', { defaultMessage: 'New column' }), icon: 'icon add-column', testEnabled: () => getCurrentDataGrid()?.addNewColumnEnabled(), onClick: () => getCurrentDataGrid().addNewColumn(), @@ -79,8 +79,8 @@ registerCommand({ id: 'dataGrid.cloneRows', category: 'Data grid', - name: 'Clone rows', - toolbarName: 'Clone row(s)', + name: _t('command.datagrid.clone_rows.name', { defaultMessage: 'Clone rows' }), + toolbarName: _t('command.datagrid.clone_rows.toolbar_name', { defaultMessage: 'Clone row(s)' }), keyText: 'CtrlOrCommand+Shift+C', testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable, onClick: () => getCurrentDataGrid().cloneRows(), @@ -89,7 +89,7 @@ registerCommand({ id: 'dataGrid.setNull', category: 'Data grid', - name: 'Set NULL', + name: _t('command.datagrid.set_null.name', { defaultMessage: 'Set NULL' }), keyText: 'CtrlOrCommand+0', testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable && !getCurrentDataGrid()?.getEditorTypes()?.supportFieldRemoval, @@ -99,7 +99,7 @@ registerCommand({ id: 'dataGrid.removeField', category: 'Data grid', - name: 'Remove field', + name: _t('command.datagrid.remove_field.name', { defaultMessage: 'Remove field' }), keyText: 'CtrlOrCommand+0', testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable && getCurrentDataGrid()?.getEditorTypes()?.supportFieldRemoval, @@ -109,7 +109,7 @@ registerCommand({ id: 'dataGrid.undo', category: 'Data grid', - name: 'Undo', + name: _t('command.datagrid.undo.name', { defaultMessage: 'Undo' }), group: 'undo', icon: 'icon undo', toolbar: true, @@ -121,7 +121,7 @@ registerCommand({ id: 'dataGrid.redo', category: 'Data grid', - name: 'Redo', + name: _t('command.datagrid.redo.name', { defaultMessage: 'Redo' }), group: 'redo', icon: 'icon redo', toolbar: true, @@ -133,7 +133,7 @@ registerCommand({ id: 'dataGrid.reconnect', category: 'Data grid', - name: 'Reconnect', + name: _t('command.datagrid.reconnect.name', { defaultMessage: 'Reconnect' }), testEnabled: () => getCurrentDataGrid() != null, onClick: () => getCurrentDataGrid().reconnect(), }); @@ -141,7 +141,7 @@ registerCommand({ id: 'dataGrid.copyToClipboard', category: 'Data grid', - name: 'Copy to clipboard', + name: _t('command.datagrid.copy_to_clipboard.name', { defaultMessage: 'Copy to clipboard' }), keyText: 'CtrlOrCommand+C', disableHandleKeyText: 'CtrlOrCommand+C', testEnabled: () => getCurrentDataGrid() != null, @@ -152,7 +152,7 @@ id: 'dataGrid.editJsonDocument', category: 'Data grid', keyText: 'CtrlOrCommand+J', - name: 'Edit row as JSON document', + name: _t('command.datagrid.edit_json_document.name', { defaultMessage: 'Edit row as JSON document' }), testEnabled: () => getCurrentDataGrid()?.editJsonEnabled(), onClick: () => getCurrentDataGrid().editJsonDocument(), }); @@ -160,15 +160,15 @@ registerCommand({ id: 'dataGrid.openSelectionInMap', category: 'Data grid', - name: 'Open selection in map', - testEnabled: () => getCurrentDataGrid() != null, // ?.openSelectionInMapEnabled(), + name: _t('command.datagrid.open_selection_in_map.name', { defaultMessage: 'Open selection in map' }), + testEnabled: () => getCurrentDataGrid() != null, onClick: () => getCurrentDataGrid().openSelectionInMap(), }); registerCommand({ id: 'dataGrid.viewJsonDocument', category: 'Data grid', - name: 'View row as JSON document', + name: _t('command.datagrid.view_json_document.name', { defaultMessage: 'View row as JSON document' }), testEnabled: () => getCurrentDataGrid()?.viewJsonDocumentEnabled(), onClick: () => getCurrentDataGrid().viewJsonDocument(), }); @@ -176,7 +176,7 @@ registerCommand({ id: 'dataGrid.viewJsonValue', category: 'Data grid', - name: 'View cell as JSON document', + name: _t('command.datagrid.view_json_value.name', { defaultMessage: 'View cell as JSON document' }), testEnabled: () => getCurrentDataGrid()?.viewJsonValueEnabled(), onClick: () => getCurrentDataGrid().viewJsonValue(), }); @@ -184,7 +184,7 @@ registerCommand({ id: 'dataGrid.openJsonArrayInSheet', category: 'Data grid', - name: 'Open array as table', + name: _t('command.datagrid.open_json_array_in_sheet.name', { defaultMessage: 'Open array as table' }), testEnabled: () => getCurrentDataGrid()?.openJsonArrayInSheetEnabled(), onClick: () => getCurrentDataGrid().openJsonArrayInSheet(), }); @@ -192,7 +192,7 @@ registerCommand({ id: 'dataGrid.saveCellToFile', category: 'Data grid', - name: 'Save cell to file', + name: _t('command.datagrid.save_cell_to_file.name', { defaultMessage: 'Save cell to file' }), testEnabled: () => getCurrentDataGrid()?.saveCellToFileEnabled(), onClick: () => getCurrentDataGrid().saveCellToFile(), }); @@ -200,7 +200,7 @@ registerCommand({ id: 'dataGrid.loadCellFromFile', category: 'Data grid', - name: 'Load cell from file', + name: _t('command.datagrid.load_cell_from_file.name', { defaultMessage: 'Load cell from file' }), testEnabled: () => getCurrentDataGrid()?.loadCellFromFileEnabled(), onClick: () => getCurrentDataGrid().loadCellFromFile(), }); @@ -212,7 +212,8 @@ // testEnabled: () => getCurrentDataGrid()?.copyJsonEnabled(), // onClick: () => getCurrentDataGrid().copyJsonDocument(), // }); - + // + // registerCommand({ id: 'dataGrid.filterSelected', category: 'Data grid', @@ -221,7 +222,6 @@ testEnabled: () => getCurrentDataGrid()?.getDisplay().filterable, onClick: () => getCurrentDataGrid().filterSelectedValue(), }); - registerCommand({ id: 'dataGrid.findColumn', category: 'Data grid', @@ -230,7 +230,6 @@ testEnabled: () => getCurrentDataGrid() != null, getSubCommands: () => getCurrentDataGrid().buildFindMenu(), }); - registerCommand({ id: 'dataGrid.hideColumn', category: 'Data grid', @@ -239,7 +238,6 @@ testEnabled: () => getCurrentDataGrid()?.canShowLeftPanel(), onClick: () => getCurrentDataGrid().hideColumn(), }); - registerCommand({ id: 'dataGrid.clearFilter', category: 'Data grid', @@ -248,7 +246,6 @@ testEnabled: () => getCurrentDataGrid()?.clearFilterEnabled(), onClick: () => getCurrentDataGrid().clearFilter(), }); - registerCommand({ id: 'dataGrid.generateSqlFromData', category: 'Data grid', @@ -257,7 +254,6 @@ testEnabled: () => getCurrentDataGrid()?.generateSqlFromDataEnabled(), onClick: () => getCurrentDataGrid().generateSqlFromData(), }); - registerCommand({ id: 'dataGrid.openFreeTable', category: 'Data grid', @@ -265,7 +261,6 @@ testEnabled: () => getCurrentDataGrid() != null, onClick: () => getCurrentDataGrid().openFreeTable(), }); - registerCommand({ id: 'dataGrid.openChartFromSelection', category: 'Data grid', @@ -273,7 +268,6 @@ testEnabled: () => getCurrentDataGrid() != null, onClick: () => getCurrentDataGrid().openChartFromSelection(), }); - registerCommand({ id: 'dataGrid.newJson', category: 'Data grid', @@ -281,7 +275,6 @@ testEnabled: () => getCurrentDataGrid()?.addJsonDocumentEnabled(), onClick: () => getCurrentDataGrid().addJsonDocument(), }); - registerCommand({ id: 'dataGrid.editCellValue', category: 'Data grid', @@ -289,7 +282,6 @@ testEnabled: () => getCurrentDataGrid()?.editCellValueEnabled(), onClick: () => getCurrentDataGrid().editCellValue(), }); - registerCommand({ id: 'dataGrid.mergeSelectedCellsIntoMirror', category: 'Data grid', @@ -297,7 +289,6 @@ testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true), onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'merge', fullRows: false }), }); - registerCommand({ id: 'dataGrid.mergeSelectedRowsIntoMirror', category: 'Data grid', @@ -305,7 +296,6 @@ testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true), onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'merge', fullRows: true }), }); - registerCommand({ id: 'dataGrid.appendSelectedCellsIntoMirror', category: 'Data grid', @@ -313,7 +303,6 @@ testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true), onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'append', fullRows: false }), }); - registerCommand({ id: 'dataGrid.appendSelectedRowsIntoMirror', category: 'Data grid', @@ -321,7 +310,6 @@ testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true), onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'append', fullRows: true }), }); - registerCommand({ id: 'dataGrid.replaceSelectedCellsIntoMirror', category: 'Data grid', @@ -329,7 +317,6 @@ testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true), onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'replace', fullRows: false }), }); - registerCommand({ id: 'dataGrid.replaceSelectedRowsIntoMirror', category: 'Data grid', @@ -430,6 +417,7 @@ import { openJsonLinesData } from '../utility/openJsonLinesData'; import contextMenuActivator from '../utility/contextMenuActivator'; import InputTextModal from '../modals/InputTextModal.svelte'; + import { _t } from '../translations'; export let onLoadNextData = undefined; export let grider = undefined; diff --git a/packages/web/src/elements/ObjectFieldsEditor.svelte b/packages/web/src/elements/ObjectFieldsEditor.svelte index 684535848..a85e2b1ad 100644 --- a/packages/web/src/elements/ObjectFieldsEditor.svelte +++ b/packages/web/src/elements/ObjectFieldsEditor.svelte @@ -7,6 +7,7 @@ import FormTextField from '../forms/FormTextField.svelte'; import FormSelectField from '../forms/FormSelectField.svelte'; import stableStringify from 'json-stable-stringify'; + import { _t } from '../translations'; export let title; export let fieldDefinitions; @@ -42,7 +43,7 @@ ({ label: x.schemaName, value: x.schemaName }))} /> {/if} diff --git a/packages/web/src/formview/FormView.svelte b/packages/web/src/formview/FormView.svelte index e14730288..612cecb54 100644 --- a/packages/web/src/formview/FormView.svelte +++ b/packages/web/src/formview/FormView.svelte @@ -15,7 +15,7 @@ registerCommand({ id: 'dataForm.refresh', category: 'Data form', - name: 'Refresh', + name: _t('common.refresh', { defaultMessage: 'Refresh' }), keyText: 'F5 | CtrlOrCommand+R', toolbar: true, isRelatedToTab: true, @@ -197,6 +197,7 @@ import resizeObserver from '../utility/resizeObserver'; import openReferenceForm from './openReferenceForm'; import { useSettings } from '../utility/metadataLoaders'; + import { _t } from '../translations'; export let conid; export let database; diff --git a/packages/web/src/impexp/SourceTargetConfig.svelte b/packages/web/src/impexp/SourceTargetConfig.svelte index d78c69f23..5418aedfd 100644 --- a/packages/web/src/impexp/SourceTargetConfig.svelte +++ b/packages/web/src/impexp/SourceTargetConfig.svelte @@ -22,6 +22,7 @@ import FormTablesSelect from './FormTablesSelect.svelte'; import { findEngineDriver } from 'dbgate-tools'; import AceEditor from '../query/AceEditor.svelte'; + import { _t } from '../translations'; export let direction; export let storageTypeField; @@ -40,14 +41,22 @@ $values[storageTypeField] == 'jsldata' ? [{ value: 'jsldata', label: 'Query result data', directions: ['source'] }] : [ - { value: 'database', label: 'Database', directions: ['source', 'target'] }, + { + value: 'database', + label: _t('common.database', { defaultMessage: 'Database' }), + directions: ['source', 'target'], + }, ...$extensions.fileFormats.map(format => ({ value: format.storageType, label: `${format.name} files(s)`, directions: getFileFormatDirections(format), })), - { value: 'query', label: 'Query', directions: ['source'] }, - { value: 'archive', label: 'Archive', directions: ['source', 'target'] }, + { value: 'query', label: _t('common.query', { defaultMessage: 'Query' }), directions: ['source'] }, + { + value: 'archive', + label: _t('common.archive', { defaultMessage: 'Archive' }), + directions: ['source', 'target'], + }, ]; $: storageType = $values[storageTypeField]; @@ -124,7 +133,7 @@ conidName={connectionIdField} databaseName={databaseNameField} name={schemaNameField} - label="Schema" + label={_t('common.schema', { defaultMessage: 'Schema' })} /> {#if tablesField} {/if} {/if} {#if storageType == 'query'} -
Query
+
{_t('common.query', { defaultMessage: 'Query' })}
{#if $values.sourceQueryType == 'json'} setFieldValue('sourceQuery', e.detail)} mode="json" /> @@ -156,7 +165,11 @@ {/if} {#if storageType == 'archive' && direction == 'source'} - + {/if} {#if format && direction == 'source'} diff --git a/packages/web/src/modals/EditJsonModal.svelte b/packages/web/src/modals/EditJsonModal.svelte index be2dce8af..01ecb7191 100644 --- a/packages/web/src/modals/EditJsonModal.svelte +++ b/packages/web/src/modals/EditJsonModal.svelte @@ -9,6 +9,7 @@ import ModalBase from './ModalBase.svelte'; import { closeCurrentModal, showModal } from './modalTools'; + import { _t } from '../translations'; export let onSave; export let json; @@ -43,7 +44,7 @@
{ try { diff --git a/packages/web/src/modals/SaveArchiveModal.svelte b/packages/web/src/modals/SaveArchiveModal.svelte index ad6ba355c..bc2d0c372 100644 --- a/packages/web/src/modals/SaveArchiveModal.svelte +++ b/packages/web/src/modals/SaveArchiveModal.svelte @@ -6,6 +6,7 @@ import FormSubmit from '../forms/FormSubmit.svelte'; import FormTextField from '../forms/FormTextField.svelte'; import { currentArchive } from '../stores'; + import { _t } from '../translations'; import ModalBase from './ModalBase.svelte'; import { closeCurrentModal } from './modalTools'; @@ -28,7 +29,7 @@ - + diff --git a/packages/web/src/modals/SaveFileModal.svelte b/packages/web/src/modals/SaveFileModal.svelte index 76d1eafdc..c324028e1 100644 --- a/packages/web/src/modals/SaveFileModal.svelte +++ b/packages/web/src/modals/SaveFileModal.svelte @@ -4,6 +4,7 @@ import FormProvider from '../forms/FormProvider.svelte'; import FormSubmit from '../forms/FormSubmit.svelte'; import FormTextField from '../forms/FormTextField.svelte'; + import { _t } from '../translations'; import { apiCall } from '../utility/api'; import getElectron from '../utility/getElectron'; @@ -56,7 +57,7 @@ Save file - + {#if electron} { closeCurrentModal(); diff --git a/packages/web/src/tableeditor/ForeignKeyEditorModal.svelte b/packages/web/src/tableeditor/ForeignKeyEditorModal.svelte index 72895ddd9..7d4c82871 100644 --- a/packages/web/src/tableeditor/ForeignKeyEditorModal.svelte +++ b/packages/web/src/tableeditor/ForeignKeyEditorModal.svelte @@ -17,6 +17,7 @@ import TextField from '../forms/TextField.svelte'; import SelectField from '../forms/SelectField.svelte'; import _ from 'lodash'; + import { _t } from '../translations'; export let constraintInfo; export let setTableInfo; @@ -204,7 +205,7 @@ { closeCurrentModal(); diff --git a/packages/web/src/tabpanel/TabsPanel.svelte b/packages/web/src/tabpanel/TabsPanel.svelte index 2e194dd8d..73a2144c3 100644 --- a/packages/web/src/tabpanel/TabsPanel.svelte +++ b/packages/web/src/tabpanel/TabsPanel.svelte @@ -1,4 +1,6 @@