Languages & localization
How Snipdeck localizes into 23 languages from one gettext .po per language, and how to add or update a translation.
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 the globe menu in the toolbar. 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ă |
Note: The
codevalue must match the folder name underlang/and the value passed to Slint’sselect_bundled_translation. The endonym is the label shown in the toolbar language menu.
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 toolbar
Use the globe menu in the toolbar to switch language at runtime. 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.
Tip: To pin a language regardless of the machine you run on, set
languageto a fixed code instead of"auto". See Settings for thesettings.jsonkeys.
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.po
That 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.
Note: Slint’s
select_bundled_translationis called after the main window exists, matching Slint’s documented order (create the component, then select the translation, then run). Selecting it earlier does not reach the first render.
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:
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.
Warning: A placeholder such as
{e}is replaced at runtime with a real value (an error message, a URL, a file path). If you drop it, the value has nowhere to go and the message reads incompletely. Always carry every placeholder from the English string into your translation.
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,
})
}
Tip: Once the language is in
LANGUAGESit appears in the toolbar globe menu automatically, andresolve_languagewill auto-detect it from a matching OS locale.
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.