mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
docs(plugins): add comprehensive i18n documentation
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.
This commit is contained in:
parent
f166ae1595
commit
5bb8b24c82
2 changed files with 650 additions and 1 deletions
634
packages/plugin-dev/PLUGIN_I18N.md
Normal file
634
packages/plugin-dev/PLUGIN_I18N.md
Normal file
|
|
@ -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](.)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue