callumalpass_tasknotes/I18N_GUIDE.md
callumalpass a65a4c9138 feat: Migrate to i18n-state-manager CLI tool
Replace custom scripts/i18n-manager.mjs with the new i18n-state-manager
package, which provides a more robust and feature-rich translation
management system.

Changes:
- Add i18n-state.config.json with project-specific patterns
- Update package.json scripts to use i18n-state CLI
- Update I18N_GUIDE.md with new tool documentation

Configuration:
- Configured patterns for this.t(), this.translate(), i18n.translate()
- Source locale: en
- 7 supported locales: de, es, fr, ja, ru, zh
- Scan directories: src/

New features available:
- find-unused: Identify potentially unused translation keys
- check-duplicates: Detect duplicate keys in translation files
- Improved stale detection with hash-based state tracking
- Better error messages and validation
- Ripgrep-based code scanning for performance

Documentation updates:
- Remove emoji formatting for professional tone
- Add ripgrep installation instructions
- Document new configuration file format
- Add examples for new commands
- Update all command outputs to reflect actual CLI behavior
- Add troubleshooting for common issues

The old scripts/i18n-manager.mjs can be removed in a future commit.
2025-11-04 13:50:02 +11:00

616 lines
No EOL
18 KiB
Markdown

# Internationalization (i18n) Management System
This project uses i18n-state-manager, a git-native translation management tool with intelligent stale detection, to ensure translation consistency and prevent missing or outdated translations from being deployed.
## Overview
The i18n system tracks:
- **Manifest**: SHA1 hashes of all English (source) strings
- **State**: Translation status for each locale (up-to-date, missing, or stale)
- **CI Enforcement**: Automatic verification in GitHub Actions
## Requirements
- Node.js >= 16.0.0
- [ripgrep](https://github.com/BurntSushi/ripgrep) (required for `check-usage` and `find-unused` commands)
## Quick Start
### For Developers
When you modify English strings in `src/i18n/resources/en.ts`:
```bash
# 1. Edit the English translation file
vim src/i18n/resources/en.ts
# 2. Update the manifest and state files
npm run i18n:sync
# 3. Commit all changes (including generated files)
git add src/i18n/resources/en.ts i18n.manifest.json i18n.state.json
git commit -m "feat: add new translation keys for feature X"
```
### For Translators
To translate strings to another language:
```bash
# 1. Check what needs translation
npm run i18n:verify
# 2. Edit the translation file (e.g., French)
vim src/i18n/resources/fr.ts
# 3. Update state to mark translations as current
npm run i18n:sync
# 4. Commit your changes
git add src/i18n/resources/fr.ts i18n.state.json
git commit -m "feat: add French translations for feature X"
```
## Available Commands
| Command | Purpose | When to Use |
|---------|---------|-------------|
| `npm run i18n:sync` | Update manifest and state files | After changing ANY translation files |
| `npm run i18n:verify` | Check for missing/stale translations | Before releasing (fails on issues) |
| `npm run i18n:status` | Show translation coverage summary | To check overall progress |
| `npm run i18n:check-usage` | Find translation keys used in code | To verify all keys in code exist in en.ts |
| `npm run i18n:find-unused` | Find keys not used in source code | To identify potentially unused translation keys |
| `npm run i18n:check-duplicates` | Check for duplicate keys | To catch copy-paste errors in translation files |
| `npm run i18n:generate-template <locale>` | Generate translation template | Creating/updating translations for a locale |
All commands are powered by i18n-state-manager. You can also run them directly:
```bash
npx i18n-state <command>
```
## How It Works
### 1. Manifest File (`i18n.manifest.json`)
Contains SHA1 hashes of all English strings:
```json
{
"common.appName": "6458145fdd07ad08ff52a2e72d531588936bdca6",
"common.cancel": "77dfd2135f4db726c47299bb55be26f7f4525a46"
}
```
### 2. State File (`i18n.state.json`)
Tracks translation status for each locale:
```json
{
"fr": {
"common.appName": {
"source": "6458145fdd07ad08ff52a2e72d531588936bdca6",
"translation": "8f3d9e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e"
},
"common.cancel": null // Missing translation
}
}
```
The system compares source hashes from the manifest with state hashes to determine:
- **Up-to-date**: Source hash matches the hash stored in state
- **Missing**: Key has no entry or null in state
- **Stale**: Source hash in manifest differs from source hash in state (source text changed)
### 3. CI/CD Enforcement
The GitHub Actions workflow automatically:
- Runs `npm run i18n:sync` on every PR/push
- Fails the build if manifest/state files are out of date
- Forces developers to commit synchronized files
## Workflow Examples
### Adding a New English String
```bash
# 1. Add to en.ts
export const en = {
common: {
newFeature: 'My new feature' // ← Add this
}
}
# 2. Sync
npm run i18n:sync
# Generated manifest from "en.ts" with 1210 keys.
# Updated state for locales: fr.
# 3. Check status
npm run i18n:status
# fr: 99% translated, 0% stale (1 new missing key)
# 4. Commit everything
git add src/i18n/resources/en.ts i18n.manifest.json i18n.state.json
git commit -m "feat: add newFeature translation key"
```
### Translating to French
```bash
# 1. See what needs translation
npm run i18n:verify
# Missing translations:
# [fr] 1 missing keys:
# - common.newFeature
# 2. Add French translation
# Edit fr.ts and add: newFeature: 'Ma nouvelle fonctionnalité'
# 3. Mark as current
npm run i18n:sync
# Updated state for locales: fr.
# 4. Verify completion
npm run i18n:verify
# All translations are up-to-date.
# 5. Commit
git add src/i18n/resources/fr.ts i18n.state.json
git commit -m "feat: add French translation for newFeature"
```
### When English String Changes
```bash
# 1. Modify existing English string
# Change "My new feature" → "My awesome feature"
# 2. Sync (this marks French as stale)
npm run i18n:sync
# 3. Check status
npm run i18n:verify
# Stale translations (source text changed):
# [fr] 1 stale keys:
# - common.newFeature
# 4. Update French translation
# Edit fr.ts: "Ma nouvelle fonctionnalité" → "Ma fonctionnalité géniale"
# 5. Mark as current
npm run i18n:sync
# 6. Commit all changes
git add src/i18n/resources/en.ts src/i18n/resources/fr.ts i18n.manifest.json i18n.state.json
git commit -m "feat: improve newFeature translation"
```
### Verifying Keys Used in Code
```bash
# 1. After adding new features, check if all translation keys exist
npm run i18n:check-usage
# Example output if keys are missing:
# Keys used in code but missing from en.ts:
# - modals.newModal.title
# - modals.newModal.save
# 2. Add missing keys to en.ts
# Edit src/i18n/resources/en.ts
# 3. Sync to update manifest
npm run i18n:sync
# 4. Verify all keys now exist
npm run i18n:check-usage
# All keys used in source code exist in en.ts
# 5. Optional: Run /translate-missing in Claude Code to auto-translate
# This will translate the new keys to all other locales
```
### Generating Translation Templates
```bash
# Generate a template for an existing locale (preserves existing translations)
npm run i18n:generate-template fr
# Output:
# Template generated: src/i18n/resources/fr.template.ts
# Statistics:
# Total keys: 1540
# Up-to-date: 1467
# Missing: 73
# Stale: 0
# The template will have:
# - Up-to-date translations preserved as-is
# - Missing translations marked as "TODO: English text"
# - Stale translations marked as "STALE: old translation" (English changed)
# Search for TODO to find what needs translation
grep "TODO:" src/i18n/resources/fr.template.ts
# Search for STALE to find outdated translations
grep "STALE:" src/i18n/resources/fr.template.ts
# Translate the TODO items, then replace the original file
mv src/i18n/resources/fr.template.ts src/i18n/resources/fr.ts
# Or create a new locale from scratch
npm run i18n:generate-template it
# All values will be "TODO: <English text>"
```
## Technical Details
### File Structure
```
├── i18n-state.config.json # Configuration for i18n-state-manager
├── i18n.manifest.json # Source string hashes
├── i18n.state.json # Translation state tracking
├── src/i18n/resources/
│ ├── en.ts # English (source) translations
│ ├── fr.ts # French translations
│ ├── de.ts # German translations
│ ├── es.ts # Spanish translations
│ ├── ja.ts # Japanese translations
│ ├── ru.ts # Russian translations
│ └── zh.ts # Chinese translations
└── .github/workflows/test.yml # CI/CD enforcement
```
### Configuration File
The `i18n-state.config.json` file configures how i18n-state-manager processes your translations:
```json
{
"sourceLocale": "en",
"resourcesDir": "src/i18n/resources",
"manifestPath": "i18n.manifest.json",
"statePath": "i18n.state.json",
"scanDirs": ["src"],
"scanExtensions": ["ts", "tsx", "js", "jsx"],
"patterns": [
{
"name": "this.t()",
"regex": "this\\.t\\([\"']([^\"']+)[\"']\\)"
},
{
"name": "this.translate()",
"regex": "this\\.translate\\([\"']([^\"']+)[\"']\\)"
}
]
}
```
**Configuration Options:**
- `sourceLocale`: The source language for your translations (typically "en")
- `resourcesDir`: Directory containing translation files
- `manifestPath`: Path to manifest file (tracks source hashes)
- `statePath`: Path to state file (tracks translation status)
- `scanDirs`: Directories to scan for translation usage
- `scanExtensions`: File extensions to include in scans
- `patterns`: Regular expressions to detect translation function calls in code
### Translation Detection Logic
The system considers a translation:
- **Missing**: Key doesn't exist in translation file (null in state)
- **Stale**: Source hash in manifest differs from source hash in state (English text changed)
- **Up-to-date**: Source hash in manifest matches source hash in state
### CI/CD Integration
The workflow step in `.github/workflows/test.yml`:
```yaml
- name: Check i18n manifest is up-to-date
run: |
npm run i18n:sync
if [[ -n "$(git status --porcelain)" ]]; then
echo "Error: i18n files are out of date"
exit 1
fi
npm run i18n:verify
```
This ensures:
- No untranslated strings slip into production
- Translation state is always tracked
- Developers must explicitly acknowledge translation needs
### 4. Usage Verification (`npm run i18n:check-usage`)
Scans your source code to find all translation function calls and verifies the keys exist in `en.ts`.
**Detected Patterns:**
The tool scans for patterns defined in `i18n-state.config.json`:
1. `this.t("key")` - Shorthand class method calls
2. `this.translate("key")` - Recommended class pattern
3. `i18n.translate("key")` - Direct service calls
4. `i18nService.translate("key")` - Service instance calls
**Example Output:**
```bash
$ npm run i18n:check-usage
Checking i18n key usage in source code...
Found 619 unique translation keys in source code
Keys used in code but missing from en.ts:
- modals.task.newField
- settings.experimental.feature
2 missing key(s) found.
Add these keys to src/i18n/resources/en.ts and run "i18n-state sync".
```
**What it finds:**
- All function calls with string literal keys
- Multi-line translation calls
- Patterns matching the configured regex patterns
**What it doesn't find:**
- Dynamically constructed keys (`` `common.${variable}` ``)
- Keys stored in variables before use
- For these cases, use `npm run i18n:find-unused` to identify potentially unused keys
**Use this to:**
- Catch typos in translation keys before runtime
- Ensure new features have translation keys in `en.ts`
- Identify keys used in code that need to be added to translations
### 5. Finding Unused Keys (`npm run i18n:find-unused`)
Scans source code to identify translation keys in `en.ts` that aren't used anywhere in the codebase.
**Example Output:**
```bash
$ npm run i18n:find-unused
Finding unused translation keys...
Statistics:
Total keys in en.ts: 1745
Keys found in source code: 619
Potentially unused keys: 1126
Coverage: 35%
Potentially unused keys (not found in source code):
[commands] 24 keys:
- commands.openCalendarView
- commands.openAdvancedCalendarView
...
Note: These keys might be:
- Dynamically constructed (e.g., `common.weekdays.${day}`)
- Used in external files or configurations
- Reserved for future features
- Truly unused and can be removed
Manually review before deleting!
```
**Important:** This command identifies potentially unused keys. Always manually review before removing any keys, as some may be:
- Dynamically constructed at runtime
- Used in configuration files
- Used by external integrations
- Reserved for planned features
## Best Practices
### Translation Management
1. **Always run `npm run i18n:sync`** after modifying translation files
2. **Commit generated files** (`i18n.manifest.json`, `i18n.state.json`) with your changes
3. **Use descriptive keys** like `features.taskList.emptyState` instead of generic ones
4. **Group related translations** using nested objects for better organization
5. **Test locally** with `npm run i18n:verify` before pushing
6. **Run `npm run i18n:check-usage`** periodically to catch missing keys early
7. **Review unused keys** with `npm run i18n:find-unused` before major releases
8. **Check for duplicates** with `npm run i18n:check-duplicates` to catch copy-paste errors
### Translation Usage in Code
**Recommended Pattern:** Use `this.translate()` for consistent, type-safe translations
```typescript
class MyComponent {
private translate: (key: TranslationKey, vars?: Record<string, any>) => string;
constructor(plugin: TaskNotesPlugin) {
this.translate = plugin.i18n.translate.bind(plugin.i18n);
}
render() {
// ✅ Recommended: Clear, type-safe, consistent
return this.translate("modals.task.title");
// ✅ Also acceptable: For interpolation
return this.translate("modals.task.count", { count: 5 });
}
}
```
**Alternative Patterns (also valid):**
```typescript
// For settings/functions where plugin is passed as parameter
function renderSettings(plugin: TaskNotesPlugin) {
const translate = (key: TranslationKey) => plugin.i18n.translate(key);
return translate("settings.general.title");
}
// For scoped prefixes (reduces repetition)
function renderCalendarSettings(plugin: TaskNotesPlugin) {
const t = (key: string) =>
plugin.i18n.translate(`views.calendar.settings.${key}`);
return {
events: t("events.showTasks"), // → "views.calendar.settings.events.showTasks"
layout: t("layout.weekStart"), // → "views.calendar.settings.layout.weekStart"
};
}
```
**What to Avoid:**
```typescript
// ❌ Avoid: Inconsistent naming (use 'translate' not 't' for main method)
this.t("key") // Use this.translate() instead
// ❌ Avoid: Direct service calls (verbose, requires plugin reference)
this.plugin.i18n.translate("key") // Use this.translate() instead
```
**Key Principles:**
- **Consistency**: Stick to one primary pattern (`this.translate()`)
- **Type Safety**: Always use `TranslationKey` type for keys
- **Scoped Helpers**: Use prefix helpers (like `const t = ...`) only when you have many keys with the same prefix
- **Descriptive Keys**: Use `modals.task.created` not `taskCreated`
## Troubleshooting
**CI fails with "i18n files are out of date"**
```bash
npm run i18n:sync
git add i18n.manifest.json i18n.state.json
git commit -m "chore: update i18n manifest and state files"
```
**Runtime errors: "Translation key not found"**
```bash
# Find which keys are used in code but missing from en.ts
npm run i18n:check-usage
# Add the missing keys to src/i18n/resources/en.ts
# Then sync
npm run i18n:sync
```
**"Keys used in code but missing from en.ts"**
This means you're calling a translation function with a key that doesn't exist yet:
```typescript
// Your code:
this.translate("modals.newFeature.title") // Key doesn't exist
// Solution: Add to src/i18n/resources/en.ts
export const en = {
modals: {
newFeature: {
title: "New Feature Title" // Add this
}
}
}
```
**"Error: This command requires ripgrep (rg) to be installed"**
The `check-usage` and `find-unused` commands require ripgrep for code scanning:
```bash
# macOS
brew install ripgrep
# Ubuntu/Debian
apt install ripgrep
# Arch Linux
pacman -S ripgrep
# Windows
choco install ripgrep
```
**"Could not load source locale" error**
- Check that `src/i18n/resources/en.ts` exists and has valid syntax
- Ensure the export follows the pattern: `export const en = { ... }`
**Wrong translation statistics**
- Run `npm run i18n:sync` to recalculate
- Check that translated strings are actually different from English
**Hook not running after editing translation files**
- Check that `.claude/settings.json` exists and contains the post-tool hook
- Verify you're editing files matching `src/i18n/resources/*.ts`
- Try restarting Claude Code CLI
**"Duplicate keys found"**
Run `npm run i18n:check-duplicates` to identify duplicate translation keys in your files. This usually happens from copy-paste errors.
## Translation Progress
Use these commands to track progress:
```bash
# Quick overview
npm run i18n:status
# Detailed missing/stale report
npm run i18n:verify
# After making changes
npm run i18n:sync
# Check for missing keys in code
npm run i18n:check-usage
# Find potentially unused keys
npm run i18n:find-unused
# Check for duplicate keys
npm run i18n:check-duplicates
```
## Claude Code Integration
### Automated Translation with Hooks
The project includes Claude Code hooks that automatically sync translation files when you edit them:
**Location:** `.claude/settings.json`
When you edit any file in `src/i18n/resources/*.ts`, the hook automatically:
1. Runs `npm run i18n:sync` to update manifest and state files
2. Checks if translations are needed and reminds you
**Example workflow:**
```bash
# 1. You edit src/i18n/resources/en.ts
# Hook automatically runs: npm run i18n:sync
# Output:
# i18n files synced
# Translations needed. Run /translate-missing to update locales.
# 2. Run the slash command to auto-translate
/translate-missing
# 3. All locales are now updated with AI translations
```
### Slash Command: `/translate-missing`
**Location:** `.claude/commands/translate-missing.md`
This command automatically translates all missing and stale keys across all 7 locales (de, es, fr, ja, ru, zh).
**Features:**
- Preserves interpolation syntax (e.g., `{{variable}}`)
- Maintains consistent tone and style with existing translations
- Handles all locales in a single command
- Automatically runs `npm run i18n:sync` after completion
**Usage:**
```bash
# Just run the command - it handles everything
/translate-missing
```
**What it does:**
1. Runs `npm run i18n:verify` to find missing/stale keys
2. Translates each missing key to the target language
3. Updates all locale files
4. Runs `npm run i18n:sync` to mark translations as current
5. Verifies completion with `npm run i18n:verify`
---
This system ensures translation quality and consistency while making the workflow as smooth as possible for both developers and translators.