Translating a novel is not the same problem as translating a page of unrelated sentences. A character may be called by a personal name in one chapter, a title in another, and a family or seniority term much later. A place name can carry both a proper name and a geographic meaning. The translation can sound fluent line by line and still become difficult to read when those choices drift across dozens of chapters.
I built Novel Translator as a local, file-based workflow around that continuity problem. It can collect raw chapters, research a project, maintain structured references, translate with cloud or local models, record what happened, and serve the finished chapters through a small reader. The model is one part of the system; the durable memory around it does most of the coordination.
The motivation comes from my own reading habits. I enjoy Chinese, Korean, and Japanese novels, but I do not understand those three source languages. Available translations can read fluently on the surface and still feel wrong when revisited, especially when forms of address, family relationships, or seniority shift halfway through a story. This project is therefore not an attempt to pretend that software replaces a translator who knows the source language. It is a reading aid that helps me flag doubtful passages, preserve decisions I have reviewed, and improve the result over time.
The hard part is continuity
A single prompt has no reliable memory of decisions made twenty chapters earlier. Sending the entire novel every time would be expensive, slow, and too large for a practical context window. Sending only the current paragraph is cheaper, but it removes the information needed to preserve tone, relationships, aliases, and the current story arc.
The workflow therefore keeps a small set of explicit files beside each project. They are inspectable and editable, and each file answers a different question: what names are canonical, how the novel should sound, what recently happened, which model translated a chapter, and where a corrected term must be propagated.
For long-form translation, context should be maintained as project data rather than left inside a temporary chat.
Research creates the first version of memory
Before translating chapter by chapter, the project research step samples the available material. It takes the first three chapters, points from the middle at roughly fifty-chapter intervals, and the final two chapters, with a cap so a large novel does not create an unbounded prompt. It can also include the synopsis and samples from chapters that have already been translated, which helps preserve names that are already established.
Gemini turns that sample into three outputs. The research report describes genre, tone, characters, setting, glossary, and content flags. The translation guide records target style, naming conventions, key phrases, and instructions that every translation engine should follow. The structured reference stores entities in a form that code can merge, validate, and inject into later prompts.
metadata.json title, languages, source, content rating
reference.json characters, places, terms, aliases, relationships
translation_guide.json tone, naming rules, key phrases
chapter_context.json recent summaries, current arc, mood, active characters
character_appearances.json characters known to appear in each translated chapter
translation_log.json engine use, chunk results, and quality signals
reference_snapshot.json baseline for propagating later correctionstextThe reference is more than a glossary
A flat word list is not enough for fiction. The reference separates characters, locations, special terms, and modern loanwords. Character profiles can include a fixed romanized spelling, age and gender when known, aliases with the situation in which they are used, and directional relationships such as how two people address each other. That distinction matters when the source uses titles, kinship terms, rank, or sect seniority instead of repeating a personal name.
{
"characters": {
"林砚": "Lin Yan"
},
"locations": {
"青云山": "Gunung Qingyun"
},
"terms": {
"灵石": "Lingshi (batu roh)"
},
"character_profiles": [
{
"original_name": "林砚",
"romanized_name": "Lin Yan",
"aliases": [
{ "romanized": "Senior Lin", "context": "used by junior disciples" }
],
"relationships": []
}
]
}jsonWhen a new chapter is analyzed, existing entities are sent back as constraints. New dictionary entries are added without overwriting established values. Missing profile details may be filled, and new aliases or relationships are appended. A deduplication pass prefers original-language keys and removes entries that point to the same normalized value. The result is a reference that grows with the story without casually renaming earlier characters.
Hierarchy is part of the meaning
Chinese novels make this problem especially visible. 妹妹 (mèimei) literally means younger sister, but it can also be used as a familiar form of address for a younger woman. Translating every occurrence as “little sister” can invent a family relationship that does not exist. Keeping every term in pinyin creates the opposite problem: the reader may miss that one character is older, younger, or holds a different position within a sect.
Sect relationships carry the same risk. 师姐 (shījiě) identifies a more senior female disciple, while 师妹 (shīmèi) identifies a more junior female disciple. These terms can appear alongside titles, names, and changing levels of familiarity. A translation engine looking at one sentence may choose sister, senior, junior, or untranslated pinyin without knowing the relationship established several chapters earlier. This is why reference.json stores more than word equivalents: it needs to preserve who is speaking to whom, the chosen form of address, and the context in which it applies.
妹妹 (mèimei)
younger biological sibling → younger sister
familiar address to a younger woman → name or context-appropriate address
师姐 (shījiě)
more senior female disciple → Shijie or senior fellow disciple
师妹 (shīmèi)
more junior female disciple → Shimei or junior fellow discipletextNot every name should be translated
Continuity also means preserving a decision not to translate something. Place names, sects, techniques, and concepts from a murim or cultivation setting can sound worse when forced into an ordinary English phrase. Each project has a pinyin_annotations toggle. When enabled, a location or term without a direct equivalent can use a romanized form followed by its target-language meaning on first occurrence. Later occurrences keep the chosen romanized form without repeating the parenthetical explanation.
The rule is selective. Slang or concepts with a natural target-language equivalent are translated directly. Geographic suffixes are translated too, producing forms such as “Chang’an City”, “Xiande Hall”, or “Wei River”. Romanized (meaning) is reserved for proper nouns or cultivation concepts whose identity would be weakened by full translation. Once a choice is recorded in reference.json, the engine is instructed to use it verbatim so a sect or place does not acquire a new name in a later chapter.
first occurrence
Dan Tian (Energy Center)
Dong Gong (Eastern Palace)
later occurrences
Dan Tian
Dong Gong
geographic suffixes
长安城 → Chang'an City
渭河 → Wei River
reference.json
source term → chosen form that must remain consistenttextThe research output is not a final authority. I often reread a translated chapter, notice an address or relationship that feels inconsistent, and check earlier chapters together with whatever supporting sources are available. Once I am more confident, I manually update reference.json or translation_guide.json. That correction becomes context for later chapters and can be propagated to older output through the sync report when needed. This manual loop matters: a model can propose project memory, but I decide which reviewed choices the project should retain.
read the translated chapter
→ flag a doubtful name, address, or relationship
→ check earlier chapters and supporting sources
→ update reference.json or translation_guide.json
→ review the change report
→ carry the corrected decision into later chapterstextContext works at three distances
Project-wide context comes from the translation guide and reference. Both are injected into translation prompts so the selected engine receives the same naming and style contract. A mandatory term block repeats up to thirty important mappings near the end of the prompt, where they are less likely to be ignored.
Chapter-to-chapter context is deliberately smaller. After a successful translation, Gemini produces a short English summary, the current story arc, mood, and up to five active characters. The project stores at most five summaries of 350 characters each, while the next translation injects the latest three. English is used for this internal memory so different cloud and local models can interpret the same context consistently; it is not part of the reader-facing translation.
Within one chapter, the text is split along line or paragraph boundaries into chunks of about two thousand characters. After each successful chunk, the last three translated sentences become context for the next chunk. This local bridge helps a scene continue naturally without asking the model to translate previous text again.
WHOLE PROJECT reference + translation guide
↓
BETWEEN CHAPTERS recent summaries + arc + mood + active characters
↓
BETWEEN CHUNKS last three translated sentences
↓
CURRENT CHUNK source text to translatetextA chapter moves through a controlled pipeline
Raw chapters can be copied into the project or fetched from a supported URL. Site-specific JSON configurations describe selectors for a generic scraper, and Playwright can render sites that require a browser. The URL log prevents the same chapter URL from being imported twice and can recognize sequential chapter URLs. Before translation, repeated copy-paste content is removed only from the in-memory input; the original raw file remains untouched.
URL or manual text
→ raw chapter file
→ remove duplicate input in memory
→ analyze new entities against the existing reference
→ merge and deduplicate reference data
→ split chapter into bounded chunks
→ translate with guide + reference + story context
→ detect refusal, failure, or untranslated CJK
→ save translated chapter
→ quality scan + engine log + character index
→ update rolling chapter summary
→ local web readertextEntity analysis runs before translation for modes that use Gemini. Ollama-only mode deliberately skips that network-dependent step and uses the reference already on disk. This makes the offline path possible, with the tradeoff that a new character or place will not be added automatically until research or analysis runs again.
Model roles can change without changing the contract
The workflow does not treat one model as permanently responsible for every task. The division follows the workload and the hardware available to me. On my PC, the local models that remain practical to run are around 8–12B parameters. They are useful for translating bounded chunks when names, terms, and important context are already supplied, but in my use their output has not been stable enough for research that must read samples across many chapters, infer character hierarchy, merge aliases, and return consistent structured JSON.
Initial research, entity analysis, and project-memory creation therefore currently use a cloud model through Gemini CLI. Local Ollama models can translate using references already stored on disk or act as fallbacks when the cloud path fails. Another mode lets Gemini CLI translate each chunk and tries local models when it times out, refuses, or leaves too much CJK text untranslated. This is not a claim that local models are always poor; I deliberately give them a narrower task so results are more predictable on my hardware.
Gemini CLI can currently translate chapters as well. Support for other cloud models through API keys is not available in the working implementation yet. I am developing that integration so research, analysis, summaries, and translation can eventually select another cloud provider without depending on Gemini CLI. This multi-provider work remains in development.
In the Gemini-primary path, a chunk first goes to Gemini. If the result fails a basic refusal or CJK check, the same contract is tried against available Ollama models. If every local result still contains source script, Gemini receives one more instruction for classical text, followed by NLLB as a last resort. Engine and chunk counts are stored so a run that silently fell back can be distinguished from one completed entirely by the requested model.
NLLB pivot modes also exist as experiments, but the project itself marks them as a poor default for Chinese, Japanese, and Korean fiction. Translating to English first and then to the target language adds another place for nuance to disappear. The useful lesson is not that more engines always improve quality; fallback should address a known failure and remain visible in the log.
Automated checks catch mechanical failures
After a chapter is saved, the quality scan assigns half of its score to remaining CJK sequences and half to compliance with reference terms that actually occur in that chapter's raw source. Limiting the denominator to relevant terms matters because the reference grows across the whole novel. The log also records the requested engine, actual Gemini or Ollama usage, failed and refused chunks, the local model used, and the resulting grade.
This score is a triage signal, not a literary-quality measurement. It can reveal an untranslated phrase or a missing canonical term, but it cannot decide whether dialogue feels natural, humor survived, a pronoun is correct, or a relationship was interpreted properly. Those still require reading and editorial judgment.
Corrections must propagate
A long project will eventually discover that an early name or term was wrong. The reference snapshot provides a baseline for detecting changes. The sync command builds a replacement report, shows the affected chapters and line numbers, asks once for confirmation, applies bounded word replacements, and records the operation in an append-only log. That is safer than silently rewriting every translated file as soon as reference.json is edited.
A separate continuity check looks for near-miss spellings of known character names. It uses the per-chapter appearance index to limit which characters should be considered, then filters candidates by initial letter, length difference, and similarity. The filters reduce false positives, but the result is still a review queue rather than an automatic proof that every name is correct.
Recovery is useful, but not finished
Each successful chunk is written to temp/translate_progress.json together with the engine that produced it. Keeping the checkpoint inside the project makes it portable and gives a failed run useful evidence. In the current implementation, however, translation initializes a new progress object and does not yet read previous chunks back into a resumed run. The file is therefore a recovery checkpoint for inspection today; automatic continuation from the last completed chunk remains work to finish.
The final output remains ordinary files
Translated chapters are plain text in each project's translated folder. A small FastAPI service exposes the available novels, chapter list, content, and covers. The Vue reader adds a chapter sidebar, previous and next navigation, font controls, dark mode, and per-novel reading progress. The reader is intentionally downstream of the translation pipeline: it does not need to know which model produced a chapter.
The code and current project structure are available in the public Novel Translator repository. This is an unfinished personal project that I continue to develop in my spare time. I enjoy reading novels myself, but literal machine translation—or AI translation that works through the text without maintaining story context—can be uncomfortable to read. That personal need is why the workflow keeps evolving. It also demonstrates the part I find most useful in practical AI work: models become easier to replace when context, decisions, checks, and outputs live outside the model call.