Petal learns French, and the pack that shipped was misspelling itself

Phase 24, the fr half: langpack, Hunspell dictionary, Piper voice, and the
lexicon coverage that turned out to have been measured already (63.1%, better
than pt-PT's 62.1%). No migration; not deployed.

The plan recorded that build_ptpt_dictionary.py "generalizes" to French. It
did not. It handled single-character flags and plain PFX/SFX and stopped on
everything else, and fr.aff uses four of the things it stopped on. FLAG long
is the dangerous one: French flags are two characters, so the old reader's
set(flagstr) yields a bag of unrelated letters and expands every entry through
the wrong paradigm without ever erroring. Plus continuation flags (French
really does affix an affixed form), NEEDAFFIX on 68,075 of 84,140 stems, and
FULLSTRIP. Renamed build_hunspell_dictionary.py with a per-language profile,
asserting that CIRCUMFIX and FORBIDDENWORD are still unused rather than
assuming it — and it rebuilds pt-PT byte-identical to the shipped asset, which
is the only thing that makes "generalized" a claim rather than a hope.

Elision was decided by building both halves and measuring. Keeping l'arbre and
its thirty-three siblings: 3,159,832 forms, 8.25 MB gzipped. Dropping them:
473,326 and 1.19 MB. They are not new words, but the tokenizer keeps internal
apostrophes, so they genuinely would have been underlined — so they moved out
of the dictionary into withElision, which splits at a known clitic and still
requires the remainder to be a word (l'zzzz stays flagged). Real nspell: 369 ms
and 74 MB, against pt-PT's 842 ms and 139 MB, on the larger language.

Where the regional trap lives is the mirror image of Portuguese's: every fr_*
Piper voice is fr_FR and Debian's fr_FR/fr_CA/fr_BE dictionaries are one shared
word list, so nothing can be quietly wrong about the country and the whole
decision sits in the copy. What French has instead is the 1990 reform, packaged
three ways; comprehensive ships, because Petal never corrects her French and
coût and cout are both correct.

Then the interim review pass, at the user's suggestion and explicitly "for
now": four models read each Latin pack independently, and only findings at
least two of them reached on their own were applied — five per pack. It earned
its keep on the pack that was already live. pt-PT was carrying pre-Acordo
spellings (adjectivos, actualmente) in a file whose own header commits to
post-Acordo, plus Brazilian decepção, because the Phase 21 greps checked for
Brazilian vocabulary and never checked the pack against its own spelling
policy. That grep now exists and was confirmed to fail on the old text before
being kept. Where reviewers agreed a line was wrong but split on the fix, the
wording is mine and the reasoning is in BUILD_PLAN rather than averaged away.

Still owed, and both packs now say so precisely: a quorum of models agreeing is
agreement, not authority. No native speaker has read either pack, and none of
this has been seen in a browser.

go build/vet/test clean, tsc, vite build, vitest 190/190.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
prosolis
2026-07-27 16:19:26 -07:00
parent 9a2e909b85
commit 071ea7b835
20 changed files with 1453 additions and 286 deletions
+79 -9
View File
@@ -3,12 +3,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { onPackChange, pack, resetPackForTests, setPackLang, shippedPacks } from './index'
import { zh } from './packs/zh'
import { ptPT } from './packs/pt-PT'
import { fr } from './packs/fr'
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]
const PACKS: Pack[] = [zh, ptPT, fr]
beforeEach(() => {
resetPackForTests()
@@ -33,8 +34,9 @@ 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.
setPackLang('fr')
// 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')
expect(pack()).toBe(zh)
setPackLang('klingon')
expect(pack()).toBe(zh)
@@ -54,7 +56,7 @@ describe('pack selection', () => {
expect(seen).not.toHaveBeenCalled()
// An unshipped pair resolves back to zh, which is also not a change.
setPackLang('fr')
setPackLang('es')
expect(seen).not.toHaveBeenCalled()
setPackLang('pt-PT')
@@ -67,7 +69,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(['pt-PT', 'zh'])
expect(codes.sort()).toEqual(['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)
@@ -199,6 +201,17 @@ describe('the pt-PT pack', () => {
expect(text, `pt-BR form "${bad}" in the pt-PT pack`).not.toContain(bad)
}
// And pre-Acordo spellings, which this grep did NOT cover until Phase 24's
// review pass found *adjectivos*, *actualmente* and *decepção* sitting in a
// file whose own header commits to post-Acordo. Guarding the pt-BR fault
// line while leaving the pack's stated spelling policy unchecked is half a
// test: both are invisible to a reviewer who doesn't read Portuguese.
// Whole words only — the English "actually" and "factual" contain "actual".
for (const bad of ['actualmente', 'adjectivo', 'decepção', 'acção', 'óptimo',
'recepção', 'objectivo', 'directamente', 'exacto']) {
expect(text, `pre-Acordo form "${bad}" in the pt-PT pack`).not.toContain(bad)
}
// And the European forms that should be there instead.
expect(ptPT.editor.synonyms).toContain('Sinónimos')
expect(ptPT.styles.academic.native).toBe('Académico')
@@ -222,15 +235,17 @@ describe('the pt-PT pack', () => {
// is the one most easily spoiled by a stray comparison. The rule is enforced
// in SQL on the backend; here it is enforced in the copy.
it('keeps the journal to growth and to her own past self', () => {
const text = JSON.stringify({ zh: zh.journal, pt: ptPT.journal }, (_k, v) =>
typeof v === 'function' ? JSON.stringify(v(2, 3)) : v,
const text = JSON.stringify(
PACKS.map((p) => p.journal),
(_k, v) => (typeof v === 'function' ? JSON.stringify(v(2, 3)) : v),
)
for (const bad of ['error', 'mistake', 'wrong', 'streak', 'average', 'erro', 'errada', '错误']) {
for (const bad of ['error', 'mistake', 'wrong', 'streak', 'average', 'erro', 'errada',
'erreur', 'faute', 'moyenne', '错误']) {
expect(text.toLowerCase(), `the journal must not talk about "${bad}"`).not.toContain(bad)
}
})
it('says the collision line the zh pair never needed', () => {
it('says the collision line the zh pair never needed (pt-PT)', () => {
// "sale", "comum" and "tarde" are words on both sides of this pair, so the
// word card's second reading is reachable copy here — unlike in zh.
expect(ptPT.editor.alsoIn).toBeTruthy()
@@ -238,6 +253,51 @@ describe('the pt-PT pack', () => {
})
})
describe('the fr pack', () => {
// French's regional question is not the dictionary's — Debian's fr_FR, fr_CA
// and fr_BE are all symlinks to one word list — so the whole of it lives in
// this file, which makes it exactly as invisible to a reviewer as the pt-BR
// forms were, and worth pinning the same way.
it('is metropolitan French, not Québécois', () => {
const text = JSON.stringify(fr, (_k, v) => (typeof v === 'function' ? v(1, 'x') : v))
for (const bad of ['courriel', 'clavarder', 'magasiner', 'fin de semaine', 'baladodiffusion']) {
expect(text, `Québécois form "${bad}" in the fr pack`).not.toContain(bad)
}
// And the voice it is read in, for the same reason the locale is pinned.
expect(fr.locale).toBe('fr-FR')
})
// The pack punctuates the way French does, which is the same habit
// prose.spaceBeforePunct tells her not to carry into her English. Both halves
// of that are deliberate and either would look like a typo to a tidier.
it('keeps French spacing and guillemets in its own copy', () => {
expect(fr.companion.greeting.native).toContain(' !')
expect(fr.prose.articleAn('apple')).toContain('« an apple »')
expect(fr.prose.spaceBeforePunct).toContain('inverse du français')
})
it('renders its interpolated lines with the value in place', () => {
expect(fr.app.duplicateTitle('Printemps')).toBe('Printemps (copie)')
expect(fr.companion.milestone(300).native).toContain('300 mots')
// French agreement is the pack's business, the same way English
// pluralisation is — the call site only ever passes a number.
expect(fr.garden.reviewDue(1)).toContain('1 mot ·')
expect(fr.garden.reviewDue(4)).toContain('4 mots ·')
expect(fr.garden.growing(1)).toContain('1 fleur au jardin')
expect(fr.garden.growing(3)).toContain('3 fleurs au jardin')
expect(fr.journal.kept(1)).toContain('1 chose que tu as retenue')
expect(fr.journal.kept(5)).toContain('5 choses que tu as retenues')
})
it('says the collision line, which this pair needs most of all', () => {
// English took so much from French that the collisions are the rule rather
// than the exception: chat, pain, coin, sale, four, car, or.
expect(fr.editor.alsoIn).toBeTruthy()
expect(fr.editor.alsoIn).not.toBe(zh.editor.alsoIn)
expect(fr.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.
@@ -255,6 +315,16 @@ describe('false friends', () => {
expect(Object.keys(ptPT.falseFriends).length).toBeGreaterThan(10)
})
it('the fr pair carries the ones that cost most', () => {
// "attend" and "pass" are the two this pair has and pt-PT does not: French
// *attendre* is to wait, and *passer un examen* is to sit one, not to pass
// it — which is the false friend most likely to end up in a real letter.
for (const word of ['actually', 'library', 'attend', 'pass', 'sensible']) {
expect(fr.falseFriends[word], word).toBeDefined()
}
expect(Object.keys(fr.falseFriends).length).toBeGreaterThan(10)
})
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)) {
+4 -3
View File
@@ -17,12 +17,13 @@ import { useSyncExternalStore } from 'react'
import type { Pack, PairLang } from './types'
import { zh } from './packs/zh'
import { ptPT } from './packs/pt-PT'
import { fr } from './packs/fr'
export type { Pack, PairLang, Line } from './types'
// Every pack Petal ships. fr and es are the same two lines apiece when their
// copy is written — TypeScript names every string a new pack still owes.
const PACKS: Partial<Record<PairLang, Pack>> = { zh, 'pt-PT': ptPT }
// 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 }
const DEFAULT_LANG: PairLang = 'zh'
+486
View File
@@ -0,0 +1,486 @@
// The French pack — the third pair, and the first written against a groove
// rather than cutting one.
//
// ⚠️ 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 — the same standard the zh copy got by
// being written for a real reader. That has still not happened. What has
// happened (2026-07-27) is an interim pass: four different models read this file
// independently as French speakers, and only findings at least two of them
// reached on their own were applied — five of them, listed in BUILD_PLAN Phase
// 24. A quorum of models agreeing is agreement, not authority: it caught a
// "clique droit" that is not French and an adjective disagreeing with "on", and
// it would not catch a line that is correct and lifeless. Treat this as a
// better-checked draft, and still expect a speaker to change the register long
// before the vocabulary.
//
// The choices this file makes, and why:
//
// * **Metropolitan French, not Québécois.** Unlike pt, the *dictionary* poses
// no regional question — Debian's fr_FR, fr_CA, fr_BE, fr_CH and fr_LU are
// all symlinks to one file — so the whole regional decision lives here in
// the copy: e-mail rather than courriel, tchatter rather than clavarder,
// week-end rather than fin de semaine. The Piper voice is fr_FR for the same
// reason. This is a choice, not a verdict; a Québécoise writer would want
// her own pack and should get one rather than a patched version of this.
// * **Tutoiement.** Petal is a companion in someone's private notebook, and
// *vous* would put a desk between them. Same call the pt-PT pack made about
// *tu* over *você*, for the same reason.
// * **"Se connecter" / "Se déconnecter"**, not "login" / "logout".
// * **French spacing and « guillemets », on purpose.** French puts a space
// before : ; ! ? and inside « », and this file does too — which is exactly
// the habit `prose.spaceBeforePunct` exists to warn her about in her
// *English*. The pack demonstrates, in its own punctuation, the rule its own
// prose note tells her not to carry across. It is an ordinary space rather
// than the typographically correct U+202F: a narrow no-break space is
// invisible in a diff, indistinguishable from a plain one in an editor, and
// the next pack author would strip it by accident. The cost is that a line
// may wrap before the « ! », which is a smaller problem than copy nobody can
// safely edit.
// * The 1990 spelling reform is left alone: this copy uses the traditional
// forms, and her dictionary accepts both (see build_hunspell_dictionary.py).
//
// French 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 fr: Pack = {
code: 'fr',
nativeName: 'Français',
locale: 'fr-FR',
app: {
duplicateTitle: (title) => `${title} (copie)`,
garden: 'Jardin de mots',
history: 'Historique',
},
auth: {
title: 'Reconnecte-toi',
titleEn: 'Please sign in again',
bodyWithDraft:
'Ce que tu viens d’écrire est gardé sur cet appareil — reconnecte-toi et tout senregistrera tout seul.',
bodyWithDraftEn: "What you just wrote is safe on this device — it'll save itself once you're back in.",
bodyPlain: 'Ta session a expiré. Tout ce que tu as écrit est déjà enregistré.',
bodyPlainEn: 'Your session expired. Everything you wrote is already saved.',
signIn: 'Se connecter · Sign in',
},
companion: {
choose: 'Choisis un compagnon · Choose a companion',
encouragements: [
{ native: 'Voilà ! Cette phrase coule beaucoup mieux 🌸', en: 'Lovely — that reads so much smoother now.' },
{ native: 'Tu écris de mieux en mieux ✨', en: "You're getting better and better." },
{ native: 'Jaime beaucoup ce changement 💕', en: 'I really like that change.' },
{ native: 'Continue comme ça — tu y arrives !', en: 'Keep going — youve got this!' },
{ native: 'Mmh, cest bien plus clair comme ça 👍', en: 'Mm, thats much clearer.' },
{ native: 'Quel joli choix de mot 🌷', en: 'Thats such a good word choice.' },
{ native: 'Oh, ce paragraphe se lit tout seul ☁️', en: 'Ooh, that paragraph flows so nicely.' },
{ native: 'Jadore te voir écrire avec plus dassurance 💛', en: 'I love watching you write with more confidence.' },
{ native: 'Chaque petit progrès compte 🌱', en: 'Every little bit of progress counts.' },
{ native: 'Tes mots brillent aujourdhui ✨', en: 'Your words are sparkling today.' },
],
tips: [
{ native: 'Astuce : en anglais, les phrases courtes se lisent mieux.', en: 'Tip: shorter English sentences often read clearer.' },
{ native: 'Noublie pas les articles « the » et « a ».', en: "Don't forget articles like “the” and “a”." },
{ native: 'Pour le passé, utilise le prétérit : go → went.', en: 'For the past, use past tense: go → went.' },
{ native: 'Lire à voix haute aide à repérer ce qui sonne bizarre.', en: 'Reading aloud helps you catch awkward spots.' },
{ native: 'Une idée par paragraphe, et tout devient limpide.', en: 'One idea per paragraph keeps it tidy.' },
{ native: 'Un doute ? Demande-moi ✨', en: 'Not sure about something? Just ask me. ✨' },
{ native: 'Le pluriel prend un « s » : two apples 🍎', en: 'Plurals take an “s”: two apples 🍎' },
// Two tips the zh pack has no use for: French and English share enough
// vocabulary to make both the traps below daily hazards.
{ native: 'Attention aux faux amis : « actually » ne veut pas dire *actuellement*.', en: 'Careful with false friends — “actually” means *in fact*.' },
{ native: 'En anglais, pas despace avant « ! » ni « ? » — cest une habitude française.', en: 'English puts no space before “!” or “?” — that space is a French habit.' },
],
breaks: [
{ native: 'Tu écris depuis un moment — étire-toi et repose tes yeux 🍵', en: "You've been writing a while — stretch and rest your eyes. 🍵" },
{ native: 'Un verre deau et cinq minutes de pause ?', en: 'Sip some water and take five?' },
{ native: 'Regarde au loin un instant, ça soulage les yeux 🌿', 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 French line leads gently into it, exactly as
// the Mandarin and Portuguese ones do.
bedtime: [
{ native: 'Ton lit doit se demander où tu es 🛏️', en: 'I bet your bed is missing you right now.' },
{ native: 'Quand tu es fatiguée, tu écris mal — va te reposer 🌙', en: 'A tired writer is a bad writer — get some rest.' },
{ native: 'La nuit porte conseil ✨', en: 'Sleep is a wondrous enabler.' },
{ native: 'Tu entends ? Non… parce que tout le monde dort, et toi aussi tu devrais 😴', en: "Hear that? No… you don't, because everyone is sleeping and you should be too." },
// French proverbs on sleep and haste, in place of the Portuguese and
// Chinese ones — a pack is not a translation of another pack.
{ native: 'Qui dort dîne.', en: 'Sleep is its own kind of supper.' },
{ native: 'Lavenir appartient à ceux qui se lèvent tôt.', en: 'The future belongs to those who get up early.' },
{ native: 'Rien ne sert de courir, il faut partir à point.', en: 'No use running; you have to set off in good time.' },
],
greeting: { native: 'Coucou ! Je te tiens compagnie 🐱', en: "Hi! I'm right here keeping you company. 🐱" },
welcomeBack: { native: 'Te revoilà ✨ on continue !', en: 'Welcome back ✨ lets keep going!' },
errors: [
{ native: 'Oups — un petit accroc, mais tes mots sont en sécurité.', en: 'Oops — a little hiccup, but your words are safe.' },
{ native: 'Zut, je me suis emmêlé les pinceaux une seconde — je reviens.', en: 'Haiya, I got stuck for a sec — back in a moment.' },
{ native: 'Ne ten fais pas, on réessaie dans un instant 🍵', en: "Don't worry — let's try again in a bit. 🍵" },
],
milestone: (words: number) => ({
native: `Ouah ! Déjà ${words} mots 🎉`,
en: `Wow — ${words} words already! Amazing. 🎉`,
}),
// Une petite chose à écrire, proposée une fois par jour à une page blanche.
// Des souvenirs et des avis, jamais des exercices : il ny a rien ici quon
// puisse rater, et cest précisément lintention.
invitations: [
{ native: 'Écris 50 mots : une petite chose qui ta fait sourire aujourdhui 🌸', en: 'Write 50 words: one small thing that made you smile today.' },
{ native: 'Écris 50 mots : la meilleure chose que tu as mangée aujourdhui', en: 'Write 50 words: the best thing you ate today.' },
{ native: 'Écris 50 mots : ce que tu vois par ta fenêtre en ce moment', en: 'Write 50 words: what you can see out of your window right now.' },
{ native: 'Écris 50 mots : un endroit où tu retournerais volontiers', en: 'Write 50 words: somewhere you would happily go back to.' },
{ native: 'Écris 50 mots : une chose que tu as apprise cette semaine', en: 'Write 50 words: one thing you learned this week.' },
{ native: 'Écris 50 mots : un message à toi-même pour dans un an', en: 'Write 50 words: something to tell yourself a year from now.' },
{ native: 'Écris 50 mots : une chanson que tu écoutes en boucle en ce moment', en: 'Write 50 words: a song you have had on lately.' },
{ native: 'Écris 50 mots : quelquun que tu aimerais remercier aujourdhui', en: 'Write 50 words: someone you would like to thank today.' },
],
inviteAccept: 'Cest parti · Lets write',
inviteDecline: 'Pas aujourdhui · Not today',
declined: { native: 'Daccord, je retourne dormir 😴', en: 'Fair enough — back to my nap. 😴' },
names: {
cat: 'Chat dormeur',
dog: 'Chien joyeux',
'wiggle-dog': 'Chien frétillant',
butterfly: 'Papillon',
parrot: 'Perroquet',
},
},
prose: {
longSentence: 'Cette phrase est un peu longue — la couper en deux ou trois la rendra plus claire 🌸',
commaSplice: 'Ici, deux phrases sont reliées par une simple virgule. Mets un point, ou relie-les avec « and / but ».',
vagueThis: (word) => `On ne sait pas bien à quoi « ${word} » renvoie — précise-le (par exemple « ${word} idea / change… »).`,
oxfordComma: 'Dans une énumération de trois éléments ou plus, une virgule avant « and / or » aide à lire (la virgule dOxford).',
transitionComma: (word) => `Après un mot de liaison en début de phrase, mets une virgule : « ${word}, … ».`,
capitalizeSentence: 'Commence chaque phrase par une majuscule.',
repeatedWord: (word) => `« ${word} » semble écrit deux fois — jette un œil.`,
capitalizeI: 'En anglais, le « I » (je) prend toujours une majuscule.',
// The rule this pack's own punctuation illustrates from the other side.
spaceBeforePunct: 'En anglais, pas despace avant la ponctuation : la virgule et le point se collent au mot. Cest linverse du français.',
spaceAfterPunct: 'Après une virgule ou un point, laisse un espace avant le mot suivant.',
articleAn: (word) => `Devant un son de voyelle, on met « an » : « an ${word} ».`,
articleA: (word) => `Devant un son de consonne, on met « a » : « a ${word} ».`,
uncountable: (word, singular) => `« ${word} » est indénombrable en anglais — pas de s, « ${singular} » suffit.`,
capitalizeProper: (fixed) => `En anglais, les langues, les nationalités, les jours et les mois prennent une majuscule : « ${fixed} ».`,
thirdPersonS: (subject, verb) => `Avec he/she/it, le verbe prend un -s : « ${subject} ${verb} ».`,
pluralAfter: (determiner, noun) => `Après « ${determiner} », le nom se met au pluriel : « ${determiner} ${noun}s ».`,
doubleDeterminer: (first, second) => `« ${first} ${second} » contient deux déterminants — nen garde quun (enlève « ${first} », par exemple).`,
thereArePlural: (noun) => `Au pluriel, on dit « there are » : « there are ${noun}… ».`,
itsOwn: '« its » = « it is ». Pour dire « son / sa », cest « its » — donc « its own ».',
itsIs: (rest) => `Ici cest « its ${rest} » (it is) ; « its » est le possessif.`,
thanNotThen: (word) => `Dans une comparaison, on écrit « than », pas « then » : « ${word} than ».`,
preposition: (wrong, right) => `En anglais on dit « ${right} », pas « ${wrong} » — cette préposition est figée.`,
collocation: (wrong, right) => `En anglais, ces mots vont ensemble comme ceci : « ${right} », et non « ${wrong} ».`,
doubleComparative: (lead, word) => `« ${word} » est déjà le comparatif — pas besoin de « ${lead} » : « ${word} » suffit.`,
peopleArePlural: (verb) => `« People » est pluriel en anglais : « people ${verb} ».`,
ageIsNotHave: (years) => `En anglais, l’âge se dit avec *to be*, pas avec *avoir* : « I am ${years} years old ».`,
agreeIsAVerb: '« Agree » est déjà le verbe — pas de *to be* devant : on dit « I agree ».',
forNotSince: (duration) => `Pour une durée, on utilise « for » : « for ${duration} ». « Since » marque le point de départ (since 2020).`,
veryBeforeVerb: (verb) => `« Very » naccompagne que les adjectifs, pas les verbes : « really ${verb} », ou « ${verb} … very much ».`,
turnOnNotOpen: (thing, on) => `En anglais, on nouvre pas un appareil, on lallume : « turn ${on ? 'on' : 'off'} the ${thing} ».`,
althoughOrBut: (word) => `En anglais, on met « ${word} » ou « but », jamais les deux dans la même phrase.`,
},
// Les faux amis entre le français et langlais — le piège qui donne le
// sentiment d’être ridicule plutôt que simplement corrigée. Doù le simple
// avertissement : Petal ne remplace jamais le mot, parce que « actually »
// était peut-être bien celui quelle voulait.
falseFriends: {
actually: {
native: '« Actually » veut dire *en fait*, pas *actuellement*. Pour « actuellement », on dit « currently » ou « nowadays ».',
en: '“Actually” means *in fact*. For the French *actuellement*, English uses “currently”.',
},
eventually: {
native: '« Eventually » veut dire *finalement, tôt ou tard* — pas *éventuellement*. Pour cela : « possibly » ou « if necessary ».',
en: '“Eventually” means *in the end*, not *possibly*.',
},
library: {
native: '« Library » est la *bibliothèque*. La *librairie* se dit « bookshop » / « bookstore ».',
en: '“Library” is where books are lent; a shop that sells them is a “bookshop”.',
},
sensible: {
native: '« Sensible » veut dire *raisonnable*. Pour *sensible*, on dit « sensitive ».',
en: '“Sensible” means level-headed; the French *sensible* is “sensitive”.',
},
assist: {
native: '« Assist » veut dire *aider*. Pour *assister à* (être présent), on dit « attend ».',
en: '“Assist” means to help; *assister à* is “attend”.',
},
attend: {
native: '« Attend » veut dire *assister à*. Pour *attendre*, on dit « wait ».',
en: '“Attend” means to go to something; *attendre* is “to wait”.',
},
pretend: {
native: '« Pretend » veut dire *faire semblant*. Pour *prétendre* (affirmer), on dit « claim ».',
en: '“Pretend” means to fake something; *prétendre* is “to claim”.',
},
deception: {
native: '« Deception » veut dire *tromperie*. La *déception* se dit « disappointment ».',
en: '“Deception” means being misled; *déception* is “disappointment”.',
},
delay: {
native: '« Delay » veut dire *retard*. Un *délai* se dit « deadline » ou « time limit ».',
en: '“Delay” is lateness; a French *délai* is a “deadline”.',
},
achieve: {
native: '« Achieve » veut dire *réussir, accomplir*. Pour *achever* (terminer), on dit « finish » ou « complete ».',
en: '“Achieve” means to accomplish; *achever* is “to finish”.',
},
rest: {
native: '« Rest » veut dire *se reposer* (ou *le reste*). Pour *rester*, on dit « stay ».',
en: '“Rest” is to relax; *rester* is “to stay”.',
},
journey: {
native: '« Journey » est un *voyage*. Une *journée* se dit « day ».',
en: '“Journey” is a trip; *journée* is a “day”.',
},
location: {
native: '« Location » est un *endroit*. La *location* (louer) se dit « rental » ou « renting ».',
en: '“Location” is a place; the French *location* is a “rental”.',
},
travel: {
native: '« Travel » veut dire *voyager*. Pour *travailler*, on dit « work ».',
en: '“Travel” means to journey; *travailler* is “to work”.',
},
pass: {
native: '« Pass an exam » veut dire *réussir* un examen. Pour *passer* un examen, on dit « take an exam ».',
en: '“Pass an exam” means you succeeded; *passer un examen* is “to take” one.',
},
resume: {
native: '« Resume » veut dire *reprendre*. Un *résumé* se dit « summary » (aux États-Unis, « résumé » est un CV).',
en: '“Resume” means to start again; a *résumé* is a “summary”.',
},
college: {
native: '« College » est lenseignement supérieur. Le *collège* français se dit « middle school ».',
en: '“College” is higher education; a French *collège* is “middle school”.',
},
envy: {
native: '« Envy » est la *jalousie*. Pour *avoir envie de*, on dit « to feel like » ou « to want ».',
en: '“Envy” is jealousy; *avoir envie de* is “to feel like”.',
},
crayon: {
native: '« Crayon » est un *crayon de couleur* (en cire). Le *crayon à papier* se dit « pencil ».',
en: '“Crayon” is a wax colouring stick; a French *crayon* is a “pencil”.',
},
coin: {
native: '« Coin » est une *pièce de monnaie*. Le *coin* (angle) se dit « corner ».',
en: '“Coin” is money; the French *coin* is a “corner”.',
},
figure: {
native: '« Figure » est un *chiffre* ou une *silhouette*. La *figure* (visage) se dit « face ».',
en: '“Figure” is a number or a shape; the French *figure* is a “face”.',
},
comprehensive: {
native: '« Comprehensive » veut dire *complet, exhaustif*. Pour *compréhensif* (indulgent), on dit « understanding ».',
en: '“Comprehensive” means thorough; *compréhensif* is “understanding”.',
},
},
docs: {
sortRecent: 'Récents · Recent',
sortTitle: 'Titre · Title',
sortLongest: 'Les plus longs · Longest',
backUpAll: 'Tout sauvegarder · Back up all :',
signOut: 'Se déconnecter · Sign out',
duplicate: 'Dupliquer · Duplicate',
searchPlaceholder: 'Rechercher · Search',
searching: 'Recherche… · Searching…',
noMatches: 'Aucun résultat · No matches',
tags: 'Étiquettes · Tags',
newTagPlaceholder: 'Nouvelle étiquette · New tag',
language: 'Langue · Language',
languageFailed: 'Le changement na pas abouti — la langue na pas bougé · Couldnt switch',
},
editor: {
askPlaceholder: 'Ask why… / Demande pourquoi…',
findPlaceholder: 'Rechercher · Find',
findNone: 'Rien · 0',
matchCase: 'Match case · Respecter la casse',
close: 'Close · Fermer',
replacePlaceholder: 'Remplacer par · Replace',
replace: 'Remplacer',
replaceAll: 'Tout',
spelling: 'Orthographe · Spelling',
noSuggestions: 'Aucune suggestion · No suggestions',
addToDictionary: 'Ajouter au dictionnaire · Add to dictionary',
readSelection: 'Lire la sélection à voix haute · Read selection aloud',
rewrite: 'Réécrire · Rewrite',
rewriting: 'Réécriture… · Rewriting…',
rewriteFailed: 'La réécriture na pas abouti — réessaie · Couldnt rewrite',
cancel: 'Annuler · Cancel',
retry: 'Réessayer · Retry',
useThis: 'Utiliser celle-ci · Use this',
word: 'Mot · Word',
inGarden: 'Déjà dans le jardin · In your garden (tap to remove)',
saveToGarden: 'Garder dans le jardin · Save to garden',
readAloud: 'Lire à voix haute · Read aloud',
readSlowly: 'Lire lentement · Read slowly',
readAloudNative: 'Lire en français · Read in French',
lookingUp: 'Recherche… · Looking up…',
definition: 'Définition · Definition',
synonyms: 'Synonymes · Synonyms',
tapToSwap: 'appuie pour changer · tap to swap',
nothingFound: 'Je nai pas trouvé ce mot · Nothing found for this word',
origin: 'Origine · Origin',
// The French pair sees this constantly — English took so much from French
// that the collisions are the rule rather than the exception: chat, pain,
// coin, sale, four, or, car, ton, son.
alsoIn: 'Cest aussi un mot français · Also a word in French',
wordBands: {
simple: { native: 'De tous les jours', en: 'Everyday word' },
standard: { native: 'Courant', en: 'Standard' },
advanced: { native: 'Avancé', en: 'Advanced' },
},
},
styles: {
natural: { native: 'Plus naturel', en: 'Natural' },
academic: { native: 'Académique', en: 'Academic' },
professional: { native: 'Professionnel', en: 'Professional' },
casual: { native: 'Décontracté', en: 'Casual' },
humorous: { native: 'Drôle', en: 'Humorous' },
creative: { native: 'Créatif', en: 'Creative' },
persuasive: { native: 'Persuasif', en: 'Persuasive' },
},
tones: {
general: { native: 'Général', en: 'General' },
academic: { native: 'Académique', en: 'Academic' },
professional: { native: 'Professionnel', en: 'Professional' },
casual: { native: 'Décontracté', en: 'Casual' },
humorous: { native: 'Drôle', en: 'Humorous' },
creative: { native: 'Créatif', en: 'Creative' },
persuasive: { native: 'Persuasif', en: 'Persuasive' },
},
exports: {
label: 'Exporter',
print: 'Imprimer / PDF',
formats: {
md: { native: 'Markdown', en: 'Markdown (.md)' },
docx: { native: 'Document Word', en: 'Word (.docx)' },
html: { native: 'Page web', en: 'Web page (.html)' },
txt: { native: 'Texte brut', en: 'Plain text (.txt)' },
},
},
garden: {
title: 'Jardin de mots · Vocabulary Garden',
titleWithFlower: '🌷 Jardin de mots · Vocabulary Garden',
reviewing: 'Révision · Reviewing — recall, then grade yourself',
subtitle: 'Words you looked up, blooming as you learn them',
reviewDue: (n) => `Réviser ${n} mot${n === 1 ? '' : 's'} · Review ${n} due 🌸`,
emptyLead: 'Ton jardin est encore vide.',
emptyHint: 'Fais un clic droit sur un mot anglais pour le chercher — et il germera ici.',
due: 'à réviser · due',
seen: (reps, intervalDays) => `revu ${reps}× · seen ${reps}× · intervalle ${intervalDays} j`,
readAloud: '🔊 Lire',
readSlowly: '🐢 Lentement',
source: '📄 Source',
remove: '🗑 Retirer',
growing: (n) => `🐱💤 ${n} fleur${n === 1 ? '' : 's'} au jardin · ${n} blossom${n > 1 ? 's' : ''} growing`,
end: 'Terminer · End',
promptProduction: 'Quel est le mot anglais ? · Which English word?',
promptRecognition: 'Quest-ce que ça veut dire ? · What does this mean?',
showAnswer: 'Voir la réponse · Show answer',
gradeAgain: { native: 'À revoir', en: 'Again' },
gradeGood: { native: 'Je men souviens', en: 'Good' },
gradeEasy: { native: 'Facile', en: 'Easy' },
},
journal: {
tabGarden: '🌷 Jardin · Garden',
tabJournal: '🌱 Progrès · Growth',
subtitle: 'Your own writing, month by month — only ever you and your past self',
empty: 'Écris encore un peu — cette page pousse à partir de ton propre travail. · Keep writing; this page grows out of your own work.',
keptHead: 'Ce mois-ci · This month',
kept: (n) => `${n} chose${n === 1 ? '' : 's'} que tu as retenue${n === 1 ? '' : 's'} · ${n} thing${n === 1 ? '' : 's'} you took on board`,
keptBefore: (n) => `${n} le mois précédent · ${n} the month before`,
stuckHead: 'Resté avec toi · Stayed with you',
stuck: (phrase, docs) =>
`« ${phrase} » — tu l’écris toute seule maintenant, dans ${docs} de tes textes · now in ${docs} of your pieces`,
fadedHead: 'Tu nas plus besoin de corriger · You stopped needing this',
faded: (pattern, times) =>
`« ${pattern} » — ${times}× à l’époque, aucune ce mois-ci · ${times}× back then, none this month`,
cheerStuck: (phrase) => ({
native: `Tu écris « ${phrase} » toute seule maintenant ! 🌱`,
en: `Youre using “${phrase}” on your own now! 🌱`,
}),
cheerFaded: (pattern) => ({
native: `Ça fait un moment que « ${pattern} » na plus besoin d’être repris 😌`,
en: `${pattern}” hasnt needed fixing in a while 😌`,
}),
},
history: {
title: 'Historique · History',
kinds: {
manual: { native: 'Point gardé', en: 'Saved point' },
auto: { native: 'Automatique', en: 'Auto' },
pre_restore: { native: 'Avant restauration', en: 'Before restore' },
},
justNow: 'just now · à linstant',
minutesAgo: (n) => `${n} min ago · il y a ${n} min`,
hoursAgo: (n) => `${n} hr ago · il y a ${n} h`,
daysAgo: (n) => `${n} day${n > 1 ? 's' : ''} ago · il y a ${n} jour${n > 1 ? 's' : ''}`,
preview: 'Aperçu · Preview',
restoring: 'Restoring…',
restoreThis: 'Restaurer cette version · Restore this version',
passport: '📜 Passeport d’écriture · Writing passport',
keepFullHistory: 'Garder tout lhistorique · Keep full history',
},
status: {
savedLocally: 'Gardé sur cet appareil · Kept on this device',
helperRestingNative: 'Lassistant se repose',
helperRestingEn: "· Petal's helper is resting · ton texte est enregistré",
soundsOn: 'Sons activés · Sounds on',
soundsOff: 'Sons coupés · Sounds off',
petalsOn: 'Pétales activés · Petals on',
petalsOff: 'Pétales coupés · Petals off',
statsTitle: 'Statistiques · Writing stats',
stats: {
words: { native: 'Mots', en: 'Words' },
characters: { native: 'Caractères', en: 'Characters' },
sentences: { native: 'Phrases', en: 'Sentences' },
paragraphs: { native: 'Paragraphes', en: 'Paragraphs' },
pages: { native: 'Pages', en: 'Pages' },
readingTime: { native: 'Temps de lecture', en: 'Reading time' },
avgWordLength: { native: 'Longueur moyenne', en: 'Avg word length' },
variety: { native: 'Variété du vocabulaire', en: 'Word variety' },
readability: { native: 'Niveau de lecture', en: 'Reading level' },
},
readability: {
easy: { native: 'Facile', en: 'Easy' },
standard: { native: 'Courant', en: 'Standard' },
fairlyHard: { native: 'Assez difficile', en: 'Fairly hard' },
advanced: { native: 'Avancé', en: 'Advanced' },
},
},
toolbar: {
untitledHeading: '(sans titre)',
outline: 'Plan · Outline',
outlineHint: 'Utilise H1/H2/H3 pour créer des titres, et le plan apparaîtra ici.',
},
update: {
available: 'Une nouvelle version est disponible',
refresh: 'Actualiser · Refresh',
dismiss: 'Plus tard · Dismiss',
},
}
+20 -11
View File
@@ -1,12 +1,21 @@
// The European Portuguese pack — the first pair that is not Chinese, and the
// one that proves a language really is data.
//
// ⚠️ WRITTEN BUT NOT YET REVIEWED BY A NATIVE SPEAKER.
// ⚠️ REVIEWED BY FOUR MODELS, NOT BY A pt-PT SPEAKER.
// SUGGESTIONS.md §3 sets the bar: "the pt-PT pack should be reviewed by a pt-PT
// speaker before it's trusted — same standard the zh copy got by being written
// for a real reader." That review has not happened. Until it does, treat every
// line here as a good-faith draft rather than as shipped copy, and expect a
// speaker to change the register long before they change the vocabulary.
// for a real reader." That has still not happened. What has happened
// (2026-07-27) is an interim pass: four different models read this file
// independently as European Portuguese speakers, and only findings at least two
// of them reached on their own were applied — see BUILD_PLAN Phase 24.
//
// It found something worth the whole exercise: this file was carrying
// **pre-Acordo spellings** — *adjectivos*, *actualmente* — in direct
// contradiction of the paragraph directly below, and *decepção*, which is the
// Brazilian form. The test greps guarded against Brazilian vocabulary and never
// against the pack's own stated spelling policy; they do now. Treat this as a
// better-checked draft, and still expect a speaker to change the register long
// before they change the vocabulary.
//
// European Portuguese, not Brazilian. That is the single most likely way for
// this file to go quietly wrong, so the choices are deliberate throughout:
@@ -93,7 +102,7 @@ export const ptPT: Pack = {
{ native: 'A tua cama deve estar com saudades 🛏️', en: 'I bet your bed is missing you right now.' },
{ native: 'Cansada não se escreve bem — vai descansar 🌙', en: 'A tired writer is a bad writer — get some rest.' },
{ native: 'Dorme sobre o assunto, que as ideias vêm sozinhas ✨', en: 'Sleep is a wondrous enabler.' },
{ native: 'Ouves? Pois não… está toda a gente a dormir, e tu também devias 😴', en: "Hear that? No… you don't, because everyone is sleeping and you should be too." },
{ native: 'Ouves? Pois não ouves… está toda a gente a dormir, e tu também devias 😴', en: "Hear that? No… you don't, because everyone is sleeping and you should be too." },
// Portuguese proverbs on sleep and haste, in place of the Chinese ones —
// a pack is not a translation of another pack.
{ native: 'Deitar cedo e cedo erguer dá saúde e faz crescer.', en: 'Early to bed and early to rise makes you healthy and helps you grow.' },
@@ -135,7 +144,7 @@ export const ptPT: Pack = {
names: {
cat: 'Gato dorminhoco',
dog: 'Cão contente',
'wiggle-dog': 'Cão abanão',
'wiggle-dog': 'Cão abana-rabo',
butterfly: 'Borboleta',
parrot: 'Papagaio',
},
@@ -170,7 +179,7 @@ export const ptPT: Pack = {
ageIsNotHave: (years) => `Em inglês a idade é com o verbo *to be*, não com *have*: “I am ${years} years old”.`,
agreeIsAVerb: '“Agree” já é o verbo — não leva *to be* à frente: diz-se “I agree”.',
forNotSince: (duration) => `Para uma duração usa-se “for”: “for ${duration}”. O “since” marca o início (since 2020).`,
veryBeforeVerb: (verb) => `“Very” só acompanha adjectivos, não verbos: “really ${verb}”, ou “${verb} … very much”.`,
veryBeforeVerb: (verb) => `“Very” só acompanha adjetivos, não verbos: “really ${verb}”, ou “${verb} … very much”.`,
turnOnNotOpen: (thing, on) => `Em inglês os aparelhos não se abrem nem se fecham — ligam-se e desligam-se: “turn ${on ? 'on' : 'off'} the ${thing}”.`,
althoughOrBut: (word) => `Em inglês usa-se “${word}” ou “but”, nunca os dois na mesma frase.`,
},
@@ -181,8 +190,8 @@ export const ptPT: Pack = {
// que ela queria.
falseFriends: {
actually: {
native: '“Actually” quer dizer *na verdade*, não *actualmente*. Para “actualmente” diz-se “currently” / “nowadays”.',
en: '“Actually” means *in fact*. For the Portuguese *actualmente*, English uses “currently”.',
native: '“Actually” quer dizer *na verdade*, não *atualmente*. Para “atualmente” diz-se “currently” / “nowadays”.',
en: '“Actually” means *in fact*. For the Portuguese *atualmente*, English uses “currently”.',
},
eventually: {
native: '“Eventually” quer dizer *por fim, mais cedo ou mais tarde* — não *eventualmente*. Para isso: “possibly” ou “if necessary”.',
@@ -245,8 +254,8 @@ export const ptPT: Pack = {
en: '“Costume” is fancy dress; *costumes* are “customs”.',
},
deception: {
native: '“Deception” é *engano*. A *decepção* é “disappointment”.',
en: '“Deception” means being misled; *decepção* is “disappointment”.',
native: '“Deception” é *engano*. A *deceção* é “disappointment”.',
en: '“Deception” means being misled; *deceção* is “disappointment”.',
},
injury: {
native: '“Injury” é uma *lesão*. A *injúria* é “insult”.',