Back to Super Productivity

Plugin Internationalization (i18n) Guide

packages/plugin-dev/PLUGIN_I18N.md

18.4.413.3 KB
Original Source

Plugin Internationalization (i18n) Guide

This guide explains how to add multi-language support to your Super Productivity plugins.

Quick Start

my-plugin/
├── manifest.json          # Declare supported languages
├── plugin.js
└── i18n/                  # Translation files
    ├── en.json           # Required - English
    ├── de.json           # Optional - German
    └── fr.json           # Optional - French

manifest.json:

json
{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "i18n": {
    "languages": ["en", "de", "fr"]
  }
}

i18n/en.json:

json
{
  "GREETING": "Hello from my plugin!",
  "TASK_COUNT": "You have {{count}} tasks",
  "BUTTONS": {
    "SAVE": "Save",
    "CANCEL": "Cancel"
  }
}

plugin.js:

javascript
// Use translations in your plugin
const greeting = api.translate('GREETING');
const taskMsg = api.translate('TASK_COUNT', { count: 5 });
const saveBtn = api.translate('BUTTONS.SAVE');

Plugin Structure

1. Manifest Configuration

Add the i18n section to your manifest.json:

json
{
  "id": "my-awesome-plugin",
  "name": "My Awesome Plugin",
  "version": "1.0.0",
  "description": "A plugin with multi-language support",
  "i18n": {
    "languages": ["en", "de", "fr", "es"]
  }
}

Fields:

  • languages (required): Array of language codes supported by your plugin
  • Must include at least "en" (English)
  • Use standard language codes: en, de, fr, es, ja, zh, etc.

2. Translation Files

Create an i18n/ folder in your plugin with JSON files for each language:

my-plugin/
├── i18n/
│   ├── en.json    # English (required)
│   ├── de.json    # German
│   ├── fr.json    # French
│   └── es.json    # Spanish

File naming: Use language codes from the manifest (e.g., en.json, de.json)

3. Translation File Format

Use hierarchical JSON structure for organization:

json
{
  "MESSAGES": {
    "WELCOME": "Welcome to the plugin!",
    "GOODBYE": "See you later!",
    "ERROR": "An error occurred: {{error}}"
  },
  "BUTTONS": {
    "SAVE": "Save",
    "CANCEL": "Cancel",
    "DELETE": "Delete"
  },
  "LABELS": {
    "TASK_NAME": "Task Name",
    "DUE_DATE": "Due Date"
  }
}

Best practices:

  • Use UPPERCASE keys for consistency
  • Group related translations together
  • Keep hierarchy simple (2-3 levels max)
  • Use descriptive key names

API Methods

translate(key, params?)

Translate a key with optional parameter interpolation.

Parameters:

  • key (string): Translation key using dot notation
  • params (object, optional): Values to interpolate into the translation

Returns: Translated string, or the key itself if translation not found

Examples:

javascript
// Simple translation
const greeting = api.translate('MESSAGES.WELCOME');
// → "Welcome to the plugin!" (en)
// → "Willkommen zum Plugin!" (de)

// With parameters
const error = api.translate('MESSAGES.ERROR', {
  error: 'Network timeout',
});
// → "An error occurred: Network timeout"

// With multiple parameters
const summary = api.translate('SUMMARY', {
  count: 5,
  type: 'tasks',
});
// → "You have 5 tasks"

// Nested keys
const btnLabel = api.translate('BUTTONS.SAVE');
// → "Save"

Fallback behavior:

  1. Try current app language (e.g., German)
  2. Fall back to English if key not found
  3. Return the key itself if not in English either
javascript
// User's language is German (de)
// de.json has: { "BUTTONS": { "SAVE": "Speichern" } }
// en.json has: { "BUTTONS": { "SAVE": "Save", "CANCEL": "Cancel" } }

api.translate('BUTTONS.SAVE'); // → "Speichern" (from de.json)
api.translate('BUTTONS.CANCEL'); // → "Cancel" (from en.json - fallback)
api.translate('BUTTONS.DELETE'); // → "BUTTONS.DELETE" (not found)

formatDate(date, format)

Format a date according to the current locale.

Parameters:

  • date (Date | string | number): Date to format
    • Date object
    • ISO 8601 string (e.g., "2026-01-16T14:30:00Z")
    • Timestamp (milliseconds since epoch)
  • format (string): Predefined format
    • "short" - Short date (1/16/26)
    • "medium" - Medium date (Jan 16, 2026)
    • "long" - Long date (January 16, 2026)
    • "time" - Time only (2:30 PM)
    • "datetime" - Date and time (1/16/26, 2:30 PM)

