fix(translate): M1-M3 medium + L1-L2 low bugs from QA review

M1: ReDoS guard — 500 char max + try/except on re.compile
M2: Performance note — O(n×m) regex documented for future optimization
M3: Verified test key names correct (metrics.py transforms cumulative_* → short)
L1: Removed @staticmethod no-op from module-level function
L2: Added aria-label to emoji indicators in CorrectionCell
This commit is contained in:
2026-05-14 17:44:50 +03:00
parent 1853d008e9
commit bb21fd3165
5 changed files with 26 additions and 8 deletions

View File

@@ -37,7 +37,6 @@ def _run_to_response(run: Any) -> dict:
# #region _dict_to_response [C:2] [TYPE Function]
# @BRIEF Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.
@staticmethod
def _dict_to_response(d: Any, entry_count: int = 0) -> dict:
return {
"id": d.id,

View File

@@ -658,6 +658,10 @@ class DictionaryManager:
if not all_entries:
return []
# PERFORMANCE NOTE: O(rows × entries) regex matching per batch.
# Acceptable for current scale (50-100 rows × <1000 entries).
# For larger batches (>500 rows or >5000 entries), consider
# replacing with an Aho-Corasick automaton (e.g., pyahocorasick).
matched: list[dict[str, Any]] = []
seen_source_match: set = set()

View File

@@ -825,13 +825,19 @@ class InlineCorrectionService:
class BulkFindReplaceService:
"""Handle bulk find-and-replace on TranslationLanguage entries."""
MAX_PATTERN_LENGTH = 500
@staticmethod
def _compile_pattern(pattern: str, is_regex: bool) -> re.Pattern:
"""Compile a search pattern (regex or plain text)."""
import re
if is_regex:
return re.compile(pattern)
return re.compile(re.escape(pattern))
if len(pattern) > BulkFindReplaceService.MAX_PATTERN_LENGTH:
raise ValueError(
f"Pattern too long (max {BulkFindReplaceService.MAX_PATTERN_LENGTH} characters)"
)
try:
return re.compile(pattern) if is_regex else re.compile(re.escape(pattern))
except re.error as e:
raise ValueError(f"Invalid regex pattern: {e}")
@staticmethod
def _find_matching_entries(

View File

@@ -608,7 +608,7 @@ class OverrideLanguageRequest(BaseModel):
# #region BulkFindReplaceRequest [C:1] [TYPE Class]
# @BRIEF Schema for bulk find-and-replace within translations for a target language.
class BulkFindReplaceRequest(BaseModel):
find_pattern: str = Field(..., description="Text or regex pattern to find")
find_pattern: str = Field(..., description="Text or regex pattern to find", max_length=500)
is_regex: bool = Field(False, description="Whether find_pattern is a regular expression")
replacement_text: str = Field(..., description="Replacement text")
target_language: str = Field(..., description="BCP-47 language code to target")
@@ -617,6 +617,14 @@ class BulkFindReplaceRequest(BaseModel):
dictionary_id: str | None = Field(None, description="Target dictionary ID for submitting corrections")
usage_notes: str | None = Field(None, description="Usage notes for dictionary entries")
submit_to_dictionary_with_context: bool = Field(False, description="If true, auto-capture source row context when submitting to dictionary")
@field_validator("find_pattern")
@classmethod
def validate_pattern_length(cls, v: str) -> str:
"""Reject patterns longer than 500 characters to prevent ReDoS."""
if len(v) > 500:
raise ValueError("Pattern too long (max 500 characters)")
return v
# #endregion BulkFindReplaceRequest

View File

@@ -183,11 +183,12 @@
{:else if uxState === 'saved'}
<div class="flex items-center gap-2">
<span class="text-sm text-gray-700">{displayValue}</span>
<span class="text-xs text-green-600" title="Saved">📝</span>
<span class="text-xs text-green-600" title="Saved" aria-label="Edited" role="img">📝</span>
<button
onclick={openDictPopup}
class="ml-1 px-1.5 py-0.5 text-xs bg-amber-100 text-amber-700 rounded hover:bg-amber-200 transition-colors"
title="Submit to Dictionary"
aria-label="Submit to Dictionary"
>
📕 Dictionary
</button>
@@ -204,7 +205,7 @@
{:else if uxState === 'dict_submitted'}
<div class="flex items-center gap-2">
<span class="text-sm text-gray-700">{displayValue}</span>
<span class="text-xs text-green-600 font-medium" title="Saved to dictionary">✅ Dictionary ✓</span>
<span class="text-xs text-green-600 font-medium" title="Saved to dictionary" aria-label="Submitted to dictionary" role="img">✅ Dictionary ✓</span>
</div>
<!-- Error mode -->