Noctalia plugins can support multiple languages through the translation system. When the user changes Noctalia’s language, your plugin automatically uses the appropriate translations.
Directory Structure
Translation files are stored in an i18n directory inside your plugin:
my-plugin/
├── manifest.json
├── BarWidget.qml
├── Panel.qml
└── i18n/
├── en.json # English (required - fallback)
├── es.json # Spanish
├── de.json # German
├── fr.json # French
└── ...
Translation File Format
Each translation file is a JSON object with nested keys:
i18n/en.json (English, required):
{
"widget": {
"title": "My Widget",
"status": "Active"
},
"panel": {
"header": "Settings",
"save": "Save Changes",
"cancel": "Cancel"
},
"messages": {
"welcome": "Welcome, {name}!",
"items": "{count} item",
"items_plural": "{count} items"
}
}
i18n/es.json (Spanish):
{
"widget": {
"title": "Mi Widget",
"status": "Activo"
},
"panel": {
"header": "Configuracion",
"save": "Guardar Cambios",
"cancel": "Cancelar"
},
"messages": {
"welcome": "Bienvenido, {name}!",
"items": "{count} elemento",
"items_plural": "{count} elementos"
}
}
i18n/de.json (German):
{
"widget": {
"title": "Mein Widget",
"status": "Aktiv"
},
"panel": {
"header": "Einstellungen",
"save": "Anderungen speichern",
"cancel": "Abbrechen"
},
"messages": {
"welcome": "Willkommen, {name}!",
"items": "{count} Element",
"items_plural": "{count} Elemente"
}
}
Using Translations
Basic Translation: tr()
Use pluginApi.tr() to translate strings:
Item {
property var pluginApi: null
NText {
// Simple translation
text: pluginApi?.tr("widget.title")
}
NText {
// Nested key access
text: pluginApi?.tr("panel.header")
}
}
With Interpolations
Pass dynamic values using interpolations:
// Translation file: "welcome": "Welcome, {name}!"
NText {
text: pluginApi?.tr("messages.welcome", { name: userName })
// Result: "Welcome, John!"
}
// Translation file: "status": "Connected to {server} on port {port}"
NText {
text: pluginApi?.tr("connection.status", {
server: serverName,
port: portNumber
})
// Result: "Connected to localhost on port 8080"
}
Plural Forms: trp()
Use pluginApi.trp() for pluralization:
// Translation file:
// "items": "{count} item"
// "items_plural": "{count} items"
NText {
text: pluginApi?.trp(
"messages.items", // Base key
itemCount, // Count for plural logic
{} // Interpolations
)
}
The system automatically appends _plural to the key when count !== 1:
count = 1→ uses"messages.items"→ “1 item”count = 5→ uses"messages.items_plural"→ “5 items”
Check Translation Exists: hasTranslation()
Check if a translation key exists before using it:
if (pluginApi?.hasTranslation("optional.feature")) {
text = pluginApi.tr("optional.feature")
} else {
text = "Default text"
}
Current Language
Access the current language code:
// Get current language
readonly property string currentLang: pluginApi?.currentLanguage || "en"
// React to language changes
onCurrentLangChanged: {
Logger.i("MyPlugin", "Language changed to:", currentLang)
}
Fallback Behavior
The translation system uses this fallback order:
- Current language translation (e.g.,
i18n/de.json) - English translation (
i18n/en.json) - The key itself wrapped in
!! !!(e.g.,!! widget.title !!)
Supported Languages
Noctalia supports these language codes:
| Code | Language |
|---|---|
ar | Arabic |
be | Belarusian |
be-Latn | Belarusian (Latin) |
ca | Catalan |
cs | Czech |
de | German |
en | English |
es | Spanish |
fr | French |
gl-ES | Galician |
he | Hebrew |
hu | Hungarian |
it | Italian |
ko | Korean |
ku | Kurdish |
nl | Dutch |
nn | Norwegian Nynorsk |
pl | Polish |
pt-BR | Portuguese (Brazil) |
ru | Russian |
sv | Swedish |
tr | Turkish |
uk-UA | Ukrainian |
vi | Vietnamese |
zh-Hans | Chinese (Simplified) |
Create translation files matching these codes to support them.
Complete Example
Plugin Structure
weather-plugin/
├── manifest.json
├── BarWidget.qml
├── Panel.qml
└── i18n/
├── en.json
├── es.json
└── de.json
i18n/en.json
{
"widget": {
"title": "Weather",
"loading": "Loading...",
"error": "Unable to fetch weather"
},
"panel": {
"header": "Weather Details",
"temperature": "Temperature",
"humidity": "Humidity",
"wind": "Wind Speed",
"forecast": "Forecast"
},
"status": {
"updated": "Updated {time} ago",
"location": "Weather for {city}"
},
"days": {
"count": "{count} day",
"count_plural": "{count} days"
}
}
BarWidget.qml
Rectangle {
id: root
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property bool loading: false
property string temperature: "72°F"
implicitWidth: row.implicitWidth + Style.marginM * 2
implicitHeight: Style.barHeight
color: Style.capsuleColor
radius: Style.radiusM
RowLayout {
id: row
anchors.centerIn: parent
spacing: Style.marginS
NIcon {
icon: "sun"
color: Color.mPrimary
}
NText {
text: root.loading
? pluginApi?.tr("widget.loading")
: root.temperature
color: Color.mOnSurface
pointSize: Style.fontSizeS
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
var tooltip = pluginApi?.tr("widget.title")
TooltipService.show(root, tooltip)
}
onExited: TooltipService.hide()
onClicked: pluginApi?.openPanel(root.screen, root)
}
}
Panel.qml
Item {
id: root
property var pluginApi: null
readonly property var geometryPlaceholder: panelContainer
readonly property bool allowAttach: true
property real contentPreferredWidth: 400 * Style.uiScaleRatio
property real contentPreferredHeight: 500 * Style.uiScaleRatio
// Weather data
property string city: "San Francisco"
property string lastUpdate: "5 minutes"
property int forecastDays: 7
anchors.fill: parent
Rectangle {
id: panelContainer
anchors.fill: parent
color: "transparent"
ColumnLayout {
anchors {
fill: parent
margins: Style.marginL
}
spacing: Style.marginL
// Header with translated title
NText {
text: pluginApi?.tr("panel.header")
pointSize: Style.fontSizeL
font.weight: Font.Bold
color: Color.mOnSurface
}
// Location with interpolation
NText {
text: pluginApi?.tr("status.location", { city: root.city })
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
// Content
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusL
ColumnLayout {
anchors {
fill: parent
margins: Style.marginL
}
spacing: Style.marginM
// Translated labels
RowLayout {
Layout.fillWidth: true
NText {
text: pluginApi?.tr("panel.temperature")
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
NText {
text: "72°F / 22°C"
color: Color.mOnSurface
font.weight: Font.Medium
}
}
RowLayout {
Layout.fillWidth: true
NText {
text: pluginApi?.tr("panel.humidity")
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
NText {
text: "65%"
color: Color.mOnSurface
font.weight: Font.Medium
}
}
NDivider {
Layout.fillWidth: true
}
// Plural example
NText {
text: pluginApi?.tr("panel.forecast")
pointSize: Style.fontSizeM
font.weight: Font.Medium
color: Color.mOnSurface
}
NText {
text: pluginApi?.trp(
"days.count",
root.forecastDays
)
color: Color.mOnSurfaceVariant
}
}
}
// Last update with interpolation
NText {
text: pluginApi?.tr("status.updated", { time: root.lastUpdate })
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight
}
}
}
}
Hot Reload for Translations
When developing with NOCTALIA_DEBUG=1, translation files are automatically watched for changes. This allows you to edit translations and see updates instantly without restarting Noctalia or reloading the entire plugin.
How It Works
- Each plugin’s translation file for the current language (e.g.,
i18n/en.json) is watched - When you save the file, only translations are reloaded (lightweight, preserves plugin state)
- A toast notification confirms the reload
- When the user changes language, the watcher automatically switches to the new language’s file
Benefits Over Full Plugin Reload
| Aspect | Translation Reload | Full Plugin Reload |
|---|---|---|
| Speed | ~300ms debounce | ~500ms debounce |
| Plugin state | Preserved | Reset |
| Component instances | Preserved | Recreated |
| Use case | Translation edits | QML code changes |
Workflow Tips
- Test translations quickly: Save your JSON file and see changes immediately
- Switch languages to test: The watcher follows the current language
- No restart needed: Keep editing while Noctalia runs
- State preserved: Your plugin’s runtime state (counters, loaded data) stays intact
Tip Start Noctalia with
NOCTALIA_DEBUG=1 qs -c noctalia-shellto enable hot reload for both QML files and translations.
Best Practices
- Always provide English: The
en.jsonfile is required as the fallback - Organize with nesting: Group related translations (widget, panel, messages)
- Keep keys consistent: Use the same structure across all language files
- Use interpolations: For dynamic content like names, numbers, dates
- Handle plurals properly: Use
trp()and_pluralsuffix keys - Test all languages: Verify translations display correctly
- Consider text length: Some languages have longer text than English
Translating Settings UI
Translations work in Settings.qml too:
ColumnLayout {
id: root
property var pluginApi: null
NTextInput {
Layout.fillWidth: true
label: pluginApi?.tr("settings.message.label")
description: pluginApi?.tr("settings.message.description")
// ...
}
NToggle {
Layout.fillWidth: true
label: pluginApi?.tr("settings.enabled.label")
description: pluginApi?.tr("settings.enabled.description")
// ...
}
}
Accessing Global Translations
You can also access Noctalia’s built-in translations via I18n:
NText {
// Access Noctalia's global translations
text: I18n.tr("common.save")
}
NText {
// Plugin-specific translations
text: pluginApi?.tr("panel.save") || I18n.tr("common.save")
}
See Also
- Getting Started - Create your first plugin
- Plugin API - Translation API reference
- Settings UI - Translate your settings interface