Returns: Formatted date string

Examples:

javascript
const now = new Date();

// Short format
api.formatDate(now, 'short');
// → "1/16/26" (en-US)
// → "16.1.26" (de)

// Long format
api.formatDate(now, 'long');
// → "January 16, 2026" (en)
// → "16. Januar 2026" (de)

// Time only
api.formatDate(now, 'time');
// → "2:30 PM" (en)
// → "14:30" (de)

// ISO string input
api.formatDate('2026-01-16T14:30:00Z', 'datetime');
// → "1/16/26, 2:30 PM" (en)

// Timestamp input
api.formatDate(1737039000000, 'medium');
// → "Jan 16, 2026" (en)

getCurrentLanguage()

Get the current app language code.

Returns: Language code (e.g., "en", "de", "fr")

Example:

javascript
const lang = api.getCurrentLanguage();
console.log(`Current language: ${lang}`);
// → "Current language: de"

// Conditional logic based on language
if (lang === 'ja' || lang === 'zh') {
  // Special handling for Asian languages
  console.log('Using CJK font');
}

Language Change Hook

Listen for language changes to update your plugin UI:

javascript
api.registerHook('languageChange', ({ newLanguage }) => {
  console.log(`Language changed to: ${newLanguage}`);

  // Plugin translations are automatically reloaded
  // Update your UI if needed
  updatePluginUI();
});

Note: Plugin translations are automatically reloaded when the language changes. You only need this hook if you have additional UI updates to perform.

Supported Languages

Super Productivity supports these language codes:

CodeLanguage
enEnglish
deGerman
esSpanish
frFrench
itItalian
ptPortuguese
pt-brPortuguese (Brazil)
ruRussian
zhChinese (Simplified)
zh-twChinese (Traditional)
jaJapanese
koKorean
arArabic
faPersian
trTurkish
plPolish
nlDutch
nbNorwegian
svSwedish
fiFinnish
csCzech
skSlovak
hrCroatian
ukUkrainian
idIndonesian
roRomanian
ro-mdRomanian (Moldova)

Complete Example

Here's a complete plugin with i18n support:

Directory structure:

task-counter-plugin/
├── manifest.json
├── plugin.js
└── i18n/
    ├── en.json
    └── de.json

manifest.json:

json
{
  "id": "task-counter",
  "name": "Task Counter",
  "version": "1.0.0",
  "description": "Count and display task statistics",
  "i18n": {
    "languages": ["en", "de"]
  }
}

i18n/en.json:

json
{
  "TITLE": "Task Statistics",
  "TOTAL_TASKS": "Total tasks: {{count}}",
  "COMPLETED_TODAY": "Completed today: {{count}}",
  "UPDATED": "Last updated: {{time}}",
  "BUTTONS": {
    "REFRESH": "Refresh",
    "CLOSE": "Close"
  }
}

i18n/de.json:

json
{
  "TITLE": "Aufgabenstatistik",
  "TOTAL_TASKS": "Gesamt Aufgaben: {{count}}",
  "COMPLETED_TODAY": "Heute erledigt: {{count}}",
  "UPDATED": "Zuletzt aktualisiert: {{time}}",
  "BUTTONS": {
    "REFRESH": "Aktualisieren",
    "CLOSE": "Schließen"
  }
}

plugin.js:

javascript
(async function () {
  // Display task statistics with translations
  async function showStatistics() {
    const tasks = await api.getTasks();
    const completedToday = tasks.filter((t) => t.isDone && isToday(t.doneOn));

    const title = api.translate('TITLE');
    const totalMsg = api.translate('TOTAL_TASKS', {
      count: tasks.length,
    });
    const completedMsg = api.translate('COMPLETED_TODAY', {
      count: completedToday.length,
    });
    const updatedMsg = api.translate('UPDATED', {
      time: api.formatDate(new Date(), 'time'),
    });
    const refreshBtn = api.translate('BUTTONS.REFRESH');

    api.showSnack({
      msg: `${title}\n${totalMsg}\n${completedMsg}\n${updatedMsg}`,
      type: 'SUCCESS',
    });
  }

  // Register menu entry
  api.registerMenuEntry({
    label: api.translate('TITLE'),
    icon: 'analytics',
    onClick: showStatistics,
  });

  // Update translations when language changes
  api.registerHook('languageChange', () => {
    console.log('Language changed, UI will update on next interaction');
  });

  function isToday(timestamp) {
    if (!timestamp) return false;
    const today = new Date();
    const date = new Date(timestamp);
    return date.toDateString() === today.toDateString();
  }
})();

