diff --git a/backend/src/models/translate.py b/backend/src/models/translate.py index de794062..b5885615 100644 --- a/backend/src/models/translate.py +++ b/backend/src/models/translate.py @@ -42,6 +42,9 @@ class TranslationJob(Base): target_key_cols = Column(JSON, nullable=True, comment="Target key column names for composite key") translation_column = Column(String, nullable=True, comment="Source column whose values will be translated") target_column = Column(String, nullable=True, comment="Target column for translated output (defaults to translation_column)") + target_language_column = Column(String, nullable=True, comment="Target column for language code (e.g. 'ru', 'en')") + target_source_column = Column(String, nullable=True, comment="Target column for source/original text") + target_source_language_column = Column(String, nullable=True, comment="Target column for detected source language (BCP-47)") context_columns = Column(JSON, nullable=True, comment="Context column names included in LLM prompt") # LLM & processing settings diff --git a/backend/src/plugins/translate/orchestrator.py b/backend/src/plugins/translate/orchestrator.py index e69918f2..028f440e 100644 --- a/backend/src/plugins/translate/orchestrator.py +++ b/backend/src/plugins/translate/orchestrator.py @@ -23,7 +23,7 @@ from collections.abc import Callable from datetime import UTC, datetime from typing import Any -from sqlalchemy.orm import Session, selectinload +from sqlalchemy.orm import Session, joinedload, selectinload from ...core.config_manager import ConfigManager from ...core.logger import belief_scope, logger @@ -338,9 +338,10 @@ class TranslationOrchestrator: run: TranslationRun, ) -> dict[str, Any]: with belief_scope("TranslationOrchestrator._generate_and_insert_sql"): - # Fetch successful records + # Fetch successful records with eager-loaded language data records = ( self.db.query(TranslationRecord) + .options(joinedload(TranslationRecord.languages)) .filter( TranslationRecord.run_id == run.id, TranslationRecord.status == "SUCCESS", @@ -378,6 +379,17 @@ class TranslationOrchestrator: if k not in columns: columns.append(k) + # Add target metadata columns for enhanced table mapping + if job.target_source_column and job.target_source_column not in columns: + columns.append(job.target_source_column) + if job.target_language_column and job.target_language_column not in columns: + columns.append(job.target_language_column) + if job.target_source_language_column and job.target_source_language_column not in columns: + columns.append(job.target_source_language_column) + + # Resolve the primary target language for INSERT + primary_language = (job.target_languages or ["en"])[0] + rows_for_sql = [] for rec in records: row_data = {} @@ -401,6 +413,21 @@ class TranslationOrchestrator: if effective_target: row_data[effective_target] = rec.target_sql or "" + # Source text column: INSERT original source text + if job.target_source_column: + row_data[job.target_source_column] = rec.source_sql or "" + + # Language column: INSERT language code (e.g. 'ru', 'en') + if job.target_language_column: + row_data[job.target_language_column] = primary_language + + # Source language column: INSERT detected source language (BCP-47) + if job.target_source_language_column: + detected = "und" + if rec.languages and len(rec.languages) > 0: + detected = rec.languages[0].source_language_detected or "und" + row_data[job.target_source_language_column] = detected + # Key columns from source data if job.target_key_cols: for k in job.target_key_cols: diff --git a/backend/src/plugins/translate/service.py b/backend/src/plugins/translate/service.py index 21283c32..46ac4b98 100644 --- a/backend/src/plugins/translate/service.py +++ b/backend/src/plugins/translate/service.py @@ -255,6 +255,9 @@ class TranslateJobService: target_key_cols=payload.target_key_cols or [], translation_column=payload.translation_column, target_column=payload.target_column, + target_language_column=payload.target_language_column, + target_source_column=payload.target_source_column, + target_source_language_column=payload.target_source_language_column, context_columns=payload.context_columns or [], target_languages=target_languages, provider_id=payload.provider_id, @@ -392,6 +395,9 @@ class TranslateJobService: target_key_cols=source.target_key_cols, translation_column=source.translation_column, target_column=source.target_column, + target_language_column=source.target_language_column, + target_source_column=source.target_source_column, + target_source_language_column=source.target_source_language_column, context_columns=source.context_columns, target_languages=source.target_languages, provider_id=source.provider_id, @@ -494,6 +500,9 @@ def job_to_response(job: TranslationJob, dict_ids: list[str] | None = None) -> T target_key_cols=job.target_key_cols or [], translation_column=job.translation_column, target_column=job.target_column, + target_language_column=job.target_language_column, + target_source_column=job.target_source_column, + target_source_language_column=job.target_source_language_column, context_columns=job.context_columns or [], target_languages=job.target_languages, provider_id=job.provider_id, diff --git a/backend/src/schemas/translate.py b/backend/src/schemas/translate.py index 1041c449..f969356b 100644 --- a/backend/src/schemas/translate.py +++ b/backend/src/schemas/translate.py @@ -42,6 +42,9 @@ class TranslateJobCreate(BaseModel): target_key_cols: list[str] | None = Field(default_factory=list, description="Target key column names") translation_column: str | None = Field(None, description="Source column to translate") target_column: str | None = Field(None, description="Target column for translated output (defaults to translation_column)") + target_language_column: str | None = Field(None, description="Target column for language code (e.g. 'ru', 'en')") + target_source_column: str | None = Field(None, description="Target column for source/original text") + target_source_language_column: str | None = Field(None, description="Target column for detected source language (BCP-47)") context_columns: list[str] | None = Field(default_factory=list, description="Context column names") target_language: str | None = Field(None, description="Target language code [DEPRECATED: use target_languages]") target_languages: list[str] | None = Field(default_factory=list, description="List of BCP-47 target language codes") @@ -72,6 +75,9 @@ class TranslateJobUpdate(BaseModel): target_key_cols: list[str] | None = None translation_column: str | None = None target_column: str | None = None + target_language_column: str | None = None + target_source_column: str | None = None + target_source_language_column: str | None = None context_columns: list[str] | None = None target_language: str | None = None target_languages: list[str] | None = None @@ -104,6 +110,9 @@ class TranslateJobResponse(BaseModel): target_key_cols: list[str] | None = None translation_column: str | None = None target_column: str | None = None + target_language_column: str | None = None + target_source_column: str | None = None + target_source_language_column: str | None = None context_columns: list[str] | None = None # source_language removed — deprecated, per-row auto-detected target_languages: list[str] | None = None diff --git a/frontend/src/lib/components/ui/MultiSelect.svelte b/frontend/src/lib/components/ui/MultiSelect.svelte index e5eec608..41630ebe 100644 --- a/frontend/src/lib/components/ui/MultiSelect.svelte +++ b/frontend/src/lib/components/ui/MultiSelect.svelte @@ -15,7 +15,7 @@ */ let { options = [], - selected = [], + selected = $bindable([]), label = '', searchable = false, placeholder = 'Search...', diff --git a/frontend/src/lib/i18n/locales/en/translate.json b/frontend/src/lib/i18n/locales/en/translate.json index 540e3abf..2b7b3fc9 100644 --- a/frontend/src/lib/i18n/locales/en/translate.json +++ b/frontend/src/lib/i18n/locales/en/translate.json @@ -57,6 +57,14 @@ "target_language": "Target Language", "status": "Status", "target_column_placeholder": "Target Column", + "target_column_label": "Target Column (translated text)", + "target_column_hint": "Column where translated text will be inserted", + "target_language_column_label": "Language Column", + "target_language_column_hint": "Column where language code (e.g. 'ru', 'en') will be inserted", + "target_source_column_label": "Source Text Column", + "target_source_column_hint": "Column where original source text will be inserted", + "target_source_language_column_label": "Source Language Column", + "target_source_language_column_hint": "Column where detected source language (BCP-47) will be inserted", "batch_size": "Batch Size", "upsert_strategy": "Strategy", "upsert_merge": "UPSERT (MERGE)", diff --git a/frontend/src/lib/i18n/locales/ru/translate.json b/frontend/src/lib/i18n/locales/ru/translate.json index 2de6e785..ad689060 100644 --- a/frontend/src/lib/i18n/locales/ru/translate.json +++ b/frontend/src/lib/i18n/locales/ru/translate.json @@ -57,6 +57,14 @@ "target_language": "Язык перевода", "status": "Статус", "target_column_placeholder": "Целевая колонка", + "target_column_label": "Целевая колонка (переведённый текст)", + "target_column_hint": "Колонка, в которую будет вставлен переведённый текст", + "target_language_column_label": "Колонка языка", + "target_language_column_hint": "Колонка, в которую будет вставлен код языка (например, 'ru', 'en')", + "target_source_column_label": "Колонка исходного текста", + "target_source_column_hint": "Колонка, в которую будет вставлен оригинальный исходный текст", + "target_source_language_column_label": "Колонка языка источника", + "target_source_language_column_hint": "Колонка, в которую будет вставлен обнаруженный язык источника (BCP-47)", "batch_size": "Размер пакета", "upsert_strategy": "Стратегия", "upsert_merge": "UPSERT (MERGE)", diff --git a/frontend/src/routes/translate/[id]/+page.svelte b/frontend/src/routes/translate/[id]/+page.svelte index 4451bcd6..31f164f9 100644 --- a/frontend/src/routes/translate/[id]/+page.svelte +++ b/frontend/src/routes/translate/[id]/+page.svelte @@ -97,6 +97,10 @@ let sourceKeyCols = $state([]); let targetKeyCols = $state([]); let translationColumn = $state(''); + let targetColumn = $state(''); + let targetLanguageColumn = $state(''); + let targetSourceColumn = $state(''); + let targetSourceLanguageColumn = $state(''); let contextColumns = $state([]); let targetLanguages = $state(['en']); let includeSourceReference = $state(true); @@ -292,6 +296,10 @@ sourceKeyCols = j.source_key_cols || []; targetKeyCols = j.target_key_cols || []; translationColumn = j.translation_column || ''; + targetColumn = j.target_column || ''; + targetLanguageColumn = j.target_language_column || ''; + targetSourceColumn = j.target_source_column || ''; + targetSourceLanguageColumn = j.target_source_language_column || ''; contextColumns = j.context_columns || []; targetLanguages = j.target_languages?.length > 0 ? j.target_languages : (j.target_language ? [j.target_language] : ['en']); providerId = j.provider_id || ''; @@ -392,6 +400,10 @@ sourceKeyCols = []; targetKeyCols = []; translationColumn = ''; + targetColumn = ''; + targetLanguageColumn = ''; + targetSourceColumn = ''; + targetSourceLanguageColumn = ''; contextColumns = []; databaseDialect = ds.database_dialect || ''; if (datasourceId && environmentId) { @@ -538,6 +550,10 @@ source_key_cols: sourceKeyCols, target_key_cols: targetKeyCols, translation_column: translationColumn || null, + target_column: targetColumn || null, + target_language_column: targetLanguageColumn || null, + target_source_column: targetSourceColumn || null, + target_source_language_column: targetSourceLanguageColumn || null, context_columns: contextColumns, target_languages: targetLanguages?.length > 0 ? targetLanguages : null, provider_id: providerId || null, @@ -989,6 +1005,62 @@ {/if} + + +
Configure which columns receive translated data, language codes, source text, and detected source language during INSERT.
+{$t.translate?.config?.target_column_hint}
+{$t.translate?.config?.target_language_column_hint}
+{$t.translate?.config?.target_source_column_hint}
+{$t.translate?.config?.target_source_language_column_hint}
+