Home
TR

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.

CodeLanguage (endonym)
enEnglish
esEspañol
zh-CN简体中文
hiहिन्दी
arالعربية
ptPortuguês
ruРусский
ja日本語
deDeutsch
frFrançais
ko한국어
itItaliano
trTürkçe
idBahasa Indonesia
viTiếng Việt
plPolski
ukУкраїнська
nlNederlands
faفارسی
thไทย
bnবাংলা
urاردو
roRomână

Note: The code value must match the folder name under lang/ and the value passed to Slint’s select_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 example zh-Hans-CN) maps to zh-CN.
  • Otherwise the primary subtag is taken (the part before - or _, so tr-TR becomes tr) 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:

  1. Saves the chosen code to the language key in settings.json.
  2. Re-translates the bundled Slint UI live via select_bundled_translation, so the interface updates immediately without a restart.
  3. 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 language to a fixed code instead of "auto". See Settings for the settings.json keys.

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 .slint files.
  • The Rust side through i18n::t("…") (and i18n::t1(...) for strings with a placeholder), defined in src/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 .po catalogs into the executable.
  • DefaultTranslationContext::None disables the translation context, so msgids are the bare English text. That is exactly what lets the same .po files also satisfy the Rust-side i18n::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_translation is 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:

  1. Copy the new msgid into each lang/<code>/LC_MESSAGES/snipdeck.po.
  2. Fill in its msgstr with the translation.
  3. 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 intactWhat 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:

  1. Create lang/<code>/LC_MESSAGES/snipdeck.po (copy the .pot, fill the msgstr values).
  2. Add a (code, endonym) entry to the LANGUAGES table in src/i18n.rs.
  3. Add a matching match arm to po_source() in src/i18n.rs that include_str!s the new file.
  4. 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 LANGUAGES it appears in the toolbar globe menu automatically, and resolve_language will auto-detect it from a matching OS locale.

See also

  • Settings — the language key and the rest of settings.json.
  • Architecture — where the i18n module and the lang/ 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.