diff --git a/.gitignore b/.gitignore index 979153806..a979e82c8 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ packages/api/src/packagedPluginsContent.js packages/web/public/*.html e2e-tests/screenshots/*.png my_guitar_shop.db +.aider* 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/constants.js b/common/translations-cli/constants.js new file mode 100644 index 000000000..4dd042072 --- /dev/null +++ b/common/translations-cli/constants.js @@ -0,0 +1,16 @@ +// @ts-check +// +const defaultLanguage = 'en'; + +/** @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 new file mode 100644 index 000000000..9c6374594 --- /dev/null +++ b/common/translations-cli/extract.js @@ -0,0 +1,84 @@ +//@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; +} + +/** @typedef {{ ignoreDuplicates?: boolean }} ExtractOptions */ + +/** + * @param {string[]} directories + * @param {string[]} extensions + * @param {ExtractOptions} options + * + * @returns {Promise>} + */ +async function extractAllTranslations(directories, extensions, options = {}) { + const { ignoreDuplicates } = options; + + 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); + + for (const key in fileTranslations) { + if (!translationKeyToFiles[key]) { + translationKeyToFiles[key] = []; + } + + translationKeyToFiles[key].push(file); + + if (!ignoreDuplicates && 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]; + } + } + } + + 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..b153eb6f9 --- /dev/null +++ b/common/translations-cli/helpers.js @@ -0,0 +1,194 @@ +//@ts-check +const path = require('path'); +const fs = require('fs'); +const { defaultLanguage } = require('./constants'); +const sortJsonKeysAlphabetically = require('./sortJsonKeysAlphabetically'); + +/** + * @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 }; +}; + +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`); + const sorted = sortJsonKeysAlphabetically(translations); + + fs.writeFileSync(file, JSON.stringify(sorted, null, 2)); +} + +/** + * @param {string} language + * @param {Record} newTranslations + */ +function updateLanguageTranslations(language, newTranslations) { + const translations = getLanguageTranslations(language); + const updatedTranslations = { ...translations, ...newTranslations }; + const sorted = sortJsonKeysAlphabetically(updatedTranslations); + + setLanguageTranslations(language, sorted); +} + +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, + formatDefaultValue, + resolveFile, + resolveDirs, + resolveExtensions, + ensureFileDirExists, + getTranslationChanges, + getDefaultTranslations, + getLanguageTranslations, + setLanguageTranslations, + updateLanguageTranslations, + getAllLanguages, + getAllNonDefaultLanguages, +}; 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 new file mode 100644 index 000000000..7233641b7 --- /dev/null +++ b/common/translations-cli/program.js @@ -0,0 +1,163 @@ +//@ts-check +const fs = require('fs'); +const { program } = require('commander'); +const { + resolveDirs, + resolveExtensions, + getTranslationChanges, + setLanguageTranslations, + getAllNonDefaultLanguages, + updateLanguageTranslations, + getDefaultTranslations, +} = require('./helpers'); +const { extractAllTranslations } = require('./extract'); +const { getMissingTranslations } = require('./addMissing'); +const { defaultLanguage, defaultExtractConfig } = require('./constants'); +const { removeUnusedAllTranslations, removeUnusedForSignelLanguage } = require('./removeUnused'); + +/** + * @typedef {import('./constants').ExtractConfig & { verbose?: boolean, ignoreUnused?: boolean }} ExtractOptions + */ + +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', defaultExtractConfig.directories) + .option('-e, --extensions ', 'file extensions to process', defaultExtractConfig.extensions) + .option('-r, --ignoreUnused', 'Ignore unused keys in the output file') + .option('-v, --verbose', 'verbose mode') + .action(async (/** @type {ExtractOptions} */ options) => { + try { + const { directories, extensions, verbose, ignoreUnused } = options; + + const resolvedRirectories = resolveDirs(directories); + const resolvedExtensions = resolveExtensions(extensions); + + const extractedTranslations = await extractAllTranslations(resolvedRirectories, resolvedExtensions); + const defaultTranslations = getDefaultTranslations(); + + const { added, removed, updated } = getTranslationChanges(defaultTranslations, extractedTranslations); + + console.log('\nTranslation changes:'); + console.log(`- Added: ${added.length} keys`); + console.log(`- ${ignoreUnused ? 'Unused' : 'Removed'}: ${removed.length} keys`); + console.log(`- Updated: ${updated.length} keys`); + console.log(`- Total: ${Object.keys(extractedTranslations).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: ${defaultLanguage[key]}`); + console.log(` New: ${extractedTranslations[key]}`); + }); + } + } + + if (ignoreUnused) { + 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]}"`); + } + } + } else { + console.log('Unused keys were removed.\n'); + setLanguageTranslations(defaultLanguage, extractedTranslations); + } + } catch (error) { + console.error(error); + console.error('Error during extraction:', error.message); + process.exit(1); + } + }); + +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); + } + }); + +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); + } + }); + +program + .command('check') + .description('Check if there are multiple default values for the same key') + .action(async () => { + try { + await extractAllTranslations(defaultExtractConfig.directories, defaultExtractConfig.extensions); + console.log('No problems found while extracting translations.'); + } catch (error) { + console.error(error); + console.error('Error during check:', 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/common/translations-cli/sortJsonKeysAlphabetically.js b/common/translations-cli/sortJsonKeysAlphabetically.js new file mode 100644 index 000000000..17ba9f326 --- /dev/null +++ b/common/translations-cli/sortJsonKeysAlphabetically.js @@ -0,0 +1,24 @@ +// @ts-check + +/** + * @param {object|string} json + * @returns {object} + */ +function sortJsonKeysAlphabetically(json) { + const obj = typeof json === 'string' ? JSON.parse(json) : json; + + if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) { + return obj; + } + + const sortedObj = Object.keys(obj) + .sort() + .reduce((result, key) => { + result[key] = obj[key]; + return result; + }, {}); + + return sortedObj; +} + +module.exports = sortJsonKeysAlphabetically; diff --git a/package.json b/package.json index b57953ab0..7314acb9f 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,11 @@ "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" + "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:check": "node common/translations-cli/index.js check" }, "dependencies": { "concurrently": "^5.1.0", 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/package.json b/packages/web/package.json index 8b9bc916e..b3caea15f 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", @@ -57,6 +58,7 @@ "uuid": "^3.4.0" }, "dependencies": { + "@messageformat/core": "^3.4.0", "chartjs-plugin-zoom": "^1.2.0", "date-fns": "^4.1.0", "debug": "^4.3.4", 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/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} + +
{_t('settings.localization', { defaultMessage: 'Localization' })}
+ { + setTimeout(() => { + internalRedirectTo('/'); + }, 100); + }} + />
Data grid
{ 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 @@