Skip to content

Commit cd61b86

Browse files
authored
Merge pull request #6801 from akatsoulas/l10n-strategies
Strategy pattern scaffolding for l10n
2 parents 409774b + 407f8d7 commit cd61b86

File tree

4 files changed

+303
-2
lines changed

4 files changed

+303
-2
lines changed

kitsune/llm/l10n/service.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from typing import Any
2+
3+
from kitsune.llm.l10n.strategies import (
4+
TranslationMethod,
5+
TranslationRequest,
6+
TranslationStrategyFactory,
7+
)
8+
from kitsune.wiki.models import Revision
9+
10+
11+
class TranslationService:
12+
"""Service for orchestrating translation operations."""
13+
14+
def __init__(self):
15+
self.factory = TranslationStrategyFactory()
16+
17+
def translate_document(self, revision: Revision, target_locales: list[str],
18+
method: TranslationMethod | None = None) -> dict[str, Any]:
19+
"""
20+
Translate a document to multiple target locales.
21+
Args:
22+
revision: The revision to translate
23+
target_locales: List of target locales
24+
method: Optional specific method, otherwise auto-selects best method
25+
Returns:
26+
Dictionary with translation results for each locale
27+
"""
28+
results = {}
29+
30+
for locale in target_locales:
31+
# Create translation request
32+
request = TranslationRequest(
33+
revision=revision,
34+
target_locale=locale,
35+
method=method or TranslationMethod.AI,
36+
priority="normal",
37+
metadata={"source_locale": revision.document.locale}
38+
)
39+
40+
# Select strategy (either specified or auto-selected)
41+
if method:
42+
strategy = self.factory.get_strategy(method)
43+
else:
44+
strategy = self.factory.select_best_strategy(request)
45+
46+
# Perform translation
47+
result = strategy.translate(request)
48+
results[locale] = {
49+
"success": result.success,
50+
"method": result.method.value if result.method else None,
51+
"method_display": result.method.label if result.method else None,
52+
"cost": result.cost,
53+
"quality_score": result.quality_score,
54+
"error": result.error_message,
55+
"metadata": result.metadata
56+
}
57+
return results
58+
59+
def get_translation_estimate(self, revision: Revision, target_locales: list[str]) -> dict[str, Any]:
60+
"""
61+
Get cost and method estimates for translation.
62+
Args:
63+
revision: The revision to translate
64+
target_locales: List of target locales
65+
Returns:
66+
Dictionary with estimates for each locale
67+
"""
68+
estimates = {}
69+
70+
for locale in target_locales:
71+
# Simple placeholder implementation
72+
estimates[locale] = {
73+
"recommended_method": "AI Translation",
74+
"cost_estimate": 0.0,
75+
"can_handle": True,
76+
"estimated_time": "5-10 minutes"
77+
}
78+
79+
return estimates

