Adding Languages to Ferrite
February 26, 2026 · View on GitHub
This guide explains how to add new translations to Ferrite and set up community translation workflows.
Current Status
| Component | Status |
|---|---|
| i18n Infrastructure | ✅ Complete |
| English (en) | ✅ Complete (base language) |
| Language Selector | ✅ In Settings → Appearance |
| Auto-detect System Locale | ✅ First launch detection |
| Translation Portal | 🔜 To be set up |
Quick Start: Adding a New Language
Step 1: Create the Locale File
Copy locales/en.yaml to a new file with the appropriate locale code:
# Examples:
cp locales/en.yaml locales/zh-CN.yaml # Chinese (Simplified)
cp locales/en.yaml locales/ja.yaml # Japanese
cp locales/en.yaml locales/ko.yaml # Korean
cp locales/en.yaml locales/de.yaml # German
cp locales/en.yaml locales/fr.yaml # French
Step 2: Translate the Strings
Edit the new file and translate each string value. Keep the keys (left side) unchanged:
# locales/zh-CN.yaml
app:
name: "Ferrite" # Keep product name
tagline: "快速、轻量的 Markdown 文本编辑器"
menu:
file:
label: "文件"
new: "新建"
open: "打开..."
save: "保存"
# ... translate all strings
Important:
- Keep YAML structure intact (indentation matters!)
- Keep placeholder variables like
%{filename},%{count},%{path}unchanged - Don't translate keys, only values
- Some strings (product names, technical terms) may stay in English
Step 3: Register the Language in Code
Edit src/config/settings.rs to add the new language variant:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Language {
#[default]
#[serde(rename = "en")]
English,
// Add new language:
#[serde(rename = "zh-CN")]
ChineseSimplified,
}
impl Language {
pub fn locale_code(&self) -> &'static str {
match self {
Language::English => "en",
Language::ChineseSimplified => "zh-CN",
}
}
pub fn native_name(&self) -> &'static str {
match self {
Language::English => "English",
Language::ChineseSimplified => "简体中文",
}
}
pub fn all() -> &'static [Language] {
&[
Language::English,
Language::ChineseSimplified,
]
}
pub fn from_locale_code(locale: &str) -> Option<Language> {
let normalized = locale.to_lowercase().replace('_', "-");
let primary_lang = normalized.split('-').next().unwrap_or(&normalized);
match primary_lang {
"en" => Some(Language::English),
"zh" => Some(Language::ChineseSimplified),
_ => None,
}
}
}
Step 4: Build and Test
cargo build
cargo run
# Or run tests:
cargo test settings::tests::test_language
Step 5: Test in the Application
- Delete
%APPDATA%\ferrite\config.json(Windows) or~/.config/ferrite/config.json(Linux/macOS) - Launch Ferrite - should detect system locale
- Go to Settings → Appearance → Language
- Select the new language
- Verify all UI strings are translated
Translation Portal Options
For community-driven translations, consider these platforms:
Option 1: Crowdin (Recommended)
Pros:
- Free for open-source projects
- Great UI for translators
- Supports YAML format
- Automatic PR creation for completed translations
- Translation memory and glossary
- Quality assurance checks
Setup:
- Sign up at crowdin.com
- Create new project → Open Source
- Add source file:
locales/en.yaml - Configure target languages
- Set up GitHub integration for automatic sync
Crowdin Config (.crowdin.yml):
project_id: "ferrite"
api_token_env: "CROWDIN_API_TOKEN"
base_path: "."
base_url: "https://api.crowdin.com"
files:
- source: /locales/en.yaml
translation: /locales/%locale%.yaml
Option 2: Weblate
Pros:
- Self-hostable (or use hosted version)
- Git-native workflow
- Strong privacy focus
- YAML support
Setup:
- Create project at hosted.weblate.org (free for FOSS)
- Connect to GitHub repository
- Configure YAML file format
- Add
locales/en.yamlas source
Critical (avoids broken PRs): In the component settings, set Monolingual base language file (or Template for new translations) to locales/en.yaml. That way Weblate mirrors the same key structure and nesting as the English file; translators only fill values. Without this, new keys can end up under wrong sections (e.g. find.about instead of about or settings.about) and the app’s t!("key.path") lookups fail. See Weblate PR 90 review for what went wrong and how to fix the component.
Option 3: POEditor
Pros:
- Simple interface
- Good for smaller projects
- Supports YAML
Option 4: Lokalise
Pros:
- AI-assisted translations
- Excellent for professional workflows
- GitHub integration
Translation Guidelines
For Translators
-
Keep placeholders intact:
# ✅ Correct message: "¿Desea guardar los cambios en \"%{filename}\"?" # ❌ Wrong - placeholder removed message: "¿Desea guardar los cambios en el archivo?" -
Preserve formatting:
# Keep newlines, special characters as in English -
Don't translate:
- Product name "Ferrite"
- Technical terms when no good equivalent exists
- Keyboard shortcuts (Ctrl, Shift, etc.)
-
Consider context:
- Menu items should be concise
- Tooltips can be more descriptive
- Error messages should be helpful
For Reviewers
- Check placeholder preservation
- Verify consistent terminology
- Test in context (some strings may be truncated in UI)
- Check for grammatical correctness
File Structure
locales/
├── en.yaml # English (base/source)
├── zh-CN.yaml # Chinese (Simplified)
├── zh-TW.yaml # Chinese (Traditional)
├── ja.yaml # Japanese
├── ko.yaml # Korean
├── de.yaml # German
├── fr.yaml # French
├── es.yaml # Spanish
└── pt-BR.yaml # Portuguese (Brazil)
Locale Codes Reference
| Language | Code | Native Name |
|---|---|---|
| English | en | English |
| Chinese (Simplified) | zh-CN | 简体中文 |
| Chinese (Traditional) | zh-TW | 繁體中文 |
| Japanese | ja | 日本語 |
| Korean | ko | 한국어 |
| German | de | Deutsch |
| French | fr | Français |
| Spanish | es | Español |
| Portuguese (Brazil) | pt-BR | Português (Brasil) |
| Russian | ru | Русский |
| Arabic | ar | العربية |
Testing Translations
Manual Testing Checklist
- All menu items display correctly
- Settings panel fully translated
- Dialogs (save, open, confirm) translated
- Error messages translated
- Keyboard shortcuts still work
- No text overflow/truncation issues
- Placeholder variables work (
%{filename}, etc.)
Automated Tests
# Run all i18n-related tests
cargo test i18n
cargo test settings::tests::test_language
# Check for missing keys (compare with en.yaml)
# TODO: Add validation script
String Categories
The en.yaml file is organized into these categories:
| Category | Description | Example Keys |
|---|---|---|
app | Application metadata | app.name, app.tagline |
menu | Menu bar items | menu.file.open, menu.edit.undo |
toolbar | Toolbar button labels | toolbar.new_file, toolbar.bold |
status | Status bar | status.line, status.modified |
dialog | Dialog boxes | dialog.unsaved_changes.* |
settings | Settings panel | settings.editor.font_size |
find | Find/Replace panel | find.placeholder, find.replace_all |
outline | Outline panel | outline.title, outline.no_headings |
sidebar | File tree sidebar | sidebar.files, sidebar.open_folder |
error | Error messages | error.file_not_found |
notification | Toast notifications | notification.file_saved |
shortcuts | Keyboard shortcuts dialog | shortcuts.category.* |
about | About dialog | about.version, about.description |
git | Git integration | git.modified, git.branch |
a11y | Accessibility labels | a11y.close_button |
Contributing Translations
Via GitHub
- Fork the repository
- Create locale file:
locales/<code>.yaml - Translate strings
- Update
src/config/settings.rs(add Language variant) - Submit PR with title:
i18n: Add <Language> translation
Via Translation Portal
(Once set up)
- Visit [translation portal URL]
- Select your language
- Translate strings in the web interface
- Translations are automatically synced to GitHub
Technical Details
rust-i18n Crate
Ferrite uses rust-i18n for internationalization:
// In src/main.rs
rust_i18n::i18n!("locales", fallback = "en");
// Usage in code
use rust_i18n::t;
let message = t!("menu.file.open"); // "Open..."
let msg = t!("dialog.unsaved_changes.message", filename = "test.md");
Locale Files Format
YAML format with nested keys:
category:
subcategory:
key: "Translated string"
key_with_placeholder: "Hello, %{name}!"
Fallback Behavior
If a translation is missing, rust-i18n falls back to English (en).
Next Steps
- Set up Crowdin (or chosen platform)
- Add priority languages: Chinese, Japanese, Korean
- Create validation script to check for missing keys
- Add CI checks for translation completeness
- Document contributor workflow on translation portal
Last updated: 2026-01-14