Skip to content

Commit 00255ad

Browse files
committed
Update CHANGELOG and enhance style differentiation rules: Added a new section to clarify the distinct communication goals for Native, Friendly, and Professional styles in translations. Updated constants and grammar processing files to enforce these differentiation rules, ensuring outputs are meaningfully different. Enhanced tests to validate the new style handling and documented the changes in the CHANGELOG.
1 parent 7c2d766 commit 00255ad

5 files changed

Lines changed: 179 additions & 12 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ Log every AI-assisted change here **in the same PR/commit** as the fix. Newest e
2828

2929
## [Unreleased]
3030

31+
### 2026-08-18 — Native / Casual / Professional must differ
32+
- **Type:** patch
33+
- **Version:** Unreleased
34+
- **Summary:** Prompt requires three distinct communication goals; near-identical Native/Friendly/Professional results are retried once.
35+
- **Why:** bug
36+
- **Files:** `backend/app/constants.py`, `backend/app/grammar.py`, `backend/app/cache.py`
37+
3138
### 2026-08-18 — Fix grammar.py SyntaxError on boot
3239
- **Type:** patch
3340
- **Version:** Unreleased

‎backend/app/cache.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
logger = logging.getLogger(__name__)
1313

14-
CACHE_SCHEMA = 11
14+
CACHE_SCHEMA = 12
1515
_client = None
1616
_client_failed = False
1717

‎backend/app/constants.py‎

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,13 @@ def locales_for(language: str) -> list[str]:
161161
Target language: {target_language}
162162
Target locale/dialect: {target_locale}
163163
164-
The three rewrites MUST be meaningfully different — not three textbook clones.
165-
- native: most natural version a local in {target_locale} would actually use.
166-
Preserve the original tone (do NOT auto-casualize; do NOT make it more formal).
167-
No unnecessarily sophisticated vocabulary.
168-
- friendly: more relaxed and conversational. Contractions and everyday phrasing.
169-
Do not add slang unnecessarily.
170-
- professional: workplace-appropriate but human. No corporate buzzwords,
171-
no academic stiffness. A real professional from that locale.
164+
The three rewrites MUST be meaningfully different — not three textbook clones
165+
and not small word substitutions of the same sentence.
166+
- native: most natural version a local in {target_locale} would actually use,
167+
keeping the user's ORIGINAL tone (do NOT auto-casualize; do NOT make it more formal).
168+
- friendly: clearly more relaxed than Native (Slack/WhatsApp). Do NOT copy Native.
169+
- professional: clearly more workplace-appropriate than Native. Restructure; do not
170+
only swap synonyms. No "I would like to kindly request...".
172171
- grammar_notes: array of {{"original","correction","explanation"}} for meaningful
173172
issues only. If none, use one item with explanation that there were no meaningful
174173
grammar mistakes (original/correction empty).
@@ -186,6 +185,48 @@ def locales_for(language: str) -> list[str]:
186185
→ YES "I want to tell him I can't make it tomorrow because I've got some work to do."
187186
""".strip()
188187

188+
STYLE_DIFFERENTIATION_RULES = """
189+
CRITICAL: The three outputs MUST be different. Do NOT generate them by small
190+
word substitutions. Each version has a different communication goal.
191+
192+
Native:
193+
The most natural version while preserving the user's ORIGINAL tone.
194+
Do not make it more casual or more professional unless the original already has that tone.
195+
Think: what would a native speaker naturally say to express exactly what this user means?
196+
Example shape (do not copy unless it fits): "Hey, I wanted to ask if you could send me
197+
the file today? I need to look it over before our meeting tomorrow. If you're busy,
198+
no worries—just let me know."
199+
200+
Friendly / Casual:
201+
This MUST sound noticeably more relaxed and conversational than Native.
202+
Allowed: contractions, fewer words, conversational expressions, phrasal verbs,
203+
shorter sentences, spoken phrasing, "no worries" / "yeah" / "sure" / "just" /
204+
"by the way" when appropriate. It should sound like Slack, WhatsApp, or iMessage
205+
to a friend or a coworker you know well. Do NOT simply copy Native.
206+
Example shape: "Hey, can you send me the file today? I wanna look it over before
207+
tomorrow's meeting. If you're busy, no worries—just let me know."
208+
209+
Professional:
210+
This MUST sound appropriate for a professional workplace.
211+
Do NOT merely replace casual words with formal synonyms. Restructure naturally.
212+
Remove conversational filler. Be concise, polite, and direct. Avoid slang and
213+
excessive formality. Do NOT use old-fashioned phrases such as
214+
"I would like to kindly request...". Do NOT sound like a formal letter unless
215+
the context requires it.
216+
Example shape: "Could you please send me the file today? I need to review it
217+
before tomorrow's meeting. If you're unable to send it today, please let me know."
218+
219+
HARD RULE — before returning, silently compare the three:
220+
1. Does Native preserve the original tone?
221+
2. Is Friendly/Casual clearly more conversational?
222+
3. Is Professional clearly more workplace-appropriate?
223+
4. Would a native speaker use each version in its intended context?
224+
If Native and Friendly/Casual are too similar, rewrite Friendly/Casual.
225+
If Native and Professional are too similar, rewrite Professional.
226+
Do NOT change the meaning just to make them different.
227+
Tone differentiation is REQUIRED. Meaning preservation is REQUIRED too.
228+
""".strip()
229+
189230
GERMAN_PERSIAN_RULES = """
190231
German ↔ Persian (mandatory when either language is German and the other is Persian):
191232
- Produce natural contemporary phrasing, not word-for-word calques.

