diff --git a/.gitignore b/.gitignore index a979e82c8..7f3e7b4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ docker/plugins .env.development.local .env.test.local .env.production.local +.env.translation npm-debug.log* yarn-debug.log* diff --git a/common/translations-cli/translate.js b/common/translations-cli/translate.js new file mode 100644 index 000000000..fd6613102 --- /dev/null +++ b/common/translations-cli/translate.js @@ -0,0 +1,132 @@ +require('dotenv').config({ path: '.env.translation' }); +const fs = require('fs'); +const path = require('path'); +const OpenAI = require('openai'); + +const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +const translationsDir = path.join(__dirname, '../../translations'); +const enFilePath = path.join(translationsDir, 'en.json'); + +const languageNames = { + 'cs.json': 'Czech', + 'de.json': 'German', + 'es.json': 'Spanish', + 'fr.json': 'French', + 'it.json': 'Italian', + 'ja.json': 'Japanese', + 'pt.json': 'Portuguese', + 'sk.json': 'Slovak', + 'zh.json': 'Chinese' +}; + +// Read source (english) +const enTranslations = JSON.parse(fs.readFileSync(enFilePath, 'utf8')); +const enKeys = Object.keys(enTranslations); + +// Get all translation files +const translationFiles = fs.readdirSync(translationsDir) + .filter(file => file.endsWith('.json') && file !== 'en.json') + .sort(); + +console.log(`Found ${enKeys.length} keys in en.json\n`); +console.log('='.repeat(80)); + +async function translateMissingIds({file, translations, missingIds}){ + const languageName = languageNames[file]; + if (!languageName) { + console.log(`No language name mapping for file: ${file}`); + return; + } + + // Build object with only missing translations + const needed = {}; + missingIds.forEach(key => { + needed[key] = enTranslations[key]; + }); + + // Get all existing translations as style examples + const existingTranslations = {}; + Object.keys(translations).forEach(key => { + if (translations[key] && !translations[key].startsWith('***')) { + existingTranslations[key] = { + en: enTranslations[key], + translated: translations[key] + }; + } + }); + + const prompt = `You are a professional translator for DbGate, a database management application. + +Translate the following English UI strings to ${languageName}. + +IMPORTANT RULES: +1. Preserve ALL placeholders exactly as they appear: {plugin}, {columnNumber}, {0}, {1}, etc. +2. Maintain technical terminology appropriately for database software +3. Match the translation style, tone, and formality of the existing translations shown below +4. Keep the same level of brevity or verbosity as the existing translations +5. Return ONLY valid JSON - no markdown, no explanations, no code blocks +6. Use the same keys as provided + +EXISTING TRANSLATIONS (for style reference): +${JSON.stringify(existingTranslations, null, 2)} + +STRINGS TO TRANSLATE: +${JSON.stringify(needed, null, 2)} + +Return format: {"key": "translated value", ...}`; + + const response = await client.chat.completions.create({ + model: 'gpt-5.1', + messages: [ + { role: 'system', content: 'You are a professional translator specializing in software localization. Match the style and tone of existing translations. Return only valid JSON.' }, + { role: 'user', content: prompt } + ], + temperature: 0.2 + }); + + let translatedJson = response.choices[0].message.content.trim(); + + // Remove markdown code blocks if present + translatedJson = translatedJson.replace(/^```json\n?/, '').replace(/\n?```$/, ''); + + return JSON.parse(translatedJson); +} + +(async () => { + for (const file of translationFiles) { + const filePath = path.join(translationsDir, file); + const translations = JSON.parse(fs.readFileSync(filePath, 'utf8')); + + const missingIds = enKeys.filter(key => !translations.hasOwnProperty(key) || (typeof translations[key] === 'string' && translations[key].startsWith('***'))); + + + console.log(`\n${file.toUpperCase()}`); + console.log('-'.repeat(80)); + + if (missingIds.length === 0) { + console.log('✓ All translations complete!'); + continue; + } else { + console.log(`Found ${missingIds.length} untranslated IDs\n`); + } + + const newTranslations = await translateMissingIds({file, translations, missingIds}); + + if (!newTranslations) { + console.log(`Skipping file due to translation error: ${file}`); + continue; + } + + for (const [key, value] of Object.entries(newTranslations)) { + translations[key] = value; + console.log(`Translated: ${key} => ${value}`); + } + + fs.writeFileSync(filePath, JSON.stringify(translations, null, 2) + '\n', 'utf8'); + console.log(`\n✓ Updated translations written to ${file}`); + } + + console.log('\n' + '='.repeat(80)); + console.log('Translation complete!\n'); +})(); diff --git a/package.json b/package.json index c34aff989..04a4c1055 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.7.1", + "version": "6.7.2-alpha.1", "name": "dbgate-all", "workspaces": [ "packages/*", diff --git a/packages/dbmodel/bin/dbmodel.js b/packages/dbmodel/bin/dbmodel.js index 596be53db..54ea49339 100755 --- a/packages/dbmodel/bin/dbmodel.js +++ b/packages/dbmodel/bin/dbmodel.js @@ -35,6 +35,12 @@ program .option('-u, --user ', 'user name') .option('-p, --password ', 'password') .option('-d, --database ', 'database name') + .option('--url ', 'database url') + .option('--file ', 'database file') + .option('--socket-path ', 'socket path') + .option('--service-name ', 'service name (for Oracle)') + .option('--auth-type ', 'authentication type') + .option('--use-ssl', 'use SSL connection') .option('--auto-index-foreign-keys', 'automatically adds indexes to all foreign keys') .option( '--load-data-condition ', @@ -48,7 +54,7 @@ program .command('deploy ') .description('Deploys model to database') .action(modelFolder => { - const { engine, server, user, password, database, transaction } = program.opts(); + const { engine, server, user, password, database, url, file, transaction } = program.opts(); // const hooks = []; // if (program.autoIndexForeignKeys) hooks.push(dbmodel.hooks.autoIndexForeignKeys); @@ -60,6 +66,13 @@ program user, password, database, + databaseUrl: url, + useDatabaseUrl: !!url, + databaseFile: file, + socketPath: program.socketPath, + serviceName: program.serviceName, + authType: program.authType, + useSsl: program.useSsl, }, modelFolder, useTransaction: transaction, diff --git a/packages/web/src/buttons/InlineUploadButton.svelte b/packages/web/src/buttons/InlineUploadButton.svelte index 9c6d8bd92..7d1c6aa32 100644 --- a/packages/web/src/buttons/InlineUploadButton.svelte +++ b/packages/web/src/buttons/InlineUploadButton.svelte @@ -5,6 +5,7 @@ import getElectron from '../utility/getElectron'; import InlineButtonLabel from '../buttons/InlineButtonLabel.svelte'; import resolveApi, { resolveApiHeaders } from '../utility/resolveApi'; + import { _t } from '../translations'; import uuidv1 from 'uuid/v1'; @@ -49,11 +50,11 @@ {#if electron} - + {:else} - {}} title="Upload file" data-testid={$$props['data-testid']} htmlFor={inputId}> + {}} title={_t('files.uploadFile', { defaultMessage: "Upload file" })} data-testid={$$props['data-testid']} htmlFor={inputId}> {/if} diff --git a/packages/web/src/buttons/UploadButton.svelte b/packages/web/src/buttons/UploadButton.svelte index 0d9a18a22..33b9f6484 100644 --- a/packages/web/src/buttons/UploadButton.svelte +++ b/packages/web/src/buttons/UploadButton.svelte @@ -1,6 +1,7 @@
- Upload file + {_t('files.uploadFile', { defaultMessage: "Upload file" })}
diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 100bb6332..008759301 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -121,7 +121,7 @@ registerCommand({ testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase, onClick: () => { openNewTab({ - title: 'New Connection', + title: _t('common.newConnection', { defaultMessage: 'New Connection' }), icon: 'img connection', tabComponent: 'ConnectionTab', }); @@ -140,7 +140,7 @@ registerCommand({ !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase && !!getCloudSigninTokenHolder(), onClick: () => { openNewTab({ - title: 'New Connection on Cloud', + title: _t('common.newConnectionCloud', { defaultMessage: 'New Connection on Cloud' }), icon: 'img cloud-connection', tabComponent: 'ConnectionTab', props: { diff --git a/packages/web/src/designer/DesignerTable.svelte b/packages/web/src/designer/DesignerTable.svelte index c94f12d51..c13099d48 100644 --- a/packages/web/src/designer/DesignerTable.svelte +++ b/packages/web/src/designer/DesignerTable.svelte @@ -17,6 +17,7 @@ import moveDrag from '../utility/moveDrag'; import ColumnLine from './ColumnLine.svelte'; import DomTableRef from './DomTableRef'; + import { _t } from '../translations'; export let conid; export let database; @@ -185,8 +186,8 @@ const handleSetTableAlias = () => { showModal(InputTextModal, { value: alias || '', - label: 'New alias', - header: 'Set table alias', + label: _t('designerTable.newAlias', { defaultMessage: 'New alias' }), + header: _t('designerTable.setTableAlias', { defaultMessage: 'Set table alias' }), onConfirm: newAlias => { onChangeTable({ ...table, @@ -210,13 +211,13 @@ return settings?.tableMenu({ designer, designerId, onRemoveTable }); } return [ - { text: 'Remove', onClick: () => onRemoveTable({ designerId }) }, + { text: _t('common.remove', { defaultMessage: 'Remove' }), onClick: () => onRemoveTable({ designerId }) }, { divider: true }, settings?.allowTableAlias && !isMultipleTableSelection && [ - { text: 'Set table alias', onClick: handleSetTableAlias }, + { text: _t('designerTable.setTableAlias', { defaultMessage: 'Set table alias' }), onClick: handleSetTableAlias }, alias && { - text: 'Remove table alias', + text: _t('designerTable.removeTableAlias', { defaultMessage: 'Remove table alias' }), onClick: () => onChangeTable({ ...table, @@ -225,11 +226,11 @@ }, ], settings?.allowAddAllReferences && - !isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) }, - settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) }, + !isMultipleTableSelection && { text: _t('designerTable.addReferences', { defaultMessage: 'Add references' }), onClick: () => onAddAllReferences(table) }, + settings?.allowChangeColor && { text: _t('designerTable.changeColor', { defaultMessage: 'Change color' }), onClick: () => onChangeTableColor(table) }, settings?.allowDefineVirtualReferences && !isMultipleTableSelection && { - text: 'Define virtual foreign key', + text: _t('designerTable.defineVirtualForeignKey', { defaultMessage: 'Define virtual foreign key' }), onClick: () => handleDefineVirtualForeignKey(table), }, settings?.appendTableSystemMenu && diff --git a/packages/web/src/forms/FormArchiveFilesSelect.svelte b/packages/web/src/forms/FormArchiveFilesSelect.svelte index c2dee45bd..53c6ee70c 100644 --- a/packages/web/src/forms/FormArchiveFilesSelect.svelte +++ b/packages/web/src/forms/FormArchiveFilesSelect.svelte @@ -7,6 +7,7 @@ import { getFormContext } from './FormProviderCore.svelte'; import FormSelectField from './FormSelectField.svelte'; + import { _t } from '../translations'; export let folderName; export let name; @@ -28,10 +29,10 @@
setFieldValue(name, _.uniq([...($values[name] || []), ...($files && $files.map(x => x.name))]))} /> - setFieldValue(name, [])} /> + setFieldValue(name, [])} />
diff --git a/packages/web/src/impexp/FilesInput.svelte b/packages/web/src/impexp/FilesInput.svelte index 37e4d719b..3afdd30e4 100644 --- a/packages/web/src/impexp/FilesInput.svelte +++ b/packages/web/src/impexp/FilesInput.svelte @@ -23,6 +23,7 @@ import ElectronFilesInput from './ElectronFilesInput.svelte'; import { addFilesToSourceList } from './ImportExportConfigurator.svelte'; import UploadButton from '../buttons/UploadButton.svelte'; + import { _t } from '../translations'; export let setPreviewSource = undefined; @@ -55,10 +56,10 @@ {:else} {/if} - + -
Drag & drop imported files here
+
{_t('importExport.dragDropImportedFilesHere', { defaultMessage: "Drag & drop imported files here" })}