Phase 7: Create documentation - Create PLUGIN_I18N.md with complete i18n guide - Quick start guide with file structure - Manifest configuration details - Translation file format and best practices - Complete API documentation (translate, formatDate, getCurrentLanguage) - Language change hook documentation - Full list of supported languages (24 languages) - Complete working example with multi-language support - Best practices and troubleshooting sections - Migration guide from hard-coded strings - Testing and performance considerations - Update README.md with i18n section - Add i18n API methods to Plugin API section - Add languageChange hook to hooks list - Add i18n example to usage section - Add i18n files to optional files list - Add i18n best practice - Link to comprehensive PLUGIN_I18N.md guide Documentation provides complete guide for plugin developers to add multi-language support to their plugins with working examples.
13 KiB
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:
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"i18n": {
"languages": ["en", "de", "fr"]
}
}
i18n/en.json:
{
"GREETING": "Hello from my plugin!",
"TASK_COUNT": "You have {{count}} tasks",
"BUTTONS": {
"SAVE": "Save",
"CANCEL": "Cancel"
}
}
plugin.js:
// 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:
{
"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:
{
"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 notationparams(object, optional): Values to interpolate into the translation
Returns: Translated string, or the key itself if translation not found
Examples:
// 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:
- Try current app language (e.g., German)
- Fall back to English if key not found
- Return the key itself if not in English either
// 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:
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:
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:
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:
| Code | Language |
|---|---|
en |
English |
de |
German |
es |
Spanish |
fr |
French |
it |
Italian |
pt |
Portuguese |
pt-br |
Portuguese (Brazil) |
ru |
Russian |
zh |
Chinese (Simplified) |
zh-tw |
Chinese (Traditional) |
ja |
Japanese |
ko |
Korean |
ar |
Arabic |
fa |
Persian |
tr |
Turkish |
pl |
Polish |
nl |
Dutch |
nb |
Norwegian |
sv |
Swedish |
fi |
Finnish |
cz |
Czech |
sk |
Slovak |
hr |
Croatian |
uk |
Ukrainian |
id |
Indonesian |
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:
{
"id": "task-counter",
"name": "Task Counter",
"version": "1.0.0",
"description": "Count and display task statistics",
"i18n": {
"languages": ["en", "de"]
}
}
i18n/en.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:
{
"TITLE": "Aufgabenstatistik",
"TOTAL_TASKS": "Gesamt Aufgaben: {{count}}",
"COMPLETED_TODAY": "Heute erledigt: {{count}}",
"UPDATED": "Zuletzt aktualisiert: {{time}}",
"BUTTONS": {
"REFRESH": "Aktualisieren",
"CLOSE": "Schließen"
}
}
plugin.js:
(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:
{
"i18n": {
"languages": ["en", "de", "fr"] // ✓ English first
}
}
2. Keep Keys Consistent
Use the same keys across all language files:
en.json:
{
"SAVE": "Save",
"CANCEL": "Cancel"
}
de.json:
{
"SAVE": "Speichern",
"CANCEL": "Abbrechen"
}
3. Use Descriptive Keys
// ✓ Good - descriptive
api.translate('BUTTONS.SAVE_TASK');
// ✗ Bad - vague
api.translate('BTN1');
4. Group Related Translations
{
"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:
{
"TASK_COUNT_SINGULAR": "{{count}} task remaining",
"TASK_COUNT_PLURAL": "{{count}} tasks remaining"
}
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:
// ✓ 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:
- Check
i18n/folder exists in your plugin - Verify JSON files are valid
- Ensure keys match exactly (case-sensitive)
- 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:
// ✓ 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:
api.showSnack({ msg: 'Task saved successfully' });
const label = 'Save Task';
After:
- Create translation files:
en.json:
{
"MESSAGES": {
"TASK_SAVED": "Task saved successfully"
},
"LABELS": {
"SAVE_TASK": "Save Task"
}
}
- Update plugin code:
api.showSnack({
msg: api.translate('MESSAGES.TASK_SAVED'),
});
const label = api.translate('LABELS.SAVE_TASK');
- Update manifest:
{
"i18n": {
"languages": ["en"]
}
}
Testing i18n
1. Test All Languages
// Switch languages in Super Productivity settings
// Verify your plugin displays correct translations
2. Test Fallbacks
// Remove a key from non-English language
// Verify it falls back to English
3. Test Parameter Interpolation
// 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
// Test all format options
const formats = ['short', 'medium', 'long', 'time', 'datetime'];
formats.forEach((fmt) => {
console.log(api.formatDate(new Date(), fmt));
});
Performance Considerations
- Translation files are loaded once at plugin activation
- Translations are cached in memory
- No performance impact on frequent
translate()calls - Language switching reuses already-loaded translations