‎backend/app/grammar.py‎

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
LEGACY_STYLE_KEYS,
1212
SEMANTIC_ACCURACY_RULES,
1313
STRUCTURE_FORMAT_RULES,
14+
STYLE_DIFFERENTIATION_RULES,
1415
STYLE_VARIANTS,
1516
TARGET_LANGUAGES,
1617
native_editor_instruction,
@@ -69,6 +70,68 @@ def flagged_invented_ready_subject(source_text: str, result: dict) -> bool:
6970
return any(target_invents_speaker_ready(t) for t in collect_style_to_texts(result))
7071

7172

73+
def _style_display_text(pair: dict | None, *, wants_translation: bool) -> str:
74+
item = pair or {}
75+
if wants_translation:
76+
return (item.get("to") or item.get("from") or "").strip()
77+
return (item.get("from") or item.get("to") or "").strip()
78+
79+
80+
def _norm_style(text: str) -> str:
81+
return " ".join((text or "").lower().split())
82+
83+
84+
def _near_duplicate(a: str, b: str) -> bool:
85+
if not a or not b:
86+
return False
87+
if a == b:
88+
return True
89+
sa, sb = set(a.split()), set(b.split())
90+
if not sa or not sb:
91+
return False
92+
overlap = len(sa & sb) / len(sa | sb)
93+
shorter = min(len(a), len(b))
94+
longer = max(len(a), len(b))
95+
if shorter < 24:
96+
return a == b
97+
return overlap >= 0.9 and shorter / longer >= 0.85
98+
99+
100+
def styles_too_similar_feedback(result: dict, *, wants_translation: bool) -> str | None:
101+
native = _norm_style(
102+
_style_display_text(result.get("native"), wants_translation=wants_translation)
103+
)
104+
friendly = _norm_style(
105+
_style_display_text(result.get("friendly"), wants_translation=wants_translation)
106+
)
107+
professional = _norm_style(
108+
_style_display_text(
109+
result.get("professional"), wants_translation=wants_translation
110+
)
111+
)
112+
if max(len(native), len(friendly), len(professional)) < 24:
113+
return None
114+
if _near_duplicate(native, friendly):
115+
return (
116+
"HARD RULE FAILED: Native and Friendly/Casual are identical or nearly "
117+
"identical. Rewrite Friendly/Casual so it is clearly more conversational "
118+
"(Slack/WhatsApp: shorter, contractions, spoken phrasing). Do NOT copy Native. "
119+
"Keep the same meaning and facts. Professional must stay workplace-appropriate."
120+
)
121+
if _near_duplicate(native, professional):
122+
return (
123+
"HARD RULE FAILED: Native and Professional are identical or nearly "
124+
"identical. Rewrite Professional by restructuring for the workplace "
125+
"(concise, polite, direct). Do NOT only swap synonyms. Keep the same meaning."
126+
)
127+
if _near_duplicate(friendly, professional):
128+
return (
129+
"HARD RULE FAILED: Friendly/Casual and Professional are nearly identical. "
130+
"Make Friendly more spoken and Professional more workplace. Same meaning."
131+
)
132+
return None
133+
134+
72135
def _style_pair(item) -> dict[str, str]:
73136
if isinstance(item, str):
74137
return {"from": item.strip(), "to": ""}
@@ -200,16 +263,16 @@ def build_styled_translation_prompt(
200263
f'"from" = natural rewrite of the input in {src_hint} '
201264
"(same enhanced source on all three).\n"
202265
f'"to" = Native / Friendly / Professional in {tgt} ({loc}). '
203-
"They must not be clones.\n"
266+
"They MUST be different communication styles, not word swaps.\n"
204267
"grammar_notes: JSON array of original/correction/explanation.\n"
205268
"Same who/what/when. Keep source layout. Short input → short output."
206269
)
207270
else:
208271
translation_block = (
209272
f"Stay in {src_hint} / {loc}. "
210273
'Put each version in "from" and set "to" to empty.\n'
211-
"Native preserves tone; Friendly is more relaxed; "
212-
"Professional is workplace-human."
274+
"Native preserves original tone; Friendly is clearly more conversational; "
275+
"Professional is clearly more workplace. Do not copy Native."
213276
)
214277

