para)(?=\s+ninguém\b)", Ci);
+ private static readonly Regex PromiseRequest = new(
+ @"(?^[\s""“«'\p{Co}-]*)(?promete)(?=\s+(?:não\s+)?\p{L}+(?:ar|er|ir)\b[^.!?]*\?\s*[\p{Co}]*$)", Ci);
+
+ public static string PromiseQuestions(string text, ReferenceHint? reference) =>
+ reference != null && !reference.StartsWithSecondPersonSubject ? text :
+ PromiseRequest.Replace(text, m => m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["v"].Value, "prometes"));
+
+ public static string PromiseComplements(string text)
+ {
+ text = PromiseLaugh.Replace(text, m =>
+ {
+ var clitic = PromisePersons[m.Groups["v"].Value];
+ var subject = Regex.Match(m.Result("$`"),
+ @"\b(?eu|tu|ele|ela|nós|vocês|eles|elas)\s+(?:(?:não|nunca|sempre|também|já|ainda|só|realmente)\s+){0,3}$", Ci);
+ if (subject.Success)
+ clitic = subject.Groups["subject"].Value.ToLowerInvariant() switch { "eu" => "me", "tu" => "te", "nós" => "nos", _ => "se" };
+ if (clitic.Length == 0) return m.Value;
+ if (CaseHelper.IsAllUpper(m.Groups["v"].Value)) clitic = clitic.ToUpperInvariant();
+ var infinitive = m.Groups["inf"].Value;
+ return m.Groups["v"].Value + m.Groups["gap"].Value +
+ (m.Groups["neg"].Success ? m.Groups["neg"].Value + clitic + " " + infinitive
+ : infinitive + "-" + clitic);
+ });
+ return TellNobody.Replace(text, m => m.Groups["v"].Value + m.Groups["gap"].Value +
+ CaseHelper.MatchCase(m.Groups["prep"].Value, "a"));
+ }
+
public static string ReviewedVerbSenses(string text, ReferenceHint? reference, Func? isBlocked = null)
{
bool Allowed(string infinitive, string form) =>
isBlocked?.Invoke(infinitive) != true && isBlocked?.Invoke(form) != true;
var english = reference?.EnglishText ?? "";
+ text = SpeechContentVocabulary(text, english, isBlocked);
+ text = PerfectScores(text, english, isBlocked);
if (Regex.IsMatch(english, @"\b(?:chatter|wires|wiretap(?:ped|ping)?|intercept(?:ed|s|ing)?)\b", Ci))
text = InterceptConversation.Replace(text, m => Allowed("grampear", m.Value)
? CaseHelper.MatchCase(m.Value, InterceptForms[m.Value]) : m.Value);
@@ -1026,7 +1597,7 @@ bool Allowed(string infinitive, string form) =>
return CaseHelper.MatchCase(m.Value, StallForms[m.Value]) +
(CaseHelper.IsAllUpper(m.Value) ? " TEMPO" : " tempo");
});
- return text;
+ return LacearVocabulary(text, reference, isBlocked);
}
private static string Reflexive(string text, Match match, string verb, bool informal)
@@ -1217,6 +1788,74 @@ public static string Conversational(string text, ReferenceHint? reference)
Before + @"(?(?:(?eu|tu|ele|ela|nós|vocês|eles|elas)\s+)?" + Adverbs + @")" +
@"(?estou|estás|está|estamos|estão|estava|estavas|estávamos|estavam|estar|" + Alternatives(Forms("ficar")) + @")" +
@"(?\s+)de\s+saco\s+(?chei[oa]s?)" + After, Ci);
+ private static readonly Regex PainInTheNeck = new(
+ DialogueBefore + @"(?um|uma|uns|umas)(?\s+)(?p(?:é|és)(?:\s+|-)no(?:\s+|-)saco)" + After, Ci);
+ private static readonly Dictionary AnnoyForms = MapForms("encher", "chatear")
+ .Concat(MapForms("torrar", "chatear")).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
+ private static readonly Regex AnnoyingSack = new(
+ DialogueBefore + @"(?:(?me|te|nos|vos|lhe|lhes)\s+)?(?" + Alternatives(AnnoyForms.Keys) + @")" +
+ @"(?-(?:me|te|nos|vos|lhe|lhes))?\s+(?:o\s+)?(?:(?meu|teu|nosso|vosso)\s+)?saco" +
+ After + @"(?!\s+(?:de|com)\b)", Ci);
+ private static readonly Regex AnnoyReference = new(
+ @"\b(?:annoy(?:s|ed|ing)?|bother(?:s|ed|ing)?|pester(?:s|ed|ing)?|nag(?:s|ged|ging)?)\b|" +
+ @"\b(?:pain\s+in\s+the\s+(?:ass|arse|neck|butt)|get(?:s|ting)?\s+on\s+\w+\s+nerves)\b", Ci);
+
+ public static string AnnoyanceSlang(string text, ReferenceHint? reference, bool pronouns, Func isBlocked)
+ {
+ text = PainInTheNeck.Replace(text, m =>
+ {
+ if (isBlocked("pé no saco") || isBlocked("pés no saco")) return m.Value;
+ var before = Regex.Match(m.Result("$`"), @"[^.!?;]*$").Value;
+ var after = Regex.Match(m.Result("$'"), @"^[^.!?;]*").Value;
+ if (Regex.IsMatch(before, @"\b(?:pôs|colocou|meteu|enfiou|enfiar|meter|pôr|colocar|com|tirou|tirar)\s+$", Ci) ||
+ Regex.IsMatch(after, @"^\s+(?:de\s+(?:dormir|papel|plástico|lixo|batatas)|com\s+)", Ci))
+ return m.Value;
+ var feminine = m.Groups["det"].Value.EndsWith("a", StringComparison.OrdinalIgnoreCase) ||
+ m.Groups["det"].Value.EndsWith("as", StringComparison.OrdinalIgnoreCase);
+ var plural = m.Groups["det"].Value.EndsWith("s", StringComparison.OrdinalIgnoreCase);
+ var thing = Regex.IsMatch(before,
+ @"\b(?:isso|isto|aquilo|tudo|o\s+(?:trabalho|trânsito|processo|filme)|a\s+(?:burocracia|papelada|espera))" +
+ @"\s+(?:é|era|foi|será|seria|parece|tornou-se)\s+$", Ci);
+ var person = feminine || Regex.IsMatch(before, @"\b(?:eu|tu|ele|ela|nós|vocês|eles|elas)" +
+ @"\s+(?:sou|és|é|somos|são|era|eras|éramos|eram|foi|fui|foram|parece|parecem)\s+$", Ci) ||
+ Regex.IsMatch(after, @"^\s+(?:da\s+polícia|do\s+(?:FBI|departamento)|que\s+(?:me|te|nos)\s+)", Ci) ||
+ Regex.IsMatch(reference?.EnglishText ?? "", @"\b(?:he|she|you|they|guy|woman|man|cop|NYPD|FBI)\b", Ci);
+ var noun = thing ? "chatice" : person ? feminine ? "chata" : "chato" : "incómodo";
+ if (plural) noun += "s";
+ var det = thing ? plural ? "umas" : "uma" : person ? m.Groups["det"].Value.ToLowerInvariant() : plural ? "uns" : "um";
+ return CaseHelper.MatchCase(m.Groups["det"].Value, det) + m.Groups["gap"].Value +
+ CaseHelper.MatchCase(m.Groups["phrase"].Value, noun);
+ });
+ if (!pronouns) return text;
+ return AnnoyingSack.Replace(text, m =>
+ {
+ var verb = m.Groups["verb"].Value;
+ var infinitive = verb.StartsWith("torr", StringComparison.OrdinalIgnoreCase) ? "torrar" : "encher";
+ if (isBlocked(infinitive) || isBlocked(verb) || isBlocked("encher o saco") || isBlocked("torrar o saco"))
+ return m.Value;
+ var before = Regex.Match(m.Result("$`"), @"[^.!?;]*$").Value;
+ var after = m.Result("$'");
+ if (PortugueseVerbs.IsFutureOrConditional(verb) ||
+ Regex.IsMatch(after, @"^\s+(?:até|cheio|vazio|azul|preto|branco)\b", Ci) ||
+ Regex.IsMatch(reference?.EnglishText ?? "", @"\b(?:fill(?:s|ed|ing)?|roast(?:s|ed|ing)?)\b[^.!?]*\b(?:bag|sack)\b|" +
+ @"\b(?:money|profit|profits)\b", Ci)) return m.Value;
+ var hasClitic = m.Groups["pre"].Success || m.Groups["post"].Success;
+ if (hasClitic && m.Groups["poss"].Success) return m.Value;
+ var stopCommand = Regex.IsMatch(before, @"\b(?:para|pare|pares|parem|deixa|deixe|deixes|deixem)\s+de\s+$", Ci);
+ if (!hasClitic && !m.Groups["poss"].Success &&
+ !stopCommand && !AnnoyReference.IsMatch(reference?.EnglishText ?? "")) return m.Value;
+ var target = CaseHelper.MatchCase(verb, AnnoyForms[verb]);
+ if (m.Groups["pre"].Success) return m.Groups["pre"].Value + " " + target;
+ if (m.Groups["post"].Success) return PortugueseVerbs.AttachEnclitic(target, m.Groups["post"].Value[1..]);
+ if (m.Groups["poss"].Success)
+ {
+ var clitic = m.Groups["poss"].Value.ToLowerInvariant() switch
+ { "meu" => "me", "teu" => "te", "nosso" => "nos", _ => "vos" };
+ return clitic + " " + target;
+ }
+ return target;
+ });
+ }
private static readonly Regex VerbalIntensifier = new(
Before + @"(?" + Alternatives(new[] { "falar", "trabalhar", "estudar", "correr" }.SelectMany(Forms)) + @")" +
@"(?\s+)(?:pra|para)\s+caramba" + After, Ci);
@@ -1237,12 +1876,57 @@ public static string Conversational(string text, ReferenceHint? reference)
["semana"] = "week", ["semanas"] = "weeks", ["mês"] = "month", ["meses"] = "months",
};
private static readonly Regex QuantifiedPoliceNoun = new(
- Before + @"(?(?:(?:muito|muitos|muitas|bem|ainda|cada\s+vez)\s+)?(?:mais|menos)|" +
+ Before + @"(?(?:há|havia|houve|haverá|haveria|haja|houvesse|houver)(?:\s+(?:ainda|já|também|sempre|só|apenas))?|" +
+ @"(?:(?:muito|muitos|muitas|bem|ainda|cada\s+vez)\s+)?(?:mais|menos)|" +
@"(?:alguns|algumas|vários|várias|muitos|muitas|poucos|poucas|tantos|tantas|quantos|quantas)|\d+)" +
@"(?\s+)(?policiais)" + After, Ci);
- public static string PoliceNouns(string text) => QuantifiedPoliceNoun.Replace(text, m =>
- m.Groups["quantity"].Value + m.Groups["gap"].Value + CaseHelper.MatchCase(m.Groups["noun"].Value, "polícias"));
+ private static readonly Regex PrepositionalPoliceNoun = new(
+ Before + @"(?para|pra|com|sem|entre|perante|por|a|em)(?\s+)" +
+ @"(?policiais)" + After +
+ @"(?=\s*(?:[,.;:!?…\uE000-\uF8FF]|$)|\s+(?:e|ou|que|como|do|da|dos|das|" +
+ @"corruptos|corruptas|honestos|honestas|armados|armadas|fardados|fardadas|" +
+ @"uniformizados|uniformizadas|feridos|feridas|mortos|mortas|suficientes|" +
+ @"presentes|envolvidos|envolvidas|disfarçados|disfarçadas)\b)", Ci);
+
+ public static string PoliceNouns(string text)
+ {
+ text = QuantifiedPoliceNoun.Replace(text, m =>
+ m.Groups["quantity"].Value + m.Groups["gap"].Value + CaseHelper.MatchCase(m.Groups["noun"].Value, "polícias"));
+ return PrepositionalPoliceNoun.Replace(text, m =>
+ {
+ if (m.Groups["prep"].Value.Equals("a", StringComparison.OrdinalIgnoreCase) &&
+ Regex.IsMatch(text[..m.Index], @"\b(?:com|sem|para|pra|por|entre|perante|em)\s+$", Ci))
+ return m.Value;
+ return m.Groups["prep"].Value + m.Groups["gap"].Value + CaseHelper.MatchCase(m.Groups["noun"].Value, "polícias");
+ });
+ }
+
+ private static readonly Regex PossessiveDoorway = new(
+ Before + @"(?na|nas)(?\s+)(?minha|tua|sua|nossa|vossa|minhas|tuas|suas|nossas|vossas)" +
+ @"(?\s+)(?portas?)" + After, Ci);
+ private static readonly Regex AtDoorReference = new(
+ @"\bat\s+(?:(?:my|your|his|her|our|their|the)\s+)(?:front\s+|back\s+)?doors?\b", Ci);
+
+ public static string DoorwayLocation(string text, ReferenceHint? reference) =>
+ PossessiveDoorway.Replace(text, m =>
+ {
+ var before = Regex.Match(m.Result("$`"), @"[^.!?;]*$").Value;
+ var after = Regex.Match(m.Result("$'"), @"^[^.!?;]*").Value;
+ var literalSurface = Regex.IsMatch(before + after,
+ @"\b(?:marcas?|buracos?|riscos?|sangue|tinta|cartaz(?:es)?|avisos?|placas?|números?|fechaduras?|maçanetas?|chaves?|desenhos?|pintad[oa]s?|colad[oa]s?|escrit[oa]s?)\b", Ci);
+ if (literalSurface) return m.Value;
+ var presence = Regex.IsMatch(before,
+ @"\b(?:há|havia|houve|haverá|haveria|está|estão|estava|estavam|ficou|ficaram|espera|esperam|esperava|esperavam)" +
+ @"\s+(?:(?:um|uma|uns|umas|o|a|os|as|algum|alguma|alguns|algumas|mais|menos|dois|duas|três)\s+)?" +
+ @"(?:polícias?|policiais|agentes?|pessoas?|homens?|mulheres|mulher|rapazes|raparigas|visitas?|alguém|ninguém)\s+$", Ci) ||
+ Regex.IsMatch(before, @"\b(?:eu|tu|ele|ela|nós|vocês|eles|elas|alguém|ninguém|polícias|agentes|pessoas|homens|mulheres)" +
+ @"\s+(?:estou|estás|está|estamos|estão|estava|estavas|estávamos|estavam|espera|esperam|esperava|esperavam)\s+$", Ci);
+ if (!presence && !AtDoorReference.IsMatch(reference?.EnglishText ?? "")) return m.Value;
+ return CaseHelper.MatchCase(m.Groups["prep"].Value,
+ m.Groups["prep"].Value.Equals("nas", StringComparison.OrdinalIgnoreCase) ? "às" : "à") +
+ m.Groups["gap"].Value + m.Groups["poss"].Value + m.Groups["space"].Value + m.Groups["door"].Value;
+ });
private static readonly Regex SlowPeriod = new(
Before + @"(?" + Alternatives(SlowPeriodReferences.Keys) + @")(?\s+)(?devagar)" +
@@ -1487,6 +2171,14 @@ public static string GroupApologyPlural(string text) =>
GroupApology.Replace(text, m => m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["v"].Value,
m.Groups["v"].Value.StartsWith("d", StringComparison.OrdinalIgnoreCase) ? "desculpem" : "perdoem"));
+ private static readonly Regex ArrivalApology = new(
+ @"(?(?:^|\n)[\s\uE000-\uF8FF-]*(?:(?:ei|olá|oi|boa\s+noite|bom\s+dia|boa\s+tarde)[!,.]\s*)?)" +
+ @"(?desculpa|desculpe|perdoa|perdoe)(?[!.\uE000-\uF8FF\s]*$)", Ci);
+ public static string ArrivalGroupApology(string text) => ArrivalApology.Replace(text, m =>
+ m.Groups["head"].Value + CaseHelper.MatchCase(m.Groups["verb"].Value,
+ m.Groups["verb"].Value.StartsWith("d", StringComparison.OrdinalIgnoreCase) ? "desculpem" : "perdoem") +
+ m.Groups["end"].Value);
+
public static string CausativeYouPlural(string text)
{
if (text.IndexOf("vocês", StringComparison.OrdinalIgnoreCase) < 0) return text;
@@ -1508,8 +2200,58 @@ public static string CausativeYouPlural(string text)
});
}
+ private static readonly Dictionary WealthToFortune = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["sou"] = "tenho", ["és"] = "tens", ["é"] = "tem", ["somos"] = "temos", ["são"] = "têm",
+ ["era"] = "tinha", ["eras"] = "tinhas", ["éramos"] = "tínhamos", ["eram"] = "tinham",
+ ["fui"] = "tive", ["foste"] = "tiveste", ["foi"] = "teve", ["fomos"] = "tivemos", ["foram"] = "tiveram",
+ ["serei"] = "terei", ["serás"] = "terás", ["será"] = "terá", ["seremos"] = "teremos", ["serão"] = "terão",
+ ["seria"] = "teria", ["serias"] = "terias", ["seríamos"] = "teríamos", ["seriam"] = "teriam",
+ ["seja"] = "tenha", ["sejas"] = "tenhas", ["sejamos"] = "tenhamos", ["sejam"] = "tenham",
+ ["fosse"] = "tivesse", ["fosses"] = "tivesses", ["fôssemos"] = "tivéssemos", ["fossem"] = "tivessem",
+ ["for"] = "tiver", ["fores"] = "tiveres", ["formos"] = "tivermos", ["forem"] = "tiverem",
+ ["ser"] = "ter", ["seres"] = "teres", ["sermos"] = "termos", ["serem"] = "terem", ["sendo"] = "tendo",
+ };
+ private static readonly Regex RichInPeople = new(
+ Before + @"(?" + Alternatives(WealthToFortune.Keys) + @")(?\s+)ric[oa]s?\s+(?:de|em)\s+" +
+ @"(?(?:(?:os|as|meus|minhas|teus|tuas|seus|suas|nossos|nossas|bons|boas|grandes|verdadeiros|verdadeiras)\s+){0,3}" +
+ @"(?:parceir[oa]s|amig[oa]s|companheir[oa]s|colegas|aliad[oa]s|família))" + After, Ci);
+ private static readonly Regex RichInPeopleReference = new(
+ @"\brich\s+(?:in|with)\s+(?:(?:my|your|his|her|our|their|good|great|close|true|loyal)\s+){0,3}" +
+ @"(?:partners|friends|companions|colleagues|allies|family)\b" +
+ @"(?!['’]|\s+(?:money|wealth|fortune|income|profits|assets)\b)", Ci);
+ private static readonly Regex LifePageantReference = new(
+ @"\blife['’]s\s+rich\s+(?:pageant|tapestry)\b", Ci);
+ private static readonly Regex LifePageant = new(
+ Before + @"(?(?:o|a|do|da|no|na|ao|à|pelo|pela)\s+)?(?ric[oa]\s+(?:trama|tapeçaria|desfile|cortejo|espetáculo)\s+da\s+vida)" +
+ After, Ci);
+
+ private static string LifePageantVocabulary(string text, ReferenceHint? reference)
+ {
+ if (!LifePageantReference.IsMatch(reference?.EnglishText ?? "")) return text;
+ return LifePageant.Replace(text, m =>
+ {
+ var phrase = m.Groups["phrase"].Value;
+ var before = m.Result("$`");
+ if (Regex.IsMatch(before, @"\b(?:um|uma|este|esta|esse|essa|aquele|aquela|todo|toda)\s+$", Ci))
+ return m.Value;
+ var article = m.Groups["article"].Value.Trim().ToLowerInvariant() switch
+ {
+ "do" or "da" => "do",
+ "no" or "na" => "no",
+ "ao" or "à" => "ao",
+ "pelo" or "pela" => "pelo",
+ _ => "o",
+ };
+ var target = article + " rico espetáculo da vida";
+ var original = m.Groups["article"].Success ? m.Value : phrase;
+ return CaseHelper.MatchCase(original, target);
+ });
+ }
+
public static string Idioms(string text, ReferenceHint? reference)
{
+ text = LifePageantVocabulary(text, reference);
text = WrongAsAdverb(text);
text = ScoreDrugsInText(text, reference);
text = SlowPeriod.Replace(text, m =>
@@ -1529,6 +2271,10 @@ public static string Idioms(string text, ReferenceHint? reference)
text = LetsGoExpression.Replace(text, m => m.Groups["head"].Value +
CaseHelper.MatchCase(m.Groups["phrase"].Value, "vamos a isso"));
var english = reference?.EnglishText;
+ if (RichInPeopleReference.IsMatch(english ?? ""))
+ text = RichInPeople.Replace(text, m => CaseHelper.MatchCase(m.Groups["v"].Value,
+ WealthToFortune[m.Groups["v"].Value]) + m.Groups["gap"].Value +
+ (CaseHelper.IsAllUpper(m.Value) ? "SORTE COM " : "sorte com ") + m.Groups["people"].Value);
if (english != null)
{
var plain = ReferenceAligner.Clean(english).Replace('’', '\'');
diff --git a/se5/BrazilianToEuropeanPortuguese/ConversionTrace.cs b/se5/BrazilianToEuropeanPortuguese/ConversionTrace.cs
index 19d58388..76088100 100644
--- a/se5/BrazilianToEuropeanPortuguese/ConversionTrace.cs
+++ b/se5/BrazilianToEuropeanPortuguese/ConversionTrace.cs
@@ -3,4 +3,9 @@ namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
public sealed record RuleChange(string Id, string Description, string Basis = "Rule-based");
public sealed record CueConversion(string Text, ReferenceHint? Hint, IReadOnlyList Rules,
- int SentenceGroup, string? ContextNote);
+ int SentenceGroup, string? ContextNote)
+{
+ internal SentenceMeaning? MeaningSentence { get; init; }
+}
+
+internal sealed record SentenceMeaning(int First, int Last, string Original, string? Reference);
diff --git a/se5/BrazilianToEuropeanPortuguese/DiscourseTags.cs b/se5/BrazilianToEuropeanPortuguese/DiscourseTags.cs
new file mode 100644
index 00000000..fba0ee93
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/DiscourseTags.cs
@@ -0,0 +1,55 @@
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal static class DiscourseTags
+{
+ private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+ private static readonly Regex Tag = new(
+ @"(?,\s*(?:não\s+)?)(?sabe|entende|percebe|compreende|viu|ouviu|entendeu|percebeu)" +
+ @"(?=\s*\?+[.!?]*[\s""”»'\uE000-\uF8FF]*$)", Ci);
+ private static readonly Dictionary Informal = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["sabe"] = "sabes", ["entende"] = "entendes", ["percebe"] = "percebes", ["compreende"] = "compreendes",
+ ["viu"] = "viste", ["ouviu"] = "ouviste", ["entendeu"] = "entendeste", ["percebeu"] = "percebeste",
+ };
+
+ internal static bool HasInformalEvidence(string text)
+ {
+ if (PortugueseConverter.YouToTuRules.IsFormal(text) ||
+ Regex.IsMatch(text, @"(?m)^\s*[-–—]|\b(?:você|vocês|vós)\b|[""«»“”]", Ci)) return false;
+ if (Regex.IsMatch(text, @"(?
+ PortugueseVerbs.IsSecondPersonForm(m.Value) && analysis.Decide(m.Index, null).Allowed);
+ }
+
+ internal static string Apply(string text, ReferenceHint? hint)
+ {
+ if (hint is { FormalAddress: true } && hint is not { ForceInformal: true } ||
+ PortugueseConverter.YouToTuRules.IsFormal(text) ||
+ Regex.IsMatch(text, @"(?m)^\s*[-–—]|\b(?:vocês|vós)\b|[""«»“”]", Ci)) return text;
+ var tag = Tag.Match(text);
+ if (!tag.Success || tag.Index == 0) return text;
+ var english = hint?.EnglishText;
+ if (!string.IsNullOrWhiteSpace(english))
+ {
+ var pattern = tag.Groups["verb"].Value.ToLowerInvariant() switch
+ {
+ "sabe" => @"you\s+know",
+ "viu" => @"(?:you\s+see|see)",
+ "ouviu" => @"(?:you\s+hear(?:\s+me)?|hear\s+me)",
+ _ => @"(?:you\s+(?:understand|get\s+it)|(?:do|did)\s+you\s+(?:understand|get\s+it))",
+ };
+ if (!Regex.IsMatch(english, @",\s*(?:" + pattern + @")\s*\?+[.!?]*\s*$", Ci))
+ return text;
+ }
+ if (hint is not { ForceInformal: true } && hint is not { InformalTagContinuation: true } &&
+ !HasInformalEvidence(text[..tag.Index])) return text;
+ var verb = tag.Groups["verb"];
+ return text[..verb.Index] + CaseHelper.MatchCase(verb.Value, Informal[verb.Value]) +
+ text[(verb.Index + verb.Length)..];
+ }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/EnglishPraiseIdioms.cs b/se5/BrazilianToEuropeanPortuguese/EnglishPraiseIdioms.cs
new file mode 100644
index 00000000..0c5f1540
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/EnglishPraiseIdioms.cs
@@ -0,0 +1,104 @@
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal static class EnglishPraiseIdioms
+{
+ private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+ private static readonly Regex EnglishAuthenticity = new(
+ @"\b(?he|she|they)(?:['’](?s|re)|\s+(?is|are|was|were))\s+" +
+ @"(?:(?:really|truly|definitely)\s+)?the\s+(?real\s+deal|genuine\s+article|real\s+McCoy)\b", Ci);
+ private static readonly Regex PersonalLiteral = new(
+ @"(?ele|ela|eles|elas)(?\s+)(?é|são|era|eram)(?\s+))?" +
+ @"(?(?:o\s+)?negócio\s+(?:real|verdadeiro)|a\s+coisa\s+(?:real|verdadeira)|" +
+ @"o\s+artigo\s+(?:genuíno|verdadeiro)|verdadeir[oa]s?)" +
+ @"(?![\p{L}\p{M}\d_-])(?=\s*(?:[,!.?…;:]|$|[\uE000-\uF8FF]))", Ci);
+ private static readonly Regex EnglishExpertise = new(
+ @"\b(?I|you|he|she|we|they|(?:this|that|these|those)\s+(?:guys?|men|women|girls?|boys?))\s+" +
+ @"(?:(?:really|certainly|definitely|always|never|sure)\s+){0,2}(?:know|knows|knew)\s+" +
+ @"(?:(?:really|certainly|definitely)\s+)?(?my|your|his|her|our|their)\s+stuff\b" +
+ @"(?!\s+(?:is|was|are|were|belongs|belonged)\b)", Ci);
+ private static readonly Regex LiteralExpertise = new(
+ @"(?conheço|conheces|conhece|conhecemos|conhecem|" +
+ @"conhecia|conhecias|conhecíamos|conheciam|sei|sabes|sabe|sabemos|sabem|sabia|sabias|sabíamos|sabiam)" +
+ @"(?\s+)(?(?:(?:mesmo|realmente|bem)\s+){0,2})(?:as\s+)?" +
+ @"(?:minhas|tuas|suas|nossas|vossas)\s+coisas(?![\p{L}\p{M}\d_-])" +
+ @"(?=\s*(?:[,!.?…;:]|$|[\uE000-\uF8FF]))", Ci);
+ private static readonly Dictionary ExpertiseForms = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["conheço"] = ("sei", "faço"), ["sei"] = ("sei", "faço"),
+ ["conheces"] = ("sabes", "fazes"), ["sabes"] = ("sabes", "fazes"),
+ ["conhece"] = ("sabe", "faz"), ["sabe"] = ("sabe", "faz"),
+ ["conhecemos"] = ("sabemos", "fazemos"), ["sabemos"] = ("sabemos", "fazemos"),
+ ["conhecem"] = ("sabem", "fazem"), ["sabem"] = ("sabem", "fazem"),
+ ["conhecia"] = ("sabia", "fazia"), ["sabia"] = ("sabia", "fazia"),
+ ["conhecias"] = ("sabias", "fazias"), ["sabias"] = ("sabias", "fazias"),
+ ["conhecíamos"] = ("sabíamos", "fazíamos"), ["sabíamos"] = ("sabíamos", "fazíamos"),
+ ["conheciam"] = ("sabiam", "faziam"), ["sabiam"] = ("sabiam", "faziam"),
+ };
+
+ public static string Apply(string text, string? english, Func blocked)
+ {
+ if (string.IsNullOrWhiteSpace(english)) return text;
+ var idioms = EnglishAuthenticity.Matches(english);
+ var candidates = PersonalLiteral.Matches(text);
+ if (idioms.Count == 1 && candidates.Count == 1)
+ {
+ var idiom = idioms[0];
+ var candidate = candidates[0];
+ var subject = candidate.Groups["subject"].Value.ToLowerInvariant();
+ var enSubject = idiom.Groups["subject"].Value.ToLowerInvariant();
+ var expected = enSubject switch { "he" => "ele", "she" => "ela", _ => subject };
+ var literal = candidate.Groups["literal"].Value;
+ var words = Regex.Matches(candidate.Value, @"[\p{L}\p{M}]+").Select(m => m.Value);
+ var before = text[..candidate.Index];
+ var omitted = subject.Length == 0;
+ var allowedOmission = enSubject != "they" &&
+ Regex.IsMatch(before, @"(?:^|[.!?;]\s*)[\s""“«'\uE000-\uF8FF-]*(?:sim\s*,\s*)?$", Ci);
+ var past = idiom.Groups["verb"].Value.ToLowerInvariant() is "was" or "were";
+ var ptPast = candidate.Groups["verb"].Value.ToLowerInvariant() is "era" or "eram";
+ var authenticity = !idiom.Groups["idiom"].Value.StartsWith("real", StringComparison.OrdinalIgnoreCase) ||
+ Regex.IsMatch(idiom.Groups["idiom"].Value, @"McCoy$", Ci);
+ // Authenticity-only contexts must not turn a genuine person into praise of their skill.
+ var authenticContext = Regex.IsMatch(english, @"\b(?:fake|impost[eo]r|counterfeit|fraud|pretend|genuine|authentic)\b", Ci);
+ if ((subject == expected || omitted && allowedOmission) && (omitted || past == ptPast) &&
+ (enSubject != "they" || subject is "eles" or "elas") &&
+ !words.Any(blocked) && !blocked(literal) && !blocked("ser") &&
+ !Regex.IsMatch(before, @"\b(?:não|nunca|nem)\s*$", Ci))
+ {
+ if (omitted) subject = expected;
+ var plural = subject is "eles" or "elas";
+ var feminine = subject is "ela" or "elas";
+ var copula = past ? plural ? "eram" : "era" : plural ? "são" : "é";
+ var adjective = authenticity || authenticContext
+ ? feminine ? plural ? "autênticas" : "autêntica" : plural ? "autênticos" : "autêntico"
+ : "mesmo " + (feminine ? plural ? "boas" : "boa" : plural ? "bons" : "bom");
+ var replacement = omitted ? subject + " " + copula + " " + adjective
+ : candidate.Groups["subject"].Value + candidate.Groups["subjectGap"].Value + copula +
+ candidate.Groups["verbGap"].Value + adjective;
+ if (CaseHelper.IsAllUpper(candidate.Value)) replacement = replacement.ToUpperInvariant();
+ else if (char.IsUpper(candidate.Value[0])) replacement = CaseHelper.Capitalize(replacement);
+ text = text[..candidate.Index] + replacement + text[(candidate.Index + candidate.Length)..];
+ }
+ }
+
+ var expertise = EnglishExpertise.Matches(english);
+ if (expertise.Count != 1 || LiteralExpertise.Matches(text).Count != 1) return text;
+ var enOwner = expertise[0].Groups["subject"].Value.ToLowerInvariant() switch
+ {
+ "i" => "my", "you" => "your", "he" => "his", "she" => "her", "we" => "our", "they" => "their",
+ _ => expertise[0].Groups["own"].Value.ToLowerInvariant(),
+ };
+ if (!expertise[0].Groups["own"].Value.Equals(enOwner, StringComparison.OrdinalIgnoreCase)) return text;
+ return LiteralExpertise.Replace(text, m =>
+ {
+ if (blocked("conhecer") || blocked("saber") || blocked("coisas") ||
+ blocked(m.Value) || Regex.Matches(m.Value, @"[\p{L}\p{M}]+").Any(w => blocked(w.Value)))
+ return m.Value;
+ var forms = ExpertiseForms[m.Groups["verb"].Value];
+ var replacement = forms.Know + m.Groups["gap"].Value + m.Groups["adv"].Value + "o que " + forms.Do;
+ return CaseHelper.IsAllUpper(m.Value) ? replacement.ToUpperInvariant()
+ : char.IsUpper(m.Value[0]) ? CaseHelper.Capitalize(replacement) : replacement;
+ });
+ }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/ExplanationConstructions.cs b/se5/BrazilianToEuropeanPortuguese/ExplanationConstructions.cs
new file mode 100644
index 00000000..fbf62202
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/ExplanationConstructions.cs
@@ -0,0 +1,77 @@
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal static class ExplanationConstructions
+{
+ private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+ private static readonly Regex Follow = new(
+ @"(?(?:^|[.!?;]\s*)[\s\uE000-\uF8FF-]*(?:(?:sim|bem|certo|está\s+bem|mas|e|então)[,]?\s+){0,3})" +
+ @"(?acompanhe|acompanha|acompanhem|acompanhai)(?=\s*[,;.!?])", Ci);
+ private static readonly Regex ReasoningLeap = new(
+ @"(?(?:este|esse|isto|isso)\s+é\s+o|(?:um|o)\s+(?:grande\s+)?)(?\s*)(?pulo)" +
+ @"(?![\p{L}\p{M}-])", Ci);
+ private static readonly Regex Continuation = new(
+ @"(?^[\s\uE000-\uF8FF]*que\s+(?:(?:não|nunca|já|também|ainda|sempre|só)\s+){0,2}" +
+ @"(?:(?me|te|se|nos|vos|lhe|lhes|o|a|os|as)\s+)?)(?[\p{L}\p{M}]+)(?![\p{L}\p{M}-])", Ci);
+ private static readonly Regex GoverningSubject = new(
+ @"(?:^|[.!?;]\s+)[\s\uE000-\uF8FF]*(?:e\s+se\s+|se\s+|mas\s+|e\s+)?" +
+ @"(?você|tu)\s+(?:(?:não|nunca|já|também|ainda|sempre|só)\s+){0,2}" +
+ @"(?[\p{L}\p{M}]+)(?(?:\s+[\p{L}\p{M}]+){0,5})\s*$", Ci);
+ private static readonly HashSet GoverningForms = new(
+ new[] { "achar", "pensar", "acreditar", "imaginar", "considerar", "decidir", "prometer" }
+ .SelectMany(Forms).Concat(new[] { "diz", "disse", "dizes", "disseste", "diria", "dirias",
+ "sabe", "sabes", "sabia", "sabias", "soube", "soubeste" }), StringComparer.OrdinalIgnoreCase);
+
+ private static IEnumerable Forms(string verb)
+ {
+ if (!VerbConjugator.TryConjugate(verb, out var forms))
+ throw new InvalidOperationException($"Cannot conjugate reviewed continuation verb: {verb}.");
+ return forms;
+ }
+
+ internal static string FollowReasoning(string text, ReferenceHint? reference, Func isBlocked,
+ bool pronouns, bool vocabulary)
+ {
+ var english = reference?.EnglishText ?? "";
+ var follows = Regex.IsMatch(english,
+ @"\b(?:stay\s+with\s+me|follow\s+(?:me|my\s+(?:reasoning|argument|logic))|bear\s+with\s+me)\b", Ci);
+ var reasoning = (Regex.IsMatch(english, @"\b(?:reasoning|argument|explanation|logic)\b", Ci) ||
+ follows && Regex.IsMatch(english, @"\bleap\b", Ci)) &&
+ !Regex.IsMatch(english, @"\b(?:jump|jumping|cliff|roof|ledge|faith|ski|jumped|physical|literal)\b", Ci);
+ if (!reasoning) return text;
+ if (pronouns && !isBlocked("acompanhar") && follows)
+ text = Follow.Replace(text, m => isBlocked(m.Groups["verb"].Value) ? m.Value :
+ m.Groups["pre"].Value + PortugueseVerbs.AttachEnclitic(m.Groups["verb"].Value,
+ CaseHelper.IsAllUpper(m.Groups["verb"].Value) ? "ME" : "me"));
+ if (vocabulary && !isBlocked("pulo") && Regex.IsMatch(english, @"\bleap\b", Ci))
+ text = ReasoningLeap.Replace(text, m => m.Groups["head"].Value + m.Groups["gap"].Value +
+ CaseHelper.MatchCase(m.Groups["noun"].Value, "salto"));
+ return text;
+ }
+
+ internal static bool HasGoverningAddressee(string previous, ReferenceHint? reference)
+ {
+ var match = GoverningSubject.Match(Regex.Replace(previous, @"<[^>]*>|\{[^}]*\}", ""));
+ if (!match.Success) return false;
+ var verb = match.Groups["verb"].Value;
+ var tail = match.Groups["tail"].Value;
+ return reference is { SecondPersonSubject: true, ThirdPerson: false } && tail.Length == 0 && GoverningForms.Contains(verb) ||
+ Regex.IsMatch(verb, @"^(?:mete|meteu|metia|meteria|metesse|meteste|metes|metias|meterias|metesses)$", Ci) &&
+ Regex.IsMatch(tail, @"^\s+na\s+(?:(?:sua|tua)\s+)?cabeça$", Ci);
+ }
+
+ internal static string ContinueAddressee(string text) => Continuation.Replace(text, m =>
+ {
+ var verb = m.Groups["verb"].Value;
+ if (m.Groups["clitic"].Value.Equals("se", StringComparison.OrdinalIgnoreCase) ||
+ PortugueseVerbs.IsSecondPersonForm(verb) || PortugueseVerbs.IsKnownInfinitive(verb) ||
+ PortugueseConverter.TuImperative.IsImpersonalConditional(verb) ||
+ Regex.IsMatch(m.Result("$'"), @"^\s+(?:(?:eu|ele|ela|eles|elas|nós|vocês|o\s+senhor|a\s+senhora)\b|" +
+ @"(?:o|a|os|as)\s+(?!seu\b|sua\b|seus\b|suas\b|teu\b|tua\b|teus\b|tuas\b)\p{L})", Ci))
+ return m.Value;
+ return PortugueseVerbs.TryThirdToSecond(verb, out var second) ||
+ PortugueseVerbs.TryGuessThirdToSecond(verb, out second)
+ ? m.Groups["pre"].Value + CaseHelper.MatchCase(verb, second) : m.Value;
+ });
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/GreetingConstructions.cs b/se5/BrazilianToEuropeanPortuguese/GreetingConstructions.cs
new file mode 100644
index 00000000..85402e38
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/GreetingConstructions.cs
@@ -0,0 +1,45 @@
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal static class GreetingConstructions
+{
+ private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+ private static readonly Regex NamedQuestion = new(
+ @"(?(?:^|[.!?]\s+|\n)[\s""“«'\uE000-\uF8FF-]*" +
+ @"(?:ei|olá|oi)\s*,\s*(?(?-i:\p{Lu}[\p{L}\p{M}'’-]+))\s*[,!?]\s*como\s+)" +
+ @"(?vai|está|anda|tem)(?=(?:\s+estado)?\s*\?)", Ci);
+ private static readonly Regex CollectiveArticle = new(
+ @"(?toda|todo)(?\s+)" +
+ @"(?equipa|equipe|família|turma|tripulação|comunidade|população|comissão|" +
+ @"grupo|povo|pelotão|batalhão|departamento)(?![\p{L}\p{M}\d_-])", Ci);
+
+ public static string CollectiveArticles(string text) => CollectiveArticle.Replace(text, m =>
+ {
+ var feminine = m.Groups["noun"].Value.ToLowerInvariant() is
+ "equipa" or "equipe" or "família" or "turma" or "tripulação" or "comunidade" or "população" or "comissão";
+ if (m.Groups["quant"].Value.EndsWith("a", StringComparison.OrdinalIgnoreCase) != feminine) return m.Value;
+ var article = feminine ? "a" : "o";
+ if (CaseHelper.IsAllUpper(m.Value)) article = article.ToUpperInvariant();
+ return m.Groups["quant"].Value + " " + article + m.Groups["gap"].Value + m.Groups["noun"].Value;
+ });
+
+ public static string InformalQuestions(string text, string? english)
+ {
+ if (string.IsNullOrWhiteSpace(english)) return text;
+ return NamedQuestion.Replace(text, m =>
+ {
+ var name = m.Groups["name"].Value;
+ if (PortugueseConverter.YouToTuRules.IsFormal(name) ||
+ !Regex.IsMatch(english, @"\b(?:hey|hi|hello)\s*,?\s+" + Regex.Escape(name) +
+ @"\s*[,!.?]?\s*how\s+(?:(?:are|have)\s+)?you\b(?!\s+(?:all|both|guys|two|and|or)\b)" +
+ @"(?:['’]re)?(?:\s+(?:doing|been|going))?", Ci))
+ return m.Value;
+ var verb = m.Groups["verb"].Value;
+ return m.Groups["pre"].Value + CaseHelper.MatchCase(verb, verb.ToLowerInvariant() switch
+ {
+ "vai" => "vais", "está" => "estás", "anda" => "andas", _ => "tens",
+ });
+ });
+ }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/HypotheticalReactions.cs b/se5/BrazilianToEuropeanPortuguese/HypotheticalReactions.cs
new file mode 100644
index 00000000..781b62d3
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/HypotheticalReactions.cs
@@ -0,0 +1,63 @@
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal static class HypotheticalReactions
+{
+ private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+ private const string Boundary = @"(?(?:(?eu|tu|ele|ela|nós|vocês|eles|elas)\s+)?" +
+ @"(?:(?:também|não|nunca|certamente|provavelmente)\s+){0,2}" +
+ @"(?ficaria|ficarias|ficaríamos|ficariam|estaria|estarias|estaríamos|estariam)" +
+ @"\s+(?:(?:também|muito|tão|mesmo|bastante|igualmente|completamente)\s+){0,3}" +
+ @"(?:zangad[oa]s?|furios[oa]s?|irritad[oa]s?|tristes?|abalad[oa]s?|contentes?|feliz|felizes|" +
+ @"assustad[oa]s?|preocupad[oa]s?|ofendid[oa]s?|indignad[oa]s?|magoad[oa]s?|chatead[oa]s?|" +
+ @"surpreendid[oa]s?|revoltad[oa]s?|brav[oa]s?))";
+ private const string Condition =
+ @"(?se\s+(?:(?eu|tu|ele|ela|nós|vocês|eles|elas)\s+)?" +
+ @"(?fosse|fosses|fôssemos|fossem)\s+(?ele|ela|eles|elas|tu|você|vocês))";
+ private static readonly Regex Trailing = new(Boundary + Reaction + @"\s+" + Condition +
+ @"(?=\s*[\uE000-\uF8FF]*(?:[.!?…;]|$))", Ci);
+ private static readonly Regex Leading = new(Boundary + Condition + @"(?,\s+)" + Reaction +
+ @"(?=\s*[\uE000-\uF8FF]*(?:[.!?…;]|$))", Ci);
+
+ internal static string Apply(string text, ReferenceHint? reference)
+ {
+ string Replace(Match m)
+ {
+ if (Regex.IsMatch(m.Result("$`"), @"\b(?:como|mesmo|ainda|só)\s+$", Ci)) return m.Value;
+ var subject = m.Groups["subject"].Value.ToLowerInvariant();
+ var inner = m.Groups["inner"].Value.ToLowerInvariant();
+ var verb = m.Groups["verb"].Value.ToLowerInvariant();
+ if (subject.Length == 0)
+ subject = inner.Length > 0 ? inner :
+ verb.EndsWith("ríamos", StringComparison.Ordinal) ? "nós" :
+ verb.EndsWith("rias", StringComparison.Ordinal) ? "tu" :
+ Regex.IsMatch(reference?.EnglishText ?? "", @"\bI(?:['’]d|\s+would)\s+(?:also\s+)?(?:be|go|get|feel)\b", Ci) ? "eu" : "";
+ var expected = subject switch
+ {
+ "eu" or "ele" or "ela" => "fosse", "tu" => "fosses", "nós" => "fôssemos",
+ "vocês" or "eles" or "elas" => "fossem", _ => "",
+ };
+ if (expected.Length == 0 || !m.Groups["copula"].Value.Equals(expected, StringComparison.OrdinalIgnoreCase) ||
+ inner.Length > 0 && inner != subject || subject == m.Groups["other"].Value.ToLowerInvariant()) return m.Value;
+ var expectedVerb = subject switch
+ {
+ "eu" or "ele" or "ela" => @"^(?:ficaria|estaria)$",
+ "tu" => @"^(?:ficarias|estarias)$", "nós" => @"^(?:ficaríamos|estaríamos)$",
+ _ => @"^(?:ficariam|estariam)$",
+ };
+ if (!Regex.IsMatch(verb, expectedVerb, Ci)) return m.Value;
+ var complement = subject switch
+ {
+ "eu" => "comigo", "tu" => "contigo", "nós" => "connosco", _ => "com " + subject,
+ };
+ var condition = CaseHelper.MatchCase(m.Groups["condition"].Value, "se fosse " + complement);
+ return m.Groups["comma"].Success
+ ? condition + m.Groups["comma"].Value + m.Groups["reaction"].Value
+ : m.Groups["reaction"].Value + " " + condition;
+ }
+ return Leading.Replace(Trailing.Replace(text, Replace), Replace);
+ }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/LocalChecks.cs b/se5/BrazilianToEuropeanPortuguese/LocalChecks.cs
index b12af0f2..d8305cc1 100644
--- a/se5/BrazilianToEuropeanPortuguese/LocalChecks.cs
+++ b/se5/BrazilianToEuropeanPortuguese/LocalChecks.cs
@@ -111,7 +111,8 @@ private static bool PluralBefore(string text, int index)
}
}
-internal sealed record CheckLine(int Index, string Original, string Converted, string? Previous, string? Reference);
+internal sealed record CheckLine(int Index, string Original, string Converted, string? Previous, string? Reference,
+ SentenceMeaning? MeaningSentence = null);
internal sealed record CheckResult(string Text, string? Note);
@@ -179,6 +180,27 @@ internal static bool MeaningReady(EmbeddingSettings settings) =>
$"to the possible reading “{reading}” - check the context; literal use is also possible.";
}
+ internal static async Task SentenceMeaningNoteAsync(SentenceMeaning sentence, string converted,
+ Func> embed)
+ {
+ var comparable = IsComparable(sentence.Original, sentence.Reference) && IsComparable(converted, sentence.Reference);
+ if (sentence.Original == converted && !comparable) return null;
+ var original = await embed(sentence.Original);
+ var final = sentence.Original == converted ? original : await embed(converted);
+ if (original == null || final == null) return null;
+ var similarity = ReferenceAligner.Dot(original, final);
+ var reference = comparable ? await embed(sentence.Reference) : null;
+ var before = reference == null ? (double?)null : ReferenceAligner.Dot(original, reference);
+ var after = reference == null ? (double?)null : ReferenceAligner.Dot(final, reference);
+ if (similarity >= MeaningThreshold && !(before < TranslationThreshold) && !(after < TranslationThreshold) &&
+ !(before - after >= (before >= FaithfulBaseline ? SuspiciousDrop : ReferenceDrop)))
+ return null;
+ return $"MiniLM (sentence context, cues {sentence.First + 1}-{sentence.Last + 1}): " +
+ $"original/converted similarity {similarity:0.00}" +
+ (reference == null ? "" : $"; English similarity {before:0.00} before the rules, {after:0.00} after") +
+ " - review the whole sentence. Meaning similarity does not validate relative pronouns or verb agreement.";
+ }
+
internal static async Task<(Dictionary Results, string Summary)> RunAsync(IReadOnlyList lines,
EmbeddingSettings settings, IProgress progress, CancellationToken token)
{
@@ -266,7 +288,9 @@ void Note(int index, string note)
// Stage 1 (before the rules): is the Brazilian line itself faithful to the English? Stage 2 (after): did the rules keep it so?
var referenced = lines.Where(l => IsComparable(l.Original, l.Reference) || IsComparable(texts[l.Index], l.Reference)).ToList();
var idiomatic = lines.Where(l => IdiomHints.MeaningReference(l.Reference) != null).ToList();
- if (changed.Count == 0 && referenced.Count == 0 && idiomatic.Count == 0) return (0, 0);
+ var sentences = lines.Where(l => l.MeaningSentence != null).GroupBy(l => l.MeaningSentence!)
+ .Where(g => g.Count() == g.Key.Last - g.Key.First + 1).ToList();
+ if (changed.Count == 0 && referenced.Count == 0 && idiomatic.Count == 0 && sentences.Count == 0) return (0, 0);
await using var runtime = await EmbeddingRuntime.StartAsync(settings, progress, token);
var cache = new Dictionary(StringComparer.Ordinal);
async Task Embed(string? text)
@@ -336,6 +360,18 @@ void Note(int index, string note)
}
if (i % 50 == 0) progress.Report($"MiniLM: idiom check {i + 1}/{idiomatic.Count}...");
}
+ foreach (var sentence in sentences)
+ {
+ token.ThrowIfCancellationRequested();
+ var converted = string.Join(" ", sentence.OrderBy(l => l.Index).Select(l => texts[l.Index]));
+ var sentenceNote = await SentenceMeaningNoteAsync(sentence.Key, converted, Embed);
+ if (sentenceNote == null) continue;
+ foreach (var line in sentence)
+ {
+ note(line.Index, sentenceNote);
+ if (flaggedLines.Add(line.Index)) translation++;
+ }
+ }
return (flagged, translation);
}
}
diff --git a/se5/BrazilianToEuropeanPortuguese/MainWindow.axaml.cs b/se5/BrazilianToEuropeanPortuguese/MainWindow.axaml.cs
index de37d250..23df3aca 100644
--- a/se5/BrazilianToEuropeanPortuguese/MainWindow.axaml.cs
+++ b/se5/BrazilianToEuropeanPortuguese/MainWindow.axaml.cs
@@ -485,7 +485,7 @@ private void BuildProposals(PortugueseLocalWordList? localWordList)
var rules = conversions[i].Rules.ToList();
var previous = i == 0 ? null : convertedLines.TryGetValue(i - 1, out var before) ? before : _blocks[i - 1].Text;
convertedLines[i] = text;
- var checkLine = new CheckLine(i, _blocks[i].Text, text, previous, reference);
+ var checkLine = new CheckLine(i, _blocks[i].Text, text, previous, reference, conversions[i].MeaningSentence);
_checkLines.Add(checkLine);
if (_checks.TryGetValue(checkLine, out var check))
{
diff --git a/se5/BrazilianToEuropeanPortuguese/MaybeSubjunctive.cs b/se5/BrazilianToEuropeanPortuguese/MaybeSubjunctive.cs
index 4719083b..03e22cdf 100644
--- a/se5/BrazilianToEuropeanPortuguese/MaybeSubjunctive.cs
+++ b/se5/BrazilianToEuropeanPortuguese/MaybeSubjunctive.cs
@@ -13,15 +13,23 @@ 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+)?" +
+ @"(?-i:\p{Lu}[\p{Ll}\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);
+ @"(?=(?\s+[\p{L}\p{M}]+)?)";
+
+ private static readonly Regex Maybe = new(
+ @"(?(?(? Irregular = new(StringComparer.OrdinalIgnoreCase)
@@ -233,6 +241,18 @@ public static string Apply(string s)
return Maybe.Replace(s, m => Replace(m, s.Substring(m.Index + m.Length)));
}
+ public static string ApplyComplements(string s) => Complements.Replace(s, m =>
+ {
+ // Negated doubt and nominal "o receio que..." do not license this inference.
+ if (Regex.IsMatch(m.Result("$`"),
+ @"(? 0 ? clitic + " " : string.Empty) + subjunctive;
+ });
+
private static string Replace(Match m, string rest)
{
var verb = m.Groups["v"].Value;
diff --git a/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs b/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs
index 21407672..1e9c39a4 100644
--- a/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs
+++ b/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs
@@ -407,6 +407,9 @@ public static (string Infinitive, string? Past, string? Present)? Find(string s)
public string Convert(string text, string? previousText = null, ReferenceHint? reference = null,
ICollection? trace = null) => ConvertCore(text, previousText, reference, trace, false);
+ internal bool HasInformalTagEvidence(string text) =>
+ DiscourseTags.HasInformalEvidence(Mask(text, new List()));
+
internal string ConvertContinuation(string text, string? previousText, ReferenceHint? reference,
ICollection trace) => ConvertCore(text, previousText, reference, trace, true);
@@ -660,6 +663,9 @@ void Track(string id, string description)
}
Track("context-phrases", "Context/relative-clause and colloquial phrases");
+ if (_options.Colloquial)
+ s = GreetingConstructions.CollectiveArticles(s);
+ Track("collective-articles", "Definite articles with reviewed whole-collective expressions");
s = MissingInfinitiveEnding.Apply(s);
Track("missing-infinitive", "Dictionary-validated infinitive after ir");
@@ -686,18 +692,44 @@ void Track(string id, string description)
if (_options.Vocabulary)
s = ConversionConstructions.Vocabulary(s, _options.Pronouns, informalConstructions);
Track("construction-vocabulary", "Complement-qualified vocabulary senses");
+ if (_options.Colloquial && _options.Vocabulary)
+ s = ConversionConstructions.AnnoyanceSlang(s, reference, _options.Pronouns, _blockedVocabularyVerbs.Contains);
+ Track("annoyance-slang", "Context-qualified Brazilian annoyance expressions");
if (_options.Vocabulary) s = ConversionConstructions.ReviewedVerbSenses(s, reference, _blockedVocabularyVerbs.Contains);
- Track("reviewed-verb-senses", "Telephone surveillance and reference-confirmed stalling/sleep senses");
+ Track("reviewed-verb-senses", "Speech content, perfect scores, surveillance and reference-confirmed verb senses");
if (_options.YouToTu) s = ConversionConstructions.PersonalInfinitives(s, informalConstructions);
Track("personal-infinitives", "Explicit-subject personal infinitives");
- if (_options.Pronouns) s = ConversionConstructions.FamilySubjects(s, reference);
- Track("family-subjects", "Reference-confirmed family subjects with definite articles");
+ if (_options.Pronouns)
+ {
+ s = ConversionConstructions.FamilySubjects(s, reference);
+ s = ConversionConstructions.FamilySurnameSubjects(s, reference, _nameArticles);
+ }
+ Track("family-subjects", "Family subjects and plural family surnames with definite articles");
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 && _options.Pronouns)
+ s = ConversionConstructions.WaitingForPeople(s, _nameArticles, _blockedVocabularyVerbs.Contains);
+ Track("waiting-for-people", "Por with reviewed personal waiting complements");
+ if (_options.Vocabulary)
+ s = ShakingVocabulary.Apply(s, reference, _blockedVocabularyVerbs.Contains);
+ Track("shaking-vocabulary", "Contextual chacoalhar: abanar, agitar, sacudir, chocalhar and reviewed figurative senses");
+ if (_options.Colloquial && _options.Pronouns)
+ s = HypotheticalReactions.Apply(s, reference);
+ Track("hypothetical-reactions", "Situational reactions: se eu fosse ele to se fosse comigo");
+ if (_options.Colloquial)
+ s = ExplanationConstructions.FollowReasoning(s, reference, _blockedVocabularyVerbs.Contains,
+ _options.Pronouns, _options.Vocabulary);
+ Track("explanation-constructions", "Reference-confirmed reasoning: acompanhe-me and salto");
+ if (_options.Vocabulary)
+ s = ReviewedDialogue.Vocabulary(s, reference, _blockedVocabularyVerbs.Contains);
+ Track("dialogue-vocabulary", "Reference-confirmed press conferences and depreciative dona");
+ if (_options.Colloquial && _options.Vocabulary)
+ s = ReviewedDialogue.Idioms(s, previousText, reference, _blockedVocabularyVerbs.Contains);
+ Track("dialogue-idioms", "Reference-confirmed hard-earned knowledge, business proposals and current price quotes");
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);
@@ -715,7 +747,10 @@ void Track(string id, string description)
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");
+ Track("police-nouns", "Existential, quantified and prepositional police personnel nouns, preserving adjectival policial");
+ if (_options.Vocabulary)
+ s = ConversionConstructions.VehiclePlates(s, previousText, reference, _blockedVocabularyVerbs.Contains);
+ Track("vehicle-plates", "Context-qualified vehicle registration plates");
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);
@@ -733,9 +768,14 @@ void Track(string id, string description)
s = YouToTuRules.AdjacentWhenClause(s);
s = YouToTuRules.OpeningInterjectionAgreement(s);
s = YouToTuRules.SecondPersonGovernedReflexive(s);
+ s = ConversionConstructions.PromiseQuestions(s, reference);
}
}
Track("address", "Address, conjugation and possessives");
+ if (_options.Pronouns)
+ s = ReviewedDialogue.Objects(s, reference, _options.YouToTu && _options.RuleEnabled("address") && (reference is { ForceInformal: true } ||
+ reference is not { FormalAddress: true } && !YouToTuRules.IsFormal(s)), _blockedVocabularyVerbs.Contains);
+ Track("dialogue-objects", "Reference-confirmed reminder recipients and owed indirect objects");
if (_options.Progressive)
{
@@ -745,6 +785,10 @@ void Track(string id, string description)
if (_options.Pronouns)
{
+ s = PronounRules.CorrectDirectLhe(s, reference,
+ _options.YouToTu && _options.RuleEnabled("address") && (reference is { ForceInformal: true } ||
+ reference is not { FormalAddress: true } && !YouToTuRules.IsFormal(s)),
+ _blockedVocabularyVerbs.Contains);
s = PronounRules.Apply(s, reference);
s = LetConstructions.Apply(s, reference);
s = PronounRules.ReplacePersonalInfinitiveObjects(s, includeYou: false);
@@ -770,10 +814,15 @@ void Track(string id, string description)
s = VocabularyRules.ReplaceBareReflexive(s);
s = DepartingBarca.Replace(s, ReplaceDepartingBarca);
s = _verbs?.Apply(s, reference) ?? s;
+ s = ReplaceRomanticPartner(s, reference);
s = YoungPerson.Replace(s, m =>
- _words.TryGetValue(m.Groups["noun"].Value, out var entry) && entry.Pt.StartsWith("miúd", StringComparison.OrdinalIgnoreCase)
- ? ReplaceYoungPerson(m, reference)
- : m.Value);
+ {
+ var noun = m.Groups["noun"].Value;
+ if (_blockedVocabularyVerbs.Contains(noun)) return m.Value;
+ var originalGaroto = noun.StartsWith("garot", StringComparison.OrdinalIgnoreCase) &&
+ _words.TryGetValue(noun, out var entry) && entry.Pt.StartsWith("miúd", StringComparison.OrdinalIgnoreCase);
+ return ReplaceYoungPerson(m, reference, originalGaroto);
+ });
}
Track("verb-vocabulary", "Contextual vocabulary and verb inflections");
@@ -837,6 +886,14 @@ void Track(string id, string description)
}
Track("progressive-command", "Progressive follow-up and imperative address");
+ if (_options.YouToTu && _options.RuleEnabled("address"))
+ s = DiscourseTags.Apply(s, reference);
+ Track("discourse-tags", "Informal agreement in detached addressee tags with local register evidence");
+
+ if (_options.Pronouns)
+ s = ConversionConstructions.GiveRequestRecipients(s, reference?.EnglishText);
+ Track("give-request-recipient", "English-confirmed missing recipients in bounded dar requests");
+
if (_options.Progressive)
{
s = ImmediateProgressive.Apply(s);
@@ -852,6 +909,17 @@ void Track(string id, string description)
s = MaybeSubjunctive.Apply(s);
Track("maybe-subjunctive", "Subjunctive and proclisis after a preceding talvez");
+ s = MaybeSubjunctive.ApplyComplements(s);
+ Track("complement-subjunctive", "Present subjunctive after expressions of fear, desire and doubt");
+
+ if (_options.Colloquial)
+ s = RelativeAndModalGrammar.TopicRelatives(s, reference is { TopicRelativeContinuation: true } && previousText != null
+ ? Mask(previousText, new List(), laterPass) : null);
+ Track("topic-relatives", "Explicit topic antecedents with the relative complement de que of falar");
+
+ s = RelativeAndModalGrammar.Conditionals(s, reference?.EnglishText);
+ Track("reference-conditional", "English-confirmed conditional tense and explicit-subject agreement");
+
s = PurposeClauses.Apply(s, reference?.EnglishText);
Track("purpose-where", "Subjunctive in onde clauses giving the purpose of a command");
@@ -864,8 +932,18 @@ void Track(string id, string description)
s = ConversionConstructions.SpokenInformation(s, previousText, reference);
Track("spoken-information", "English-confirmed figurative give me requests about information");
+ if (_options.Colloquial && _options.Vocabulary)
+ s = EnglishPraiseIdioms.Apply(s, reference?.EnglishText, _blockedVocabularyVerbs.Contains);
+ Track("english-praise-idioms", "English-confirmed personal praise, authenticity and expertise idioms");
+
+ if (_options.Colloquial && _options.Pronouns)
+ s = ConversionConstructions.PoliteInfinitiveCommands(s);
+ Track("polite-infinitive-commands", "Plural polite commands confirmed by conditional addressee agreement");
+ if (_options.Colloquial)
+ s = ConversionConstructions.SpokenIdioms(s, reference, _options.Vocabulary, _blockedVocabularyVerbs.Contains);
+ Track("spoken-idioms", "Saying nonsense, habitual time clauses and drug-confirmed intoxication slang");
if (_options.Colloquial && _options.RuleEnabled("natural-word-order"))
- s = ConversionConstructions.NaturalWordOrder(s, reference, _nameArticles);
+ s = ConversionConstructions.NaturalWordOrder(s, reference, _nameArticles, _options.Pronouns);
Track("natural-word-order", "Habitual sempre after lexical verbs and recoverable progressive subjects");
if (_options.Colloquial && _options.Pronouns) s = ConversionConstructions.NaturalDialogue(s, reference, _options.Vocabulary);
Track("natural-dialogue", "Departure commands and reference-confirmed departure slang and certainty questions");
@@ -877,6 +955,8 @@ void Track(string id, string description)
Track("personal-name-complements", "Definite articles in pensar em complements with reviewed personal names");
if (_options.Vocabulary && _options.Pronouns) s = ConversionConstructions.Complaints(s, informalConstructions);
+ if (_options.Pronouns) s = ConversionConstructions.PromiseComplements(s);
+ if (_options.Colloquial) s = ConversionConstructions.DoorwayLocation(s, reference);
if (_options.Vocabulary && !_blockedVocabularyVerbs.Contains("ferrar"))
s = ConversionConstructions.FerrarSlangVocabulary(s, reference);
Track("construction-vocabulary", "Complement-qualified vocabulary senses");
@@ -908,6 +988,9 @@ void Track(string id, string description)
if (_options.Pronouns) s = ConversionConstructions.CausativeYouPlural(s);
s = ConversionConstructions.GroupApologyPlural(s);
Track("final-grammar", "Agreement, ordinal/preposition cleanup and reference-guided finite verb");
+ if (_options.Colloquial && _options.Pronouns && reference is { GroupArrivalApology: true })
+ s = ConversionConstructions.ArrivalGroupApology(s);
+ Track("arrival-group-apology", "Plural apology by a named arrival to a waiting group");
return Unmask(s, masks);
}
@@ -1169,6 +1252,11 @@ private string ReplaceWord(Match m, string text, ReferenceHint? reference)
return m.Value;
}
+ if (entry.Br == "tolices" && _options.Colloquial && _options.RuleEnabled("spoken-idioms") &&
+ !_blockedVocabularyVerbs.Contains("tolices") &&
+ ConversionConstructions.KeepSpokenTolices(text[..m.Groups["key"].Index]))
+ 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) };
@@ -1219,20 +1307,55 @@ internal static string ReplaceWordEntry(Match m, WordEntry entry)
return detText + pt + adjText;
}
- // "garoto" is "miúdo" (a kid) by default, but "rapaz" for a young man: "o seu garoto é o detetive chefe"
- // (English "your boy") → "o seu rapaz". Without a reference, "meu/teu/seu/nosso garoto" ("my boy") is "rapaz".
+ // Explicit English boy/girl uses rapaz/rapariga across source synonyms; unconfirmed garoto keeps its existing defaults.
private static readonly Regex YoungPerson = new(
- NotWordBefore + @"(?(?:(?:meu|teu|seu|nosso|vosso|meus|teus|seus|nossos|vossos)\s+)?)(?garot(?:o|a|os|as))" + NotWordAfter, Ci);
+ NotWordBefore + @"(?(?:(?:meu|teu|seu|nosso|vosso|meus|teus|seus|nossos|vossos)\s+)?)" +
+ @"(?(?:garot|menin|miúd|moç)(?:o|a|os|as)|moleques?|molecas?|guris?|gurias?)" + NotWordAfter, Ci);
- private static string ReplaceYoungPerson(Match m, ReferenceHint? reference)
+ private static readonly Regex PartnerPerson = new(
+ NotWordBefore + @"(?ex[-\s])?(?(?:garot|menin|miúd|moç|namorad)(?:o|a|os|as)|moleques?|molecas?|guris?|gurias?|rapazes|rapaz|raparigas?)" +
+ NotWordAfter, Ci);
+
+ private string ReplaceRomanticPartner(string text, ReferenceHint? reference)
+ {
+ var english = reference?.EnglishText;
+ if (english == null) return text;
+ var people = PartnerPerson.Matches(text);
+ foreach (var feminine in new[] { false, true })
+ {
+ var partners = Regex.Matches(english, feminine ? @"\bgirlfriends?\b" : @"\bboyfriends?\b", Ci);
+ if (partners.Count != 1 ||
+ Regex.IsMatch(english, feminine
+ ? @"\b(?:girls?|lass|ladies|lady|women|woman|daughters?|sisters?|mothers?|wives|wife|friends?|kids?|children|child|bab(?:y|ies))\b"
+ : @"\b(?:boys?|guys?|lads?|men|man|sons?|brothers?|fathers?|husbands?|friends?|kids?|children|child|bab(?:y|ies))\b", Ci))
+ continue;
+ var candidates = people.Where(m => IsFemininePerson(m.Groups["noun"].Value) == feminine).ToArray();
+ if (candidates.Length != 1) continue;
+ var noun = candidates[0].Groups["noun"].Value;
+ var plural = noun.EndsWith("s", StringComparison.OrdinalIgnoreCase);
+ if (plural != partners[0].Value.EndsWith("s", StringComparison.OrdinalIgnoreCase) ||
+ noun.StartsWith("namorad", StringComparison.OrdinalIgnoreCase) || _blockedVocabularyVerbs.Contains(noun))
+ continue;
+ var target = feminine ? plural ? "namoradas" : "namorada" : plural ? "namorados" : "namorado";
+ text = PartnerPerson.Replace(text, m => m.Groups["noun"].Value.Equals(noun, StringComparison.OrdinalIgnoreCase)
+ ? m.Groups["ex"].Value + CaseHelper.MatchCase(m.Groups["noun"].Value, target) : m.Value);
+ }
+ return text;
+ }
+
+ private static bool IsFemininePerson(string noun) =>
+ noun.EndsWith("a", StringComparison.OrdinalIgnoreCase) || noun.EndsWith("as", StringComparison.OrdinalIgnoreCase);
+
+ private static string ReplaceYoungPerson(Match m, ReferenceHint? reference, bool originalGaroto)
{
var noun = m.Groups["noun"].Value;
var lower = noun.ToLowerInvariant();
var feminine = lower[^1] == 'a' || lower.EndsWith("as", StringComparison.Ordinal);
var plural = lower.EndsWith('s');
- var adult = reference is { Kid: false } && (feminine ? reference.Girl : reference.Boy) ||
- reference is not { Kid: true } && !feminine && m.Groups["poss"].Length > 0;
- if (!adult)
+ var explicitEnglish = feminine ? reference is { ExplicitGirl: true } : reference is { ExplicitBoy: true };
+ var adult = originalGaroto && (reference is { Kid: false } && (feminine ? reference.Girl : reference.Boy) ||
+ reference is not { Kid: true } && !feminine && m.Groups["poss"].Length > 0);
+ if (!explicitEnglish && !adult)
{
return m.Value;
}
@@ -2203,7 +2326,6 @@ private static readonly (Regex Regex, MatchEvaluator Evaluator)[] Rules =
(new Regex(@"(?,\s*)(?ok|okay)(?=\s*[?.!]|\s*$)", Ci), m => m.Groups["pre"].Value + "está bem"),
(new Regex(@"(?,\s*)(?hein)(?=\s*\?)", Ci), m => m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["w"].Value, "não")),
(new Regex(SentenceStart + @"(?hein)(?=\s*\?)", Ci), m => m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["w"].Value, "o quê")),
- (new Regex(SentenceStart + @"(?ei)(?=\s*,)", Ci), m => m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["w"].Value, "olha")),
(new Regex(SentenceStart + @"(?lógico|beleza)(?=\s*[.!]|\s*$)", Ci), m => m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["w"].Value,
m.Groups["w"].Value.Equals("lógico", StringComparison.OrdinalIgnoreCase) ? "claro" : "fixe")),
(new Regex(NotWordBefore + @"(?ver|vendo|vejo|vês|vê|vemos|veem|via|vias|viam|vi|viu|viste|vimos|viram|estar\s+a\s+ver|a\s+ver)(?\s+tudo)?\s+(?dobrado|dobrada|dobrados|dobradas|duplo|em\s+dobro)" + NotWordAfter, Ci),
@@ -2642,7 +2764,7 @@ private static readonly (Regex Regex, MatchEvaluator Evaluator)[] Rules =
(new Regex(NotWordBefore + @"(?dessa|nessa)(?\s+)(?vez)" + NotWordAfter, Ci),
m => CaseHelper.MatchCase(m.Groups["w"].Value, m.Groups["w"].Value.ToLowerInvariant() == "dessa" ? "desta" : "nesta") + m.Groups["sp"].Value + m.Groups["v"].Value),
// "Eu falei que..." → "Eu disse que...", "Eu falei!" → "Eu disse!".
- (new Regex(@"(?falei|falou|falaste|falaram|falamos|falámos|falar|fala|falam)(?-(?:me|te|lhe|nos|lhes))?(?=\s+que" + NotWordAfter + ")", Ci),
+ (new Regex(@"(?falei|falou|falaste|falaram|falamos|falámos|falar|fala|falam)(?-(?:me|te|lhe|nos|lhes))?(?=\s+que" + NotWordAfter + ")", Ci),
m => CaseHelper.MatchCase(m.Groups["v"].Value, m.Groups["v"].Value.ToLowerInvariant() switch
{
"falei" => "disse", "falou" => "disse", "falaste" => "disseste", "falaram" => "disseram", "falar" => "dizer",
@@ -2948,6 +3070,9 @@ public static string Apply(string s, string? previousText = null, ReferenceHint?
return ApplyFormal(s);
}
+ if (reference is { AddresseeContinuation: true })
+ s = ExplanationConstructions.ContinueAddressee(s);
+ s = GreetingConstructions.InformalQuestions(s, reference?.EnglishText);
s = ConversionConstructions.InformalPredicates(s, reference);
s = PronounRules.ReplaceCompoundObjects(s, includeYou: true);
s = PronounRules.ReplacePersonalInfinitiveObjects(s, includeYou: true);
@@ -3054,14 +3179,23 @@ public static string Apply(string s, string? previousText = null, ReferenceHint?
s = StandaloneYou.Replace(s, m => CaseHelper.MatchCase(m.Value, "tu"));
if (droppedYou)
{
+ ClauseAgreementAnalysis? analysis = null;
+ bool Allows(Match m) => (analysis ??= new ClauseAgreementAnalysis(s))
+ .Decide(m.Groups["v"].Index, reference).Allowed;
s = RememberWhen.Replace(s, ReplaceRememberWhen);
- s = QuestionVerb.Replace(s, ReplaceQuestionVerb);
- s = BareKnowHow.Replace(s, ReplaceQuestionVerb);
- s = WhatQuestion.Replace(s, m => PortugueseVerbs.TryThirdToSecond(m.Groups["v"].Value, out var second)
+ s = QuestionVerb.Replace(s, m => Allows(m) ? ReplaceQuestionVerb(m) : m.Value);
+ analysis = null;
+ s = BareKnowHow.Replace(s, m => Allows(m) ? ReplaceQuestionVerb(m) : m.Value);
+ analysis = null;
+ s = WhatQuestion.Replace(s, m => Allows(m) && PortugueseVerbs.TryThirdToSecond(m.Groups["v"].Value, out var second)
? m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["v"].Value, second)
: m.Value);
- s = NegativeImperative.Replace(s, ReplaceNegativeImperative);
- s = BareNeed.Replace(s, m => m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["v"].Value, PortugueseVerbs.ThirdToSecond[m.Groups["v"].Value.ToLowerInvariant()]));
+ analysis = null;
+ s = NegativeImperative.Replace(s, m => Allows(m) ? ReplaceNegativeImperative(m) : m.Value);
+ analysis = null;
+ s = BareNeed.Replace(s, m => Allows(m)
+ ? m.Groups["pre"].Value + CaseHelper.MatchCase(m.Groups["v"].Value, PortugueseVerbs.ThirdToSecond[m.Groups["v"].Value.ToLowerInvariant()])
+ : m.Value);
}
if (reference is { SecondPerson: true })
{
@@ -3072,13 +3206,15 @@ public static string Apply(string s, string? previousText = null, ReferenceHint?
? ReplaceReferenceInitialVerb(m, reference) : m.Value);
}
- if (reference.SecondPersonSubject && !reference.ThirdPerson)
+ if (reference.SecondPersonSubject)
{
ClauseAgreementAnalysis? analysis = null;
- s = ReferenceSubordinateVerb.Replace(s, m => (analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed
+ s = ReferenceSubordinateVerb.Replace(s, m => (!reference.ThirdPerson || ReportedAddressee(m, reference)) &&
+ (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
+ if (!reference.ThirdPerson)
+ s = ReferenceCausalVerb.Replace(s, m => (analysis ??= new ClauseAgreementAnalysis(s)).Decide(m.Groups["v"].Index, reference).Allowed
? ReplaceReferenceSecondPersonVerb(m) : m.Value);
}
if (reference.StartsWithSecondPersonSubject)
@@ -3170,7 +3306,8 @@ 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+)?" +
- @"(?:(?:" + Adverbs + @"|talvez|possivelmente|ao\s+menos|pelo\s+menos)\s+){0,3})" +
+ @"(?:(?:" + Adverbs + @"|talvez|possivelmente|ao\s+menos|pelo\s+menos)\s+){0,3}" +
+ @"(?:(?se)\s+)?)" +
@"(?\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".
@@ -3257,7 +3394,7 @@ internal static string SecondPersonGovernedReflexive(string s) => GovernedReflex
{
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)"))
+ Regex.IsMatch(v, "^(?:deix|v[êie]|viste|ouv|mand|faz|fizeste|fazias|sent|és|eras|foste)"))
return m.Value;
return m.Groups["v"].Value + m.Groups["mid"].Value + m.Groups["inf"].Value + "-" + CaseHelper.MatchCase(m.Groups["cl"].Value, "te");
});
@@ -3365,6 +3502,16 @@ private static bool CoordinatedNotSameSubject(Match m)
@"(?(?: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);
+ private static bool ReportedAddressee(Match match, ReferenceHint reference) =>
+ Regex.IsMatch(match.Result("$`"),
+ @"\b(?:diz|dizem|disse|disseram|dizia|diziam|afirma|afirmou|afirmaram|acha|achou|acharam|" +
+ @"pensa|pensou|pensaram|sabe|sabia|sabiam|garante|garantiu|garantiram|confirmou|confirmaram|" +
+ @"explicou|explicaram|avisou|avisaram)\s*$", Ci) &&
+ Regex.IsMatch(reference.EnglishText ?? "",
+ @"^[^.!?;]{1,60}\b(?:says?|said|tells?|told|thinks?|thought|knows?|knew|confirms?|confirmed|" +
+ @"explains?|explained|warns?|warned|assures?|assured)\s+(?:that\s+)?you(?:['’](?:re|ll|ve|d)\b|\s+\p{L})" +
+ @"(?:(?!\b(?:he|she|they|we|I|it)\b)[^.!?;])*[.!?]?\s*$", 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+" +
@@ -3373,7 +3520,7 @@ private static bool CoordinatedNotSameSubject(Match m)
// "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);
+ @"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|envolv|compromet|dedic|empenh|responsabiliz)", Ci);
private static readonly Regex ImpersonalWhatVerb = new(
@"^(?:acontece|aconteceu|acontecia|acontecerá|aconteceria|aconteça|acontecesse)$", Ci);
@@ -3400,6 +3547,14 @@ private static string ReplaceReferenceSubordinateVerb(Match m, ReferenceHint ref
@"^(?:vai|ia|está|estava|pode|podia|poderia|quer|queria|tem|tinha|anda|andava|costuma|costumava|vem|vinha)$", Ci)))
return m.Value;
var relativeYou = EnglishRelativeYou.IsMatch(reference.EnglishText ?? "") && SecondPersonClauseBefore(prefix);
+ if (!relativeYou && SecondPersonClauseBefore(prefix) &&
+ Regex.Match(prefix, @"(?[\p{L}\p{M}]+)\s+$", Ci) is { Success: true } antecedent &&
+ !antecedent.Groups["noun"].Value.Equals("certeza", StringComparison.OrdinalIgnoreCase) &&
+ !PortugueseVerbs.IsKnownVerbForm(antecedent.Groups["noun"].Value))
+ return m.Value;
if (m.Groups["rel"].Success && !relativeYou) return m.Value;
if (Regex.IsMatch(m.Groups["pre"].Value, @"\b(?:não|nunca|já|ainda|sempre|também|só|apenas)\b", Ci) && !relativeYou &&
!Regex.IsMatch(prefix, @"^[\s""“«'\uE000-\uF8FF-]*$|\b" +
@@ -3438,6 +3593,7 @@ private static string ReplaceReferenceSubordinateVerb(Match m, ReferenceHint ref
private static string ReplaceReferenceInitialVerb(Match m, ReferenceHint reference)
{
var verb = m.Groups["v"].Value;
+ if (m.Groups["cl"].Success && !PronominalVerb.IsMatch(verb)) return m.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))
@@ -3483,7 +3639,11 @@ private static string ReplaceReferenceInitialVerb(Match m, ReferenceHint referen
if (CopularWhatVerb.IsMatch(verb) && Regex.IsMatch(reference.EnglishText ?? "",
@"[.!?;]\s*(?:he|she|it|that|this|they|there)(?:\b|['’])", Ci))
return m.Value;
- return ReplaceReferenceSecondPersonVerb(m);
+ var result = ReplaceReferenceSecondPersonVerb(m);
+ var clitic = m.Groups["cl"];
+ return result == m.Value || !clitic.Success ? result
+ : result[..(clitic.Index - m.Index)] + CaseHelper.MatchCase(clitic.Value, "te") +
+ result[(clitic.Index - m.Index + clitic.Length)..];
}
private static string ReplaceReferenceSecondPersonVerb(Match m)
@@ -3604,6 +3764,9 @@ private static string ReplaceNegativeImperative(Match m)
private static string ReplaceQuestionVerb(Match m)
{
+ if (m.Groups["v"].Value.Equals("está", StringComparison.OrdinalIgnoreCase) &&
+ Regex.IsMatch(m.Result("$'"), @"^\s+(?:(?:o|a)\s+)?(?-i:\p{Lu}[\p{Ll}\p{M}'’-]+)\s*\?", Ci))
+ return m.Value;
// "King, acha que sim?" speaks to a friend; "Comissário, acha...?" stays formal.
if (m.Groups["voc"].Success && PronominalVerbs.Titles.Contains(m.Groups["voc"].Value.TrimEnd(',', ' ', '\t')))
{
@@ -4278,7 +4441,7 @@ public static string Apply(string s, ReferenceHint? reference)
internal static class PronounRules
{
- private const string Start = @"(?(?:^|[.!?…:;]\s+|(?(?:^|[.!?…:;]\s+|(?,\s+(?:[\uE000-\uF8FF]\s*)*)";
private static readonly HashSet NotVerbs = new(StringComparer.OrdinalIgnoreCase)
@@ -4292,7 +4455,7 @@ internal static class PronounRules
};
// "Me diga" / "Te amo" at the start of a sentence → "Diga-me" / "Amo-te"
- private static readonly Regex StartClitic = new(Start + @"(?Me|Te|Lhe|Lhes|Nos|ME|TE|LHE|LHES|NOS)\s+(?\p{L}+)" + NotWordAfter + @"(?!-)", Cs);
+ private static readonly Regex StartClitic = new(Start + @"(?me|te|lhe|lhes|nos|vos)\s+(?\p{L}+)" + NotWordAfter + @"(?!-)", Ci);
// Brazilian "Apenas me mate" (English "just kill me") → "Mate-me apenas": in Portugal the command comes first and the
// pronoun goes after it. Only imperatives move - "Apenas me deixou" is a statement and stays.
@@ -4346,14 +4509,24 @@ private static string ReplaceReflexiveCommand(Match m)
}
// "Nos vemos" → "Vemo-nos"
- private static readonly Regex NosVemos = new(Start + @"(?Nos|NOS)\s+(?\p{L}+mos)" + NotWordAfter, Cs);
+ private static readonly Regex NosVemos = new(Start + @"(?nos)\s+(?\p{L}+mos)" +
+ NotWordAfter + @"(?!-)", Ci);
+
+ private static readonly Regex ReferenceLeadingSe = new(Start + @"(?se)\s+(?\p{L}+)" +
+ NotWordAfter + @"(?!-)", Ci);
+ private static readonly Regex ExplicitGovernedReflexive = new(
+ NotWordBefore + @"(?(?eu|nós|vós)\s+(?:não\s+)?(?\p{L}+)" +
+ @"\s+(?:(?:de|a)\s+)?)(?\p{L}+(?:ar|er|ir|ôr))-(?se)" + NotWordAfter, Ci);
+ private static readonly Regex AttachedReflexive = new(
+ NotWordBefore + @"(?[\p{L}\p{M}]+)-(?me|te|se|nos|vos)" +
+ @"(?-(?:ei|ás|á|emos|eis|ão|ia|ias|íamos|íeis|iam))?(?![\p{L}\p{M}\d_-])", Ci);
// ", me liga" → ", liga-me"
private static readonly Regex CommaClitic = new(AfterComma + @"(?me|te|lhe|lhes|nos)\s+(?\p{Ll}+)" + NotWordAfter + @"(?!-)", Cs);
// "Eu te amo" / "Ele se chama" / "Ela o viu" at the start of a clause → "Eu amo-te" / "Ele chama-se" / "Ela viu-o"
private static readonly Regex SubjectClitic = new(
- @"(?(?:^|[.!?…:;,]\s+|\n)(?:(?(?:^|[.!?…:;,]\s+|\n)(?:(?(?:[Ee]u|[Ee]le|[Ee]la|[Nn]ós|[Ee]les|[Ee]las|[Tt]u|[Vv]ocê|[Vv]ocês|EU|ELE|ELA|NÓS|ELES|ELAS|TU|VOCÊ|VOCÊS))\s+" +
@"(?me|te|se|nos|vos|lhe|lhes|o|a|os|as|ME|TE|SE|NOS|VOS|LHE|LHES|O|A|OS|AS)\s+(?\p{L}+)" + NotWordAfter + @"(?!-)", Cs);
@@ -4368,6 +4541,7 @@ private static string ReplaceReflexiveCommand(Match m)
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|" +
+ @"encaix|apoi|import|resolv|certific|esforç|resign|" +
@"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);
@@ -4736,6 +4910,8 @@ private static string AddInnerArticle(Match m)
var prev = m.Groups["prev"].Value;
var poss = m.Groups["poss"].Value;
var noun = m.Groups["n"].Value;
+ if (prev.Equals("por", StringComparison.OrdinalIgnoreCase) && noun.Equals("causa", StringComparison.OrdinalIgnoreCase))
+ return m.Value;
if (ArticleContractions.Contains(prev))
{
return m.Value; // "pega nas tuas coisas" already has the article
@@ -4836,6 +5012,85 @@ private static string AddInnerArticle(Match m)
@"(? DirectLheForms = BuildDirectLheForms();
+ private static Dictionary BuildDirectLheForms()
+ {
+ var result = new Dictionary