Best Practices

1. Always Include English

English is the fallback language. Always provide en.json:

json
{
  "i18n": {
    "languages": ["en", "de", "fr"] // ✓ English first
  }
}

2. Keep Keys Consistent

Use the same keys across all language files:

en.json:

json
{
  "SAVE": "Save",
  "CANCEL": "Cancel"
}

de.json:

json
{
  "SAVE": "Speichern",
  "CANCEL": "Abbrechen"
}

3. Use Descriptive Keys

javascript
// ✓ Good - descriptive
api.translate('BUTTONS.SAVE_TASK');

// ✗ Bad - vague
api.translate('BTN1');
json
{
  "ERRORS": {
    "NETWORK": "Network error",
    "PERMISSION": "Permission denied",
    "VALIDATION": "Invalid input"
  },
  "SUCCESS": {
    "SAVED": "Saved successfully",
    "DELETED": "Deleted successfully"
  }
}

5. Handle Plurals Carefully

Use parameters for dynamic pluralization:

json
{
  "TASK_COUNT_SINGULAR": "{{count}} task remaining",
  "TASK_COUNT_PLURAL": "{{count}} tasks remaining"
}
javascript
const count = tasks.length;
const key = count === 1 ? 'TASK_COUNT_SINGULAR' : 'TASK_COUNT_PLURAL';
const msg = api.translate(key, { count });

6. Date Formatting

Always use formatDate() instead of manual formatting:

javascript
// ✓ Good - locale-aware
const formatted = api.formatDate(task.dueDate, 'short');

// ✗ Bad - hard-coded format
const formatted = `${month}/${day}/${year}`;

Troubleshooting

Plugin Shows Keys Instead of Translations

Cause: Translation files not loaded or keys don't match

Solution:

  1. Check i18n/ folder exists in your plugin
  2. Verify JSON files are valid
  3. Ensure keys match exactly (case-sensitive)
  4. Check browser console for errors

Wrong Language Displayed

Cause: Language not supported by plugin

Solution:

  • Add the language to manifest i18n.languages
  • Create the corresponding JSON file
  • Plugin falls back to English for unsupported languages

Translations Not Updating

Cause: Plugin code caching translations

Solution:

  • Call api.translate() each time you need the translation
  • Don't cache translation results
  • The API handles caching internally

Parameters Not Interpolating

Cause: Wrong placeholder syntax or missing parameter

Solution:

javascript
// ✓ Correct syntax
api.translate('MESSAGE', { name: 'John' }); // "Hello, John"

// ✗ Wrong - missing curly braces
('Hello, {{name}}'); // ✓ Correct
('Hello, $name'); // ✗ Wrong

// ✗ Wrong - parameter name doesn't match
api.translate('MESSAGE', { user: 'John' }); // Won't replace {{name}}

Migration from Hard-coded Strings

If you have an existing plugin with hard-coded strings:

Before:

javascript
api.showSnack({ msg: 'Task saved successfully' });
const label = 'Save Task';

After:

  1. Create translation files:

en.json:

json
{
  "MESSAGES": {
    "TASK_SAVED": "Task saved successfully"
  },
  "LABELS": {
    "SAVE_TASK": "Save Task"
  }
}
  1. Update plugin code:
javascript
api.showSnack({
  msg: api.translate('MESSAGES.TASK_SAVED'),
});
const label = api.translate('LABELS.SAVE_TASK');
  1. Update manifest:
json
{
  "i18n": {
    "languages": ["en"]
  }
}

Testing i18n

1. Test All Languages

javascript
// Switch languages in Super Productivity settings
// Verify your plugin displays correct translations

2. Test Fallbacks

javascript
// Remove a key from non-English language
// Verify it falls back to English

3. Test Parameter Interpolation

javascript
// Test with various parameter values
const msg = api.translate('COUNT', { count: 0 });
const msg = api.translate('COUNT', { count: 1 });
const msg = api.translate('COUNT', { count: 100 });

4. Test Date Formats

javascript
// Test all format options
const formats = ['short', 'medium', 'long', 'time', 'datetime'];
formats.forEach((fmt) => {
  console.log(api.formatDate(new Date(), fmt));
});

Performance Considerations

  1. Translation files are loaded once at plugin activation
  2. Translations are cached in memory
  3. No performance impact on frequent translate() calls
  4. Language switching reuses already-loaded translations

See Also