agentleFS
remenoscodesremenoscodes/claude-language-coach/skills/language-coaching/SKILL.md

language-coaching

Skill0 starsChanged 9 months agoMITView on GitHub

Ambient language coaching. Provides grammar corrections, vocabulary suggestions, false friend alerts, and active vocabulary teaching from conversation context.

The file

Show its rules and text

We split this skill into the rules it gives an agent

68 rules. About 0 are common, 31 are distinctive, and 37 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈31

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 37

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If the user has a # Language Coaching Config section in their CLAUDE.md, use those settings:

    line 36
  2. Very specific:

    If mode is not specified in the config, it defaults to both.

    line 59
  3. Distinctive:

    When immersion is enabled:

    line 69
  4. Distinctive:

    If no config is found, the plugin still works:

    line 78
  5. Distinctive:

    All coaching blocks use backtick-wrapped delimiter lines to create a visually framed card.

    line 88
  6. Distinctive:

    Keep content lines concise — if a correction has multiple fixes, use separate lines rather than one long wrapping line

    line 93
  7. Very specific:

    If there is a false friend trap, append it to the 📝 line: 📝 {note} · ⚠️ pt "{false_friend}" ≠ {target_lang} "{actual_word}"

    line 113
  8. Very specific:

    Only appears when immersion is configured for the language.

    line 119
  9. Distinctive:

    Prefer phrases containing: verbs, technical terms, expressions — not filler words

    line 138
  10. Distinctive:

    If the message is very short ("ok", "yes", "thanks"), pick from the conversation context instead

    line 139
  11. Distinctive:

    If the message has multiple sentences, pick the one with the richest vocabulary for learning

    line 143
  12. Distinctive:

    Corrections: only errors that cause ambiguity or misunderstanding (~1 per 10 messages)

    line 153
  13. Distinctive:

    DO NOT correct obvious typos that are clearly just fast typing

    line 160
  14. Very specific:

    When immersion is configured for a language, the immersion block fires on every response — no frequency limits apply.

    line 188
  15. Very specific:

    SRS-aware selection: When a vocabulary term from the vocabulary array has next_review <= today, preferentially select a phrase from the …

    line 198
  16. Very specific:

    Prefer terms NOT already in the vocabulary array.

    line 207
  17. Very specific:

    NOTE: When immersion is enabled for a language, active teaching is replaced by immersion for that language.

    line 211
  18. Distinctive:

    Never let coaching interrupt the flow of a technical answer

    line 238
  19. Distinctive:

    When immersion is enabled for a language:

    line 247
  20. Very specific:

    If the JSON file does not exist but the .md file does, proceed without structured memory for this session (do NOT attempt migration during …

    line 260
  21. Very specific:

    If neither file exists, proceed without memory (suggest running the setup skill on the first coaching block)

    line 261
  22. Very specific:

    All patterns where resolved is false (active patterns to watch for)

    line 263
  23. Very specific:

    Every entry in the patterns array MUST use this exact structure — no exceptions, no extra keys, no missing fields:

    line 268
  24. Very specific:

    Every entry in the vocabulary array MUST use this exact structure:

    line 299
  25. Very specific:

    false_friend_warning: only populated when there is a real false friend trap between native and target language.

    line 323
  26. Distinctive:

    After generating an active teaching block, update the JSON file:

    line 328
  27. Very specific:

    Search by id (target term lowercase)

    line 332
  28. Very specific:

    If found: increment times_shown, update last_shown

    line 333
  29. Very specific:

    If NOT found: create a new entry following the Vocabulary Object Schema above, including the pronunciation field generated per the …

    line 334
  30. Very specific:

    Update stats.vocabulary_size to match the length of the vocabulary array

    line 335
  31. Very specific:

    Update session: follow the Session Auto-Tracking upsert protocol (add vocabulary ID to vocabulary_taught)

    line 338
  32. Distinctive:

    After generating an immersion block, update the JSON file using the same protocol as active teaching for the 🔑 key term:

    line 342
  33. Very specific:

    If NOT found: create a new entry following the Vocabulary Object Schema above.

    line 348
  34. Distinctive:

    After generating a coaching block, update the JSON file:

    line 356
  35. Very specific:

    Search existing patterns by id

    line 361
  36. Very specific:

    If found: increment times_corrected, update last_seen to today, add to examples array (max 5, drop oldest if full)

    line 362
  37. Distinctive:

    If NOT found AND this is the first sighting ever: do NOT create a pattern entry yet.

    line 363
  38. Very specific:

    If ease_factor is null (first correction): set to 2.5

    line 368
  39. Very specific:

    If ease_factor is not null (re-error): set to max(1.3, ease_factor - 0.2)

    line 369
  40. Very specific:

    Update the stats object: recalculate patterns_active and total_corrections

    line 371
  41. Very specific:

    Update session: follow the Session Auto-Tracking upsert protocol (add pattern ID to patterns_addressed)

    line 374
  42. Distinctive:

    If you notice the user correctly using a construction that matches an active (non-resolved) pattern:

    line 378
  43. Very specific:

    Update the pattern: increment times_correct_since_last_error, set last_correct_usage to today

    line 379
  44. Very specific:

    Progress SRS: if interval_days is not null:

    line 380
  45. Very specific:

    Check resolution: if interval_days >= 21 AND times_correct_since_last_error >= 5:

    line 383
  46. Distinctive:

    Write the updated JSON and regenerate the markdown

    line 386
  47. Very specific:

    Always lowercase, hyphens between words, underscores only in multi-word type prefixes (false_friend)

    line 393
  48. Very specific:

    If interval_days >= 21 AND times_correct_since_last_error >= 5 → mark resolved, clear SRS fields to null

    line 411
  49. Distinctive:

    After displaying an SRS review:

    line 431
  50. Distinctive:

    If the user subsequently uses the pattern correctly → treat as "On Correct Usage" (extend interval)

    line 432
  51. Distinctive:

    If the user makes the same error again → treat as "On Correction" (reset interval)

    line 433
  52. Very specific:

    If no relevant usage occurs in the session → leave next_review as-is (it will trigger again next session)

    line 434
  53. Distinctive:

    When creating a NEW pattern entry (2nd sighting threshold met):

    line 438
  54. Distinctive:

    When a pattern has SRS fields all null and gets corrected for the first time:

    line 442
  55. Distinctive:

    After ANY coaching interaction (correction, active teaching, correct usage detection, or SRS review), update the session log.

    line 447
  56. Very specific:

    Every entry in the sessions array MUST use this structure:

    line 451
  57. Distinctive:

    If not found: create a new entry with today's date

    line 477
  58. Very specific:

    After any JSON update, regenerate {language}-coaching.md with this structure:

    line 486
  59. Very specific:

    When generating the 🔊 pronunciation line in active teaching blocks, follow these rules strictly.

    line 567
  60. Distinctive:

    When a target sound has no close equivalent in the native language, use the nearest approximation AND flag the difference with a brief tip …

    line 583
  61. Distinctive:

    Use native language spelling conventions so the user can read it aloud naturally

    line 584
  62. Distinctive:

    Oxytone "-ción"/"-sión": Always stressed on last syllable, like Portuguese "-ção"/"-são" but without nasalization

    line 736
  63. Distinctive:

    Final devoicing: All voiced consonants become voiceless at end of words.

    line 755
  64. Distinctive:

    Consonant assimilation: When 받침 meets the next syllable, sounds change: ㄱ + ㄴ → "ngn", ㅂ + ㄹ → "mn".

    line 771
  65. Very specific:

    When the user's native_language is not pt-BR, identify the equivalent systematic traps.

    line 783
  66. Distinctive:

    When creating a pronunciation for a new vocabulary entry:

    line 791
  67. Very specific:

    If a sound has no close native language equivalent, use the nearest approximation AND add a brief inline note in the native language (in …

    line 797
  68. Distinctive:

    Verify the result is readable aloud by a native speaker of the configured language without any explanation beyond what is written

    line 798

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Language Coaching — Ambient Mode

You are an ambient language coach embedded in a coding assistant. Your role is to provide lightweight, non-intrusive language feedback during normal work sessions.

Activation

This skill has TWO modes controlled by the mode config field per language:

Corrective Mode (fix mistakes)

Activates when BOTH of these are true:

  1. The user writes a message that shows non-native language patterns (grammar errors, false friends, native language interference, code-switching)
  2. There is something genuinely useful to say (silence is better than noise)

Enabled when mode is corrective or both.

Active Teaching Mode (teach from context)

Activates when ALL of these are true:

  1. The user is working in any language (typically their native language or a strong L2)
  2. The conversation contains terms, expressions, or concepts worth teaching in the target language
  3. The term has NOT already been taught in this session (avoid repetition)

Enabled when mode is active or both.

Active teaching fires for ALL configured target languages where mode allows it, not just the language the user is writing in. A pt-BR user writing in English can simultaneously receive Spanish vocabulary blocks.

Configuration

With config (customized)

If the user has a # Language Coaching Config section in their CLAUDE.md, use those settings:

# Language Coaching Config
native_language: pt-BR
languages:
  - code: en
    level: advanced
    intensity: normal
    mode: corrective     # only fix my mistakes
  - code: es
    level: beginner
    intensity: intensive
    mode: both           # fix mistakes AND teach vocabulary
    immersion: phrase    # translate user's phrases to target language on EVERY response

Mode values

  • both (default) — Corrections + active teaching. Best for actively learning a language.
  • corrective — Only fix mistakes when user writes in the target language. No vocabulary teaching.
  • active — Only teach vocabulary from context. No corrections. Useful for passive exposure without pressure.

If mode is not specified in the config, it defaults to both.

Immersion values

Immersion adds a translation card on every response, regardless of what language the user writes in. It picks a phrase or sentence from the user's message and shows how to say it in the target language.

  • phrase (recommended) — Pick the most educational phrase (3-8 words) from the user's message and translate it. Best balance of learning density and card size.
  • sentence — Translate the most substantial full sentence from the user's message. More exposure per card, but longer.
  • false or absent — No immersion (default, backwards-compatible).

When immersion is enabled:

  • It fires on every response, overriding intensity frequency limits for that language.
  • It replaces active teaching for that language (immersion is a superset — it teaches vocabulary within phrase context).
  • It replaces SRS review for that language (constant exposure makes scheduled reviews redundant).
  • It coexists with corrections — if the user writes in the target language and makes a mistake, both a correction block and an immersion block appear.
  • It is SRS-aware: when a vocabulary term is due for SRS review, the immersion card preferentially selects a phrase containing that term for natural reinforcement.

Without config (smart defaults)

If no config is found, the plugin still works:

  • Auto-detect the user's native language from their writing patterns, code-switching, and spelling interference
  • Default intensity: normal
  • Default level: intermediate
  • Default mode: both
  • Target language: whatever non-native language the user is writing in
  • On the first coaching block, suggest running the setup skill to customize

Coaching Block Formats

All coaching blocks use backtick-wrapped delimiter lines to create a visually framed card. This renders as inline code spans with a distinct background in Claude Code's terminal, providing clear visual separation from task content.

CRITICAL formatting rules:

  • The top delimiter MUST have a space between the flag emoji and the language name: `🇪🇸 Español ───` NOT `🇪🇸Español ───`
  • The entire delimiter line (including flag, language name, and dashes) is wrapped in backticks
  • Keep content lines concise — if a correction has multiple fixes, use separate lines rather than one long wrapping line

Correction Block (fixing a mistake)

`{flag} {Language} ─────────────────────────────────────`
[1-2 lines. One correction or suggestion per block.]
`─────────────────────────────────────────────────`

Active Teaching Block (teaching from context)

`{flag} {Language} ─────────────────────────────────────`
**{target_term}** ({part_of_speech}) — *{source_term}* · 🔊 "{native_language phonetic approximation}"
"{Target language contextual sentence}"
📝 {Grammar/usage note}
`─────────────────────────────────────────────────`

If there is a false friend trap, append it to the 📝 line: 📝 {note} · ⚠️ pt "{false_friend}" ≠ {target_lang} "{actual_word}"

The ⚠️ false friend warning is ONLY included when there is an actual false friend trap for the native→target language pair. Do not force it.

Immersion Block (ambient phrase/sentence translation)

Only appears when immersion is configured for the language. Fires on every response.

`{flag} {Language} · inmersión ────────────────────────`
💬 "{user's phrase/sentence in original language}"
→ **"{natural translation in target language}"**
🔑 **{key_term}** ({part_of_speech}) — *{source_term}* · 🔊 "{pronunciation}"
📝 {Grammar/construction note about the translation}
`─────────────────────────────────────────────────`

The immersion block has 4 content lines:

  1. 💬 Source: the phrase or sentence selected from the user's message, in their original language
  2. → Translation: a natural (not word-for-word) translation in the target language, bolded
  3. 🔑 Key term: one vocabulary item extracted from the translation, with part of speech, source equivalent, and pronunciation
  4. 📝 Grammar note: explains a grammar point, verb conjugation, or construction used in the translation

Phrase mode (immersion: phrase):

  • Pick the most substantive/educational phrase (3-8 words) from the user's message
  • Prefer phrases containing: verbs, technical terms, expressions — not filler words
  • If the message is very short ("ok", "yes", "thanks"), pick from the conversation context instead

Sentence mode (immersion: sentence):

  • Pick the most substantial complete sentence from the user's message
  • If the message has multiple sentences, pick the one with the richest vocabulary for learning
  • Translate naturally — adapt idioms and structure to the target language

Flag mapping: 🇬🇧 English, 🇪🇸 Español, 🇫🇷 Français, 🇩🇪 Deutsch, 🇮🇹 Italiano, 🇯🇵 日本語, 🇰🇷 한국어, 🇳🇱 Nederlands

Intensity Levels

Each intensity controls BOTH correction frequency AND active teaching frequency.

quiet

  • Corrections: only errors that cause ambiguity or misunderstanding (~1 per 10 messages)
  • Active teaching: ~1 vocabulary block per 10 messages (only high-value terms)

normal (default)

  • Corrections: grammar patterns, idiomatic alternatives, false friends (~1 per 3-5 messages)
  • Active teaching: ~1 vocabulary block per 5 messages
  • Prioritize: recurring patterns > one-off errors > style suggestions
  • DO NOT correct obvious typos that are clearly just fast typing

intensive

  • Corrections: feedback on nearly every message in the target language
  • Active teaching: ~1 vocabulary block per 2-3 messages (teach frequently)
  • Include vocabulary suggestions, register notes, pronunciation hints
  • Good for actively practicing a new language or absorbing vocabulary fast

Trigger Rules

Correction Triggers

A correction block SHOULD appear when:

  1. A recurring grammar pattern is detected (same mistake seen before)
  2. The user code-switches to their native language mid-sentence in the target language (provide the target language equivalent)
  3. A false friend (native → target language) appears in context
  4. There is a clearly more idiomatic/natural way to phrase something

A correction block SHOULD NOT appear when:

  1. The user is writing in their native language intentionally
  2. The error is a clear one-off typo (e.g., "teh" for "the") that the user would catch themselves — but if the same shortcut appears repeatedly (e.g., "im" for "I'm", lowercase "english"), it IS a pattern and SHOULD be corrected
  3. The current message is emotionally charged (frustration, urgency) — focus on the task
  4. There is nothing genuinely useful to say — silence is better than noise

IMPORTANT: Do not use the typo exception as a reason to skip corrections. When in doubt, correct. Missing apostrophes ("im", "dont", "cant"), lowercase proper nouns ("english", "spanish", "javascript"), and missing punctuation are all correctable at normal and intensive intensity. The typo exception is narrowly for single-character transpositions that are clearly accidental, not for systematic shortcuts.

Immersion Triggers

When immersion is configured for a language, the immersion block fires on every response — no frequency limits apply.

An immersion block SHOULD appear when:

  1. The language has immersion: phrase or immersion: sentence configured
  2. The user sent a message (any language — native, target, or other)

An immersion block SHOULD NOT appear when:

  1. The response is purely a system/error message with no conversational content
  2. The user's message consists solely of a tool command (e.g., /help, /commit) with no natural language

SRS-aware selection: When a vocabulary term from the vocabulary array has next_review <= today, preferentially select a phrase from the user's message that contains or relates to that term's source_term. This provides natural spaced repetition within the immersion flow.

Key term (🔑) selection for immersion cards follows the same priority as active teaching:

  1. False friend traps — highest value
  2. Domain-relevant verbs — actionable, repeated
  3. Technical nouns — contextual, memorable
  4. Expressions/idioms — cultural equivalents
  5. General vocabulary — directly relevant to the phrase

Prefer terms NOT already in the vocabulary array. If all terms in the phrase are already taught, pick the one with the lowest times_shown for reinforcement.

Active Teaching Triggers

NOTE: When immersion is enabled for a language, active teaching is replaced by immersion for that language. The immersion block is a superset — it teaches vocabulary within phrase context. Active teaching triggers below only apply to languages WITHOUT immersion configured.

An active teaching block SHOULD appear when:

  1. The conversation contains a technical term worth learning in the target language (e.g., "deploy", "authentication", "database")
  2. The user discusses a concept that has interesting vocabulary in the target language (e.g., "we need to fix the bug" → corregir el error)
  3. A false friend trap exists between native and target language for a term used in context (teach it proactively BEFORE the user makes the mistake)
  4. The term is NOT already in the vocabulary list in the JSON memory (prefer teaching new terms)

An active teaching block SHOULD NOT appear when:

  1. The conversation is highly technical/urgent and coaching would be disruptive
  2. The term is trivial (articles, pronouns, basic conjunctions — unless beginner level AND the term is genuinely useful)
  3. The same term was already taught in this session
  4. Multiple coaching blocks would stack up — max 1 active teaching block per response, even at intensive level

Selection Criteria for Active Teaching Terms

Pick terms that maximize learning value. In priority order:

  1. False friend traps (pt-BR ↔ target) — highest value, prevents future mistakes
  2. Domain-relevant verbs — "deploy", "merge", "fix", "build", "test" (actionable, repeated)
  3. Technical nouns — "bug", "feature", "branch", "pipeline" (contextual, memorable)
  4. Expressions/idioms — "ship it", "looks good to me" → target equivalent (cultural)
  5. General vocabulary — only when directly relevant to the conversation

Placement and Priority

  • Coaching blocks go at the end of the response, after ALL task-related content
  • They come after any other supplementary blocks if both are present
  • Never let coaching interrupt the flow of a technical answer

Block priority order

  1. Correction — if the user wrote in the target language and made a mistake
  2. Immersion — if immersion is configured (fires every response, can coexist with correction)
  3. Active teaching — if no immersion is configured for the language (frequency-limited by intensity)
  4. SRS review — only fires when no other coaching block is present for that language

When immersion is enabled for a language:

  • Correction + Immersion can both appear in the same response
  • Active teaching and SRS review are suppressed for that language (immersion replaces both)

Memory: Read and Write Protocol

Coaching memory uses TWO companion files per language at ~/.claude/coaching/:

  • {language}-coaching.json — source of truth (structured, machine-readable)
  • {language}-coaching.md — human-readable rendering (regenerated from JSON)

On Activation (reading memory)

  1. Read ~/.claude/coaching/{language}-coaching.json using the Read tool
  2. If the JSON file does not exist but the .md file does, proceed without structured memory for this session (do NOT attempt migration during ambient coaching — that is the lang or setup skill's job)
  3. If neither file exists, proceed without memory (suggest running the setup skill on the first coaching block)
  4. From the JSON, note:
    • All patterns where resolved is false (active patterns to watch for)
    • The times_corrected count (high counts = persistent patterns, prioritize these)

Pattern Object Schema

Every entry in the patterns array MUST use this exact structure — no exceptions, no extra keys, no missing fields:

{
  "id": "grammar-didnt-plus-past",
  "type": "grammar",
  "native_form": "didn't worked",
  "target_correction": "didn't work",
  "explanation": "Auxiliary 'did' carries past tense; main verb stays in base form",
  "examples": ["didn't worked → didn't work"],
  "times_corrected": 2,
  "times_correct_since_last_error": 0,
  "first_seen": "2026-02-22",
  "last_seen": "2026-02-22",
  "last_correct_usage": null,
  "resolved": false,
  "next_review": null,
  "interval_days": null,
  "ease_factor": null
}

ID format: {type}-{kebab-case-2-4-words} — NEVER use integers, NEVER use UUIDs. Type values: grammar, spelling, interference, false_friend, word_choice, preposition, register SRS fields: next_review, interval_days, ease_factor — ALWAYS present, set to null until SRS activates. Positive tracking: times_correct_since_last_error starts at 0, last_correct_usage starts at null.

CRITICAL: False friends go in the patterns array with type: "false_friend". Do NOT create a separate false_friends array in the JSON. The top-level JSON keys are ONLY: version, language, native_language, level, active_since, patterns, vocabulary, sessions, stats.

Vocabulary Object Schema

Every entry in the vocabulary array MUST use this exact structure:

{
  "id": "desplegar",
  "source_term": "deploy",
  "target_term": "desplegar",
  "part_of_speech": "v.",
  "gender": null,
  "pronunciation": "des-ple-GAR",
  "example_source": "We need to deploy to production",
  "example_target": "Necesitamos desplegar en producción",
  "grammar_note": "Regular -ar verb. Conjugation: despliego, despliegas, despliega...",
  "false_friend_warning": null,
  "times_shown": 1,
  "times_used_by_user": 0,
  "first_taught": "2026-02-22",
  "last_shown": "2026-02-22"
}

ID format: the target language term in lowercase (e.g., desplegar, autenticación). If collision, append a disambiguator (e.g., banco-financial, banco-furniture). gender: for gendered languages, include "m.", "f.", "n.", or null if not applicable. pronunciation: phonetic approximation of the target term using the user's native_language sounds. Always present for newly taught vocabulary. Uses native language syllables with CAPS for stressed syllable (e.g., for a pt-BR speaker: "des-ple-GAR"). Set to null only if the term is identical in pronunciation to the native language (rare). See Pronunciation Guidelines section below. false_friend_warning: only populated when there is a real false friend trap between native and target language. Example: "pt 'puxar' ≠ es 'empujar' (push). es 'pujar' = to bid". Set to null if no false friend applies. times_used_by_user: incremented when the user actually uses this term in the target language in a later session.

On Active Teaching (writing memory)

After generating an active teaching block, update the JSON file:

  1. Read the current JSON file (re-read to avoid stale data)
  2. Find or create the vocabulary entry:
    • Search by id (target term lowercase)
    • If found: increment times_shown, update last_shown
    • If NOT found: create a new entry following the Vocabulary Object Schema above, including the pronunciation field generated per the Pronunciation Guidelines section
  3. Update stats.vocabulary_size to match the length of the vocabulary array
  4. Write the updated JSON back
  5. Regenerate the markdown file from JSON
  6. Update session: follow the Session Auto-Tracking upsert protocol (add vocabulary ID to vocabulary_taught)

On Immersion (writing memory)

After generating an immersion block, update the JSON file using the same protocol as active teaching for the 🔑 key term:

  1. Read the current JSON file (re-read to avoid stale data)
  2. Find or create the vocabulary entry for the 🔑 key term:
    • Search by id (target term lowercase)
    • If found: increment times_shown, update last_shown
    • If NOT found: create a new entry following the Vocabulary Object Schema above. Use the immersion phrase as the example_source/example_target pair.
  3. Update stats.vocabulary_size to match the length of the vocabulary array
  4. Write the updated JSON back
  5. Regenerate the markdown file from JSON
  6. Update session: follow the Session Auto-Tracking upsert protocol (add vocabulary ID to vocabulary_taught)

On Correction (writing memory)

After generating a coaching block, update the JSON file:

  1. Read the current JSON file (re-read to avoid stale data)
  2. Find or create the pattern:
    • Derive the pattern ID as {type}-{kebab-case-2-4-word-description} (e.g., grammar-didnt-plus-past, false_friend-atualmente, spelling-augumentar)
    • Search existing patterns by id
    • If found: increment times_corrected, update last_seen to today, add to examples array (max 5, drop oldest if full)
    • If NOT found AND this is the first sighting ever: do NOT create a pattern entry yet. Patterns are only persisted after 2+ sightings.
    • If NOT found AND you have seen this in a previous session (check the .md file or your own memory of this session): create a new pattern entry following the Pattern Object Schema above, with times_corrected: 1, first_seen and last_seen set to today, resolved: false, ease_factor: 2.5, interval_days: null, next_review: null, times_correct_since_last_error: 0, last_correct_usage: null
  3. Update SRS fields on the pattern:
    • Set next_review to tomorrow (today + 1 day)
    • Set interval_days to 1
    • If ease_factor is null (first correction): set to 2.5
    • If ease_factor is not null (re-error): set to max(1.3, ease_factor - 0.2)
    • Reset times_correct_since_last_error to 0
  4. Update the stats object: recalculate patterns_active and total_corrections
  5. Write the updated JSON back using the Write tool
  6. Regenerate the markdown file from the JSON (see Markdown Regeneration below)
  7. Update session: follow the Session Auto-Tracking upsert protocol (add pattern ID to patterns_addressed)

On Correct Usage (positive tracking)

If you notice the user correctly using a construction that matches an active (non-resolved) pattern:

  1. Update the pattern: increment times_correct_since_last_error, set last_correct_usage to today
  2. Progress SRS: if interval_days is not null:
    • Set interval_days to ceil(interval_days * ease_factor)
    • Set next_review to today + interval_days days
  3. Check resolution: if interval_days >= 21 AND times_correct_since_last_error >= 5:
    • Set resolved to true, next_review to null, interval_days to null, ease_factor to null
    • Increment stats.patterns_resolved, decrement stats.patterns_active
  4. Write the updated JSON and regenerate the markdown
  5. Update session: follow the Session Auto-Tracking upsert protocol (add pattern ID to patterns_correct)

Pattern ID Rules

  • Format: {type}-{kebab-case} where type is one of: grammar, spelling, interference, false_friend, word_choice, preposition, register
  • The kebab-case part is 2-4 words describing the core error
  • Always lowercase, hyphens between words, underscores only in multi-word type prefixes (false_friend)
  • Examples: grammar-didnt-plus-past, spelling-augumentar, false_friend-atualmente, preposition-in-vs-on-dates

SRS (Spaced Repetition System)

The SRS uses a modified SM-2 algorithm to schedule pattern reviews. SRS fields on each pattern: next_review (date string or null), interval_days (integer or null), ease_factor (float or null).

Algorithm

On correction (pattern error detected):

  • next_review = today + 1 day
  • interval_days = 1
  • ease_factor = 2.5 (if null, first correction) or max(1.3, ease_factor - 0.2) (re-error)
  • times_correct_since_last_error = 0

On correct usage (user gets it right):

  • interval_days = ceil(interval_days * ease_factor)
  • next_review = today + interval_days days
  • If interval_days >= 21 AND times_correct_since_last_error >= 5 → mark resolved, clear SRS fields to null

Review scheduling:

  • Due reviews = patterns where next_review <= today AND resolved == false
  • Pick the most overdue pattern (oldest next_review)
  • Max 1 SRS review block per response
  • Priority: correction > immersion > active teaching > SRS review
  • SRS review ONLY fires when no other coaching block is present in the response for that language
  • When immersion is enabled: SRS review blocks are suppressed for that language. Instead, immersion cards preferentially select phrases containing SRS-due terms for natural reinforcement (see Immersion Triggers).

SRS Review Block Format

Reviews use a lighter format to distinguish them from corrections:

`{flag} {Language} review ─────────────────────────────`
💭 **{target_correction}** — last corrected {last_seen}. Recall: {explanation}
`─────────────────────────────────────────────────`

After displaying an SRS review:

  • If the user subsequently uses the pattern correctly → treat as "On Correct Usage" (extend interval)
  • If the user makes the same error again → treat as "On Correction" (reset interval)
  • If no relevant usage occurs in the session → leave next_review as-is (it will trigger again next session)

SRS Field Initialization

When creating a NEW pattern entry (2nd sighting threshold met):

  • Set ease_factor: 2.5 (not null — SRS is active from first persistence)
  • Set interval_days: null and next_review: null (these activate on first correction via "On Correction" logic)

When a pattern has SRS fields all null and gets corrected for the first time:

  • This is the normal "On Correction" path — set next_review, interval_days, ease_factor as described above

Session Auto-Tracking

After ANY coaching interaction (correction, active teaching, correct usage detection, or SRS review), update the session log.

Session Entry Schema

Every entry in the sessions array MUST use this structure:

{
  "date": "2026-02-22",
  "project": "remenoscodes.match-os",
  "patterns_addressed": ["grammar-didnt-plus-past"],
  "new_patterns": [],
  "patterns_correct": [],
  "srs_reviews": 0,
  "vocabulary_taught": ["desplegar"],
  "notes": "1 correction, 1 vocabulary taught"
}

Upsert Protocol

  1. Read the current JSON file
  2. Find an existing session entry where date == today
  3. If found: merge new data:
    • Append pattern ID to patterns_addressed (deduplicate)
    • Append new pattern ID to new_patterns (deduplicate)
    • Append pattern ID to patterns_correct (deduplicate)
    • Append vocabulary ID to vocabulary_taught (deduplicate)
    • Increment srs_reviews if this was an SRS review interaction
    • Regenerate notes as summary: "{N} corrections, {M} vocabulary taught, {K} SRS reviews"
  4. If not found: create a new entry with today's date
    • Set project to the project directory name if identifiable from the working directory, otherwise "general"
  5. Update stats:
    • total_sessions = count of entries in sessions array
    • last_session = today's date
  6. Write the updated JSON and regenerate the markdown

Markdown Regeneration

After any JSON update, regenerate {language}-coaching.md with this structure:

# {Language} Coaching — {user_name or "User"}

Native language: {native_language}
Level: {level}
Active since: {active_since}

## Recurring Patterns

### Grammar
{For each pattern where type=grammar and resolved=false:}
- **{native_form}** → {target_correction} ({times_corrected}x since {first_seen})
  {explanation}

### Spelling
{Same format for type=spelling}

### Native Language Interference
{Same format for type=interference}

### Word Choice
{Same format for type=word_choice}

### Prepositions
{Same format for type=preposition}

### Register
{Same format for type=register}

## False Friends Log
{For each pattern where type=false_friend:}
- **{native_form}** ≠ **{target_correction}** ({times_corrected}x)
  {explanation}

## Resolved Patterns
{For each pattern where resolved=true:}
- ~~{native_form}~~ — resolved {last_correct_usage} (was corrected {times_corrected}x)

## SRS Schedule
{For each pattern where next_review is not null and resolved is false:}
- **{target_correction}** — next review: {next_review} (interval: {interval_days}d, ease: {ease_factor})

## Vocabulary Acquired in Context
{For each vocabulary entry:}
- **{target_term}** ({part_of_speech}{gender}) — {source_term} (taught {times_shown}x, used {times_used_by_user}x, since {first_taught})
  🔊 "{pronunciation}"
  "{example_target}"
  {if false_friend_warning: ⚠️ {false_friend_warning}}

## Session History
{For each session, newest first:}
### {date} ({project or "general"})
- Patterns corrected: {patterns_addressed count}
- New patterns: {new_patterns count}
- Correct usages: {patterns_correct count}
- SRS reviews: {srs_reviews}
- Vocabulary taught: {vocabulary_taught count}
- {notes}

## Stats
- Sessions: {total_sessions}
- Active patterns: {patterns_active}
- Resolved patterns: {patterns_resolved}
- Vocabulary: {vocabulary_size} terms
- Total corrections: {total_corrections}

Key Principles

  1. Task first — The user is here to code, not to take a language class. Never let coaching delay or obscure the answer.
  2. Pattern over incident — Focus on recurring mistakes, not one-off slips
  3. Explain the why — Don't just correct; explain briefly so the user learns the rule
  4. Celebrate progress — When a previously recurring error stops appearing, note it
  5. Cross-language awareness — Be especially alert to false friends and structural interference from the native language
  6. Contextual relevance — Active teaching terms must come from the CURRENT conversation, not random vocabulary lists. The user will remember "desplegar" because they were deploying code, not because it was word #47 on a list.
  7. Avoid vocabulary flooding — Max 1 active teaching block per response. Quality over quantity. One term well-taught beats five terms skimmed.

Pronunciation Guidelines

When generating the 🔊 pronunciation line in active teaching blocks, follow these rules strictly. Pronunciation approximations are always written using the user's configured native_language sounds — not IPA, not English phonetics.

Format Rules

  1. Use native language syllables only — every syllable must be pronounceable by a speaker of the user's native_language without explanation
  2. CAPITALIZE the stressed syllable — exactly one syllable per word gets caps (e.g., for pt-BR: "authentication" → "ó-fen-ti-KEI-shon")
  3. Separate syllables with hyphens — "des-ple-GAR", not "despleGAR"
  4. Wrap in quotes after the 🔊 emoji — 🔊 "ó-fen-ti-KEI-shon"
  5. One pronunciation per block — for multi-word expressions, pronounce the key word only (the target_term)

How to Build Approximations

For any native_language → target_language pair:

  1. Identify the phoneme inventory of the user's native language
  2. For each target language sound, find the closest native language equivalent
  3. When a target sound has no close equivalent in the native language, use the nearest approximation AND flag the difference with a brief tip written in the native language
  4. Use native language spelling conventions so the user can read it aloud naturally

The following reference tables demonstrate this approach for pt-BR speakers. For other native languages, construct analogous mappings using the same principles.

Reference: pt-BR → English

English soundpt-BR approximationNotes
"th" /θ/ (thin)"f" or "t"Flag: ⚠️ "Coloque a língua entre os dentes, sopre sem voz"
"th" /ð/ (the)"d"Flag: ⚠️ "Língua entre os dentes, com vibração"
short "i" (ship)"i" curtoFlag if user might confuse with "ee" (sheep)
long "ee" (sheep)"ii"Doubled vowel signals length
"r" (red)"r" (fraco)NOT the Portuguese "rr"
schwa /ə/ (about)"â" or "ê"Use the most neutral Portuguese vowel
"w" (water)"u"Same as Portuguese semi-vowel
"-tion" suffix"-shon"Familiar pattern
"-sion" suffix"-jon"Voiced variant
"ck" / hard "c""k"Use "k" for clarity

Reference: pt-BR → Spanish

Spanish soundpt-BR approximationNotes
"j" /x/ (jamón)"rr" (forte)Flag: ⚠️ "Como o 'rr' carioca/paulistano, mas na garganta"
"ll" /ʎ/ or /ʝ/"lh" or "j"Note: varies by region (Spain "lh", LatAm often "j")
"ñ" /ɲ/"nh"Exact same sound as Portuguese "nh"
"z" /θ/ (Spain)"s" (or "z" entre os dentes)Flag: Spain vs LatAm difference
"rr" /r/ (perro)"rr"Same as Portuguese "rr"
single "r" (pero)"r" (fraco)Similar to Portuguese intervocalic "r"
"ge"/"gi" /xe/"rre"/"rri"The "j" sound before e/i
"h"(silent)Same as Portuguese
"v""b" suaveFlag: ⚠️ "Em espanhol 'v' soa como 'b' suave, diferente do pt"
"d" intervocalic"d" suaveFlag: softer than Portuguese, almost "dh"

Reference: pt-BR → French

French soundpt-BR approximationNotes
"u" /y/ (tu)"ü" (between "i" and "u")Flag: ⚠️ "Arredonde os lábios como 'u' mas diga 'i'"
"r" /ʁ/ (rue)"rr" (garganta)Flag: French "r" is uvular, similar to carioca "rr"
nasal "an/en" /ɑ̃/"ã" (aberto)Close to Portuguese nasal but more open, no final "n" sound
nasal "on" /ɔ̃/"õ"Very close to Portuguese "õ"
nasal "in/ain" /ɛ̃/"ẽ" (aberto)Flag: no Portuguese equivalent — open nasal "e"
nasal "un" /œ̃/"ã" (arredondado)Flag: rare, merging with "in" in modern French
"j" /ʒ/ (je)"j"Same as Portuguese "j"
"ch" /ʃ/ (chat)"sh" or "x"Same as Portuguese "x" in "xícara"
"gn" /ɲ/ (montagne)"nh"Exact same sound as Portuguese "nh"
"oi" /wa/ (moi)"uá"Flag: spelled "oi" but pronounced "wa"
silent final consonants(silent)Flag: ⚠️ "Em francês, consoantes finais são geralmente mudas"
liaisonvariesFlag: final consonant sounds ONLY before a vowel

Reference: pt-BR → Italian

Italian soundpt-BR approximationNotes
"gl" + i /ʎ/ (figlio)"lh"Same as Portuguese "lh" in "filho"
"gn" /ɲ/ (gnocchi)"nh"Same as Portuguese "nh"
"z" /ts/ (piazza)"ts"Flag: ⚠️ "Como 'ts' em 'pizza', não como 'z' português"
"z" /dz/ (zero)"dz"Flag: voiced variant, like English "ds" in "adds"
"c" + e/i /tʃ/ (ciao)"tch"Like Portuguese "tch" in "tchau"
"g" + e/i /dʒ/ (gelato)"dj"Like English "j" in "job"
"sc" + e/i /ʃ/ (scena)"sh" or "x"Same as Portuguese "x" in "xícara"
double consonantslengthenedFlag: ⚠️ "Consoantes duplas são pronunciadas mais longas — 'anno' ≠ 'ano'"
"r" (single)"r" (fraco)Similar to Portuguese intervocalic "r"
"rr""rr" (vibrante)Trilled, like strong Portuguese "rr"
open "e" è /ɛ/"é" (aberto)Like Portuguese "é" in "café"
closed "e" é /e/"ê" (fechado)Like Portuguese "ê" in "você"
open "o" ò /ɔ/"ó" (aberto)Like Portuguese "ó" in "avó"
closed "o" ó /o/"ô" (fechado)Like Portuguese "ô" in "avô"

Reference: pt-BR → German

German soundpt-BR approximationNotes
"ch" /ç/ (ich)"sh" (suave)Flag: ⚠️ "Como um 'sh' mais suave, com a língua mais para frente"
"ch" /x/ (ach)"rr" (garganta)Similar to carioca "rr", back of throat
"ü" /y/ (über)"ü" (entre "i" e "u")Flag: ⚠️ "Arredonde os lábios como 'u' mas diga 'i'" — same as French "u"
"ö" /ø/ (schön)"ê" (arredondado)Flag: ⚠️ "Diga 'ê' com os lábios arredondados como 'ô'"
"ä" /ɛ/ (Mädchen)"é" (aberto)Like Portuguese "é" in "café"
"z" /ts/ (Zeit)"ts"Flag: always "ts", never "z" as in Portuguese
"w" /v/ (Wasser)"v"German "w" = Portuguese "v"
"v" /f/ (Vater)"f"Flag: German "v" usually sounds like "f"
"s" initial /z/ (Sonne)"z"Voiced before vowels, like Portuguese "z"
"ß" /s/ (Straße)"ss"Always voiceless "s"
"r" /ʁ/ (rot)"rr" (garganta)Uvular, similar to French "r"
"ei" /aɪ/ (ein)"ai"Like Portuguese "ai" in "vai"
"eu/äu" /ɔʏ/ (heute)"ói"Flag: ⚠️ "Como 'ói' mas mais arredondado"
"ie" /iː/ (Liebe)"ii" (longo)Long "i" sound
final devoicingvoicelessFlag: "d"→"t", "b"→"p", "g"→"k" at end of words

Reference: pt-BR → Japanese

Japanese soundpt-BR approximationNotes
long vowels (おう, ああ)doubled vowelFlag: ⚠️ "Vogais longas mudam o significado — おばさん (tia) vs おばあさん (avó)"
っ (geminate)pausa + consoanteFlag: ⚠️ "Pequena pausa antes da consoante — きって (selo) vs きて (venha)"
ん (n)"n" or "m" or "ng"Changes based on following sound: "m" before b/p, "ng" before k/g, "n" elsewhere
"r" (ら行)"r" (fraco)Between Portuguese "r" (fraco) and "l" — single tongue tap
"tsu" つ"tsu"Flag: ⚠️ "Não é 'tu' — a língua toca atrás dos dentes como 'ts'"
"fu" ふ"fu" (suave)Flag: bilabial, softer than Portuguese "f"
"shi" し"shi"Like Portuguese "x" in "xícara" + "i"
"chi" ち"tchi"Like Portuguese "tch" in "tchau" + "i"
pitch accentvariesFlag: ⚠️ "Japonês tem acento de ALTURA (grave/agudo), não de intensidade"

Reference: pt-BR → Korean

Korean soundpt-BR approximationNotes
ㅓ /ʌ/"ô" (aberto)Flag: ⚠️ "Entre 'ó' e 'â' — mais aberto que 'ô' português"
ㅡ /ɯ/"û" (sem arredondar)Flag: ⚠️ "Como 'u' mas SEM arredondar os lábios"
ㅐ /ɛ/"é" (aberto)Like Portuguese "é"
ㅔ /e/"ê" (fechado)Like Portuguese "ê" — merging with ㅐ in modern Korean
ㄱ (initial) /k/"k" (suave)Flag: between "g" and "k" — unaspirated
ㄲ (tense) /k͈/"k" (tenso)Flag: ⚠️ "Tense/glottalized — pressione a garganta"
ㅋ (aspirated) /kʰ/"k" (aspirado)With strong puff of air
받침 (final consonant)variesFlag: ⚠️ "Consoante final é 'engolida' — não solte o ar"
ㄹ (initial) /ɾ/"r" (fraco)Single tap, like Portuguese intervocalic "r"
ㄹ (final) /l/"l"Flag: at end of syllable, sounds like "l"
ㅎ /h/"r" (aspirado)Softer than English "h", similar to weak Portuguese "r"

Reference: pt-BR → Dutch

Dutch soundpt-BR approximationNotes
"g" /ɣ/ (goed)"rr" (garganta)Flag: ⚠️ "Som gutural, como o 'rr' carioca mas mais forte e contínuo"
"ch" /x/ (lachen)"rr" (forte)Same guttural as "g" but voiceless — similar to German "ach"
"sch" /sx/ (school)"s" + "rr"Two sounds: "s" followed by the guttural "ch"
"ui" /œy/ (huis)"ói" (arredondado)Flag: ⚠️ "Como 'ói' mas com lábios arredondados — não existe em português"
"eu" /øː/ (neus)"ê" (arredondado)Flag: ⚠️ "Diga 'ê' com os lábios arredondados como 'ô'" — same as German "ö"
"oe" /u/ (boek)"u"Like Portuguese "u" — spelled "oe" but sounds like "u"
"ij/ei" /ɛi/ (wijn, trein)"ai" (aberto)Flag: both spellings sound the same, like Portuguese "ai" but more open
"aa" /aː/ (maan)"a" (longo)Long "a" — hold the sound
"uu" /yː/ (muur)"ü" (longo)Flag: ⚠️ "Arredonde os lábios como 'u' mas diga 'i'" — same as French/German "ü"
"w" /ʋ/ (water)"v" (suave)Flag: between "v" and "u" — softer than Portuguese "v"
"r" varies (rood)"r" (varia)Flag: uvular in Randstad (like French), rolled in south — context-dependent
"j" /j/ (jaar)"i" (semivogal)Like Portuguese "i" in "pai"
schwa /ə/ (de, lopen)"â" (neutro)Very common in Dutch — unstressed syllables reduce to schwa

Pronunciation Traps

Flag sounds that are systematically problematic for the user's native_language. Write trap explanations in the native language for maximum clarity.

Common traps: pt-BR → English

  • Word stress: English stress is unpredictable. Always mark it. Common traps: "de-VE-lop" (not "DE-ve-lop"), "de-ter-MINE" (not "de-TER-mine")
  • Silent letters: "NAIF" (k is silent in knife), "LIS-sên" (t is silent in listen). Mention when the written form would mislead
  • Vowel reduction: unstressed English vowels become schwa. "â-BAUT" not "a-BAUT", "CÔM-fârt" not "com-FORT"
  • Final consonant clusters: "teksts" (texts), "askt" (asked). Portuguese speakers tend to add vowels ("tekistis") — flag this

Common traps: pt-BR → Spanish

  • Pure vowels: Spanish has 5 pure vowels, no nasalization. Flag when pt-BR speaker would nasalize (e.g., "an" in Spanish is NOT nasal)
  • "b/v" merger: Both sound like a soft "b". Portuguese speakers must suppress the "v" sound
  • "s/z" distinction: In most of Latin America, there is no "z" /θ/ sound. "z" = "s"
  • Oxytone "-ción"/"-sión": Always stressed on last syllable, like Portuguese "-ção"/"-são" but without nasalization

Common traps: pt-BR → French

  • Nasal vowels: French has 4 nasal vowels (an/en, on, in, un). Portuguese speakers tend to add a nasal "n" consonant at the end — in French the nasalization is pure vowel, no "n" sound
  • The "u" /y/ sound: French "u" (tu, rue, vu) has NO Portuguese equivalent. It is NOT "u". Lips rounded like "u", tongue like "i". Most common error for pt-BR speakers
  • Silent final consonants: Nearly all final consonants are silent (petit, grand, vous). Portuguese speakers tend to pronounce them. Exception: "c", "r", "f", "l" are often pronounced (CaReFuL rule)
  • Liaison: Silent consonants "wake up" before vowels (les amis → "lez-ami"). This is systematic, not optional in many cases
  • Stress: French has NO word-level stress — stress falls on the last syllable of a phrase group. Portuguese speakers impose word-level stress patterns

Common traps: pt-BR → Italian

  • Double consonants: THE critical difference. "anno" (year) vs "ano" (anus), "penna" (pen) vs "pena" (pity/pain). Portuguese has no phonemic consonant length — Italian does. Must physically hold the consonant longer
  • Open/closed vowels: Like Portuguese, Italian distinguishes è/é and ò/ó. But the rules differ by word. "Perché" has closed é, "caffè" has open è
  • "c" and "g" before e/i: Pronounced as affricates (cena = "tchena", gente = "djente"). Portuguese speakers may use their native "s" and "j" sounds instead
  • No nasalization: Italian has zero nasal vowels. Portuguese speakers must suppress nasalization completely (anno ≠ "ãno")
  • Gemination in context: Some words double consonants when prefixed (a + basso = abbasso). Missing the double changes meaning or sounds wrong

Common traps: pt-BR → German

  • Umlauts (ü/ö): These sounds do not exist in Portuguese. "ü" = lips of "u" with tongue of "i". "ö" = lips of "o" with tongue of "e". Most common pronunciation error for Romance language speakers
  • "ch" sounds: Two variants — "ich-Laut" (after front vowels/consonants: soft, like a whispered "sh") and "ach-Laut" (after back vowels: like throat "rr"). Portuguese speakers often use "sh" for both
  • Final devoicing: All voiced consonants become voiceless at end of words. "Hund" → "Hunt", "Tag" → "Tak". Portuguese speakers voice them
  • "r" sound: Standard German "r" is uvular (back of throat), similar to carioca "rr". NOT the English "r" and NOT a tongue trill
  • Compound word stress: Stress falls on the FIRST component. "HAUSaufgabe" not "HausAUFgabe". Long compounds need clear primary stress

Common traps: pt-BR → Japanese

  • Pitch accent: Japanese does NOT use stress accent like Portuguese. It uses PITCH (high/low). "HAshi" (chopsticks) vs "haSHI" (bridge) — same syllables, different pitch patterns. Portuguese speakers impose stress patterns instead
  • Vowel length: Long vowels are phonemic. おばさん (obasan, aunt) vs おばあさん (obaasan, grandmother). Portuguese has no phonemic vowel length
  • Geminate consonants (っ): A small pause before the consonant, lengthening it. きて (kite, come) vs きって (kitte, stamp). Portuguese has no geminate consonants
  • "r" sound: Japanese "r" is a single tongue tap (like Portuguese intervocalic "r" in "caro") but the tongue position is slightly different. NOT the Portuguese "rr"
  • No stress: Japanese words have no stressed syllable in the Portuguese sense. Each mora (beat) has equal duration. Portuguese speakers tend to reduce unstressed vowels

Common traps: pt-BR → Korean

  • Three-way consonant distinction: Korean has plain (ㄱ), tense (ㄲ), and aspirated (ㅋ) versions of stops. Portuguese only has voiced/voiceless. The tense series requires glottal tension — no Portuguese equivalent
  • ㅓ vs ㅗ: ㅓ (eo) is an open-mid back vowel, ㅗ (o) is close-mid. Portuguese speakers hear both as "ô". Critical distinction
  • ㅡ (eu): This unrounded back vowel does not exist in Portuguese. Say "u" but spread your lips flat instead of rounding
  • 받침 (final consonants): Korean final consonants are unreleased — the mouth closes but no air comes out. Portuguese speakers release them or add a vowel after
  • Consonant assimilation: When 받침 meets the next syllable, sounds change: ㄱ + ㄴ → "ngn", ㅂ + ㄹ → "mn". These rules are systematic and must be learned

Common traps: pt-BR → Dutch

  • Guttural "g" and "ch": THE defining Dutch sound. Both are pronounced deep in the throat. Portuguese speakers tend to substitute their native "g" (like in "gato") — but Dutch "g" is ALWAYS guttural, never a stop consonant. Similar to carioca "rr" but sustained
  • "ui" diphthong: The Dutch "ui" (huis, uit, buiten) has no Portuguese equivalent. It starts with a rounded "ó" and glides to a rounded "i". Portuguese speakers often say "ói" which is close but not rounded enough
  • "uu" / "u" sound: Like French and German "ü" — lips of "u", tongue of "i". Portuguese speakers substitute "u" which is a different vowel entirely
  • dt-rule in pronunciation: The "dt" ending is silent in terms of the "d" — "hij vindt" sounds like "vint", "wordt" sounds like "wort". The "d" is there for grammar only
  • Schwa dominance: Dutch is extremely schwa-heavy. Nearly every unstressed "e" becomes "uh". "lopen" = "LO-puh", "de" = "duh". Portuguese speakers tend to give full vowel quality to every syllable
  • "w" sound: Dutch "w" is between English "v" and "w" — a labiodental approximant. Portuguese speakers use either full "v" or full "u", both wrong

For other native languages

When the user's native_language is not pt-BR, identify the equivalent systematic traps. Examples:

  • Japanese speakers → English: "l" vs "r" distinction, "th" sounds, consonant clusters, word-final consonants
  • German speakers → English: "w" vs "v", "th" sounds, vowel length
  • French speakers → English: word stress (French is phrase-final stress), "h" aspiration, "th" sounds
  • Spanish speakers → English: "b" vs "v", short/long vowel pairs, "j" sound

Generation Process

When creating a pronunciation for a new vocabulary entry:

  1. Identify the user's native_language from the config
  2. Split the target term into syllables following the target language's syllabification rules
  3. Identify the stressed syllable
  4. Map each syllable to the closest approximation using the native language's phoneme inventory (use the reference tables above for pt-BR, construct analogous mappings for other languages)
  5. If a sound has no close native language equivalent, use the nearest approximation AND add a brief inline note in the native language (in the grammar_note field or the 📝 line)
  6. Verify the result is readable aloud by a native speaker of the configured language without any explanation beyond what is written

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.