From dbb8bd6c4e0c14511473f6c96666fdf2d157ed19 Mon Sep 17 00:00:00 2001 From: busya Date: Sat, 9 May 2026 23:10:15 +0300 Subject: [PATCH] fix: persist environment selection, support Kilo AI provider, resolve Superset virtual columns for translation preview - Add environment_id to TranslationJob model/schema/API + DB migration - Pass environmentId from page to TranslationPreview and fetchPreview - Fix _fetch_sample_rows: use result_type='samples' to include virtual cols - Add 'kilo' and 'openrouter' to supported LLM provider types - Make response_format conditional (skip for non-OpenAI upstream providers) - Remove source_table field from form (redundant with datasource) - Restore datasourceSearch display from saved job on page load - Add request/response logging for LLM API calls --- .opencode/agents/frontend-coder.md | 2 +- .opencode/opencode.jsonc | 2 +- .../api/routes/translate/_preview_routes.py | 1 + backend/src/core/database.py | 33 ++++++ backend/src/models/translate.py | 3 + backend/src/plugins/translate/executor.py | 22 +++- backend/src/plugins/translate/orchestrator.py | 2 +- backend/src/plugins/translate/preview.py | 108 ++++++++++++------ backend/src/plugins/translate/service.py | 10 +- backend/src/schemas/translate.py | 4 + frontend/src/lib/api/translate.js | 8 +- .../translate/TranslationPreview.svelte | 6 +- frontend/src/lib/i18n/locales/en.json | 60 +++++++++- frontend/src/lib/i18n/locales/ru.json | 60 +++++++++- .../src/routes/translate/[id]/+page.svelte | 39 ++++--- 15 files changed, 292 insertions(+), 68 deletions(-) diff --git a/.opencode/agents/frontend-coder.md b/.opencode/agents/frontend-coder.md index a3615be0..bf3f2173 100644 --- a/.opencode/agents/frontend-coder.md +++ b/.opencode/agents/frontend-coder.md @@ -1,6 +1,6 @@ --- description: Frontend implementation specialist for Svelte UI work and browser-driven validation; uses browser-first practice for visible UX verification and route-level debugging. -mode: subagent +mode: all model: opencode-go/deepseek-v4-flash temperature: 0.1 permission: diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index ad40b26b..43f0833f 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -5,7 +5,7 @@ "type": "local", "command": ["npx", "chrome-devtools-mcp@latest", "--browser-url=http://127.0.0.1:9222" ], - "enabled": false + "enabled": true }, "axiom": { "type": "local", diff --git a/backend/src/api/routes/translate/_preview_routes.py b/backend/src/api/routes/translate/_preview_routes.py index 53c84522..d599972b 100644 --- a/backend/src/api/routes/translate/_preview_routes.py +++ b/backend/src/api/routes/translate/_preview_routes.py @@ -52,6 +52,7 @@ async def preview_translation( job_id=job_id, sample_size=payload.sample_size, prompt_template=payload.prompt_template, + env_id=payload.env_id, ) return result except ValueError as e: diff --git a/backend/src/core/database.py b/backend/src/core/database.py index 36acbcbe..6c05f3fa 100644 --- a/backend/src/core/database.py +++ b/backend/src/core/database.py @@ -475,6 +475,38 @@ def _ensure_filter_source_enum_values(bind_engine): # @SIDE_EFFECT: Executes ALTER TABLE statements against dataset review tables in the application database. # @RELATION: [DEPENDS_ON] ->[DatasetReviewSession] # @RELATION: [DEPENDS_ON] ->[ImportedFilter] +def _ensure_translation_jobs_columns(bind_engine): + with belief_scope("_ensure_translation_jobs_columns"): + table_name = "translation_jobs" + inspector = inspect(bind_engine) + if table_name not in inspector.get_table_names(): + return + + existing_columns = { + str(column.get("name") or "").strip() + for column in inspector.get_columns(table_name) + } + + if "environment_id" not in existing_columns: + try: + with bind_engine.begin() as connection: + connection.execute( + text( + "ALTER TABLE translation_jobs " + "ADD COLUMN environment_id VARCHAR" + ) + ) + logger.reflect( + "Added environment_id column to translation_jobs", + ) + except Exception as migration_error: + logger.explore( + "Failed to add environment_id to translation_jobs", + extra={"error": str(migration_error)}, + ) + raise + + def _ensure_dataset_review_session_columns(bind_engine): with belief_scope("_ensure_dataset_review_session_columns"): inspector = inspect(bind_engine) @@ -573,6 +605,7 @@ def init_db(): ensure_connection_configs_table(engine) _ensure_filter_source_enum_values(engine) _ensure_dataset_review_session_columns(engine) + _ensure_translation_jobs_columns(engine) # [/DEF:init_db:Function] diff --git a/backend/src/models/translate.py b/backend/src/models/translate.py index dd7ca61d..e2f4a0de 100644 --- a/backend/src/models/translate.py +++ b/backend/src/models/translate.py @@ -53,6 +53,9 @@ class TranslationJob(Base): batch_size = Column(Integer, nullable=False, default=50, comment="Records per batch") upsert_strategy = Column(String, nullable=False, default="MERGE", comment="MERGE, INSERT, UPDATE") + # Environment association + environment_id = Column(String, nullable=True, comment="Superset environment ID for datasource access") + created_by = Column(String, nullable=True) created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) diff --git a/backend/src/plugins/translate/executor.py b/backend/src/plugins/translate/executor.py index b5a23b70..6dc9ceab 100644 --- a/backend/src/plugins/translate/executor.py +++ b/backend/src/plugins/translate/executor.py @@ -527,12 +527,13 @@ class TranslationExecutor: model = provider.default_model or "gpt-4o-mini" provider_type = provider.provider_type.lower() if provider.provider_type else "openai" - if provider_type in ("openai", "openai_compatible"): + if provider_type in ("openai", "openai_compatible", "openrouter", "kilo"): return self._call_openai_compatible( base_url=provider.base_url, api_key=api_key, model=model, prompt=prompt, + provider_type=provider_type, ) else: raise ValueError(f"Unsupported provider type '{provider_type}'") @@ -549,6 +550,7 @@ class TranslationExecutor: api_key: str, model: str, prompt: str, + provider_type: str = "openai", ) -> str: with belief_scope("TranslationExecutor._call_openai_compatible"): import requests as http_requests @@ -565,10 +567,26 @@ class TranslationExecutor: {"role": "user", "content": prompt}, ], "temperature": 0.1, - "response_format": {"type": "json_object"}, + "max_tokens": 4096, } + # Structured output (response_format) only for native OpenAI — upstream providers routed via + # Kilo/OpenRouter may not support it (e.g. StepFun returns "structured_outputs is not supported") + if provider_type in ("openai", "openai_compatible"): + payload["response_format"] = {"type": "json_object"} + logger.reason( + f"LLM request model={payload.get('model')} " + f"provider_type={provider_type} " + f"response_format={'yes' if 'response_format' in payload else 'no'} " + f"prompt_len={len(prompt)}" + ) response = http_requests.post(url, headers=headers, json=payload, timeout=180) + if not response.ok: + logger.explore( + f"LLM API error status={response.status_code} " + f"model={payload.get('model')} " + f"body={response.text[:2000]}" + ) response.raise_for_status() data = response.json() diff --git a/backend/src/plugins/translate/orchestrator.py b/backend/src/plugins/translate/orchestrator.py index 17e55e2f..fd6bc2e5 100644 --- a/backend/src/plugins/translate/orchestrator.py +++ b/backend/src/plugins/translate/orchestrator.py @@ -387,7 +387,7 @@ class TranslationOrchestrator: # Submit to Superset try: - env_id = job.source_dialect or "" + env_id = job.environment_id or job.source_dialect or "" executor = SupersetSqlLabExecutor(self.config_manager, env_id) result = executor.execute_and_poll( sql=sql, diff --git a/backend/src/plugins/translate/preview.py b/backend/src/plugins/translate/preview.py index a9db69bb..80835ca1 100644 --- a/backend/src/plugins/translate/preview.py +++ b/backend/src/plugins/translate/preview.py @@ -147,6 +147,7 @@ class TranslationPreview: job_id: str, sample_size: int = 10, prompt_template: Optional[str] = None, + env_id: Optional[str] = None, ) -> Dict[str, Any]: with belief_scope("TranslationPreview.preview_rows"): logger.reason("Starting preview for job", {"job_id": job_id, "sample_size": sample_size}) @@ -173,12 +174,22 @@ class TranslationPreview: source_rows = self._fetch_sample_rows( job=job, sample_size=sample_size, + env_id=env_id, ) if not source_rows: raise ValueError("No rows returned from datasource for preview") actual_row_count = len(source_rows) - logger.reason("Fetched sample rows", {"actual_count": actual_row_count}) + logger.reason(f"Fetched {actual_row_count} sample row(s)") + + # Debug: log first row keys and translation column value + if source_rows: + first_row = source_rows[0] + logger.reason( + f"First source row keys={list(first_row.keys())} " + f"translation_col={job.translation_column} " + f"val='{first_row.get(job.translation_column, '')}'" + ) # 4. Build prompt context from rows all_source_texts = [] @@ -513,17 +524,18 @@ class TranslationPreview: # @PRE: job has source_datasource_id and translation_column. # @POST: Returns list of dicts with row data. # @SIDE_EFFECT: Calls Superset chart data endpoint. - def _fetch_sample_rows(self, job: TranslationJob, sample_size: int = 10) -> List[Dict[str, Any]]: + def _fetch_sample_rows(self, job: TranslationJob, sample_size: int = 10, env_id: Optional[str] = None) -> List[Dict[str, Any]]: with belief_scope("TranslationPreview._fetch_sample_rows"): - # Find environment config using source_dialect as env_id + # Determine environment: prefer explicit env_id, then job.environment_id, then job.source_dialect (legacy) environments = self.config_manager.get_environments() + target_env_id = env_id or job.environment_id or job.source_dialect or "" env_config = next( - (e for e in environments if e.id == job.source_dialect), + (e for e in environments if e.id == target_env_id), None, ) if not env_config: logger.explore("Could not find environment for datasource", { - "env_id": job.source_dialect, + "env_id": target_env_id, }) # Fallback: try first environment if environments: @@ -539,12 +551,12 @@ class TranslationPreview: # Fetch dataset detail to build proper query context dataset_detail = client.get_dataset_detail(int(job.source_datasource_id)) - # Determine columns to query - query_columns = [job.translation_column] - if job.context_columns: - query_columns.extend(job.context_columns) - - # Build query context for chart data endpoint + # Build query context for chart data endpoint. + # Virtual columns (e.g. comment_text_ru) are NOT resolved when: + # - result_type="query" (physical columns only) + # - query_mode="raw" (virtual columns unavailable in raw mode) + # Solution: remove both result_type="query" AND query_mode="raw", + # use aggregate mode with no metrics — this resolves virtual columns. query_context = client.build_dataset_preview_query_context( dataset_id=int(job.source_datasource_id), dataset_record=dataset_detail, @@ -552,13 +564,18 @@ class TranslationPreview: effective_filters=[], ) - # Modify to fetch specific columns as raw data (no aggregation) + # Modify: use result_type="samples" which returns sample data + # including all columns (physical + virtual), without needing + # explicit column objects that trigger validation errors. queries = query_context.get("queries", []) if queries: - queries[0]["columns"] = query_columns - queries[0]["metrics"] = [] queries[0]["row_limit"] = sample_size - queries[0]["result_type"] = "query" + queries[0].pop("result_type", None) + queries[0].pop("columns", None) + queries[0]["metrics"] = [] + query_context["result_type"] = "samples" + form_data = query_context.get("form_data", {}) + form_data.pop("query_mode", None) try: response = client.network.request( @@ -568,23 +585,22 @@ class TranslationPreview: headers={"Content-Type": "application/json"}, ) except Exception as e: - # Try legacy endpoint as fallback - logger.explore("Chart data endpoint failed, trying legacy", {"error": str(e)}) - try: - response = client.network.request( - method="POST", - endpoint="/explore_json/form_data", - params={"form_data": json.dumps(query_context.get("form_data", {}))}, - headers={"Content-Type": "application/json"}, - ) - except Exception as e2: - raise ValueError( - f"Failed to fetch sample data from Superset: {e2}" - ) + logger.explore("Chart data API failed", {"error": str(e)}) + raise ValueError(f"Failed to fetch sample data from Superset: {e}") # Parse response rows = self._extract_data_rows(response) - logger.reason("Extracted data rows", {"count": len(rows)}) + logger.reason(f"Extracted {len(rows)} data row(s)") + + # Debug: log first row keys and translation column value + if rows: + first_row = rows[0] + logger.reason( + f"Row keys={list(first_row.keys())} " + f"target_col={job.translation_column} " + f"val='{first_row.get(job.translation_column, '')}'" + ) + return rows # [/DEF:_fetch_sample_rows:Function] @@ -646,12 +662,13 @@ class TranslationPreview: model = provider.default_model or "gpt-4o-mini" provider_type = provider.provider_type.lower() if provider.provider_type else "openai" - if provider_type in ("openai", "openai_compatible"): + if provider_type in ("openai", "openai_compatible", "openrouter", "kilo"): response_text = self._call_openai_compatible( base_url=provider.base_url, api_key=api_key, model=model, prompt=prompt, + provider_type=provider_type, ) else: raise ValueError(f"Unsupported provider type '{provider_type}' for preview") @@ -675,6 +692,7 @@ class TranslationPreview: api_key: str, model: str, prompt: str, + provider_type: str = "openai", ) -> str: with belief_scope("TranslationPreview._call_openai_compatible"): import requests as http_requests @@ -691,10 +709,26 @@ class TranslationPreview: {"role": "user", "content": prompt}, ], "temperature": 0.1, - "response_format": {"type": "json_object"}, + "max_tokens": 4096, } + # Structured output (response_format) only for native OpenAI — upstream providers routed via + # Kilo/OpenRouter may not support it (e.g. StepFun returns "structured_outputs is not supported") + if provider_type in ("openai", "openai_compatible"): + payload["response_format"] = {"type": "json_object"} + logger.reason( + f"LLM request model={payload.get('model')} " + f"provider_type={provider_type} " + f"response_format={'yes' if 'response_format' in payload else 'no'} " + f"prompt_len={len(prompt)}" + ) response = http_requests.post(url, headers=headers, json=payload, timeout=120) + if not response.ok: + logger.explore( + f"LLM API error status={response.status_code} " + f"model={payload.get('model')} " + f"body={response.text[:2000]}" + ) response.raise_for_status() data = response.json() @@ -716,6 +750,8 @@ class TranslationPreview: @staticmethod def _parse_llm_response(response_text: str, expected_count: int) -> Dict[str, str]: with belief_scope("TranslationPreview._parse_llm_response"): + logger.reason(f"Raw LLM response length={len(response_text)} preview={response_text[:500]}") + try: data = json.loads(response_text) except json.JSONDecodeError: @@ -732,6 +768,7 @@ class TranslationPreview: rows = data.get("rows", []) if not isinstance(rows, list): + logger.explore(f"LLM response has no 'rows' array, keys={list(data.keys())} text_preview={response_text[:300]}") raise ValueError("LLM response missing 'rows' array") translations: Dict[str, str] = {} @@ -742,11 +779,10 @@ class TranslationPreview: translations[row_id] = translation if len(translations) < expected_count: - logger.explore("LLM returned fewer translations than expected", { - "expected": expected_count, - "received": len(translations), - "missing": [str(i) for i in range(expected_count) if str(i) not in translations], - }) + logger.explore( + f"LLM returned fewer translations expected={expected_count} " + f"got={len(translations)} response_preview={response_text[:600]}" + ) return translations # [/DEF:_parse_llm_response:Function] diff --git a/backend/src/plugins/translate/service.py b/backend/src/plugins/translate/service.py index 2e0ff094..dd9cef77 100644 --- a/backend/src/plugins/translate/service.py +++ b/backend/src/plugins/translate/service.py @@ -55,8 +55,10 @@ def get_dialect_from_database(database_record: Dict[str, Any]) -> str: # Map Superset backend names to normalized dialect dialect_map = { "postgresql": "postgresql", + "greenplum": "postgresql", "mysql": "mysql", "clickhouse": "clickhouse", + "clickhousedb": "clickhouse", "sqlite": "sqlite", "mssql": "mssql", "oracle": "oracle", @@ -211,11 +213,11 @@ class TranslateJobService: # Detect database dialect and validate columns if datasource is specified dialect = payload.database_dialect - if payload.source_datasource_id and payload.source_dialect: + if payload.source_datasource_id and (payload.environment_id or payload.source_dialect): # If no explicit dialect, try to detect it if not dialect: try: - env_id = payload.source_dialect + env_id = payload.environment_id or payload.source_dialect _, detected_dialect = fetch_datasource_metadata( int(payload.source_datasource_id), env_id, @@ -246,6 +248,7 @@ class TranslateJobService: provider_id=payload.provider_id, batch_size=payload.batch_size, upsert_strategy=payload.upsert_strategy, + environment_id=payload.environment_id, status="DRAFT", created_by=self.current_user, ) @@ -287,7 +290,7 @@ class TranslateJobService: # Re-detect dialect if datasource changed if payload.source_datasource_id and not payload.database_dialect: try: - env_id = (payload.source_dialect or job.source_dialect) + env_id = (payload.environment_id or payload.source_dialect or job.environment_id or job.source_dialect) _, detected_dialect = fetch_datasource_metadata( int(payload.source_datasource_id), env_id, @@ -470,6 +473,7 @@ def job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) - created_at=job.created_at, updated_at=job.updated_at, dictionary_ids=dict_ids or [], + environment_id=job.environment_id, ) # [/DEF:job_to_response:Function] diff --git a/backend/src/schemas/translate.py b/backend/src/schemas/translate.py index e6a7081b..9c3c0324 100644 --- a/backend/src/schemas/translate.py +++ b/backend/src/schemas/translate.py @@ -32,6 +32,7 @@ class TranslateJobCreate(BaseModel): batch_size: int = Field(50, description="Records per batch") upsert_strategy: str = Field("MERGE", description="UPSERT strategy: MERGE, INSERT, UPDATE") dictionary_ids: Optional[List[str]] = Field(default_factory=list, description="Associated terminology dictionary IDs") + environment_id: Optional[str] = Field(None, description="Superset environment ID") # [/DEF:TranslateJobCreate:Class] @@ -57,6 +58,7 @@ class TranslateJobUpdate(BaseModel): upsert_strategy: Optional[str] = None status: Optional[str] = None dictionary_ids: Optional[List[str]] = None + environment_id: Optional[str] = None # [/DEF:TranslateJobUpdate:Class] @@ -86,6 +88,7 @@ class TranslateJobResponse(BaseModel): created_at: datetime updated_at: Optional[datetime] = None dictionary_ids: Optional[List[str]] = None + environment_id: Optional[str] = None class Config: from_attributes = True @@ -214,6 +217,7 @@ class DictionaryImportResult(BaseModel): class PreviewRequest(BaseModel): sample_size: int = Field(10, ge=1, le=100, description="Number of sample rows to preview") prompt_template: Optional[str] = Field(None, description="Optional custom prompt template") + env_id: Optional[str] = Field(None, description="Superset environment ID for preview data fetch") # [DEF:PreviewRowUpdate:Class] diff --git a/frontend/src/lib/api/translate.js b/frontend/src/lib/api/translate.js index 973c3a69..24669653 100644 --- a/frontend/src/lib/api/translate.js +++ b/frontend/src/lib/api/translate.js @@ -119,9 +119,13 @@ export async function fetchDatasourceColumns(datasourceId, envId) { // @PURPOSE: Trigger a translation preview for a job. // @PRE: jobId is non-empty string. // @POST: Returns preview session with rows and cost estimate. -export async function fetchPreview(jobId, sampleSize = 10) { +export async function fetchPreview(jobId, sampleSize = 10, envId = '') { try { - return await api.postApi(`/translate/jobs/${jobId}/preview`, { sample_size: sampleSize }); + const body = { sample_size: sampleSize }; + if (envId) { + body.env_id = envId; + } + return await api.postApi(`/translate/jobs/${jobId}/preview`, body); } catch (error) { throw normalizeTranslateError(error, 'Failed to run preview'); } diff --git a/frontend/src/lib/components/translate/TranslationPreview.svelte b/frontend/src/lib/components/translate/TranslationPreview.svelte index 0cff9beb..6de809be 100644 --- a/frontend/src/lib/components/translate/TranslationPreview.svelte +++ b/frontend/src/lib/components/translate/TranslationPreview.svelte @@ -31,8 +31,8 @@ fetchPreviewRecords } from '$lib/api/translate.js'; - /** @type {{ jobId: string, onAccept?: () => void }} */ - let { jobId, onAccept = () => {} } = $props(); + /** @type {{ jobId: string, environmentId?: string, onAccept?: () => void }} */ + let { jobId, environmentId = '', onAccept = () => {} } = $props(); /** @type {'idle'|'loading'|'preview_loaded'|'preview_error'|'accepted'|'stale_config'} */ let uxState = $state('idle'); @@ -56,7 +56,7 @@ uxState = 'loading'; errorMessage = ''; try { - const result = await fetchPreview(jobId, sampleSize); + const result = await fetchPreview(jobId, sampleSize, environmentId); session = result; records = (result.records || []).map(r => ({ ...r, _isEdited: false })); costEstimate = result.cost_estimate || null; diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 8a3c6b63..3fc4c094 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -1455,7 +1455,17 @@ "recent_runs": "Recent Runs", "id_label": "ID: {id}", "save": "Save", - "cancel": "Cancel" + "cancel": "Cancel", + "job_created": "Translation job created", + "job_updated": "Translation job updated", + "job_not_found": "Job not found: {id}", + "save_failed": "Failed to save translation job", + "name_required": "Name is required", + "translation_column_required": "Translation column is required", + "key_mapping_required": "Please add at least one key column", + "redundant_column_warning": "Column '{col}' appears in both source and target key columns", + "insert_retry_started": "Insert retry started", + "insert_retry_failed": "Insert retry failed" }, "preview": { "title": "Preview", @@ -1467,7 +1477,53 @@ "apply_failed": "Failed to apply corrections", "no_records": "No preview records available", "loading": "Loading preview...", - "fetch_failed": "Failed to load preview" + "fetch_failed": "Failed to load preview", + + "sample_size": "Sample size:", + "run_preview": "Run Preview", + "running_preview": "Running preview...", + "retry_preview": "Retry", + "preview_loaded": "Loaded {count} records", + "preview_failed": "Failed to run preview", + "accepted_badge": "Accepted", + "active_badge": "Active", + "cost_estimate": "Cost Estimate", + "sample_rows": "Sample rows:", + "sample_tokens": "Tokens:", + "sample_cost": "Sample cost:", + "est_total_cost": "Estimated total cost:", + "rows_count": "Total: {count}", + "approved_count": "Approved: {count}", + "rejected_count": "Rejected: {count}", + "pending_count": "Pending: {count}", + "approve_all": "Approve All", + "re_run": "Re-run", + "table_number": "#", + "table_source": "Source SQL", + "table_translation": "Translation", + "table_status": "Status", + "table_actions": "Actions", + "empty_placeholder": "(empty)", + "pending_placeholder": "Pending translation", + "edited_label": "(edited)", + "save_edit": "Save", + "cancel_edit": "Cancel", + "approve": "Approve", + "edit": "Edit", + "reject": "Reject", + "quality_gate": "Quality Gate", + "quality_gate_ready": "All rows reviewed. You can accept the preview.", + "quality_gate_pending": "Not all rows reviewed. Acceptance is blocked.", + "accepting": "Accepting...", + "accept_preview": "Accept Preview", + "accepted_title": "Preview Accepted", + "accepted_body": "Preview accepted successfully. You can now run the translation.", + "run_preview_again": "Run Again", + "approve_failed": "Failed to approve row", + "reject_failed": "Failed to reject row", + "edit_failed": "Failed to save changes", + "row_updated": "Row updated", + "accept_failed": "Failed to accept preview" }, "jobs": { "title": "Translation Jobs", diff --git a/frontend/src/lib/i18n/locales/ru.json b/frontend/src/lib/i18n/locales/ru.json index 9eecfaeb..229a942a 100644 --- a/frontend/src/lib/i18n/locales/ru.json +++ b/frontend/src/lib/i18n/locales/ru.json @@ -1455,7 +1455,17 @@ "recent_runs": "Последние запуски", "id_label": "ID: {id}", "save": "Сохранить", - "cancel": "Отмена" + "cancel": "Отмена", + "job_created": "Задание перевода создано", + "job_updated": "Задание перевода обновлено", + "job_not_found": "Задание не найдено: {id}", + "save_failed": "Не удалось сохранить задание перевода", + "name_required": "Название обязательно", + "translation_column_required": "Колонка для перевода обязательна", + "key_mapping_required": "Добавьте хотя бы одну ключевую колонку", + "redundant_column_warning": "Колонка '{col}' присутствует в ключевых колонках источника и цели", + "insert_retry_started": "Повторная вставка запущена", + "insert_retry_failed": "Повторная вставка не удалась" }, "preview": { "title": "Предпросмотр", @@ -1467,7 +1477,53 @@ "apply_failed": "Не удалось применить исправления", "no_records": "Нет записей для предпросмотра", "loading": "Загрузка предпросмотра...", - "fetch_failed": "Не удалось загрузить предпросмотр" + "fetch_failed": "Не удалось загрузить предпросмотр", + + "sample_size": "Размер выборки:", + "run_preview": "Запустить предпросмотр", + "running_preview": "Выполняется предпросмотр...", + "retry_preview": "Повторить", + "preview_loaded": "Загружено {count} записей", + "preview_failed": "Не удалось выполнить предпросмотр", + "accepted_badge": "Принято", + "active_badge": "Активен", + "cost_estimate": "Оценка стоимости", + "sample_rows": "Строк в выборке:", + "sample_tokens": "Токенов:", + "sample_cost": "Стоимость выборки:", + "est_total_cost": "Оценка общей стоимости:", + "rows_count": "Всего: {count}", + "approved_count": "Одобрено: {count}", + "rejected_count": "Отклонено: {count}", + "pending_count": "Ожидает: {count}", + "approve_all": "Одобрить все", + "re_run": "Запустить заново", + "table_number": "№", + "table_source": "Исходный SQL", + "table_translation": "Перевод", + "table_status": "Статус", + "table_actions": "Действия", + "empty_placeholder": "(пусто)", + "pending_placeholder": "Ожидает перевода", + "edited_label": "(изменено)", + "save_edit": "Сохранить", + "cancel_edit": "Отмена", + "approve": "Одобрить", + "edit": "Изменить", + "reject": "Отклонить", + "quality_gate": "Контроль качества", + "quality_gate_ready": "Все строки проверены. Можно принять предпросмотр.", + "quality_gate_pending": "Не все строки проверены. Принятие заблокировано.", + "accepting": "Принятие...", + "accept_preview": "Принять предпросмотр", + "accepted_title": "Предпросмотр принят", + "accepted_body": "Предпросмотр успешно принят. Теперь можно запустить перевод.", + "run_preview_again": "Запустить повторно", + "approve_failed": "Не удалось одобрить строку", + "reject_failed": "Не удалось отклонить строку", + "edit_failed": "Не удалось сохранить изменения", + "row_updated": "Строка обновлена", + "accept_failed": "Не удалось принять предпросмотр" }, "jobs": { "title": "Задания перевода", diff --git a/frontend/src/routes/translate/[id]/+page.svelte b/frontend/src/routes/translate/[id]/+page.svelte index 23d78894..0b155a39 100644 --- a/frontend/src/routes/translate/[id]/+page.svelte +++ b/frontend/src/routes/translate/[id]/+page.svelte @@ -267,8 +267,19 @@ // If there's a datasource, load its columns if (j.source_datasource_id && environments.length > 0) { datasourceId = j.source_datasource_id; - // Try to find env - source_dialect may hold env_id - if (environments.length > 0) { + // Restore the search display from saved table info + if (j.source_table) { + const dialect = j.database_dialect || ''; + datasourceSearch = dialect + ? `${j.source_table} (${dialect})` + : j.source_table; + } else { + datasourceSearch = `Dataset #${j.source_datasource_id}`; + } + // Restore environment from stored job, fall back to first available + if (j.environment_id && environments.some(e => e.id === j.environment_id)) { + environmentId = j.environment_id; + } else { environmentId = environments[0]?.id || ''; } await loadColumnsForDatasource(); @@ -361,8 +372,14 @@ /** @param {Event} event */ async function handleEnvChange(event) { - environmentId = event.target.value; - if (datasourceId && environmentId) { + const newEnv = event.target.value; + environmentId = newEnv; + // Clear search state, but KEEP datasourceId if already loaded from job + // — just reload columns with the new environment + datasourceSearch = ''; + datasourceList = []; + databaseDialect = ''; + if (datasourceId) { await loadColumnsForDatasource(); } } @@ -461,6 +478,7 @@ upsert_strategy: upsertStrategy, dictionary_ids: dictionaryIds, database_dialect: databaseDialect || sourceDialect, + environment_id: environmentId || null, }; try { @@ -858,16 +876,7 @@

{$t.translate?.config?.target_table_title}

-
-
- - -
+

{$t.translate?.preview?.title}

- +
{:else}