215278
context_block = (
@@ -228,6 +291,9 @@ def build_styled_translation_prompt(
228291
system_prompt = f"""
229292
{editor}
230293
294+
STYLE DIFFERENTIATION (mandatory):
295+
{STYLE_DIFFERENTIATION_RULES}
296+
231297
SEMANTIC ACCURACY (mandatory):
232298
{SEMANTIC_ACCURACY_RULES}
233299
@@ -334,6 +400,11 @@ def _once(retry_feedback=None):
334400
"and for pickup use 'come pick it up' / 'come by to collect it' / 'abholen'."
335401
)
336402
)
403+
similar = styles_too_similar_feedback(
404+
result, wants_translation=wants_translation
405+
)
406+
if similar and "error" not in result:
407+
result = _once(retry_feedback=similar)
337408
result["provider"] = used_provider
338409
return result
339410
except json.JSONDecodeError:

‎backend/tests/test_translation_quality.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ def test_prompt_requires_distinct_outputs(self):
171171
)
172172
self.assertIn("MUST be meaningfully different", system)
173173
self.assertIn("do NOT auto-casualize", system)
174+
self.assertIn("Do NOT simply copy Native", system)
175+
self.assertIn("HARD RULE", system)
174176

175177

176178
class ParseContractTests(unittest.TestCase):
@@ -215,6 +217,52 @@ def test_parses_structured_grammar_notes(self):
215217
self.assertEqual(parsed["grammarNotes"][0]["original"], "I am agree")
216218
self.assertIn("I agree", parsed["grammar_notes"])
217219

220+
221+
class StyleDifferentiationTests(unittest.TestCase):
222+
def test_flags_identical_native_and_friendly(self):
223+
result = {
224+
"native": {"from": "", "to": "Could you send me the file today please."},
225+
"friendly": {"from": "", "to": "Could you send me the file today please."},
226+
"professional": {
227+
"from": "",
228+
"to": "Please send the file today so I can review it before the meeting.",
229+
},
230+
}
231+
feedback = grammar.styles_too_similar_feedback(
232+
result, wants_translation=True
233+
)
234+
self.assertIsNotNone(feedback)
235+
self.assertIn("Friendly", feedback)
236+
237+
def test_allows_distinct_versions(self):
238+
result = {
239+
"native": {
240+
"from": "",
241+
"to": "Hey, I wanted to ask if you could send me the file today?",
242+
},
243+
"friendly": {
244+
"from": "",
245+
"to": "Hey, can you send me the file today? I wanna look it over before tomorrow's meeting.",
246+
},
247+
"professional": {
248+
"from": "",
249+
"to": "Could you please send me the file today? I need to review it before tomorrow's meeting.",
250+
},
251+
}
252+
self.assertIsNone(
253+
grammar.styles_too_similar_feedback(result, wants_translation=True)
254+
)
255+
256+
def test_skips_very_short_texts(self):
257+
result = {
258+
"native": {"from": "Thanks.", "to": ""},
259+
"friendly": {"from": "Thanks.", "to": ""},
260+
"professional": {"from": "Thank you.", "to": ""},
261+
}
262+
self.assertIsNone(
263+
grammar.styles_too_similar_feedback(result, wants_translation=False)
264+
)
265+
218266
def test_best_version_falls_back_to_canonical(self):
219267
raw = {
220268
"detected_lang": "English",

0 commit comments

Comments
 (0)