Languages and localization
How Snipdeck localizes into 23 languages from one gettext .po per language, and how to add or update a translation.
On this page (5 sections)
Snipdeck is fully localized into 23 languages. On first launch it picks your language from the OS locale, and you can override it at any time from ⋯ → Language in the toolbar. This works the same on Windows and Linux. This page explains how that works under the hood and how to add or update a translation.
Supported languages
Snipdeck ships 23 UI languages. The list and the order below come straight from the LANGUAGES table in src/i18n.rs; English is first because it is the source language.
| Code | Language (endonym) |
|---|---|
en | English |
es | Español |
zh-CN | 简体中文 |
hi | हिन्दी |
ar | العربية |
pt | Português |
ru | Русский |
ja | 日本語 |
de | Deutsch |
fr | Français |
ko | 한국어 |
it | Italiano |
tr | Türkçe |
id | Bahasa Indonesia |
vi | Tiếng Việt |
pl | Polski |
uk | Українська |
nl | Nederlands |
fa | فارسی |
th | ไทย |
bn | বাংলা |
ur | اردو |
ro | Română |
Choosing a language
Automatic detection
When the language setting is "auto" (the default), Snipdeck resolves your language from the OS locale at startup. The resolver is best-effort and always falls back to English:
- A locale beginning with
zh(for examplezh-Hans-CN) maps tozh-CN. - Otherwise the primary subtag is taken (the part before
-or_, sotr-TRbecomestr) and matched case-insensitively against the supported codes. - If the locale is missing or unsupported, Snipdeck uses
en.
If you set language to an explicit, supported code in settings.json, that wins over auto-detection. An unsupported explicit code is ignored and the OS locale is consulted instead.
Switching from the ⋯ menu
To switch language at runtime, click ⋯ in the gallery toolbar and choose Language. The list shows each language by its own name. Picking a language does three things at once:
- Saves the chosen code to the
languagekey insettings.json. - Re-translates the bundled Slint UI live via
select_bundled_translation, so the interface updates immediately without a restart. - Rebuilds the Rust-provided card text (such as month labels and source labels) so it matches the new language too.
On Linux the tray menu is built once; it switches to the new language when Snipdeck restarts.
How localization works
The defining property of Snipdeck’s localization is that one gettext .po per language drives the entire app — both the Slint UI and the Rust-side messages — from a single catalog.
One catalog, two consumers
Every translatable string lives in exactly one place per language:
lang/<code>/LC_MESSAGES/snipdeck.poThat file feeds two consumers:
- The Slint UI through the
@tr("…")macro in the.slintfiles. - The Rust side through
i18n::t("…")(andi18n::t1(...)for strings with a placeholder), defined insrc/i18n.rs.
Both consumers look up the same English text as the msgid, so a single translated msgstr covers a string no matter which layer renders it.
Bundled at build time, no runtime gettext
Translations are compiled into the binary at build time, so there is no gettext C dependency and no external catalog files to ship. The Slint side is bundled in build.rs:
let config = slint_build::CompilerConfiguration::new()
.with_bundled_translations("lang")
.with_default_translation_context(slint_build::DefaultTranslationContext::None);Two details make the shared-catalog design possible:
with_bundled_translations("lang")embeds the.pocatalogs into the executable.DefaultTranslationContext::Nonedisables the translation context, so msgids are the bare English text. That is exactly what lets the same.pofiles also satisfy the Rust-sidei18n::t()lookups.
The build script re-runs whenever anything in lang/ changes (cargo:rerun-if-changed=lang), keeping the bundled UI translations in sync with the catalogs.
The Rust side embeds the same files independently with include_str!, one match arm per language in po_source(). Each catalog is parsed lazily into a msgid -> msgstr map the first time that language is used, then cached.
English is the source language
English needs no catalog. Because msgids are the English strings, i18n::t() returns its argument unchanged whenever the active language is English. The same fallback applies to any other language when a translation is missing or its msgstr is empty: Snipdeck falls back to the English msgid rather than showing a blank. This means a partially translated language is always usable — untranslated strings simply appear in English.
Adding or updating a translation
The template
lang/snipdeck.pot is the master template. It lists every translatable English string as a msgid with an empty msgstr, grouped by area with #. comments (toolbar buttons, tooltips, and so on). It is the canonical list of what needs translating.
To start a new language, copy the template to the language’s catalog path and fill in each msgstr:
cp lang/snipdeck.pot lang/<code>/LC_MESSAGES/snipdeck.poIn Windows PowerShell, the same is Copy-Item lang\snipdeck.pot lang\<code>\LC_MESSAGES\snipdeck.po.
A filled-in entry looks like this (from the Spanish catalog):
msgid "New"
msgstr "Nuevo"
msgid "Take a snip and copy it to the clipboard"
msgstr "Hacer un recorte y copiarlo al portapapeles"Leave each catalog header’s Language: field set to the language code (for example Language: es).
Updating an existing language
When new strings are added to the app, they are wrapped in @tr("…") (in .slint) or i18n::t("…") (in Rust), and the new msgid is added to lang/snipdeck.pot. To update a translation:
- Copy the new msgid into each
lang/<code>/LC_MESSAGES/snipdeck.po. - Fill in its
msgstrwith the translation. - Leave untouched any string you do not translate — it falls back to English automatically.
Preserving placeholders and escapes
The most important rule when translating: keep the structural tokens intact, in the right place, or the runtime substitution will break.
| Keep intact | What it is |
|---|---|
{}, {e}, {err}, {url}, {path} | Runtime placeholders substituted by Rust; do not translate or reorder away from their meaning |
\r\n, \n, \t, \", \\ | C-style escapes the .po parser unescapes |
•, —, →, ·, ▾, … | Literal punctuation and glyphs used in the UI |
The parser supported by Snipdeck handles standard msgid / msgstr pairs, C-style escapes, and multi-line continuation (consecutive quoted lines). It ignores comments, the header (the empty msgid), plurals, and msgctxt.
Do not translate
Leave these brand and technical tokens verbatim in every language: Snipdeck, OCR, Win, Ctrl+F, Ctrl+V, Imgur, imgur.com, imgur_client_id, catbox.moe, MAPI.
Adding a brand-new language
To wire up a language that does not exist yet, after creating its .po:
- Create
lang/<code>/LC_MESSAGES/snipdeck.po(copy the.pot, fill themsgstrvalues). - Add a
(code, endonym)entry to theLANGUAGEStable insrc/i18n.rs. - Add a matching
matcharm topo_source()insrc/i18n.rsthatinclude_str!s the new file. - Rebuild. The build script re-bundles the catalogs automatically.
// src/i18n.rs — both edits use the same <code>
pub const LANGUAGES: &[(&str, &str)] = &[
// …existing entries…
("xx", "Endonym"),
];
fn po_source(code: &str) -> Option<&'static str> {
Some(match code {
// …existing arms…
"xx" => include_str!("../lang/xx/LC_MESSAGES/snipdeck.po"),
_ => return None,
})
}See also
- Settings — the
languagekey and the rest ofsettings.json. - Architecture — where the
i18nmodule and thelang/catalogs sit in the codebase. - OCR + translate — translating a snip’s text, which is separate from the UI language.
- Keyboard shortcuts — modifier-key labels that stay verbatim across languages.
If something on this page is missing or wrong, write to [email protected].