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
+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',
},
}