();
_checkLines.Clear();
@@ -513,7 +578,7 @@ private void UpdateSummary()
var review = _proposals.Count(p => p.HasReview);
_summaryLabel.Text = $"{selected} of {total} change(s) selected." + (review > 0 ? $" {review} to review." : string.Empty) +
$" Showing {_proposals.Count(p => p.VisibleInReview)} of {_proposals.Count} row(s).";
- _applyButton.IsEnabled = selected > 0;
+ _applyButton.IsEnabled = selected > 0 || NameSettingsChanged;
}
private void OnFindingFilterChanged(object? sender, RoutedEventArgs e)
@@ -690,7 +755,7 @@ or PlatformNotSupportedException or System.ComponentModel.Win32Exception or Inva
private void UpdateSummaryOrEmpty()
{
if (_proposals.Count > 0) UpdateSummary();
- else _applyButton.IsEnabled = false;
+ else _applyButton.IsEnabled = NameSettingsChanged;
}
private void OnCancelReference(object? sender, RoutedEventArgs e) => _referenceCancel?.Cancel();
@@ -850,6 +915,7 @@ await RunReferenceJob(async (progress, token) =>
protected override void OnClosed(EventArgs e)
{
+ _closed = true;
_referenceCancel?.Cancel();
EmbeddingRuntime.KillAll();
base.OnClosed(e);
@@ -878,7 +944,8 @@ private void OnApply(object? sender, RoutedEventArgs e)
if (changesByLine.Count == 0)
{
- App.Response = new PluginResponse { Status = PluginStatus.Cancelled };
+ App.Response = NameSettingsChanged ? BatchMode.BuildConfigureResponse(SavedOptions) :
+ new PluginResponse { Status = PluginStatus.Cancelled };
Close();
return;
}
diff --git a/se5/BrazilianToEuropeanPortuguese/MaybeSubjunctive.cs b/se5/BrazilianToEuropeanPortuguese/MaybeSubjunctive.cs
new file mode 100644
index 00000000..4719083b
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/MaybeSubjunctive.cs
@@ -0,0 +1,354 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+///
+/// "Talvez" before the verb takes the subjunctive, and the subjunctive attracts the clitic:
+/// "talvez ele esqueceu-se" → "talvez ele se tenha esquecido", "talvez é" → "talvez seja",
+/// "talvez ela estava" → "talvez ela estivesse". A verb before "talvez" ("Esqueceu-se, talvez") is left alone.
+///
+internal static class MaybeSubjunctive
+{
+ private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+
+ private static readonly Regex Maybe = new(
+ @"(?(?eu|tu|ele|ela|nós|vós|eles|elas|você|vocês|" +
+ @"(?:o|a|os|as)\s+(?:(?:meu|minha|meus|minhas|teu|tua|teus|tuas|seu|sua|seus|suas|nosso|nossa|nossos|nossas)\s+)?[\p{L}\p{M}]+|" +
+ @"(?-i:\p{Lu})[\p{L}\p{M}]+)\s+)?" +
+ @"(?(?:(?:não|nunca|já|ainda|também|só|mesmo|realmente)\s+){0,2}))" +
+ @"(?:(?me|te|se|nos|vos|lhe|lhes)\s+)?" +
+ @"(?[\p{L}\p{M}]+)(?:-(?me|te|se|nos|vos|lhe|lhes|o|a|os|as))?(?![\p{L}\p{M}-])" +
+ @"(?=(?\s+[\p{L}\p{M}]+)?)", Ci);
+
+ // Present and imperfect indicative of irregular verbs → subjunctive.
+ private static readonly Dictionary Irregular = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["é"] = "seja", ["sou"] = "seja", ["és"] = "sejas", ["somos"] = "sejamos", ["são"] = "sejam",
+ ["era"] = "fosse", ["eras"] = "fosses", ["éramos"] = "fôssemos", ["eram"] = "fossem",
+ ["está"] = "esteja", ["estou"] = "esteja", ["estás"] = "estejas", ["estamos"] = "estejamos", ["estão"] = "estejam",
+ ["estava"] = "estivesse", ["estavas"] = "estivesses", ["estávamos"] = "estivéssemos", ["estavam"] = "estivessem",
+ ["tem"] = "tenha", ["tenho"] = "tenha", ["tens"] = "tenhas", ["temos"] = "tenhamos", ["têm"] = "tenham",
+ ["tinha"] = "tivesse", ["tinhas"] = "tivesses", ["tínhamos"] = "tivéssemos", ["tinham"] = "tivessem",
+ ["vai"] = "vá", ["vou"] = "vá", ["vais"] = "vás",
+ ["ia"] = "fosse", ["ias"] = "fosses", ["íamos"] = "fôssemos", ["iam"] = "fossem",
+ ["há"] = "haja", ["havia"] = "houvesse",
+ ["pode"] = "possa", ["posso"] = "possa", ["podes"] = "possas", ["podemos"] = "possamos", ["podem"] = "possam",
+ ["podia"] = "pudesse", ["podias"] = "pudesses", ["podíamos"] = "pudéssemos", ["podiam"] = "pudessem",
+ ["sabe"] = "saiba", ["sei"] = "saiba", ["sabes"] = "saibas", ["sabemos"] = "saibamos", ["sabem"] = "saibam",
+ ["sabia"] = "soubesse", ["sabias"] = "soubesses", ["sabíamos"] = "soubéssemos", ["sabiam"] = "soubessem",
+ ["quer"] = "queira", ["quero"] = "queira", ["queres"] = "queiras", ["queremos"] = "queiramos", ["querem"] = "queiram",
+ ["queria"] = "quisesse", ["querias"] = "quisesses", ["queríamos"] = "quiséssemos", ["queriam"] = "quisessem",
+ ["faz"] = "faça", ["faço"] = "faça", ["fazes"] = "faças", ["fazemos"] = "façamos", ["fazem"] = "façam",
+ ["fazia"] = "fizesse", ["fazias"] = "fizesses", ["fazíamos"] = "fizéssemos", ["faziam"] = "fizessem",
+ ["diz"] = "diga", ["digo"] = "diga", ["dizes"] = "digas", ["dizemos"] = "digamos", ["dizem"] = "digam",
+ ["dizia"] = "dissesse", ["dizias"] = "dissesses", ["dizíamos"] = "disséssemos", ["diziam"] = "dissessem",
+ ["vê"] = "veja", ["vejo"] = "veja", ["vês"] = "vejas", ["vemos"] = "vejamos", ["veem"] = "vejam", ["vêem"] = "vejam",
+ ["vem"] = "venha", ["venho"] = "venha", ["vens"] = "venhas", ["vêm"] = "venham",
+ ["dá"] = "dê", ["dou"] = "dê", ["dás"] = "dês", ["damos"] = "demos", ["dão"] = "deem",
+ ["põe"] = "ponha", ["ponho"] = "ponha", ["pões"] = "ponhas", ["pomos"] = "ponhamos", ["põem"] = "ponham",
+ ["punha"] = "pusesse", ["punham"] = "pusessem",
+ ["traz"] = "traga", ["trago"] = "traga", ["trazes"] = "tragas", ["trazem"] = "tragam",
+ ["trazia"] = "trouxesse", ["traziam"] = "trouxessem",
+ ["sai"] = "saia", ["saio"] = "saia", ["sais"] = "saias", ["saem"] = "saiam",
+ ["ouve"] = "ouça", ["ouço"] = "ouça", ["ouves"] = "ouças", ["ouvem"] = "ouçam",
+ ["pede"] = "peça", ["peço"] = "peça", ["pedes"] = "peças", ["pedem"] = "peçam",
+ ["perde"] = "perca", ["perco"] = "perca", ["perdes"] = "percas", ["perdem"] = "percam",
+ ["consegue"] = "consiga", ["consegues"] = "consigas", ["conseguem"] = "consigam",
+ ["sente"] = "sinta", ["sinto"] = "sinta", ["sentes"] = "sintas", ["sentem"] = "sintam",
+ ["segue"] = "siga", ["sigo"] = "siga", ["segues"] = "sigas", ["seguem"] = "sigam",
+ ["prefere"] = "prefira", ["prefiro"] = "prefira", ["preferes"] = "prefiras", ["preferem"] = "prefiram",
+ ["lê"] = "leia", ["leio"] = "leia", ["lês"] = "leias", ["leem"] = "leiam", ["lêem"] = "leiam",
+ ["cabe"] = "caiba", ["vale"] = "valha", ["valem"] = "valham", ["dorme"] = "durma", ["dormem"] = "durmam",
+ ["mente"] = "minta", ["mentem"] = "mintam", ["foge"] = "fuja", ["fogem"] = "fujam", ["sobe"] = "suba", ["sobem"] = "subam",
+ ["acredita"] = "acredite", ["acredito"] = "acredite",
+ };
+
+ // Irregular past forms → person index (0 eu/ele, 1 tu, 2 nós, 3 eles) and participle used with "tenha".
+ private static readonly Dictionary IrregularPast = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["fui"] = (0, "?"), ["foste"] = (1, "?"), ["foi"] = (0, "?"), ["fomos"] = (2, "?"), ["foram"] = (3, "?"),
+ ["estive"] = (0, "estado"), ["estiveste"] = (1, "estado"), ["esteve"] = (0, "estado"), ["estivemos"] = (2, "estado"), ["estiveram"] = (3, "estado"),
+ ["tive"] = (0, "tido"), ["tiveste"] = (1, "tido"), ["teve"] = (0, "tido"), ["tivemos"] = (2, "tido"), ["tiveram"] = (3, "tido"),
+ ["fiz"] = (0, "feito"), ["fizeste"] = (1, "feito"), ["fez"] = (0, "feito"), ["fizemos"] = (2, "feito"), ["fizeram"] = (3, "feito"),
+ ["disse"] = (0, "dito"), ["disseste"] = (1, "dito"), ["dissemos"] = (2, "dito"), ["disseram"] = (3, "dito"),
+ ["vi"] = (0, "visto"), ["viste"] = (1, "visto"), ["viu"] = (0, "visto"), ["vimos"] = (2, "visto"), ["viram"] = (3, "visto"),
+ ["vim"] = (0, "vindo"), ["vieste"] = (1, "vindo"), ["veio"] = (0, "vindo"), ["viemos"] = (2, "vindo"), ["vieram"] = (3, "vindo"),
+ ["pus"] = (0, "posto"), ["puseste"] = (1, "posto"), ["pôs"] = (0, "posto"), ["pusemos"] = (2, "posto"), ["puseram"] = (3, "posto"),
+ ["dei"] = (0, "dado"), ["deste"] = (1, "dado"), ["deu"] = (0, "dado"), ["demos"] = (2, "dado"), ["deram"] = (3, "dado"),
+ ["soube"] = (0, "sabido"), ["soubeste"] = (1, "sabido"), ["soubemos"] = (2, "sabido"), ["souberam"] = (3, "sabido"),
+ ["pude"] = (0, "podido"), ["pôde"] = (0, "podido"), ["puderam"] = (3, "podido"),
+ ["quis"] = (0, "querido"), ["quiseste"] = (1, "querido"), ["quiseram"] = (3, "querido"),
+ ["trouxe"] = (0, "trazido"), ["trouxeste"] = (1, "trazido"), ["trouxeram"] = (3, "trazido"),
+ ["houve"] = (0, "havido"),
+ };
+
+ private static readonly Dictionary IrregularParticiples = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["abrir"] = "aberto", ["escrever"] = "escrito", ["cobrir"] = "coberto", ["descobrir"] = "descoberto",
+ ["pagar"] = "pago", ["ganhar"] = "ganho", ["dizer"] = "dito", ["fazer"] = "feito", ["ver"] = "visto",
+ ["vir"] = "vindo", ["pôr"] = "posto",
+ };
+
+ // After "foi" these complements mean movement ("tenha ido embora"); otherwise "foi" is "ser" ("tenha sido").
+ private static readonly Regex Movement = new(@"^\s+(?:embora|lá|ali|aí|para|pra|até|ao|à|aos|às|daqui|dali)$", Ci);
+
+ private static readonly string[] Have = { "tenha", "tenhas", "tenhamos", "tenham" };
+
+ private const string SubjunctiveAuxiliary =
+ "tenha|tenhas|tenhamos|tenham|tivesse|tivesses|tivéssemos|tivessem|seja|sejas|sejamos|sejam|esteja|estejas|estejamos|estejam";
+
+ // Inside a clause the subjunctive is never enclitic: "talvez ele tenha-lhe ligado" → "talvez ele lhe tenha ligado".
+ private static readonly Regex EncliticSubjunctive = new(
+ @"(?[\p{L}\p{M}]+[ \t]+)(?" + SubjunctiveAuxiliary + @")-(?me|te|se|nos|vos|lhe|lhes|o|a|os|as)(?![\p{L}\p{M}-])", Ci);
+
+ // "esquecer-se de" / "lembrar-se de" keep the pronoun in compound subjunctives: "que ele tenha esquecido de" →
+ // "que ele se tenha esquecido de"; a direct object ("tenha esquecido o nome") is kept.
+ private static readonly Regex ReflexiveCompound = new(
+ @"(?(?eu|tu|ele|ela|nós|eles|elas|[\p{L}\p{M}]+)\s+(?(?:(?:não|nunca|já|também|só)\s+){0,2}))" +
+ @"(?tenha|tenhas|tenhamos|tenham|tivesse|tivesses|tivéssemos|tivessem)\s+(?esquecid[oa]s?|lembrad[oa]s?)" +
+ @"(?=\s+(?:de|do|da|dos|das|disso|disto|daquilo|dele|dela|deles|delas|dum|duma)(?![\p{L}\p{M}])|\s+(?:tudo\s+)?mal(?![\p{L}\p{M}])|\s*[.!?…,;]|\s*$)", Ci);
+
+ // "talvez há dois anos" is "maybe two years ago", not existential "haver".
+ private static readonly Regex TimeAgo = new(
+ @"^\s+(?:\d+|um|uma|uns|umas|dois|duas|três|quatro|cinco|seis|sete|oito|nove|dez|vinte|trinta|cem|muito|muitos|muitas|pouco|poucos|poucas|alguns|algumas|bastante|mais|menos)" +
+ @"(?:\s+[\p{L}\p{M}]+)?\s+(?:segundos?|minutos?|horas?|dias?|semanas?|meses|mês|anos?|décadas?|tempo)(?![\p{L}\p{M}])", Ci);
+
+ // A relative "que" whose antecedent is the object: "Algo que tenhas esquecido" is not reflexive.
+ private static readonly Regex ObjectAntecedent = new(
+ @"(? NotClauseHosts = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "me", "te", "se", "nos", "vos", "lhe", "lhes", "o", "a", "os", "as",
+ };
+
+ // The same verbs in indicative/conditional compound tenses: "ele tinha esquecido de" → "ele tinha-se esquecido de",
+ // "que ele tinha esquecido de" → "que ele se tinha esquecido de", "teria esquecido" → "ter-se-ia esquecido".
+ private static readonly Regex ReflexiveIndicative = new(
+ @"(?tenho|tens|tem|temos|têm|tinha|tinhas|tínhamos|tinham|havia|havias|havíamos|haviam|" +
+ @"teria|terias|teríamos|teriam|terei|terás|terá|teremos|terão)\s+(?esquecid[oa]s?|lembrad[oa]s?)" +
+ @"(?=\s+(?:de|do|da|dos|das|disso|disto|daquilo|dele|dela|deles|delas|dum|duma)(?![\p{L}\p{M}])|\s+(?:tudo\s+)?mal(?![\p{L}\p{M}])|\s*[.!?…,;]|\s*$)", Ci);
+
+ private static readonly Regex IndicativeClause = new(@"(?:[^.!?…;,\n]|\n(?![-–—]))*$", RegexOptions.CultureInvariant);
+
+ private static readonly HashSet ProclisisTriggers = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "não", "nunca", "jamais", "já", "também", "ainda", "sempre", "só", "nem", "apenas", "talvez", "que", "quem", "ninguém",
+ "alguém", "tudo", "nada", "bem", "quando", "onde", "como", "porque", "enquanto", "embora", "caso", "se", "quanto",
+ "todos", "todas", "ambos", "ambas", "logo", "até", "mesmo",
+ };
+
+ private static readonly HashSet ThirdPersonSubjects = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "ele", "ela", "você", "isso", "alguém", "ninguém", "quem", "pai", "mãe", "filho", "filha", "marido", "mulher",
+ };
+
+ private static readonly HashSet NotNames = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "ontem", "hoje", "agora", "antes", "depois", "afinal", "então", "entretanto", "aqui", "ali", "lá", "assim", "claro", "sim",
+ "bem", "mas", "e", "ou", "pois", "porém", "contudo", "acho", "pensei", "talvez", "se", "quando", "porque", "como", "onde",
+ "tu", "nós", "eles", "elas", "vocês", "quem", "alguém", "ninguém", "isto", "aquilo", "tudo", "nada", "olha", "ok", "bom",
+ "pronto", "enfim", "também", "sempre", "nunca", "já", "ainda", "só", "aí", "cá", "senão", "logo", "desta", "dessa",
+ };
+
+ // A capitalised clause opener is a name subject ("Frank tinha esquecido") unless it is a common word or a verb form.
+ private static bool LooksLikeName(string word) =>
+ word.Length > 1 && !NotNames.Contains(word) && !word.EndsWith("mente", StringComparison.OrdinalIgnoreCase) &&
+ !PortugueseVerbs.IsKnownVerbForm(word);
+
+ private static string ReflexiveIndicativeReplace(string s, Match m)
+ {
+ var aux = m.Groups["aux"].Value;
+ var lower = aux.ToLowerInvariant();
+ var clause = IndicativeClause.Match(s[..m.Index]).Value;
+ var words = Regex.Matches(clause, @"[\p{L}\p{M}]+").Select(w => w.Value).ToList();
+ var previous = words.Count > 0 ? words[^1] : null;
+ if (previous != null && NotClauseHosts.Contains(previous)) return m.Value;
+ // Without a following "de" the object may be fronted: "o que tinha esquecido".
+ var tail = s[(m.Index + m.Length)..];
+ var openEnd = !Regex.IsMatch(tail, @"^\s+(?:de|do|da|dos|das|disso|disto|daquilo|dele|dela|deles|delas|dum|duma|(?:tudo\s+)?mal)(?![\p{L}\p{M}])", Ci);
+ if (openEnd && words.Any(w => w.ToLowerInvariant() is "que" or "quê" or "qual" or "quais")) return m.Value;
+
+ string? clitic = lower switch
+ {
+ "tens" or "tinhas" or "havias" or "terias" or "terás" => "te",
+ "temos" or "tínhamos" or "havíamos" or "teríamos" or "teremos" => "nos",
+ "têm" or "tinham" or "haviam" or "teriam" or "terão" or "tem" or "terá" => "se",
+ "tenho" or "terei" => "me",
+ _ => null,
+ };
+ if (clitic == null)
+ {
+ // "tinha/havia/teria" are first or third person: decide from the subject right before it.
+ var subjectIndex = words.Count - 1;
+ while (subjectIndex >= 0 && words[subjectIndex].ToLowerInvariant() is "não" or "nunca" or "já" or "também" or "só" or "ainda" or "mesmo")
+ subjectIndex--;
+ if (subjectIndex < 0) return m.Value;
+ var subject = words[subjectIndex];
+ if (subject.Equals("eu", StringComparison.OrdinalIgnoreCase)) clitic = "me";
+ else if (ThirdPersonSubjects.Contains(subject) ||
+ (char.IsUpper(subject[0]) && !CaseHelper.IsAllUpper(subject) && (subjectIndex > 0 || LooksLikeName(subject)))) clitic = "se";
+ else return m.Value;
+ }
+ if (CaseHelper.IsAllUpper(aux)) clitic = clitic.ToUpperInvariant();
+ var gap = m.Value.Substring(aux.Length, m.Value.Length - aux.Length - m.Groups["part"].Length);
+
+ if (words.Any(ProclisisTriggers.Contains))
+ return clitic + " " + (char.IsUpper(aux[0]) && !CaseHelper.IsAllUpper(aux) && clause.Trim().Length == 0
+ ? aux.ToLowerInvariant() : aux) + gap + m.Groups["part"].Value;
+ var attached = PortugueseVerbs.AttachMesoclitic(aux, clitic, false) ?? PortugueseVerbs.AttachEnclitic(aux, clitic);
+ return attached + gap + m.Groups["part"].Value;
+ }
+
+ public static string ApplyLate(string s)
+ {
+ s = EncliticSubjunctive.Replace(s, m => m.Groups["pre"].Value + m.Groups["cl"].Value + " " + m.Groups["v"].Value);
+ s = ReflexiveCompound.Replace(s, m =>
+ {
+ if (m.Groups["subj"].Value.Equals("que", StringComparison.OrdinalIgnoreCase) &&
+ ObjectAntecedent.IsMatch(s.Substring(0, m.Index))) return m.Value;
+ if (NotClauseHosts.Contains(m.Groups["subj"].Value) && m.Groups["adv"].Length == 0) return m.Value;
+ var aux = m.Groups["aux"].Value.ToLowerInvariant();
+ var subj = m.Groups["subj"].Value.ToLowerInvariant();
+ var clitic = aux switch
+ {
+ "tenhas" or "tivesses" => "te",
+ "tenhamos" or "tivéssemos" => "nos",
+ "tenha" or "tivesse" when subj == "eu" => "me",
+ _ => "se",
+ };
+ if (CaseHelper.IsAllUpper(m.Groups["aux"].Value)) clitic = clitic.ToUpperInvariant();
+ return m.Groups["pre"].Value + clitic + " " + m.Groups["aux"].Value + " " + m.Groups["part"].Value;
+ });
+ if (s.IndexOf("esquecid", StringComparison.OrdinalIgnoreCase) < 0 && s.IndexOf("lembrad", StringComparison.OrdinalIgnoreCase) < 0) return s;
+ return ReflexiveIndicative.Replace(s, m => ReflexiveIndicativeReplace(s, m));
+ }
+
+ public static string Apply(string s)
+ {
+ if (s.IndexOf("talvez", StringComparison.OrdinalIgnoreCase) < 0) return s;
+ return Maybe.Replace(s, m => Replace(m, s.Substring(m.Index + m.Length)));
+ }
+
+ private static string Replace(Match m, string rest)
+ {
+ var verb = m.Groups["v"].Value;
+ var lower = verb.ToLowerInvariant();
+ var hasSubject = m.Groups["subj"].Success || m.Groups["cl"].Success || m.Groups["ecl"].Success;
+ string? replacement = null;
+
+ if (lower == "há" && TimeAgo.IsMatch(rest)) return m.Value;
+
+ if (Irregular.TryGetValue(lower, out var subjunctive))
+ {
+ replacement = subjunctive;
+ }
+ else if (IrregularPast.TryGetValue(lower, out var past))
+ {
+ var participle = past.Participle == "?"
+ ? Movement.IsMatch(m.Groups["next"].Value) ? "ido" : "sido"
+ : past.Participle;
+ replacement = Have[past.Person] + " " + participle;
+ }
+ else if (TryRegular(lower, hasSubject, m.Groups["next"].Value.Trim(), out var regular))
+ {
+ replacement = regular;
+ }
+
+ if (replacement == null) return m.Value;
+
+ if (CaseHelper.IsAllUpper(verb) && verb.Length > 1) replacement = replacement.ToUpperInvariant();
+ else if (char.IsUpper(verb[0])) replacement = CaseHelper.Capitalize(replacement);
+
+ var clitic = m.Groups["cl"].Success ? m.Groups["cl"].Value.TrimEnd() : m.Groups["ecl"].Value;
+ if (m.Groups["cl"].Success && m.Groups["ecl"].Success) return m.Value;
+ return m.Groups["pre"].Value + (clitic.Length > 0 ? clitic + " " : string.Empty) + replacement;
+ }
+
+ // Present indicative → present subjunctive only ("podemos" → "possamos"); past and imperfect forms are rejected.
+ internal static bool TryPresentSubjunctive(string form, out string subjunctive)
+ {
+ subjunctive = string.Empty;
+ var lower = form.ToLowerInvariant();
+ string? result = null;
+ if (lower is "consigo") result = "consiga";
+ else if (lower is "conseguimos") result = "consigamos";
+ else if (Irregular.TryGetValue(lower, out var irregular))
+ {
+ if (irregular.EndsWith("sse", StringComparison.Ordinal) || irregular.EndsWith("sses", StringComparison.Ordinal) ||
+ irregular.EndsWith("ssemos", StringComparison.Ordinal) || irregular.EndsWith("ssem", StringComparison.Ordinal)) return false;
+ result = irregular;
+ }
+ else if (TryRegular(lower, true, string.Empty, out var regular) && !regular.Contains(' ') &&
+ !regular.EndsWith("sse", StringComparison.Ordinal) && !regular.EndsWith("ssem", StringComparison.Ordinal) &&
+ !regular.EndsWith("sses", StringComparison.Ordinal) && !regular.EndsWith("ssemos", StringComparison.Ordinal))
+ result = regular;
+ if (result == null) return false;
+ subjunctive = CaseHelper.IsAllUpper(form) && form.Length > 1 ? result.ToUpperInvariant()
+ : char.IsUpper(form[0]) ? CaseHelper.Capitalize(result) : result;
+ return true;
+ }
+
+ // Slots of VerbConjugator: present 1-5, preterite 6-9, imperfect 10-13, present subjunctive 23-26,
+ // imperfect subjunctive 27-30, participle 35, European preterite "nós" 42.
+ private static bool TryRegular(string form, bool hasSubject, string next, out string replacement)
+ {
+ replacement = string.Empty;
+ foreach (var infinitive in CandidateInfinitives(form))
+ {
+ if (!PortugueseVerbs.IsKnownInfinitive(infinitive) || !VerbConjugator.TryConjugate(infinitive, out var forms)) continue;
+ var slot = Array.FindIndex(forms, f => f.Equals(form, StringComparison.OrdinalIgnoreCase));
+ if (slot < 0) continue;
+ // Without a subject, a bare present singular is often a noun or preposition ("talvez para", "talvez nada").
+ if (!hasSubject && slot is 1 or 3) return false;
+ // "talvez marcas de agulha": a bare "-as/-es" form before "de" is usually a plural noun.
+ if (!hasSubject && slot == 2 && Regex.IsMatch(next, "^(?:de|do|da|dos|das)$", Ci)) return false;
+ var participle = IrregularParticiples.TryGetValue(infinitive, out var p) ? p : forms[35];
+ string? result = slot switch
+ {
+ 1 or 3 => forms[23],
+ 2 => forms[24],
+ 5 => forms[26],
+ 6 or 8 => "tenha " + participle,
+ 7 => "tenhas " + participle,
+ 9 => "tenham " + participle,
+ 42 when infinitive.EndsWith("ar", StringComparison.Ordinal) => "tenhamos " + participle,
+ 10 => forms[27],
+ 11 => forms[28],
+ 12 => forms[29],
+ 13 => forms[30],
+ _ => null,
+ };
+ if (result == null) continue;
+ replacement = result;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static IEnumerable CandidateInfinitives(string form)
+ {
+ for (var cut = 1; cut <= Math.Min(6, form.Length - 2); cut++)
+ {
+ var stem = form[..^cut];
+ foreach (var fixedStem in new[]
+ {
+ stem,
+ stem.EndsWith("qu", StringComparison.Ordinal) ? stem[..^2] + "c" : null,
+ stem.EndsWith("gu", StringComparison.Ordinal) ? stem[..^2] + "g" : null,
+ stem.EndsWith('ç') ? stem[..^1] + "c" : null,
+ stem.EndsWith('j') ? stem[..^1] + "g" : null,
+ })
+ {
+ if (fixedStem == null) continue;
+ yield return fixedStem + "ar";
+ yield return fixedStem + "er";
+ yield return fixedStem + "ir";
+ }
+ }
+ }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/NameArticleQuestionWindow.cs b/se5/BrazilianToEuropeanPortuguese/NameArticleQuestionWindow.cs
new file mode 100644
index 00000000..a5f617c3
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/NameArticleQuestionWindow.cs
@@ -0,0 +1,63 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal sealed class NameArticleQuestionWindow : Window
+{
+ internal string PersonalName { get; }
+ internal Button MasculineButton { get; }
+ internal Button FeminineButton { get; }
+ internal Button NoArticleButton { get; }
+ internal Button SkipButton { get; }
+
+ internal NameArticleQuestionWindow(string name)
+ {
+ PersonalName = name;
+ Title = "Article for a personal name";
+ Width = 560;
+ Height = 270;
+ CanResize = false;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ MasculineButton = Choice("Use o", "o");
+ FeminineButton = Choice("Use a", "a");
+ NoArticleButton = Choice("No article / not a person", "none");
+ SkipButton = new Button { Content = "Not now", IsCancel = true };
+ SkipButton.Click += (_, _) => Close(null);
+ Content = new StackPanel
+ {
+ Margin = new Thickness(16),
+ Spacing = 14,
+ Children =
+ {
+ new TextBlock
+ {
+ Text = $"Which article should be used for \"{name}\"?",
+ TextWrapping = TextWrapping.Wrap,
+ FontWeight = FontWeight.SemiBold,
+ },
+ new TextBlock
+ {
+ Text = "A supported conversion needs a name article. Choose o or a only if this is a personal name. " +
+ "The choice goes into your editable list; Apply saves it for later files. " +
+ "Not now skips this name for the current preview.",
+ TextWrapping = TextWrapping.Wrap,
+ },
+ new WrapPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Children = { MasculineButton, FeminineButton, NoArticleButton, SkipButton },
+ },
+ },
+ };
+ }
+
+ private Button Choice(string label, string article)
+ {
+ var button = new Button { Content = label, Margin = new Thickness(0, 0, 8, 6) };
+ button.Click += (_, _) => Close(article);
+ return button;
+ }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/NameArticlesWindow.cs b/se5/BrazilianToEuropeanPortuguese/NameArticlesWindow.cs
new file mode 100644
index 00000000..f7da52f7
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/NameArticlesWindow.cs
@@ -0,0 +1,76 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal sealed class NameArticlesWindow : Window
+{
+ private readonly TextBox _editor;
+ private readonly TextBlock _error;
+ internal string Value => _editor.Text ?? "";
+ internal TextBox Editor => _editor;
+ internal TextBlock Error => _error;
+ internal Button SaveButton { get; }
+
+ internal NameArticlesWindow(string value)
+ {
+ Title = "Personal name articles";
+ Width = 620;
+ Height = 480;
+ MinWidth = 480;
+ MinHeight = 350;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+
+ _editor = new TextBox
+ {
+ Name = "NameArticlesEditor",
+ Text = value,
+ AcceptsReturn = true,
+ TextWrapping = TextWrapping.NoWrap,
+ VerticalAlignment = VerticalAlignment.Stretch,
+ };
+ _error = new TextBlock { Name = "NameArticlesError", TextWrapping = TextWrapping.Wrap, Foreground = Brushes.Red };
+ var cancel = new Button { Content = "Cancel", MinWidth = 90, IsCancel = true };
+ cancel.Click += (_, _) => Close(false);
+ var save = new Button { Name = "SaveNameArticles", Content = "Use list", MinWidth = 90, IsDefault = true };
+ SaveButton = save;
+ save.Click += (_, _) =>
+ {
+ try
+ {
+ _ = new PersonalNameArticles(Value);
+ Close(true);
+ }
+ catch (InvalidDataException ex)
+ {
+ _error.Text = ex.Message;
+ }
+ };
+ var buttons = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ Spacing = 8,
+ Children = { cancel, save },
+ };
+ var panel = new DockPanel { Margin = new Thickness(16), LastChildFill = true };
+ var instructions = new TextBlock
+ {
+ Text = "One single-word name per line: Jamie = o, Alex = a, or Maria = none.\n" +
+ "Choices override built-in names in supported rules. Remove a line to restore the default. " +
+ "The same choice applies to every file using these settings.",
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(0, 0, 0, 10),
+ };
+ DockPanel.SetDock(instructions, Dock.Top);
+ DockPanel.SetDock(buttons, Dock.Bottom);
+ DockPanel.SetDock(_error, Dock.Bottom);
+ panel.Children.Add(instructions);
+ panel.Children.Add(buttons);
+ panel.Children.Add(_error);
+ panel.Children.Add(_editor);
+ Content = panel;
+ }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/PersonalNameArticles.cs b/se5/BrazilianToEuropeanPortuguese/PersonalNameArticles.cs
new file mode 100644
index 00000000..aeb5d33f
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/PersonalNameArticles.cs
@@ -0,0 +1,43 @@
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal sealed class PersonalNameArticles
+{
+ private readonly Dictionary _entries = new(StringComparer.OrdinalIgnoreCase);
+ private readonly ISet? _unresolved;
+ private static readonly Regex NamePattern = new(@"^\p{L}[\p{L}\p{M}]*$", RegexOptions.CultureInvariant);
+
+ public PersonalNameArticles(string text, ISet? unresolved = null)
+ {
+ _unresolved = unresolved;
+ var lines = text.Split('\n');
+ for (var i = 0; i < lines.Length; i++)
+ {
+ var line = lines[i].Trim();
+ if (line.Length == 0) continue;
+ var parts = line.Split('=');
+ if (parts.Length != 2 || !NamePattern.IsMatch(parts[0].Trim()))
+ throw new InvalidDataException($"Name articles, line {i + 1}: use Name = o, Name = a, or Name = none (one single-word name per line).");
+ var name = parts[0].Trim();
+ var article = parts[1].Trim().ToLowerInvariant();
+ if (article is not ("o" or "a" or "none"))
+ throw new InvalidDataException($"Name articles, line {i + 1}: article must be o, a, or none.");
+ if (!_entries.TryAdd(name, article))
+ throw new InvalidDataException($"Name articles, line {i + 1}: {name} is listed more than once.");
+ }
+ }
+
+ public bool TryGet(string name, out string article)
+ {
+ article = "";
+ return name.Length > 0 && char.IsUpper(name[0]) && _entries.TryGetValue(name, out article!);
+ }
+
+ public void ReportUnknown(string name)
+ {
+ if (_unresolved != null && name.Length > 1 && char.IsUpper(name[0]) &&
+ !NaturaDictionary.Load(true).Check(name.ToLowerInvariant()))
+ _unresolved.Add(name);
+ }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs b/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs
index 1a24dfee..21407672 100644
--- a/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs
+++ b/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs
@@ -26,25 +26,44 @@ public sealed class PortugueseConverter
private const char PlaceholderBase = '\uE000';
private static readonly Lazy BuiltIn = new(LoadBuiltIn);
+ private static readonly HashSet PersonalObjectHomographs = new(StringComparer.OrdinalIgnoreCase)
+ { "honra", "honras", "surra", "surras", "contacto", "contactos", "ficha", "fichas", "recusa", "recusas", "monitora", "monitoras" };
+
+ internal static bool HasNominalDeterminerBefore(string before) => Regex.IsMatch(before,
+ NotWordBefore + @"(?:o|a|os|as|um|uma|uns|umas|este|esta|esse|essa|meu|minha|teu|tua|seu|sua)" +
+ @"(?:\s+(?:grande|pequeno|pequena|breve|bom|boa|mau|má|próprio|própria|último|última)){0,2}\s+$", Ci);
+
+ private static bool IsPersonalObjectVerb(Match match)
+ {
+ return !PersonalObjectHomographs.Contains(match.Groups["v"].Value) ||
+ !HasNominalDeterminerBefore(match.Result("$`"));
+ }
private static readonly Regex BringHimIn = new(@"(? _changedGender = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Regex? _predicateDemonstrativeRegex;
private readonly Dictionary _words = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet _localClothingWords = new(StringComparer.OrdinalIgnoreCase);
private readonly Regex? _prefixRegex;
private readonly Dictionary _prefixes = new(StringComparer.OrdinalIgnoreCase);
private readonly VerbVocabulary? _verbs;
+ private readonly HashSet _blockedVocabularyVerbs = new(StringComparer.OrdinalIgnoreCase);
- public PortugueseConverter(ConversionOptions? options = null, PortugueseLocalWordList? localWordList = null)
+ public PortugueseConverter(ConversionOptions? options = null, PortugueseLocalWordList? localWordList = null,
+ ISet? unresolvedNameArticles = null)
{
_options = options ?? ConversionOptions.Default;
+ _nameArticles = new PersonalNameArticles(_options.NameArticles, unresolvedNameArticles);
var builtIn = BuiltIn.Value;
var ignored = new HashSet(localWordList?.Ignored ?? new List(), StringComparer.OrdinalIgnoreCase);
+ _blockedVocabularyVerbs.UnionWith(ignored);
+ _blockedVocabularyVerbs.UnionWith(localWordList?.Words.Select(w => NormalizeSpaces(w.Item1)) ?? Enumerable.Empty());
foreach (var entry in builtIn.Words)
{
@@ -74,11 +93,11 @@ public PortugueseConverter(ConversionOptions? options = null, PortugueseLocalWor
if (_options.Vocabulary)
{
- var ownWords = new HashSet(localWordList?.Words.Select(w => NormalizeSpaces(w.Item1)) ?? Enumerable.Empty(), StringComparer.OrdinalIgnoreCase);
_verbs = new VerbVocabulary(
builtIn.Verbs,
- infinitive => ignored.Contains(infinitive) || ownWords.Contains(infinitive),
- form => _words.ContainsKey(form) || ignored.Contains(form));
+ _blockedVocabularyVerbs.Contains,
+ form => ignored.Contains(form) || _words.TryGetValue(form, out var word) &&
+ !(word.Br is "briga" or "brigas" && word.RequiredDeterminerGender == 'f'));
}
var keep = builtIn.Keep.Concat(ignored).Where(k => k.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
@@ -115,6 +134,11 @@ public PortugueseConverter(ConversionOptions? options = null, PortugueseLocalWor
@"(?\s+d(?:e|o|a|os|as)\s+[\p{L}\p{M}-]+(?:\s+[\p{L}\p{M}-]+)?\s+(?:(?:ainda|já|não|também|sempre|nunca)\s+)?" +
@"(?:está|estava|estão|estavam|é|era|são|eram|foi|foram|ficou|ficaram|parece|parecem|continua|continuam)\s+)(?" + AgreeingAdjectiveChain + ")",
Ci);
+ // "Que papo é esse?" → "Que conversa é essa?": the demonstrative agrees with the new noun.
+ _predicateDemonstrativeRegex = new Regex(
+ NotWordBefore + @"(?" + Alternation(_changedGender.Keys) + ")" + NotWordAfter +
+ @"(?\s+(?:é|era|foi|será)\s+)(?esse|este|aquele|esses|estes|aqueles|essa|esta|aquela|essas|estas|aquelas)(?=(?:\s+(?:aí|ali|lá|aqui))?\s*(?:[?!.…,]|$|<))",
+ Ci);
}
if (_words.Count > 0)
@@ -397,6 +421,7 @@ private string ConvertCore(string text, string? previousText, ReferenceHint? ref
// One rule can produce input for an earlier rule ("Me coloque na espera" → "Coloque-me na espera"
// → "Coloque-me em espera"), so repeat until stable; running the plugin twice then changes nothing.
var current = text;
+ YouToTuRules.OriginalText = text;
for (var pass = 0; pass < MaxPasses; pass++)
{
var next = ConvertOnce(current, previousText, reference, laterPass: pass > 0 || protectOutputs, trace);
@@ -590,7 +615,7 @@ void Track(string id, string description)
"missing-infinitive" => "Dictionary-validated",
"word-list" or "orthography" => "Dictionary/rule-based",
"address" or "context-phrases" or "inclusive-hearing" or "verb-vocabulary" or "reference-clitics" or
- "construction-vocabulary" or "subject-continuity" or "reference-idioms" =>
+ "construction-vocabulary" or "reviewed-verb-senses" or "subject-continuity" or "reference-idioms" =>
"Context-sensitive; reference-assisted when available",
_ => "Rule-based",
}));
@@ -617,6 +642,8 @@ void Track(string id, string description)
" " + m.Groups["v"].Value);
}
+ s = PurposeClauses.FixHyphenatedArticles(s);
+
if (_options.Colloquial)
{
s = ColloquialRules.Apply(s);
@@ -654,11 +681,13 @@ void Track(string id, string description)
reference is not { FormalAddress: true } && !YouToTuRules.IsFormal(s));
s = ClothingConstructions.PeopleAndClothes(s, reference, _options.Vocabulary, _options.Pronouns);
Track("clothing-complements", "Person-to-ground and owner-aware clothing complements");
- if (_options.Pronouns) s = ConversionConstructions.Government(s);
+ if (_options.Pronouns) s = ConversionConstructions.Government(s, reference);
Track("verb-government", "Recollection and dating complements");
if (_options.Vocabulary)
s = ConversionConstructions.Vocabulary(s, _options.Pronouns, informalConstructions);
Track("construction-vocabulary", "Complement-qualified vocabulary senses");
+ if (_options.Vocabulary) s = ConversionConstructions.ReviewedVerbSenses(s, reference, _blockedVocabularyVerbs.Contains);
+ Track("reviewed-verb-senses", "Telephone surveillance and reference-confirmed stalling/sleep senses");
if (_options.YouToTu) s = ConversionConstructions.PersonalInfinitives(s, informalConstructions);
Track("personal-infinitives", "Explicit-subject personal infinitives");
if (_options.Pronouns) s = ConversionConstructions.FamilySubjects(s, reference);
@@ -666,6 +695,11 @@ void Track(string id, string description)
if (_options.Pronouns && informalConstructions)
s = ConversionConstructions.ReferenceObjectClitics(s, reference, _options.Vocabulary);
Track("reference-object-clitics", "Reference-confirmed informal past direct objects");
+ if (_options.Progressive && _options.Pronouns && informalConstructions)
+ s = ConversionConstructions.WaitingComplements(s, reference);
+ Track("waiting-complements", "Reference-confirmed kept-you-waiting complements");
+ if (_options.Colloquial) s = ConversionConstructions.SpatialPosition(s, reference);
+ Track("spatial-position", "Reference-confirmed close spatial position rather than temporal logo");
if (informalConstructions) s = ConversionConstructions.Clauses(s, reference);
Track("subject-continuity", "Controlled and reference-confirmed clause subjects");
if (_options.Colloquial && _options.Progressive && _options.Pronouns)
@@ -680,6 +714,8 @@ void Track(string id, string description)
Track("explanatory-introductions", "Reference-confirmed explanatory thing is constructions");
if (_options.Vocabulary) s = ConversionConstructions.EmergencyConstructions(s, reference);
Track("emergency-constructions", "Reference-confirmed incident response and medical arrest");
+ if (_options.Vocabulary) s = ConversionConstructions.PoliceNouns(s);
+ Track("police-nouns", "Police personnel nouns after quantifiers, preserving adjectival policial");
if (_options.Vocabulary) s = ConversionConstructions.TravelStops(s, reference);
Track("travel-stops", "Reference-confirmed stopping places and brief journey stops");
if (_options.Colloquial && _options.Vocabulary) s = ConversionConstructions.Idioms(s, reference);
@@ -691,6 +727,13 @@ void Track(string id, string description)
if (_options.YouToTu)
{
s = YouToTuRules.Apply(s, previousText, reference, _options.Pronouns && _options.Colloquial);
+ s = YouToTuRules.DropVoceBeforeSecondPerson(s);
+ if (reference is not { FormalAddress: true } && !YouToTuRules.IsFormal(s))
+ {
+ s = YouToTuRules.AdjacentWhenClause(s);
+ s = YouToTuRules.OpeningInterjectionAgreement(s);
+ s = YouToTuRules.SecondPersonGovernedReflexive(s);
+ }
}
Track("address", "Address, conjugation and possessives");
@@ -702,7 +745,9 @@ void Track(string id, string description)
if (_options.Pronouns)
{
- s = PronounRules.Apply(s);
+ s = PronounRules.Apply(s, reference);
+ s = LetConstructions.Apply(s, reference);
+ s = PronounRules.ReplacePersonalInfinitiveObjects(s, includeYou: false);
}
Track("pronouns", "Pronoun placement");
@@ -724,7 +769,7 @@ void Track(string id, string description)
s = VocabularyRules.ReplaceShutUp(s, _options.YouToTu && reference is not { FormalAddress: true });
s = VocabularyRules.ReplaceBareReflexive(s);
s = DepartingBarca.Replace(s, ReplaceDepartingBarca);
- s = _verbs?.Apply(s) ?? s;
+ s = _verbs?.Apply(s, reference) ?? s;
s = YoungPerson.Replace(s, m =>
_words.TryGetValue(m.Groups["noun"].Value, out var entry) && entry.Pt.StartsWith("miúd", StringComparison.OrdinalIgnoreCase)
? ReplaceYoungPerson(m, reference)
@@ -734,7 +779,7 @@ void Track(string id, string description)
if (_wordRegex != null)
{
- s = _wordRegex.Replace(s, ReplaceWord);
+ s = _wordRegex.Replace(s, m => ReplaceWord(m, s, reference));
}
if (_options.Vocabulary) s = ClothingConstructions.PluralUnderwear(s);
@@ -744,6 +789,12 @@ void Track(string id, string description)
m.Groups["n"].Value + m.Groups["pre"].Value + Regex.Replace(m.Groups["chain"].Value, @"[\p{L}]+",
a => NotAgreeing.Contains(a.Value) ? a.Value : SwapAdjectiveGender(a.Value, _changedGender[m.Groups["n"].Value])));
}
+ if (_predicateDemonstrativeRegex != null)
+ {
+ s = _predicateDemonstrativeRegex.Replace(s, m =>
+ m.Groups["n"].Value + m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["d"].Value,
+ Determiners.ToGender(m.Groups["d"].Value.ToLowerInvariant(), _changedGender[m.Groups["n"].Value])));
+ }
Track("word-list", "Word-list spelling/vocabulary and gender agreement");
if (_options.Vocabulary) s = ClothingConstructions.Sportswear(s, reference,
key => !_localClothingWords.Contains(NormalizeSpaces(key)));
@@ -756,7 +807,10 @@ void Track(string id, string description)
if (reference is { This: true })
{
- s = NearDemonstrative.Replace(s, m => CaseHelper.MatchCase(m.Value, NearDemonstratives[m.Value]));
+ // "Que drama é esse?" is an idiomatic challenge, not a pointer to something near.
+ var predicatesThis = Regex.IsMatch(reference?.EnglishText ?? "", @"(?:\b(?:is|was)|['’]s)\s+(?:all\s+)?this\b", Ci);
+ s = NearDemonstrative.Replace(s, m => !predicatesThis && Regex.IsMatch(m.Result("$`"), @"(?
{
var d = m.Groups["d"].Value;
- if (d.Equals("a", StringComparison.Ordinal) && Regex.IsMatch(s.Substring(m.Index + m.Length), @"^\s+\p{L}+(?:ar|er|ir)" + NotWordAfter))
+ if (d.Equals("a", StringComparison.Ordinal) && Regex.IsMatch(s.Substring(m.Index + m.Length), @"^\s+\p{L}+(?:ar|er|ir)(?:-(?:me|te|se|nos|vos|o|a|os|as|lo|la|los|las|lhe|lhes))?" + NotWordAfter))
{
- return m.Value; // "pegar a fazer" is not an object
+ return m.Value; // "pegar a fazer" / "a divertir-me" is not an object
}
var to = ContractionMap[d];
@@ -1092,7 +1161,7 @@ private static string Unmask(string text, List masks)
return sb.ToString();
}
- private string ReplaceWord(Match m)
+ private string ReplaceWord(Match m, string text, ReferenceHint? reference)
{
var keyText = m.Groups["key"].Value;
if (!_words.TryGetValue(NormalizeSpaces(keyText), out var entry))
@@ -1100,6 +1169,9 @@ private string ReplaceWord(Match m)
return m.Value;
}
+ if (entry.RequiredDeterminerGender != null &&
+ entry.Br is "briga" or "brigas" or "bate-boca" or "bate-bocas")
+ entry = entry with { Pt = ConflictVocabulary.NounTarget(text, m.Groups["key"], reference, entry.Pt) };
return ReplaceWordEntry(m, entry);
}
@@ -1506,6 +1578,7 @@ internal static class VocabularyRules
private static readonly Regex LawContext = new(
NotWordBefore + @"(?:lei|leis|advogad\p{L}*|tribunal|juiz|juíza|ilegal|ilegais|contrato|processo|polícia|direitos?|jurídic\p{L}*|crime|documentos?|tutor|tutela|guarda|idade|casamento|legalmente|autorização|licença|maioridade|menoridade|distância|limite|prazo|mínim\p{L}*|máxim\p{L}*|representante|custódia|porte|arma|armas|testamento|herdeir\p{L}*|estatuto|residência|defesa|requisitos?|procedimentos?)" + NotWordAfter, Ci);
+ private static readonly Regex LawfulOrderBefore = new(NotWordBefore + @"(?:ordem|ordens)\s+$", Ci);
// "Isso é ruim" → "Isso é mau", "uma notícia ruim" → "uma notícia má".
private static readonly Regex Bad = new(NotWordBefore + @"(?:(?\p{L}+)\s+)?(?:(?é|está|era|estava|foi|ficou|fica|parece|são|estão|eram|seja|ser|estar|tão|muito|bem|meio|nada|mais|menos|super)\s+)?(?ruim|ruins)" + NotWordAfter, Ci);
@@ -1587,7 +1660,7 @@ private static string ReplaceNag(Match m)
// "Atirou nele" / "Atirem!" → "Disparou contra ele" / "Disparem!" ("atirar" alone means "to throw" in Portugal).
private static readonly Regex Shoot = new(
- NotWordBefore + @"(?atir(?:ar|ou|ei|aram|a|e|em|o|amos|ámos|ando|ava|avam|aria|asse|ou-se)?)" + NotWordAfter +
+ NotWordBefore + @"(?atir(?:ar|ou|ei|aste|aram|a|as|e|es|em|o|amos|ámos|ando|ado|ados|ava|avas|avam|aria|arias|asse|asses|ará|arás|ou-se)?)" + NotWordAfter +
@"(?:(?\s+)(?em|no|na|nos|nas|num|numa|nele|nela|neles|nelas|nisso|nisto)" + NotWordAfter + @"|(?=\s*[.!]|\s*$))", Ci);
private static string ReplaceShoot(Match m)
@@ -1596,7 +1669,8 @@ private static string ReplaceShoot(Match m)
var shoot = CaseHelper.MatchCase(v[..4], "disp") + (CaseHelper.IsAllUpper(v) ? "AR" : "ar") + v[4..];
if (!m.Groups["p"].Success)
{
- return shoot;
+ // "Foi atirado." is "thrown".
+ return Regex.IsMatch(v, "ados?$", RegexOptions.IgnoreCase) ? m.Value : shoot;
}
var p = m.Groups["p"].Value.ToLowerInvariant();
@@ -1921,7 +1995,8 @@ public static string Apply(string s)
CaseHelper.MatchCase(m.Groups["w"].Value, m.Groups["w"].Value.EndsWith("s", StringComparison.OrdinalIgnoreCase) ? "sanitas" : "sanita"));
if (!LawContext.IsMatch(s))
{
- s = Legal.Replace(s, m => CaseHelper.MatchCase(m.Value, m.Groups["s"].Success ? "fixes" : "fixe"));
+ s = Legal.Replace(s, m => LawfulOrderBefore.IsMatch(m.Result("$`")) ? m.Value :
+ CaseHelper.MatchCase(m.Value, m.Groups["s"].Success ? "fixes" : "fixe"));
}
s = Bad.Replace(s, ReplaceBad);
@@ -1932,6 +2007,8 @@ public static string Apply(string s)
PortugueseVerbs.IsInfinitive(adj) || Regex.IsMatch(adj, @"(?:ou|ei|eu|iu|am|em|aste|este|iste|ste|mos)$", Ci) || adj.Length < 3 ||
// Right after a subject the word is a verb: "Você planeja demais", "Ele reclama demais".
SubjectBefore.IsMatch(s.AsSpan(0, m.Index).ToString()) ||
+ // After a proclitic the word is a verb: "quando eu me aproximava demais".
+ Regex.IsMatch(s.AsSpan(0, m.Index).ToString(), @"(? CaseHelper.MatchCase(m.Value, "toda a gente")),
(new Regex(NotWordBefore + @"pelo\s+jeito" + NotWordAfter, Ci), m => CaseHelper.MatchCase(m.Value, "pelos vistos")),
(new Regex(NotWordBefore + @"do\s+jeito\s+que" + NotWordAfter, Ci), m => CaseHelper.MatchCase(m.Value, "como")),
- (new Regex(NotWordBefore + @"(?o|um|de\s+um|do|no|esse|desse|nesse|este|deste|neste|que|qualquer|outro|de\s+outro|do\s+mesmo|o\s+mesmo|de\s+qualquer)\s+jeito(?:\s+(?certo|errado|certeiro|estranho|engraçado|esquisito|novo|velho|perfeito|honesto|educado|bonito|lindo|fofo|louco|maluco|antigo|único|próprio|exato|tradicional|normal|rápido|direto|chato|típico))?" + NotWordAfter, Ci), m =>
+ // "do meu jeito" / "no seu jeito" / "do jeito dele" → "à minha maneira" / "à sua maneira" / "à maneira dele".
+ // "do teu jeito maluco e errático" → "à tua maneira maluca e errática"; "do teu jeito, não da Bina" → "à tua maneira, não à da Bina".
+ (new Regex(NotWordBefore + @"(?do|no|ao)\s+(?meu|teu|seu|nosso|vosso)\s+jeito" + NotWordAfter +
+ @"(?(?:,?\s+(?:e\s+)?(?!(?:não|senão|como|do|no|o|ao|da|na|a|isso|isto|tudo|disso|nisso|quando|pro|mesmo|logo|assim|cedo|demo|agora|nunca|para|pra|ela|essa|esta|nada|toda|cada|ainda|fora|embora|ora)" + NotWordAfter + @")\p{Ll}+[oa]s?" + NotWordAfter + @"(?=\s*(?:e\s|,|de\s|[.!?…]|$))){0,2})" +
+ @"(?:(?,?\s+(?:e\s+)?não\s+)(?:do\s+(?meu|teu|seu|nosso|vosso)(?=\s*(?:[.,!?…;]|$))|(?=d[oa]s?\s)))?", Ci), m =>
+ {
+ var adj = Regex.Replace(m.Groups["adj"].Value, @"(?\p{Ll}{3,})o(?s?)(?![\p{L}\p{M}])", a =>
+ PortugueseVerbs.IsKnownVerbForm(a.Value) ? a.Value : a.Groups["w"].Value + "a" + a.Groups["s"].Value);
+ var head = m.Value[..(m.Groups["adj"].Success && m.Groups["adj"].Length > 0 ? m.Groups["adj"].Index - m.Index : m.Groups["tail"].Success ? m.Groups["tail"].Index - m.Index : m.Length)];
+ return CaseHelper.MatchCase(head, "à " + m.Groups["pos"].Value.ToLowerInvariant() switch
+ {
+ "meu" => "minha", "teu" => "tua", "seu" => "sua", "nosso" => "nossa", _ => "vossa",
+ } + " maneira") + adj + (!m.Groups["tail"].Success ? "" : m.Groups["tail"].Value + "à " + (!m.Groups["tpos"].Success ? "" :
+ m.Groups["tpos"].Value.ToLowerInvariant() switch { "meu" => "minha", "teu" => "tua", "seu" => "sua", "nosso" => "nossa", _ => "vossa" }));
+ }),
+ (new Regex(NotWordBefore + @"(?do|no|ao)\s+jeito\s+(?=(?:dele|dela|deles|delas|d[oa]s?\s+\p{L}+)" + NotWordAfter + @")(?!(?:d[oa]s?\s+)?que" + NotWordAfter + ")", Ci), m =>
+ CaseHelper.MatchCase(m.Value, "à maneira ")),
+ (new Regex(NotWordBefore + @"(?(?:o|um|do|no|de\s+um|pelo)\s+(?:único|melhor|pior)|o|um|de\s+um|do|no|esse|desse|nesse|este|deste|neste|que|qualquer|outro|de\s+outro|do\s+mesmo|o\s+mesmo|de\s+qualquer)\s+jeito(?:\s+(?certo|errado|certeiro|estranho|engraçado|esquisito|novo|velho|perfeito|honesto|educado|bonito|lindo|fofo|louco|maluco|antigo|único|próprio|exato|tradicional|normal|rápido|direto|chato|típico))?" + NotWordAfter, Ci), m =>
{
var d = m.Groups["d"].Value;
- var fem = Regex.Replace(d.ToLowerInvariant(), @"\b(?:o|um|do|no|esse|desse|nesse|este|deste|neste|outro|mesmo)\b", w => w.Value switch
+ var fem = Regex.Replace(d.ToLowerInvariant(), @"\b(?:o|um|do|no|pelo|esse|desse|nesse|este|deste|neste|outro|mesmo|único)\b", w => w.Value switch
{
- "o" => "a", "um" => "uma", "do" => "da", "no" => "na", "esse" => "essa", "desse" => "dessa", "nesse" => "nessa",
- "este" => "esta", "deste" => "desta", "neste" => "nesta", "outro" => "outra", _ => "mesma",
+ "o" => "a", "um" => "uma", "do" => "da", "no" => "na", "pelo" => "pela", "esse" => "essa", "desse" => "dessa", "nesse" => "nessa",
+ "este" => "esta", "deste" => "desta", "neste" => "nesta", "outro" => "outra", "único" => "única", _ => "mesma",
});
var adj = m.Groups["adj"].Value;
if (adj.Length > 0 && adj.EndsWith("o", StringComparison.Ordinal))
@@ -2233,8 +2327,114 @@ private static readonly (Regex Regex, MatchEvaluator Evaluator)[] Rules =
(new Regex(NotWordBefore + @"(?tem|tinha|têm|tenha|ter)\s+que\s+ver\s+com" + NotWordAfter, Ci), m => m.Groups["t"].Value + " a ver com"),
(new Regex(NotWordBefore + @"(?tenho|tens|tem|temos|têm|tinha|tinhas|tínhamos|tinham|tive|tiveste|teve|tivemos|tiveram|terei|terás|terá|teremos|terão|teria|terias|teríamos|teriam|ter|tenha|tenhas|tenhamos|tenham|tendo|tiver|tiveres|tivermos|tiverem|tivesse|tivesses|tivéssemos|tivessem)\s+que(?=\s+(?:me\s+|te\s+|se\s+|nos\s+|lhe\s+|o\s+|a\s+)?(?:\p{L}*(?:ar|er|ir|ôr|or)" + NotWordAfter + @"|\p{L}+-(?:l[oa]s?|me|te|se|nos|vos|lhes?)" + NotWordAfter + "))", Ci),
m => m.Groups["t"].Value + (CaseHelper.IsAllUpper(m.Value) ? " DE" : " de")),
- (new Regex(NotWordBefore + @"(?tenho|tens|tem|temos|têm|tinha|tinhas|tínhamos|tinham|tive|tiveste|teve|tivemos|tiveram|ter|tenha|tenhas|tiver|tivesse)\s+(?certeza)" + NotWordAfter, Ci),
+ (new Regex(NotWordBefore + @"(?tenho|tens|tem|temos|têm|tinha|tinhas|tínhamos|tinham|tive|tiveste|teve|tivemos|tiveram|ter|teres|termos|terem|tenha|tenhas|tenhamos|tenham|tiver|tiveres|tivermos|tiverem|tivesse|tivesses|tivéssemos|tivessem|terei|terás|terá|teremos|terão|teria|terias|teríamos|teriam|tendo)\s+(?certeza)" + NotWordAfter, Ci),
m => m.Groups["t"].Value + (CaseHelper.IsAllUpper(m.Value) ? " A " : " a ") + m.Groups["c"].Value),
+ // Brazilian short participles after ter/haver: "tinha pego" → "tinha pegado", "tinha chego" → "tinha chegado";
+ // "tinhas entendido" → "tinhas percebido" (the bare "Entendido." reply is kept).
+ (new Regex(NotWordBefore + @"(?tenho|tens|tem|temos|têm|tinha|tinhas|tínhamos|tinham|ter|teres|termos|terem|tenha|tenhas|tenhamos|tenham|tivesse|tivesses|tivéssemos|tivessem|tiver|tiverem|teria|terias|teríamos|teriam|terá|terão|havia|haviam|haver|tendo)" +
+ @"(?\s+(?:(?:já|não|nunca|também|ainda|me|te|se|lhe|lhes|nos|vos|o|a|os|as)\s+){0,2})(?pego|chego|trago|entendido)" + NotWordAfter + @"(?!-)", Ci),
+ m => m.Groups["t"].Value + m.Groups["gap"].Value + CaseHelper.MatchCase(m.Groups["p"].Value, m.Groups["p"].Value.ToLowerInvariant() switch
+ {
+ "pego" => "pegado", "chego" => "chegado", "entendido" => "percebido", _ => "trazido",
+ })),
+ // Physical "tocar" takes "em" in Portugal: "Não toque minhas bolas" → "Não toques nas minhas bolas"; music is still "tocar a minha música".
+ (new Regex(NotWordBefore + @"(?toca|tocas|tocam|tocamos|toque|toques|toquem|toquei|tocou|tocaste|tocaram|tocava|tocavas|tocavam|tocar|tocares|tocarem|tocando|tocado|tocaria|tocarás|tocará)\s+" +
+ @"(?:(?o|a|os|as)\s+)?(?meu|minha|meus|minhas|teu|tua|teus|tuas|seu|sua|seus|suas|nosso|nossa|nossos|nossas|vosso|vossa|vossos|vossas)(?=\s+(?\p{L}+))" + NotWordAfter, Ci),
+ m =>
+ {
+ if (Regex.IsMatch(m.Groups["n"].Value, @"^(?:música|músicas|canção|canções|piano|guitarra|guitarras|violão|viola|bateria|baixo|flauta|trompete|saxofone|sax|harmónica|gaita|violino|tema|hino|disco|banda|campainha|sino|buzina|alarme|sirene|parte|vez|melodia|cena)$", Ci))
+ return m.Value;
+ var poss = m.Groups["poss"].Value;
+ var art = PronounRules.ArticleFor(poss);
+ return m.Groups["v"].Value + " " + (CaseHelper.IsAllUpper(m.Value) ? ("N" + art).ToUpperInvariant() : "n" + art) + " " + poss;
+ }),
+ // "bancar o durão" / "banca a certinha" → "armar-se em durão" is clitic-heavy; "fazer de durão" keeps the meaning in Portugal.
+ (new Regex(NotWordBefore + @"(?bancar|banca|bancas|bancam|bancou|bancaste|bancaram|bancava|bancavas|bancavam|bancando|banque|banques|banquem|bancares|bancarem|bancaria|bancarias)\s+(?:o|a|os|as)\s+" +
+ @"(?=(?herói|heroína|durão|durona|durões|vítima|vítimas|esperto|esperta|idiota|inocente|santo|santa|santinho|santinha|valentão|valentona|valente|mártir|detetive|ama|babá|rebelde|difícil|chefe|polícia|juiz|juíza|advogado|advogada|pai|mãe|louco|louca|maluco|maluca|engraçado|engraçada|machão|tímido|tímida|sonso|sonsa|otário|otária|bom|boa|mau|má|forte|corajoso|corajosa|\p{Ll}{3,}inh[oa]s?|\p{Ll}{2,}(? CaseHelper.MatchCase(m.Groups["v"].Value, m.Groups["v"].Value.ToLowerInvariant() switch
+ {
+ "bancar" => "fazer", "banca" => "faz", "bancas" => "fazes", "bancam" => "fazem", "bancou" => "fez", "bancaste" => "fizeste",
+ "bancaram" => "fizeram", "bancava" => "fazia", "bancavas" => "fazias", "bancavam" => "faziam", "bancando" => "fazendo",
+ "banque" => "faça", "banques" => "faças", "banquem" => "façam", "bancares" => "fazeres", "bancarem" => "fazerem",
+ "bancaria" => "faria", _ => "farias",
+ }) + (CaseHelper.IsAllUpper(m.Value) ? " DE " : " de ")),
+ // "ligue para polícia" → "liga para a polícia".
+ (new Regex(NotWordBefore + @"(?para|pra)\s+(?polícia)" + NotWordAfter + @"(?!-)", Ci),
+ m => m.Groups["p"].Value + (CaseHelper.IsAllUpper(m.Value) ? " A " : " a ") + m.Groups["n"].Value),
+ // "Podes despedir-se" → "Podes despedir-te": a reflexive infinitive under a "tu" verb refers to "tu".
+ (new Regex(NotWordBefore + @"(?podes|deves|vais|queres|consegues|precisas\s+de|tens\s+de|tens\s+que|costumas|tentas|sabes|devias|podias|querias|ias|irás|poderás|deverás)" +
+ @"(?\s+(?:(?:não|nunca|já|ainda|sempre|também|só|mesmo)\s+){0,2})(?:(?\p{Ll}+[aeiô]r)-se|se\s+(?\p{Ll}+[aeiô]r)(?!-))" + NotWordAfter, Ci),
+ m => m.Groups["aux"].Value + m.Groups["gap"].Value + m.Groups["inf"].Value + (CaseHelper.IsAllUpper(m.Value) ? "-TE" : "-te")),
+ // Brazilian intransitive "sentar": "Porque nunca sentavas quando vinhas" → "nunca te sentavas".
+ (new Regex(@"(?(?sentava|sentavas|sentávamos|sentavam|sentou|sentei|sentaste|sentámos|sentaram|sento|sentas|sentamos|sentam)" +
+ NotWordAfter + @"(?!-)(?=\s*[.,!?…;]|\s*$|\s+(?:aqui|ali|aí|lá|cá|na|no|nas|nos|numa|num|em|ao\s+lado|quando|comigo|contigo|connosco|à\s+mesa|perto|direito|direita|sempre|logo|outra\s+vez|de\s+novo)" + NotWordAfter + @")", Ci),
+ m =>
+ {
+ var v = m.Groups["v"].Value.ToLowerInvariant();
+ var before = m.Result("$`");
+ var cl = v switch
+ {
+ "sentavas" or "sentaste" or "sentas" => "te",
+ "sentei" or "sento" => "me",
+ "sentávamos" or "sentámos" or "sentamos" => "nos",
+ "sentava" when Regex.IsMatch(before, @"(? "me",
+ _ => "se",
+ };
+ if (CaseHelper.IsAllUpper(m.Groups["v"].Value)) cl = cl.ToUpperInvariant();
+ // "Você sentou ali": keep the clitic proclitic so the address rules can still read "você se sentou".
+ return m.Groups["lead"].Success || Regex.IsMatch(before, @"(?minto|mentes|mente|mentimos|mentem|minta|mintas|mintam|menti|mentiste|mentiu|mentiram|mentia|mentias|mentiam|mentir|mentires|mentirem|mentirei|mentirá|mentirás|mentiria|mentisse|mentindo|mentido|" +
+ @"pergunt(?:o|as|a|amos|am|ei|aste|ou|ámos|aram|ava|avas|avam|ar|ares|arem|e|es|em|arei|ará|arás|aria|asse|ando|ado))(?-(?:o|a|lhe))?\s+para\s+" +
+ @"(?:(?o|a|os|as)\s+(?=\p{L})|(?=(?-i:\p{Lu})\p{Ll}+(?![\p{L}\p{M}])(?!\s+(?:\p{Ll}+[aei]r)(?![\p{L}\p{M}]))))(?!(?:\p{L}+\s+)?\p{Ll}+[aeiô]r(?![\p{L}\p{M}]))", Ci),
+ m =>
+ {
+ var v = m.Groups["v"].Value.ToLowerInvariant();
+ var before = m.Result("$`");
+ // "Tem uma ocasião em mente para…", "Tenho perguntas para o pai", "perguntas para as quais": nouns, not verbs.
+ if (v == "mente" && Regex.IsMatch(before, NotWordBefore + @"(?:em|na|a|da|sua|tua|minha|nossa|tem|tens|tenho)\s+(?:\p{L}+\s+)?$", Ci) ||
+ v is "pergunta" or "perguntas" && Regex.IsMatch(before, NotWordBefore +
+ @"(?:uma|umas|a|as|duas|três|algumas|muitas|poucas|várias|mais|essa|esta|essas|estas|minha|minhas|tua|tuas|sua|suas|nossa|nossas|outra|outras|última|primeira|tenho|tens|tem|temos|têm|faças|faça|fazer|faz|fazes|fiz|fez|sem|de|com|há|boa|boas|tantas|só|nenhuma)\s+(?:\p{L}+\s+)?$", Ci) ||
+ Regex.IsMatch(m.Result("$'"), @"^(?:qual|quais)" + NotWordAfter, Ci))
+ return m.Value;
+ return m.Groups["v"].Value + m.Groups["cl"].Value + " " + (m.Groups["art"].Value.ToLowerInvariant() switch
+ {
+ "o" => "ao ", "a" => "à ", "os" => "aos ", "as" => "às ", _ => "a ",
+ });
+ }),
+ // Brazilian "retornar" for calls and people: "retornar a ligação" → "devolver a chamada", "nunca me retornou" → "nunca me respondeu".
+ (new Regex(NotWordBefore + @"(?retorn(?:ar|o|as|a|amos|am|ei|aste|ou|ámos|aram|ava|avas|avam|e|es|em|ará|arás|arei|aria|asse|ares|arem|ando|ado))" +
+ @"(?-(?:me|te|lhe|nos|lhes))?(?=\s+(?:(?:a|as|o|os|minha|minhas|tua|tuas|sua|suas|nossa|essa|esta)\s+)?(?:ligação|ligações|chamada|chamadas|telefonema|telefonemas)" + NotWordAfter + @")", Ci),
+ m => CaseHelper.MatchCase(m.Groups["v"].Value, "devolv" + RetornarEnding(m.Groups["v"].Value[6..])) + m.Groups["cl"].Value),
+ (new Regex(@"(?:(?(?retorn(?:ar|o|as|a|amos|am|ei|aste|ou|ámos|aram|ava|avas|avam|e|es|em|ará|arás|arei|aria|asse|ares|arem))" + NotWordAfter + @"(?!-)" +
+ @"|" + NotWordBefore + @"(?retorn(?:ar|o|as|a|amos|am|ei|aste|ou|ámos|aram|ava|avas|avam|e|es|em|ará|arás|arei|aria|asse|ares|arem|ando))(?-(?:me|te|lhe|nos|lhes))" + NotWordAfter + @")" +
+ @"(?!\s+(?:para|a|ao|à|de|do|da)\s+(?:casa|cidade|país|trabalho|escola|lugar|sítio|base|terra|origem|normal|vida|carro|cena|local|estrada|ponto)" + NotWordAfter + @")", Ci),
+ // "me retornou o dinheiro" returns a thing: "devolveu-me o dinheiro".
+ m => m.Groups["pc"].Value + CaseHelper.MatchCase(m.Groups["v"].Value,
+ (Regex.IsMatch(m.Result("$'"), @"^\s+(?:o|a|os|as|um|uma|uns|umas|meu|minha|meus|minhas|teu|tua|teus|tuas|seu|sua|seus|suas|nosso|nossa|isso|isto|tudo)" + NotWordAfter, Ci)
+ ? "devolv" : "respond") + RetornarEnding(m.Groups["v"].Value[6..])) + m.Groups["cl"].Value),
+ // Clipped "Provável demoremos aqui" / "Provável que chova" → "É provável que demoremos aqui".
+ (new Regex(SentenceStart + @"(?provável|improvável|possível|impossível)\s+(?:(?que)\s+)?(?=(?\p{L}+)" + NotWordAfter + @")", Ci),
+ m => !m.Groups["que"].Success && !(PortugueseVerbs.IsKnownVerbForm(m.Groups["v"].Value.ToLowerInvariant()) &&
+ Regex.IsMatch(m.Groups["v"].Value, @"^\p{L}{2,}(?:[ae]mos|[ae]m)$", Ci) || Regex.IsMatch(m.Groups["v"].Value, @"^\p{L}{2,}[ae]mos$", Ci) ||
+ PortugueseVerbs.IsInfinitive(m.Groups["v"].Value.ToLowerInvariant())) ? m.Value
+ : m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["adj"].Value, "É") + " " + m.Groups["adj"].Value.ToLowerInvariant() + " " +
+ (!m.Groups["que"].Success && PortugueseVerbs.IsInfinitive(m.Groups["v"].Value) ? "" : CaseHelper.IsAllUpper(m.Groups["adj"].Value) ? "QUE " : "que ")),
+ // "se não estiveres a cuidar de si mesmo" → "de ti mesmo": a "tu" clause keeps "ti"/"contigo".
+ (new Regex(@"(?(?si|consigo)(?=\s+(?:mesm|própri)[oa]s?(?![\p{L}\p{M}]))", Ci),
+ m => !YouToTuRules.SecondPersonClauseBefore(m.Result("$`")) ? m.Value
+ : m.Groups["p"].Value + CaseHelper.MatchCase(m.Groups["si"].Value, m.Groups["si"].Value.Equals("si", StringComparison.OrdinalIgnoreCase) ? "ti" : "contigo")),
+ // "Devias livrar-te desta gente, não se juntar a eles" → "não te juntar": a coordinated infinitive shares the person.
+ (new Regex(@"(?
(?se)\s+(?=\p{Ll}+[aeiô]r(?![\p{L}\p{M}-]))", Ci),
+ m => !YouToTuRules.SecondPersonClauseBefore(m.Result("$`"), requireTeClitic: true) ? m.Value
+ : m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["cl"].Value, "te") + " "),
+ // Existential past "tinha" with people: "Tinha muita gente neste andar" → "Havia muita gente neste andar".
+ (new Regex(SentenceStart + @"(?(?:mas|e|aqui|ali|lá|então)\s*,?\s+)?(?não\s+)?(?tinha)(?=\s+(?:(?:muita|tanta|pouca|bastante|mais|menos)\s+gente|gente|alguém|ninguém|(?:muitas|tantas|poucas|várias|umas|algumas|duas|três)\s+pessoas)" + NotWordAfter + @")(?![^.!?\n]*\?)", Ci),
+ m => m.Groups["pre"].Value + m.Groups["a"].Value + m.Groups["no"].Value + CaseHelper.MatchCase(m.Groups["w"].Value, "havia")),
// precisar + infinitivo → precisar de + infinitivo
(new Regex(NotWordBefore + @"(?preciso|precisas|precisa|precisamos|precisam|precisava|precisavas|precisávamos|precisavam|precisei|precisaste|precisou|precisámos|precisaram|precisar|precise|precises|precisem|precisaria|precisarias|precisariam|precisará|precisarás|precisarão|precisasse|precisares|precisarem)\s+(?=(?:(?:me|te|se|nos|lhe|o|a)\s+)?\p{L}*(?:ar|er|ir|ôr)(?!\p{L}))", Ci),
m => m.Groups["p"].Value + (CaseHelper.IsAllUpper(m.Groups["p"].Value) ? " DE " : " de ")),
@@ -2557,10 +2757,18 @@ public static string Apply(string s)
// ================================================================== você → tu
+ private static string RetornarEnding(string ending) => ending.ToLowerInvariant() switch
+ {
+ "ar" => "er", "o" => "o", "as" => "es", "a" => "e", "amos" => "emos", "am" => "em", "ei" => "i", "aste" => "este",
+ "ou" => "eu", "ámos" => "emos", "aram" => "eram", "ava" => "ia", "avas" => "ias", "avam" => "iam", "e" => "a", "es" => "as",
+ "em" => "am", "ará" => "erá", "arás" => "erás", "arei" => "erei", "aria" => "eria", "asse" => "esse", "ares" => "eres",
+ "arem" => "erem", "ando" => "endo", "ado" => "ido", _ => "er",
+ };
+
internal static class YouToTuRules
{
private const string You = "você";
- private const string Adverbs = "não|nunca|jamais|também|já|ainda|sempre|só|realmente|simplesmente|provavelmente|certamente|nem|apenas|agora|quase";
+ internal const string Adverbs = "não|nunca|jamais|também|já|ainda|sempre|só|realmente|simplesmente|provavelmente|certamente|nem|apenas|agora|quase|bem\\s+que";
internal const string ProclisisTriggers = "não|nunca|jamais|também|já|ainda|sempre|só|nem|apenas|quase|talvez|que|quem|ninguém|alguém|tudo|todos";
private static readonly Regex Contigo = new(NotWordBefore + @"com\s+você" + NotWordAfter, Ci);
@@ -2665,6 +2873,15 @@ private static string ReplaceLookAtYou(Match m)
private static readonly Regex ObjectYou = new(
@"(?" + NotWordBefore + @"(?:" + ProclisisTriggers + @")\s+)?" + NotWordBefore + @"(?\p{L}+)\s+você(?!\s+mesm)" + NotWordAfter, Ci);
+ // "acho você chique demais" / "Não acho você bonita": "você" is the object of a judging verb with a predicative.
+ private static readonly Regex JudgedYou = new(
+ @"(?" + NotWordBefore + @"(?:" + ProclisisTriggers + @")\s+)?" + NotWordBefore +
+ @"(?acho|achei|achava|achamos|achámos|acha|achou|achavam|acham|acharam|considero|considerei|considerava|julgo|julguei|julgava)\s+você\s+" +
+ @"(?(?:(?:muito|tão|bem|super|bastante|mesmo|meio|um\s+pouco)\s+)?(?\p{L}+))(?=\s*(?:[.!?,…;]|$)|\s+(?:demais|também|e)" + NotWordAfter + ")", Ci);
+
+ private static readonly Regex JudgedYouFiniteVerb = new(
+ @"^(?:é|era|foi|será|está|estava|tem|tinha|vai|ia|pode|podia|sabe|sabia|quer|queria|fala|faz|fez|gosta|precisa|deve|devia|devias|consegue|entende|merece|mente|mentiu|tá|disse|viu|fica|ficou)$", Ci);
+
private static readonly Regex SubjectYou = new(
@"(?" + NotWordBefore + @"(?:que|se|quando|porque|onde|como|quem|enquanto|embora|caso|talvez|quanto|qual|até\s+que|antes\s+que|depois\s+que)\s+)?" +
NotWordBefore + @"(?você|tu)(?(?:\s+(?:" + Adverbs + @")" + NotWordAfter + @"){0,2})\s+(?:(?se|me|nos|o|a|os|as|lhe|te)\s+)?(?\p{L}+)" + NotWordAfter + @"(?!-)", Ci);
@@ -2673,12 +2890,19 @@ private static string ReplaceLookAtYou(Match m)
private static readonly Regex YouInfinitive = new(
@"(?" + NotWordBefore + @"(?:se|de|para|por|sem|até|ao|após|antes\s+de|depois\s+de|maravilha|alegria|bom|ótimo|melhor|pior|importante|preciso|tempo|hora|caso|vez)\s+)você\s+(?\p{Ll}+(?:ar|er|ir|ôr))" + NotWordAfter + @"(?!\s+se" + NotWordAfter + ")", Ci);
+ // "Para você usá-lo" / "sem você ajudar-me" → "Para o usares" / "sem me ajudares".
+ private static readonly Regex YouEncliticInfinitive = new(
+ @"(?" + NotWordBefore + @"(?:para|pra|sem|até|apesar\s+de|em\s+vez\s+de|antes\s+de|depois\s+de)\s+)você\s+" +
+ @"(?:(?\p{Ll}+[áêôi])-(?lo|la|los|las)|(?\p{Ll}+(?:ar|er|ir))-(?me|nos|o|a|os|as))" + NotWordAfter + "(?!-)", Ci);
+
// "enfraquecer você" → "enfraquecer-te".
private static readonly Regex InfinitiveYou = new(NotWordBefore + @"(?\p{Ll}+(?:ar|er|ir))\s+você(?!\s+(?:mesm|e\s+(?:o|a|os|as|eu|ele|ela|\p{Lu}))" + NotWordAfter + ")" + NotWordAfter, Ci);
// "Você e o Rui" / "e você também" → "Tu e o Rui" / "e tu também".
private static readonly Regex YouAnd = new(NotWordBefore + @"(?você)(?=\s+(?:e\s+(?:o|a|os|as|eu|ele|ela|\p{Lu})|também)" + NotWordAfter + ")", Ci);
+ private static readonly Regex CopularBeforeYou = new(@"^(?:é|és|era|eras|foi|foste|será|seria|seja|fosse|sou|são|está|estava|esteve|ficou|fica|parece|parecia)$", Ci);
+
private static readonly Regex StandaloneYou = new(NotWordBefore + @"você(?=\s*(?:mesm[oa]\s*)?[.!?,…;:]|\s*$)", Ci);
private static readonly Regex WaitingForYou = new(
@@ -2725,7 +2949,31 @@ public static string Apply(string s, string? previousText = null, ReferenceHint?
}
s = ConversionConstructions.InformalPredicates(s, reference);
+ s = PronounRules.ReplaceCompoundObjects(s, includeYou: true);
+ s = PronounRules.ReplacePersonalInfinitiveObjects(s, includeYou: true);
s = LookAtYou.Replace(s, ReplaceLookAtYou);
+ s = YouEncliticInfinitive.Replace(s, m =>
+ {
+ string inf, clitic;
+ if (m.Groups["lw"].Success)
+ {
+ var w = m.Groups["lw"].Value;
+ inf = w.ToLowerInvariant() == "pô" ? "pôr" : w[..^1] + w[^1] switch { 'á' => "ar", 'ê' => "er", 'ô' => "or", _ => "ir" };
+ clitic = m.Groups["lcl"].Value[1..];
+ }
+ else
+ {
+ inf = m.Groups["inf"].Value;
+ clitic = m.Groups["cl"].Value;
+ }
+
+ if (!PortugueseVerbs.IsKnownInfinitive(inf) || PortugueseVerbs.IsFutureOrConditional(inf))
+ {
+ return m.Value;
+ }
+
+ return m.Groups["pre"].Value + clitic + " " + PortugueseVerbs.PersonalInfinitive(inf, "es");
+ });
s = YouInfinitive.Replace(s, m =>
{
var inf = m.Groups["inf"].Value;
@@ -2770,11 +3018,34 @@ public static string Apply(string s, string? previousText = null, ReferenceHint?
return PortugueseVerbs.AttachEnclitic(inf, CaseHelper.IsAllUpper(inf) ? "TE" : "te");
});
- s = YouAnd.Replace(s, m => CaseHelper.MatchCase(m.Groups["w"].Value, "tu"));
+ s = YouAnd.Replace(s, m =>
+ {
+ // "Nunca vi você também": after a transitive verb "você também" is the object, left to ObjectYou ("Nunca te vi também").
+ var before = Regex.Match(m.Result("$`"), @"([\p{L}\p{M}]+)\s+$");
+ if (m.Result("$'").TrimStart().StartsWith("também", StringComparison.OrdinalIgnoreCase) && before.Success &&
+ PortugueseVerbs.IsTransitiveForm(before.Groups[1].Value) && !NotObjectVerbs.Contains(before.Groups[1].Value) &&
+ !CopularBeforeYou.IsMatch(before.Groups[1].Value))
+ {
+ return m.Value;
+ }
+
+ return CaseHelper.MatchCase(m.Groups["w"].Value, "tu");
+ });
s = EmphaticYou.Replace(s, ReplaceEmphaticYou);
s = DativeYou.Replace(s, ReplaceDativeYou);
s = CommandYou.Replace(s, m => m.Groups["pre"].Value + m.Groups["v"].Value + " " + CaseHelper.MatchCase(m.Groups["you"].Value, "tu"));
s = StartCommandYou.Replace(s, m => m.Groups["pre"].Value + m.Groups["v"].Value + " " + CaseHelper.MatchCase(m.Groups["you"].Value, "tu"));
+ s = JudgedYou.Replace(s, m =>
+ {
+ if (JudgedYouFiniteVerb.IsMatch(m.Groups["w"].Value)) return m.Value;
+ var verb = m.Groups["v"].Value;
+ var te = CaseHelper.IsAllUpper(verb) ? "TE" : "te";
+ var trig = m.Groups["trig"].Value;
+ var head = trig.Length > 0 || PronounRules.AfterSubordinateSubject(m.Result("$`"))
+ ? trig + te + " " + verb
+ : PortugueseVerbs.AttachEnclitic(verb, te);
+ return head + " " + m.Groups["adj"].Value;
+ });
s = ObjectYou.Replace(s, ReplaceObject);
s = SubjectYou.Replace(s, m => ReplaceSubject(m, conversationalObjectConditional));
s = WaitingForYou.Replace(s, m =>
@@ -2796,16 +3067,96 @@ public static string Apply(string s, string? previousText = null, ReferenceHint?
{
if (reference.StartsWithSecondPersonSubject)
{
- s = ReferenceSubjectVerb.Replace(s, ReplaceReferenceSecondPersonVerb);
+ ClauseAgreementAnalysis? analysis = null;
+ s = ReferenceSubjectVerb.Replace(s, m => (analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed
+ ? ReplaceReferenceInitialVerb(m, reference) : m.Value);
}
if (reference.SecondPersonSubject && !reference.ThirdPerson)
{
- s = ReferenceSubordinateVerb.Replace(s, m => ReplaceReferenceSubordinateVerb(m, reference));
- s = ReferenceCausalVerb.Replace(s, ReplaceReferenceSecondPersonVerb);
+ ClauseAgreementAnalysis? analysis = null;
+ s = ReferenceSubordinateVerb.Replace(s, m => (analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed
+ ? ReplaceReferenceSubordinateVerb(m, reference) : m.Value);
+ analysis = null;
+ s = ReferenceCausalVerb.Replace(s, m => (analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed
+ ? ReplaceReferenceSecondPersonVerb(m) : m.Value);
+ }
+ if (reference.StartsWithSecondPersonSubject)
+ {
+ // "Sentir-se-á diferente" next to "You might feel differently" → "Sentir-te-ás diferente".
+ ClauseAgreementAnalysis? analysis = null;
+ s = ReferenceInitialMesoclisis.Replace(s, m => (analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["root"].Index, reference).Allowed
+ ? m.Groups["pre"].Value + m.Groups["root"].Value + CaseHelper.MatchCase(m.Groups["tail"].Value,
+ m.Groups["tail"].Value.ToLowerInvariant() == "-se-á" ? "-te-ás" : "-te-ias")
+ : m.Value);
+ analysis = null;
+ // Still proclitic at this stage: "Se sentirá diferente" → "Sentir-te-ás diferente".
+ s = ReferenceInitialProcliticFuture.Replace(s, m =>
+ {
+ var root = m.Groups["root"].Value;
+ if (Regex.IsMatch(root, @"^(?:ser|estar|haver|ir|ter)$", Ci) || !TuImperative.IsInfinitive(root.ToLowerInvariant()) ||
+ !(analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["root"].Index, reference).Allowed)
+ return m.Value;
+ var upper = CaseHelper.IsAllUpper(m.Value.Trim());
+ var lead = char.IsUpper(m.Groups["se"].Value[0]) ? char.ToUpperInvariant(root[0]) + root[1..] : root;
+ var tail = m.Groups["end"].Value.ToLowerInvariant() == "á" ? "-te-ás" : "-te-ias";
+ return m.Groups["pre"].Value + (upper ? (lead + tail).ToUpperInvariant() : lead + tail);
+ });
+ }
+ if (reference.SecondPersonSubject && !reference.ThirdPerson && EnglishCoordinatedYou.IsMatch(reference.EnglishText ?? ""))
+ {
+ // "Podias beneficiar-te..., mas recusou" next to "..., but you refused" → "mas recusaste".
+ ClauseAgreementAnalysis? analysis = null;
+ s = ReferenceCoordinatedVerb.Replace(s, m =>
+ {
+ var v = m.Groups["v"].Value;
+ if (ImpersonalWhatVerb.IsMatch(v) || CopularWhatVerb.IsMatch(v) || PortugueseVerbs.IsSecondPersonForm(v) ||
+ !(PortugueseVerbs.IsKnownVerbForm(v.ToLowerInvariant()) || RegularPreterite(v)) || InfinitiveOnly(v) ||
+ !SecondPersonClauseBefore(m.Result("$`")) || CoordinatedNotSameSubject(m) ||
+ !(analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed)
+ return m.Value;
+ return ReplaceReferenceSecondPersonVerb(m);
+ });
+ }
+ if (reference.SecondPersonSubject && EnglishWhYou.IsMatch(reference.EnglishText ?? "") &&
+ (!reference.ThirdPerson || !EnglishThirdSubject.IsMatch(reference.EnglishText ?? "")))
+ {
+ ClauseAgreementAnalysis? analysis = null;
+ s = ReferenceWhQuestionVerb.Replace(s, m =>
+ {
+ var v = m.Groups["v"].Value;
+ // "O que está acontecendo?" is about events even next to "What are you doing?".
+ if (ImpersonalWhatVerb.IsMatch(v) || CopularWhatVerb.IsMatch(v) || PortugueseVerbs.IsSecondPersonForm(v) ||
+ !PortugueseVerbs.IsKnownVerbForm(v.ToLowerInvariant()) || InfinitiveOnly(v) ||
+ m.Groups["cl"].Success && !PronominalVerb.IsMatch(v) ||
+ Regex.IsMatch(m.Result("$'"), @"^\s+(?:a\s+)?(?:acontec|pass|ocorr|rol|suced|hav|decorr)\p{L}*", Ci) ||
+ Regex.IsMatch(m.Result("$'"), @"^\s+(?:o|a|os|as|um|uma|seu|sua|teu|tua|meu|minha)\s", Ci) && UnaccusativeVerb.IsMatch(v) ||
+ !(analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed)
+ return m.Value;
+ var replaced = ReplaceReferenceSecondPersonVerb(m);
+ if (replaced == m.Value) return m.Value;
+ var cl = m.Groups["cl"];
+ return !cl.Success ? replaced
+ : replaced[..(cl.Index - m.Index)] + CaseHelper.MatchCase(cl.Value, "te") + replaced[(cl.Index - m.Index + cl.Length)..];
+ });
+ }
+ // "Ele quer sair contigo, se puder" next to "He wants to take you out if you're free": the nearest marker is "contigo".
+ if (EnglishInnerConditionalYou.IsMatch(reference.EnglishText ?? "") && (reference.SecondPersonSubject || reference.ThirdPerson) &&
+ (!reference.ThirdPerson || !EnglishInnerConditionalOther.IsMatch(reference.EnglishText ?? "")))
+ {
+ ClauseAgreementAnalysis? analysis = null;
+ s = ReferenceInnerConditionalVerb.Replace(s, m => !TuImperative.IsImpersonalConditional(m.Groups["v"].Value) &&
+ ConsistentInnerConditionalPrefix(m.Result("$`")) &&
+ (!reference.ThirdPerson || SecondPersonClauseBefore(m.Result("$`"))) &&
+ (analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed
+ ? ReplaceReferenceSecondPersonVerb(m) : m.Value);
}
if (reference.ConditionalYouSubject)
- s = ReferenceConditionalVerb.Replace(s, ReplaceReferenceSecondPersonVerb);
+ {
+ ClauseAgreementAnalysis? analysis = null;
+ s = ReferenceConditionalVerb.Replace(s, m => (analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed
+ ? ReplaceReferenceSecondPersonVerb(m) : m.Value);
+ }
if (reference.RepeatedYouSubject)
s = ConversionConstructions.ReportedPastIntention(s, reference);
}
@@ -2819,9 +3170,190 @@ public static string Apply(string s, string? previousText = null, ReferenceHint?
private static readonly Regex ReferenceSubjectVerb = new(
@"(?^[\s""“«'\uE000-\uF8FF]*(?:(?:não|sim)\s*[.!]\s*)?" +
@"(?:(?:então|bem|ora|bom|pois|mas|e|agora)[,;]?\s+)?" +
- @"(?:(?:talvez|provavelmente|certamente|possivelmente|não|nunca|já|ainda|só|apenas|também)\s+){0,3})" +
+ @"(?:(?:" + Adverbs + @"|talvez|possivelmente|ao\s+menos|pelo\s+menos)\s+){0,3})" +
@"(?\p{L}+)" + NotWordAfter + @"(?=\s|[.!?…]|$|[\uE000-\uF8FF])(?!-)", Ci);
+ // "Não deves ter percebido se só ouviu de passagem" next to "...if you just heard it": a mid-sentence condition about "you".
+ private static readonly Regex ReferenceInnerConditionalVerb = new(
+ @"(?(?<=\p{L}[,;]?\s+)(?:se\s+(?:(?:não|nunca|já|ainda|sempre|só|apenas|também)\s+){1,3}|se\s+(?=(?:puder|quiser|estiver|tiver|fizer|souber|vier|disser|trouxer|puser|couber|vir)" + NotWordAfter + @")|(?:quando|caso)\s+(?:(?:não|nunca|já|ainda|sempre|só|apenas|também)\s+){0,3}))" +
+ @"(?\p{L}+)" + NotWordAfter + @"(?!-)", Ci);
+ // Looks back through the sentence for the nearest person marker: "tu" verbs/pronouns win, "você"/third-person verbs stop.
+ internal static bool SecondPersonClauseBefore(string before, bool requireTeClitic = false)
+ {
+ var sentence = Regex.Match(before, @"[^.!?…]*$").Value;
+ var words = Regex.Matches(sentence, @"[\p{L}\p{M}]+(?:-[\p{L}\p{M}]+)*").Select(w => w.Value.ToLowerInvariant()).Reverse().Take(12);
+ foreach (var w in words)
+ {
+ if (w is "ele" or "ela" or "eles" or "elas" or "você" or "vocês" or "senhor" or "senhora" or "lhe" or "lhes" or "si" or "consigo" or "a gente")
+ return false;
+ if (w.EndsWith("-te") || w is "te" or "ti" or "contigo")
+ return true;
+ if (w is "para" or "pra" or "como") continue;
+ if (PortugueseVerbs.IsKnownVerbForm(w) && !PortugueseVerbs.IsInfinitive(w) && !PortugueseVerbs.IsSecondPersonForm(w) &&
+ PortugueseVerbs.TryThirdToSecond(w, out _))
+ return false;
+ if (requireTeClitic) continue;
+ if (w is "tu" or "teu" or "tua" or "teus" or "tuas" || PortugueseVerbs.IsSecondPersonForm(w))
+ return true;
+ }
+ return false;
+ }
+
+ // "porque pergunta se já sabe?" would become mixed address: only convert after a main clause that is already "tu".
+ private static bool ConsistentInnerConditionalPrefix(string before)
+ {
+ var sentence = Regex.Match(before, @"[^.!?…\n]*$").Value;
+ if (Regex.IsMatch(sentence, NotWordBefore + @"(?:você|vocês|si|consigo|senhor|senhora)" + NotWordAfter, Ci)) return false;
+ // The nearest person marker wins: "Vais ficar tão animado quando vir" (tu) vs "porque pergunta se já sabe" (você).
+ var words = Regex.Matches(sentence, @"(?\p{L}+)(?-(?:me|nos|lhe|lhes|o|a|os|as|te))?").Cast().Reverse();
+ foreach (var w in words)
+ {
+ var word = w.Groups["w"].Value;
+ var lower = word.ToLowerInvariant();
+ if (lower.Length < 2 || NotObjectVerbs.Contains(lower) || InfinitiveOnly(lower) ||
+ Regex.IsMatch(lower, @"(?:ado|ada|ido|ida|ados|idos)$") || ImpersonalWhatVerb.IsMatch(lower) || CopularWhatVerb.IsMatch(lower) ||
+ TuImperative.IsImpersonalConditional(lower) ||
+ Regex.IsMatch(lower, @"^(?:falta|faltava|faltou|basta|bastava|há|havia|houve|parece|parecia|acontece|aconteceu|é|era|foi|está|estava|esteve|fica|ficou|para|pra|como|entre|sobre|contra)$"))
+ continue;
+ if (PortugueseVerbs.IsSecondPersonForm(lower) || lower is "tu" or "te" or "ti" or "contigo" || w.Groups["cl"].Value == "-te" ||
+ Regex.IsMatch(lower, @"\p{L}{2,}(?:aste|este|iste)$"))
+ return true;
+ if (PortugueseVerbs.IsKnownVerbForm(lower) && TuImperative.ToTu(word, negated: true) is { } tu && !tu.Equals(word, StringComparison.OrdinalIgnoreCase)) return false;
+ if (!w.Groups["cl"].Success && PortugueseVerbs.IsKnownVerbForm(lower) && PortugueseVerbs.TryThirdToSecond(lower, out _)) return false;
+ }
+ return true;
+ }
+
+ // "você bem que me podias beijar" → "bem que me podias beijar": a leftover "você" subject of an already-tu verb.
+ private static readonly Regex VoceBeforeSecondPerson = new(
+ @"(?você)\s+" +
+ @"(?(?:(?:bem\s+que|até|mesmo|ainda|já|não|nunca|só|também|sempre|realmente|agora|nem)\s+){0,3}(?:(?:me|te|nos|lhe|lhes|o|a|os|as)\s+)?)" +
+ @"(?\p{L}+)" + NotWordAfter, Ci);
+
+ internal static string DropVoceBeforeSecondPerson(string s) => VoceBeforeSecondPerson.Replace(s, m =>
+ {
+ var v = m.Groups["v"].Value;
+ if (!PortugueseVerbs.IsSecondPersonForm(v) && !Regex.IsMatch(v, @"\p{L}{2,}(?:aste|este|iste)$", Ci))
+ return m.Value;
+ var rest = m.Groups["gap"].Value + v;
+ return char.IsUpper(m.Groups["voce"].Value[0]) ? char.ToUpperInvariant(rest[0]) + rest[1..] : rest;
+ });
+
+ // "Desculpe, porque vais apreender?" → "Desculpa, …": an opening interjection agrees with a tu sentence.
+ private static readonly Regex OpeningInterjection = new(
+ @"(?(?:^|[.!?…]\s+|\n|(?desculpe|olhe|ouça|oiça|escute|veja|espere|repare|perdoe)(?\s*,[^.!?…\n]*(?:\n[^.!?…\n-–—][^.!?…\n]*)*)", Ci);
+
+ private static readonly Dictionary InterjectionTu = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["desculpe"] = "desculpa", ["olhe"] = "olha", ["ouça"] = "ouve", ["oiça"] = "ouve", ["escute"] = "escuta",
+ ["veja"] = "vê", ["espere"] = "espera", ["repare"] = "repara", ["perdoe"] = "perdoa",
+ };
+
+ // "Adoras abrir-se demais" / "Tentas esconder-se": the infinitive shares the tu subject, so the reflexive is "te".
+ private static readonly Regex GovernedReflexive = new(
+ NotWordBefore + @"(?\p{L}+s)(?\s+(?:(?:de|a|em)\s+)?)(?\p{L}+(?:ar|er|ir|ôr|or))-(?se)" + NotWordAfter, Ci);
+
+ internal static string SecondPersonGovernedReflexive(string s) => GovernedReflexive.Replace(s, m =>
+ {
+ var v = m.Groups["v"].Value.ToLowerInvariant();
+ if (!PortugueseVerbs.IsSecondPersonForm(v) || !PortugueseVerbs.IsInfinitive(m.Groups["inf"].Value) ||
+ Regex.IsMatch(v, "^(?:deix|v[êie]|viste|ouv|mand|faz|fizeste|fazias|sent|és|eras|foste|estás|estavas)"))
+ return m.Value;
+ return m.Groups["v"].Value + m.Groups["mid"].Value + m.Groups["inf"].Value + "-" + CaseHelper.MatchCase(m.Groups["cl"].Value, "te");
+ });
+
+ internal static string OpeningInterjectionAgreement(string s) => OpeningInterjection.Replace(s, m =>
+ {
+ var rest = m.Groups["rest"].Value;
+ if (Regex.IsMatch(rest, NotWordBefore + @"(?:você|vocês|senhor|senhora|si|consigo)" + NotWordAfter, Ci) ||
+ !SecondPersonClauseBefore(rest.TrimStart(',', ' ')) && !Regex.IsMatch(rest, NotWordBefore + @"(?:tu|te|ti|contigo|teu|tua|teus|tuas)" + NotWordAfter + @"|-te" + NotWordAfter, Ci))
+ return m.Value;
+ return m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["w"].Value, InterjectionTu[m.Groups["w"].Value]) + rest;
+ });
+
+ // "nunca te sentavas quando vinha" → "quando vinhas": a when-clause right after a tu verb (no object between) shares its subject.
+ private static readonly Regex AdjacentWhen = new(
+ NotWordBefore + @"(?\p{L}+(?:-(?:te|me|nos|o|a|os|as|lhe|lhes))?)(?\s+(?:aqui|ali|aí|lá|cá|sempre|logo|bem|mal|cedo|tarde|primeiro))?" +
+ @"(?\s+(?:quando|enquanto|sempre\s+que|assim\s+que|logo\s+que)\s+(?:(?:não|nunca|já|ainda|sempre|só|também)\s+){0,2}(?:(?:me|te|nos)\s+)?)" +
+ @"(?\p{Ll}+)" + NotWordAfter + @"(?!-)(?!\s+(?:ele|ela|eles|elas|você|vocês|alguém|ninguém|o|a|os|as)" + NotWordAfter + ")", Ci);
+
+ internal static string AdjacentWhenClause(string s) => AdjacentWhen.Replace(s, m =>
+ {
+ var main = m.Groups["main"].Value;
+ var mainVerb = main.Split('-')[0];
+ if (!(PortugueseVerbs.IsSecondPersonForm(mainVerb) || Regex.IsMatch(mainVerb, @"\p{L}{2,}(?:aste|este|iste)$", Ci)) ||
+ !SecondPersonClauseBefore(m.Result("$`") + main))
+ return m.Value;
+ var v = m.Groups["v"].Value;
+ if (ImpersonalWhatVerb.IsMatch(v) || CopularWhatVerb.IsMatch(v) || TuImperative.IsImpersonalConditional(v) ||
+ PortugueseVerbs.IsSecondPersonForm(v) || InfinitiveOnly(v) ||
+ !(PortugueseVerbs.IsKnownVerbForm(v) || RegularPreterite(v)) ||
+ !PortugueseVerbs.TryThirdToSecond(v, out var second) && !PortugueseVerbs.TryGuessThirdToSecond(v, out second))
+ return m.Value;
+ return m.Value[..(m.Groups["v"].Index - m.Index)] + CaseHelper.MatchCase(v, second);
+ });
+
+ // "quer"/"requer" end in -er but are finite third-person forms.
+ private static bool InfinitiveOnly(string verb) => PortugueseVerbs.IsInfinitive(verb) && !Regex.IsMatch(verb, @"^(?:re|mal|bem)?quer$", Ci);
+
+ // "recusou" is not in the built-in lexicon; a lower-case -ou/-eu/-iu form whose -ar/-er/-ir infinitive is in the dictionary is a preterite.
+ private static bool RegularPreterite(string verb)
+ {
+ var m = Regex.Match(verb, @"^(?\p{Ll}{3,})(?ou|eu|iu)$");
+ return m.Success && TuImperative.IsInfinitive(m.Groups["stem"].Value + m.Groups["e"].Value[0] switch { 'o' => "ar", 'e' => "er", _ => "ir" });
+ }
+
+ private static readonly Regex ReferenceCoordinatedVerb = new(
+ @"(?(?:,\s*(?:(?:e|mas|porém)\s+)?|(?<=\p{L})\s+(?:e|mas|ou)\s+)(?:(?:não|nunca|já|ainda|sempre|também|só)\s+){0,2}(?:(?:me|nos)\s+)?)" +
+ @"(?\p{L}+)" + NotWordAfter + @"(?!-)", Ci);
+
+ // "Se achas que…, esquece" (a command after a condition), "…, está bem?" (a tag), "…, não tem volta" (impersonal).
+ private static bool CoordinatedNotSameSubject(Match m)
+ {
+ var v = m.Groups["v"].Value;
+ var after = m.Result("$'");
+ var sentence = Regex.Match(m.Result("$`"), @"[^.!?…\n]*$").Value;
+ // Only an affirmative present form can be a command; "Quando eras pequena, não conseguia" is a statement.
+ if (m.Groups["pre"].Value.TrimStart().StartsWith(',') &&
+ !Regex.IsMatch(m.Groups["pre"].Value, @"(?^[\s""“«'\uE000-\uF8FF]*(?:(?:então|bem|mas|e)[,;]?\s+)?)(?\p{L}+r)(?-se-(?:á|ia))(?![\p{L}\p{M}-])", Ci);
+
+ private static readonly Regex ReferenceInitialProcliticFuture = new(
+ @"(?^[\s""“«'\uE000-\uF8FF]*(?:(?:então|bem|mas|e)[,;]?\s+)?)(?se)\s+(?\p{L}+r)(?á|ia)(?![\p{L}\p{M}-])", Ci);
+
+ // "Mandy, como se sentiu quando...?" next to "how did you feel when...": the wh-question is about "you".
+ private static readonly Regex ReferenceWhQuestionVerb = new(
+ @"(?(?:^|[.!?…:;]\s+|\n|(?se)\s+|(?:me|nos|lhe|lhes|o|a|os|as)\s+)?)" +
+ @"(?\p{L}+)" + NotWordAfter + @"(?!-|\s+(?:ele|ela|eles|elas)" + NotWordAfter + @"|\s+(?:o|a|os|as)\s+(?-i:\p{Lu}))(?=[^.!?]*\?)", Ci);
+
+ // "Quando chegou a carta?": with these verbs the following noun is the subject, not an object.
+ private static readonly Regex UnaccusativeVerb = new(
+ @"^(?:cheg|começ|comec|acab|termin|morr|nasc|ca[iíu]|sa[iíu]|entr|volt|aparec|desaparec|fic|est|é$|er|fo|vei|vem|vin|parti|funcion|surg|mud|correu|corre|aconte|pass|dur|cust|val|sobr|falt|bast|import|interess|pareceu|parece|deu|dá|serv)", Ci);
+
+ // "Where did you leave him?": "him" is only an object, so the wh-question is still about "you".
+ private static readonly Regex EnglishThirdSubject = new(@"\b(?:he|she|they|it['’]s|it\s+(?:is|was|will|would|has|had|does|did))\b", Ci);
+
+ private static readonly Regex EnglishWhYou = new(
+ @"\b(?:how|where|why|what|when)\s+(?:the\s+(?:hell|heck|fuck)\s+)?(?:did|do|were|are|have|had|will|would|can|could)\s+you\b", Ci);
+
+ private static readonly Regex EnglishInnerConditionalYou = new(@"\S\s+(?:if|when|unless)\s+you\b", Ci);
+ private static readonly Regex EnglishInnerConditionalOther = new(@"\b(?:if|when|unless)\s+(?:he|she|they|it|we|I)\b", Ci);
+
private static readonly Regex ReferenceConditionalVerb = new(
@"(?^[\s""“«'\uE000-\uF8FF]*(?:(?:e|mas|ou|então|bem)[,;]?\s+)?" +
@"(?:se|quando|sempre\s+que|a\s+menos\s+que|assim\s+que)\s+" +
@@ -2829,9 +3361,20 @@ public static string Apply(string s, string? previousText = null, ReferenceHint?
@"(?\p{L}+)" + NotWordAfter + @"(?!-)", Ci);
private static readonly Regex ReferenceSubordinateVerb = new(
- @"(?(?(?(?:o|a|os|as)\s+(?:qual|quais)))\s+(?:(?:não|nunca|já|ainda|sempre|também|só|apenas)\s+){0,3})" +
@"(?\p{L}+)(?![\p{L}\p{M}-])", Ci);
+ // "questions you don't want": "you" is the subject of a relative clause, not the opening of the sentence.
+ private static readonly Regex EnglishRelativeYou = new(
+ @"\b(?!(?:if|when|because|and|but|or|so|unless|since|why|what|how|where|maybe|then|look|now|well|okay|ok|please|that's|thing|is|was|are|were)\b)\p{L}+\s+(?:that\s+|which\s+)?you\s+" +
+ @"(?:don['’]t|do\s+not|didn['’]t|can['’]t|cannot|won['’]t|wouldn['’]t|never|haven['’]t|want|need|know|knew|said|say|did|do|have|had|got|can|could|will|would)\b", Ci);
+
+ // "O que se passou?" / "O que se pode fazer?" are impersonal; only verbs that are pronominal for a person take "te".
+ private static readonly Regex PronominalVerb = new(
+ @"^(?:sent|sint|lembr|esquec|escond|preocup|magoa|machuc|chate|assust|zang|cans|despe|despi|vest|deit|levant|arrepend|divert|" +
+ @"apresent|cas|separ|junt|afast|livr|interess|import|sai|safa|safou|met|enfi|perd|aproxim|habitu|acostum|queix|vir|vira|tornou|torna|chama|chamou|atrev|sentou|senta|acalm|feri|fer|irrit|ofend|mex|mud)", Ci);
+
private static readonly Regex ImpersonalWhatVerb = new(
@"^(?:acontece|aconteceu|acontecia|acontecerá|aconteceria|aconteça|acontecesse)$", Ci);
@@ -2844,7 +3387,21 @@ private static string ReplaceReferenceSubordinateVerb(Match m, ReferenceHint ref
if (Regex.IsMatch(prefix, @"\b(?:quer|queres|queria|querias|querem|quiser)\s*$", Ci) &&
Regex.IsMatch(reference.EnglishText ?? "", @"\bwant(?:ed|s)?\s+me\s+to\b", Ci))
return m.Value;
- if (Regex.IsMatch(m.Groups["pre"].Value, @"\b(?:não|nunca|já|ainda|sempre|também|só|apenas)\b", Ci) &&
+ // "Não que estar certo faz…" / "tens que estar": a bare infinitive after "que" is not a personal one.
+ if (InfinitiveOnly(m.Groups["v"].Value) &&
+ Regex.IsMatch(prefix, @"(?\p{L}+)" + NotWordAfter, Ci);
+ // The cue as it was before conversion: an initial "Tenha"/"Saia" was a command, so its "Tem"/"Sai" must stay.
+ [ThreadStatic] internal static string? OriginalText;
+
+ private static string ReplaceReferenceInitialVerb(Match m, ReferenceHint reference)
+ {
+ var verb = m.Groups["v"].Value;
+ if (Regex.IsMatch(m.Result("$'"), @"^[\s.!…\uE000-\uF8FF]*$", Ci) &&
+ Regex.IsMatch(reference.EnglishText ?? "",
+ @"[.!?]\s*(?:it|he|she|they)\s+(?:does|did|is|was|has|will|can)(?:n't|\s+not)?[.!…\s]*$", Ci))
+ return m.Value;
+ if (verb.EndsWith("e", StringComparison.OrdinalIgnoreCase) &&
+ TuImperative.ToTu(verb) != null &&
+ Regex.IsMatch(m.Result("$'"), @"^\s+(?:\p{L}+ndo|a\s+\p{L}+(?:ar|er|ir)|você|tu)\b", Ci))
+ return m.Value;
+ // "Tem modos" / "Espera aqui" next to "You watch your manners" / "Get out": already a tu command, not "tens"/"esperas".
+ if (Regex.IsMatch(m.Groups["pre"].Value, @"^[\s""“«'\uE000-\uF8FF-]*$") && char.IsUpper(verb[0]) &&
+ OriginalText is { } original && Regex.Match(original, @"^[\s""“«'\uE000-\uF8FF-]*(?\p{L}+)") is { Success: true } first &&
+ !first.Groups["w"].Value.Equals(verb, StringComparison.OrdinalIgnoreCase) &&
+ TuImperative.ToTu(first.Groups["w"].Value) is { } converted && converted.Equals(verb, StringComparison.OrdinalIgnoreCase))
+ return m.Value;
+ // "Pergunte ao Bobby quando..." is a command: leave it to the imperative rules ("Pergunta ao Bobby").
+ if (Regex.IsMatch(m.Groups["pre"].Value, @"^[\s""“«'\uE000-\uF8FF-]*$") && char.IsUpper(verb[0]) &&
+ TuImperative.ToTu(verb) is { } command && !command.Equals(verb, StringComparison.OrdinalIgnoreCase) &&
+ !Regex.IsMatch(m.Result("$'"), @"^\s*\?"))
+ return m.Value;
+ if (verb.EndsWith("a", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(TuImperative.ToTu(verb[..^1] + "e"), verb, StringComparison.OrdinalIgnoreCase) &&
+ Regex.IsMatch(reference.EnglishText ?? "", @"\byou\s+(?:keep|continue|try)\b", Ci) &&
+ Regex.IsMatch(m.Groups["pre"].Value, @"^[\s""“«'\uE000-\uF8FF]*$", Ci) &&
+ Regex.IsMatch(m.Result("$'"), @"^\s+a\s+\p{L}+(?:ar|er|ir)\b", Ci))
+ return m.Value;
+ // "You'll have to ask Bobby" / "You need to find me" → "Pergunta ao Bobby" / "Vem procurar-me": a command, not "perguntas".
+ if (Regex.IsMatch(m.Groups["pre"].Value, @"^[\s""“«'\uE000-\uF8FF-]*$") && char.IsUpper(verb[0]) &&
+ PortugueseVerbs.TryThirdToSecond(verb, out var presentSecond) && presentSecond.Length > verb.Length &&
+ presentSecond.StartsWith(verb[..^1], StringComparison.OrdinalIgnoreCase) && !CopularWhatVerb.IsMatch(verb) &&
+ !Regex.IsMatch(verb, @"^(?:precisa|tem|deve|pode|vai|consegue|quer|sabe|está|anda|fica|continua|costuma|tenta)$", Ci) &&
+ Regex.IsMatch(reference.EnglishText ?? "", @"^[\W]*you(?:['’]ll\s+have\s+to|\s+will\s+have\s+to|\s+have\s+to|(?:['’]ve|\s+have)?\s+got\s+to|(?:['’]ve)?\s+gotta|\s+need\s+to|\s+should|\s+must|['’]d\s+better|\s+better)\b", Ci))
+ return m.Value;
+ if (!PortugueseVerbs.TryThirdToSecond(verb, out _) &&
+ !Regex.IsMatch(verb, @"(?:ou|eu|iu|ará|erá|irá)$", Ci))
+ return m.Value;
+ if (Regex.IsMatch(m.Value + m.Result("$'"),
+ @"^[\s""“«'\uE000-\uF8FF]*(?:desculpa|desculpe)[.!?…\s\uE000-\uF8FF]*$", Ci))
+ return m.Value;
+ if (CopularWhatVerb.IsMatch(verb) &&
+ !Regex.IsMatch(reference.EnglishText ?? "",
+ @"\byou(?:['’](?:re|ll\s+be|d\s+be|ve\s+been)\b|\s+(?:are|were|will\s+be|would\s+be|have\s+been|had\s+been)\b|\s+(?:not\s+)?(?:a|an)\s+\p{L})", Ci))
+ return m.Value;
+ if (CopularWhatVerb.IsMatch(verb) && Regex.IsMatch(reference.EnglishText ?? "",
+ @"[.!?;]\s*(?:he|she|it|that|this|they|there)(?:\b|['’])", Ci))
+ return m.Value;
+ return ReplaceReferenceSecondPersonVerb(m);
+ }
+
private static string ReplaceReferenceSecondPersonVerb(Match m)
{
var verb = m.Groups["v"].Value;
+ // "para ti e para os teus amigos": "para" is the verb "parar" only before "de", "com", "já", "aí" or a pause.
+ if (verb.Equals("para", StringComparison.OrdinalIgnoreCase) &&
+ !Regex.IsMatch(m.Result("$'"), @"^(?:\s*[.,!?…;:]|\s*$|\s+(?:de|com|já|aí|ali|lá|agora|logo|imediatamente)" + NotWordAfter + ")", Ci))
+ return m.Value;
if (PortugueseVerbs.IsSecondPersonForm(verb) || Regex.IsMatch(verb, @"(?:aste|este|iste)$", Ci))
{
return m.Value;
@@ -2946,10 +3564,11 @@ private static string ReplaceRememberWhen(Match m)
["mata"] = "mates", ["machuca"] = "magoes", ["magoa"] = "magoes", ["complica"] = "compliques", ["atrapalha"] = "atrapalhes",
["interrompe"] = "interrompas", ["mexe"] = "mexas", ["fuma"] = "fumes", ["usa"] = "uses", ["seja"] = "sejas",
["olhe"] = "olhes", ["finge"] = "finjas", ["acredita"] = "acredites", ["tenta"] = "tentes", ["toma"] = "tomes", ["xinga"] = "insultes",
+ ["goza"] = "gozes", ["estressa"] = "stresses", ["enche"] = "enchas", ["provoca"] = "provoques", ["empurra"] = "empurres",
};
private static readonly Regex NegativeImperative = new(
- @"(?(?:^|[.!?…:;]\s+|\n|(?(?:^|[.!?…:;]\s+|\n|(?não)\s+(?:(?me|se|te|nos|o|a|os|as|lhe)\s+)?(?\p{L}+)" + NotWordAfter + @"(?!-)(?[^.!?\n]*)(?[.!…]|$)", Ci);
private static string ReplaceNegativeImperative(Match m)
@@ -3139,8 +3758,12 @@ private static string ReplaceFormalSubject(Match m)
return m.Groups["sub"].Value + result;
}
+ private static readonly Regex YouBeforeInfinitive = new(@"^\s+(?:(?:me|te|se|nos|lhe)\s+)?[\p{L}\p{M}]+(?:ar|er|ir|ôr|por)(?![\p{L}\p{M}-])", Ci);
+ private static readonly Regex CausativeOrPerception = new(@"^(?:deix|mand|fa[zç]|fi[zc]|fe[zi]|v[eêi]|ou[vç]|s[ei]nt|obrig|forç|impe|proib|permit|lev|p[ôoõu]|ajud|ensin|convid|man[dt])", Ci);
+
private static string ReplaceObject(Match m)
{
+ if (!IsPersonalObjectVerb(m)) return m.Value;
var verb = m.Groups["v"].Value;
var trig = m.Groups["trig"].Value;
var te = CaseHelper.IsAllUpper(verb) ? "TE" : "te";
@@ -3156,6 +3779,12 @@ private static string ReplaceObject(Match m)
return m.Value;
}
+ // "esperei você me pedir" → "esperei pedires-me": "você" is the infinitive's subject unless the verb is causative or perceptive.
+ if (YouBeforeInfinitive.IsMatch(m.Result("$'")) && !CausativeOrPerception.IsMatch(verb))
+ {
+ return m.Value;
+ }
+
return trig.Length > 0 || PronounRules.AfterSubordinateSubject(m.Result("$`"))
? trig + te + " " + verb
: PortugueseVerbs.AttachEnclitic(verb, te);
@@ -3243,11 +3872,12 @@ internal static class ProgressiveRules
"estou|estás|está|estamos|estão|estava|estavas|estávamos|estavam|estive|estiveste|esteve|estivemos|estiveram|" +
"estarei|estarás|estará|estaremos|estarão|estaria|estarias|estaríamos|estariam|esteja|estejas|estejamos|estejam|" +
"estivesse|estivesses|estivéssemos|estivessem|estiver|estiveres|estivermos|estiverem|estar|estares|estarmos|estarem|estado|estando|" +
- "fico|ficas|fica|ficamos|ficam|ficava|ficavas|ficávamos|ficavam|fiquei|ficaste|ficou|ficámos|ficaram|ficar|fique|fiques|fiquem|ficarei|ficará|ficaria|" +
- "continuo|continuas|continua|continuamos|continuam|continuava|continuavam|continuei|continuou|continuaram|continuar|continue|continuem|" +
+ "fico|ficas|fica|ficamos|ficam|ficava|ficavas|ficávamos|ficavam|fiquei|ficaste|ficou|ficámos|ficaram|ficar|fique|fiques|fiquem|ficarei|ficará|ficaria|ficarias|" +
+ "continuo|continuas|continua|continuamos|continuam|continuava|continuavam|continuei|continuou|continuaram|continuar|continue|continues|continuem|" +
"continuarei|continuarás|continuará|continuaremos|continuarão|continuaria|continuarias|continuaríamos|continuariam|" +
- "ando|andas|anda|andamos|andam|andava|andavas|andavam|andei|andou|andaram|" +
- "vivo|vives|vive|vivemos|vivem|vivia|viviam";
+ "ando|andas|anda|andamos|andam|andava|andavas|andávamos|andavam|andei|andaste|andou|andámos|andaram|andar|andares|ande|andes|andem|andaria|andarias|andariam|andasse|andasses|" +
+ "continuavas|continuaste|continuámos|" +
+ "vivo|vives|vive|vivemos|vivem|vivia|vivias|viviam|vivi|viveste|viveu|viveram";
private const string Adverbs = "só|apenas|ainda|sempre|já|quase|mesmo|realmente|simplesmente|bem|mal|lá|aqui|aí|ali|agora|não|nunca|também|todo\\s+o\\s+tempo|literalmente|seriamente|meio|tipo|tudo|mais|meio\\s+que";
@@ -3269,7 +3899,7 @@ internal static class ProgressiveRules
// "Vi ele correndo" / "viu uns miúdos pegarem" → "vi-o a correr" / "viu uns miúdos a pegar".
private static readonly Regex Perception = new(
NotWordBefore + @"(?(?:vi|viu|viste|vimos|viram|vejo|vês|vê|vemos|veem|via|vias|víamos|viam|ver|vendo|ouvi|ouviu|ouviste|ouvimos|ouviram|ouço|ouves|ouve|ouvem|ouvia|ouviam|ouvir|senti|sentiu|sentimos|sentiram|sinto|sente|sentir|notei|notou|notaram|reparei|reparou|encontrei|encontrou|encontraram|encontrámos|encontramos|apanhei|apanhou|apanharam|peguei|pegou|pegaram|imagino|imagina|imaginei|imaginou|imaginaste|imaginar|imaginava|deixei|deixou|deixa|deixe)(?:-(?:o|a|os|as|me|te|nos|lo|la|los|las))?)" +
- @"(?(?:\s+(?!(?:que|se|quando|como|onde|porque|e|mas|ou|não|já)" + NotWordAfter + @")[\p{L}-]+){0,3}?)\s+(?\p{Ll}+(?:ando|endo|indo|ondo|arem|erem|irem|írem|orem))" + NotWordAfter, Ci);
+ @"(?(?:\s+(?!(?:que|se|quando|como|onde|porque|e|mas|ou|não|já)" + NotWordAfter + @")[\p{L}-]+){0,3}?)\s+(?:(?se|me|te|nos)\s+(?=\p{Ll}+ndo))?(?\p{Ll}+(?:ando|endo|indo|ondo|arem|erem|irem|írem|orem))" + NotWordAfter, Ci);
private static readonly HashSet NotPersonalInfinitives = new(StringComparer.OrdinalIgnoreCase)
{
@@ -3356,7 +3986,7 @@ private static string WithClitic(string infinitive, Group clitic) =>
clitic.Success ? PortugueseVerbs.AttachEnclitic(infinitive, clitic.Value) : infinitive;
private const string Clitic = @"(?:(?me|te|se|nos|lhe|lhes)\s+)?";
- private const string Gerund = @"(?\p{Ll}+(?:ando|endo|indo|ondo))" + NotWordAfter + "(?:-(?me|te|se|nos|lhe|lhes|o|a|os|as|lo|la|los|las)" + NotWordAfter + ")?";
+ private const string Gerund = @"(?\p{Ll}+(?:ando|endo|indo|ondo))(?:-(?me|te|se|nos|lhe|lhes|o|a|os|as|lo|la|los|las))?" + NotWordAfter;
// "Estou a abrir-me e tentando ser otimista" → "... e a tentar ser otimista".
private static readonly Regex Coordinated = new(
@@ -3372,7 +4002,8 @@ private static string WithClitic(string infinitive, Group clitic) =>
NotWordBefore + @"(?gente|alguém|ninguém|todos|todas|pessoas|pessoal|tipo|tipos|homem|homens|mulher|mulheres|miúdo|miúdos|miúda|miúdas|rapaz|rapazes|rapariga|raparigas|criança|crianças|" +
@"casal|casais|carro|carros|comboio|comboios|polícia|polícias|agente|agentes|soldados|guardas|ele|ela|eles|elas|nós|vocês|mensagem|mensagens|recado|bilhete|" +
@"carta|e-mail|email|nota|postal|cartaz|placa|aviso|vídeo|foto|fotografia|filho|filhos|filha|filhas|pai|mãe|pais|amigos|amigas|vizinhos|clientes|equipa|outros|outras|algo|alguma\s+coisa|coisa|coisas|nada|vigia|vigias|" +
- @"tu|ti|mim|eu|pessoa|cidadão|cidadãos|cara|caras|gajo|gajos|gaja|gajas|oficial|oficiais|grupo|unidade|modelos|loiras|loiros)\s+" +
+ @"tu|ti|mim|eu|pessoa|cidadão|cidadãos|cara|caras|gajo|gajos|gaja|gajas|oficial|oficiais|grupo|unidade|modelos|loiras|loiros|música|músicas)" +
+ @"(?(?:\s+(?:ainda|sempre|já|também|só|apenas)){0,2})\s+" +
Clitic + Gerund, Ci);
// "Há um elevador saindo do chão" → "Há um elevador a sair do chão".
@@ -3384,13 +4015,23 @@ private static string WithClitic(string infinitive, Group clitic) =>
private static readonly Regex TimeSpanGerund = new(
@"(?" + NotWordBefore + @"(?:(?:há|faz|durante|por)\s+(?:(?:quase|mais\s+de|uns|umas)\s+)?(?:\p{L}+|\d+)\s+(?:anos|meses|dias|semanas|horas|minutos)|(?:a|uma)\s+noite(?:\s+toda)?|o\s+dia(?:\s+todo)?|todo\s+o\s+dia|uma\s+hora|horas)" + NotWordAfter + @")\s+" + Clitic + Gerund, Ci);
+ private static readonly Regex LocativeGerund = new(
+ @"(?" + NotWordBefore + @"(?:no|na|nos|nas|num|numa)\s+" +
+ @"(?:(?:meu|minha|meus|minhas|teu|tua|seu|sua|nosso|nossa)\s+)?\p{L}+)" +
+ @"(?(?:\s+(?:ainda|sempre|já|também|só|apenas)){0,2})" +
+ @"(?\s+|,\s+(?=tirando\s+(?:fotos|fotografias)\b))" + Clitic + Gerund, Ci);
+ private static readonly HashSet NotLocativeGerunds = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "envolvendo", "incluindo", "excluindo", "contendo", "abrangendo", "considerando", "sendo", "tendo",
+ };
+
// "Uma vigia me mandando sair" → "Uma vigia a mandar-me sair": a pronoun before a gerund is never Portuguese.
private static readonly Regex CliticGerund = new(
@"(?" + NotWordBefore + @"(?!(?:vai|vou|vais|vamos|vão|ia|ias|íamos|iam|foi|fui|foram|vem|venho|vens|vêm|vinha|vinham|e|ou|não|já|que|se)" + NotWordAfter + @")\p{L}+)\s+(?me|te|nos|lhe|lhes)\s+" + Gerund, Ci);
// "- Recarregando!" / "Só conferindo." (answers) → "- A recarregar!" / "Só a conferir."
private static readonly Regex BareGerund = new(
- @"(?(?:^|[.!?…]\s+|\n|(?(?:só|apenas|ainda|sempre)\s+)?" + Clitic + Gerund + @"(?=(?:\s+[\p{L}\p{N}'-]+){0,3}\s*(?:[.!?…]|$))", Ci);
+ @"(?(?:^|[.!?…]\s+|\n|(?(?:só|apenas|ainda|sempre)\s+)?" + Clitic + @"(?\p{Ll}+(?:ando|endo|indo|ondo))" + NotWordAfter + @"(?=(?:\s+[\p{L}\p{N}'-]+){0,3}\s*(?:[.!?…]|$))", Ci);
private static readonly Regex LookAtPronoun = new(
NotWordBefore + @"(?a\s+)(?olhar)-(?me|te|se|nos|vos)(?=\s|[.!?,…]|$)", Ci);
@@ -3465,6 +4106,9 @@ public static string Apply(string s)
infinitive = g[..^2].Replace("ír", "ir");
}
+ // "Sinto o meu útero se rasgando" → "a rasgar-se".
+ if (m.Groups["cl"].Success)
+ infinitive = PortugueseVerbs.AttachEnclitic(infinitive, m.Groups["cl"].Value.ToLowerInvariant());
return m.Groups["v"].Value + m.Groups["obj"].Value + " a " + infinitive;
});
for (var i = 0; i < 2; i++)
@@ -3473,9 +4117,12 @@ public static string Apply(string s)
}
s = AdjectiveGerund.Replace(s, m => ReplaceGerund(m, m.Groups["adj"].Value + " "));
- s = NounGerund.Replace(s, m => ReplaceGerund(m, m.Groups["n"].Value + " "));
+ s = NounGerund.Replace(s, m => ReplaceGerund(m, m.Groups["n"].Value + m.Groups["adv"].Value + " "));
s = ThereIsGerund.Replace(s, m => ReplaceGerund(m, m.Groups["n"].Value + " "));
s = TimeSpanGerund.Replace(s, m => ReplaceGerund(m, m.Groups["n"].Value + " "));
+ s = LocativeGerund.Replace(s, m => NotLocativeGerunds.Contains(m.Groups["g"].Value)
+ ? m.Value
+ : ReplaceGerund(m, m.Groups["n"].Value + m.Groups["adv"].Value + m.Groups["gap"].Value));
s = CliticGerund.Replace(s, m => ReplaceGerund(m, m.Groups["n"].Value + " "));
s = BareGerund.Replace(s, m => NotBareGerunds.Contains(m.Groups["g"].Value)
? m.Value
@@ -3494,12 +4141,16 @@ public static string Apply(string s)
private static bool IsBlockedGerund(string word) => NotGerunds.Contains(word) || word is "Armando" or "ARMANDO";
- private static bool IsGerund(string word) => word.Length > 4 && !IsBlockedGerund(word);
+ // Only "pôr" compounds have an "-ondo" gerund ("propondo"); "respondo" / "escondo" are first-person presents.
+ private static bool IsGerund(string word) => word.Length > 4 && !IsBlockedGerund(word) &&
+ !(word.EndsWith("ondo", StringComparison.OrdinalIgnoreCase) &&
+ (!word.EndsWith("pondo", StringComparison.OrdinalIgnoreCase) || word.EndsWith("spondo", StringComparison.OrdinalIgnoreCase)));
private static string Replace(Match m)
{
var gerund = m.Groups["g"].Value;
- if (IsBlockedGerund(gerund) && !gerund.Equals("vendo", StringComparison.OrdinalIgnoreCase))
+ if (IsBlockedGerund(gerund) && !gerund.Equals("vendo", StringComparison.OrdinalIgnoreCase) ||
+ gerund.Length > 4 && !IsGerund(gerund) && !IsBlockedGerund(gerund))
{
return m.Value;
}
@@ -3526,6 +4177,105 @@ internal static string ToInfinitive(string gerund)
// ================================================================== pronoun placement
+ // "deixaste que te usassem" → "deixaste-te usar", "deixaste-os usarem-te" → "deixaste-os usar-te".
+ internal static class LetConstructions
+ {
+ private const string Lead = @"(?(?:^|[.!?…;:\n]\s*|[-–—]\s*|,\s*|(?(?:não|nunca|jamais|nem|já|também|só|ainda|sempre)\s+)?";
+
+ // English "you let yourself get used": the dropped subject of "deixou" is "tu".
+ private static readonly Regex DroppedYouLet = new(
+ Lead + Trig + @"(?deixou|deixava|deixará|deixaria)(?=\s+que\s+te\s)", RegexOptions.IgnoreCase);
+
+ private static readonly Regex ReflexiveLet = new(
+ @"(?não|nunca|jamais|nem|já|também|só|ainda|sempre|que|porque|se|quando)\s+)?" +
+ @"(?deixaste|deixas|deixavas|deixarás|deixarias|deixes|deixa)\s+que\s+te\s+(?[\p{L}\p{M}]+)(?![\p{L}\p{M}-])",
+ RegexOptions.IgnoreCase);
+
+ private static readonly Regex CausativePersonalInfinitive = new(
+ @"(?(?[\p{L}\p{M}]+?)(?arem|erem|irem|ares|eres|ires)(?=-|(?![\p{L}\p{M}]))",
+ RegexOptions.IgnoreCase);
+
+ private static readonly Dictionary IrregularSubjunctives = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["façam"] = "fazer", ["fizessem"] = "fazer", ["vejam"] = "ver", ["vissem"] = "ver", ["tragam"] = "trazer",
+ ["trouxessem"] = "trazer", ["digam"] = "dizer", ["dissessem"] = "dizer", ["ponham"] = "pôr", ["pusessem"] = "pôr",
+ ["ouçam"] = "ouvir", ["levem"] = "levar", ["sigam"] = "seguir", ["percam"] = "perder",
+ };
+
+ private static string? SubjunctiveToInfinitive(string verb)
+ {
+ if (IrregularSubjunctives.TryGetValue(verb, out var irregular))
+ {
+ return irregular;
+ }
+
+ var w = verb.ToLowerInvariant();
+ foreach (var (ending, inf) in new[] { ("assem", "ar"), ("essem", "er"), ("issem", "ir"), ("em", "ar"), ("am", "er"), ("am", "ir") })
+ {
+ if (w.EndsWith(ending, StringComparison.Ordinal) && w.Length > ending.Length + 1)
+ {
+ var candidate = w[..^ending.Length] + inf;
+ if (PortugueseVerbs.IsInfinitive(candidate) && PortugueseVerbs.IsTransitiveForm(candidate))
+ {
+ return candidate;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static string Apply(string s, ReferenceHint? reference)
+ {
+ if (Regex.IsMatch(reference?.EnglishText ?? "", @"\blet(?:s|ting)?\s+yourself\b", RegexOptions.IgnoreCase))
+ {
+ s = DroppedYouLet.Replace(s, m =>
+ {
+ var d = m.Groups["d"].Value;
+ var second = d.ToLowerInvariant() switch
+ {
+ "deixou" => "deixaste",
+ "deixava" => "deixavas",
+ "deixará" => "deixarás",
+ _ => "deixarias",
+ };
+ return m.Groups["lead"].Value + m.Groups["trig"].Value + CaseHelper.MatchCase(d, second);
+ });
+ }
+
+ s = ReflexiveLet.Replace(s, m =>
+ {
+ var inf = SubjunctiveToInfinitive(m.Groups["v"].Value);
+ if (inf == null)
+ {
+ return m.Value;
+ }
+
+ var d = m.Groups["d"].Value;
+ var upper = CaseHelper.IsAllUpper(d) && d.Length > 1;
+ var te = upper ? "TE" : "te";
+ if (upper)
+ {
+ inf = inf.ToUpperInvariant();
+ }
+
+ var trig = m.Groups["trig"];
+ return trig.Success
+ ? trig.Value + " " + te + " " + d + " " + inf
+ : PortugueseVerbs.AttachEnclitic(d, te) + " " + inf;
+ });
+
+ return CausativePersonalInfinitive.Replace(s, m =>
+ {
+ var end = m.Groups["end"].Value;
+ var inf = m.Groups["stem"].Value + end[..2];
+ return PortugueseVerbs.IsInfinitive(inf.ToLowerInvariant()) ? m.Groups["head"].Value + inf : m.Value;
+ });
+ }
+ }
+
internal static class PronounRules
{
private const string Start = @"(?(?:^|[.!?…:;]\s+|(?Se|SE)\s+(?!(?:se|esta|have|i)r(?:á|ia)\b)(?\p{L}+(?:rá|rás|rão|rei|remos|ria|rias|riam|ríamos))" + NotWordAfter + @"(?!-)", Cs);
+
private static readonly Regex ReflexiveCommand = new(
Start + @"(?se)\s+(?afaste|afastem|aproxime|aproximem|sente|sentem|levante|levantem|deite|deitem|" +
@"cale|calem|prepare|preparem|proteja|protejam|esconda|escondam|apresse|apressem|despache|despachem|" +
@@ -3608,6 +4361,30 @@ private static string ReplaceReflexiveCommand(Match m)
private static readonly Regex ModalClitic = new(
NotWordBefore + @"(?\p{L}+)(?-(?:me|te|nos|lhe))?\s+(?me|te|se|nos|vos|lhe|lhes)\s+(?\p{L}+(?:ar|er|ir|or|ôr))" + NotWordAfter + @"(?!-)", Ci);
+ // "deixou-o se vestir" / "deixa a menina se vestir" → "deixou-o vestir-se" / "deixa a menina vestir-se".
+ private static readonly Regex CausativeReflexive = new(
+ NotWordBefore + @"(?(?(?:me|te|nos)\s+)?(?(?:deix|mand|fa[zç]|fiz|fez|ouv|ouç|vi|vê|vej|v[eê]m)\p{L}*)(?-(?:o|a|os|as|lo|la|los|las|no|na|nos|me|te|lhe|lhes))?" +
+ @"(?\s+(?:o|a|os|as)\s+(?\p{L}+))?)\s+(?se)\s+(?\p{L}+(?:ar|er|ir)(?:em)?)" + NotWordAfter + @"(?!-)", Ci);
+
+ private static readonly Regex ReflexiveInfinitive = new(
+ @"^(?:sent|lembr|esquec|escond|preocup|magoa|machuc|chate|assust|zang|cans|despi|vest|deit|levant|arrepend|divert|apresent|cas|separ|junt|afast|" +
+ @"livr|interess|met|enfi|perd|aproxim|habitu|acostum|queix|vir|torn|cham|atrev|acalm|feri|irrit|ofend|mex|mud|afog|jog|atir|mat|suicid|rend|entreg|" +
+ @"cal|ajoelh|barbe|pente|lav|maqui|prepar|despedi|despe|comport|esforç|concentr|mov|aquec|molh|sec|sujar|limp|divorci|beij|abraç|odi|ama|conhec|ve|v[eê]r)", Ci);
+
+ private static string ReplaceCausativeReflexive(Match m)
+ {
+ var verb = m.Groups["v"].Value;
+ var infinitive = Regex.Replace(verb.ToLowerInvariant(), "em$", "");
+ if (!PortugueseVerbs.IsKnownVerbForm(m.Groups["cv"].Value.ToLowerInvariant()) || !PortugueseVerbs.IsInfinitive(infinitive) ||
+ !PortugueseVerbs.IsKnownInfinitive(infinitive) && !NaturaDictionary.Load(true).Check(infinitive) ||
+ !m.Groups["hy"].Success && !m.Groups["pro"].Success && !(m.Groups["np"].Success && !PortugueseVerbs.IsKnownVerbForm(m.Groups["n"].Value.ToLowerInvariant())) ||
+ !ReflexiveInfinitive.IsMatch(infinitive))
+ return m.Value;
+ // "mandou-nos se sentar" / "fazer-te se sentir": the object already names who → "mandou-nos sentar", "fazer-te sentir".
+ var held = m.Groups["hy"].Success ? m.Groups["hy"].Value.TrimStart('-').ToLowerInvariant() : m.Groups["pro"].Value.Trim().ToLowerInvariant();
+ return m.Groups["head"].Value + " " + (held is "me" or "te" or "nos" ? verb : PortugueseVerbs.AttachEnclitic(verb, m.Groups["cl"].Value));
+ }
+
// "obriga-os a te verem" / "dopar-te e depois te afogar" → "a verem-te" / "depois afogar-te".
private static readonly Regex LinkedInfinitiveClitic = new(
NotWordBefore + @"(?a|e\s+depois|depois|e|ou|mas)\s+(?me|te|se|nos|vos|lhe|lhes)\s+" +
@@ -3646,6 +4423,9 @@ private static string ReplaceLinkedInfinitive(Match m)
"deixo", "sei", "sabe", "sabes", "gostaria", "gostava", "costumo", "costuma", "acabei", "acabou", "comecei",
"começou", "tenho", "tens", "tem", "temos", "têm", "tinha", "vem", "tentar", "querer", "poder", "dever", "conseguir",
"começar", "costumar", "ousar", "precisar", "deixar", "saber", "continuar",
+ "adoro", "adoras", "adora", "adoramos", "adoram", "adorava", "adoravas", "adoravam", "odeio", "odeias", "odeia", "odiamos", "odeiam", "odiava",
+ "prefiro", "preferes", "prefere", "preferimos", "preferem", "preferia", "preferias", "detesto", "detestas", "detesta", "detestam",
+ "pretendo", "pretendes", "pretende", "pretendemos", "pretendem", "decidi", "decidiste", "decidiu", "decidimos", "decidiram",
};
// Infinitives that do not take a direct object ("Pode ser ele", "até chegar ele").
@@ -3804,11 +4584,11 @@ private static string AddStartArticle(Match m)
: m.Groups["pre"].Value + (m.Groups["pre"].Value.TrimEnd().EndsWith(',') || char.IsLower(poss[0]) ? article : CaseHelper.Capitalize(article)) + " " + poss.ToLowerInvariant() + " " + m.Groups["n"].Value;
}
- private static string ArticleFor(string possessive)
+ internal static string ArticleFor(string possessive)
{
var lower = possessive.ToLowerInvariant();
- return lower is "minha" or "tua" or "sua" or "nossa" ? "a"
- : lower is "minhas" or "tuas" or "suas" or "nossas" ? "as"
+ return lower is "minha" or "tua" or "sua" or "nossa" or "vossa" ? "a"
+ : lower is "minhas" or "tuas" or "suas" or "nossas" or "vossas" ? "as"
: lower.EndsWith('s') ? "os" : "o";
}
@@ -3960,6 +4740,15 @@ private static string AddInnerArticle(Match m)
{
return m.Value; // "pega nas tuas coisas" already has the article
}
+ // "Um beijo meu para ti" / "uma amiga minha": after a determiner + noun the possessive follows the noun.
+ if (prev.Length > 2 && !Conjunctions.Contains(prev) && !PlainPrepositions.Contains(prev) && !SerForms.Contains(prev) &&
+ !Regex.IsMatch(prev, @"^(?:que|quem|qual|como|onde|quando|quanto|mais|menos|tudo|isso|isto|disse|diz|fez|faz)$", Ci) &&
+ Regex.IsMatch(m.Result("$`"), @"(? 5 && Regex.IsMatch(prev, @"(?:ando|endo|indo)$", Ci) &&
!Regex.IsMatch(prev, @"^(?:quando|comando|brando|lindo|findo|contrabando|segundo)$", Ci);
- if (Vocatives.Contains(noun) && !PlainPrepositions.Contains(prev) && !subjectAfterConjunction && !objectOfVerb ||
+ if (Vocatives.Contains(noun) && !PlainPrepositions.Contains(prev) && prev.ToLowerInvariant() is not ("de" or "em" or "por") &&
+ !subjectAfterConjunction && !objectOfVerb ||
noun.Equals("Deus", StringComparison.Ordinal) ||
(poss.Equals("nossa", StringComparison.OrdinalIgnoreCase) && noun.Equals("senhora", StringComparison.OrdinalIgnoreCase)))
{
@@ -4008,7 +4798,17 @@ private static string AddInnerArticle(Match m)
: m.Value;
}
- if (PlainPrepositions.Contains(prev) || Conjunctions.Contains(prev) || LooksLikeVerbForm(prev) || TuImperative.IsCommand(prev) ||
+ // "Não ferva minhas bolas": a word straight after a negation is a verb even when the lexicon lacks that form.
+ var negatedVerb = Regex.IsMatch(m.Result("$`"), @"(? 3 && prev.EndsWith('o') && !Regex.IsMatch(prev, @"(?:ado|ido|ão)$", Ci) &&
+ !Regex.IsMatch(prev, @"^(?:claro|pronto|bravo|certo|exato|cuidado|isso|isto|aquilo|tudo|como|quando|mesmo|muito|pouco|tanto|outro|novo|todo|nosso|vosso)$", Ci) &&
+ !Regex.IsMatch(noun, @"^(?:para|de|e|que|com|por|em|é|está|foi)$", Ci) &&
+ (PortugueseVerbs.IsKnownInfinitive(prev[..^1] + "ar") || PortugueseVerbs.IsKnownInfinitive(prev[..^1] + "er") ||
+ PortugueseVerbs.IsKnownInfinitive(prev[..^1] + "ir") ||
+ Regex.IsMatch(m.Result("$`"), @"(?:^|[,;:.!?…]\s*|\n|(?(?:mand|deix|fa[zç]|fiz|vi|v[eê])\p{L}*-(?:l?[oa]s?|nos|vos)\s+)(?\p{Ll}+(?:a|e|i)r)em" + NotWordAfter, Ci);
- public static string Apply(string s)
+ // "tenha visto ele" → "o tenha visto", "ter encontrado você" → "ter-te encontrado": the clitic goes with the auxiliary.
+ private static readonly Regex CompoundObject = new(
+ @"(?ter|tenho|tens|tem|temos|têm|tinha|tinhas|tínhamos|tinham|tenha|tenhas|tenhamos|tenham|" +
+ @"tivesse|tivesses|tivéssemos|tivessem|tiver|tiveres|tivermos|tiverem|teria|terias|teríamos|teriam|terei|terás|terá|teremos|terão|tendo)\s+" +
+ @"(?[\p{L}\p{M}]+(?:ado|ido)|visto|feito|posto)\s+(?você|ele|ela|eles|elas)(?![\p{L}\p{M}-])(?!\s+(?:mesm[oa]s?|própri[oa]s?|[\p{L}\p{M}]+ndo|[\p{L}\p{M}]+(?:arem|erem|irem|ares|eres|ires))\b)",
+ RegexOptions.IgnoreCase);
+
+ private static readonly Regex CompoundProclisis = new(
+ @"(?(?o|a|os|as)\s+(?:(?:meu|minha|meus|minhas|teu|tua|teus|tuas|nosso|nossa|nossos|nossas)\s+)?)?(?eles|elas|ele|ela|nós|vocês|eu|tu|[\p{L}\p{M}]+)\s+(?(?:não|nunca|já|também|só)\s+)?)" +
+ @"(?[\p{L}\p{M}]+?(?:ar|er|ir)(?:em|es|mos)?)(?:\s+(?você|ele|ela|eles|elas)(?![\p{L}\p{M}-])(?:(?=\s+(?[\p{L}\p{M}]+)))?|-(?te|o|a|os|as|no|na|nas|me))(?![\p{L}\p{M}-])",
+ RegexOptions.IgnoreCase);
+
+ // "para usares ele" / "para usare-lo" / "para ajudarmos você" → "para o usares" / "para te ajudarmos":
+ // without a subject the personal ending identifies it, and the preposition still attracts the clitic.
+ private static readonly Regex DroppedSubjectInfinitiveObject = new(
+ @"(?(?(?:não|nunca|já|também|só)\s+)?)" +
+ @"(?:(?[\p{L}\p{M}]+?(?:ar|er|ir)(?:em|es|mos))(?:\s+(?você|ele|ela|eles|elas)(?![\p{L}\p{M}-])(?:(?=\s+(?[\p{L}\p{M}]+)))?|-(?