A translation endpoint plugin for XUnity.AutoTranslator that sends in-game text to a locally-hosted, OpenAI-compatible chat-completions server for translation — LM Studio, Ollama (via its OpenAI-compatible API surface), or anything else exposing the same API shape.
This project is offline-only by design: it never talks to a cloud/hosted LLM provider, only to a server you run yourself.
- Works with any OpenAI-compatible
/v1/chat/completionsserver (LM Studio, Ollama, etc.) - Optional alternative endpoint(s) — e.g. LM Studio as primary, Ollama as alternative (or two instances of either) — tried in order if the primary is unreachable or fails
- Optional API key (
Authorization: Bearer <key>) for servers that require one - Fully configurable model name, temperature, top_p, and max tokens
- Editable system prompt and user prompt template, with
{{SourceLanguage}},{{DestinationLanguage}}and{{Input}}placeholders - Optional per-endpoint source/destination language override
- Strips
<think>...</think>reasoning blocks some local models emit - No third-party runtime dependencies (JSON/YAML handling is self-contained)
- Logs a confirmation line to BepInEx's
LogOutput.logon load (version, primary endpoint/model, number of alternatives configured), so you can confirm the plugin actually started without needing to trigger a translation first
- A game already modded with BepInEx and XUnity.AutoTranslator
- LM Studio or Ollama running locally with a model loaded and its OpenAI-compatible API server enabled
- Download the latest release zip from the Releases page.
- Copy
LlmTranslateOffline.dllinto:BepInEx/plugins/XUnity.AutoTranslator/Translators/ - Copy the example config into:
(If you skip this step, the plugin generates this file itself with the same defaults the first time it runs.)
BepInEx/config/LlmTranslateOffline.yaml - In
BepInEx/config/AutoTranslatorConfig.ini, under[Service], set:Endpoint=LlmTranslateOffline - Edit
BepInEx/config/LlmTranslateOffline.yamlto point at your LM Studio/Ollama server, model, and API key, then launch the game.
All settings live in BepInEx/config/LlmTranslateOffline.yaml, generated automatically
on first run. See examples/LlmTranslateOffline.example.yaml
for the full file with comments.
| Key | Default | Description |
|---|---|---|
Endpoint |
http://localhost:1234/v1/chat/completions |
Chat-completions URL. LM Studio default shown; Ollama's OpenAI-compatible default is http://localhost:11434/v1/chat/completions. |
ApiKey |
(empty) | Sent as Authorization: Bearer <ApiKey> when non-empty. |
Model |
local-model |
Model name/id exactly as your server expects it. |
AlternativeEndpoint / AlternativeApiKey / AlternativeModel |
(empty) | Optional alternative server, tried when the primary is unreachable or fails. Leave AlternativeEndpoint empty to disable. Add more with numbered keys (AlternativeEndpoint2, etc., contiguous). Named "Alternative" — not "Fallback" — to avoid confusion with AutoTranslatorConfig.ini's own unrelated FallbackEndpoint setting, which swaps in a different translator service entirely. |
AlternativeTimeoutSeconds |
60 |
How long to wait for each alternative endpoint before giving up on it. |
Temperature |
0.3 |
Sampling temperature. |
TopP |
1.0 |
Nucleus sampling parameter. |
MaxTokens |
1000 |
Max tokens in the completion. |
StripReasoning |
true |
Strips <think>...</think> blocks from the response. |
SourceLanguage / DestinationLanguage |
(empty) | Override the language pair for this endpoint only; empty uses the values from AutoTranslatorConfig.ini. |
SystemPrompt |
translation system prompt | Sent as the system role message. Supports {{SourceLanguage}} / {{DestinationLanguage}}. |
UserPromptTemplate |
{{Input}} |
Sent as the user role message. Supports {{SourceLanguage}}, {{DestinationLanguage}}, {{Input}}. |
Restart the game after editing the config to apply changes.
Requires the .NET SDK (works on Linux, macOS, and Windows — no Windows-only tooling needed):
dotnet build -c ReleaseThe output DLL is written to bin/Release/XUnity.AutoTranslator.Plugin.LlmTranslateOffline.dll.
Nothing is being translated. Where do I look first?
BepInEx/LogOutput.log. The plugin logs a line on load with its version, primary
endpoint/model, and alternative count. If that line is missing, the DLL is not in
BepInEx/plugins/XUnity.AutoTranslator/Translators/ or Endpoint=LlmTranslateOffline is
not set in AutoTranslatorConfig.ini. If it is present, the problem is the server or the
config values.
I upgraded and my alternative endpoint stopped working.
The config keys were renamed twice: Fallback* (1.2.0) → Secondary* (2.0.0) →
Alternative* (2.1.0). Old names are not read and produce no warning — rename them in
BepInEx/config/LlmTranslateOffline.yaml.
My model outputs its reasoning along with the translation.
StripReasoning (on by default) removes <think>...</think> blocks. If your model uses a
different marker, it will come through — adjust SystemPrompt to suppress it.
- Config changes need a game restart. There is no hot reload.
- One request at a time. Concurrency is fixed at 1, so bulk translation of a text-heavy scene is bounded by your model's throughput.
- No retry on a single endpoint. Each alternative is tried once, in order. A server that is still loading a model returns an error and is skipped rather than retried.
- Primary endpoint's real request timeout is not configurable — only the preflight
reachability check and
AlternativeTimeoutSecondsare; the request itself uses XUnity.AutoTranslator's own timeout. - No streaming. The full completion is awaited before any text is returned.
- Prompts are global.
SystemPrompt/UserPromptTemplateapply to the primary and every alternative; they cannot be set per endpoint. - Translation quality is entirely your local model's. This plugin only transports text.
This project follows Semantic Versioning. See
CHANGELOG.md for a version-by-version history of changes.
MIT — see LICENSE.
- Releases are drafted automatically by CI on every push to
main; the release body is the matchingCHANGELOG.mdsection. - This project's code, CI, and documentation were developed with the help of AI (Claude).