The es pair, and a dictionary that was quietly Spain's

Phase 25. Spanish was never built — the groundwork was all [x] (DreamDict
data, the prompt language, the L1 rule gating, TTS env-discovery), which is
why the plan read as though it had shipped. shippedPairs was the honest
answer: the server had been refusing es on purpose.

The langpack is neutral Latin American, chosen with the user: tú, ustedes,
no vosotros, and the pan-American half of every vocabulary split. A vitest
greps for the peninsular twins the way fr is greped for québécismes —
including coger, which is not merely regional but obscene through most of
Latin America.

The dictionary is the story. Debian's hunspell-es symlinks twenty country
codes to one file, which reads as pan-Hispanic; RLA publishes twenty-four
builds per release, one per country plus a generic es that is the union,
and Debian ships peninsular es_ES. The 58,622-form gap is essentially
voseo, so the first version of this commit underlined vení and tenés as
misspellings and called it a considered gap.

The MUST_ACCEPT list was written to catch exactly that and structurally
could not: it asserted the pan-Hispanic vocabulary, and every RLA variant
carries the full pan-Hispanic vocabulary — only the paradigms are
localised. The REP table cited as the second witness is shared by all
builds too. Two independent-looking proofs, neither able to distinguish
anything, agreeing with each other.

The profile now demands what discriminates, each verified against the build
it targets: voseo rejects es_ES and Debian, vosotros rejects es_MX, and
arepa/chévere/bacán reject es_AR, which has both paradigms and would
otherwise pass. 717,640 forms, 1.74 MB gzipped, 762 ms / 97 MB in a real
nspell. fr and pt-PT rebuild byte-identical from their own upstream debs,
so the shared script still means what it meant.

Shipping the union is fr's call arrived at from the other side: coût and
cout are both correct French, tienes and tenés are both correct Spanish.
The dictionary holds every variety because underlining is all it can do;
the copy picks a register because speaking requires one.

Reviewed by four models at the usual >=2-of-4 threshold, 5 of 27 findings
applied — one catching the bedtime proverb as fr's Qui dort dîne calqued
into Spanish, gloss and all, which is the rule the fr header states. One
below-threshold finding (a missing ¡, seen by 1 of 4 because an absent
opening mark has no closing ! to look wrong against) was applied and turned
into an assertion instead: the suite now rejects any native line that
closes ? or ! without opening one.

piper-es on es_MX-ald-medium, not the es_ES-davefx-medium the plan named —
six of Piper's nine Spanish voices are peninsular, so the obvious pick was
the pt-PT trap through a different door.

go build/vet/test, tsc, vite, vitest 251/251.

