Note to first-time contributors: Please open a discussion post in Discussions and describe your changes before submitting a pull request.
Before submitting, make sure you've checked the following:
Target branch: Please verify that the pull request targets the dev branch.
Description: Provide a concise description of the changes made in this pull request.
Changelog: Ensure a changelog entry following the format of Keep a Changelog is added at the bottom of the PR description.
Documentation: Have you updated relevant documentation Open WebUI Docs, or other documentation sources?
Dependencies: Are there any new dependencies? Have you updated the dependency versions in the documentation?
Testing: Have you written and run sufficient tests to validate the changes?
Code review: Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
Prefix: To clearly categorize this pull request, prefix the pull request title using one of the following:
BREAKING CHANGE: Significant changes that may affect compatibility
build: Changes that affect the build system or external dependencies
ci: Changes to our continuous integration processes or workflows
chore: Refactor, cleanup, or other non-functional code changes
docs: Documentation update or addition
feat: Introduces a new feature or enhancement to the codebase
fix: Bug fix or error correction
i18n: Internationalization or localization changes
perf: Performance improvement
refactor: Code restructuring for better maintainability, readability, or scalability
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.)
test: Adding missing tests or correcting existing tests
WIP: Work in progress, a temporary label for incomplete or ongoing work
Changelog Entry
Description
Pre-translated all missing strings using Qwen3 235B 2507 with the following script:
#!/usr/bin/env python3
import json
import argparse
import requests
import time
import sys
from collections import OrderedDict
def load_json_ordered(file_path):
"""Load JSON while preserving key order using OrderedDict"""
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f, object_pairs_hook=OrderedDict)
def save_json_ordered(file_path, data, indent):
"""Save JSON with preserved order and configurable indentation"""
# Handle indent conversion: try int first, then use as string
try:
indent_value = int(indent)
except (ValueError, TypeError):
indent_value = indent
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=indent_value, ensure_ascii=False)
def get_context(data, current_key, nctx):
"""Collect surrounding context with translations (nctx per side)"""
keys = list(data.keys())
idx = keys.index(current_key)
context_pre = []
context_post = []
# Collect preceding context (up to nctx non-empty entries)
i = idx - 1
count = 0
preceding = []
while i >= 0 and count < nctx:
k = keys[i]
if data[k]:
preceding.append((k, data[k]))
count += 1
i -= 1
context_pre = list(reversed(preceding)) # Reverse to maintain file order
# Collect following context (up to nctx non-empty entries)
i = idx + 1
count = 0
while i < len(keys) and count < nctx:
k = keys[i]
if data[k]:
context_post.append((k, data[k]))
count += 1
i += 1
return context_pre, context_post
def translate_string(api_url, api_key, model, lang, source_str, context, retry):
"""Translate a string using LLM with retry logic"""
# Format context examples
context_str = "\n".join([
f'"{k}" : "{v}"'
for k, v in context
])
# Prepare translation request
system_msg = f"You are a professional translator. Translate English strings to {lang}. Only output the translation without any additional text, quotes, or explanations. Do NOT change date formats, quotation marks or decimal separators."
user_msg = (
f"Here are translation examples:\n\n{context_str}\n\n"
f"Translate this string to {lang}:\n\"{source_str}\""
)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg}
],
"max_tokens": 1000
}
# Construct full endpoint URL
endpoint = f"{api_url.rstrip('/')}/chat/completions"
# Retry logic
for attempt in range(retry + 1):
try:
response = requests.post(
endpoint,
headers=headers,
json=payload,
timeout=30
)
response.raise_for_status()
result = response.json()
# Extract translation
translation = result["choices"][0]["message"]["content"].strip()
if not translation:
raise ValueError("Empty translation response")
return translation
except (requests.exceptions.RequestException,
KeyError,
IndexError,
ValueError) as e:
if attempt < retry:
wait = 2 ** attempt
print(f" Retry {attempt+1}/{retry} in {wait}s: {str(e)}", file=sys.stderr)
time.sleep(wait)
else:
raise RuntimeError(f"Translation failed after {retry} retries") from e
def main():
parser = argparse.ArgumentParser(description='Translate empty locale strings using LLM')
parser.add_argument('json_file', type=str, help='Locale JSON file to process')
parser.add_argument('--api-url', required=True, help='LLM API base URL (endpoint will be api-url/chat/completions)')
parser.add_argument('--api-key', required=True, help='API authentication key')
parser.add_argument('--model', required=True, help='LLM model name')
parser.add_argument('--lang', required=True, help='Target language (e.g., "German")')
parser.add_argument('--nctx', type=int, default=5, help='Number of context strings on EACH side (preceding/following) (default: 5)')
parser.add_argument('--retry', type=int, default=3, help='Retry attempts per string (default: 3)')
parser.add_argument('--indent', default='4', help='JSON indentation (integer for spaces, or string like "\\t") (default: 4)')
args = parser.parse_args()
# Load locale data with preserved order
try:
data = load_json_ordered(args.json_file)
except Exception as e:
print(f"Error loading JSON file: {e}", file=sys.stderr)
sys.exit(1)
# Track failed keys to prevent infinite loops
failed_keys = set()
processed = 0
total_empty = sum(1 for v in data.values() if v == "")
print(f"Found {total_empty} untranslated strings. Starting translation...")
while True:
# Find next untranslated string (skipping previously failed)
current_key = None
for key, value in data.items():
if value == "" and key not in failed_keys:
current_key = key
break
if not current_key:
break # No more untranslated strings
print(f"\nTranslating: '{current_key}'")
context_pre, context_post = get_context(data, current_key, args.nctx)
# Show context for debugging
if context_pre:
for (k, v) in context_pre:
print(f" '{k}': '{v}'")
print(f" > '{current_key}': ''")
if context_post:
for (k, v) in context_post:
print(f" '{k}': '{v}'")
try:
translation = translate_string(
api_url=args.api_url,
api_key=args.api_key,
model=args.model,
lang=args.lang,
source_str=current_key,
context=(context_pre + context_post),
retry=args.retry
)
data[current_key] = translation
save_json_ordered(args.json_file, data, args.indent)
processed += 1
print(f" ✅ Success: '{translation}'")
except Exception as e:
print(f" ❌ Failed: {str(e)}", file=sys.stderr)
failed_keys.add(current_key)
# Final status
remaining = total_empty - processed
print(f"\nTranslation complete. Processed: {processed}, Remaining: {remaining}")
if failed_keys:
print("Failed keys (check logs for details):")
for key in failed_keys:
print(f" - {key}")
if __name__ == "__main__":
main()
Thoroughly curated the result, including a few select minor changes to existing translations. Every single string has been reviewed and should be acceptable at a minimum.
Not every string has been inspected for looks/fit in actual deployment, given the high volume of translated strings.
Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the Contributor License Agreement (CLA), and I am providing my contributions under its terms.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
## 📋 Pull Request Information
**Original PR:** https://github.com/open-webui/open-webui/pull/17399
**Author:** [@Ithanil](https://github.com/Ithanil)
**Created:** 9/12/2025
**Status:** ✅ Merged
**Merged:** 9/12/2025
**Merged by:** [@tjbck](https://github.com/tjbck)
**Base:** `dev` ← **Head:** `german_i18n`
---
### 📝 Commits (1)
- [`c9e69fb`](https://github.com/open-webui/open-webui/commit/c9e69fb2f4d19bc39b9060f0a3b9afdf10eeff36) 100% german translation (~ 250 strings added/changed)
### 📊 Changes
**1 file changed** (+249 additions, -249 deletions)
<details>
<summary>View changed files</summary>
📝 `src/lib/i18n/locales/de-DE/translation.json` (+249 -249)
</details>
### 📄 Description
# Pull Request Checklist
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
**Before submitting, make sure you've checked the following:**
- [x] **Target branch:** Please verify that the pull request targets the `dev` branch.
- [x] **Description:** Provide a concise description of the changes made in this pull request.
- [x] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
- [x] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
- [x] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
- [x] **Testing:** Have you written and run sufficient tests to validate the changes?
- [x] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
- [x] **Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
- **BREAKING CHANGE**: Significant changes that may affect compatibility
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows
- **chore**: Refactor, cleanup, or other non-functional code changes
- **docs**: Documentation update or addition
- **feat**: Introduces a new feature or enhancement to the codebase
- **fix**: Bug fix or error correction
- **i18n**: Internationalization or localization changes
- **perf**: Performance improvement
- **refactor**: Code restructuring for better maintainability, readability, or scalability
- **style**: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.)
- **test**: Adding missing tests or correcting existing tests
- **WIP**: Work in progress, a temporary label for incomplete or ongoing work
# Changelog Entry
### Description
Pre-translated all missing strings using Qwen3 235B 2507 with the following script:
```
#!/usr/bin/env python3
import json
import argparse
import requests
import time
import sys
from collections import OrderedDict
def load_json_ordered(file_path):
"""Load JSON while preserving key order using OrderedDict"""
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f, object_pairs_hook=OrderedDict)
def save_json_ordered(file_path, data, indent):
"""Save JSON with preserved order and configurable indentation"""
# Handle indent conversion: try int first, then use as string
try:
indent_value = int(indent)
except (ValueError, TypeError):
indent_value = indent
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=indent_value, ensure_ascii=False)
def get_context(data, current_key, nctx):
"""Collect surrounding context with translations (nctx per side)"""
keys = list(data.keys())
idx = keys.index(current_key)
context_pre = []
context_post = []
# Collect preceding context (up to nctx non-empty entries)
i = idx - 1
count = 0
preceding = []
while i >= 0 and count < nctx:
k = keys[i]
if data[k]:
preceding.append((k, data[k]))
count += 1
i -= 1
context_pre = list(reversed(preceding)) # Reverse to maintain file order
# Collect following context (up to nctx non-empty entries)
i = idx + 1
count = 0
while i < len(keys) and count < nctx:
k = keys[i]
if data[k]:
context_post.append((k, data[k]))
count += 1
i += 1
return context_pre, context_post
def translate_string(api_url, api_key, model, lang, source_str, context, retry):
"""Translate a string using LLM with retry logic"""
# Format context examples
context_str = "\n".join([
f'"{k}" : "{v}"'
for k, v in context
])
# Prepare translation request
system_msg = f"You are a professional translator. Translate English strings to {lang}. Only output the translation without any additional text, quotes, or explanations. Do NOT change date formats, quotation marks or decimal separators."
user_msg = (
f"Here are translation examples:\n\n{context_str}\n\n"
f"Translate this string to {lang}:\n\"{source_str}\""
)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg}
],
"max_tokens": 1000
}
# Construct full endpoint URL
endpoint = f"{api_url.rstrip('/')}/chat/completions"
# Retry logic
for attempt in range(retry + 1):
try:
response = requests.post(
endpoint,
headers=headers,
json=payload,
timeout=30
)
response.raise_for_status()
result = response.json()
# Extract translation
translation = result["choices"][0]["message"]["content"].strip()
if not translation:
raise ValueError("Empty translation response")
return translation
except (requests.exceptions.RequestException,
KeyError,
IndexError,
ValueError) as e:
if attempt < retry:
wait = 2 ** attempt
print(f" Retry {attempt+1}/{retry} in {wait}s: {str(e)}", file=sys.stderr)
time.sleep(wait)
else:
raise RuntimeError(f"Translation failed after {retry} retries") from e
def main():
parser = argparse.ArgumentParser(description='Translate empty locale strings using LLM')
parser.add_argument('json_file', type=str, help='Locale JSON file to process')
parser.add_argument('--api-url', required=True, help='LLM API base URL (endpoint will be api-url/chat/completions)')
parser.add_argument('--api-key', required=True, help='API authentication key')
parser.add_argument('--model', required=True, help='LLM model name')
parser.add_argument('--lang', required=True, help='Target language (e.g., "German")')
parser.add_argument('--nctx', type=int, default=5, help='Number of context strings on EACH side (preceding/following) (default: 5)')
parser.add_argument('--retry', type=int, default=3, help='Retry attempts per string (default: 3)')
parser.add_argument('--indent', default='4', help='JSON indentation (integer for spaces, or string like "\\t") (default: 4)')
args = parser.parse_args()
# Load locale data with preserved order
try:
data = load_json_ordered(args.json_file)
except Exception as e:
print(f"Error loading JSON file: {e}", file=sys.stderr)
sys.exit(1)
# Track failed keys to prevent infinite loops
failed_keys = set()
processed = 0
total_empty = sum(1 for v in data.values() if v == "")
print(f"Found {total_empty} untranslated strings. Starting translation...")
while True:
# Find next untranslated string (skipping previously failed)
current_key = None
for key, value in data.items():
if value == "" and key not in failed_keys:
current_key = key
break
if not current_key:
break # No more untranslated strings
print(f"\nTranslating: '{current_key}'")
context_pre, context_post = get_context(data, current_key, args.nctx)
# Show context for debugging
if context_pre:
for (k, v) in context_pre:
print(f" '{k}': '{v}'")
print(f" > '{current_key}': ''")
if context_post:
for (k, v) in context_post:
print(f" '{k}': '{v}'")
try:
translation = translate_string(
api_url=args.api_url,
api_key=args.api_key,
model=args.model,
lang=args.lang,
source_str=current_key,
context=(context_pre + context_post),
retry=args.retry
)
data[current_key] = translation
save_json_ordered(args.json_file, data, args.indent)
processed += 1
print(f" ✅ Success: '{translation}'")
except Exception as e:
print(f" ❌ Failed: {str(e)}", file=sys.stderr)
failed_keys.add(current_key)
# Final status
remaining = total_empty - processed
print(f"\nTranslation complete. Processed: {processed}, Remaining: {remaining}")
if failed_keys:
print("Failed keys (check logs for details):")
for key in failed_keys:
print(f" - {key}")
if __name__ == "__main__":
main()
```
**Thoroughly** curated the result, including a few select minor changes to existing translations. Every single string has been reviewed and should be *acceptable* at a minimum.
Not every string has been inspected for looks/fit in actual deployment, given the high volume of translated strings.
### Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
---
<sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/17399
Author: @Ithanil
Created: 9/12/2025
Status: ✅ Merged
Merged: 9/12/2025
Merged by: @tjbck
Base:
dev← Head:german_i18n📝 Commits (1)
c9e69fb100% german translation (~ 250 strings added/changed)📊 Changes
1 file changed (+249 additions, -249 deletions)
View changed files
📝
src/lib/i18n/locales/de-DE/translation.json(+249 -249)📄 Description
Pull Request Checklist
Note to first-time contributors: Please open a discussion post in Discussions and describe your changes before submitting a pull request.
Before submitting, make sure you've checked the following:
devbranch.Changelog Entry
Description
Pre-translated all missing strings using Qwen3 235B 2507 with the following script:
Thoroughly curated the result, including a few select minor changes to existing translations. Every single string has been reviewed and should be acceptable at a minimum.
Not every string has been inspected for looks/fit in actual deployment, given the high volume of translated strings.
Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the Contributor License Agreement (CLA), and I am providing my contributions under its terms.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.