WinterCMS research
This commit is contained in:
432
docs/research/wintercms/queststream-14-i18n/14-RESEARCH.md
Normal file
432
docs/research/wintercms/queststream-14-i18n/14-RESEARCH.md
Normal file
@@ -0,0 +1,432 @@
|
||||
# Phase 14: Translation Polish & Testing - Research
|
||||
|
||||
**Researched:** 2026-01-29
|
||||
**Domain:** i18n Polish translations, pluralization testing, translation coverage audit
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Summary
|
||||
|
||||
This phase completes Polish translations for all Vue i18n strings and validates the bilingual experience through manual testing and automated pluralization verification. The codebase has 1858 translation keys in both `en.json` and `pl.json` that are currently synchronized, but some Polish translations may be placeholders (English strings as values). The primary work involves: (1) generating a coverage report to identify untranslated strings, (2) reviewing/fixing Polish translations including proper pluralization, (3) adding Vitest unit tests for Polish pluralization rules, and (4) manual walkthrough testing in Polish.
|
||||
|
||||
Key findings:
|
||||
1. **Translation files are synchronized** - Both `en.json` and `pl.json` have 1858 keys, but some PL values may be English placeholders
|
||||
2. **Polish pluralization is configured** - The `i18n.config.ts` has a custom `pluralizationRules.pl` function implementing 4-form rules
|
||||
3. **18 plural strings exist** - Grep found 18 keys using pipe `|` separator for pluralization
|
||||
4. **No unit test infrastructure** - The project uses Playwright for E2E but has no Vitest setup for unit tests
|
||||
5. **CLDR Polish rules are complex** - Polish has 4 forms: one, few, many, other with specific mathematical rules for each
|
||||
|
||||
**Primary recommendation:** Create a simple Node.js coverage report script (not a full npm package), add Vitest with minimal setup for pluralization unit tests, generate side-by-side review markdown for translation audit, then execute manual walkthrough testing.
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| @nuxt/test-utils | latest | Nuxt test utilities | Official Nuxt testing library |
|
||||
| vitest | latest | Unit test runner | Default for Nuxt 3, fast, TypeScript native |
|
||||
| Node.js fs module | built-in | JSON file comparison | No external deps for simple script |
|
||||
|
||||
### Supporting
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| happy-dom | latest | DOM environment for Vitest | Only if testing components (not needed for pure function tests) |
|
||||
|
||||
### Alternatives Considered
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| Custom Node script | i18n-check npm | Overkill for 2-file comparison, adds dependency |
|
||||
| Vitest | Jest | Jest requires more config for ESM/TypeScript |
|
||||
| Manual markdown review | Spreadsheet | Markdown stays in repo, version controlled |
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
cd vue-queststream-app
|
||||
pnpm add -D @nuxt/test-utils vitest
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended Project Structure
|
||||
```
|
||||
vue-queststream-app/
|
||||
├── i18n/
|
||||
│ ├── locales/
|
||||
│ │ ├── en.json
|
||||
│ │ └── pl.json
|
||||
│ └── i18n.config.ts # Pluralization rules
|
||||
├── scripts/
|
||||
│ └── i18n-coverage.ts # Coverage report script
|
||||
├── tests/
|
||||
│ └── i18n/
|
||||
│ └── polish-pluralization.test.ts # Pluralization unit tests
|
||||
└── vitest.config.ts # Vitest configuration
|
||||
```
|
||||
|
||||
### Pattern 1: Translation Coverage Report Script
|
||||
**What:** Node.js script to compare en.json vs pl.json
|
||||
**When to use:** Before translation review, in CI optionally
|
||||
**Example:**
|
||||
```typescript
|
||||
// scripts/i18n-coverage.ts
|
||||
import en from '../i18n/locales/en.json'
|
||||
import pl from '../i18n/locales/pl.json'
|
||||
|
||||
const enKeys = Object.keys(en)
|
||||
const plKeys = Object.keys(pl)
|
||||
|
||||
// Find keys where PL value equals EN value (potential untranslated)
|
||||
const untranslated = enKeys.filter(key =>
|
||||
en[key] === pl[key] && !key.includes('|') // Exclude plurals
|
||||
)
|
||||
|
||||
console.log(`Total keys: ${enKeys.length}`)
|
||||
console.log(`Potentially untranslated: ${untranslated.length}`)
|
||||
untranslated.forEach(key => console.log(` - ${key}`))
|
||||
```
|
||||
|
||||
### Pattern 2: Polish Pluralization Unit Tests
|
||||
**What:** Test the custom pluralization function with comprehensive values
|
||||
**When to use:** Verify plural form selection is correct for all edge cases
|
||||
**Example:**
|
||||
```typescript
|
||||
// tests/i18n/polish-pluralization.test.ts
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
// Polish pluralization function (extracted from i18n.config.ts)
|
||||
function plPluralRule(choice: number, choicesLength: number): number {
|
||||
if (choice === 0) return 0
|
||||
|
||||
const teen = choice > 10 && choice < 20
|
||||
const endsWithOne = choice % 10 === 1
|
||||
|
||||
if (!teen && endsWithOne) return 1
|
||||
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) return 2
|
||||
|
||||
return choicesLength < 4 ? 2 : 3
|
||||
}
|
||||
|
||||
describe('Polish pluralization rules', () => {
|
||||
// 4-form tests (one | few | many | other)
|
||||
const testCases = [
|
||||
{ n: 0, expected: 0, form: 'zero' },
|
||||
{ n: 1, expected: 1, form: 'one' },
|
||||
{ n: 2, expected: 2, form: 'few' },
|
||||
{ n: 3, expected: 2, form: 'few' },
|
||||
{ n: 4, expected: 2, form: 'few' },
|
||||
{ n: 5, expected: 3, form: 'many' },
|
||||
{ n: 11, expected: 3, form: 'many (teen)' },
|
||||
{ n: 12, expected: 3, form: 'many (teen)' },
|
||||
{ n: 21, expected: 1, form: 'one (21)' },
|
||||
{ n: 22, expected: 2, form: 'few (22)' },
|
||||
{ n: 25, expected: 3, form: 'many' },
|
||||
{ n: 100, expected: 3, form: 'many' },
|
||||
{ n: 101, expected: 1, form: 'one (101)' },
|
||||
{ n: 102, expected: 2, form: 'few (102)' },
|
||||
{ n: 105, expected: 3, form: 'many' },
|
||||
]
|
||||
|
||||
testCases.forEach(({ n, expected, form }) => {
|
||||
it(`returns form ${expected} (${form}) for n=${n}`, () => {
|
||||
expect(plPluralRule(n, 4)).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Pattern 3: Side-by-Side Translation Review File
|
||||
**What:** Markdown table with EN | PL | Context for human review
|
||||
**When to use:** Translation audit workflow
|
||||
**Example:**
|
||||
```markdown
|
||||
| English | Polish | Context |
|
||||
|---------|--------|---------|
|
||||
| Save Changes | Zapisz zmiany | Button in settings forms |
|
||||
| {count} quest \| {count} quests | {count} misja \| {count} misje \| {count} misji \| {count} misji | Quest count - 4 forms: one\|few\|many\|other |
|
||||
| Hi {name}! | Cześć {name}! | Child dashboard greeting |
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
- **Importing vue-i18n in unit tests:** Test the pluralization function directly, not through i18n runtime
|
||||
- **Using E2E for pluralization:** Playwright is slow for 50+ test cases, use unit tests
|
||||
- **Translating everything at once:** Generate report first, review systematically
|
||||
- **Skipping visual checks:** Polish strings are often 20-30% longer than English
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| JSON key comparison | Full npm package | Simple Node script | 50 lines vs dependency |
|
||||
| Plural rule testing | Browser testing | Vitest unit tests | 100x faster |
|
||||
| Translation review | Manual file comparison | Generated markdown table | Structured, auditable |
|
||||
| Coverage tracking | Spreadsheet | Script output + git diff | Version controlled |
|
||||
|
||||
**Key insight:** This phase is about validation, not new features. Keep tooling minimal - a script and a test file.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Testing Pluralization Through vue-i18n Runtime
|
||||
**What goes wrong:** Tests are slow, require full Nuxt context, flaky
|
||||
**Why it happens:** Assumption that i18n must be tested end-to-end
|
||||
**How to avoid:** Extract pluralization function, test as pure function
|
||||
**Warning signs:** Tests taking >100ms each, requiring `mountSuspended`
|
||||
|
||||
### Pitfall 2: Missing Polish Plural Forms
|
||||
**What goes wrong:** "5 misja" instead of "5 misji" (wrong form)
|
||||
**Why it happens:** Only providing 2 forms when Polish needs 4
|
||||
**How to avoid:** Ensure all plural keys have `form0 | form1 | form2 | form3` in pl.json
|
||||
**Warning signs:** Pipe-separated values with different count in EN vs PL
|
||||
|
||||
### Pitfall 3: False Positives in Coverage Report
|
||||
**What goes wrong:** Marking valid strings as untranslated
|
||||
**Why it happens:** Some strings are intentionally same in both languages (e.g., "QuestStream", "XP")
|
||||
**How to avoid:** Add exclusion list for proper nouns, abbreviations
|
||||
**Warning signs:** Coverage report showing brand names, technical terms
|
||||
|
||||
### Pitfall 4: Overlooking String Length in UI
|
||||
**What goes wrong:** Polish text truncated or breaks layout
|
||||
**Why it happens:** Polish words are longer than English equivalents
|
||||
**How to avoid:** Manual visual walkthrough of all pages in Polish
|
||||
**Warning signs:** Ellipsis (...), text overflow, button text wrapping
|
||||
|
||||
### Pitfall 5: Inconsistent Translation Tone
|
||||
**What goes wrong:** Mixed formal/informal Polish ("Ty" vs "Pan/Pani")
|
||||
**Why it happens:** No style guide, different translation sessions
|
||||
**How to avoid:** Review all translations together, enforce consistent tone (informal for QuestStream)
|
||||
**Warning signs:** "Zaloguj się" (formal) mixed with "Cześć!" (informal)
|
||||
|
||||
### Pitfall 6: Incorrect Teen Number Handling
|
||||
**What goes wrong:** "11 misja" or "12 misje" (should be "11 misji", "12 misji")
|
||||
**Why it happens:** Polish teen numbers (11-19) use "many" form, not "one"/"few"
|
||||
**How to avoid:** Test values 11-19 explicitly in unit tests
|
||||
**Warning signs:** Grammar errors specifically on 11-19
|
||||
|
||||
## Code Examples
|
||||
|
||||
### vitest.config.ts (Minimal Setup)
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['tests/**/*.{test,spec}.ts'],
|
||||
environment: 'node', // No DOM needed for pure function tests
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Coverage Report Script
|
||||
```typescript
|
||||
// scripts/i18n-coverage.ts
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
interface TranslationFile {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
// Load translation files
|
||||
const en: TranslationFile = JSON.parse(
|
||||
readFileSync(resolve(__dirname, '../i18n/locales/en.json'), 'utf-8')
|
||||
)
|
||||
const pl: TranslationFile = JSON.parse(
|
||||
readFileSync(resolve(__dirname, '../i18n/locales/pl.json'), 'utf-8')
|
||||
)
|
||||
|
||||
// Strings that are intentionally the same in both languages
|
||||
const EXCLUSIONS = [
|
||||
'QuestStream',
|
||||
'XP',
|
||||
'PIN',
|
||||
// Add brand names, abbreviations, etc.
|
||||
]
|
||||
|
||||
const enKeys = Object.keys(en)
|
||||
const plKeys = Object.keys(pl)
|
||||
|
||||
// Keys in EN but not in PL
|
||||
const missingInPl = enKeys.filter(k => !(k in pl))
|
||||
|
||||
// Keys where PL value equals EN value (potential untranslated)
|
||||
const untranslated = enKeys.filter(key => {
|
||||
if (EXCLUSIONS.includes(en[key])) return false
|
||||
if (key.includes('|')) return false // Skip plurals (different structure)
|
||||
return en[key] === pl[key]
|
||||
})
|
||||
|
||||
// Plural keys with mismatched form counts
|
||||
const pluralMismatch = enKeys.filter(key => {
|
||||
if (!key.includes('|')) return false
|
||||
const enForms = (en[key].match(/\|/g) || []).length + 1
|
||||
const plForms = (pl[key].match(/\|/g) || []).length + 1
|
||||
return enForms !== plForms
|
||||
})
|
||||
|
||||
console.log('\n=== i18n Coverage Report ===\n')
|
||||
console.log(`Total keys: ${enKeys.length}`)
|
||||
console.log(`Missing in pl.json: ${missingInPl.length}`)
|
||||
console.log(`Potentially untranslated: ${untranslated.length}`)
|
||||
console.log(`Plural form mismatch: ${pluralMismatch.length}`)
|
||||
|
||||
if (missingInPl.length > 0) {
|
||||
console.log('\n--- Missing in pl.json ---')
|
||||
missingInPl.forEach(k => console.log(` ${k}`))
|
||||
}
|
||||
|
||||
if (untranslated.length > 0) {
|
||||
console.log('\n--- Potentially Untranslated ---')
|
||||
untranslated.slice(0, 20).forEach(k => console.log(` ${k}: "${en[k]}"`))
|
||||
if (untranslated.length > 20) {
|
||||
console.log(` ... and ${untranslated.length - 20} more`)
|
||||
}
|
||||
}
|
||||
|
||||
if (pluralMismatch.length > 0) {
|
||||
console.log('\n--- Plural Form Mismatch (EN vs PL) ---')
|
||||
pluralMismatch.forEach(k => {
|
||||
const enForms = (en[k].match(/\|/g) || []).length + 1
|
||||
const plForms = (pl[k].match(/\|/g) || []).length + 1
|
||||
console.log(` ${k}: EN has ${enForms} forms, PL has ${plForms} forms`)
|
||||
})
|
||||
}
|
||||
|
||||
// Exit code for CI
|
||||
const hasIssues = missingInPl.length > 0 || pluralMismatch.length > 0
|
||||
process.exit(hasIssues ? 1 : 0)
|
||||
```
|
||||
|
||||
### Running Coverage Report
|
||||
```bash
|
||||
npx tsx scripts/i18n-coverage.ts
|
||||
```
|
||||
|
||||
### Polish Pluralization Test Values
|
||||
Based on CLDR rules, the comprehensive test set for Polish:
|
||||
```typescript
|
||||
// Test values that cover all plural categories
|
||||
const testValues = [
|
||||
// Zero form (index 0)
|
||||
0,
|
||||
|
||||
// One form (index 1) - ends in 1, not 11
|
||||
1, 21, 31, 41, 51, 101, 121,
|
||||
|
||||
// Few form (index 2) - ends in 2-4, not 12-14
|
||||
2, 3, 4, 22, 23, 24, 32, 33, 34, 102, 103, 104,
|
||||
|
||||
// Many form (index 3) - everything else including teens
|
||||
5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 16, 17, 18, 19, // Teens always "many"
|
||||
20, 25, 26, 27, 28, 29, 30,
|
||||
100, 105, 111, 112 // Large numbers
|
||||
]
|
||||
```
|
||||
|
||||
### Manual Testing Checklist Template
|
||||
```markdown
|
||||
## Manual Polish Testing Checklist
|
||||
|
||||
### Auth Flow
|
||||
- [ ] /login - All labels, buttons, errors in Polish
|
||||
- [ ] /register - Form labels, validation messages
|
||||
- [ ] Password reset flow
|
||||
|
||||
### Parent Dashboard
|
||||
- [ ] /parent - Welcome message, stats, pending approvals
|
||||
- [ ] /parent/children - Child cards, empty state
|
||||
- [ ] /parent/templates - Template library, filters
|
||||
- [ ] /parent/settings - All 7 tabs
|
||||
- [ ] /parent/profile - All 3 tabs
|
||||
|
||||
### Child Dashboard
|
||||
- [ ] /child - Hero greeting, stats, quests
|
||||
- [ ] /child/quests - Quest cards, status labels
|
||||
- [ ] /child/shop - Rewards, purchase flow
|
||||
- [ ] /child/achievements - Badges, unlock dates
|
||||
- [ ] /child/profile - Stats, PIN change
|
||||
|
||||
### Visual Checks (Polish strings often longer)
|
||||
- [ ] Buttons don't overflow
|
||||
- [ ] Table headers fit
|
||||
- [ ] Modal titles don't truncate
|
||||
- [ ] Navigation labels fit
|
||||
|
||||
### Pluralization Live Check
|
||||
- [ ] 1 quest assigned
|
||||
- [ ] 2 quests assigned
|
||||
- [ ] 5 quests assigned
|
||||
- [ ] 21 quests assigned (tricky!)
|
||||
```
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Jest for Vue | Vitest | 2023 | Faster, better TypeScript support |
|
||||
| @nuxtjs/i18n v8 | @nuxtjs/i18n v10 | 2025 | Better lazy loading, bundle optimization |
|
||||
| Manual key audit | Automated coverage scripts | Always | Catch regressions in CI |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- `vue-i18n-jest`: Use `vitest` directly
|
||||
- `nuxt-vitest`: Merged into `@nuxt/test-utils`
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Console Warning Logging**
|
||||
- What we know: vue-i18n can log fallback warnings
|
||||
- What's unclear: How to capture warnings during manual testing
|
||||
- Recommendation: Set `missingWarn: true` in i18n.config.ts during Polish testing, watch browser console
|
||||
|
||||
2. **CI Integration Scope**
|
||||
- What we know: Coverage script can exit with error code
|
||||
- What's unclear: Should this block deploys or just warn?
|
||||
- Recommendation: Start as warning, consider blocking after Phase 14 complete
|
||||
|
||||
## Polish Pluralization Rules Reference
|
||||
|
||||
Based on CLDR (Unicode Common Locale Data Repository):
|
||||
|
||||
| Category | Condition | Examples | Form Index |
|
||||
|----------|-----------|----------|------------|
|
||||
| **zero** | n = 0 | 0 | 0 |
|
||||
| **one** | n % 10 = 1 AND n % 100 != 11 | 1, 21, 31, 101 | 1 |
|
||||
| **few** | n % 10 in 2..4 AND n % 100 not in 12..14 | 2, 3, 4, 22, 23, 24 | 2 |
|
||||
| **many** | n % 10 = 0 OR n % 10 in 5..9 OR n % 100 in 11..14 | 0, 5, 10, 11, 12, 13, 14, 15, 20, 25 | 3 |
|
||||
|
||||
Example translations:
|
||||
```
|
||||
1 misja, 2 misje, 5 misji, 11 misji, 21 misja, 22 misje
|
||||
1 dzień, 2 dni, 5 dni, 11 dni, 21 dzień, 22 dni
|
||||
1 dziecko, 2 dzieci, 5 dzieci, 11 dzieci, 21 dziecko, 22 dzieci
|
||||
```
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- Project codebase: `vue-queststream-app/i18n/locales/*.json` - verified key counts
|
||||
- Project codebase: `vue-queststream-app/i18n/i18n.config.ts` - verified pluralization rule
|
||||
- [Nuxt Testing Documentation](https://nuxt.com/docs/getting-started/testing) - Vitest setup
|
||||
- [Vue I18n Pluralization](https://vue-i18n.intlify.dev/guide/essentials/pluralization) - Pipe syntax
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- [CLDR Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules) - Polish rule reference
|
||||
- Phase 13 RESEARCH.md - established i18n patterns
|
||||
- Phase 13 SUMMARY files - translation string counts
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- WebSearch for testing tools - validation needed during implementation
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH - verified with official Nuxt docs
|
||||
- Architecture patterns: HIGH - patterns verified with codebase
|
||||
- Pluralization rules: HIGH - verified with CLDR reference
|
||||
- Testing approach: MEDIUM - Vitest setup needs validation during implementation
|
||||
- Pitfalls: HIGH - based on Phase 13 learnings
|
||||
|
||||
**Research date:** 2026-01-29
|
||||
**Valid until:** 90 days (stable patterns, minimal ecosystem churn expected)
|
||||
Reference in New Issue
Block a user