diff --git a/packages/plugin-dev/PLUGIN_I18N.md b/packages/plugin-dev/PLUGIN_I18N.md new file mode 100644 index 000000000..7241914a7 --- /dev/null +++ b/packages/plugin-dev/PLUGIN_I18N.md @@ -0,0 +1,634 @@ +# 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: + +| 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**: + +```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'); +``` + +### 4. Group Related Translations + +```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" + } +} +``` + +2. Update plugin code: + +```javascript +api.showSnack({ + msg: api.translate('MESSAGES.TASK_SAVED'), +}); +const label = api.translate('LABELS.SAVE_TASK'); +``` + +3. 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 + +- [Plugin Development Guide](README.md) +- [Plugin API Reference](../plugin-api/README.md) +- [Example Plugins](.) diff --git a/packages/plugin-dev/README.md b/packages/plugin-dev/README.md index d97a192e2..015d170a8 100644 --- a/packages/plugin-dev/README.md +++ b/packages/plugin-dev/README.md @@ -42,7 +42,6 @@ npm run list ``` 3. **Update plugin metadata**: - - Edit `manifest.json` with your plugin details - Update `package.json` with your plugin name and description @@ -155,6 +154,14 @@ The plugin receives a global `PluginAPI` object with these capabilities: - `persistDataSynced()` - Save plugin data - `loadSyncedData()` - Load saved data +### Internationalization (i18n) + +- `translate(key, params?)` - Get translated text +- `formatDate(date, format)` - Format dates with locale +- `getCurrentLanguage()` - Get current language code + +See [PLUGIN_I18N.md](PLUGIN_I18N.md) for the complete i18n guide. + ### Hooks Register handlers for lifecycle events: @@ -163,6 +170,7 @@ Register handlers for lifecycle events: - `taskUpdate` - Task modified - `taskDelete` - Task removed - `currentTaskChange` - Active task changed +- `languageChange` - App language changed - `finishDay` - End of day ### Example Usage @@ -187,6 +195,11 @@ PluginAPI.registerShortcut({ console.log(`You have ${tasks.length} tasks`); }, }); + +// Use translations (if plugin has i18n support) +const greeting = PluginAPI.translate('MESSAGES.GREETING'); +const taskCount = PluginAPI.translate('TASK_COUNT', { count: tasks.length }); +const dueDate = PluginAPI.formatDate(task.dueDate, 'short'); ``` ## Building for Distribution @@ -218,6 +231,7 @@ Optional files: - `index.html` - UI for iframe plugins - `icon.svg` - Plugin icon +- `i18n/*.json` - Translation files for multi-language support ## Publishing Your Plugin @@ -331,6 +345,7 @@ PluginAPI.registerHook('taskUpdate', (data: unknown) => { 4. **User Experience**: Provide clear feedback with snack messages 5. **Permissions**: Only request permissions you actually need 6. **Version Compatibility**: Set appropriate `minSupVersion` +7. **Internationalization**: Add i18n support to reach more users (see [PLUGIN_I18N.md](PLUGIN_I18N.md)) ## Troubleshooting