kitsune/llm/l10n/strategies.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
from abc import ABC, abstractmethod
2+
from dataclasses import dataclass, field
3+
from typing import Any
4+
5+
from django.conf import settings
6+
from django.db import models
7+
8+
from kitsune.wiki.models import Revision
9+
10+
11+
class TranslationMethod(models.TextChoices):
12+
"""Available translation methods."""
13+
AI = "ai", "AI Translation"
14+
VENDOR = "vendor", "Vendor Translation"
15+
HYBRID = "hybrid", "AI + Human Review"
16+
MANUAL = "manual", "Manual Translation"
17+
18+
19+
@dataclass
20+
class TranslationRequest:
21+
"""Request object for translation operations."""
22+
revision: Revision
23+
target_locale: str
24+
method: Any
25+
priority: str = "normal"
26+
metadata: dict[str, Any] = field(default_factory=dict)
27+
28+
29+
@dataclass
30+
class TranslationResult:
31+
"""Result object for translation operations."""
32+
success: bool
33+
translated_content: str | None = None
34+
method: Any = None
35+
cost: float = 0.0
36+
quality_score: float = 0.0
37+
error_message: str | None = None
38+
metadata: dict[str, Any] = field(default_factory=dict)
39+
40+
41+
class TranslationStrategy(ABC):
42+
"""Abstract base class for translation strategies."""
43+
44+
@abstractmethod
45+
def translate(self, request: TranslationRequest) -> TranslationResult:
46+
"""Translate content using this strategy."""
47+
pass
48+
49+
@abstractmethod
50+
def can_handle(self, request: TranslationRequest) -> bool:
51+
"""Check if this strategy can handle the request."""
52+
pass
53+
54+
@abstractmethod
55+
def get_cost_estimate(self, request: TranslationRequest) -> float:
56+
"""Get estimated cost for this translation."""
57+
pass
58+
59+
60+
class AITranslationStrategy(TranslationStrategy):
61+
"""AI/LLM-based translation strategy."""
62+
63+
def translate(self, request: TranslationRequest) -> TranslationResult:
64+
"""Translate using AI/LLM."""
65+
try:
66+
from kitsune.llm.l10n.translator import translate
67+
result = translate(request.revision.document, request.target_locale)
68+
69+
# Ensure translated_content is a string
70+
translated_content = result.get("content", "")
71+
if not isinstance(translated_content, str):
72+
translated_content = str(translated_content)
73+
74+
return TranslationResult(
75+
success=True,
76+
translated_content=translated_content,
77+
method=TranslationMethod.AI,
78+
cost=0.0,
79+
quality_score=0.85,
80+
metadata={"ai_model": "gpt-4", "result": result}
81+
)
82+
except Exception as e:
83+
return TranslationResult(
84+
success=False,
85+
method=TranslationMethod.AI,
86+
error_message=str(e)
87+
)
88+
89+
def can_handle(self, request: TranslationRequest) -> bool:
90+
"""AI can handle most content types."""
91+
return True
92+
93+
def get_cost_estimate(self, request: TranslationRequest) -> float:
94+
"""AI translation is typically free."""
95+
return 0.0
96+
97+
def _create_translated_revision(self, request: TranslationRequest, result: TranslationResult) -> Revision:
98+
"""Create a translated revision."""
99+
# TODO: Implement revision creation
100+
# Should create the revision object
101+
return Revision.objects.first()
102+
103+
104+
class HybridTranslationStrategy(TranslationStrategy):
105+
"""AI + Human review translation strategy."""
106+
107+
def translate(self, request: TranslationRequest) -> TranslationResult:
108+
"""Translate using AI + human review workflow."""
109+
try:
110+
# Step 1: AI translation
111+
ai_strategy = AITranslationStrategy()
112+
ai_result = ai_strategy.translate(request)
113+
114+
if not ai_result.success:
115+
return ai_result
116+
117+
review_task = self._create_review_task(request, ai_result)
118+
119+
return TranslationResult(
120+
success=True,
121+
translated_content=ai_result.translated_content,
122+
method=TranslationMethod.HYBRID,
123+
cost=10.0,
124+
quality_score=0.95,
125+
metadata={
126+
"ai_result": ai_result.metadata,
127+
"review_task_id": review_task.get("id"),
128+
"status": "pending_review"
129+
}
130+
)
131+
except Exception as e:
132+
return TranslationResult(
133+
success=False,
134+
method=TranslationMethod.HYBRID,
135+
error_message=str(e)
136+
)
137+
138+
def can_handle(self, request: TranslationRequest) -> bool:
139+
"""Hybrid is the default strategy for all AI enabled locales."""
140+
return True
141+
142+
def get_cost_estimate(self, request: TranslationRequest) -> float:
143+
"""Estimate cost for hybrid approach."""
144+
# TODO: Implement cost estimation for hybrid approach
145+
return 0.0
146+
147+
def _create_review_task(self, request: TranslationRequest, ai_result: TranslationResult) -> dict[str, Any]:
148+
"""Create a review task for human translator."""
149+
# Placeholder for task creation
150+
# notify and create the revision?
151+
return {}
152+
153+
154+
class ManualTranslationStrategy(TranslationStrategy):
155+
"""Manual human translation strategy."""
156+
157+
def translate(self, request: TranslationRequest) -> TranslationResult:
158+
"""Create manual translation task."""
159+
try:
160+
manual_task = self._create_manual_task(request)
161+
162+
return TranslationResult(
163+
success=True,
164+
translated_content="",
165+
method=TranslationMethod.MANUAL,
166+
cost=50.0,
167+
quality_score=0.98,
168+
metadata={
169+
"task_id": manual_task.get("id"),
170+
"status": "assigned_to_translator",
171+
"estimated_completion": manual_task.get("estimated_completion")
172+
}
173+
)
174+
except Exception as e:
175+
return TranslationResult(
176+
success=False,
177+
method=TranslationMethod.MANUAL,
178+
error_message=str(e)
179+
)
180+
181+
def can_handle(self, request: TranslationRequest) -> bool:
182+
"""Manual translation can handle any content."""
183+
return True
184+
185+
def get_cost_estimate(self, request: TranslationRequest) -> float:
186+
"""Estimate cost for manual translation."""
187+
return 0.0
188+
189+
def _create_manual_task(self, request: TranslationRequest) -> dict[str, Any]:
190+
"""Create a manual translation task."""
191+
# Placeholder for task creation
192+
return {}
193+
194+
195+
class TranslationStrategyFactory:
196+
"""Factory for creating translation strategies."""
197+
198+
def __init__(self):
199+
self._strategies = {
200+
TranslationMethod.AI: AITranslationStrategy(),
201+
TranslationMethod.HYBRID: HybridTranslationStrategy(),
202+
TranslationMethod.MANUAL: ManualTranslationStrategy(),
203+
}
204+
205+
def get_strategy(self, method: TranslationMethod) -> TranslationStrategy:
206+
"""Get strategy for the specified method."""
207+
if method not in self._strategies:
208+
raise ValueError(f"Unknown translation method: {method}")
209+
return self._strategies[method]
210+
211+
def select_best_strategy(self, request: TranslationRequest) -> TranslationStrategy:
212+
"""Select the best strategy based on business rules."""
213+
if request.target_locale in settings.AI_ENABLED_LOCALES:
214+
return self._strategies[TranslationMethod.AI]
215+
return self._strategies[TranslationMethod.MANUAL]

kitsune/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,3 +1375,6 @@ def setup_logger(name, formatter_str, level=logging.DEBUG):
13751375

13761376
# shell_plus conf
13771377
SHELL_PLUS_DONT_LOAD = ["silk"]
1378+
1379+
# AI Translation
1380+
AI_ENABLED_LOCALES = ["es"]

kitsune/wiki/views.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -814,9 +814,13 @@ def review_revision(request, document_slug, revision_id):
814814

815815
doc.save()
816816

817-
# Send notifications of approvedness and readiness:
818-
if rev.is_ready_for_localization or rev.is_approved:
817+
# Send notifications of approvedness
818+
if rev.is_approved:
819819
ApprovedOrReadyUnion(rev).fire(exclude=[rev.creator, request.user])
820+
if rev.is_ready_for_localization:
821+
# Trigger automatic translation with strategy pattern
822+
# example: TranslationService().translate_document(rev, target_locales)
823+
pass
820824

821825
# Send an email (not really a "notification" in the sense that
822826
# there's a Watch table entry) to revision creator.

0 commit comments

Comments
 (0)