Not deployed, not seen in a browser, not read by a native speaker, and no
es account exists.
This commit is contained in:
prosolis
2026-07-28 18:25:59 -07:00
parent 39d4e4770a
commit 9224c44fff
14 changed files with 972 additions and 18 deletions
+68
View File
@@ -0,0 +1,68 @@
Spanish spelling dictionary
===========================
The word list in `es.dic.gz` and the suggestion directives in `es.aff` are
derived from the **generic** Spanish Hunspell dictionary published by the RLA-ES
project ("Recursos Lingüísticos Abiertos del Español"), release v2.9.
Copyright (C) Santiago Bosio and the RLA-ES contributors
License: GPL-3+ or LGPL-3+ or MPL-1.1+
Tri-licensed; you may choose freely among the three. Petal
redistributes under the MPL. Full texts:
https://www.gnu.org/licenses/gpl-3.0.en.html
https://www.gnu.org/licenses/lgpl-3.0.en.html
https://www.mozilla.org/en-US/MPL/1.1/
Upstream: https://github.com/sbosio/rla-es
Source: https://github.com/sbosio/rla-es/releases/download/v2.9/es.oxt
(an .oxt is a zip; es.aff and es.dic are at its root)
Not the Debian package, and that is the point
---------------------------------------------
`hunspell-es` looks like the obvious source and is the wrong one. It installs
twenty country codes — `es_AR` through `es_VE` — all symlinked to a single file,
which reads like "one pan-Hispanic dictionary". It is not. RLA publishes
twenty-four dictionaries per release: one per country, plus a **generic `es`**
that is the union of all of them, and Debian ships the **peninsular `es_ES`**
build under the collapsed name.
Measured against v2.9, expanded to surface forms:
Debian hunspell-es 659,085 forms voseo: no vosotros: yes
upstream es_ES 659,018 forms voseo: no vosotros: yes
upstream es_MX 554,923 forms voseo: no vosotros: no
upstream es_AR 669,605 forms voseo: yes vosotros: yes
upstream es (generic) 717,640 forms voseo: yes vosotros: yes <-- this
The 58,622-form gap between Debian's file and the generic one is essentially the
**voseo** paradigm — `vení`, `tenés`, `querés`, `sabés`, `andá` — the ordinary
present tense of Argentina, Uruguay, Paraguay and much of Central America. Under
the Debian package, a writer using it would have had her own verbs underlined as
misspellings.
Why the generic build rather than one country
---------------------------------------------
The only thing this dictionary can do is underline something. *Tienes* and
*tenés* are both correct Spanish, taught in different countries, and a writing
companion has no business marking one of them wrong — the same reasoning that
makes the French dictionary here the `-comprehensive` packaging rather than
`-classical` or `-revised`. The generic build accepts every variety, so Petal
underlines only what no Spanish speaker anywhere would write.
How the build proves it got this file
-------------------------------------
Vocabulary cannot tell these builds apart: *every* RLA variant carries the full
pan-Hispanic lexicon, so *computadora* alongside *ordenador* passes on the
peninsular file too. (The `REP` table is likewise no evidence — its `ll`/`y` and
`ás`/`az` entries look like yeísmo and seseo but are shared by all builds.) Only
the verb paradigms are localised, so the `es` profile in
`scripts/build_hunspell_dictionary.py` demands, all at once:
* **voseo** (`vení`, `tenés`, `querés`) — rejects `es_ES` and Debian's package;
* **vosotros** (`tenéis`, `escribid`) — rejects `es_MX`;
* **another region's everyday words** (`arepa`, `chévere`, `bacán`) — rejects
`es_AR`, which has both paradigms and would otherwise pass.
Only the generic build satisfies all three. Each of the four neighbouring builds
was run through the profile and confirmed to fail.
+29
View File
@@ -0,0 +1,29 @@
SET UTF-8
TRY aeroinsctldumpbgfvhzóíjáqéñxyúükwAEROINSCTLDUMPBGFVHZÓÍJÁQÉÑXYÚÜKW
REP 19
REP ás az
REP az ás
REP cc x
REP és ez
REP ez és
REP güe hue
REP güi hui
REP hue güe
REP hui güi
REP ís iz
REP ío ido
REP ke que
REP ki qui
REP ll y
REP mb nv
REP nv mb
REP seci cesi
REP x cc
REP y ll
MAP 6
MAP aáAÁ
MAP eéEÉ
MAP iíIÍ
MAP oóOÓ
MAP uúüUÚÜ
MAP nñNÑ
Binary file not shown.
+17
View File
@@ -98,6 +98,23 @@ const PAIR_DICTS: Partial<Record<PairLang, DictSpec>> = {
extendedAlphabet: true,
elision: FR_ELISION,
},
// Spanish glues its pronouns onto the *end* of a verb rather than little words
// onto the front, so there is no elision list here — and none is needed: the
// upstream dictionary carries the enclitic forms itself (dámelo, hacérselo,
// escribiéndolo), because unlike French's thirty-four prefix rules they do not
// multiply the word list into the megabytes.
//
// This is RLA's *generic* build, not Debian's hunspell-es — the latter is the
// peninsular one under a pan-Hispanic-looking name, and it rejects vení and
// tenés. See web/public/dictionaries/es/LICENSE: the whole of Spanish is
// accepted here, because underlining is the only thing this file can do.
es: {
lang: 'es',
aff: '/dictionaries/es/es.aff',
dic: '/dictionaries/es/es.dic.gz',
gzipped: true,
extendedAlphabet: true,
},
}
// Where the list lived before it had an owner (Phase 7). Read once, handed to
+114 -5
View File
@@ -4,12 +4,13 @@ import { onPackChange, pack, resetPackForTests, setPackLang, shippedPacks } from
import { zh } from './packs/zh'
import { ptPT } from './packs/pt-PT'
import { fr } from './packs/fr'
import { es } from './packs/es'
import type { Pack } from './types'
// Every pack that ships. Shape assertions run over all of them, because the
// point of Phase 19 was that a language is data — and data that only the first
// author's pack satisfies isn't a shape, it's a coincidence.
const PACKS: Pack[] = [zh, ptPT, fr]
const PACKS: Pack[] = [zh, ptPT, fr, es]
// Every string a pack would ever put on screen, and nothing else — field names
// excluded (see the pt-PT grep below for what including them cost). Templates are
@@ -49,8 +50,10 @@ describe('pack selection', () => {
it('falls back rather than blanking on a pair with no pack yet', () => {
// A pair_lang the deployment has no copy for is a deployment that got ahead
// of its translation. She should still get a working editor. (This was 'fr'
// until Phase 24 gave fr a pack; 'es' is the pair still waiting for one.)
setPackLang('es')
// until Phase 24, then 'es' until Phase 25 — every pair PairLang names now
// has a pack, so the stand-in is a regional code Petal has not decided
// about, which is the realistic version of this failure anyway.)
setPackLang('es-ES')
expect(pack()).toBe(zh)
setPackLang('klingon')
expect(pack()).toBe(zh)
@@ -70,7 +73,7 @@ describe('pack selection', () => {
expect(seen).not.toHaveBeenCalled()
// An unshipped pair resolves back to zh, which is also not a change.
setPackLang('es')
setPackLang('es-ES')
expect(seen).not.toHaveBeenCalled()
setPackLang('pt-PT')
@@ -83,7 +86,7 @@ describe('pack selection', () => {
// matching allowlist exists to enforce from the other side.
it('offers exactly the pairs it has copy for', () => {
const codes = shippedPacks().map((p) => p.code)
expect(codes.sort()).toEqual(['fr', 'pt-PT', 'zh'])
expect(codes.sort()).toEqual(['es', 'fr', 'pt-PT', 'zh'])
// Every offered pair names itself, because a writer stranded on the wrong
// pack can only read the label that is in her own language.
for (const p of shippedPacks()) expect(p.nativeName.length).toBeGreaterThan(0)
@@ -364,6 +367,97 @@ describe('the fr pack', () => {
})
})
describe('the es pack', () => {
// Spanish has no regional question in its dictionary at all — hunspell-es
// ships twenty country codes and every one is a symlink to one pan-Hispanic
// word list — so, even more than with French, the entire regional decision
// lives in this file. Neutral Latin American was chosen deliberately, and a
// stray peninsular form is invisible to everyone reviewing the diff.
it('is Latin American, not peninsular', () => {
const text = copyOf(es).toLowerCase()
for (const bad of [
'ordenador', 'vosotros', 'zumo', 'patata', 'coche',
'gafas', 'billete', 'chaval', 'guay',
// The one that is not merely regional: *coger* is an everyday verb in
// Spain and obscene through most of Latin America. A companion in a
// private notebook must never produce it by accident.
'coger',
]) {
expect(text, `peninsular form "${bad}" in the es pack`).not.toMatch(
new RegExp(`\\b${bad}\\b`),
)
}
// And the accent it is read aloud in. Six of Piper's nine Spanish voices
// are es_ES, so the wrong country is the easy default here — the pt-PT
// trap, not the fr non-question.
expect(es.locale).toBe('es-MX')
})
// Spanish opens its questions and exclamations, and the easiest place to
// forget is exactly where a reviewer's eye slides past: inside a Line's
// native half, and inside an interpolated template. Every opening mark in the
// pack is deliberate; a missing one is a typo the type system cannot see.
it('opens every question and exclamation it closes', () => {
const lines: { native: string; where: string }[] = []
const walk = (node: unknown, path: string) => {
if (typeof node === 'function') return walk((node as (...a: unknown[]) => unknown)(1, 'x'), path)
if (!node || typeof node !== 'object') return
const rec = node as Record<string, unknown>
// A Line is the one shape whose `native` is pure Spanish — the flat
// "Spanish · English" strings carry English punctuation too, so they are
// asserted by hand below rather than by rule.
if (typeof rec.native === 'string' && typeof rec.en === 'string') {
lines.push({ native: rec.native, where: path })
return
}
for (const [k, v] of Object.entries(rec)) walk(v, path ? `${path}.${k}` : k)
}
walk(es, '')
expect(lines.length).toBeGreaterThan(40)
for (const { native, where } of lines) {
if (native.includes('?')) expect(native, `${where} closes ? without ¿`).toContain('¿')
if (native.includes('!')) expect(native, `${where} closes ! without ¡`).toContain('¡')
}
// An exclamative opening with Qué/Cómo/Cuánto is the case that slips past a
// reader, because it carries no closing "!" to look wrong against — the
// whole pair is simply absent. The quorum review caught exactly one of
// these ("Qué linda elección de palabra"), and only one reviewer of four
// saw it, which is the argument for asserting it instead of re-reviewing it.
for (const { native, where } of lines) {
expect(native, `${where} opens an exclamative without ¡`).not.toMatch(
/^(Qué|Cómo|Cuánto|Cuánta)\b/,
)
}
// And the two-language strings, by hand: both halves punctuate their own way.
expect(es.garden.promptRecognition).toBe('¿Qué significa? · What does this mean?')
expect(es.garden.promptProduction).toContain('¿Cuál es la palabra en inglés?')
})
it('renders its interpolated lines with the value in place', () => {
expect(es.app.duplicateTitle('Primavera')).toBe('Primavera (copia)')
expect(es.companion.milestone(300).native).toContain('300 palabras')
// Spanish agreement is the pack's business; the call site only ever passes
// a number. `flor`/`flores` is the irregular one — it takes -es, not -s.
expect(es.garden.reviewDue(1)).toContain('1 palabra ·')
expect(es.garden.reviewDue(4)).toContain('4 palabras ·')
expect(es.garden.growing(1)).toContain('1 flor en el jardín')
expect(es.garden.growing(3)).toContain('3 flores en el jardín')
expect(es.journal.kept(1)).toContain('1 cosa que te llevaste')
expect(es.journal.kept(5)).toContain('5 cosas que te llevaste')
expect(es.status.petalsToPolish(1).native).toContain('1 pétalo por pulir')
expect(es.status.petalsToPolish(2).native).toContain('2 pétalos por pulir')
})
it('says the collision line, which this pair meets constantly', () => {
// real, red, once, pie, sin, pan, mayor, sale, ropa — Spanish and English
// collide about as often as French and English do.
expect(es.editor.alsoIn).toBeTruthy()
expect(es.editor.alsoIn).not.toBe(zh.editor.alsoIn)
expect(es.editor.alsoIn).not.toBe(fr.editor.alsoIn)
expect(es.editor.alsoIn).not.toBe(ptPT.editor.alsoIn)
})
})
// False friends are a per-pair dataset rather than copy: the Latin pairs carry
// the traps their writers actually fall into, and the zh pair legitimately has
// none. Both halves of that are worth pinning.
@@ -391,6 +485,21 @@ describe('false friends', () => {
expect(Object.keys(fr.falseFriends).length).toBeGreaterThan(10)
})
it('the es pair carries the ones that cost most', () => {
// "embarrassed" is the reason this feature exists at all: *embarazada* is
// "pregnant", and it is the single false friend most likely to be said out
// loud to a room. "molest" is the other one that has to be here, because
// *molestar* is an everyday word and the English is not.
for (const word of ['embarrassed', 'molest', 'actually', 'realize', 'exit', 'carpet']) {
expect(es.falseFriends[word], word).toBeDefined()
}
// Spanish shares more Latin with English than either of the other Latin
// pairs, so this list is the longest of the four and should stay that way.
expect(Object.keys(es.falseFriends).length).toBeGreaterThan(
Object.keys(fr.falseFriends).length,
)
})
it('is keyed by the lowercase English word, so a lookup can find it', () => {
for (const p of PACKS) {
for (const key of Object.keys(p.falseFriends)) {
+6 -3
View File
@@ -18,12 +18,15 @@ import type { Pack, PairLang } from './types'
import { zh } from './packs/zh'
import { ptPT } from './packs/pt-PT'
import { fr } from './packs/fr'
import { es } from './packs/es'
export type { Pack, PairLang, Line } from './types'
// Every pack Petal ships. es is the same two lines when its copy is written —
// TypeScript names every string a new pack still owes.
const PACKS: Partial<Record<PairLang, Pack>> = { zh, 'pt-PT': ptPT, fr }
// Every pack Petal ships — and now every pair PairLang names, so this map is
// no longer Partial by necessity. It stays Partial anyway: the next pair will
// be declared in the type before its copy exists, exactly as es was, and the
// gap between the two is the point.
const PACKS: Partial<Record<PairLang, Pack>> = { zh, 'pt-PT': ptPT, fr, es }
const DEFAULT_LANG: PairLang = 'zh'
+550
View File
@@ -0,0 +1,550 @@
// The Spanish pack — the fourth pair, and the second written straight into the
// groove Phase 24 cut.
//
// ⚠️ REVIEWED BY FOUR MODELS, NOT BY A NATIVE SPEAKER.
// SUGGESTIONS.md §3 sets the bar: a pack should be reviewed by someone who
// speaks the pair before it is trusted. That has still not happened. What has
// happened (2026-07-28) is the same interim pass the fr and pt-PT packs got —
// four models read this file independently as Latin American Spanish speakers,
// and only findings at least two of them reached on their own were applied,
// listed in BUILD_PLAN Phase 25. A quorum of models agreeing is agreement, not
// authority: it can catch a verb form no one says and a register that slips into
// Spain, and it cannot catch a line that is correct and lifeless. Treat this as a
// better-checked draft.
//
// The choices this file makes, and why:
//
// * **Latin American neutral, not peninsular.** Chosen deliberately (user,
// 2026-07-28) over es-ES: it is the Spanish far more people write, and the
// "neutral" register is a real thing that Spanish-language publishing and
// dubbing have spent decades stabilising. So: *tú* for the singular,
// **ustedes** for the plural and no *vosotros* anywhere, and the pan-American
// half of every vocabulary split — *computadora*, *celular*, *carro*, *jugo*,
// *papa*, *departamento*, *lentes*, *boleto*. A vitest greps this file for the
// peninsular twins the way the fr pack is grepped for québécismes, because a
// stray *ordenador* is invisible to everyone reviewing the diff.
// * **The dictionary makes the opposite choice on purpose, and that is not a
// contradiction.** This copy is Latin American; the spelling dictionary
// behind it accepts *every* variety of Spanish, peninsular and voseante
// alike (RLA's generic build — see web/public/dictionaries/es/LICENSE). The
// two answer different questions: the copy is Petal *speaking*, where a
// register has to be chosen, and the dictionary is Petal *listening*, where
// the only available action is to underline something. Choosing a register
// to write in costs a reader nothing; choosing one to accept would tell her
// that her own conjugation is a typo.
// * **Tuteo.** Petal is a companion in someone's private notebook, and *usted*
// would put a desk between them — the same call the pt-PT pack made about
// *tu* over *você* and the fr pack about *tu* over *vous*.
// * **Tuteo here, voseo accepted there.** This copy says *tú*, because neutral
// Latin American is tuteo and something had to be chosen. The dictionary
// nonetheless accepts *vení* and *tenés*, so a Rioplatense writer is never
// told her own present tense is a misspelling — she simply reads a companion
// that speaks a slightly different Spanish than she writes, which is true of
// every Spanish speaker reading anything.
// * **¿Inverted marks, siempre!** Spanish opens questions and exclamations and
// this file does too — including inside interpolated lines, where it is
// easiest to forget. It is also the one punctuation habit that costs her
// nothing in English: unlike the French space before « ! », there is no
// Spanish mark to carry across by accident, so no prose note has to warn
// about it.
// * Quotation marks are the curly “ ” rather than « », which is the American
// convention and the commoner one in Spanish outside Spain.
//
// Spanish first, English underneath — same shape as the other packs, for the same
// reason: she reads her own language faster, and the English half is what she is
// here to learn.
import type { Pack } from '../types'
export const es: Pack = {
code: 'es',
nativeName: 'Español',
// Mexican Spanish is the standard "neutral" broadcast variety and the one
// Piper has a Latin American voice for (es_MX-ald-medium). The peninsular
// es_ES-davefx-medium the plan originally named would have read this copy in
// the accent it was written to avoid.
locale: 'es-MX',
app: {
duplicateTitle: (title) => `${title} (copia)`,
garden: 'Jardín de palabras',
history: 'Historial',
},
auth: {
title: 'Vuelve a entrar',
titleEn: 'Please sign in again',
bodyWithDraft:
'Lo que acabas de escribir está guardado en este dispositivo — vuelve a entrar y se guardará solo.',
bodyWithDraftEn: "What you just wrote is safe on this device — it'll save itself once you're back in.",
bodyPlain: 'Tu sesión expiró. Todo lo que escribiste ya está guardado.',
bodyPlainEn: 'Your session expired. Everything you wrote is already saved.',
signIn: 'Iniciar sesión · Sign in',
},
companion: {
choose: 'Elige un compañero · Choose a companion',
encouragements: [
{ native: '¡Eso! Esa oración fluye mucho mejor 🌸', en: 'Lovely — that reads so much smoother now.' },
{ native: 'Cada vez escribes mejor ✨', en: "You're getting better and better." },
{ native: 'Me gusta mucho ese cambio 💕', en: 'I really like that change.' },
{ native: '¡Sigue así, lo estás logrando!', en: 'Keep going — youve got this!' },
{ native: 'Mmm, así queda mucho más claro 👍', en: 'Mm, thats much clearer.' },
{ native: '¡Qué linda elección de palabra! 🌷', en: 'Thats such a good word choice.' },
{ native: 'Ay, ese párrafo se lee solito ☁️', en: 'Ooh, that paragraph flows so nicely.' },
{ native: 'Me encanta verte escribir con más confianza 💛', en: 'I love watching you write with more confidence.' },
{ native: 'Cada avance cuenta, por chiquito que sea 🌱', en: 'Every little bit of progress counts.' },
{ native: 'Hoy tus palabras están brillando ✨', en: 'Your words are sparkling today.' },
],
tips: [
{ native: 'Consejo: en inglés, las oraciones cortas se leen mejor.', en: 'Tip: shorter English sentences often read clearer.' },
{ native: 'No olvides los artículos “the” y “a”.', en: "Don't forget articles like “the” and “a”." },
{ native: 'Para el pasado, usa el pretérito: go → went.', en: 'For the past, use past tense: go → went.' },
{ native: 'Leer en voz alta ayuda a notar lo que suena raro.', en: 'Reading aloud helps you catch awkward spots.' },
{ native: 'Una idea por párrafo y todo queda clarito.', en: 'One idea per paragraph keeps it tidy.' },
{ native: '¿Tienes una duda? Pregúntame ✨', en: 'Not sure about something? Just ask me. ✨' },
{ native: 'El plural lleva “s”: two apples 🍎', en: 'Plurals take an “s”: two apples 🍎' },
// Two tips the zh pack has no use for. Spanish and English share so much
// Latin vocabulary that the false friends are a daily hazard, and the
// subject pronoun is the habit Spanish speakers drop most often.
{ native: 'Cuidado con los falsos amigos: “actually” no significa *actualmente*.', en: 'Careful with false friends — “actually” means *in fact*.' },
{ native: 'En inglés el sujeto casi nunca se omite: “it is raining”, no “is raining”.', en: 'English almost always needs a subject: “it is raining”, not “is raining”.' },
],
breaks: [
{ native: 'Llevas rato escribiendo — estírate y descansa la vista 🍵', en: "You've been writing a while — stretch and rest your eyes. 🍵" },
{ native: '¿Un vaso de agua y cinco minutos de pausa?', en: 'Sip some water and take five?' },
{ native: 'Mira a lo lejos un momento, la vista te lo agradece 🌿', en: 'Look into the distance for a moment — give your eyes a break. 🌿' },
],
// Late-night nudges. The English wit is the user's own and is kept word for
// word across every pack; the Spanish line leads gently into it, exactly as
// the Mandarin, Portuguese and French ones do.
bedtime: [
{ native: 'Tu cama ha de estar preguntándose dónde andas 🛏️', en: 'I bet your bed is missing you right now.' },
{ native: 'Cansada, escribes mal — ve a descansar 🌙', en: 'A tired writer is a bad writer — get some rest.' },
{ native: 'Mejor consúltalo con la almohada ✨', en: 'Sleep is a wondrous enabler.' },
{ native: '¿Oyes? No… porque todos están dormidos, y tú deberías estarlo también 😴', en: "Hear that? No… you don't, because everyone is sleeping and you should be too." },
// Spanish sayings about sleep and haste, in place of the French and
// Portuguese ones — a pack is not a translation of another pack.
{ native: 'Dormir es el mejor remedio.', en: 'Sleep is the best medicine.' },
{ native: 'A quien madruga, Dios lo ayuda.', en: 'The early riser gets a hand from above.' },
{ native: 'No por mucho madrugar amanece más temprano.', en: 'Rising earlier will not make the sun come up sooner.' },
],
greeting: { native: '¡Hola! Aquí te hago compañía 🐱', en: "Hi! I'm right here keeping you company. 🐱" },
welcomeBack: { native: '¡Volviste! ✨ Sigamos', en: 'Welcome back ✨ lets keep going!' },
errors: [
{ native: 'Uy — un tropiezo chiquito, pero tus palabras están a salvo.', en: 'Oops — a little hiccup, but your words are safe.' },
{ native: 'Ay, me enredé un segundo — ya vuelvo.', en: 'Haiya, I got stuck for a sec — back in a moment.' },
{ native: 'No te preocupes, lo intentamos de nuevo en un ratito 🍵', en: "Don't worry — let's try again in a bit. 🍵" },
],
milestone: (words: number) => ({
native: `¡Guau! Ya llevas ${words} palabras 🎉`,
en: `Wow — ${words} words already! Amazing. 🎉`,
}),
// Algo pequeño que escribir, ofrecido una vez al día a una página en blanco.
// Recuerdos y opiniones, nunca ejercicios: aquí no hay nada que se pueda
// reprobar, y esa es justamente la intención.
invitations: [
{ native: 'Escribe 50 palabras: algo pequeño que te hizo sonreír hoy 🌸', en: 'Write 50 words: one small thing that made you smile today.' },
{ native: 'Escribe 50 palabras: lo más rico que comiste hoy', en: 'Write 50 words: the best thing you ate today.' },
{ native: 'Escribe 50 palabras: lo que ves por tu ventana en este momento', en: 'Write 50 words: what you can see out of your window right now.' },
{ native: 'Escribe 50 palabras: un lugar al que volverías con gusto', en: 'Write 50 words: somewhere you would happily go back to.' },
{ native: 'Escribe 50 palabras: algo que aprendiste esta semana', en: 'Write 50 words: one thing you learned this week.' },
{ native: 'Escribe 50 palabras: un mensaje para ti misma dentro de un año', en: 'Write 50 words: something to tell yourself a year from now.' },
{ native: 'Escribe 50 palabras: una canción que no has parado de escuchar estos días', en: 'Write 50 words: a song you have had on lately.' },
{ native: 'Escribe 50 palabras: alguien a quien te gustaría agradecerle hoy', en: 'Write 50 words: someone you would like to thank today.' },
],
inviteAccept: 'Vamos · Lets write',
inviteDecline: 'Hoy no · Not today',
declined: { native: 'Está bien, me regreso a dormir 😴', en: 'Fair enough — back to my nap. 😴' },
names: {
cat: 'Gato dormilón',
dog: 'Perro alegre',
'wiggle-dog': 'Perro meneacola',
butterfly: 'Mariposa',
parrot: 'Loro',
},
},
prose: {
longSentence: 'Esta oración quedó un poco larga — partirla en dos o tres la hace más clara 🌸',
commaSplice: 'Aquí hay dos oraciones unidas solo por una coma. Pon un punto, o únelas con “and / but”.',
vagueThis: (word) => `No queda claro a qué se refiere “${word}” — precísalo (por ejemplo “${word} idea / change…”).`,
oxfordComma: 'En una lista de tres o más elementos, una coma antes de “and / or” ayuda a leer (la coma de Oxford).',
transitionComma: (word) => `Después de un conector al inicio de la oración va una coma: “${word}, …”.`,
capitalizeSentence: 'Empieza cada oración con mayúscula.',
repeatedWord: (word) => `Parece que “${word}” quedó escrito dos veces — échale un ojo.`,
capitalizeI: 'En inglés, “I” (yo) siempre va con mayúscula.',
spaceBeforePunct: 'En inglés no se deja espacio antes de la puntuación: la coma y el punto van pegados a la palabra, igual que en español.',
spaceAfterPunct: 'Después de una coma o un punto, deja un espacio antes de la siguiente palabra.',
articleAn: (word) => `Antes de un sonido de vocal se usa “an”: “an ${word}”.`,
articleA: (word) => `Antes de un sonido de consonante se usa “a”: “a ${word}”.`,
uncountable: (word, singular) => `${word}” es incontable en inglés — no lleva s, basta con “${singular}”.`,
capitalizeProper: (fixed) => `En inglés, los idiomas, las nacionalidades, los días y los meses van con mayúscula: “${fixed}”.`,
thirdPersonS: (subject, verb) => `Con he/she/it, el verbo lleva -s: “${subject} ${verb}”.`,
pluralAfter: (determiner, noun) => `Después de “${determiner}”, el sustantivo va en plural: “${determiner} ${noun}s”.`,
doubleDeterminer: (first, second) => `${first} ${second}” lleva dos determinantes — deja solo uno (quita “${first}”, por ejemplo).`,
thereArePlural: (noun) => `En plural se dice “there are”: “there are ${noun}…”.`,
itsOwn: '“its” = “it is”. Para decir “su”, es “its” — o sea “its own”.',
itsIs: (rest) => `Aquí va “its ${rest}” (it is); “its” es el posesivo.`,
thanNotThen: (word) => `En una comparación se escribe “than”, no “then”: “${word} than”.`,
preposition: (wrong, right) => `En inglés se dice “${right}”, no “${wrong}” — esa preposición es fija.`,
collocation: (wrong, right) => `En inglés estas palabras van juntas así: “${right}”, y no “${wrong}”.`,
doubleComparative: (lead, word) => `${word}” ya es el comparativo — no necesita “${lead}”: con “${word}” basta.`,
peopleArePlural: (verb) => `“People” es plural en inglés: “people ${verb}”.`,
// Interferencia del español. Las tres primeras son las que de verdad se ven
// todos los días; el resto las tiene el pack aunque este par no las corra.
ageIsNotHave: (years) => `En inglés la edad se dice con *to be*, no con *tener*: “I am ${years} years old”.`,
agreeIsAVerb: '“Agree” ya es el verbo — no lleva *to be* delante: se dice “I agree”, no “I am agree”.',
forNotSince: (duration) => `Para una duración se usa “for”: “for ${duration}”. “Since” marca el punto de partida (since 2020).`,
veryBeforeVerb: (verb) => `“Very” solo acompaña adjetivos, no verbos: “really ${verb}”, o “${verb}… very much”.`,
turnOnNotOpen: (thing, on) => `En inglés los aparatos no se abren, se encienden: “turn ${on ? 'on' : 'off'} the ${thing}”.`,
althoughOrBut: (word) => `En inglés se pone “${word}” o “but”, nunca los dos en la misma oración.`,
},
// Los falsos amigos entre el español y el inglés — la trampa que hace sentir
// ridícula en vez de simplemente corregida. Por eso son solo un aviso: Petal
// nunca cambia la palabra, porque “actually” bien pudo ser la que quería.
//
// El español comparte tanto latín con el inglés que esta lista es la más larga
// de los cuatro packs, y *embarrassed* es la razón por la que existe la función.
falseFriends: {
embarrassed: {
native: '“Embarrassed” significa *apenada, avergonzada*. *Embarazada* se dice “pregnant”.',
en: '“Embarrassed” means ashamed; the Spanish *embarazada* is “pregnant”.',
},
actually: {
native: '“Actually” significa *en realidad*, no *actualmente*. Para *actualmente* se dice “currently” o “nowadays”.',
en: '“Actually” means *in fact*. For the Spanish *actualmente*, English uses “currently”.',
},
eventually: {
native: '“Eventually” significa *al final, tarde o temprano* — no *eventualmente*. Para eso: “possibly” o “if necessary”.',
en: '“Eventually” means *in the end*, not *possibly*.',
},
realize: {
native: '“Realize” significa *darse cuenta*. Para *realizar* (llevar a cabo) se dice “carry out” o “do”.',
en: '“Realize” means to become aware; *realizar* is “to carry out”.',
},
assist: {
native: '“Assist” significa *ayudar*. Para *asistir* (ir a algo) se dice “attend”.',
en: '“Assist” means to help; *asistir a* is “to attend”.',
},
attend: {
native: '“Attend” significa *asistir a*. Para *atender* (a alguien) se dice “serve” o “take care of”.',
en: '“Attend” means to go to something; *atender* is “to serve”.',
},
support: {
native: '“Support” significa *apoyar*. Para *soportar* (aguantar algo molesto) se dice “put up with” o “bear”.',
en: '“Support” means to back someone up; *soportar* is “to put up with”.',
},
sensible: {
native: '“Sensible” significa *sensato*. Para *sensible* se dice “sensitive”.',
en: '“Sensible” means level-headed; the Spanish *sensible* is “sensitive”.',
},
sympathetic: {
native: '“Sympathetic” significa *comprensivo, solidario*. Para *simpático* se dice “nice” o “friendly”.',
en: '“Sympathetic” means understanding; *simpático* is “nice”.',
},
library: {
native: '“Library” es la *biblioteca*. La *librería* se dice “bookshop” o “bookstore”.',
en: '“Library” is where books are lent; a shop that sells them is a “bookstore”.',
},
exit: {
native: '“Exit” es la *salida*. El *éxito* se dice “success”.',
en: '“Exit” is the way out; *éxito* is “success”.',
},
success: {
native: '“Success” es el *éxito*. Un *suceso* se dice “event”.',
en: '“Success” is achievement; a Spanish *suceso* is an “event”.',
},
carpet: {
native: '“Carpet” es la *alfombra*. Una *carpeta* se dice “folder”.',
en: '“Carpet” covers a floor; a *carpeta* is a “folder”.',
},
discussion: {
native: '“Discussion” es una *conversación*, sin pelea. Una *discusión* (riña) se dice “argument”.',
en: '“Discussion” is calm; a Spanish *discusión* is an “argument”.',
},
argument: {
native: '“Argument” es una *discusión* o *pelea*. El *argumento* de una historia se dice “plot”.',
en: '“Argument” is a quarrel; the *argumento* of a story is its “plot”.',
},
introduce: {
native: '“Introduce” es *presentar* a alguien. Para *introducir* (meter) se dice “insert” o “put in”.',
en: '“Introduce” is to present someone; *introducir* is “to insert”.',
},
molest: {
native: '“Molest” significa *abusar sexualmente* — nunca se usa por *molestar*. Para eso: “bother” o “annoy”.',
en: '“Molest” means to abuse; the everyday *molestar* is “to bother”.',
},
constipated: {
native: '“Constipated” significa *estreñida*. Para *constipada* (resfriada) se dice “to have a cold”.',
en: '“Constipated” is a bowel problem; *constipado* is “a cold”.',
},
large: {
native: '“Large” significa *grande*. Para *largo* se dice “long”.',
en: '“Large” means big; *largo* is “long”.',
},
lecture: {
native: '“Lecture” es una *conferencia* o *clase*. La *lectura* se dice “reading”.',
en: '“Lecture” is a talk; *lectura* is “reading”.',
},
parents: {
native: '“Parents” son los *padres*. Los *parientes* se dicen “relatives”.',
en: '“Parents” are your mother and father; *parientes* are “relatives”.',
},
record: {
native: '“Record” significa *grabar* o *registrar*. Para *recordar* se dice “remember”.',
en: '“Record” means to register; *recordar* is “to remember”.',
},
remove: {
native: '“Remove” significa *quitar*. Para *remover* (revolver) se dice “stir”.',
en: '“Remove” means to take away; *remover* is “to stir”.',
},
rope: {
native: '“Rope” es la *cuerda*. La *ropa* se dice “clothes”.',
en: '“Rope” is cord; *ropa* is “clothes”.',
},
once: {
native: '“Once” significa *una vez*. El número *once* se dice “eleven”.',
en: '“Once” means one time; the Spanish *once* is “eleven”.',
},
question: {
native: '“Question” es una *pregunta*. Una *cuestión* (asunto) se dice “matter” o “issue”.',
en: '“Question” is something you ask; a *cuestión* is a “matter”.',
},
compromise: {
native: '“Compromise” es un *acuerdo con concesiones*. Un *compromiso* (obligación) se dice “commitment”.',
en: '“Compromise” is meeting halfway; a *compromiso* is a “commitment”.',
},
career: {
native: '“Career” es la *trayectoria profesional*. La *carrera* que se estudia se dice “degree” o “major”.',
en: '“Career” is your working life; a university *carrera* is a “degree”.',
},
ultimately: {
native: '“Ultimately” significa *a fin de cuentas*. Para *últimamente* se dice “lately”.',
en: '“Ultimately” means in the end; *últimamente* is “lately”.',
},
idiom: {
native: '“Idiom” es una *expresión hecha*. El *idioma* se dice “language”.',
en: '“Idiom” is a set phrase; *idioma* is “language”.',
},
deception: {
native: '“Deception” significa *engaño*. La *decepción* se dice “disappointment”.',
en: '“Deception” means being misled; *decepción* is “disappointment”.',
},
pretend: {
native: '“Pretend” significa *fingir*. Para *pretender* (aspirar a) se dice “intend” o “claim”.',
en: '“Pretend” means to fake; *pretender* is “to intend”.',
},
},
docs: {
sortRecent: 'Recientes · Recent',
sortTitle: 'Título · Title',
sortLongest: 'Los más largos · Longest',
backUpAll: 'Respaldar todo · Back up all:',
signOut: 'Cerrar sesión · Sign out',
duplicate: 'Duplicar · Duplicate',
searchPlaceholder: 'Buscar · Search',
searching: 'Buscando… · Searching…',
noMatches: 'Sin resultados · No matches',
tags: 'Etiquetas · Tags',
newTagPlaceholder: 'Nueva etiqueta · New tag',
language: 'Idioma · Language',
languageFailed: 'No se pudo cambiar el idioma — sigue igual · Couldnt switch',
},
editor: {
askPlaceholder: 'Ask why… / Pregunta por qué…',
chatFailed: 'No logré responder — vuelve a preguntarme, por favor. 🌸\n\nI had trouble answering just now — please ask me again. 🌸',
findPlaceholder: 'Buscar · Find',
findNone: 'Nada · 0',
matchCase: 'Match case · Distinguir mayúsculas',
close: 'Close · Cerrar',
replacePlaceholder: 'Reemplazar por · Replace',
replace: 'Reemplazar',
replaceAll: 'Todo',
translateLabel: 'Traducción · Translate',
spelling: 'Ortografía · Spelling',
noSuggestions: 'Sin sugerencias · No suggestions',
addToDictionary: 'Agregar al diccionario · Add to dictionary',
readSelection: 'Leer la selección en voz alta · Read selection aloud',
rewrite: 'Reescribir · Rewrite',
rewriting: 'Reescribiendo… · Rewriting…',
rewriteFailed: 'No se pudo reescribir — inténtalo otra vez · Couldnt rewrite',
cancel: 'Cancelar · Cancel',
retry: 'Reintentar · Retry',
useThis: 'Usar esta · Use this',
word: 'Palabra · Word',
inGarden: 'Ya está en el jardín · In your garden (tap to remove)',
saveToGarden: 'Guardar en el jardín · Save to garden',
readAloud: 'Leer en voz alta · Read aloud',
readSlowly: 'Leer despacio · Read slowly',
readAloudNative: 'Leer en español · Read in Spanish',
lookingUp: 'Buscando… · Looking up…',
definition: 'Definición · Definition',
synonyms: 'Sinónimos · Synonyms',
tapToSwap: 'toca para cambiar · tap to swap',
nothingFound: 'No encontré esta palabra · Nothing found for this word',
origin: 'Origen · Origin',
// El par español ve esto seguido: inglés y español comparten tanto latín que
// los choques son la regla y no la excepción — real, red, once, pie, sin,
// pan, mayor, sale, ropa.
alsoIn: 'También es una palabra en español · Also a word in Spanish',
wordBands: {
simple: { native: 'De todos los días', en: 'Everyday word' },
standard: { native: 'Común', en: 'Standard' },
advanced: { native: 'Avanzada', en: 'Advanced' },
},
},
styles: {
natural: { native: 'Más natural', en: 'Natural' },
academic: { native: 'Académico', en: 'Academic' },
professional: { native: 'Profesional', en: 'Professional' },
casual: { native: 'Informal', en: 'Casual' },
humorous: { native: 'Divertido', en: 'Humorous' },
creative: { native: 'Creativo', en: 'Creative' },
persuasive: { native: 'Persuasivo', en: 'Persuasive' },
},
tones: {
general: { native: 'General', en: 'General' },
academic: { native: 'Académico', en: 'Academic' },
professional: { native: 'Profesional', en: 'Professional' },
casual: { native: 'Informal', en: 'Casual' },
humorous: { native: 'Divertido', en: 'Humorous' },
creative: { native: 'Creativo', en: 'Creative' },
persuasive: { native: 'Persuasivo', en: 'Persuasive' },
},
exports: {
label: 'Exportar',
print: 'Imprimir / PDF',
formats: {
md: { native: 'Markdown', en: 'Markdown (.md)' },
docx: { native: 'Documento de Word', en: 'Word (.docx)' },
html: { native: 'Página web', en: 'Web page (.html)' },
txt: { native: 'Texto sin formato', en: 'Plain text (.txt)' },
},
},
garden: {
title: 'Jardín de palabras · Vocabulary Garden',
titleWithFlower: '🌷 Jardín de palabras · Vocabulary Garden',
reviewing: 'Repaso · Reviewing — recall, then grade yourself',
subtitle: 'Words you looked up, blooming as you learn them',
reviewDue: (n) => `Repasar ${n} palabra${n === 1 ? '' : 's'} · Review ${n} due 🌸`,
emptyLead: 'Tu jardín todavía está vacío.',
emptyHint: 'Haz clic derecho en una palabra en inglés para buscarla — y aquí brotará.',
due: 'por repasar · due',
seen: (reps, intervalDays) => `repasada ${reps}× · seen ${reps}× · intervalo ${intervalDays} d`,
readAloud: '🔊 Leer',
readSlowly: '🐢 Despacio',
source: '📄 Fuente',
remove: '🗑 Quitar',
growing: (n) => `🐱💤 ${n} flor${n === 1 ? '' : 'es'} en el jardín · ${n} blossom${n > 1 ? 's' : ''} growing`,
end: 'Terminar · End',
promptProduction: '¿Cuál es la palabra en inglés? · Which English word?',
promptRecognition: '¿Qué significa? · What does this mean?',
showAnswer: 'Ver la respuesta · Show answer',
gradeAgain: { native: 'Repetir', en: 'Again' },
gradeGood: { native: 'La recuerdo', en: 'Good' },
gradeEasy: { native: 'Fácil', en: 'Easy' },
},
journal: {
tabGarden: '🌷 Jardín · Garden',
tabJournal: '🌱 Avances · Growth',
subtitle: 'Your own writing, month by month — only ever you and your past self',
empty: 'Sigue escribiendo un poco más — esta página crece a partir de tu propio trabajo. · Keep writing; this page grows out of your own work.',
keptHead: 'Este mes · This month',
kept: (n) => `${n} cosa${n === 1 ? '' : 's'} que te llevaste · ${n} thing${n === 1 ? '' : 's'} you took on board`,
keptBefore: (n) => `${n} el mes pasado · ${n} the month before`,
stuckHead: 'Se te quedó · Stayed with you',
stuck: (phrase, docs) =>
`${phrase}” — ya la escribes sola, en ${docs} de tus textos · now in ${docs} of your pieces`,
fadedHead: 'Ya no necesitas corregir esto · You stopped needing this',
faded: (pattern, times) =>
`${pattern}” — ${times}× antes, ninguna este mes · ${times}× back then, none this month`,
cheerStuck: (phrase) => ({
native: `¡Ya usas “${phrase}” tú solita! 🌱`,
en: `Youre using “${phrase}” on your own now! 🌱`,
}),
cheerFaded: (pattern) => ({
native: `Hace rato que “${pattern}” no necesita corrección 😌`,
en: `${pattern}” hasnt needed fixing in a while 😌`,
}),
},
history: {
title: 'Historial · History',
kinds: {
manual: { native: 'Punto guardado', en: 'Saved point' },
auto: { native: 'Automático', en: 'Auto' },
pre_restore: { native: 'Antes de restaurar', en: 'Before restore' },
},
justNow: 'ahora mismo · just now',
minutesAgo: (n) => `hace ${n} min · ${n} min ago`,
hoursAgo: (n) => `hace ${n} h · ${n} hr ago`,
daysAgo: (n) => `hace ${n} día${n > 1 ? 's' : ''} · ${n} day${n > 1 ? 's' : ''} ago`,
preview: 'Vista previa · Preview',
restoring: 'Restaurando… · Restoring…',
restoreThis: 'Restaurar esta versión · Restore this version',
passport: '📜 Pasaporte de escritura · Writing passport',
keepFullHistory: 'Guardar todo el historial · Keep full history',
},
status: {
savedLocally: 'Guardado en este dispositivo · Kept on this device',
helperRestingNative: 'El ayudante está descansando',
helperRestingEn: "· Petal's helper is resting · tu texto está guardado",
petalsToPolish: (n) => ({
native: `${n} ${n === 1 ? 'pétalo' : 'pétalos'} por pulir`,
en: `${n} ${n === 1 ? 'petal' : 'petals'} to polish`,
}),
soundsOn: 'Sonidos activados · Sounds on',
soundsOff: 'Sonidos desactivados · Sounds off',
petalsOn: 'Pétalos activados · Petals on',
petalsOff: 'Pétalos desactivados · Petals off',
statsTitle: 'Estadísticas · Writing stats',
stats: {
words: { native: 'Palabras', en: 'Words' },
characters: { native: 'Caracteres', en: 'Characters' },
sentences: { native: 'Oraciones', en: 'Sentences' },
paragraphs: { native: 'Párrafos', en: 'Paragraphs' },
pages: { native: 'Páginas', en: 'Pages' },
readingTime: { native: 'Tiempo de lectura', en: 'Reading time' },
avgWordLength: { native: 'Longitud promedio', en: 'Avg word length' },
variety: { native: 'Variedad de vocabulario', en: 'Word variety' },
readability: { native: 'Nivel de lectura', en: 'Reading level' },
},
readability: {
easy: { native: 'Fácil', en: 'Easy' },
standard: { native: 'Común', en: 'Standard' },
fairlyHard: { native: 'Algo difícil', en: 'Fairly hard' },
advanced: { native: 'Avanzado', en: 'Advanced' },
},
},
toolbar: {
untitledHeading: '(sin título)',
outline: 'Esquema · Outline',
outlineHint: 'Usa H1/H2/H3 para crear títulos, y el esquema aparecerá aquí.',
},
update: {
available: 'Hay una versión nueva disponible',
refresh: 'Actualizar · Refresh',
dismiss: 'Más tarde · Dismiss',
},
}