feat(translate): enhanced target table column mapping + fix multi-language save

Multi-language save fix:
- MultiSelect.svelte: selected prop changed to $bindable([])
  (without $bindable, bind:selected never propagated to parent component,
  so target_languages always stayed as default ['en'])

Target table column mapping (INSERT enhancement):
- target_language_column — stores language code (e.g. 'ru', 'en')
- target_source_column — stores original source text
- target_source_language_column — stores detected BCP-47 source language
- SQL generator includes these columns in INSERT when configured
- Frontend: 4 input fields under 'Target Column Mapping' section
  with clear labels, placeholders, and i18n hints

Backend: model, schema, service, orchestrator
Frontend: job config page, i18n en+ru
DB: 3 new columns added to production
This commit is contained in:
2026-05-15 09:09:39 +03:00
parent 30d7f3f725
commit a74c50206a
8 changed files with 139 additions and 3 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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