c2cd5e01e1
El SDK CI - dev / build-and-test (pull_request) Successful in 3m34s
elb.el: - Auto-detect Homebrew OpenSSL (-L$(brew --prefix openssl)/lib) so -lssl resolves on macOS without manual flags; no-op on Linux - Add -include elp-c-decls.h when present in out_dir: resolves undeclared cross-module calls in packages like ELP that lack explicit imports ELP source: - Add import "morphology.el" to all 29 language morphology modules - Add language module imports to morphology.el (all langs it dispatches to) These were missing since ELP was originally built as a monolithic unit
678 lines
25 KiB
EmacsLisp
678 lines
25 KiB
EmacsLisp
// morphology-fr.el - French morphology for the NLG engine.
|
||
//
|
||
// Implements fusional French verb conjugation, noun pluralization, gender
|
||
// inference, article agreement, and interrogative inversion. Designed as a
|
||
// companion to morphology.el and called by the engine when the language
|
||
// profile code is "fr".
|
||
//
|
||
// Verb tenses covered: present, future, imparfait, passé composé.
|
||
// Persons: first/second/third × singular/plural.
|
||
// Verb groups: -er (regular), -ir (regular finir-type), -re (regular vendre-type)
|
||
// + a core set of common irregular verbs.
|
||
//
|
||
// Liaison / elision notes:
|
||
// - le/la → l' before vowel-initial nouns (handled in fr_agree_article)
|
||
// - est-ce que can precede any statement for yes/no questions (fr_question_inversion)
|
||
// - Inversion inserts -t- between vowel-final verb form and 3s pronoun: parle-t-il
|
||
//
|
||
// Depends on: morphology.el (str_ends_with, str_slice, str_len helpers)
|
||
|
||
// ── String helpers (local, matching morphology.el conventions) ────────────────
|
||
|
||
import "morphology.el"
|
||
fn fr_str_ends(s: String, suf: String) -> Bool {
|
||
return str_ends_with(s, suf)
|
||
}
|
||
|
||
fn fr_str_drop_last(s: String, n: Int) -> String {
|
||
let len: Int = str_len(s)
|
||
if n >= len {
|
||
return ""
|
||
}
|
||
return str_slice(s, 0, len - n)
|
||
}
|
||
|
||
fn fr_str_last_char(s: String) -> String {
|
||
let n: Int = str_len(s)
|
||
if n == 0 {
|
||
return ""
|
||
}
|
||
return str_slice(s, n - 1, n)
|
||
}
|
||
|
||
fn fr_str_last2(s: String) -> String {
|
||
let n: Int = str_len(s)
|
||
if n < 2 {
|
||
return s
|
||
}
|
||
return str_slice(s, n - 2, n)
|
||
}
|
||
|
||
fn fr_is_vowel_start(s: String) -> Bool {
|
||
let n: Int = str_len(s)
|
||
if n == 0 {
|
||
return false
|
||
}
|
||
let c: String = str_slice(s, 0, 1)
|
||
if str_eq(c, "a") { return true }
|
||
if str_eq(c, "e") { return true }
|
||
if str_eq(c, "é") { return true }
|
||
if str_eq(c, "è") { return true }
|
||
if str_eq(c, "ê") { return true }
|
||
if str_eq(c, "i") { return true }
|
||
if str_eq(c, "î") { return true }
|
||
if str_eq(c, "o") { return true }
|
||
if str_eq(c, "ô") { return true }
|
||
if str_eq(c, "u") { return true }
|
||
if str_eq(c, "û") { return true }
|
||
if str_eq(c, "h") { return true }
|
||
return false
|
||
}
|
||
|
||
// ── Verb group detection ──────────────────────────────────────────────────────
|
||
//
|
||
// Returns "er" | "ir" | "re" | "irregular".
|
||
// Irregular detection is done by checking against a known list first; all other
|
||
// verbs are classified by ending.
|
||
|
||
fn fr_is_known_irregular(verb: String) -> Bool {
|
||
if str_eq(verb, "être") { return true }
|
||
if str_eq(verb, "avoir") { return true }
|
||
if str_eq(verb, "aller") { return true }
|
||
if str_eq(verb, "faire") { return true }
|
||
if str_eq(verb, "pouvoir") { return true }
|
||
if str_eq(verb, "vouloir") { return true }
|
||
if str_eq(verb, "venir") { return true }
|
||
if str_eq(verb, "dire") { return true }
|
||
if str_eq(verb, "voir") { return true }
|
||
if str_eq(verb, "prendre") { return true }
|
||
if str_eq(verb, "mettre") { return true }
|
||
if str_eq(verb, "savoir") { return true }
|
||
return false
|
||
}
|
||
|
||
fn fr_verb_group(base: String) -> String {
|
||
if fr_is_known_irregular(base) { return "irregular" }
|
||
if fr_str_ends(base, "er") { return "er" }
|
||
if fr_str_ends(base, "ir") { return "ir" }
|
||
if fr_str_ends(base, "re") { return "re" }
|
||
return "er"
|
||
}
|
||
|
||
fn fr_stem(base: String) -> String {
|
||
return fr_str_drop_last(base, 2)
|
||
}
|
||
|
||
// ── Person/number slot index ──────────────────────────────────────────────────
|
||
//
|
||
// 0 = 1s (je), 1 = 2s (tu), 2 = 3s (il/elle), 3 = 1p (nous), 4 = 2p (vous), 5 = 3p (ils/elles)
|
||
|
||
fn fr_slot(person: String, number: String) -> Int {
|
||
if str_eq(person, "first") {
|
||
if str_eq(number, "singular") { return 0 }
|
||
return 3
|
||
}
|
||
if str_eq(person, "second") {
|
||
if str_eq(number, "singular") { return 1 }
|
||
return 4
|
||
}
|
||
if str_eq(number, "singular") { return 2 }
|
||
return 5
|
||
}
|
||
|
||
// ── Irregular present tense ───────────────────────────────────────────────────
|
||
//
|
||
// être: suis, es, est, sommes, êtes, sont
|
||
// avoir: ai, as, a, avons, avez, ont
|
||
// aller: vais, vas, va, allons, allez, vont
|
||
// faire: fais, fais, fait, faisons, faites, font
|
||
// pouvoir: peux, peux, peut, pouvons, pouvez, peuvent
|
||
// vouloir: veux, veux, veut, voulons, voulez, veulent
|
||
// venir: viens, viens, vient, venons, venez, viennent
|
||
// dire: dis, dis, dit, disons, dites, disent
|
||
// voir: vois, vois, voit, voyons, voyez, voient
|
||
// prendre: prends, prends, prend, prenons, prenez, prennent
|
||
// mettre: mets, mets, met, mettons, mettez, mettent
|
||
// savoir: sais, sais, sait, savons, savez, savent
|
||
|
||
fn fr_irregular_present(verb: String, person: String, number: String) -> String {
|
||
let slot: Int = fr_slot(person, number)
|
||
|
||
if str_eq(verb, "être") {
|
||
if slot == 0 { return "suis" }
|
||
if slot == 1 { return "es" }
|
||
if slot == 2 { return "est" }
|
||
if slot == 3 { return "sommes" }
|
||
if slot == 4 { return "etes" }
|
||
return "sont"
|
||
}
|
||
|
||
// ASCII alias used by morph_map_canonical — identical conjugation.
|
||
if str_eq(verb, "etre") {
|
||
if slot == 0 { return "suis" }
|
||
if slot == 1 { return "es" }
|
||
if slot == 2 { return "est" }
|
||
if slot == 3 { return "sommes" }
|
||
if slot == 4 { return "etes" }
|
||
return "sont"
|
||
}
|
||
|
||
if str_eq(verb, "avoir") {
|
||
if slot == 0 { return "ai" }
|
||
if slot == 1 { return "as" }
|
||
if slot == 2 { return "a" }
|
||
if slot == 3 { return "avons" }
|
||
if slot == 4 { return "avez" }
|
||
return "ont"
|
||
}
|
||
|
||
if str_eq(verb, "aller") {
|
||
if slot == 0 { return "vais" }
|
||
if slot == 1 { return "vas" }
|
||
if slot == 2 { return "va" }
|
||
if slot == 3 { return "allons" }
|
||
if slot == 4 { return "allez" }
|
||
return "vont"
|
||
}
|
||
|
||
if str_eq(verb, "faire") {
|
||
if slot == 0 { return "fais" }
|
||
if slot == 1 { return "fais" }
|
||
if slot == 2 { return "fait" }
|
||
if slot == 3 { return "faisons" }
|
||
if slot == 4 { return "faites" }
|
||
return "font"
|
||
}
|
||
|
||
if str_eq(verb, "pouvoir") {
|
||
if slot == 0 { return "peux" }
|
||
if slot == 1 { return "peux" }
|
||
if slot == 2 { return "peut" }
|
||
if slot == 3 { return "pouvons" }
|
||
if slot == 4 { return "pouvez" }
|
||
return "peuvent"
|
||
}
|
||
|
||
if str_eq(verb, "vouloir") {
|
||
if slot == 0 { return "veux" }
|
||
if slot == 1 { return "veux" }
|
||
if slot == 2 { return "veut" }
|
||
if slot == 3 { return "voulons" }
|
||
if slot == 4 { return "voulez" }
|
||
return "veulent"
|
||
}
|
||
|
||
if str_eq(verb, "venir") {
|
||
if slot == 0 { return "viens" }
|
||
if slot == 1 { return "viens" }
|
||
if slot == 2 { return "vient" }
|
||
if slot == 3 { return "venons" }
|
||
if slot == 4 { return "venez" }
|
||
return "viennent"
|
||
}
|
||
|
||
if str_eq(verb, "dire") {
|
||
if slot == 0 { return "dis" }
|
||
if slot == 1 { return "dis" }
|
||
if slot == 2 { return "dit" }
|
||
if slot == 3 { return "disons" }
|
||
if slot == 4 { return "dites" }
|
||
return "disent"
|
||
}
|
||
|
||
if str_eq(verb, "voir") {
|
||
if slot == 0 { return "vois" }
|
||
if slot == 1 { return "vois" }
|
||
if slot == 2 { return "voit" }
|
||
if slot == 3 { return "voyons" }
|
||
if slot == 4 { return "voyez" }
|
||
return "voient"
|
||
}
|
||
|
||
if str_eq(verb, "prendre") {
|
||
if slot == 0 { return "prends" }
|
||
if slot == 1 { return "prends" }
|
||
if slot == 2 { return "prend" }
|
||
if slot == 3 { return "prenons" }
|
||
if slot == 4 { return "prenez" }
|
||
return "prennent"
|
||
}
|
||
|
||
if str_eq(verb, "mettre") {
|
||
if slot == 0 { return "mets" }
|
||
if slot == 1 { return "mets" }
|
||
if slot == 2 { return "met" }
|
||
if slot == 3 { return "mettons" }
|
||
if slot == 4 { return "mettez" }
|
||
return "mettent"
|
||
}
|
||
|
||
if str_eq(verb, "savoir") {
|
||
if slot == 0 { return "sais" }
|
||
if slot == 1 { return "sais" }
|
||
if slot == 2 { return "sait" }
|
||
if slot == 3 { return "savons" }
|
||
if slot == 4 { return "savez" }
|
||
return "savent"
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
// ── Regular present tense ─────────────────────────────────────────────────────
|
||
//
|
||
// -er: -e, -es, -e, -ons, -ez, -ent
|
||
// -ir: -is, -is, -it, -issons, -issez, -issent (finir-type; stem gets -iss- in plural)
|
||
// -re: -s, -s, -(nothing), -ons, -ez, -ent
|
||
|
||
fn fr_regular_present(stem: String, vgroup: String, slot: Int) -> String {
|
||
if str_eq(vgroup, "er") {
|
||
if slot == 0 { return stem + "e" }
|
||
if slot == 1 { return stem + "es" }
|
||
if slot == 2 { return stem + "e" }
|
||
if slot == 3 { return stem + "ons" }
|
||
if slot == 4 { return stem + "ez" }
|
||
return stem + "ent"
|
||
}
|
||
|
||
if str_eq(vgroup, "ir") {
|
||
// finir-type: singular uses bare stem, plural uses stem + -iss-
|
||
if slot == 0 { return stem + "is" }
|
||
if slot == 1 { return stem + "is" }
|
||
if slot == 2 { return stem + "it" }
|
||
if slot == 3 { return stem + "issons" }
|
||
if slot == 4 { return stem + "issez" }
|
||
return stem + "issent"
|
||
}
|
||
|
||
// -re (vendre-type)
|
||
if slot == 0 { return stem + "s" }
|
||
if slot == 1 { return stem + "s" }
|
||
if slot == 2 { return stem }
|
||
if slot == 3 { return stem + "ons" }
|
||
if slot == 4 { return stem + "ez" }
|
||
return stem + "ent"
|
||
}
|
||
|
||
// ── Regular future tense ──────────────────────────────────────────────────────
|
||
//
|
||
// Future is formed from the infinitive (minus silent -e for -re verbs) + endings:
|
||
// -ai, -as, -a, -ons, -ez, -ont
|
||
|
||
fn fr_future_stem(base: String, vgroup: String) -> String {
|
||
// -re verbs drop the final -e before adding future endings
|
||
if str_eq(vgroup, "re") {
|
||
return fr_str_drop_last(base, 1)
|
||
}
|
||
return base
|
||
}
|
||
|
||
fn fr_regular_future(fstem: String, slot: Int) -> String {
|
||
if slot == 0 { return fstem + "ai" }
|
||
if slot == 1 { return fstem + "as" }
|
||
if slot == 2 { return fstem + "a" }
|
||
if slot == 3 { return fstem + "ons" }
|
||
if slot == 4 { return fstem + "ez" }
|
||
return fstem + "ont"
|
||
}
|
||
|
||
// ── Irregular future stems ────────────────────────────────────────────────────
|
||
//
|
||
// Returns the irregular future stem, or "" if regular.
|
||
|
||
fn fr_irregular_future_stem(verb: String) -> String {
|
||
if str_eq(verb, "être") { return "ser" }
|
||
if str_eq(verb, "avoir") { return "aur" }
|
||
if str_eq(verb, "aller") { return "ir" }
|
||
if str_eq(verb, "faire") { return "fer" }
|
||
if str_eq(verb, "pouvoir") { return "pourr" }
|
||
if str_eq(verb, "vouloir") { return "voudr" }
|
||
if str_eq(verb, "venir") { return "viendr" }
|
||
if str_eq(verb, "voir") { return "verr" }
|
||
if str_eq(verb, "savoir") { return "saur" }
|
||
return ""
|
||
}
|
||
|
||
// ── Regular imparfait ─────────────────────────────────────────────────────────
|
||
//
|
||
// Imparfait is formed from the nous-present stem (infinitive minus -er/-ir/-re,
|
||
// then add -iss for -ir verbs in nous-form) + endings:
|
||
// -ais, -ais, -ait, -ions, -iez, -aient
|
||
//
|
||
// For -er verbs: stem = infinitive minus -er
|
||
// For -ir verbs: stem = infinitive minus -ir (bare stem, not -iss- — imparfait
|
||
// uses the basic stem, unlike present plural which uses -iss-)
|
||
// For -re verbs: stem = infinitive minus -re
|
||
// Exception: être uses ét- as the imparfait stem.
|
||
|
||
fn fr_imperfect_stem(base: String, vgroup: String) -> String {
|
||
if str_eq(base, "être") { return "ét" }
|
||
return fr_stem(base)
|
||
}
|
||
|
||
fn fr_regular_imperfect(istem: String, slot: Int) -> String {
|
||
if slot == 0 { return istem + "ais" }
|
||
if slot == 1 { return istem + "ais" }
|
||
if slot == 2 { return istem + "ait" }
|
||
if slot == 3 { return istem + "ions" }
|
||
if slot == 4 { return istem + "iez" }
|
||
return istem + "aient"
|
||
}
|
||
|
||
// ── Passé composé (past compound) ────────────────────────────────────────────
|
||
//
|
||
// Passé composé = auxiliary (avoir or être) + past participle.
|
||
// Most verbs use avoir; a core set of motion/state verbs use être.
|
||
//
|
||
// This function returns a two-word string "auxiliary participle".
|
||
// The caller is responsible for agreement of the past participle when être is
|
||
// used (feminine adds -e, plural adds -s); this function returns the masculine
|
||
// singular participle unconditionally.
|
||
|
||
fn fr_uses_etre(verb: String) -> Bool {
|
||
if str_eq(verb, "aller") { return true }
|
||
if str_eq(verb, "venir") { return true }
|
||
if str_eq(verb, "partir") { return true }
|
||
if str_eq(verb, "arriver") { return true }
|
||
if str_eq(verb, "entrer") { return true }
|
||
if str_eq(verb, "sortir") { return true }
|
||
if str_eq(verb, "naître") { return true }
|
||
if str_eq(verb, "mourir") { return true }
|
||
if str_eq(verb, "rester") { return true }
|
||
if str_eq(verb, "tomber") { return true }
|
||
if str_eq(verb, "monter") { return true }
|
||
if str_eq(verb, "descendre") { return true }
|
||
if str_eq(verb, "rentrer") { return true }
|
||
if str_eq(verb, "retourner") { return true }
|
||
if str_eq(verb, "passer") { return true }
|
||
return false
|
||
}
|
||
|
||
// Returns the past participle (masculine singular form).
|
||
fn fr_past_participle(verb: String) -> String {
|
||
// Irregular participles
|
||
if str_eq(verb, "être") { return "été" }
|
||
if str_eq(verb, "avoir") { return "eu" }
|
||
if str_eq(verb, "aller") { return "allé" }
|
||
if str_eq(verb, "faire") { return "fait" }
|
||
if str_eq(verb, "pouvoir") { return "pu" }
|
||
if str_eq(verb, "vouloir") { return "voulu" }
|
||
if str_eq(verb, "venir") { return "venu" }
|
||
if str_eq(verb, "dire") { return "dit" }
|
||
if str_eq(verb, "voir") { return "vu" }
|
||
if str_eq(verb, "prendre") { return "pris" }
|
||
if str_eq(verb, "mettre") { return "mis" }
|
||
if str_eq(verb, "savoir") { return "su" }
|
||
if str_eq(verb, "naître") { return "né" }
|
||
if str_eq(verb, "mourir") { return "mort" }
|
||
// Regular participles by group
|
||
let vgroup: String = fr_verb_group(verb)
|
||
if str_eq(vgroup, "er") {
|
||
return fr_str_drop_last(verb, 2) + "é"
|
||
}
|
||
if str_eq(vgroup, "ir") {
|
||
return fr_str_drop_last(verb, 2) + "i"
|
||
}
|
||
// -re verbs: drop -re, add -u
|
||
return fr_str_drop_last(verb, 2) + "u"
|
||
}
|
||
|
||
// Conjugates the avoir auxiliary in the present (for passé composé).
|
||
fn fr_avoir_present(slot: Int) -> String {
|
||
if slot == 0 { return "ai" }
|
||
if slot == 1 { return "as" }
|
||
if slot == 2 { return "a" }
|
||
if slot == 3 { return "avons" }
|
||
if slot == 4 { return "avez" }
|
||
return "ont"
|
||
}
|
||
|
||
// Conjugates the être auxiliary in the present (for passé composé).
|
||
fn fr_etre_present(slot: Int) -> String {
|
||
if slot == 0 { return "suis" }
|
||
if slot == 1 { return "es" }
|
||
if slot == 2 { return "est" }
|
||
if slot == 3 { return "sommes" }
|
||
if slot == 4 { return "êtes" }
|
||
return "sont"
|
||
}
|
||
|
||
// ── Full conjugation entry point ──────────────────────────────────────────────
|
||
//
|
||
// fr_conjugate: conjugate a French verb.
|
||
//
|
||
// verb: French infinitive (e.g. "parler", "être", "venir")
|
||
// tense: "present" | "future" | "imperfect" | "past"
|
||
// (note: "past" returns the passé composé as "aux participle")
|
||
// person: "first" | "second" | "third"
|
||
// number: "singular" | "plural"
|
||
|
||
fn fr_conjugate(verb: String, tense: String, person: String, number: String) -> String {
|
||
let slot: Int = fr_slot(person, number)
|
||
|
||
if str_eq(tense, "present") {
|
||
let irreg: String = fr_irregular_present(verb, person, number)
|
||
if !str_eq(irreg, "") {
|
||
return irreg
|
||
}
|
||
let vgroup: String = fr_verb_group(verb)
|
||
let stem: String = fr_stem(verb)
|
||
return fr_regular_present(stem, vgroup, slot)
|
||
}
|
||
|
||
if str_eq(tense, "future") {
|
||
let irreg_stem: String = fr_irregular_future_stem(verb)
|
||
if !str_eq(irreg_stem, "") {
|
||
return fr_regular_future(irreg_stem, slot)
|
||
}
|
||
let vgroup: String = fr_verb_group(verb)
|
||
let fstem: String = fr_future_stem(verb, vgroup)
|
||
return fr_regular_future(fstem, slot)
|
||
}
|
||
|
||
if str_eq(tense, "imperfect") {
|
||
let vgroup: String = fr_verb_group(verb)
|
||
let istem: String = fr_imperfect_stem(verb, vgroup)
|
||
return fr_regular_imperfect(istem, slot)
|
||
}
|
||
|
||
if str_eq(tense, "past") {
|
||
// Passé composé: auxiliary + past participle
|
||
let pp: String = fr_past_participle(verb)
|
||
if fr_uses_etre(verb) {
|
||
let aux: String = fr_etre_present(slot)
|
||
return aux + " " + pp
|
||
}
|
||
let aux: String = fr_avoir_present(slot)
|
||
return aux + " " + pp
|
||
}
|
||
|
||
// Unknown tense: return infinitive unchanged
|
||
return verb
|
||
}
|
||
|
||
// ── Noun gender inference ─────────────────────────────────────────────────────
|
||
//
|
||
// Returns "m" (masculine), "f" (feminine), or "unknown".
|
||
//
|
||
// Heuristics (common French patterns):
|
||
// ends in -tion/-sion/-xion -> feminine (nation, passion, connexion)
|
||
// ends in -ure -> feminine (voiture, culture)
|
||
// ends in -ette -> feminine (omelette, cigarette)
|
||
// ends in -eur (abstract) -> feminine (couleur, peur, chaleur)
|
||
// ends in -eur (agent) -> masculine (acteur, serveur) — can't always distinguish
|
||
// ends in -eur (no rule) -> try -teur → masculine (docteur, auteur)
|
||
// ends in -ment -> masculine (sentiment, gouvernement)
|
||
// ends in -age -> masculine (voyage, fromage)
|
||
// ends in -isme -> masculine (socialisme)
|
||
// ends in -eau -> masculine (tableau, gâteau)
|
||
// ends in -er/-é -> masculine (boucher, café)
|
||
// ends in -ée -> feminine (journée, idée)
|
||
// ends in -ie -> feminine (philosophie, géographie)
|
||
// ends in -ance/-ence -> feminine (chance, science)
|
||
// ends in -té/-tié -> feminine (beauté, amitié)
|
||
// ends in -ude -> feminine (attitude, solitude)
|
||
// ends in -ade -> feminine (salade, promenade)
|
||
// ends in -ette -> feminine (already covered)
|
||
// ends in -e (generic) -> often feminine but not reliable; return "unknown"
|
||
|
||
fn fr_gender(noun: String) -> String {
|
||
// Feminine patterns (check more specific before general)
|
||
if fr_str_ends(noun, "tion") { return "f" }
|
||
if fr_str_ends(noun, "sion") { return "f" }
|
||
if fr_str_ends(noun, "xion") { return "f" }
|
||
if fr_str_ends(noun, "ure") { return "f" }
|
||
if fr_str_ends(noun, "ette") { return "f" }
|
||
if fr_str_ends(noun, "ance") { return "f" }
|
||
if fr_str_ends(noun, "ence") { return "f" }
|
||
if fr_str_ends(noun, "ité") { return "f" }
|
||
if fr_str_ends(noun, "té") { return "f" }
|
||
if fr_str_ends(noun, "tié") { return "f" }
|
||
if fr_str_ends(noun, "ude") { return "f" }
|
||
if fr_str_ends(noun, "ade") { return "f" }
|
||
if fr_str_ends(noun, "ée") { return "f" }
|
||
if fr_str_ends(noun, "ie") { return "f" }
|
||
// Masculine patterns
|
||
if fr_str_ends(noun, "ment") { return "m" }
|
||
if fr_str_ends(noun, "age") { return "m" }
|
||
if fr_str_ends(noun, "isme") { return "m" }
|
||
if fr_str_ends(noun, "eau") { return "m" }
|
||
if fr_str_ends(noun, "eur") { return "m" }
|
||
if fr_str_ends(noun, "er") { return "m" }
|
||
if fr_str_ends(noun, "é") { return "m" }
|
||
return "unknown"
|
||
}
|
||
|
||
// ── Noun pluralization ────────────────────────────────────────────────────────
|
||
//
|
||
// French plural rules:
|
||
// already ends in -s, -x, or -z -> unchanged
|
||
// ends in -eau -> add -x (bateau → bateaux)
|
||
// ends in -eu -> add -x (jeu → jeux; bleu → bleus is exception)
|
||
// ends in -al -> replace -al with -aux (animal → animaux)
|
||
// ends in -ail (most) -> replace -ail with -aux (travail → travaux; bail)
|
||
// otherwise -> add -s
|
||
|
||
fn fr_invariant_plural(noun: String) -> String {
|
||
// Words already ending in -s, -x, -z are unchanged in plural
|
||
let last: String = fr_str_last_char(noun)
|
||
if str_eq(last, "s") { return noun }
|
||
if str_eq(last, "x") { return noun }
|
||
if str_eq(last, "z") { return noun }
|
||
return ""
|
||
}
|
||
|
||
fn fr_pluralize(noun: String) -> String {
|
||
let inv: String = fr_invariant_plural(noun)
|
||
if !str_eq(inv, "") {
|
||
return inv
|
||
}
|
||
if fr_str_ends(noun, "eau") {
|
||
return noun + "x"
|
||
}
|
||
if fr_str_ends(noun, "eu") {
|
||
return noun + "x"
|
||
}
|
||
if fr_str_ends(noun, "al") {
|
||
return fr_str_drop_last(noun, 2) + "aux"
|
||
}
|
||
if fr_str_ends(noun, "ail") {
|
||
return fr_str_drop_last(noun, 3) + "aux"
|
||
}
|
||
return noun + "s"
|
||
}
|
||
|
||
// ── Article agreement ─────────────────────────────────────────────────────────
|
||
//
|
||
// fr_agree_article: return the correct French article for a noun.
|
||
//
|
||
// noun: the noun (used for gender inference and elision check)
|
||
// definite: "true" for definite (le/la/l'/les), "false" for indefinite (un/une/des)
|
||
// number: "singular" | "plural"
|
||
//
|
||
// Elision: le/la → l' before a vowel- or h-initial noun (handled here).
|
||
|
||
fn fr_agree_article(noun: String, definite: String, number: String) -> String {
|
||
let gender: String = fr_gender(noun)
|
||
let is_plural: Bool = str_eq(number, "plural")
|
||
let is_def: Bool = str_eq(definite, "true")
|
||
let vowel_start: Bool = fr_is_vowel_start(noun)
|
||
|
||
if is_def {
|
||
if is_plural {
|
||
return "les"
|
||
}
|
||
// singular
|
||
if vowel_start {
|
||
return "l'"
|
||
}
|
||
if str_eq(gender, "f") {
|
||
return "la"
|
||
}
|
||
return "le"
|
||
}
|
||
|
||
// indefinite
|
||
if is_plural {
|
||
return "des"
|
||
}
|
||
if str_eq(gender, "f") {
|
||
return "une"
|
||
}
|
||
return "un"
|
||
}
|
||
|
||
// ── Question inversion ────────────────────────────────────────────────────────
|
||
//
|
||
// fr_question_inversion: form a yes/no question using subject-verb inversion.
|
||
//
|
||
// subject: pronoun string: "je" | "tu" | "il" | "elle" | "nous" | "vous" | "ils" | "elles"
|
||
// verb_form: the conjugated verb form (e.g. "parle", "mange", "est")
|
||
//
|
||
// Rules:
|
||
// - Verb and subject are joined with a hyphen: "parle-t-il ?"
|
||
// - When the verb form ends in a vowel and the subject starts with a vowel
|
||
// (il, elle, ils, elles), insert euphonic -t-: "parle-t-il ?"
|
||
// - Je inversion is archaic; "est-ce que je ...?" is preferred — this function
|
||
// generates "est-ce que je <verb_form> ?" for first-person singular.
|
||
// - The result ends with " ?"
|
||
|
||
fn fr_subject_starts_vowel(subject: String) -> Bool {
|
||
if str_eq(subject, "il") { return true }
|
||
if str_eq(subject, "elle") { return true }
|
||
if str_eq(subject, "ils") { return true }
|
||
if str_eq(subject, "elles") { return true }
|
||
return false
|
||
}
|
||
|
||
fn fr_verb_ends_vowel(verb_form: String) -> Bool {
|
||
let last: String = fr_str_last_char(verb_form)
|
||
if str_eq(last, "a") { return true }
|
||
if str_eq(last, "e") { return true }
|
||
if str_eq(last, "é") { return true }
|
||
if str_eq(last, "i") { return true }
|
||
if str_eq(last, "o") { return true }
|
||
if str_eq(last, "u") { return true }
|
||
return false
|
||
}
|
||
|
||
fn fr_question_inversion(subject: String, verb_form: String) -> String {
|
||
// First-person singular: use est-ce que construction
|
||
if str_eq(subject, "je") {
|
||
return "est-ce que je " + verb_form + " ?"
|
||
}
|
||
|
||
// Determine whether to insert -t-
|
||
let need_t: Bool = false
|
||
if fr_verb_ends_vowel(verb_form) {
|
||
if fr_subject_starts_vowel(subject) {
|
||
let need_t = true
|
||
}
|
||
}
|
||
|
||
if need_t {
|
||
return verb_form + "-t-" + subject + " ?"
|
||
}
|
||
return verb_form + "-" + subject + " ?"
|
||
}
|