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:

  1. Current language translation (e.g., i18n/de.json)
  2. English translation (i18n/en.json)
  3. 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

  1. Test translations quickly: Save your JSON file and see changes immediately
  2. Switch languages to test: The watcher follows the current language
  3. No restart needed: Keep editing while Noctalia runs
  4. State preserved: Your plugin’s runtime state (counters, loaded data) stays intact

Tip Start Noctalia with NOCTALIA_DEBUG=1 qs -c noctalia-shell to enable hot reload for both QML files and translations.

Best Practices

  1. Always provide English: The en.json file is required as the fallback
  2. Organize with nesting: Group related translations (widget, panel, messages)
  3. Keep keys consistent: Use the same structure across all language files
  4. Use interpolations: For dynamic content like names, numbers, dates
  5. Handle plurals properly: Use trp() and _plural suffix keys
  6. Test all languages: Verify translations display correctly
  7. 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


This site uses Just the Docs, a documentation theme for Jekyll.