Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.12"]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: pip install openai tqdm

- name: Syntax gate (compileall)
run: python -m compileall . -q -x '(archive|__pycache__|\.git)'

- name: Run tests
run: python -m unittest discover -s tests -v
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# W40K Rogue Trader Translator (EN → PT-BR)

![tests](https://github.com/ltsuemitsu/w40k-tradutor/actions/workflows/tests.yml/badge.svg)

A fan-made toolkit to translate *Warhammer 40,000: Rogue Trader* (Owlcat Games)
localization files from English to Brazilian Portuguese, using LLM APIs
(DeepSeek by default; also Zhipu GLM or any OpenAI-compatible endpoint).
Expand Down Expand Up @@ -126,6 +128,19 @@ Tracked in `GUI_ROADMAP.md`. Highlights: direct function integration in the
GUI (replacing subprocess calls), interactive string reviewer, secure key
storage via `keyring`, PyInstaller packaging, cost dashboard, tests.

## Development

Run the test suite (stdlib `unittest` only — no pytest needed):

```bash
pip install openai tqdm
python -m unittest discover -s tests -v
```

Tests use tiny synthetic fixtures — no network, no API keys, no LLM calls.
CI runs a compile-all syntax gate plus the suite on Ubuntu and Windows ×
Python 3.10/3.12 (see `.github/workflows/tests.yml`).

## Contributing

Issues and PRs are welcome. Please **never** commit game localization files,
Expand Down
Empty file added tests/__init__.py
Empty file.
134 changes: 134 additions & 0 deletions tests/test_diff_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
"""Tests for diff_tool.py — audit, update detection, smart diff.

Pure functions over synthetic Owlcat localization dicts:
{"strings": {"<uuid>": {"Offset": int, "Text": str}}}. No network, no files needed.
"""
import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from diff_tool import audit_translation, detect_update, smart_diff


def _loc(entries):
"""entries: dict uuid -> text. Builds a localization dict."""
return {"strings": {k: {"Offset": i, "Text": t} for i, (k, t) in enumerate(entries.items())}}


EMPTY_GLOSSARY = {"metadata": {}, "terms": []}


class TestDetectUpdate(unittest.TestCase):
def test_only_added_and_changed_in_delta(self):
en_old = _loc({
"uuid-1": "Hello world.",
"uuid-2": "The Emperor protects.",
"uuid-3": "This string was removed.",
})
en_new = _loc({
"uuid-1": "Hello world.", # unchanged
"uuid-2": "The Emperor protects us all.", # modified
"uuid-4": "Brand new string.", # added
})
pt_current = _loc({"uuid-1": "Olá mundo.", "uuid-2": "O Imperador protege."})

result = detect_update(en_new, en_old, pt_current)

self.assertEqual({k for k, _ in result["new_keys"]}, {"uuid-4"})
self.assertEqual({k for k, _, _ in result["modified_keys"]}, {"uuid-2"})
self.assertEqual(set(result["removed_keys"]), {"uuid-3"})
self.assertEqual(set(result["unchanged_keys"]), {"uuid-1"})

def test_modified_entry_carries_old_text(self):
en_old = _loc({"uuid-2": "Old wording."})
en_new = _loc({"uuid-2": "New wording."})
result = detect_update(en_new, en_old, _loc({}))
_, item, old_text = result["modified_keys"][0]
self.assertEqual(item["Text"], "New wording.")
self.assertEqual(old_text, "Old wording.")


class TestAuditTranslation(unittest.TestCase):
def test_flags_untranslated_and_accepts_translated(self):
orig = _loc({
"uuid-1": "The sword causes damage to the enemy.",
"uuid-2": "The door is locked tight.",
})
trans = _loc({
"uuid-1": "A espada causa dano ao inimigo.", # properly translated
"uuid-2": "The door is locked tight.", # EN == PT: untranslated
})
cats = audit_translation(orig, trans, EMPTY_GLOSSARY)

identical_keys = {k for k, _, _ in cats["identical"]}
ok_keys = {k for k, _, _ in cats["ok"]}
self.assertIn("uuid-2", identical_keys)
self.assertIn("uuid-1", ok_keys)
self.assertNotIn("uuid-1", identical_keys)
self.assertNotIn("uuid-2", ok_keys)

def test_glossary_term_is_not_flagged_as_untranslated(self):
glossary = {"metadata": {}, "terms": [{
"term_english": "Plasma Gun", "term_translated": "Plasma Gun",
"category": "weapon", "preserve": True,
}]}
orig = _loc({"uuid-1": "Plasma Gun"})
trans = _loc({"uuid-1": "Plasma Gun"}) # same text, but it is a glossary term
cats = audit_translation(orig, trans, glossary)
preserved_keys = {k for k, _, _ in cats["glossary_preserved"]}
identical_keys = {k for k, _, _ in cats["identical"]}
self.assertIn("uuid-1", preserved_keys)
self.assertNotIn("uuid-1", identical_keys)


class TestSmartDiff(unittest.TestCase):
GLOSSARY = {"metadata": {}, "terms": [{
"term_english": "Plasma Gun", "term_translated": "Plasma Gun",
"category": "weapon", "preserve": True, "source": "test",
"context": "", "confidence": "high", "usage_count": 1,
"created_at": "2026-01-01T00:00:00",
}]}

def test_flags_needs_work_and_detects_preserved_in_context(self):
orig = _loc({
"uuid-1": "Equip the Plasma Gun before the fight.",
"uuid-2": "The door is locked tight.",
"uuid-3": "The window hangs open today.",
})
trans = _loc({
# Translated, keeps the glossary term in EN (correct behavior)
"uuid-1": "Equipe a Plasma Gun antes da luta.",
# Fully translated, no glossary term involved
"uuid-2": "A porta está bem trancada.",
# Untranslated -> needs work
"uuid-3": "The window hangs open today.",
})
result, needs_work = smart_diff(orig, trans, self.GLOSSARY)

preserved_keys = {k for k, _, _, _ in result["preserved"]}
ok_keys = {k for k, _, _ in result["ok"]}
needs_keys = {k for k, _, _, _ in needs_work}

self.assertIn("uuid-1", preserved_keys)
# The glossary term found in context is reported
for k, _, _, found_terms in result["preserved"]:
if k == "uuid-1":
self.assertIn("Plasma Gun", found_terms)
self.assertIn("uuid-2", ok_keys)
self.assertIn("uuid-3", needs_keys)
self.assertEqual(result["needs_work"], needs_work)

def test_pure_mechanic_name_is_ok_even_untranslated(self):
orig = _loc({"uuid-1": "Plasma Gun"})
trans = _loc({"uuid-1": "Plasma Gun"})
result, needs_work = smart_diff(orig, trans, self.GLOSSARY)
ok_keys = {k for k, _, _ in result["ok"]}
self.assertIn("uuid-1", ok_keys)
self.assertEqual(needs_work, [])


if __name__ == "__main__":
unittest.main()
107 changes: 107 additions & 0 deletions tests/test_glossary_wiki.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
"""Tests for glossary_manager.py (entry model + load/save) and
wiki_sync.get_wiki_data() (static wiki term dataset).

No network: get_wiki_data() returns module-level constants only.
"""
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from glossary_manager import GlossaryEntry, load_glossary, save_glossary
from wiki_sync import get_wiki_data


class TestGlossaryEntry(unittest.TestCase):
def test_to_dict_from_dict_round_trip(self):
entry = GlossaryEntry(
term_english="Plasma Gun",
term_translated="Arma de Plasma",
category="weapon",
context="Ranged weapon",
confidence="high",
first_seen_batch=3,
usage_count=7,
created_at="2026-01-01T12:00:00",
)
clone = GlossaryEntry.from_dict(entry.to_dict())
self.assertEqual(clone, entry)
self.assertEqual(clone.to_dict(), entry.to_dict())

def test_created_at_auto_filled(self):
entry = GlossaryEntry(term_english="X", term_translated="Y", category="outro")
self.assertTrue(entry.created_at)

def test_defaults(self):
entry = GlossaryEntry(term_english="X", term_translated="Y", category="outro")
self.assertEqual(entry.context, "")
self.assertEqual(entry.confidence, "high")
self.assertEqual(entry.usage_count, 1)
self.assertEqual(entry.first_seen_batch, 0)


class TestLoadSaveGlossary(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.path = os.path.join(self._tmp.name, "glossary.json")

def test_missing_file_returns_initialized_structure(self):
data = load_glossary(self.path)
self.assertEqual(data["terms"], [])
self.assertEqual(data["metadata"]["total_terms"], 0)
self.assertIn("version", data["metadata"])
self.assertIn("updated_at", data["metadata"])

def test_save_updates_metadata_counts(self):
data = load_glossary(self.path)
data["terms"].append({"term_english": "A", "term_translated": "B", "category": "outro"})
data["terms"].append({"term_english": "C", "term_translated": "D", "category": "outro"})
save_glossary(self.path, data)

with open(self.path, "r", encoding="utf-8") as f:
saved = json.load(f)
self.assertEqual(saved["metadata"]["total_terms"], 2)
self.assertTrue(saved["metadata"]["updated_at"])
self.assertEqual(len(saved["terms"]), 2)

def test_save_then_load_round_trip(self):
data = load_glossary(self.path)
data["terms"].append({"term_english": "Plasma Gun", "term_translated": "Arma de Plasma",
"category": "weapon", "preserve": True})
save_glossary(self.path, data)
reloaded = load_glossary(self.path)
self.assertEqual(reloaded["terms"], data["terms"])
self.assertEqual(reloaded["metadata"]["total_terms"], 1)


class TestWikiData(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.wiki = get_wiki_data()

def test_sixteen_categories(self):
self.assertEqual(len(self.wiki), 16)

def test_total_term_count(self):
total = sum(len(terms) for terms in self.wiki.values())
self.assertGreater(total, 2500)

def test_no_category_is_empty(self):
for category, terms in self.wiki.items():
self.assertGreater(len(terms), 0, f"category {category!r} is empty")

def test_every_term_is_a_non_empty_string(self):
for category, terms in self.wiki.items():
for term in terms:
self.assertIsInstance(term, str, f"{category}: {term!r}")
self.assertTrue(term.strip(), f"{category}: blank term {term!r}")


if __name__ == "__main__":
unittest.main()
Loading
Loading