A cross-platform CLI/TUI for managing .resx files with free AI translation, code scanning, and CI/CD automation
The Problem Every .NET Developer Faces
You've built a great .NET application. Now you need to support multiple languages. You create your first .resx file, add some keys, and everything works beautifully in English. Then the requests come in: "Can we support French? German? Greek?"
You create Resources.fr.resx, copy all the keys, send them to a translator, paste the translations back... and your build breaks. An XML tag got corrupted. A key is missing. A duplicate slipped in. You're now manually diff-ing XML files trying to figure out what went wrong.
If you're on Linux or macOS, it's even worse. Visual Studio's ResX Resource Manager? Windows-only. Editing XML by hand? Error-prone and painful. You end up writing bash scripts that barely work, or worse, you spin up a Windows VM just to manage resource files.
There had to be a better way.
Understanding .NET Localization: The Foundation
Before we dive into the solution, let's understand how .NET actually handles localization. If you've ever wondered what those .resx files really are and how they work at runtime, this section is for you.
What Are .resx Files?
Resource files (.resx) are XML-based files that store localized strings, images, and other resources. Here's what a typical resource file looks like:
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="WelcomeMessage" xml:space="preserve">
<value>Welcome to our application!</value>
<comment>Shown on the home page</comment>
</data>
<data name="SaveButton" xml:space="preserve">
<value>Save</value>
</data>
<data name="CancelButton" xml:space="preserve">
<value>Cancel</value>
</data>
</root>
Each <data> element contains:
- name: The key you use in code
- value: The translated text
- comment (optional): Context for translators
The Resource File Naming Convention
.NET uses a specific naming convention to organize translations:
Resources/
├── SharedResources.resx ← Default/invariant culture (usually English)
├── SharedResources.fr.resx ← French
├── SharedResources.de.resx ← German
├── SharedResources.el.resx ← Greek
└── SharedResources.fr-CA.resx ← French Canadian (specific culture)
The pattern is simple: {BaseName}.{culture-code}.resx
How the ResourceManager Works
When your application runs, .NET's ResourceManager automatically loads the correct resource file based on the current culture:
User's Culture: fr-CA (French Canadian)
│
↓
Look for: SharedResources.fr-CA.resx
│
↓ (not found)
Fall back to: SharedResources.fr.resx
│
↓ (not found)
Fall back to: SharedResources.resx (default)
This fallback mechanism ensures users always see something, even if a specific translation is missing. But it also means missing translations can go unnoticed until production.
The Culture Fallback Chain
┌─────────────────────────────────────────────────────────┐
│ Application Runtime │
│ │
│ CurrentUICulture: "el-GR" (Greek - Greece) │
└────────────────────┬────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ ResourceManager.GetString("WelcomeMessage") │
└────────────┬────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ 1. Try: Resources.el-GR.resx │
│ File not found │
└────────────┬────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ 2. Try: Resources.el.resx │
│ Found! Return Greek translation │
└─────────────────────────────────────────────────┘
│
↓ (if not found)
┌─────────────────────────────────────────────────┐
│ 3. Fallback: Resources.resx │
│ Return default (English) │
└─────────────────────────────────────────────────┘
Common Pain Points in .NET Localization
Through building LocalizationManager, I encountered (and solved) these recurring issues:
-
XML Corruption: One misplaced
<or&breaks your entire build - Missing Translations: Keys exist in the default file but not in translated files
- Duplicate Keys: Same key defined twice = build error
-
Empty Values: Is an empty
<value></value>intentional or a mistake? -
Sync Issues: You add a key to
Resources.resxbut forget to add it to all language files - No Validation: You only discover issues when the build fails
- Manual Translation: Expensive, slow, and doesn't scale
Why Existing Tools Fall Short
- Visual Studio: Windows-only, requires full IDE installation
- ResX Resource Manager: Excellent tool, but also Windows-only
- Manual XML Editing: Works everywhere, but error-prone and lacks validation
- Online Tools: Require uploading sensitive data, no local workflow integration
- Custom Scripts: Everyone reinvents the wheel poorly
The gap was clear: We needed a cross-platform, command-line-first tool that could handle the entire localization workflow from validation to automated translation to CI/CD integration.
The Solution: LocalizationManager
I built LocalizationManager (lrm) to be the tool I wished existed when I started working with .NET localization on Linux.
Core Philosophy
- Cross-platform first: Native binaries for Linux, Windows, macOS, including ARM64
- CLI-first, with a bonus TUI: Perfect for automation and CI/CD, but also great for interactive use
- Complete workflow: Not just a translator, but the entire pipeline
- Modern approach: AI translation with caching, smart batch processing
- Developer-friendly: JSON output, exit codes, extensive documentation
Key Features at a Glance
- ✅ Validation: Detect missing translations, duplicates, XML errors
- 📊 Statistics: Translation coverage with progress bars
- 🔍 Code Scanning: Find unused keys and missing references in your source code
- 🌐 Auto-Translation: 7 providers including Google, DeepL, OpenAI, Claude, and local Ollama
- 📺 Interactive TUI: Visual editing with keyboard shortcuts
- 🚀 CI/CD Ready: JSON output, exit codes, GitHub Actions integration
- 💾 Import/Export: CSV format for working with translators
- 📝 Full CLI: 15+ commands for every localization task
Architecture & Technical Design
Let me share some of the key architectural decisions that make LocalizationManager work well.
Technology Stack
- Language: C# / .NET 9.0
- TUI Framework: Spectre.Console (amazing for rich terminal interfaces)
- CLI Framework: Spectre.Console.Cli (command routing and parsing)
- Translation APIs: Native HTTP clients (no heavy SDK dependencies)
- Caching: SQLite for translation cache
- Packaging: Self-contained single-file executables
Translation Provider Architecture
One of the most important design decisions was creating an abstraction for translation providers. This allows users to choose the best provider for their needs while keeping the code maintainable.
┌─────────────────────────────────────────────────────────────┐
│ TranslationProviderFactory │
│ │
│ Create(providerName) → ITranslationProvider │
└───────────────────┬─────────────────────────────────────────┘
│
┌───────────┴───────────┬──────────────┬──────────────┐
↓ ↓ ↓ ↓
┌───────────────┐ ┌───────────────┐ ┌──────────┐ ┌──────────┐
│ GoogleProvider│ │ DeepLProvider│ │ Ollama │ │ OpenAI │
│ │ │ │ │ Provider │ │ Provider │
│ - Translate │ │ - Translate │ │ │ │ │
│ - Batch │ │ - Batch │ │ - Local │ │ - GPT-4 │
│ - Cache │ │ - Cache │ │ - Free │ │ - Claude │
└───────────────┘ └───────────────┘ └──────────┘ └──────────┘
│ │ │ │
└───────────────────────┴──────────────┴──────────────┘
│
┌────────────▼─────────────┐
│ TranslationCache │
│ (SQLite, 30-day) │
│ │
│ - Reduces API costs │
│ - Provider-specific │
│ - SHA-256 cache keys │
└──────────────────────────┘
Key Benefits:
- Add new providers without changing existing code
- Each provider handles its own rate limiting
- Centralized caching reduces costs by 80%+
- Providers can be traditional (Google) or AI-powered (Claude)
Using the Translation System
Switch between providers and control translation behavior:
# Translate all missing keys
lrm translate --only-missing --provider google
# Use specific provider
lrm translate --provider ollama # Free local translation
lrm translate --provider deepl # High quality
# Target specific languages only
lrm translate --target-languages fr,de,es --provider google
# Re-translate specific keys
lrm translate "Error*" --provider deepl --overwrite
# Preview before saving
lrm translate --only-missing --dry-run
# Batch control for rate limiting
lrm translate --batch-size 5 --provider google
Key options:
-
--provider- google, deepl, openai, claude, ollama, libretranslate, azureopenai, azuretranslator -
--only-missing- Skip keys with existing translations -
--target-languages- Comma-separated language codes (fr,de,es) -
--overwrite- Allow re-translating existing values (use with key pattern) -
--dry-run- Preview without saving -
--batch-size- Control concurrent API calls -
--no-cache- Bypass the 30-day SQLite cache
Caching: All translations cached 30 days in ~/.local/share/LocalizationManager/translations.db, reducing API costs by 80%+.
The AI Translation Revolution
Here's something exciting: we added support for modern AI translation providers alongside traditional machine translation services.
Provider Comparison:
| Provider | Type | Quality | Privacy | API Key Required |
|---|---|---|---|---|
| Google Translate | Traditional MT | High | Cloud | Yes |
| DeepL | Traditional MT | Highest (EU) | Cloud | Yes |
| LibreTranslate | Traditional MT | Good | Self-host | Optional |
| OpenAI GPT | AI (LLM) | Excellent | Cloud | Yes |
| Claude | AI (LLM) | Excellent | Cloud | Yes |
| Ollama | AI (Local LLM) | Very Good | Local | No |
| Azure OpenAI | AI (LLM) | Excellent | Enterprise | Yes |
Why AI Translation Matters:
- Context Awareness: LLMs understand context better than traditional MT
- Technical Terminology: Better at preserving technical terms and code snippets
- Tone Preservation: Maintains the original tone (formal, casual, technical)
- Free Option: Ollama runs locally for free with surprisingly good results
- Privacy: With Ollama, your data never leaves your machine
Code Scanning: Keeping Code and Resources in Sync
One unique feature is the code scanner that analyzes your source code to find localization key usage:
# Scan C#, Razor, XAML files for key usage
lrm scan --source-path ./src --format json
# Output shows:
# ✓ Scanned 245 files
# Found 156 key references
# ⚠ 3 keys used in code but missing from .resx
# ⚠ 12 keys in .resx but never used
The scanner detects patterns like:
Resources.WelcomeMessageGetString("SaveButton")Translate("ErrorText")-
{x:Static res:Strings.PageTitle}(XAML) -
@Resources.Message(Razor)
The scan command reports the issues - you then use other commands to fix them:
# 1. Scan and save results
lrm scan --format json > scan-results.json
# 2. Add missing keys interactively (prompts for proper values)
jq -r '.missingKeys[]?.key // empty' scan-results.json | while read key; do
lrm add "$key" -i # Interactive mode - prompts for values in each language
done
# OR: Add as placeholders for manual review later
jq -r '.missingKeys[]?.key // empty' scan-results.json | while read key; do
lrm add "$key" --lang default:"$key" --comment "TODO: Add proper text (from code scan)"
done
# ⚠️ Important: Edit .resx files to add proper English text before running translate!
# 3. After adding proper values, then translate
lrm translate --only-missing
This helps prevent:
- Dead keys in resource files (unused keys)
- Missing keys that will fail at runtime
- Localization drift over time
The Complete Workflow: From Chaos to Automation
Here's the workflow that LocalizationManager enables, from manual pain to full automation:
Before: The Manual Nightmare
Developer adds English string
↓
Export .resx to Excel/CSV somehow
↓
Email to translator (wait 3 days)
↓
Receive translated Excel/CSV
↓
Manually copy-paste into .resx files
↓
Build fails (XML corruption)
↓
Fix XML manually
↓
Build succeeds, but French is missing 3 keys
↓
Repeat entire process for those 3 keys
↓
Week wasted
After: The Automated Flow
┌──────────────────────────────────────────────────────────────┐
│ │
│ Step 1: VALIDATE │
│ ├─ Check XML syntax │
│ ├─ Find duplicate keys │
│ └─ Detect empty values │
│ │
└────────────────┬─────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ │
│ Step 2: SCAN CODE (optional) │
│ ├─ Find keys used in source files │
│ ├─ Detect missing keys (in code but not in .resx) │
│ ├─ Detect unused keys (in .resx but not in code) │
│ └─ Generate JSON report for processing │
│ │
└────────────────┬─────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ │
│ Step 2a: ADD MISSING KEYS (if scan found any) │
│ ├─ Parse scan results JSON │
│ ├─ Use 'lrm add' command for each missing key │
│ └─ Keys now added to default .resx file │
│ │
└────────────────┬─────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ │
│ Step 3: CHECK MISSING │
│ ├─ Find untranslated keys │
│ ├─ Generate report per language │
│ └─ Count: 47 keys need translation │
│ │
└────────────────┬─────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ │
│ Step 4: AUTO-TRANSLATE │
│ ├─ Batch translate missing keys │
│ ├─ Use cache for repeated strings │
│ ├─ Apply rate limiting │
│ └─ Result: 47/47 translations completed │
│ │
└────────────────┬─────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ │
│ Step 5: RE-VALIDATE │
│ ├─ Verify all XML is still valid │
│ ├─ Confirm no keys were corrupted │
│ └─ Status: All checks passed │
│ │
└────────────────┬─────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ │
│ Step 6: COMMIT & PR │
│ ├─ Git commit with detailed message │
│ ├─ Create pull request │
│ └─ Time elapsed: 2 minutes │
│ │
└──────────────────────────────────────────────────────────────┘
Running the Workflow: Five Ways
Choose the approach that matches your team's needs:
- Manual CLI - Step-by-step learning and exploration
- Bash Script A - Auto-translate existing keys with proper values
- Bash Script B - Scan code and add placeholders for review
- Ollama (Free!) - Private local translation, zero cost 🆓
- Duplicate Cleanup - Merge or delete duplicate keys
1. Manual CLI (step by step):
# Step 1: Validate
lrm validate --path ./Resources
# Step 2: Check what's missing
lrm validate --missing-only --format json > missing.json
# Step 3: Translate only what's missing
lrm translate --only-missing --provider openai
# Step 4: Verify
lrm validate
2. Automated Script (Option A - Translate Existing Keys):
#!/bin/bash
# Workflow for auto-translating keys that already have proper English text
set -e
# Validate
lrm validate
# Translate missing translations
lrm translate --only-missing --provider google
# Final validation
lrm validate
# Commit and create PR
git add Resources/
git commit -m "🌐 Auto-translate missing localization keys"
gh pr create --title "Auto-translate localizations"
2b. Automated Script (Option B - Scan and Add Keys for Review):
#!/bin/bash
# Workflow for scanning code and adding placeholder keys that need manual review
set -e
# Validate
lrm validate
# Scan code and add missing keys
lrm scan --source-path ./src --format json > scan-results.json
if [ -s scan-results.json ]; then
jq -r '.missingKeys[]?.key // empty' scan-results.json | while read key; do
[ -n "$key" ] && lrm add --key "$key" --value "$key" --comment "TODO: Add proper English text (from code scan)"
done
fi
# Final validation
lrm validate
# Commit and create PR for manual review
git add Resources/
git commit -m "📋 Add placeholder keys from code scan - manual review required"
gh pr create --title "Review and add text for new localization keys" --body "⚠️ These keys were added with placeholder values. Please edit the .resx files to add proper English text before running auto-translation."
⚠️ Important: Option B adds keys with placeholder values (key name as the value). Do NOT run
lrm translateuntil after manually editing the .resx files to add proper English text. Otherwise, it will translate the placeholder "WelcomeMessage" literally instead of your intended English message.
3a. GitHub Actions - Translate Existing Keys:
name: Auto-Translate Existing Keys
on:
push:
paths: ['Resources/**/*.resx'] # Only .resx changes
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup LRM
run: |
wget https://github.com/nickprotop/LocalizationManager/releases/latest/download/lrm-linux-x64.tar.gz
tar -xzf lrm-linux-x64.tar.gz
chmod +x linux-x64/lrm
echo "$PWD/linux-x64" >> $GITHUB_PATH
- name: Validate → Translate → Validate
env:
LRM_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
# Validate
lrm validate
# Translate all missing translations
lrm translate --only-missing --provider openai
# Final validation
lrm validate
- name: Commit Changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Resources/
git commit -m "🌐 Auto-translate missing localization keys" || echo "No changes"
git push
3b. GitHub Actions - Scan and Add Placeholders:
name: Scan Code and Add Placeholders
on:
push:
paths: ['src/**/*.cs'] # Only code changes
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup LRM
run: |
wget https://github.com/nickprotop/LocalizationManager/releases/latest/download/lrm-linux-x64.tar.gz
tar -xzf lrm-linux-x64.tar.gz
chmod +x linux-x64/lrm
echo "$PWD/linux-x64" >> $GITHUB_PATH
- name: Scan → Add → Create PR
run: |
# Validate
lrm validate
# Scan code for missing keys
lrm scan --source-path ./src --format json > scan-results.json
# Add missing keys found in code
jq -r '.missingKeys[]?.key // empty' scan-results.json | while read key; do
if [ -n "$key" ]; then
lrm add --key "$key" --value "$key" --comment "TODO: Add proper English text (from code scan)"
fi
done
# Validate additions
lrm validate
- name: Create PR for Review
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b scan/add-keys-$(date +%s)
git add Resources/
git commit -m "📋 Add placeholder keys from code scan" || exit 0
git push --set-upstream origin HEAD
gh pr create \
--title "Review: Add proper text for new localization keys" \
--body "⚠️ **Manual Review Required**
These keys were added with placeholder values (key name as value).
**Next steps:**
1. Edit the .resx files to replace placeholder values with proper English text
2. After merging, the auto-translate workflow will handle other languages"
⚠️ Important: These are two separate workflows. Workflow 3a handles translation of keys that already have proper English text. Workflow 3b adds new keys found in code but does NOT translate them - it creates a PR for manual review first.
4. Privacy-First Local Translation (Zero Cost with Ollama):
The best translation is free! Use Ollama to translate locally without cloud APIs:
#!/bin/bash
# One-time setup: Install Ollama and pull a model
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3.2
# Validate resources
lrm validate
# Translate locally - no cloud, no cost, no data leaving your machine
lrm translate --only-missing --provider ollama
# Validate
lrm validate
Why Ollama is amazing:
- 💰 $0 cost - Unlimited translations forever
- 🔒 Complete privacy - Data never leaves your machine
- ✈️ Works offline - No internet required after model download
- 🔑 No API keys - One less secret to manage
- 🎯 Good quality - llama3.2 handles technical UI text well
- ⚖️ GDPR/HIPAA friendly - Perfect for sensitive data
Perfect for:
- Medical records UI (HIPAA compliance)
- Banking applications (PCI/SOX compliance)
- Internal tools (security policies)
- Bootstrapped projects (zero budget)
- High-volume needs (no quota limits)
5. Clean Up Duplicate Keys (Merge or Delete):
Resource files can accumulate duplicate keys from merges, imports, or accidents. LRM helps clean them up:
#!/bin/bash
# Step 1: Detect duplicates
lrm validate --format json > validation.json
duplicate_count=$(jq '[.issues[]? | select(.type=="Duplicate")] | length' validation.json)
if [ "$duplicate_count" -gt 0 ]; then
echo "⚠️ Found $duplicate_count duplicate keys"
# Step 2: View duplicates
lrm validate # Shows table with duplicate warnings
# Step 3: Choose your cleanup strategy
# Option A: Merge interactively (choose which value to keep per language)
lrm merge-duplicates ErrorMessage
# Shows:
# ErrorMessage [1]: "An error occurred"
# ErrorMessage [2]: "Error occurred"
# Which to keep for default language? [1/2]: 1
# (repeats for each language)
# Option B: Auto-merge keeping first occurrence
lrm merge-duplicates ErrorMessage --auto-first -y
# Option C: Merge ALL duplicates at once (keeps first of each)
lrm merge-duplicates --all --auto-first -y
# Option D: Delete all occurrences entirely
lrm delete ErrorMessage --all-duplicates -y
# Step 4: Validate cleanup
lrm validate
echo "✅ Duplicates cleaned up"
fi
Common scenarios that create duplicates:
- ❌ Git merge conflicts in .resx files
- ❌ Importing CSV files with existing keys
- ❌ Manual XML editing mistakes
- ❌ Merging resource files from different branches
Interactive merge example:
$ lrm merge-duplicates WelcomeMessage
Found 2 occurrences of 'WelcomeMessage':
Default language:
[1] "Welcome to our app!"
[2] "Welcome to our application!"
Which to keep? [1/2]: 1
French (fr):
[1] "Bienvenue dans notre app !"
[2] "Bienvenue dans notre application !"
Which to keep? [1/2]: 2
✓ Merged WelcomeMessage: kept occurrence 1 for default, occurrence 2 for fr
Pro tip: Always validate after cleanup to ensure no issues remain!
Real-World Usage Examples
Let's see LocalizationManager in action with real examples.
Installation (One Command)
# Linux/macOS
curl -sSL https://raw.githubusercontent.com/nickprotop/LocalizationManager/main/install-lrm.sh | bash
# Windows (PowerShell)
iwr -useb https://raw.githubusercontent.com/nickprotop/LocalizationManager/main/install-lrm.ps1 | iex
That's it. No dependencies, no .NET SDK required. Self-contained binary.
Basic Workflow
# Navigate to your Resources folder
cd MyApp/Resources
# Check current status
lrm stats
# Output:
# ┌──────────────────┬───────┬──────────┬───────────┐
# │ Language │ Keys │ Coverage │ File Size │
# ├──────────────────┼───────┼──────────┼───────────┤
# │ English (en) │ 252 │ 100.0% │ 45.2 KB │
# │ French (fr) │ 245 │ 97.2% │ 43.1 KB │
# │ German (de) │ 238 │ 94.4% │ 41.8 KB │
# │ Greek (el) │ 252 │ 100.0% │ 46.3 KB │
# └──────────────────┴───────┴──────────┴───────────┘
# Translate missing French and German keys
lrm translate --only-missing --target-languages fr,de --provider openai
# Output:
# 🌐 Translating 14 keys to 2 languages...
# ✓ fr: 7/7 keys translated
# ✓ de: 7/7 keys translated
# ✓ Translations saved
# ⚡ Used cache for 3 translations
Interactive TUI Mode
# Launch the Terminal UI
lrm edit
╔═══════════════════════════════════════════════════════════╗
║ LocalizationManager - Interactive Editor ║
╠═══════════════════════════════════════════════════════════╣
║ ║
║ Key: WelcomeMessage ║
║ ┌─────────────────────────────────────────────────────┐ ║
║ │ Default (en): Welcome to our application! │ ║
║ │ French (fr): Bienvenue dans notre application! │ ║
║ │ German (de): Willkommen in unserer Anwendung! │ ║
║ │ Greek (el): Καλώς ήρθατε στην εφαρμογή μας! │ ║
║ └─────────────────────────────────────────────────────┘ ║
║ ║
║ [Ctrl+T] Translate [Ctrl+S] Save [Ctrl+Q] Quit ║
╚═══════════════════════════════════════════════════════════╝
AI Translation Comparison
Let's translate a technical string with different providers:
Source (English):
"Click the Save button to persist your changes to the database."
Traditional MT (Google Translate) → French:
"Cliquez sur le bouton Enregistrer pour conserver vos modifications dans la base de données."
Accurate but mechanical
AI (GPT-4o-mini) → French:
"Cliquez sur le bouton Enregistrer pour sauvegarder vos modifications dans la base de données."
More natural, preserves "Save" as "Enregistrer" (common UI term)
AI (Claude) → French:
"Cliquez sur le bouton Enregistrer pour sauvegarder définitivement vos modifications dans la base de données."
Adds "définitivement" (permanently) - excellent context awareness
Provider Features:
- Traditional APIs: Consistent quality, proven reliability
- AI (Cloud): Excellent context awareness, requires API key
- Ollama (Local): Free to use, runs on your machine, no API key needed ⭐
Configuration for AI Providers
Create lrm.json in your Resources folder:
{
"defaultLanguageCode": "en",
"translation": {
"defaultProvider": "openai",
"aiProviders": {
"openai": {
"model": "gpt-4o-mini",
"customSystemPrompt": "Translate UI text naturally, preserve technical terms",
"rateLimitPerMinute": 60
},
"ollama": {
"apiUrl": "http://localhost:11434",
"model": "llama3.2",
"rateLimitPerMinute": 10
}
}
}
}
Set your API keys:
# Via environment variables (recommended)
export LRM_OPENAI_API_KEY="sk-..."
export LRM_CLAUDE_API_KEY="sk-ant-..."
# Or via secure credential store
lrm config set-api-key --provider openai --key "sk-..."
# Ollama needs no API key!
Architecture Highlights: Why It Works
1. Self-Contained Binaries
Using .NET's publishing options, LocalizationManager compiles to a single, self-contained executable:
- No .NET runtime installation required
- No dependencies to manage
- ~50MB binary includes everything
- Fast startup (~100ms)
2. Cross-Platform Terminal UI
Spectre.Console makes the TUI work beautifully on all platforms:
Windows Command Prompt ✓
Windows Terminal ✓
PowerShell ✓
Linux Terminal ✓
macOS Terminal ✓
SSH Sessions ✓
3. JSON Output for Automation
Every command supports --format json for easy parsing:
lrm validate --format json | jq '.isValid'
# true
lrm stats --format json | jq '.statistics[].coverage'
# 100.0
# 97.2
# 94.4
Perfect for:
- CI/CD pipelines
- Custom dashboards
- Monitoring and alerts
- Integration with other tools
4. Intelligent Rate Limiting
Each translation provider has configurable rate limits to avoid hitting API quotas:
// Automatic rate limiting
public class RateLimiter
{
private readonly int _requestsPerMinute;
public async Task WaitAsync()
{
// Automatically throttles requests
// Tracks request times in sliding window
// Prevents API rate limit errors
}
}
5. Batch Processing with Fallback
Translations are processed in batches when providers support it:
// Try batch translation first
var batchResult = await TranslateBatchAsync(requests);
// Fall back to individual translation if batch fails
if (batchResult.HasErrors)
{
foreach (var request in failedRequests)
{
await TranslateAsync(request);
}
}
Getting Started
Try It Now
# Install
curl -sSL https://raw.githubusercontent.com/nickprotop/LocalizationManager/main/install-lrm.sh | bash
# Go to your Resources folder
cd YourProject/Resources
# Check status
lrm stats
# Validate everything
lrm validate
# Start interactive mode
lrm edit
Next Steps
- Read the docs: Full documentation
- Star on GitHub: GitHub Repository
- Try the workflow: Follow the Quick Start guide
- Set up CI/CD: Check out the CI/CD examples
- Join the community: Report issues, request features, contribute!
Contributing
LocalizationManager is MIT licensed and welcomes contributions:
- 🐛 Bug reports: Found an issue? Let us know!
- ✨ Feature requests: Have an idea? Open a discussion!
- 🔧 Pull requests: Code contributions welcome!
- 📖 Documentation: Help improve the docs!
Conclusion: Making .NET Localization Painless
LocalizationManager started as a personal itch I needed to scratch: managing .resx files on Linux without Windows tools. It evolved into a comprehensive solution that:
✅ Works on any platform (Linux, Windows, macOS, ARM64)
✅ Automates the complete workflow (validate → translate → commit → PR)
✅ Integrates modern AI translation (OpenAI, Claude, Ollama)
✅ Smart caching reduces redundant API calls
✅ Provides both CLI and TUI for automation and interactive use
✅ Scans your source code to keep resources in sync
✅ Integrates seamlessly with CI/CD pipelines
Whether you're a solo developer managing translations for a side project or a team handling enterprise-scale localization, LocalizationManager provides the tools you need to work efficiently.
The best part? It's completely free and open source. No subscriptions, no locked features, no vendor lock-in. Just a tool that works.
Links & Resources
- GitHub Repository: https://github.com/nickprotop/LocalizationManager
- Documentation: https://github.com/nickprotop/LocalizationManager/tree/main/docs
- Installation Guide: https://github.com/nickprotop/LocalizationManager/docs/INSTALLATION.md
- CI/CD Examples: https://github.com/nickprotop/LocalizationManager/docs/CICD.md
- Translation Guide: https://github.com/nickprotop/LocalizationManager/docs/TRANSLATION.md
- Issue Tracker: https://github.com/nickprotop/LocalizationManager/issues
Built with ❤️ by Nikolaos Protopapas
Licensed under MIT - Free to use, modify, and distribute
Did you find this article helpful? Please share it with your fellow .NET developers!
Tags: #dotnet #devops #opensource #localization #ai #linux #translation #csharp #cli #automation

Top comments (0)