From 80941ee5af00e82b8a0e43281e369c03037b6781 Mon Sep 17 00:00:00 2001 From: SMAP-git <333812182+SMAP-git@users.noreply.github.com> Date: Wed, 11 Nov 2026 12:33:45 +0100 Subject: [PATCH] Release v1.7.0 contextual grammar baseline before LanguageTool Preserve the tested deterministic conversion baseline and accumulated dialogue, vocabulary, agreement and reference improvements. LanguageTool integration is intentionally excluded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnnoyanceSlangTests.cs | 102 +++ .../BigCompareObjectTests.cs | 4 +- .../ClauseAgreementAnalysisTests.cs | 4 + .../CollectiveAndGreetingTests.cs | 114 +++ .../ConstructionTests.cs | 2 +- .../ConverterTests.cs | 7 +- .../DirectLheTests.cs | 75 ++ .../DiscourseTagTests.cs | 129 +++ .../DoorwayLocationTests.cs | 66 ++ .../EnglishPraiseIdiomTests.cs | 98 +++ .../ExplanationTests.cs | 149 ++++ .../FormalDativeTests.cs | 86 ++ .../GiveRequestRecipientTests.cs | 104 +++ .../HabitualFamilyTests.cs | 146 ++++ .../HypotheticalReactionTests.cs | 89 ++ .../InterrogationAgreementTests.cs | 146 ++++ .../LifePageantTests.cs | 53 ++ .../NaturalDialogueTests.cs | 2 +- .../PeopleVocabularyTests.cs | 154 ++++ .../PerfectScoreTests.cs | 95 +++ .../PoliceNounTests.cs | 51 ++ .../PoliteInfinitiveCommandTests.cs | 90 ++ .../PromiseAndBreakInTests.cs | 117 +++ .../ReferenceInitialAdverbTests.cs | 2 +- .../ReferenceTests.cs | 2 +- .../ReflexiveCoverageTests.cs | 166 ++++ .../RelativeAndModalGrammarTests.cs | 267 ++++++ .../ReviewedDialogueTests.cs | 270 ++++++ .../ShakingVocabularyTests.cs | 191 +++++ .../SpeechContentTests.cs | 139 ++++ .../SpokenIdiomTests.cs | 138 ++++ .../VehiclePlateTests.cs | 113 +++ .../WaitingArrivalTests.cs | 138 ++++ .../BatchMode.cs | 3 +- .../ClauseAgreementAnalysis.cs | 18 +- .../ConversionConstructions.cs | 770 +++++++++++++++++- .../ConversionTrace.cs | 7 +- .../DiscourseTags.cs | 55 ++ .../EnglishPraiseIdioms.cs | 104 +++ .../ExplanationConstructions.cs | 77 ++ .../GreetingConstructions.cs | 45 + .../HypotheticalReactions.cs | 63 ++ .../LocalChecks.cs | 40 +- .../MainWindow.axaml.cs | 2 +- .../MaybeSubjunctive.cs | 28 +- .../PortugueseConverter.cs | 427 +++++++++- .../PortugueseOrthography.cs | 2 + .../PortugueseVerbs.cs | 44 +- .../PortugueseWordList.xml | 12 +- se5/BrazilianToEuropeanPortuguese/README.md | 517 +++++++++++- .../README.pt-PT.md | 549 ++++++++++++- .../ReferenceSubtitle.cs | 50 +- .../RelativeAndModalGrammar.cs | 126 +++ .../ReviewedDialogue.cs | 143 ++++ .../ShakingVocabulary.cs | 117 +++ .../SubtitleConversion.cs | 65 +- se5/BrazilianToEuropeanPortuguese/plugin.json | 2 +- 57 files changed, 6480 insertions(+), 95 deletions(-) create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/AnnoyanceSlangTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/CollectiveAndGreetingTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/DirectLheTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/DiscourseTagTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/DoorwayLocationTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/EnglishPraiseIdiomTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/ExplanationTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/FormalDativeTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/GiveRequestRecipientTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/HabitualFamilyTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/HypotheticalReactionTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/InterrogationAgreementTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/LifePageantTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/PeopleVocabularyTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/PerfectScoreTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/PoliteInfinitiveCommandTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/PromiseAndBreakInTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/ReflexiveCoverageTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/RelativeAndModalGrammarTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/ReviewedDialogueTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/ShakingVocabularyTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/SpeechContentTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/SpokenIdiomTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/VehiclePlateTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese.Tests/WaitingArrivalTests.cs create mode 100644 se5/BrazilianToEuropeanPortuguese/DiscourseTags.cs create mode 100644 se5/BrazilianToEuropeanPortuguese/EnglishPraiseIdioms.cs create mode 100644 se5/BrazilianToEuropeanPortuguese/ExplanationConstructions.cs create mode 100644 se5/BrazilianToEuropeanPortuguese/GreetingConstructions.cs create mode 100644 se5/BrazilianToEuropeanPortuguese/HypotheticalReactions.cs create mode 100644 se5/BrazilianToEuropeanPortuguese/RelativeAndModalGrammar.cs create mode 100644 se5/BrazilianToEuropeanPortuguese/ReviewedDialogue.cs create mode 100644 se5/BrazilianToEuropeanPortuguese/ShakingVocabulary.cs diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/AnnoyanceSlangTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/AnnoyanceSlangTests.cs new file mode 100644 index 00000000..32572ef1 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/AnnoyanceSlangTests.cs @@ -0,0 +1,102 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class AnnoyanceSlangTests +{ + [Theory] + [InlineData("quando um pé no saco da polícia\ncomeçou a fazer perguntas.", + "when some NYPD dick started asking questions.", + "quando um chato da polícia\ncomeçou a fazer perguntas.")] + [InlineData("Ele é um pé no saco.", null, "Ele é um chato.")] + [InlineData("Ela é uma pé no saco.", null, "Ela é uma chata.")] + [InlineData("Eles são uns pés no saco.", null, "Eles são uns chatos.")] + [InlineData("Elas são umas pés no saco.", null, "Elas são umas chatas.")] + [InlineData("Ele era um pé-no-saco.", null, "Ele era um chato.")] + [InlineData("Isso é um pé no saco.", null, "Isso é uma chatice.")] + [InlineData("O trabalho era um pé no saco.", null, "O trabalho era uma chatice.")] + [InlineData("É um pé no saco.", null, "É um incómodo.")] + [InlineData("ELE É UM PÉ NO SACO.", null, "ELE É UM CHATO.")] + [InlineData("Ele é um pé no saco.", null, "Ele é um chato.")] + [InlineData("Ele me enche o saco.", "He annoys me.", "Ele chateia-me.")] + [InlineData("Ele me encheu o saco.", null, "Ele chateou-me.")] + [InlineData("Não me enche o saco.", "He doesn't annoy me.", "Não me chateia.")] + [InlineData("Ele enche o meu saco.", null, "Ele chateia-me.")] + [InlineData("Ele enche meu saco.", null, "Ele chateia-me.")] + [InlineData("Ela torra o nosso saco.", null, "Ela chateia-nos.")] + [InlineData("Eles me torravam o saco.", null, "Eles chateavam-me.")] + [InlineData("Para de me encher o saco.", "Stop bothering me.", "Para de me chatear.")] + [InlineData("Para de encher o saco.", null, "Para de chatear.")] + [InlineData("Para de torrar meu saco.", null, "Para de me chatear.")] + [InlineData("Ele te torrou o saco.", null, "Ele chateou-te.")] + [InlineData("Ele nos enchia o saco.", null, "Ele chateava-nos.")] + [InlineData("Ele enche-me o saco.", null, "Ele chateia-me.")] + [InlineData("Ele torrou-me o saco.", null, "Ele chateou-me.")] + [InlineData("Encher o saco.", "To annoy.", "Chatear.")] + [InlineData("Torrar o saco.", "To pester.", "Chatear.")] + [InlineData("ELE ME ENCHE O SACO.", null, "ELE CHATEIA-ME.")] + [InlineData("Ele me torrou o saco.", null, "Ele chateou-me.")] + public void ReviewedAnnoyanceExpressionsUseEuropeanEquivalents(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Pôs um pé no saco.", null)] + [InlineData("Ele pôs um pé no saco de dormir.", null)] + [InlineData("Ele está com um pé no saco.", null)] + [InlineData("Encheu o saco de batatas.", null)] + [InlineData("Encheu o saco com dinheiro.", null)] + [InlineData("Ele encheu o saco.", "He made a lot of money.")] + [InlineData("Ele torrou o saco.", null)] + [InlineData("Ele torrou o saco de café.", null)] + [InlineData("Ele encheu o meu saco com arroz.", null)] + [InlineData("Ele encheu-me o saco de moedas.", null)] + [InlineData("Ele encheu o meu saco até cima.", null)] + [InlineData("Ele encheu o meu saco.", "He filled my bag.")] + [InlineData("Ele encherá o meu saco.", null)] + [InlineData("O saco está cheio.", null)] + public void LiteralAndFinancialSensesArePreserved(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void OptionsIgnoredPhrasesAndCustomWordsWin() + { + const string source = "Ele é um pé no saco."; + foreach (var options in new[] + { + new ConversionOptions(Vocabulary: false), + new ConversionOptions(Colloquial: false), + new ConversionOptions(DisabledRules: "annoyance-slang"), + }) + Assert.Equal(source, new PortugueseConverter(options).Convert(source)); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("pé no saco"); + words.Ignored.Add("encher o saco"); + var converter = new PortugueseConverter(localWordList: words); + Assert.Equal(source, converter.Convert(source)); + Assert.Equal("Ele enche-me o saco.", converter.Convert("Ele me enche o saco.")); + Assert.Equal("Ele me enche o saco.", new PortugueseConverter(new ConversionOptions(Pronouns: false)) + .Convert("Ele me enche o saco.")); + Assert.Equal("Ele é um chato.", new PortugueseConverter(new ConversionOptions(Pronouns: false)).Convert(source)); + var custom = new PortugueseLocalWordList(); + custom.Words.Add(("pé no saco", "melga")); + Assert.Equal("Ele é um melga.", new PortugueseConverter(localWordList: custom).Convert(source)); + } + + [Fact] + public void ExactCuePreservesBothLinesAndTiming() + { + var cue = new SrtBlock { StartMs = 807371, EndMs = 810759, + Text = "quando um pé no saco da polícia\ncomeçou a fazer perguntas." }; + var result = Assert.Single(SubtitleConversion.Convert(new[] { cue }, ConversionOptions.Default, + english: new string?[] { "when some NYPD dick started asking questions." })); + Assert.Equal("quando um chato da polícia\ncomeçou a fazer perguntas.", result.Text); + Assert.Equal(807371, cue.StartMs); + Assert.Equal(810759, cue.EndMs); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/BigCompareObjectTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/BigCompareObjectTests.cs index ab6256c9..62743f36 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/BigCompareObjectTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/BigCompareObjectTests.cs @@ -172,7 +172,7 @@ public void ScoringNeedsDrugOrEnglishEvidence() [InlineData("Ele foi faturar no bairro.", "He went to score in the neighborhood.", "Ele foi comprar no bairro.")] [InlineData("É onde eu costumava faturar.", "That's where I used to score.", "É onde eu costumava comprar.")] [InlineData("Ele tentou faturar dele.", "He tried to score from him.", "Ele tentou comprar dele.")] - [InlineData("Faturou ontem à noite?", "Did you score last night?", "Comprou ontem à noite?")] + [InlineData("Faturou ontem à noite?", "Did you score last night?", "Compraste ontem à noite?")] [InlineData("Ele faturou!", "He scored a goal!", "Ele marcou!")] [InlineData("O time precisa faturar.", "The team needs to score.", "A equipa precisa de marcar.")] [InlineData("Ele faturou no segundo tempo.", "He scored in the second half.", "Ele marcou na segunda parte.")] @@ -572,7 +572,7 @@ public void RemainingAddressAndReflexiveGapsUseEuropeanForms(string input, strin [InlineData("Pode ir aproveitar seu fim de semana.", "You go have your little weekend.", "Podes ir aproveitar o teu fim de semana.")] [InlineData("Lembra do meu noivo, David.", "You remember my fiancé, David.", "Lembras-te do meu noivo, David.")] [InlineData("Que papo é esse? Como pode ter certeza?", "What the hell? Um, how... How are you sure though?", "Que conversa é essa? Como podes ter a certeza?")] - [InlineData("Pode pegar um caderninho\nque está na minha bolsa?", "Can you reach into my purse and hand me my tiny notebook?", "Pode pegar num caderninho\nque está na minha bolsa?")] + [InlineData("Pode pegar um caderninho\nque está na minha bolsa?", "Can you reach into my purse and hand me my tiny notebook?", "Podes pegar num caderninho\nque está na minha bolsa?")] [InlineData("De que merda está falando?", "What the fuck are you talkin' about?", "De que merda estás a falar?")] [InlineData("Para que arma se só vai bater na porta?", "Why are you takin' a gun if you just have to knock on the door?", "Para que arma se só vais bater à porta?")] [InlineData("Isso vai a julgamento mês que vem.", "Would you mind if I take a look? This is going to trial next month.", "Isto vai a julgamento mês que vem.")] diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ClauseAgreementAnalysisTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ClauseAgreementAnalysisTests.cs index ebc37018..3384a811 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/ClauseAgreementAnalysisTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ClauseAgreementAnalysisTests.cs @@ -9,6 +9,9 @@ public sealed class ClauseAgreementAnalysisTests [InlineData("Certeza?", "Certeza", "NominalUse")] [InlineData("Ajuda financeira.", "Ajuda", "NominalUse")] [InlineData("Ela precisa de ajuda.", "precisa", "OtherSubject")] + [InlineData("Toda a equipa\nprecisa de uma mascote.", "precisa", "OtherSubject")] + [InlineData("Toda equipa\nprecisa de mascotes.", "precisa", "OtherSubject")] + [InlineData("Cada equipa\nconsegue sair.", "consegue", "OtherSubject")] [InlineData("Maria trabalha aqui.", "trabalha", "OtherSubject")] [InlineData("É necessário sair.", "É", "Impersonal")] [InlineData("A pessoa que tu achas que precisa de ajuda.", "precisa", "RelativeSubject")] @@ -25,6 +28,7 @@ public void StructuralReasonsBlockUnsafeAgreement(string text, string verb, stri [Theory] [InlineData("Precisa de ajuda.", "Precisa")] + [InlineData("O quê, está a controlar-me?", "está")] [InlineData("Se não precisar de ajuda.", "precisar")] [InlineData("Tu sabes que precisa de ajuda.", "precisa")] [InlineData("Ela saiu. Precisa de ajuda.", "Precisa")] diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/CollectiveAndGreetingTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/CollectiveAndGreetingTests.cs new file mode 100644 index 00000000..57f8f080 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/CollectiveAndGreetingTests.cs @@ -0,0 +1,114 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class CollectiveAndGreetingTests +{ + [Theory] + [InlineData("Toda equipe\nprecisa de uma mascote.", "Every team needs a mascot.", "Toda a equipa\nprecisa de uma mascote.")] + [InlineData("Toda a equipa\nprecisa de várias mascotes.", null, "Toda a equipa\nprecisa de várias mascotes.")] + [InlineData("A equipa\nprecisa de ajuda.", "You need help.", "A equipa\nprecisa de ajuda.")] + [InlineData("Cada equipa\nconsegue fazer isso.", null, "Cada equipa\nconsegue fazer isso.")] + [InlineData("Todo grupo\nmerece ter ajuda.", null, "Todo o grupo\nmerece ter ajuda.")] + [InlineData("A família\nsabe como ajudar.", "You know how to help.", "A família\nsabe como ajudar.")] + [InlineData("A equipa\nquer sair?", "Do you want to leave?", "A equipa\nquer sair?")] + [InlineData("A equipa\nnão fala.", null, "A equipa\nnão fala.")] + [InlineData("As equipas\nprecisam de ajuda.", null, "As equipas\nprecisam de ajuda.")] + [InlineData("Toda equipe\nprecisa de uma mascote.", "Every team needs a mascot.", + "Toda a equipa\nprecisa de uma mascote.")] + [InlineData("TODA EQUIPE\nPRECISA DE UMA MASCOTE.", "Every team needs a mascot.", + "TODA A EQUIPA\nPRECISA DE UMA MASCOTE.")] + public void SubjectSurvivesSubtitleLineBreak(string source, string? english, string expected) => + Check(source, english, expected); + + [Theory] + [InlineData("Precisa de ajuda.", "You need help.", "Precisas de ajuda.")] + [InlineData("A equipa saiu.\nPrecisa de ajuda.", "You need help.", "A equipa saiu.\nPrecisas de ajuda.")] + [InlineData("-A equipa chegou.\n-Precisa de ajuda?", "Do you need help?", + "-A equipa chegou.\n-Precisas de ajuda?")] + [InlineData("Tu\nprecisa de ajuda.", "You need help.", "Tu\nprecisas de ajuda.")] + public void RealClauseAndDialogueBoundariesStillPermitTu(string source, string english, string expected) => + Check(source, english, expected); + + [Theory] + [InlineData("-Danny, ei.\n-Ei, Shelly, como vai?", "Danny, hey. Hey, Shelly, how you doing?", + "-Danny, ei.\n-Ei, Shelly, como vais?")] + [InlineData("Ei, Ana, como está?", "Hey, Ana, how are you?", "Ei, Ana, como estás?")] + [InlineData("Olá, João, como vai?", "Hello, João, how are you?", "Olá, João, como vais?")] + [InlineData("Oi, Ana, como tem estado?", "Hi, Ana, how have you been?", "Olá, Ana, como tens estado?")] + [InlineData("Ei, Ana, como anda?", "Hey, Ana, how are you doing?", "Ei, Ana, como andas?")] + [InlineData("Ei, pessoal.", "Hey, guys. Hello. Hi.", "Ei, pessoal.")] + [InlineData("Ei, rapazes!", "Hey, boys!", "Ei, rapazes!")] + [InlineData("Ei, pare!", "Hey, stop!", "Ei, para!")] + [InlineData("Ei, pessoal.", null, "Ei, pessoal.")] + [InlineData("Olha, pessoal.", "Look, guys.", "Olha, pessoal.")] + [InlineData("Ei, Ana, como vai?", "Hey, Ana, how are you?", "Ei, Ana, como vais?")] + [InlineData("EI, ANA, COMO VAI?", "Hey, Ana, how are you?", "EI, ANA, COMO VAIS?")] + public void GreetingsAreNotVisualCommands(string source, string? english, string expected) => + Check(source, english, expected); + + [Theory] + [InlineData("Ei, Shelly, como vai?", "Hey, Shelly, how is he doing?")] + [InlineData("Ei, Shelly, como vai?", null)] + [InlineData("Ei, Shelly, como vai?", "Hey, Maria, how are you?")] + [InlineData("Ei, Shelly, como vai o trabalho?", "Hey, Shelly, how is work?")] + [InlineData("Ei, Shelly, como vão?", "Hey, Shelly, how are you all?")] + [InlineData("Ei, Shelly, como vai?", "Hey, Shelly, how are you all?")] + [InlineData("Olá, doutor, como vai?", "Hello, Doctor, how are you?")] + public void UncertainAndFormalGreetingsRemainProtected(string source, string? english) => + Check(source, english, source); + + [Fact] + public void GreetingDoesNotOverrideExplicitFormalAddress() + { + const string source = "Ei, Shelly, como vai?"; + var hint = ReferenceHint.FromText("Hey, Shelly, how are you?")!; + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: hint with { FormalAddress = true })); + foreach (var options in new[] { new ConversionOptions(YouToTu: false), + new ConversionOptions(DisabledRules: "address") }) + Assert.Equal(source, new PortugueseConverter(options).Convert(source, reference: hint)); + } + + [Fact] + public void ArticleOptionAndIgnoredPhrasesRemainAuthoritative() + { + const string source = "Toda equipa precisa de ajuda."; + Assert.Equal(source, new PortugueseConverter(new ConversionOptions(DisabledRules: "collective-articles")) + .Convert(source)); + Assert.Equal(source, new PortugueseConverter(new ConversionOptions(Colloquial: false)).Convert(source)); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("Ei, Shelly, como vai"); + Assert.Equal("Ei, Shelly, como vai?", new PortugueseConverter(localWordList: words) + .Convert("Ei, Shelly, como vai?", reference: ReferenceHint.FromText("Hey, Shelly, how are you?"))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ActualEpisodeCuesPreserveOriginalsAndTimes(bool sentenceContext) + { + var blocks = new[] + { + new SrtBlock { StartMs = 52886, EndMs = 54321, Text = "Toda equipe\nprecisa de uma mascote." }, + new SrtBlock { StartMs = 102570, EndMs = 105740, Text = "-Danny, ei.\n-Ei, Shelly, como vai?" }, + new SrtBlock { StartMs = 106674, EndMs = 107808, Text = "Ei, pessoal." }, + }; + var originals = blocks.Select(b => (b.Text, b.StartMs, b.EndMs)).ToArray(); + var results = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: sentenceContext), + english: new string?[] { "Every team needs a mascot.", + "WOMAN: Danny, hey. Hey, Shelly, how you doing?", "Hey, guys. Hello. Hi." }); + Assert.Equal("Toda a equipa\nprecisa de uma mascote.", results[0].Text); + Assert.Equal("-Danny, ei.\n-Ei, Shelly, como vais?", results[1].Text); + Assert.Equal("Ei, pessoal.", results[2].Text); + Assert.Equal(originals, blocks.Select(b => (b.Text, b.StartMs, b.EndMs)).ToArray()); + } + + private static void Check(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var reference = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: reference)); + Assert.Equal(expected, converter.Convert(expected, reference: reference)); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs index 7334729f..947ea05d 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs @@ -83,7 +83,7 @@ public void SpokenInformationPreservesLiteralCompleteAndUnconfirmedRequests(stri [InlineData("Eras meu vizinho de andar.", "We lived on the same floor.", "Vivíamos no mesmo andar.")] [InlineData("VOCÊ É MEU VIZINHO DE ANDAR.", "We live on the same floor.", "VIVEMOS NO MESMO ANDAR.")] [InlineData("Você é meu vizinho de andar.", "Hey, we live on the same floor.", "Vivemos no mesmo andar.")] - [InlineData("Ei, somos vizinhos da mesma rua!", "Hey, we live on the same street!", "Olha, vivemos na mesma rua!")] + [InlineData("Ei, somos vizinhos da mesma rua!", "Hey, we live on the same street!", "Ei, vivemos na mesma rua!")] public void SharedResidenceRequiresMatchingEnglishMeaning(string source, string english, string expected) { var reference = ReferenceHint.FromText(english); diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ConverterTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ConverterTests.cs index ca782f0b..63983835 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/ConverterTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ConverterTests.cs @@ -136,7 +136,6 @@ public void NaturalHabitualWordOrderAndRecoverableProgressiveSubjects(string sou [InlineData("Sempre que chego.")] [InlineData("Sempre posso ajudar.")] [InlineData("Sempre estou a trabalhar.")] - [InlineData("Sempre me dizem isso.")] [InlineData("Sempre fazer o melhor.")] [InlineData("Quando eu estava a trabalhar.")] [InlineData("Quando eu também estou a trabalhar.")] @@ -377,8 +376,8 @@ public void UniversityFeesAreNotConvertedToBribesInTheSameRun(string source, str [InlineData("Ela estava pagando mico.", "Ela estava a passar vergonha.")] [InlineData("Ela ficou de bobeira.", "Ela ficou sem fazer nada.")] [InlineData("Elas ficaram de bobeira.", "Elas ficaram sem fazer nada.")] - [InlineData("Ele fala abobrinha.", "Ele fala disparates.")] - [InlineData("Eles falavam abobrinhas.", "Eles falavam disparates.")] + [InlineData("Ele fala abobrinha.", "Ele diz disparates.")] + [InlineData("Eles falavam abobrinhas.", "Eles diziam disparates.")] [InlineData("Tomei uma breja.", "Bebi uma cerveja.")] [InlineData("Tomamos brejas.", "Bebemos cervejas.")] [InlineData("Ele estava tomando uma breja.", "Ele estava a beber uma cerveja.")] @@ -1099,7 +1098,7 @@ public void UnknownInflectionsAreNotInvented() [InlineData("Os caquis e as berinjelas.", "Os dióspiros e as beringelas.")] [InlineData("Calças caqui.", "Calças caqui.")] [InlineData("Abobrinha grelhada.", "Curgete grelhada.")] - [InlineData("Falar abobrinha.", "Falar disparates.")] + [InlineData("Falar abobrinha.", "Dizer disparates.")] [InlineData("A farinha de rosca.", "O pão ralado.")] [InlineData("Leite integral.", "Leite gordo.")] [InlineData("As balas de menta.", "Os rebuçados de menta.")] diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/DirectLheTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/DirectLheTests.cs new file mode 100644 index 00000000..60eb2277 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/DirectLheTests.cs @@ -0,0 +1,75 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class DirectLheTests +{ + [Theory] + [InlineData("Eu lhe respeito imensamente.", "I have so much respect for you.", "Eu respeito-o imensamente.")] + [InlineData("Eu respeito-lhe imensamente.", "I respect you immensely.", "Eu respeito-o imensamente.")] + [InlineData("Eu não lhe respeito.", "I don't respect you.", "Eu não o respeito.")] + [InlineData("Sei que ele lhe respeita.", "I know he respects you.", "Sei que ele o respeita.")] + [InlineData("Eu lhe admirava.", "I admired you.", "Eu admirava-o.")] + [InlineData("Eu lhe admiro.", "I admire her.", "Eu admiro-a.")] + [InlineData("Eu lhes admiro.", "I admire them.", "Eu admiro-os.")] + [InlineData("Eu lhe amo.", "I love you.", "Eu amo-o.")] + [InlineData("Ela lhe conhece.", "She knows him.", "Ela conhece-o.")] + [InlineData("Ele lhe abraçou.", "He hugged her.", "Ele abraçou-a.")] + [InlineData("Eu lhe protegerei.", "I will protect you.", "Eu protegê-lo-ei.")] + [InlineData("Eu respeitar-lhe-ei.", "I will respect you.", "Eu respeitá-lo-ei.")] + [InlineData("Eu não lhe respeitaria.", "I would not respect you.", "Eu não o respeitaria.")] + [InlineData("Eu lhe respeito.", "I respect you.", "Eu respeito-o.")] + [InlineData("EU LHE RESPEITO.", "I respect you.", "EU RESPEITO-O.")] + [InlineData("Lhe respeito.", "I respect you.", "Respeito-o.")] + [InlineData("Senhora, eu lhe respeito.", "Ma'am, I respect you.", "Senhora, eu respeito-a.")] + public void DirectObjectsAreNotDatives(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var reference = ReferenceHint.FromText(english)! with { FormalAddress = true }; + Assert.Equal(expected, converter.Convert(source, reference: reference)); + Assert.Equal(expected, converter.Convert(expected, reference: reference)); + } + + [Theory] + [InlineData("Eu lhe respeito imensamente.", "I have so much respect for you.", "Eu respeito-te imensamente.")] + [InlineData("Eu respeito-lhe.", "I respect you.", "Eu respeito-te.")] + [InlineData("Eu não lhe respeito.", "I don't respect you.", "Eu não te respeito.")] + [InlineData("Eu lhe admirava.", "I admired you.", "Eu admirava-te.")] + [InlineData("Eu lhe protegerei.", "I will protect you.", "Eu proteger-te-ei.")] + [InlineData("Eu lhe respeito.", "I respect him.", "Eu respeito-o.")] + public void InformalAddressChangesOnlyTheConfirmedAddressee(string source, string english, string expected) => + Assert.Equal(expected, new PortugueseConverter().Convert(source, + reference: ReferenceHint.FromText(english)! with { ForceInformal = true })); + + [Theory] + [InlineData("Eu lhe respeito.", null, "Eu respeito-lhe.")] + [InlineData("Eu lhe admiro a coragem.", "I admire your courage.", "Eu admiro-lhe a coragem.")] + [InlineData("Eu admiro-lhe muito a coragem.", "I admire you for your courage.", "Eu admiro-lhe muito a coragem.")] + [InlineData("Eu lhe conheço os defeitos.", "I know his flaws.", "Eu conheço-lhe os defeitos.")] + [InlineData("Eu lhe telefono.", "I phone you.", "Eu telefono-lhe.")] + [InlineData("Ele lhe mentiu.", "He lied to you.", "Ele mentiu-lhe.")] + [InlineData("Eu lhe respeito e lhe admiro.", "I respect and admire you.", "Eu respeito-lhe e admiro-lhe.")] + [InlineData("Eu lhe respeito.", "I respect you and her.", "Eu respeito-lhe.")] + [InlineData("Eu lhe respeito.", "I respect them.", "Eu respeito-lhe.")] + [InlineData("Eu lhes respeito.", "I respect him.", "Eu respeito-lhes.")] + public void PossessiveDativesAndAmbiguousRecipientsAreProtected(string source, string? english, string expected) + { + var hint = ReferenceHint.FromText(english); + var converter = new PortugueseConverter(); + var actual = converter.Convert(source, reference: hint is null ? null : hint with { FormalAddress = true }); + Assert.Equal(expected, actual); + } + + [Fact] + public void DisabledSettingsAndLocalOverridesWin() + { + foreach (var options in new[] { new ConversionOptions(Pronouns: false), + new ConversionOptions(DisabledRules: "pronouns") }) + Assert.Equal("Eu lhe respeito.", new PortugueseConverter(options) + .Convert("Eu lhe respeito.", reference: ReferenceHint.FromText("I respect you."))); + var words = new PortugueseLocalWordList(); + words.Words.Add(("respeitar", "respeitar")); + Assert.Equal("Eu respeito-lhe.", new PortugueseConverter(localWordList: words) + .Convert("Eu lhe respeito.", reference: ReferenceHint.FromText("I respect you."))); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/DiscourseTagTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/DiscourseTagTests.cs new file mode 100644 index 00000000..6d4b021d --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/DiscourseTagTests.cs @@ -0,0 +1,129 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class DiscourseTagTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ActualEpisodeContinuationUsesConvertedInformalCommand(bool context) + { + var blocks = new[] + { + new SrtBlock { StartMs = 85686, EndMs = 88720, Text = "Ouça, isto nunca vai ficar\nmais fácil," }, + new SrtBlock { StartMs = 88922, EndMs = 92707, Text = "mas temos que começar\na caminhar, sabe?" }, + }; + var original = blocks.Select(b => (b.Text, b.StartMs, b.EndMs)).ToArray(); + var converted = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: context), + english: new string?[] { "Look, this is... never gonna get easier,", + "but we do kind of have to start putting one foot in front of the other, you know?" }); + Assert.Equal("Ouve, isto nunca vai ficar\nmais fácil,", converted[0].Text); + Assert.Equal("mas temos de começar\na caminhar, sabes?", converted[1].Text); + Assert.Contains(converted[1].Rules, r => r.Id == "discourse-tags"); + Assert.Equal(original, blocks.Select(b => (b.Text, b.StartMs, b.EndMs)).ToArray()); + } + + [Theory] + [InlineData("Tu podes ajudar, sabe?", "You can help, you know?", "Tu podes ajudar, sabes?")] + [InlineData("Ouve, temos de ir, sabe?", "Listen, we have to go, you know?", "Ouve, temos de ir, sabes?")] + [InlineData("É a tua vez, entende?", "It's your turn, you understand?", "É a tua vez, percebes?")] + [InlineData("É a tua vez, percebe?", "It's your turn, you get it?", "É a tua vez, percebes?")] + [InlineData("É a tua vez, compreende?", null, "É a tua vez, compreendes?")] + [InlineData("Tu podes ir, viu?", "You can go, you see?", "Tu podes ir, viste?")] + [InlineData("Tu podes ir, ouviu?", "You can go, you hear me?", "Tu podes ir, ouviste?")] + [InlineData("Tu podes ir, entendeu?", "You can go, did you understand?", "Tu podes ir, percebeste?")] + [InlineData("Tu podes ir, sabe?", "You can go, you know?", "Tu podes ir, sabes?")] + [InlineData("TU PODES IR, SABE?", "You can go, you know?", "TU PODES IR, SABES?")] + public void DetachedTagsAgreeWithLocalInformalEvidence(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Temos de ir, sabe?", "We have to go, you know?")] + [InlineData("Ele sabe?", "Does he know?")] + [InlineData("É a tua vez, ele sabe?", "It's your turn, does he know?")] + [InlineData("É a tua vez, sabe como fazer?", "It's your turn, do you know how?")] + [InlineData("É a tua vez, sabe?", "It's your turn, he knows?")] + [InlineData("É a tua vez, sabe?", "It's your turn, you all know?")] + [InlineData("É a tua vez, sabe? Outra coisa.", "It's your turn, you know? Another thing.")] + [InlineData("-É a tua vez.\n-Sabe?", "It's your turn. You know?")] + [InlineData("Ela tem uma irmã, sabe?", "She has a sister, you know?")] + public void AmbiguousLiteralPluralAndDialogueCasesRemainUnchanged(string text, string english) => + Assert.Equal(text, DiscourseTags.Apply(text, ReferenceHint.FromText(english))); + + [Theory] + [InlineData("Ouve, isto é difícil,", "mas temos de ir, sabe?")] + public void FormalAndDisabledSettingsWin(string previous, string text) + { + foreach (var options in new[] { new ConversionOptions(YouToTu: false), + new ConversionOptions(DisabledRules: "address"), new ConversionOptions(DisabledRules: "discourse-tags") }) + Assert.DoesNotContain("sabes?", new PortugueseConverter(options).Convert(text, previous, + ReferenceHint.FromText("We have to go, you know?"))); + var hint = ReferenceHint.FromText("We have to go, you know?")!; + Assert.Equal(text, DiscourseTags.Apply(text, hint with { FormalAddress = true, InformalTagContinuation = true })); + } + + [Theory] + [InlineData(true, false, false, false)] + [InlineData(false, true, false, false)] + [InlineData(false, false, true, false)] + [InlineData(false, false, false, true)] + public void PreviousCueEvidenceDoesNotCrossUnsafeBoundaries(bool gap, bool speaker, bool selected, bool sentenceEnd) + { + var blocks = new[] + { + new SrtBlock { StartMs = 0, EndMs = 1000, Text = "Ouve, isto é difícil" + (sentenceEnd ? "." : ",") }, + new SrtBlock { StartMs = gap ? 5000 : 1100, EndMs = 6000, Text = "mas temos de ir, sabe?" }, + }; + var converted = SubtitleConversion.Convert(blocks, new ConversionOptions(), + english: new string?[] { "Listen, this is hard,", "but we have to go, you know?" }, + speakers: speaker ? new string?[] { "A", "B" } : null, + selected: selected ? new HashSet { 1 } : null); + Assert.Equal(blocks[1].Text, converted[1].Text); + } + + [Fact] + public void IgnoredTagRemainsUntouched() + { + var words = new PortugueseLocalWordList(); + words.Ignored.Add("sabe"); + Assert.Equal("É a tua vez, sabe?", new PortugueseConverter(localWordList: words) + .Convert("É a tua vez, sabe?", reference: ReferenceHint.FromText("It's your turn, you know?"))); + } + + [Theory] + [InlineData(null, "mas temos de ir, sabes?")] + [InlineData("ouve", "mas temos de ir, sabe?")] + public void ConvertedPreviousEvidenceWorksWithoutEnglishAndHonorsIgnoredWords(string? ignored, string expected) + { + var words = new PortugueseLocalWordList(); + if (ignored != null) words.Ignored.Add(ignored); + var result = SubtitleConversion.Convert(new[] + { + new SrtBlock { StartMs = 0, EndMs = 1000, Text = "Ouve, isto é difícil," }, + new SrtBlock { StartMs = 1100, EndMs = 3000, Text = "mas temos de ir, sabe?" }, + }, new ConversionOptions(), words); + Assert.Equal(expected, result[1].Text); + } + + [Fact] + public void DisablingTagRulePreventsPreviousCueInferenceFromChangingTheTag() + { + foreach (var options in new[] { new ConversionOptions(YouToTu: false), + new ConversionOptions(DisabledRules: "address"), new ConversionOptions(DisabledRules: "discourse-tags") }) + { + var result = SubtitleConversion.Convert(new[] + { + new SrtBlock { StartMs = 0, EndMs = 1000, Text = "Ouve, isto é difícil," }, + new SrtBlock { StartMs = 1100, EndMs = 3000, Text = "mas temos de ir, sabe?" }, + }, options); + Assert.Equal("mas temos de ir, sabe?", result[1].Text); + } + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/DoorwayLocationTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/DoorwayLocationTests.cs new file mode 100644 index 00000000..631675f1 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/DoorwayLocationTests.cs @@ -0,0 +1,66 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class DoorwayLocationTests +{ + [Theory] + [InlineData("Por que há policiais\nna minha porta?", "Why are there cops at my door?", "Porque há polícias\nà minha porta?")] + [InlineData("Há policiais na tua porta.", null, "Há polícias à tua porta.")] + [InlineData("Há alguém na nossa porta.", null, "Há alguém à nossa porta.")] + [InlineData("Estão na minha porta.", "They are at my door.", "Estão à minha porta.")] + [InlineData("Eles estão na minha porta.", null, "Eles estão à minha porta.")] + [InlineData("Há dois agentes nas nossas portas.", null, "Há dois agentes às nossas portas.")] + [InlineData("Encontraram-se na minha porta.", "They met at my door.", "Encontraram-se à minha porta.")] + [InlineData("HÁ POLICIAIS NA MINHA PORTA.", "There are cops at my door.", "HÁ POLÍCIAS À MINHA PORTA.")] + [InlineData("Há policiais na minha porta.", "There are cops at my door.", "Há polícias à minha porta.")] + public void PeopleAtTheDoorUseEuropeanLocation(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Há sangue na minha porta.")] + [InlineData("Há marcas na minha porta.")] + [InlineData("Está escrito na minha porta.")] + [InlineData("O aviso está na minha porta.")] + [InlineData("Há um buraco na tua porta.")] + [InlineData("A chave está na minha porta.")] + [InlineData("O desenho na minha porta.")] + [InlineData("As dobradiças estão na minha porta.")] + public void SurfaceAndUnresolvedUsesArePreserved(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Fact] + public void LiteralDoorSurfaceWinsOverOtherReferenceLocation() + { + Assert.Equal("Ele está à porta. Há sangue na minha porta.", new PortugueseConverter() + .Convert("Ele está à porta. Há sangue na minha porta.", + reference: ReferenceHint.FromText("He is at my door. There is blood on my door."))); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("na minha porta"); + Assert.Equal("Há polícias na minha porta.", new PortugueseConverter(localWordList: words) + .Convert("Há policiais na minha porta.", reference: ReferenceHint.FromText("There are cops at my door."))); + } + + [Fact] + public void ReportedCueKeepsTimingAndRespectsOptions() + { + var cue = new SrtBlock { StartMs = 904687, EndMs = 906446, Text = "Por que há policiais\nna minha porta?" }; + var result = Assert.Single(SubtitleConversion.Convert(new[] { cue }, ConversionOptions.Default, + english: new string?[] { "Why are there cops at my door?" })); + Assert.Equal("Porque há polícias\nà minha porta?", result.Text); + Assert.Equal(904687, cue.StartMs); + Assert.Equal(906446, cue.EndMs); + Assert.Equal("Há policiais à minha porta.", new PortugueseConverter(new ConversionOptions(Vocabulary: false)) + .Convert("Há policiais na minha porta.", reference: ReferenceHint.FromText("There are cops at my door."))); + Assert.Equal("Há polícias na minha porta.", new PortugueseConverter(new ConversionOptions(Colloquial: false)) + .Convert("Há policiais na minha porta.")); + Assert.Equal("Há polícias na minha porta.", new PortugueseConverter(new ConversionOptions(DisabledRules: "construction-vocabulary")) + .Convert("Há policiais na minha porta.")); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/EnglishPraiseIdiomTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/EnglishPraiseIdiomTests.cs new file mode 100644 index 00000000..619762fb --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/EnglishPraiseIdiomTests.cs @@ -0,0 +1,98 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class EnglishPraiseIdiomTests +{ + [Theory] + [InlineData("Sim, verdadeiro, sim.", "Yeah, he's the real deal, yeah.", "Sim, ele é mesmo bom, sim.")] + [InlineData("Sim, verdadeira, sim.", "Yeah, she's the real deal, yeah.", "Sim, ela é mesmo boa, sim.")] + [InlineData("Ele é o negócio real.", "He's the real deal.", "Ele é mesmo bom.")] + [InlineData("Ela é a coisa real.", "She is the real deal.", "Ela é mesmo boa.")] + [InlineData("Eles são verdadeiros.", "They are the real deal.", "Eles são mesmo bons.")] + [InlineData("Elas são verdadeiras.", "They're the real deal.", "Elas são mesmo boas.")] + [InlineData("Ela era a coisa real.", "She was the real deal.", "Ela era mesmo boa.")] + [InlineData("Ela\né a coisa real.", "She is the real deal.", "Ela\né mesmo boa.")] + [InlineData("Ela é\na coisa real.", "She is the real deal.", "Ela é\nmesmo boa.")] + [InlineData("Ele é o artigo genuíno.", "He's the genuine article.", "Ele é autêntico.")] + [InlineData("Ela é o negócio real.", "She's the real McCoy.", "Ela é autêntica.")] + [InlineData("Ele é verdadeiro.", "He's the real deal, not an impostor.", "Ele é autêntico.")] + [InlineData("SIM, VERDADEIRO, SIM.", "Yeah, he's the real deal, yeah.", "SIM, ELE É MESMO BOM, SIM.")] + [InlineData("Sim, verdadeiro, sim.", "Yeah, he's the real deal, yeah.", + "Sim, ele é mesmo bom, sim.")] + [InlineData("Sim. Esses caras conhecem\nmesmo suas coisas.", "Yeah. This guys really knows his stuff.", + "Sim. Estes tipos sabem\nmesmo o que fazem.")] + [InlineData("Ela conhece as suas coisas.", "She knows her stuff.", "Ela sabe o que faz.")] + [InlineData("Ele sabe suas coisas.", "He knows his stuff.", "Ele sabe o que faz.")] + [InlineData("Nós conhecemos nossas coisas.", "We know our stuff.", "Nós sabemos o que fazemos.")] + [InlineData("Eu conheço minhas coisas.", "I know my stuff.", "Eu sei o que faço.")] + [InlineData("Tu conheces tuas coisas.", "You know your stuff.", "Tu sabes o que fazes.")] + [InlineData("Eles conheciam suas coisas.", "They knew their stuff.", "Eles sabiam o que faziam.")] + [InlineData("ELAS CONHECEM SUAS COISAS.", "They know their stuff.", "ELAS SABEM O QUE FAZEM.")] + [InlineData("Ela conhece suas coisas.", "She knows her stuff.", "Ela sabe o que faz.")] + public void ConfirmedIdiomsUseNaturalPortuguese(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var reference = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: reference)); + Assert.Equal(expected, converter.Convert(expected, reference: reference)); + } + + [Theory] + [InlineData("Sim, verdadeiro, sim.", null)] + [InlineData("Ele é verdadeiro.", "He is honest.")] + [InlineData("Ele é um verdadeiro amigo.", "He's the real deal.")] + [InlineData("O negócio real foi bom.", "He's the real deal.")] + [InlineData("Ela é verdadeira.", "He's the real deal.")] + [InlineData("Ele era verdadeiro.", "He's the real deal.")] + [InlineData("Verdadeiros.", "They're the real deal.")] + [InlineData("Ele é verdadeiro.", "He isn't the real deal.")] + [InlineData("Ele não é verdadeiro.", "He's not the real deal.")] + [InlineData("A realidade dos agentes.", "The real deal from our officers.")] + [InlineData("A coisa real.", "This painting is the genuine article.")] + [InlineData("Sim, verdadeiro, sim.", "He's the real dealer.")] + [InlineData("Ele é verdadeiro.", "He's the real deal. She's the real deal.")] + [InlineData("Ele é verdadeiro. Ela é verdadeira.", "He's the real deal.")] + [InlineData("Ele conhece as suas coisas.", "He recognizes his belongings.")] + [InlineData("Ele conhece as suas coisas.", "He knows her stuff.")] + [InlineData("Ele conhece as suas coisas.", "He knows his stuff is downstairs.")] + [InlineData("Ele conhece as suas coisas antigas.", "He knows his stuff.")] + [InlineData("Ele conhece as suas coisas.", "He knows his stuff. She knows her stuff.")] + public void MissingAmbiguousAndLiteralEvidenceIsProtected(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void SettingsAndLocalChoicesWin() + { + const string source = "Sim, verdadeiro, sim."; + var reference = ReferenceHint.FromText("Yeah, he's the real deal, yeah."); + foreach (var options in new[] { new ConversionOptions(Vocabulary: false), + new ConversionOptions(Colloquial: false), new ConversionOptions(DisabledRules: "english-praise-idioms") }) + Assert.Equal(source, new PortugueseConverter(options).Convert(source, reference: reference)); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("Sim, verdadeiro, sim"); + Assert.Equal(source, new PortugueseConverter(localWordList: words).Convert(source, reference: reference)); + words = new PortugueseLocalWordList(); + words.Words.Add(("verdadeiro", "genuíno")); + Assert.Equal("Sim, genuíno, sim.", new PortugueseConverter(localWordList: words).Convert(source, reference: reference)); + words = new PortugueseLocalWordList(); + words.Words.Add(("conhecer", "reconhecer")); + Assert.Equal("Ele conhece as suas coisas.", new PortugueseConverter(localWordList: words) + .Convert("Ele conhece as suas coisas.", reference: ReferenceHint.FromText("He knows his stuff."))); + } + + [Fact] + public void EpisodeCueKeepsTimesAndOriginal() + { + const string source = "Sim, verdadeiro, sim.\nPode parar de falar agora?"; + var block = new SrtBlock { StartMs = 25126, EndMs = 28555, Text = source }; + var conversion = Assert.Single(SubtitleConversion.Convert(new[] { block }, ConversionOptions.Default, + english: new string?[] { "Yeah, he's the real deal, yeah. Will you just... just stop talking now?" })); + Assert.Equal("Sim, ele é mesmo bom, sim.\nPodes parar de falar agora?", conversion.Text); + Assert.Equal(source, block.Text); + Assert.Equal(25126, block.StartMs); + Assert.Equal(28555, block.EndMs); + Assert.Contains(conversion.Rules, rule => rule.Id == "english-praise-idioms"); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ExplanationTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ExplanationTests.cs new file mode 100644 index 00000000..2f761d58 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ExplanationTests.cs @@ -0,0 +1,149 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class ExplanationTests +{ + [Theory] + [InlineData("Sim, mas acompanhe,\nporque esse é o pulo.", "Yeah, but stay with me, 'cause this is the leap.", + "Sim, mas acompanhe-me,\nporque este é o salto.")] + [InlineData("Sim, mas acompanhe-me,\nporque esse é o pulo.", "Yeah, but stay with me, 'cause this is the leap.", + "Sim, mas acompanhe-me,\nporque este é o salto.")] + [InlineData("Acompanha, este é o pulo.", "Follow me, this is the leap.", "Acompanha-me, este é o salto.")] + [InlineData("Acompanhem, este é o pulo.", "Stay with me, this is the leap.", "Acompanhem-me, este é o salto.")] + [InlineData("Acompanhe, vou explicar.", "Follow my reasoning, I will explain.", "Acompanha-me, vou explicar.")] + [InlineData("Isto é um grande pulo.", "This is a big leap in reasoning.", "Isto é um grande salto.")] + [InlineData("Acompanhe, este é o pulo.", "Stay with me, this is the leap.", "Acompanha-me, este é o salto.")] + [InlineData("ACOMPANHE, ESTE É O PULO.", "Stay with me, this is the leap.", "ACOMPANHA-ME, ESTE É O SALTO.")] + public void FollowAnExplanationAndReasoningLeap(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Sim, mas acompanhe, porque esse é o pulo.", null)] + [InlineData("Acompanhe, este é o pulo.", "Stay with me on the roof before the leap.")] + [InlineData("Acompanhe a cantora, este é o pulo.", "Accompany the singer, this is the leap.")] + [InlineData("Não acompanhe, este é o pulo.", "Stay with me, this is the leap.")] + [InlineData("Acompanhe, é um pulo de fé.", "Stay with me, it is a leap of faith.")] + [InlineData("Acompanhe, este é o pulo.", "Stay with me, this is a physical leap.")] + public void UnconfirmedPhysicalAndCompleteComplementsDoNotGainAnObject(string source, string? english) + { + var converted = new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english)); + Assert.DoesNotContain("-me", converted); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AddressContinuesAcrossCuesWithoutRequiringSentenceContext(bool sentenceContext) + { + var blocks = Scene(); + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: sentenceContext), + english: new string?[] { "What if you got it in your head", "to seek revenge in kind?" }); + Assert.Equal("E se meteste\nna tua cabeça", result[0].Text); + Assert.Equal("que terias a tua vingança?", result[1].Text); + Assert.Contains(result[1].Rules, r => r.Id == "address"); + Assert.Equal(1195438, blocks[1].StartMs); + } + + [Theory] + [InlineData("Você pensa", "que precisaria de ajuda.", "que precisarias de ajuda.")] + [InlineData("Tu disseste", "que teria sua resposta.", "que terias a tua resposta.")] + [InlineData("Você acredita", "que conseguiria vencer.", "que conseguirias vencer.")] + [InlineData("Tu imaginas", "que estaria pronto.", "que estarias pronto.")] + [InlineData("Você pensa", "que ela teria razão.", "que ela teria razão.")] + [InlineData("Você disse ao João", "que teria razão.", "que teria razão.")] + [InlineData("Ela pensa", "que teria razão.", "que teria razão.")] + [InlineData("Você disse.", "que teria razão.", "que teria razão.")] + public void ReviewedGoverningClausesPropagateOnlyTheSameSubject(string previous, string current, string expected) + { + var blocks = Scene(); + blocks[0].Text = previous; + blocks[1].Text = current; + Assert.Equal(expected, SubtitleConversion.Convert(blocks, ConversionOptions.Default, + english: new string?[] { null, expected != current ? "that you would continue." : null })[1].Text); + } + + [Theory] + [InlineData("Você pensa que teria sua resposta.", "You think you would have your answer.", "Pensas que terias a tua resposta.")] + [InlineData("Tu acreditas que conseguiria vencer.", "You believe you would manage to win.", "Tu acreditas que conseguirias vencer.")] + public void SameSentenceKeepsSubjectAcrossQue(string source, string english, string expected) => + Assert.Equal(expected, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void AnUnresolvedReportedSubjectIsNotAssumedToBeTheAddressee() + { + var blocks = Scene(); + blocks[0].Text = "Você pensa"; + blocks[1].Text = "que precisaria de ajuda."; + Assert.Equal(blocks[1].Text, SubtitleConversion.Convert(blocks, ConversionOptions.Default)[1].Text); + } + + [Theory] + [InlineData("que teria ele?")] + [InlineData("que chegaria a polícia.")] + [InlineData("que se faria justiça.")] + public void PostposedAndImpersonalSubjectsAreNotInherited(string current) + { + var blocks = Scene(); + blocks[1].Text = current; + var result = SubtitleConversion.Convert(blocks, ConversionOptions.Default)[1]; + Assert.Equal(current, result.Text); + } + + [Fact] + public void SceneSpeakerRegisterAndOptionBoundariesAreRespected() + { + foreach (var options in new[] + { + new ConversionOptions(Address: AddressMode.Formal), + new ConversionOptions(YouToTu: false), + new ConversionOptions(DisabledRules: "address"), + }) + Assert.StartsWith("que teria ", SubtitleConversion.Convert(Scene(), options)[1].Text); + Assert.StartsWith("que teria ", SubtitleConversion.Convert(Scene(), ConversionOptions.Default, + speakers: new string?[] { "Frank", "Erin" })[1].Text); + var distant = Scene(); + distant[1].StartMs += 10000; + distant[1].EndMs += 10000; + Assert.StartsWith("que teria ", SubtitleConversion.Convert(distant, ConversionOptions.Default)[1].Text); + Assert.Equal("que teria a sua vingança?", SubtitleConversion.Convert(Scene(), ConversionOptions.Default, + english: new string?[] { "What if you got it in your head", "that he would have his revenge?" })[1].Text); + var formal = ReferenceHint.FromText("Stay with me, this is the leap.")! with { FormalAddress = true }; + Assert.Equal("Acompanhe-me, este é o salto.", new PortugueseConverter().Convert( + "Acompanhe, este é o pulo.", reference: formal)); + } + + [Fact] + public void ExplanationOptionsAndLocalOverridesAreRespected() + { + const string text = "Acompanhe, este é o pulo."; + var hint = ReferenceHint.FromText("Stay with me, this is the leap."); + foreach (var options in new[] + { + new ConversionOptions(Colloquial: false), + new ConversionOptions(DisabledRules: "explanation-constructions"), + }) + { + var rules = new List(); + new PortugueseConverter(options).Convert(text, reference: hint, trace: rules); + Assert.DoesNotContain(rules, r => r.Id == "explanation-constructions"); + } + var words = new PortugueseLocalWordList(); + words.Ignored.Add("pulo"); + words.Ignored.Add("acompanhar"); + Assert.DoesNotContain("-me", new PortugueseConverter(localWordList: words).Convert(text, reference: hint)); + Assert.Contains("pulo", new PortugueseConverter(localWordList: words).Convert(text, reference: hint)); + } + + private static SrtBlock[] Scene() => + [ + new() { StartMs = 1192973, EndMs = 1195265, Text = "E se você meteu\nna sua cabeça" }, + new() { StartMs = 1195438, EndMs = 1197064, Text = "que teria sua vingança?" }, + ]; +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/FormalDativeTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/FormalDativeTests.cs new file mode 100644 index 00000000..c3ba6ecf --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/FormalDativeTests.cs @@ -0,0 +1,86 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class FormalDativeTests +{ + [Theory] + [InlineData("- Ele mentiu para você.\n- Só um pouco.", "He lied to you, dummy. Just a little bit.", + "- Ele mentiu-lhe.\n- Só um pouco.")] + [InlineData("Ele mentiu para si.", "He lied to you.", "Ele mentiu-lhe.")] + [InlineData("Ele não mentiu para si.", "He didn't lie to you.", "Ele não lhe mentiu.")] + [InlineData("Sei que ele mentiu para si.", "I know he lied to you.", "Sei que ele lhe mentiu.")] + [InlineData("Ele mentirá para si.", "He will lie to you.", "Ele mentir-lhe-á.")] + [InlineData("Ele mentiria para si.", "He would lie to you.", "Ele mentir-lhe-ia.")] + [InlineData("Ele não mentiria para si.", "He would not lie to you.", "Ele não lhe mentiria.")] + [InlineData("Ele está mentindo para você.", "He is lying to you.", "Ele está a mentir-lhe.")] + [InlineData("Ela respondeu para si.", "She replied to you.", "Ela respondeu-lhe.")] + [InlineData("Ela telefonou para si.", "She phoned you.", "Ela telefonou-lhe.")] + [InlineData("Ele explicou a si.", "He explained to you.", "Ele explicou-lhe.")] + [InlineData("Ele escreveu para si.", "He wrote to you.", "Ele escreveu-lhe.")] + [InlineData("Ele mentiu\npara si.", "He lied to you.", "Ele\nmentiu-lhe.")] + [InlineData("Ele não mentiu\npara si.", "He didn't lie to you.", "Ele não\nlhe mentiu.")] + [InlineData("Ele contou para si.", "He told you.", "Ele contou-lhe.")] + [InlineData("Ele mandou para si.", "He sent you.", "Ele mandou-lhe.")] + [InlineData("ELE MENTIU PARA SI.", "He lied to you.", "ELE MENTIU-LHE.")] + [InlineData("Ele mentiu para si.", "He lied to you.", "Ele mentiu-lhe.")] + public void FormalRecipientsUseExistingDativePlacement(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english)! with { FormalAddress = true }; + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Ele mentiu para si.", null)] + [InlineData("Ele mentiu para si.", "He lied to himself.")] + [InlineData("Ele mentiu para si mesmo.", "He lied to you.")] + [InlineData("Ele mentiu para si e para ela.", "He lied to you and her.")] + [InlineData("Ele mentiu para si e para mim.", "He lied to you and me.")] + [InlineData("Ele mentiu para si e para si.", "He lied to you.")] + [InlineData("Ele mentiu só para si.", "He lied only to you.")] + [InlineData("Ele mentiu para si própria.", "He lied to you.")] + [InlineData("Ele mentiu para si, não para ele.", "He lied to you, not to him.")] + [InlineData("Ele pensou para si.", "He thought to himself.")] + [InlineData("Ele mentiu para si.", "He lied to you and himself.")] + [InlineData("Isto é para si.", "This is for you.")] + public void ReflexiveContrastiveAndUnconfirmedUsesArePreserved(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, + reference: ReferenceHint.FromText(english) is { } hint ? hint with { FormalAddress = true } : null)); + + [Fact] + public void InformalAddressAndDisabledPronounsAreRespected() + { + var hint = ReferenceHint.FromText("He lied to you.")!; + Assert.Equal("Ele mentiu-te.", new PortugueseConverter().Convert("Ele mentiu para você.", reference: hint)); + Assert.Equal("Ele mentiu a SI.", new PortugueseConverter() + .Convert("Ele mentiu para SI.", reference: hint with { FormalAddress = true })); + foreach (var options in new[] { new ConversionOptions(Pronouns: false), + new ConversionOptions(DisabledRules: "pronouns") }) + Assert.Equal("Ele mentiu para si.", new PortugueseConverter(options) + .Convert("Ele mentiu para si.", reference: hint with { FormalAddress = true })); + } + + [Fact] + public void AmbiguousMultipleRecipientsAreNotAssignedByCueLevelEvidence() + { + const string source = "Ele mentiu para si. Ela contou para si."; + Assert.Equal(source, new PortugueseConverter().Convert(source, + reference: ReferenceHint.FromText("He lied to you. She told herself.")! with { FormalAddress = true })); + } + + [Fact] + public void EpisodeCuePreservesOriginalAndTimings() + { + var blocks = new[] { new SrtBlock { StartMs = 1260534, EndMs = 1262422, + Text = "- Ele mentiu para você.\n- Só um pouco." } }; + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(Address: AddressMode.Formal), + english: ["He lied to you, dummy. Just a little bit."]); + Assert.Equal("- Ele mentiu-lhe.\n- Só um pouco.", Assert.Single(result).Text); + Assert.Equal("- Ele mentiu para você.\n- Só um pouco.", blocks[0].Text); + Assert.Equal(1260534, blocks[0].StartMs); + Assert.Equal(1262422, blocks[0].EndMs); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/GiveRequestRecipientTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/GiveRequestRecipientTests.cs new file mode 100644 index 00000000..a478c73b --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/GiveRequestRecipientTests.cs @@ -0,0 +1,104 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class GiveRequestRecipientTests +{ + [Theory] + [InlineData(true, "Rapazes, deem-me um segundo,\ncerto?")] + [InlineData(false, "Rapazes, dêem-me um segundo,\ncerto?")] + public void ActualPluralRequestKeepsPluralAndSelectedOrthography(bool postAgreement, string expected) + { + const string source = "Garotos, dêem um segundo,\ncerto?"; + var converter = new PortugueseConverter(new ConversionOptions(PostAgreement: postAgreement)); + var hint = ReferenceHint.FromText("Boys, give me a second, okay? See you guys later. Bye. Bye."); + var trace = new List(); + Assert.Equal(expected, converter.Convert(source, reference: hint, trace: trace)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + Assert.Contains(trace, r => r.Id == "give-request-recipient"); + } + + [Theory] + [InlineData("Dá um minuto.", "Give me a minute.", "Dá-me um minuto.")] + [InlineData("Dê um momento.", "Give me a moment.", "Dê-me um momento.")] + [InlineData("Deem dois segundos.", "Give us two seconds.", "Deem-nos dois segundos.")] + [InlineData("Dai uma oportunidade.", "Give me a chance.", "Dai-me uma oportunidade.")] + [InlineData("Pessoal, deem uma mão.", "Guys, give us a hand.", "Pessoal, deem-nos uma mão.")] + [InlineData("Dê um pouco de tempo.", "Give me some time.", "Dê-me um pouco de tempo.")] + [InlineData("Dê mais tempo.", "Give me some more time.", "Dê-me mais tempo.")] + [InlineData("Dê uma pausa.", "Give me a break.", "Dê-me uma pausa.")] + [InlineData("Dê uma ajuda.", "Give me some help.", "Dê-me uma ajuda.")] + [InlineData("Dê a sua atenção.", "Give me your attention.", "Dê-me a sua atenção.")] + [InlineData("Ana, dê um segundo.", "Ana, give me a second.", "Ana, dê-me um segundo.")] + [InlineData("Por favor, dê um minuto.", "Please, give me a minute.", "Por favor, dê-me um minuto.")] + [InlineData("DEEM UM SEGUNDO.", "Give us a second.", "DEEM-NOS UM SEGUNDO.")] + public void ReferenceRecoversRecipientWithoutChangingPerson(string source, string english, string expected) + { + Assert.Equal(expected, ConversionConstructions.GiveRequestRecipients(source, english)); + Assert.Equal(expected, ConversionConstructions.GiveRequestRecipients(expected, english)); + } + + [Theory] + [InlineData("Deem um segundo.", null)] + [InlineData("Deem um segundo.", "Give him a second.")] + [InlineData("Deem um segundo.", "Don't give me a second.")] + [InlineData("Deem um segundo.", "He said give me a second.")] + [InlineData("Ele dê um segundo.", "Give me a second.")] + [InlineData("Se ele dê um segundo.", "Give me a second.")] + [InlineData("Não dê um segundo.", "Give me a second.")] + [InlineData("Deem um segundo a ele.", "Give me a second.")] + [InlineData("Deem um segundo para pensar.", "Give me a second.")] + [InlineData("Deem-me um segundo.", "Give me a second.")] + [InlineData("Deem-lhe um segundo.", "Give me a second.")] + [InlineData("Deem um minuto.", "Give me a second.")] + [InlineData("Deem um relógio.", "Give me a watch.")] + [InlineData("Deem um segundo. Deem um segundo.", "Give me a second.")] + [InlineData("Deem um segundo.", "Give me a second. Give us a second.")] + [InlineData("Deem um segundo.", "Can you give me a second?")] + public void AmbiguousLiteralAndAlreadySuppliedRecipientsAreProtected(string text, string? english) => + Assert.Equal(text, ConversionConstructions.GiveRequestRecipients(text, english)); + + [Fact] + public void FormalAddressDoesNotRemoveTheEnglishRecipient() + { + var hint = ReferenceHint.FromText("Give me a second.")! with { FormalAddress = true }; + Assert.Equal("Dê-me um segundo.", new PortugueseConverter().Convert("Dê um segundo.", reference: hint)); + Assert.Equal("Deem-me um segundo.", new PortugueseConverter().Convert("Deem um segundo.", reference: hint)); + } + + [Fact] + public void MarkupIgnoredPhrasesAndDisabledRulesAreRespected() + { + const string text = "Deem um segundo."; + var hint = ReferenceHint.FromText("Give me a second."); + Assert.Equal("Deem-me um segundo.", new PortugueseConverter().Convert("Deem um segundo.", reference: hint)); + foreach (var options in new[] { new ConversionOptions(Pronouns: false), + new ConversionOptions(DisabledRules: "give-request-recipient") }) + Assert.Equal(text, new PortugueseConverter(options).Convert(text, reference: hint)); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("deem um segundo"); + Assert.Equal(text, new PortugueseConverter(localWordList: words).Convert(text, reference: hint)); + } + + [Theory] + [InlineData(true, "deem", "deem")] + [InlineData(true, "dêem", "deem")] + [InlineData(false, "deem", "dêem")] + [InlineData(false, "dêem", "dêem")] + public void DarPluralSpellingIsAuthoritativeEvenWhenDictionaryAcceptsBoth(bool post, string word, string expected) => + Assert.Equal(expected + "-me.", new PortugueseConverter(new ConversionOptions(PostAgreement: post)) + .Convert(word + "-me.")); + + [Fact] + public void SourceAndCueTimingsRemainUnchanged() + { + var block = new SrtBlock { StartMs = 123438, EndMs = 125591, Text = "Garotos, dêem um segundo,\ncerto?" }; + var result = SubtitleConversion.Convert(new[] { block }, new ConversionOptions(), + english: new string?[] { "Boys, give me a second, okay? See you guys later. Bye. Bye." }); + Assert.Equal("Rapazes, deem-me um segundo,\ncerto?", result[0].Text); + Assert.Equal("Garotos, dêem um segundo,\ncerto?", block.Text); + Assert.Equal(123438, block.StartMs); + Assert.Equal(125591, block.EndMs); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/HabitualFamilyTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/HabitualFamilyTests.cs new file mode 100644 index 00000000..d3184ba4 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/HabitualFamilyTests.cs @@ -0,0 +1,146 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class HabitualFamilyTests +{ + [Theory] + [InlineData("Sempre me apoiam.", "Apoiam-me sempre.")] + [InlineData("Sempre me dizem isso.", "Dizem-me sempre isso.")] + [InlineData("Eles sempre me apoiam.", "Eles apoiam-me sempre.")] + [InlineData("Ela sempre me apoia.", "Ela apoia-me sempre.")] + [InlineData("Ele sempre nos apoiou.", "Ele apoiou-nos sempre.")] + [InlineData("Eles sempre me apoiavam.", "Eles apoiavam-me sempre.")] + [InlineData("Eles sempre te ajudam.", "Eles ajudam-te sempre.")] + [InlineData("Sempre nos ajudam.", "Ajudam-nos sempre.")] + [InlineData("Eles sempre se ajudam.", "Eles ajudam-se sempre.")] + [InlineData("Eles sempre lhe respondem.", "Eles respondem-lhe sempre.")] + [InlineData("Eles sempre os ajudam.", "Eles ajudam-nos sempre.")] + [InlineData("Nós sempre nos ajudamos.", "Nós ajudamo-nos sempre.")] + [InlineData("Sempre me defendem.", "Defendem-me sempre.")] + [InlineData("Sempre me protegem.", "Protegem-me sempre.")] + [InlineData("Sempre me acompanham.", "Acompanham-me sempre.")] + [InlineData("Porque sempre me apoiam.", "Porque me apoiam sempre.")] + [InlineData("Porque eles sempre me apoiam.", "Porque eles me apoiam sempre.")] + [InlineData("Quando sempre me ajudam.", "Quando me ajudam sempre.")] + [InlineData("Disse que eles sempre me ajudam.", "Disse que eles me ajudam sempre.")] + [InlineData("SEMPRE ME APOIAM.", "APOIAM-ME SEMPRE.")] + [InlineData("Sempre me apoiam.", "Apoiam-me sempre.")] + [InlineData("Sempre me\napoiam.", "Apoiam-me\nsempre.")] + [InlineData("Sempre, me apoiam.", "Sempre, apoiam-me.")] + public void HabitualOrderAlsoPlacesCliticsCorrectly(string source, string expected) => + AssertConversion(source, null, expected); + + [Theory] + [InlineData("Não me apoiam sempre.")] + [InlineData("Nunca me apoiam.")] + [InlineData("Sempre me apoiarão.")] + [InlineData("Sempre me apoiariam.")] + [InlineData("Eles sempre me podem ajudar.")] + [InlineData("Sempre te achei muito inteligente.")] + public void OtherAttractorsPausesAndUnreviewedVerbChainsAreNotReordered(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Theory] + [InlineData("É como Reagan são criados.", "They're Reagans. It's how we're raised.", "É como os Reagan são criados.")] + [InlineData("É como Reagan são criados.", null, "É como os Reagan são criados.")] + [InlineData("Reagan são criados assim.", null, "Os Reagan são criados assim.")] + [InlineData("Reagan foram educadas assim.", null, "As Reagan foram educadas assim.")] + [InlineData("Silva são unidos.", "The Silvas are close.", "Os Silva são unidos.")] + [InlineData("Smith chegaram.", "The Smiths arrived.", "Os Smith chegaram.")] + [InlineData("Smith estavam em casa.", "The Smiths were at home.", "Os Smith estavam em casa.")] + [InlineData("Smith têm razão.", "The Smith family is right.", "Os Smith têm razão.")] + [InlineData("Smith têm filhas.", "The Smiths have daughters.", "Os Smith têm filhas.")] + [InlineData("Smith nunca chegaram.", "The Smiths never arrived.", "Os Smith nunca chegaram.")] + [InlineData("Smith foram mortas.", "The Smiths were killed.", "As Smith foram mortas.")] + [InlineData("Reagan sempre são criados assim.", null, "Os Reagan sempre são criados assim.")] + [InlineData("Smith são irmãs.", "The Smiths are sisters.", "As Smith são irmãs.")] + [InlineData("Disse que Reagan são criados assim.", null, "Disse que os Reagan são criados assim.")] + [InlineData("É como Smith serão educados.", "It's how the Smiths will be raised.", "É como os Smith serão educados.")] + [InlineData("Cerqueira da Mota chegaram.", "The Cerqueira da Motas arrived.", "Os Cerqueira da Mota chegaram.")] + [InlineData("REAGAN SÃO CRIADOS ASSIM.", "The Reagans are raised this way.", "OS REAGAN SÃO CRIADOS ASSIM.")] + [InlineData("É como Reagan são criados.", null, "É como os Reagan são criados.")] + [InlineData("É como Reagan\nsão criados.", null, "É como os Reagan\nsão criados.")] + public void PluralFamilySubjectsReceiveArticlesWithoutChangingSurnameSpelling(string source, string? english, string expected) => + AssertConversion(source, english, expected); + + [Theory] + [InlineData("É como Reagan é criado.", "It's how Reagan is raised.")] + [InlineData("É como os Reagan são criados.", "It's how the Reagans are raised.")] + [InlineData("As Smith são irmãs.", "The Smiths are sisters.")] + [InlineData("João e Maria são criados assim.", null)] + [InlineData("Reagan, são criados assim?", null)] + [InlineData("Crianças são criadas assim.", null)] + [InlineData("Reagan são altos.", null)] + [InlineData("Reagan são criados assim.", "Reagan is raised this way.")] + [InlineData("É como eles são criados.", "It's how they are raised.")] + [InlineData("É como todos são criados.", null)] + public void SingularNamesCommonNounsCoordinationAndExistingArticlesAreProtected(string source, string? english) => + AssertConversion(source, english, source); + + [Fact] + public void OptionsAndLocalExceptionsRemainEffective() + { + const string family = "É como Reagan são criados."; + foreach (var options in new[] + { + new ConversionOptions(Pronouns: false), + new ConversionOptions(DisabledRules: "family-subjects"), + new ConversionOptions(NameArticles: "Reagan = none"), + }) + Assert.Equal(family, new PortugueseConverter(options).Convert(family)); + foreach (var options in new[] + { + new ConversionOptions(Colloquial: false), + new ConversionOptions(Pronouns: false), + new ConversionOptions(DisabledRules: "natural-word-order"), + }) + Assert.Equal("Sempre me apoiam.", new PortugueseConverter(options).Convert("Sempre me apoiam.")); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("Sempre me apoiam"); + words.Ignored.Add("Reagan"); + var converter = new PortugueseConverter(localWordList: words); + Assert.Equal("Sempre me apoiam.", converter.Convert("Sempre me apoiam.")); + Assert.Equal(family, converter.Convert(family)); + } + + [Fact] + public void ExactEpisodeCueKeepsBothLinesAndTiming() + { + var cue = new SrtBlock { StartMs = 13362, EndMs = 16219, + Text = "Sempre me apoiam.\nÉ como Reagan são criados." }; + var result = Assert.Single(SubtitleConversion.Convert(new[] { cue }, ConversionOptions.Default, + english: new string?[] { "They always got my back. They're Reagans. It's how we're raised." })); + Assert.Equal("Apoiam-me sempre.\nÉ como os Reagan são criados.", result.Text); + Assert.Equal(13362, cue.StartMs); + Assert.Equal(16219, cue.EndMs); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void FamilyTopicCanBeResumedByPluralMembersInFollowingCue(bool sentenceContext) + { + var blocks = new[] + { + new SrtBlock { StartMs = 11098, EndMs = 12492, Text = "A minha família..." }, + new SrtBlock { StartMs = 13362, EndMs = 16219, + Text = "Sempre me apoiam.\nÉ como Reagan são criados." }, + }; + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: sentenceContext), + english: new string?[] { "My family--", "They always got my back. They're Reagans. It's how we're raised." }); + Assert.Equal("A minha família...", result[0].Text); + Assert.Equal("Apoiam-me sempre.\nÉ como os Reagan são criados.", result[1].Text); + Assert.Equal("A minha família apoia-me sempre.", new PortugueseConverter() + .Convert("A minha família me apoia sempre.")); + } + + private static void AssertConversion(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var reference = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: reference)); + Assert.Equal(expected, converter.Convert(expected, reference: reference)); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/HypotheticalReactionTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/HypotheticalReactionTests.cs new file mode 100644 index 00000000..c81a6b84 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/HypotheticalReactionTests.cs @@ -0,0 +1,89 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class HypotheticalReactionTests +{ + [Theory] + [InlineData("Eu também ficaria\nmuito brava se fosse ele.", "Eu também ficaria\nmuito zangada se fosse comigo.")] + [InlineData("Eu ficaria zangado se eu fosse ela.", "Eu ficaria zangado se fosse comigo.")] + [InlineData("Eu estaria triste se fosse ele.", "Eu estaria triste se fosse comigo.")] + [InlineData("Eu não ficaria tão ofendida se fosse ela.", "Eu não ficaria tão ofendida se fosse comigo.")] + [InlineData("Nós ficaríamos preocupados se fôssemos eles.", "Nós ficaríamos preocupados se fosse connosco.")] + [InlineData("Tu ficarias furioso se fosses ele.", "Tu ficarias furioso se fosse contigo.")] + [InlineData("Ela ficaria indignada se fosse ele.", "Ela ficaria indignada se fosse com ela.")] + [InlineData("Eles ficariam abalados se fossem ela.", "Eles ficariam abalados se fosse com eles.")] + [InlineData("Se eu fosse ele, eu ficaria zangada.", "Se fosse comigo, eu ficaria zangada.")] + [InlineData("Se eu fosse ele, ficaria zangada.", "Se fosse comigo, ficaria zangada.")] + [InlineData("Ficaria zangada se eu fosse ele.", "Ficaria zangada se fosse comigo.")] + [InlineData("Se nós fôssemos eles, ficaríamos tristes.", "Se fosse connosco, ficaríamos tristes.")] + [InlineData("Eu ficaria zangada se fosse ele.", "Eu ficaria zangada se fosse comigo.")] + [InlineData("EU FICARIA ZANGADA SE FOSSE ELE.", "EU FICARIA ZANGADA SE FOSSE COMIGO.")] + [InlineData("Tu estarias assustado se fosses ela.", "Tu estarias assustado se fosse contigo.")] + [InlineData("Nós estaríamos revoltados se fôssemos ele.", "Nós estaríamos revoltados se fosse connosco.")] + [InlineData("Elas estariam magoadas se fossem ele.", "Elas estariam magoadas se fosse com elas.")] + public void ReactionsUseTheAffectedPerson(string source, string expected) + { + var converter = new PortugueseConverter(); + Assert.Equal(expected, converter.Convert(source)); + Assert.Equal(expected, converter.Convert(expected)); + } + + [Theory] + [InlineData("Eu ficaria zangada se fosse ele.", "Eu ficaria zangada se fosse ele.")] + [InlineData("Eu estaria triste se tivesse perdido.", "Eu estaria triste se tivesse perdido.")] + [InlineData("Ela ficaria zangada se soubesse.", "Ela ficaria zangada se soubesse.")] + [InlineData("Estaria contente se pudesse.", "Estaria contente se pudesse.")] + [InlineData("Eu ficaria feliz se ganhasse.", "Eu ficaria feliz se ganhasse.")] + [InlineData("Nós ficaríamos tristes se perdêssemos.", "Nós ficaríamos tristes se perdêssemos.")] + public void ConditionalSeNeverBecomesReflexive(string source, string expected) => + Assert.Equal(expected, new PortugueseConverter(new ConversionOptions(Colloquial: false)).Convert(source)); + + [Theory] + [InlineData("Se eu fosse ele, procuraria um advogado.")] + [InlineData("Eu ficaria famoso se fosse ele.")] + [InlineData("Eu ficaria zangada se ela fosse ele.")] + [InlineData("Eu ficaria zangada se fosse ele no palco.")] + [InlineData("Eu ficaria zangada se fosse ele que mentiu.")] + [InlineData("Eu ficaria zangada se não fosse ele.")] + [InlineData("Eu ficaria zangada se fosse eu.")] + [InlineData("Eu ficaria zangada como se fosse ele.")] + [InlineData("Ficaria triste se fosse ele.")] + public void IdentityAdviceAndAmbiguousReactionsAreNotReworded(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Fact] + public void ReferenceResolvesAnOmittedFirstPerson() + { + var converter = new PortugueseConverter(); + Assert.Equal("Ficaria triste se fosse comigo.", converter.Convert("Ficaria triste se fosse ele.", + reference: ReferenceHint.FromText("I'd be sad if I were him."))); + Assert.Equal("Ficaria triste se fosse ele.", converter.Convert("Ficaria triste se fosse ele.", + reference: ReferenceHint.FromText("She'd be sad if she were him."))); + } + + [Fact] + public void OptionsIgnoredPhrasesAndTraceAreRespected() + { + const string source = "Eu ficaria zangada se fosse ele."; + foreach (var options in new[] + { + new ConversionOptions(Colloquial: false), new ConversionOptions(Pronouns: false), + new ConversionOptions(DisabledRules: "hypothetical-reactions"), + }) + Assert.Equal(source, new PortugueseConverter(options).Convert(source)); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("se fosse ele"); + Assert.Equal(source, new PortugueseConverter(localWordList: words).Convert(source)); + var trace = new List(); + new PortugueseConverter().Convert(source, trace: trace); + Assert.Contains(trace, r => r.Id == "hypothetical-reactions"); + } + + [Fact] + public void RealReflexivesStillConvert() + { + Assert.Equal("O rapaz chama-se João.", new PortugueseConverter().Convert("O rapaz se chama João.")); + Assert.Equal("O rapaz sentou-se.", new PortugueseConverter().Convert("O rapaz se sentou.")); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/InterrogationAgreementTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/InterrogationAgreementTests.cs new file mode 100644 index 00000000..f9f56d03 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/InterrogationAgreementTests.cs @@ -0,0 +1,146 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class InterrogationAgreementTests +{ + [Theory] + [InlineData("Já morou na Pensilvânia?", "Did you ever live in Pennsylvania?", "Já viveste na Pensilvânia?")] + [InlineData("Já viveu lá?", "Have you ever lived there?", "Já viveste lá?")] + [InlineData("Trabalhou aqui?", "Did you work here?", "Trabalhaste aqui?")] + [InlineData("Já recebeu a carta?", "Have you received the letter?", "Já recebeste a carta?")] + [InlineData("Já partiu?", "Did you leave already?", "Já partiste?")] + [InlineData("Já esteve aqui?", "Have you been here?", "Já estiveste aqui?")] + [InlineData("Já se envolveu", "You ever involved", "Já te envolveste")] + [InlineData("Já se envolveu\nem roubo de carros lá?", "Were you ever involved in car theft there?", + "Já te envolveste\nem roubo de carros lá?")] + [InlineData("Se envolveu nisso?", "Were you involved in that?", "Envolveste-te nisso?")] + [InlineData("Já se arrependeu?", "Have you ever regretted it?", "Já te arrependeste?")] + [InlineData("Já se comprometeu?", "Have you committed yourself?", "Já te comprometeste?")] + [InlineData("Já viveu lá?", "Did you ever live there?", "Já viveste lá?")] + [InlineData("JÁ VIVEU LÁ?", "Did you ever live there?", "JÁ VIVESTE LÁ?")] + [InlineData("Já vendeu o carro que estava na garagem?", "Did you sell the car that was in the garage?", + "Já vendeste o carro que estava na garagem?")] + [InlineData("Conhece a mulher que viu?", "Do you know the woman you saw?", "Conheces a mulher que viste?")] + [InlineData("Você é um desses caras\nque gosta de entregar", "Are you one of these guys", + "És um desses tipos\nque gosta de entregar")] + [InlineData("Você é casado,\ntenho certeza que entende.", "Yeah. See, you're married; I'm sure you understand.", + "És casado,\ntenho a certeza que percebes.")] + public void ReferenceConfirmedInformalQuestions(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Já viveu lá?", "Did he ever live there?")] + [InlineData("Já viveu lá?", "Did you and Maria live there?")] + [InlineData("Já viveu lá?", "Did you both live there?")] + [InlineData("Já viveu lá?", null)] + [InlineData("Já ele viveu lá?", "Did you ever live there?")] + [InlineData("O João viveu lá?", "Did you ever live there?")] + [InlineData("Já se vende a casa?", "Are you selling the house?")] + public void OtherAndUncertainSubjectsStayProtected(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Theory] + [InlineData("Receio que\nvocê está preso.", "I'm afraid you're under arrest.", "Receio que\nestejas preso.")] + [InlineData("Receio que estás preso.", null, "Receio que estejas preso.")] + [InlineData("Receio que ele está preso.", "I'm afraid he is under arrest.", "Receio que ele esteja preso.")] + [InlineData("Temo que tu tens razão.", null, "Temo que tu tenhas razão.")] + [InlineData("Receamos que eles estão presos.", null, "Receamos que eles estejam presos.")] + [InlineData("Receio que ela precisa de ajuda.", null, "Receio que ela precise de ajuda.")] + [InlineData("Desejo que tu ficas bem.", null, "Desejo que tu fiques bem.")] + [InlineData("Duvido que ele sabe.", null, "Duvido que ele saiba.")] + [InlineData("Receio que ele engana-se.", null, "Receio que ele se engane.")] + [InlineData("Receio que ele não pode vir.", null, "Receio que ele não possa vir.")] + [InlineData("RECEIO QUE ESTÁS PRESO.", null, "RECEIO QUE ESTEJAS PRESO.")] + [InlineData("Receio que estás preso.", null, "Receio que estejas preso.")] + public void ComplementMoodPreservesSubject(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Não duvido que ele sabe.")] + [InlineData("O receio que ele tem.")] + [InlineData("Receio que sim.")] + [InlineData("Receio que não.")] + [InlineData("Receio que ele esteve preso.")] + [InlineData("Disse que ele está preso.")] + [InlineData("Acho que ele está preso.")] + [InlineData("Receio que estejas preso.")] + public void NonTargetsStayUnchanged(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Theory] + [InlineData("Já viveu lá?", "Did you ever live there?")] + [InlineData("Já se envolveu?", "Were you ever involved?")] + public void FormalAndDisabledAddressKeepThirdPerson(string source, string english) + { + var hint = ReferenceHint.FromText(english)!; + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: hint with { FormalAddress = true })); + Assert.Equal(source, new PortugueseConverter(new ConversionOptions(YouToTu: false)).Convert(source, reference: hint)); + Assert.Equal(source, new PortugueseConverter(new ConversionOptions(DisabledRules: "address")).Convert(source, reference: hint)); + } + + [Fact] + public void FormalInvertedQuestionRemainsFormal() => + Assert.Equal("Tem a certeza de que não quer\nque desembale aquilo?", new PortugueseConverter() + .Convert("Está certo que não quer\nque eu desembale aquilo?", + reference: ReferenceHint.FromText("Are you sure you don't want me to unpack that?")! with { FormalAddress = true })); + + [Fact] + public void FormalMoodCorrectionDoesNotImplyInformalAddress() => + Assert.Equal("Receio que esteja preso.", new PortugueseConverter(new ConversionOptions(YouToTu: false)) + .Convert("Receio que está preso.")); + + [Fact] + public void ComplementRuleCanBeDisabled() => + Assert.Equal("Receio que estás preso.", new PortugueseConverter( + new ConversionOptions(DisabledRules: "complement-subjunctive")).Convert("Receio que estás preso.")); + + [Fact] + public void IgnoredPhraseRemainsAuthoritative() + { + var words = new PortugueseLocalWordList(); + words.Ignored.Add("Receio que estás preso"); + Assert.Equal("Receio que estás preso.", new PortugueseConverter(localWordList: words) + .Convert("Receio que estás preso.")); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SplitScenePreservesCueBoundariesAndTimes(bool sentenceContext) + { + const string source = "140\n00:07:09,530 --> 00:07:11,987\nParker Mack.\nÉ seu nome completo?\n\n" + + "141\n00:07:11,990 --> 00:07:13,118\nSim, certo.\n\n" + + "142\n00:07:13,141 --> 00:07:14,934\nJá morou na Pensilvânia?\n\n" + + "143\n00:07:14,936 --> 00:07:17,627\nFui à faculdade lá.\nPor quê? O que há?\n\n" + + "144\n00:07:17,628 --> 00:07:18,989\nJá se envolveu\n\n" + + "145\n00:07:19,007 --> 00:07:21,365\nem roubo de carro lá?\n\n" + + "149\n00:07:29,178 --> 00:07:31,968\nReceio que\nvocê está preso.\n\n"; + var blocks = SubRipParser.Parse(source); + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: sentenceContext), + english: new string?[] { "Parker Mack--is that your full name?", "Yeah, that's right.", + "Did you ever live in Pennsylvania?", "I went to college there. Why? What's up?", + "You ever involved", "in a grand larceny auto situation down there?", + "I'm afraid you're under arrest." }); + Assert.Equal("Já viveste na Pensilvânia?", result[2].Text); + Assert.Equal("Já te envolveste", result[4].Text); + Assert.Equal("Receio que\nestejas preso.", result[6].Text); + Assert.Equal(blocks.Count, result.Count); + Assert.Equal(449178L, blocks[6].StartMs); + Assert.Equal(451968L, blocks[6].EndMs); + Assert.Equal("Já morou na Pensilvânia?", blocks[2].Text); + Assert.Equal("Já se envolveu", blocks[4].Text); + Assert.Equal("Receio que\nvocê está preso.", blocks[6].Text); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/LifePageantTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/LifePageantTests.cs new file mode 100644 index 00000000..1df0d499 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/LifePageantTests.cs @@ -0,0 +1,53 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class LifePageantTests +{ + [Theory] + [InlineData("Rica trama da vida.", "Life's rich pageant.", "O rico espetáculo da vida.")] + [InlineData("A rica trama da vida.", "Life's rich pageant.", "O rico espetáculo da vida.")] + [InlineData("Rico desfile da vida.", "Life’s rich pageant.", "O rico espetáculo da vida.")] + [InlineData("Faz parte da rica trama da vida.", "It's all part of life's rich pageant.", "Faz parte do rico espetáculo da vida.")] + [InlineData("Faz parte do rico desfile da vida.", "It's all part of life's rich pageant.", "Faz parte do rico espetáculo da vida.")] + [InlineData("É parte de uma rica trama da vida.", "It's part of life's rich pageant.", "É parte de uma rica trama da vida.")] + [InlineData("Faz parte de toda a rica trama da vida.", "It's all part of life's rich tapestry.", "Faz parte de toda a rica trama da vida.")] + [InlineData("Na rica trama da vida.", "In life's rich tapestry.", "No rico espetáculo da vida.")] + [InlineData("Rica tapeçaria da vida.", "Life's rich tapestry.", "O rico espetáculo da vida.")] + [InlineData("RICA TRAMA DA VIDA!", "Life's rich pageant!", "O RICO ESPETÁCULO DA VIDA!")] + [InlineData("Rica trama da vida.", "Life's rich pageant.", "O rico espetáculo da vida.")] + [InlineData("Rica trama\nda vida.", "Life's rich pageant.", "O rico espetáculo\nda vida.")] + [InlineData("O rico espetáculo da vida.", "Life's rich pageant.", "O rico espetáculo da vida.")] + public void ReferenceConfirmedLifeMetaphorUsesEuropeanRendering(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Rica trama da vida.", null)] + [InlineData("Rica trama da vida.", "The story of her life.")] + [InlineData("O concurso de beleza começa.", "The beauty pageant begins.")] + [InlineData("A tapeçaria é rica.", "The tapestry is rich.")] + [InlineData("O desfile da vida começa.", "The pageant begins.")] + [InlineData("A vida é rica.", "Life's rich pageant.")] + [InlineData("São as voltas que a vida dá.", "Life's rich pageant.")] + [InlineData("A rica comédia humana.", "Life's rich pageant.")] + public void LiteralAndAlreadyIdiomaticTextIsPreserved(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void OptionsAndDisabledStageAreRespected() + { + var reference = ReferenceHint.FromText("Life's rich pageant."); + foreach (var options in new[] + { + new ConversionOptions(Vocabulary: false), + new ConversionOptions(Colloquial: false), + new ConversionOptions(DisabledRules: "reference-idioms"), + }) + Assert.Equal("Rica trama da vida.", new PortugueseConverter(options).Convert("Rica trama da vida.", reference: reference)); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/NaturalDialogueTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/NaturalDialogueTests.cs index 5af4092e..0f1561f1 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/NaturalDialogueTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/NaturalDialogueTests.cs @@ -25,7 +25,7 @@ public sealed class NaturalDialogueTests [InlineData("João! João.\nVá embora!", "John! John.", "João! João.\nVá-se!")] [InlineData("Está certo que não quer\nque eu desembale aquilo?", "Are you sure you don't want me to unpack that?", - "Tem a certeza de que não quer\nque desembale aquilo?")] + "Tens a certeza de que não queres\nque desembale aquilo?")] public void NaturalDialogueKeepsMeaningAndRegister(string source, string english, string expected) { var converter = new PortugueseConverter(); diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/PeopleVocabularyTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/PeopleVocabularyTests.cs new file mode 100644 index 00000000..bf887922 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/PeopleVocabularyTests.cs @@ -0,0 +1,154 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class PeopleVocabularyTests +{ + [Theory] + [InlineData("Os meninos estão crescendo bem.", "The boys are growing into the role.", "Os rapazes estão a crescer bem.")] + [InlineData("As meninas chegaram.", "The girls arrived.", "As raparigas chegaram.")] + [InlineData("O menino chegou.", "The boy arrived.", "O rapaz chegou.")] + [InlineData("A menina chegou.", "The girl arrived.", "A rapariga chegou.")] + [InlineData("Os garotos chegaram.", "The boys arrived.", "Os rapazes chegaram.")] + [InlineData("As garotas chegaram.", "The girls arrived.", "As raparigas chegaram.")] + [InlineData("Os miúdos chegaram.", "The boys arrived.", "Os rapazes chegaram.")] + [InlineData("As miúdas chegaram.", "The girls arrived.", "As raparigas chegaram.")] + [InlineData("Os moleques chegaram.", "The boys arrived.", "Os rapazes chegaram.")] + [InlineData("As molecas chegaram.", "The girls arrived.", "As raparigas chegaram.")] + [InlineData("Os moços chegaram.", "The boys arrived.", "Os rapazes chegaram.")] + [InlineData("As moças chegaram.", "The girls arrived.", "As raparigas chegaram.")] + [InlineData("Os guris chegaram.", "The boys arrived.", "Os rapazes chegaram.")] + [InlineData("As gurias chegaram.", "The girls arrived.", "As raparigas chegaram.")] + [InlineData("Os meninos e as meninas chegaram.", "The boys and girls arrived.", "Os rapazes e as raparigas chegaram.")] + [InlineData("Os meninos pequenos chegaram.", "The little boys arrived.", "Os rapazes pequenos chegaram.")] + [InlineData("As meninas pequenas chegaram.", "The little girls arrived.", "As raparigas pequenas chegaram.")] + [InlineData("Os meninos chegaram com as crianças.", "The boys arrived with the children.", "Os rapazes chegaram com as crianças.")] + [InlineData("OS MENINOS CHEGARAM.", "The boys arrived.", "OS RAPAZES CHEGARAM.")] + [InlineData("As meninas chegaram.", "The girls arrived.", "As raparigas chegaram.")] + [InlineData("Por favor, minha garota.\nEla precisa de ajuda.", "Please, my girlfriend-- she needs help.", + "Por favor, minha namorada.\nEla precisa de ajuda.")] + [InlineData("A menina chegou.", "My girlfriend arrived.", "A namorada chegou.")] + [InlineData("O menino chegou.", "My boyfriend arrived.", "O namorado chegou.")] + [InlineData("A minha rapariga chegou.", "My girlfriend arrived.", "A minha namorada chegou.")] + [InlineData("O meu rapaz chegou.", "My boyfriend arrived.", "O meu namorado chegou.")] + [InlineData("A moça chegou.", "His girlfriend arrived.", "A namorada chegou.")] + [InlineData("A guria chegou.", "His girlfriend arrived.", "A namorada chegou.")] + [InlineData("O guri chegou.", "Her boyfriend arrived.", "O namorado chegou.")] + [InlineData("As garotas chegaram.", "Their girlfriends arrived.", "As namoradas chegaram.")] + [InlineData("Os garotos chegaram.", "Their boyfriends arrived.", "Os namorados chegaram.")] + [InlineData("A ex-garota chegou.", "His ex-girlfriend arrived.", "A ex-namorada chegou.")] + [InlineData("A GAROTA CHEGOU.", "His girlfriend arrived.", "A NAMORADA CHEGOU.")] + [InlineData("A minha miúda chegou.", "My girlfriend arrived.", "A minha namorada chegou.")] + public void ExplicitEnglishGenderedNounsCoverSourceVariants(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var reference = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: reference)); + Assert.Equal(expected, converter.Convert(expected, reference: reference)); + } + + [Theory] + [InlineData("Os meninos chegaram.", null, "Os meninos chegaram.")] + [InlineData("As meninas chegaram.", "The children arrived.", "As meninas chegaram.")] + [InlineData("Os garotos chegaram.", "The kids arrived.", "Os miúdos chegaram.")] + [InlineData("A minha garota chegou.", null, "A minha miúda chegou.")] + [InlineData("A minha garota chegou.", "My daughter arrived.", "A minha miúda chegou.")] + [InlineData("A menina chegou.", "My friend arrived.", "A menina chegou.")] + [InlineData("A menina e a namorada chegaram.", "The girl and his girlfriend arrived.", "A rapariga e a namorada chegaram.")] + [InlineData("A menina e a moça chegaram.", "His girlfriend and her sister arrived.", "A menina e a rapariga chegaram.")] + [InlineData("O menino chegou.", "His girlfriend arrived.", "O menino chegou.")] + [InlineData("As meninas chegaram.", "His girlfriend arrived.", "As meninas chegaram.")] + [InlineData("A menina chegou.", "His girlfriends arrived.", "A menina chegou.")] + [InlineData("A minha namorada chegou.", "My girlfriend arrived.", "A minha namorada chegou.")] + [InlineData("Os meninos chegaram.", "They arrived.", "Os meninos chegaram.")] + public void MissingOrDifferentEnglishDoesNotInventBoyGirlEvidence(string source, string? english, string expected) => + Assert.Equal(expected, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Theory] + [InlineData("Tento me lembrar\nque sou rico de parceiros,", "Keep trying to remind myself I'm rich with partners, but", "Tento lembrar-me\nque tenho sorte com parceiros,")] + [InlineData("Sou rica de amigas.", "I'm rich in friends.", "Tenho sorte com amigas.")] + [InlineData("Somos ricos em parceiros.", "We're rich with partners.", "Temos sorte com parceiros.")] + [InlineData("Ela é rica de amigos.", "She is rich in friends.", "Ela tem sorte com amigos.")] + [InlineData("Eles são ricos em amigos.", "They are rich in friends.", "Eles têm sorte com amigos.")] + [InlineData("Eu era rico de amigos.", "I was rich in friends.", "Eu tinha sorte com amigos.")] + [InlineData("Serei rico de aliados.", "I'll be rich in allies.", "Terei sorte com aliados.")] + [InlineData("Seria rica em companheiras.", "I'd be rich in companions.", "Teria sorte com companheiras.")] + [InlineData("Não sou rico de amigos.", "I'm not rich in friends.", "Não tenho sorte com amigos.")] + [InlineData("Não fui rico de amigos.", "I was not rich in friends.", "Não tive sorte com amigos.")] + [InlineData("Que sejamos ricos em amigos.", "May we be rich in friends.", "Que tenhamos sorte com amigos.")] + [InlineData("Se fossem ricos de amigos.", "If they were rich in friends.", "Se tivessem sorte com amigos.")] + [InlineData("Sou rico em bons amigos.", "I'm rich in good friends.", "Tenho sorte com bons amigos.")] + [InlineData("Somos ricos de colegas.", "We're rich with colleagues.", "Temos sorte com colegas.")] + [InlineData("É bom ser rico de amigos.", "It's good to be rich in friends.", "É bom ter sorte com amigos.")] + [InlineData("SOU RICO DE PARCEIROS.", "I'm rich with partners.", "TENHO SORTE COM PARCEIROS.")] + [InlineData("Sou rico de amigos.", "I'm rich in friends.", "Tenho sorte com amigos.")] + public void FigurativeHumanWealthUsesNaturalFortuneConstruction(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var reference = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: reference)); + Assert.Equal(expected, converter.Convert(expected, reference: reference)); + } + + [Theory] + [InlineData("Sou rico de parceiros.", null)] + [InlineData("Sou rico de parceiros.", "My partners made me rich.")] + [InlineData("Sou rico de parceiros.", "I'm rich with partners' money.")] + [InlineData("Sou rico em dinheiro.", "I'm rich in money.")] + [InlineData("O leite é rico em cálcio.", "Milk is rich in calcium.")] + [InlineData("O país é rico em recursos.", "The country is rich in resources.")] + public void LiteralAndUnconfirmedWealthIsNotReinterpreted(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void SettingsAndLocalOverridesAreRespected() + { + var hint = ReferenceHint.FromText("The boys and girls arrived."); + var words = new PortugueseLocalWordList(); + words.Words.Add(("meninos", "jovens")); + words.Ignored.Add("meninas"); + Assert.Equal("Os jovens e as meninas chegaram.", + new PortugueseConverter(localWordList: words).Convert("Os meninos e as meninas chegaram.", reference: hint)); + Assert.Equal("Os meninos chegaram.", new PortugueseConverter(new ConversionOptions(Vocabulary: false)) + .Convert("Os meninos chegaram.", reference: hint)); + Assert.Equal("Os meninos chegaram.", new PortugueseConverter(new ConversionOptions(DisabledRules: "verb-vocabulary")) + .Convert("Os meninos chegaram.", reference: hint)); + var rich = ReferenceHint.FromText("I'm rich with partners."); + foreach (var options in new[] + { + new ConversionOptions(Vocabulary: false), + new ConversionOptions(Colloquial: false), + new ConversionOptions(DisabledRules: "reference-idioms"), + }) + Assert.Equal("Sou rico de parceiros.", new PortugueseConverter(options).Convert("Sou rico de parceiros.", reference: rich)); + } + + [Fact] + public void PartnerVocabularyRespectsSettingsAndLocalOverrides() + { + var hint = ReferenceHint.FromText("My girlfriend arrived."); + var words = new PortugueseLocalWordList(); + words.Words.Add(("menina", "jovem")); + words.Ignored.Add("garota"); + var converter = new PortugueseConverter(localWordList: words); + Assert.Equal("A jovem chegou.", converter.Convert("A menina chegou.", reference: hint)); + Assert.Equal("A garota chegou.", converter.Convert("A garota chegou.", reference: hint)); + foreach (var options in new[] { new ConversionOptions(Vocabulary: false), + new ConversionOptions(DisabledRules: "verb-vocabulary") }) + Assert.Equal("A menina chegou.", new PortugueseConverter(options).Convert("A menina chegou.", reference: hint)); + } + + [Fact] + public void ScreenshotPartnerUsesAlignedEnglishAndCannotBeChangedBackByJudge() + { + const string source = "Por favor, minha garota.\nEla precisa de ajuda."; + var blocks = new[] { new SrtBlock { StartMs = 1000, EndMs = 4000, Text = source } }; + var english = new[] { "Please, my girlfriend--\nshe needs help." }; + var result = Assert.Single(SubtitleConversion.Convert(blocks, new ConversionOptions(), english: english)); + Assert.Equal("Por favor, minha namorada.\nEla precisa de ajuda.", result.Text); + Assert.Empty(JudgeAlternatives.Generate(source, result.Text)); + Assert.Equal(1000, blocks[0].StartMs); + Assert.Equal(4000, blocks[0].EndMs); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/PerfectScoreTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/PerfectScoreTests.cs new file mode 100644 index 00000000..66d19da7 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/PerfectScoreTests.cs @@ -0,0 +1,95 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class PerfectScoreTests +{ + [Theory] + [InlineData("Na verdade, gabaritei.", "Actually, I nailed it.", "Na verdade, acertei em tudo.")] + [InlineData("Eu gabaritei a prova.", "I aced the test.", "Eu acertei em tudo na prova.")] + [InlineData("Ela gabaritou o exame.", null, "Ela acertou em tudo no exame.")] + [InlineData("Gabaritamos as provas.", null, "Acertamos em tudo nas provas.")] + [InlineData("Gabaritámos os testes.", null, "Acertámos em tudo nos testes.")] + [InlineData("Gabaritar este exame.", null, "Acertar em tudo neste exame.")] + [InlineData("Ele gabaritará esse teste.", null, "Ele acertará em tudo nesse teste.")] + [InlineData("Ela gabaritaria uma prova.", null, "Ela acertaria em tudo numa prova.")] + [InlineData("Eu gabaritei\nas questões.", null, "Eu acertei\ntodas as questões.")] + [InlineData("Gabaritou todas as perguntas.", null, "Acertou todas as perguntas.")] + [InlineData("Eu não gabaritei.", null, "Eu não acertei em tudo.")] + [InlineData("Eu gabarito.", null, "Eu acerto em tudo.")] + [InlineData("Tu gabaritas.", null, "Tu acertas em tudo.")] + [InlineData("Ela gabaritava.", null, "Ela acertava em tudo.")] + [InlineData("Eles gabaritavam.", null, "Eles acertavam em tudo.")] + [InlineData("Eu gabaritarei.", null, "Eu acertarei em tudo.")] + [InlineData("Nós gabaritaríamos.", null, "Nós acertaríamos em tudo.")] + [InlineData("Se eu gabaritasse.", null, "Se eu acertasse em tudo.")] + [InlineData("Se nós gabaritássemos.", null, "Se nós acertássemos em tudo.")] + [InlineData("Se eles gabaritarem.", null, "Se eles acertarem em tudo.")] + [InlineData("Vós gabaritais.", null, "Vós acertais em tudo.")] + [InlineData("Para gabaritarem.", null, "Para acertarem em tudo.")] + [InlineData("Tenho gabaritado.", null, "Tenho acertado em tudo.")] + [InlineData("O exame foi gabaritado.", null, "O exame foi resolvido sem erros.")] + [InlineData("As provas foram gabaritadas.", null, "As provas foram resolvidas sem erros.")] + [InlineData("Gabaritando.", null, "A acertar em tudo.")] + [InlineData("GABARITEI O TESTE.", null, "ACERTEI EM TUDO NO TESTE.")] + [InlineData("Gabaritei.", null, "Acertei em tudo.")] + [InlineData("O gabarito da prova.", null, "As soluções da prova.")] + [InlineData("Os gabaritos dos testes.", null, "As soluções dos testes.")] + public void ExamAchievementAndAnswerKeys(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("O gabarito de madeira.")] + [InlineData("O gabarito está aqui.")] + [InlineData("Ele tem gabarito.")] + [InlineData("Não tenho gabarito.")] + [InlineData("Gabarito de madeira.")] + [InlineData("Profissional de gabarito.")] + [InlineData("Ele gabaritou o candidato.")] + [InlineData("Ele gabaritou para o cargo.")] + [InlineData("O técnico está gabaritado para o cargo.")] + [InlineData("O técnico está gabaritado.")] + [InlineData("O meu gabarito da prova.")] + [InlineData("Pelo gabarito da prova.")] + [InlineData("A gabaritada.")] + [InlineData("gabaritei_ontem")] + [InlineData("microgabaritei")] + [InlineData("Eu acertei as perguntas.")] + [InlineData("Gabaritar-me.")] + public void OtherSensesAndUnrelatedWordsArePreserved(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Fact] + public void OptionsAndLocalExceptionsRemainAuthoritative() + { + foreach (var options in new[] { new ConversionOptions(Vocabulary: false), + new ConversionOptions(DisabledRules: "reviewed-verb-senses") }) + Assert.Equal("Gabaritei.", new PortugueseConverter(options).Convert("Gabaritei.")); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("gabaritar"); + words.Ignored.Add("gabarito"); + Assert.Equal("Gabaritei.", new PortugueseConverter(localWordList: words).Convert("Gabaritei.")); + Assert.Equal("O gabarito da prova.", new PortugueseConverter(localWordList: words).Convert("O gabarito da prova.")); + words.Words.Add(("gabaritei", "brilhei")); + Assert.Equal("Brilhei.", new PortugueseConverter(localWordList: words).Convert("Gabaritei.")); + Assert.Equal("Gabaritei.", new PortugueseConverter().Convert("Gabaritei.", + reference: ReferenceHint.FromText("I qualified for the job."))); + } + + [Fact] + public void EpisodeCueRetainsTimingAndOriginal() + { + var blocks = new[] { new SrtBlock { StartMs = 2489711, EndMs = 2492103, Text = "Na verdade, gabaritei." } }; + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(), english: ["Actually, I nailed it."]); + Assert.Equal("Na verdade, acertei em tudo.", Assert.Single(result).Text); + Assert.Equal("Na verdade, gabaritei.", blocks[0].Text); + Assert.Equal(2489711, blocks[0].StartMs); + Assert.Equal(2492103, blocks[0].EndMs); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/PoliceNounTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/PoliceNounTests.cs index 94cf96c9..89699bfd 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/PoliceNounTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/PoliceNounTests.cs @@ -8,11 +8,45 @@ public sealed class PoliceNounTests [InlineData("Sem isso, haveria muito\nmais policiais", "Sem isso, haveria muito\nmais polícias")] [InlineData("Precisamos de mais policiais.", "Precisamos de mais polícias.")] [InlineData("Há menos policiais.", "Há menos polícias.")] + [InlineData("Porque há policiais?", "Porque há polícias?")] + [InlineData("Havia policiais.", "Havia polícias.")] + [InlineData("Houve policiais.", "Houve polícias.")] + [InlineData("Haverá policiais.", "Haverá polícias.")] + [InlineData("Haveria policiais.", "Haveria polícias.")] + [InlineData("Se houver policiais.", "Se houver polícias.")] + [InlineData("Há ainda policiais.", "Há ainda polícias.")] + [InlineData("HÁ POLICIAIS.", "HÁ POLÍCIAS.")] + [InlineData("Há policiais.", "Há polícias.")] [InlineData("Chegaram 20 policiais.", "Chegaram 20 polícias.")] [InlineData("MAIS POLICIAIS.", "MAIS POLÍCIAS.")] [InlineData("As operações policiais.", "As operações policiais.")] [InlineData("Mais operações policiais.", "Mais operações policiais.")] [InlineData("Há muitos carros policiais.", "Há muitos carros policiais.")] + [InlineData("Há operações policiais.", "Há operações policiais.")] + [InlineData("Havia investigações policiais.", "Havia investigações policiais.")] + [InlineData("Não para policiais.\nPara policiais, ele é responsável", + "Não para polícias.\nPara polícias, ele é responsável")] + [InlineData("pelas bombas apontadas\npara policiais, fim da história.", + "pelas bombas apontadas\npara polícias, fim da história.")] + [InlineData("Falei com policiais.", "Falei com polícias.")] + [InlineData("Saí sem policiais.", "Saí sem polícias.")] + [InlineData("Estava entre policiais corruptos.", "Estava entre polícias corruptos.")] + [InlineData("Perante policiais, ficou calado.", "Perante polícias, ficou calado.")] + [InlineData("Foi feito por policiais.", "Foi feito por polícias.")] + [InlineData("Entregaram isto a policiais.", "Entregaram isto a polícias.")] + [InlineData("Confio em policiais honestos.", "Confio em polícias honestos.")] + [InlineData("Falei com policiais que conheço.", "Falei com polícias que conheço.")] + [InlineData("Falei com policiais e médicos.", "Falei com polícias e médicos.")] + [InlineData("Para policiais do bairro.", "Para polícias do bairro.")] + [InlineData("PARA POLICIAIS.", "PARA POLÍCIAS.")] + [InlineData("Para policiais.", "Para polícias.")] + [InlineData("Para operações policiais.", "Para operações policiais.")] + [InlineData("Com carros policiais.", "Com carros policiais.")] + [InlineData("Sem provas policiais.", "Sem provas policiais.")] + [InlineData("Em romances policiais.", "Em romances policiais.")] + [InlineData("As séries policiais.", "As séries policiais.")] + [InlineData("Falou com a policiais,", "Falou com a policiais,")] + [InlineData("Para a policiais.", "Para a policiais.")] public void QuantifiersIdentifyPersonnelWithoutChangingAdjectives(string source, string expected) { var converter = new PortugueseConverter(); @@ -28,10 +62,27 @@ public void ReferenceAndUserOptionsRemainEffective() reference: ReferenceHint.FromText("There are more cops."))); Assert.Equal("Há mais agentes.", new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText("There are more officers."))); + Assert.Equal("Há polícias.", new PortugueseConverter().Convert("Há policiais.", + reference: ReferenceHint.FromText("There are cops."))); + Assert.Equal("Há agentes.", new PortugueseConverter().Convert("Há policiais.", + reference: ReferenceHint.FromText("There are officers."))); Assert.Equal(source, new PortugueseConverter(new ConversionOptions(Vocabulary: false)).Convert(source)); Assert.Equal(source, new PortugueseConverter(new ConversionOptions(DisabledRules: "police-nouns")).Convert(source)); + Assert.Equal("Há policiais.", new PortugueseConverter(new ConversionOptions(DisabledRules: "police-nouns")) + .Convert("Há policiais.")); var words = new PortugueseLocalWordList(); words.Ignored.Add("mais policiais"); Assert.Equal(source, new PortugueseConverter(localWordList: words).Convert(source)); + words.Ignored.Add("policiais"); + Assert.Equal("Há policiais.", new PortugueseConverter(localWordList: words).Convert("Há policiais.")); + Assert.Equal("Para policiais.", new PortugueseConverter(localWordList: words).Convert("Para policiais.")); + Assert.Equal("Para policiais.", new PortugueseConverter(new ConversionOptions(DisabledRules: "police-nouns")) + .Convert("Para policiais.")); + Assert.Equal("Para policiais.", new PortugueseConverter(new ConversionOptions(Vocabulary: false)) + .Convert("Para policiais.")); + Assert.Equal("Para polícias.", new PortugueseConverter().Convert("Para policiais.", + reference: ReferenceHint.FromText("To cops."))); + Assert.Equal("Para agentes.", new PortugueseConverter().Convert("Para policiais.", + reference: ReferenceHint.FromText("To officers."))); } } diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/PoliteInfinitiveCommandTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/PoliteInfinitiveCommandTests.cs new file mode 100644 index 00000000..28bd3608 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/PoliteInfinitiveCommandTests.cs @@ -0,0 +1,90 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class PoliteInfinitiveCommandTests +{ + [Theory] + [InlineData("54 anos. Por favor,\navisar se avistarem o suspeito.", + "54 anos. Por favor,\navisem se avistarem o suspeito.")] + [InlineData("Por favor, avisar se virem o suspeito.", "Por favor, avisem se virem o suspeito.")] + [InlineData("Por favor, informar se souberem.", "Por favor, informem se souberem.")] + [InlineData("Por favor, comunicar à central se virem o suspeito.", "Por favor, comuniquem à central se virem o suspeito.")] + [InlineData("Por favor, informar a central se virem o suspeito.", "Por favor, informem a central se virem o suspeito.")] + [InlineData("Por favor, ligar quando chegarem.", "Por favor, liguem quando chegarem.")] + [InlineData("Por favor, telefonar se precisarem.", "Por favor, telefonem se precisarem.")] + [InlineData("Por favor, chamar se quiserem.", "Por favor, chamem se quiserem.")] + [InlineData("Por favor, verificar se encontrarem algo.", "Por favor, verifiquem se encontrarem algo.")] + [InlineData("Por favor, fazer se puderem.", "Por favor, façam se puderem.")] + [InlineData("Por favor, dizer se tiverem dúvidas.", "Por favor, digam se tiverem dúvidas.")] + [InlineData("Por favor, sair quando forem chamados.", "Por favor, saiam quando forem chamados.")] + [InlineData("Por favor, seguir quando chegarem.", "Por favor, sigam quando chegarem.")] + [InlineData("Por favor, pedir se precisarem.", "Por favor, peçam se precisarem.")] + [InlineData("Por favor, manter se quiserem.", "Por favor, mantenham se quiserem.")] + [InlineData("Por favor, comparecer quando puderem.", "Por favor, compareçam quando puderem.")] + [InlineData("Por favor, trazer se tiverem.", "Por favor, tragam se tiverem.")] + [InlineData("Por favor, não entrar se virem o suspeito.", "Por favor, não entrem se virem o suspeito.")] + [InlineData("Por favor, avisar-me se virem o suspeito.", "Por favor, avisem-me se virem o suspeito.")] + [InlineData("Por favor, não avisar-me se virem o suspeito.", "Por favor, não me avisem se virem o suspeito.")] + [InlineData("Por favor avisar se vocês virem o suspeito.", "Por favor avisem se vocês virem o suspeito.")] + [InlineData("Por favor, avisar se não virem o suspeito.", "Por favor, avisem se não virem o suspeito.")] + [InlineData("Por favor, avisar assim que chegarem.", "Por favor, avisem assim que chegarem.")] + [InlineData("POR FAVOR, AVISAR SE AVISTAREM O SUSPEITO.", "POR FAVOR, AVISEM SE AVISTAREM O SUSPEITO.")] + public void SharedPluralConditionalIdentifiesRequestAddressees(string source, string expected) + { + var converter = new PortugueseConverter(); + Assert.Equal(expected, converter.Convert(source)); + Assert.Equal(expected, converter.Convert(expected)); + } + + [Theory] + [InlineData("Por favor, avisar se ele chegar.")] + [InlineData("Por favor, avisar se eles virem o suspeito.")] + [InlineData("Por favor, avisar se os agentes virem o suspeito.")] + [InlineData("Por favor, avisar se eles se virem.")] + [InlineData("Por favor, avisar se houver problemas.")] + [InlineData("Por favor, avisar se houverem problemas.")] + [InlineData("Por favor, avisar se acontecerem problemas.")] + [InlineData("Por favor, avisar se existirem dúvidas.")] + [InlineData("Por favor, avisar se avistar o suspeito.")] + [InlineData("Por favor, avisar se vires o suspeito.")] + [InlineData("Por favor, avisar.")] + [InlineData("É favor avisar se avistarem o suspeito.")] + [InlineData("Nunca fazer funcionar a bomba a seco.")] + [InlineData("Avisar se avistarem o suspeito.")] + [InlineData("Por favor, avisar que eles chegaram.")] + [InlineData("Por favor, informar os agentes se eles chegarem.")] + public void ImpersonalAndDifferentSubjectInstructionsAreNotGuessed(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Fact] + public void OptionsAndLocalExceptionsWin() + { + const string source = "Por favor, avisar se avistarem o suspeito."; + foreach (var options in new[] + { + new ConversionOptions(Colloquial: false), + new ConversionOptions(Pronouns: false), + new ConversionOptions(DisabledRules: "polite-infinitive-commands"), + }) + Assert.Equal(source, new PortugueseConverter(options).Convert(source)); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("avisar"); + Assert.Equal(source, new PortugueseConverter(localWordList: words).Convert(source)); + Assert.Equal("Por favor, avisem se avistarem o suspeito.", + new PortugueseConverter(new ConversionOptions(YouToTu: false)).Convert(source)); + } + + [Fact] + public void ExactDispatchCueRetainsTimingAndLineBreaks() + { + var cue = new SrtBlock { StartMs = 2096332, EndMs = 2099952, + Text = "54 anos. Por favor,\navisar se avistarem o suspeito." }; + var result = Assert.Single(SubtitleConversion.Convert(new[] { cue }, ConversionOptions.Default, + english: new string?[] { "54 years of age. Please advise if contact is made with subject." })); + Assert.Equal("54 anos. Por favor,\navisem se avistarem o suspeito.", result.Text); + Assert.Equal(2096332, cue.StartMs); + Assert.Equal(2099952, cue.EndMs); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/PromiseAndBreakInTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/PromiseAndBreakInTests.cs new file mode 100644 index 00000000..ff629234 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/PromiseAndBreakInTests.cs @@ -0,0 +1,117 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class PromiseAndBreakInTests +{ + [Theory] + [InlineData("Elas vão lacear.", "They'll break in.", "Elas vão ceder.")] + [InlineData("Os sapatos lacearam.", null, "Os sapatos cederam.")] + [InlineData("As botas laceiam com o uso.", null, "As botas cedem com o uso.")] + [InlineData("O sapato laceará.", null, "O sapato cederá.")] + [InlineData("Se laceasse, servia.", null, "Se cedesse, servia.")] + [InlineData("Ela está laceando os sapatos.", null, "Ela está a alargar os sapatos.")] + [InlineData("Vou lacear os sapatos.", null, "Vou alargar os sapatos.")] + [InlineData("Vou lacear sapatos.", null, "Vou alargar sapatos.")] + [InlineData("Vou lacear meus sapatos.", null, "Vou alargar os meus sapatos.")] + [InlineData("Vou lacear um pouco as botas.", null, "Vou alargar um pouco as botas.")] + [InlineData("Laceei as luvas.", null, "Alarguei as luvas.")] + [InlineData("Ela laceou o couro.", null, "Ela amoleceu o couro.")] + [InlineData("O couro vai lacear.", null, "O couro vai amolecer.")] + [InlineData("Os sapatos estão laceados.", null, "Os sapatos estão alargados.")] + [InlineData("Vou laceá-los.", null, "Vou alargá-los.")] + [InlineData("ELAS VÃO LACEAR.", null, "ELAS VÃO CEDER.")] + [InlineData("Elas vão lacear.", null, "Elas vão ceder.")] + public void PhysicalBreakInUsesTheAppropriateEuropeanVerb(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Ela laceou o vestido com fitas.")] + [InlineData("Vou lacear o vestido com laços.")] + public void DecoratingWithBowsIsNotPhysicalStretching(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Theory] + [InlineData("Promete não contar\npara ninguém?", "Promise not to tell anybody?", "Prometes não contar\na ninguém?")] + [InlineData("Promete não rir?", "Promise not to laugh?", "Prometes não te rir?")] + [InlineData("Promete não contar?", "Promise not to tell?", "Prometes não contar?")] + [InlineData("Promete guardar segredo?", "Do you promise to keep a secret?", "Prometes guardar segredo?")] + [InlineData("Promete não rir?", null, "Prometes não te rir?")] + [InlineData("Prometo não rir.", null, "Prometo não me rir.")] + [InlineData("Prometemos não rir.", null, "Prometemos não nos rir.")] + [InlineData("Ela promete não rir.", "She promises not to laugh.", "Ela promete não se rir.")] + [InlineData("Eu prometia não rir.", null, "Eu prometia não me rir.")] + [InlineData("Eu também prometia não rir.", null, "Eu também prometia não me rir.")] + [InlineData("Eu prometeria não rir.", null, "Eu prometeria não me rir.")] + [InlineData("Ele prometia não rir.", null, "Ele prometia não se rir.")] + [InlineData("Prometes rir?", null, "Prometes rir-te?")] + [InlineData("Prometes não te rir?", null, "Prometes não te rir?")] + [InlineData("Prometes não rir-se?", null, "Prometes não te rir?")] + [InlineData("Prometes não se rir?", null, "Prometes não te rir?")] + [InlineData("Prometo nunca rir.", null, "Prometo nunca me rir.")] + [InlineData("Prometeram não rir.", null, "Prometeram não se rir.")] + [InlineData("Prometeste não rir.", null, "Prometeste não te rir.")] + [InlineData("Nós prometíamos não rir.", null, "Nós prometíamos não nos rir.")] + [InlineData("Não contaria para ninguém.", null, "Não contaria a ninguém.")] + [InlineData("PROMETE NÃO RIR?", "Promise not to laugh?", "PROMETES NÃO TE RIR?")] + [InlineData("Promete não rir?", "Promise not to laugh?", "Prometes não te rir?")] + [InlineData("Não contei para ninguém.", null, "Não contei a ninguém.")] + [InlineData("Não disse para ninguém.", null, "Não disse a ninguém.")] + [InlineData("Não disseste para ninguém.", null, "Não disseste a ninguém.")] + [InlineData("Não diríamos para ninguém.", null, "Não diríamos a ninguém.")] + public void PromiseRequestsAndTheirComplementsUseConsistentAddress(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Ela promete não contar?", "Does she promise not to tell?")] + [InlineData("Promete não contar?", "Does he promise not to tell?")] + [InlineData("Promete não contar.", null)] + [InlineData("Prometia não rir.", null)] + [InlineData("Prometeria não rir.", null)] + [InlineData("Ele fez isso para ninguém rir.", null)] + [InlineData("Vou comprar para ninguém.", null)] + public void OtherSubjectsAndUnrelatedPrepositionsArePreserved(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void EllipticalEnglishPromiseIdentifiesTheAddressee() + { + var hint = ReferenceHint.FromText("Promise not to laugh?")!; + Assert.True(hint.SecondPerson); + Assert.True(hint.SecondPersonSubject); + Assert.True(hint.StartsWithSecondPersonSubject); + Assert.False(ReferenceHint.FromText("I promise not to laugh.")!.StartsWithSecondPersonSubject); + } + + [Fact] + public void FormalAndDisabledOptionsKeepTheirIntendedBehavior() + { + var cue = new SrtBlock { StartMs = 1000, EndMs = 2000, Text = "Promete não rir?" }; + Assert.Equal("Promete não se rir?", Assert.Single(SubtitleConversion.Convert(new[] { cue }, + new ConversionOptions(Address: AddressMode.Formal), english: new string?[] { "Promise not to laugh?" })).Text); + Assert.Equal("Promete não rir?", new PortugueseConverter(new ConversionOptions(YouToTu: false, Pronouns: false)) + .Convert(cue.Text)); + Assert.Equal("Elas vão lacear.", new PortugueseConverter(new ConversionOptions(Vocabulary: false)) + .Convert("Elas vão lacear.")); + Assert.Equal("Elas vão lacear.", new PortugueseConverter(new ConversionOptions(DisabledRules: "reviewed-verb-senses")) + .Convert("Elas vão lacear.")); + Assert.Equal("Prometes não rir?", new PortugueseConverter(new ConversionOptions(Pronouns: false)) + .Convert("Promete não rir?", reference: ReferenceHint.FromText("Promise not to laugh?"))); + Assert.Equal("Promete não se rir?", new PortugueseConverter(new ConversionOptions(DisabledRules: "address")) + .Convert("Promete não rir?", reference: ReferenceHint.FromText("Promise not to laugh?"))); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("lacear"); + Assert.Equal("Elas vão lacear.", new PortugueseConverter(localWordList: words).Convert("Elas vão lacear.")); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ReferenceInitialAdverbTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ReferenceInitialAdverbTests.cs index 8412bd8b..211212fc 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/ReferenceInitialAdverbTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ReferenceInitialAdverbTests.cs @@ -72,7 +72,7 @@ public void TimingAlignedCueUsesItsOwnEnglishSubject() var conversions = SubtitleConversion.Convert(blocks, ConversionOptions.Default, english: aligned.Texts); Assert.Equal("Realmente gostas\ndesse tipo, não?", conversions[0].Text); Assert.Contains(conversions[0].Rules, rule => rule.Id == "address"); - Assert.StartsWith("Sim, verdadeiro, sim.", conversions[1].Text); + Assert.Equal("Sim, ele é mesmo bom, sim.\nPodes parar de falar agora?", conversions[1].Text); var response = BatchMode.BuildOkResponse(blocks.ToList(), conversions.Select((cue, i) => (cue, i)) .ToDictionary(item => item.i, item => item.cue.Text), ConversionOptions.Default); var output = SubRipParser.Parse(response.Subtitle!.Native); diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ReferenceTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ReferenceTests.cs index 1f90fa3c..2ab126e1 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/ReferenceTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ReferenceTests.cs @@ -1398,7 +1398,7 @@ public sealed class YoungPersonTests [InlineData("Esses garotos são polícias.", "Those guys are cops.", "Esses rapazes são polícias.")] [InlineData("A garota da loja ligou.", "The girl from the shop called.", "A rapariga da loja ligou.")] [InlineData("O garoto tem cinco anos.", "The kid is five.", "O miúdo tem cinco anos.")] - [InlineData("O garoto chorou.", "The little boy cried.", "O miúdo chorou.")] + [InlineData("O garoto chorou.", "The little boy cried.", "O rapaz chorou.")] [InlineData("O garoto chorou.", "He cried.", "O miúdo chorou.")] public void EnglishReferenceChoosesRapazOrMiudo(string text, string english, string expected) => Assert.Equal(expected, Converter.Convert(text, null, ReferenceHint.FromText(english))); diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ReflexiveCoverageTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ReflexiveCoverageTests.cs new file mode 100644 index 00000000..4db0ca3f --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ReflexiveCoverageTests.cs @@ -0,0 +1,166 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class ReflexiveCoverageTests +{ + public static IEnumerable RegularPronominalExamples() + { + foreach (var (first, plural) in new[] + { + ("lavo", "lavamos"), ("penteio", "penteamos"), ("barbeio", "barbeamos"), + ("deito", "deitamos"), ("levanto", "levantamos"), ("sento", "sentamos"), + ("afasto", "afastamos"), ("aproximo", "aproximamos"), ("preparo", "preparamos"), + ("apresento", "apresentamos"), ("lembro", "lembramos"), ("esqueço", "esquecemos"), + ("queixo", "queixamos"), ("arrependo", "arrependemos"), ("esforço", "esforçamos"), + ("atrevo", "atrevemos"), ("resigno", "resignamos"), ("reconcilio", "reconciliamos"), + ("preocupo", "preocupamos"), ("interesso", "interessamos"), ("importo", "importamos"), + ("comporto", "comportamos"), ("concentro", "concentramos"), ("habituo", "habituamos"), + ("adapto", "adaptamos"), ("recuso", "recusamos"), ("dedico", "dedicamos"), + ("engano", "enganamos"), ("magoo", "magoamos"), ("protejo", "protegemos"), + ("defendo", "defendemos"), ("recolho", "recolhemos"), ("escondo", "escondemos"), + }) + { + yield return new object[] { "Eu me " + first + ".", "Eu " + first + "-me." }; + yield return new object[] { "Nos " + plural + ".", char.ToUpperInvariant(plural[0]) + plural[1..^1] + "-nos." }; + yield return new object[] { "Nunca nos " + plural + ".", "Nunca nos " + plural + "." }; + } + } + + [Theory] + [MemberData(nameof(RegularPronominalExamples))] + public void RegularPronominalFamiliesUseSharedMechanics(string source, string expected) + { + var converter = new PortugueseConverter(); + Assert.Equal(expected, converter.Convert(source)); + Assert.Equal(expected, converter.Convert(expected)); + } + + [Theory] + [InlineData("\"Nos conhecemos\nhá muito tempo\"?", "\"Conhecemo-nos\nhá muito tempo\"?")] + [InlineData("Nos conhecemos há imenso tempo.", "Conhecemo-nos há imenso tempo.")] + [InlineData("«Nos encontramos amanhã.»", "«Encontramo-nos amanhã.»")] + [InlineData("'Nos abraçamos.'", "'Abraçamo-nos.'")] + [InlineData("\"Nos conhecemos.\"", "\"Conhecemo-nos.\"")] + [InlineData("- Nos conhecemos.", "- Conhecemo-nos.")] + [InlineData("NOS CONHECEMOS.", "CONHECEMO-NOS.")] + [InlineData("nos conhecemos.", "conhecemo-nos.")] + [InlineData("\"Nós nos conhecemos.\"", "\"Nós conhecemo-nos.\"")] + [InlineData("Nos arrependemos.", "Arrependemo-nos.")] + [InlineData("Nos esforçamos.", "Esforçamo-nos.")] + [InlineData("Nos queixamos.", "Queixamo-nos.")] + [InlineData("Nos atrevemos.", "Atrevemo-nos.")] + [InlineData("Nos abstemos.", "Abstemo-nos.")] + [InlineData("Nos resignamos.", "Resignamo-nos.")] + [InlineData("Nos entreolhamos.", "Entreolhamo-nos.")] + [InlineData("Nos reconciliamos.", "Reconciliamo-nos.")] + [InlineData("Nos aproximámos.", "Aproximámo-nos.")] + [InlineData("Nos levantávamos.", "Levantávamo-nos.")] + [InlineData("Nos conheceremos.", "Conhecer-nos-emos.")] + [InlineData("Nos arrependeríamos.", "Arrepender-nos-íamos.")] + [InlineData("Nos resignaremos.", "Resignar-nos-emos.")] + [InlineData("\"Me arrependo.\"", "\"Arrependo-me.\"")] + [InlineData("Me resigno.", "Resigno-me.")] + [InlineData("me arrependo.", "arrependo-me.")] + [InlineData("Me queixei.", "Queixei-me.")] + [InlineData("Me esforcei.", "Esforcei-me.")] + [InlineData("Eu me resigno.", "Eu resigno-me.")] + [InlineData("Ela se resignou.", "Ela resignou-se.")] + [InlineData("A equipa se reconciliou.", "A equipa reconciliou-se.")] + [InlineData("Os irmãos se entreolharam.", "Os irmãos entreolharam-se.")] + [InlineData("Eu me resignarei.", "Eu resignar-me-ei.")] + [InlineData("Me tenho lavado.", "Tenho-me lavado.")] + [InlineData("Eu quero vestir-se.", "Eu quero vestir-me.")] + [InlineData("Nós podemos esquecer-se.", "Nós podemos esquecer-nos.")] + [InlineData("Nós estamos a preparar-se.", "Nós estamos a preparar-nos.")] + [InlineData("Eu preciso de concentrar-se.", "Eu preciso de concentrar-me.")] + [InlineData("Vós podeis defender-se.", "Vós podeis defender-vos.")] + [InlineData("Conhecemos-nos.", "Conhecemo-nos.")] + [InlineData("Encontrámos-nos.", "Encontrámo-nos.")] + [InlineData("Ele não lava-se.", "Ele não se lava.")] + [InlineData("Nós nunca conhecemo-nos.", "Nós nunca nos conhecemos.")] + [InlineData("Ela disse que arrependeu-se.", "Ela disse que se arrependeu.")] + [InlineData("Resignarei-me.", "Resignar-me-ei.")] + [InlineData("Eu não resignar-me-ei.", "Eu não me resignarei.")] + [InlineData("Ela não arrepender-se-ia.", "Ela não se arrependeria.")] + [InlineData("Vendem-se casas.", "Vendem-se casas.")] + [InlineData("Aqui trabalha-se.", "Aqui trabalha-se.")] + [InlineData("Me dar um sermão.", "Dar-me um sermão.")] + [InlineData("Certo. Te digo.", "Certo. Digo-te.")] + [InlineData("Te dou vinte minutos.", "Dou-te vinte minutos.")] + [InlineData("Me deram um bolo.", "Deram-me um bolo.")] + [InlineData("-Só cala-te um minuto.", "-Só cala-te um minuto.")] + [InlineData("Sem escuta,\napenas sente-se com ele.", "Sem escuta,\napenas sente-se com ele.")] + public void SharedPlacementCoversReflexiveReciprocalAndInherentPronouns(string source, string expected) + { + var converter = new PortugueseConverter(); + Assert.Equal(expected, converter.Convert(source)); + Assert.Equal(expected, converter.Convert(expected)); + } + + [Theory] + [InlineData("Não nos conhecemos.")] + [InlineData("Já nos conhecemos.")] + [InlineData("Também nos conhecemos.")] + [InlineData("Nunca nos resignamos.")] + [InlineData("Disse que nos conhecemos.")] + [InlineData("Disse que nós nos conhecemos.")] + [InlineData("Quando nos encontramos.")] + [InlineData("Se nos conhecermos.")] + [InlineData("Para nos conhecermos.")] + [InlineData("Nos casos graves.")] + [InlineData("Nos ramos das árvores.")] + [InlineData("Nos termos do contrato.")] + [InlineData("\"Nos próximos dias.\"")] + [InlineData("Nos mesmos sítios.")] + [InlineData("Se me conhecesse, saberia.")] + [InlineData("Perguntou se nos conhecemos.")] + [InlineData("Se resignou, ficou sem emprego.")] + [InlineData("Se lavar a roupa, avisa.")] + [InlineData("Ele lava o carro.")] + [InlineData("Ela lembra o pai.")] + [InlineData("Eu deixo lavar-se.")] + [InlineData("Eu quero lavar-me.")] + [InlineData("Para conhecermo-nos.")] + [InlineData("Eles me queriam também.")] + [InlineData("Por favor, comunicar à central se virem o suspeito.")] + [InlineData("A não ser que Malcom James\nse declare culpado")] + [InlineData("Aposto que se perguntasse,\nela se disporia")] + public void AttractorsPrepositionsConditionsAndNonReflexiveUsesRemain(string source) => + Assert.Equal(source, new PortugueseConverter(new ConversionOptions(Colloquial: false)).Convert(source)); + + [Theory] + [InlineData("Se arrependeu.", "He regretted it.", "Arrependeu-se.")] + [InlineData("\"Se resignou.\"", "She resigned herself to it.", "\"Resignou-se.\"")] + [InlineData("Se esforçou?", "Did he make an effort?", "Esforçou-se?")] + [InlineData("Se resignou, ficou sem emprego.", "If he quit, he lost his job.", "Se resignou, ficou sem emprego.")] + [InlineData("Se resignou.", "I wonder whether he resigned.", "Se resignou.")] + [InlineData("Se está numa delegacia\nprecisa de advogado.", "You're in a police station, you sure as hell need a lawyer.", + "Se está numa esquadra\nprecisas de advogado.")] + public void ReferenceDistinguishesLeadingSeFromConditionalSe(string source, string english, string expected) => + Assert.Equal(expected, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Theory] + [InlineData(false, "")] + [InlineData(true, "pronouns")] + public void SharedFixesRespectPronounSettings(bool pronouns, string disabled) + { + const string source = "\"Nos conhecemos.\""; + Assert.Equal(source, new PortugueseConverter(new ConversionOptions(Pronouns: pronouns, DisabledRules: disabled)) + .Convert(source)); + } + + [Fact] + public void EpisodeQuoteKeepsOriginalAndTimings() + { + const string source = "\"Nos conhecemos\nhá muito tempo\"?"; + var blocks = new[] { new SrtBlock { StartMs = 1571854, EndMs = 1574677, Text = source } }; + var result = Assert.Single(SubtitleConversion.Convert(blocks, new ConversionOptions(), + english: ["\"We've known each other a long time\"?"])); + Assert.Equal("\"Conhecemo-nos\nhá muito tempo\"?", result.Text); + Assert.Equal(source, blocks[0].Text); + Assert.Equal(1571854, blocks[0].StartMs); + Assert.Equal(1574677, blocks[0].EndMs); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/RelativeAndModalGrammarTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/RelativeAndModalGrammarTests.cs new file mode 100644 index 00000000..20061c91 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/RelativeAndModalGrammarTests.cs @@ -0,0 +1,267 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class RelativeAndModalGrammarTests +{ + [Theory] + [InlineData("Havia algo sobre o que esperava falar contigo.", "Havia algo de que esperava falar contigo.")] + [InlineData("Há algo\nsobre o que falar.", "Há algo\nde que falar.")] + [InlineData("Não há nada sobre o que possamos falar.", "Não há nada de que possamos falar.")] + [InlineData("Uma coisa sobre o que eu queria falar.", "Uma coisa de que eu queria falar.")] + [InlineData("A questão sobre o que falámos.", "A questão de que falámos.")] + [InlineData("O assunto sobre o que falaste.", "O assunto de que falaste.")] + [InlineData("HÁ ALGO SOBRE O QUE FALAR.", "HÁ ALGO DE QUE FALAR.")] + [InlineData("Há algo sobre o que falar.", "Há algo de que falar.")] + public void FalarTopicRelativesKeepTheAntecedent(string source, string expected) => + Check(source, null, expected); + + [Theory] + [InlineData("Falámos sobre o que aconteceu.")] + [InlineData("Quero saber sobre o que falaste.")] + [InlineData("Há algo sobre o qual esperava falar contigo.")] + [InlineData("Há algo de que esperava falar contigo.")] + [InlineData("Há algo sobre o que aconteceu antes de falar.")] + [InlineData("Há algo sobre o que escrever.")] + [InlineData("Há algo. Sobre o que falar?")] + [InlineData("Uma questão sobre o que é falar.")] + public void FreeRelativesQuestionsAndOtherComplementsAreNotRewritten(string source) => + Assert.Equal(source, RelativeAndModalGrammar.TopicRelatives(source)); + + [Theory] + [InlineData("Sim. E ela falarei nisto\naté eu ceder.", "Yeah. And she'd bring it up until I relented.", + "Sim. E ela falaria nisto\naté eu ceder.")] + [InlineData("Ele falará contigo.", "He would talk to you.", "Ele falaria contigo.")] + [InlineData("Ela dirá isso.", "She would say that.", "Ela diria isso.")] + [InlineData("Eles farão isso.", "They would do that.", "Eles fariam isso.")] + [InlineData("Nós traremos isso.", "We would bring it.", "Nós traríamos isso.")] + [InlineData("Eu esperarei.", "I would wait.", "Eu esperaria.")] + [InlineData("Tu esperarás.", "You would wait.", "Tu esperarias.")] + [InlineData("Vocês esperarão.", "You would wait.", "Vocês esperariam.")] + [InlineData("Ela não falarei.", "She wouldn't talk.", "Ela não falaria.")] + [InlineData("Ela nunca dirá isso.", "She would never say that.", "Ela nunca diria isso.")] + [InlineData("Ela manterei isso.", "She would keep that.", "Ela manteria isso.")] + [InlineData("ELA FALAREI NISTO.", "She'd bring it up.", "ELA FALARIA NISTO.")] + [InlineData("Ela falarei nisto.", "She'd bring it up.", "Ela falaria nisto.")] + public void EnglishWouldRepairsTenseAndPerson(string source, string english, string expected) => + Check(source, english, expected); + + [Theory] + [InlineData("Ela falarei.", null)] + [InlineData("Ela falará.", "She will talk.")] + [InlineData("Ela falará.", "He would talk.")] + [InlineData("Ela falará.", "She wouldn't talk.")] + [InlineData("Ela não falará.", "She would talk.")] + [InlineData("Ela falará.", "She'd already left.")] + [InlineData("Ela falará.", "She'd read it.")] + [InlineData("Ela virá.", "She'd come home.")] + [InlineData("Ela falará.", "She would rather leave.")] + [InlineData("Ela falará.", "She could talk.")] + [InlineData("Ela falará.", "She would talk but he will leave.")] + [InlineData("Ela falará. Ele dirá isso.", "She would talk. He would say that.")] + [InlineData("Ela falará. Ela dirá isso.", "She would talk.")] + [InlineData("Ela falar-me-á.", "She would talk to me.")] + [InlineData("A mulher falará.", "She would talk.")] + [InlineData("Ela falava.", "She would talk.")] + public void AmbiguousModalContextsAreNotRewritten(string source, string? english) => + Assert.Equal(source, RelativeAndModalGrammar.Conditionals(source, english)); + + [Theory] + [InlineData("Ela falarei.", true)] + [InlineData("Ele dirás.", true)] + [InlineData("Nós farei.", true)] + [InlineData("Eu falarei.", false)] + [InlineData("Ela falará.", false)] + [InlineData("Ela falaria.", false)] + [InlineData("Eles falariam.", false)] + public void WrongFuturePersonIsReviewedWithoutGuessingTense(string text, bool review) => + Assert.Equal(review, GrammarReview.Note(text)?.Contains("future/conditional") == true); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ActualSplitCuesPreserveTextAndTimings(bool context) + { + var blocks = new[] + { + new SrtBlock { StartMs = 114548, EndMs = 116604, Text = "Na verdade, Danny,\nhavia algo" }, + new SrtBlock { StartMs = 116609, EndMs = 118344, Text = "sobre o que esperava\nfalar com você." }, + new SrtBlock { StartMs = 181723, EndMs = 185361, Text = "Sim. E ela falarei nisto\naté eu ceder." }, + }; + var originals = blocks.Select(b => (b.Text, b.StartMs, b.EndMs)).ToArray(); + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: context), + english: new string?[] { "Actually, Danny, there was something", + "I was hoping to talk to you about.", "Yeah. And she'd bring it up until I relented." }); + Assert.Equal(originals[0].Text, result[0].Text); + Assert.Equal("de que esperava\nfalar contigo.", result[1].Text); + Assert.Equal("Sim. E ela falaria nisto\naté eu ceder.", result[2].Text); + if (context) + { + Assert.Contains(result[1].Rules, r => r.Id == "topic-relatives"); + Assert.Same(result[0].MeaningSentence, result[1].MeaningSentence); + Assert.Contains("havia algo sobre o que", result[1].MeaningSentence!.Original); + Assert.Contains("there was something I was hoping", result[1].MeaningSentence!.Reference); + } + else Assert.Null(result[1].MeaningSentence); + Assert.Equal(originals, blocks.Select(b => (b.Text, b.StartMs, b.EndMs)).ToArray()); + } + + [Theory] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + public void SplitAntecedentsDoNotCrossUnsafeBoundaries(bool speakerChange, bool timeGap, bool selection) + { + var blocks = new[] + { + new SrtBlock { StartMs = 0, EndMs = 1000, Text = "Havia algo" }, + new SrtBlock { StartMs = timeGap ? 10000 : 1005, EndMs = 12000, Text = "sobre o que esperava falar contigo." }, + }; + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: true), + selected: selection ? new HashSet { 1 } : null, + speakers: speakerChange ? new string?[] { "Ana", "João" } : null); + Assert.Equal(blocks[1].Text, result[1].Text); + Assert.Null(result[1].MeaningSentence); + } + + [Fact] + public void RuleSettingsAndIgnoredPhrasesWin() + { + const string relative = "Há algo sobre o que falar."; + Assert.Equal(relative, new PortugueseConverter(new ConversionOptions(DisabledRules: "topic-relatives")).Convert(relative)); + Assert.Equal(relative, new PortugueseConverter(new ConversionOptions(Colloquial: false)).Convert(relative)); + var hint = ReferenceHint.FromText("She would talk."); + const string modal = "Ela falarei."; + Assert.Equal(modal, new PortugueseConverter(new ConversionOptions(DisabledRules: "reference-conditional")) + .Convert(modal, reference: hint)); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("ela falarei"); + words.Ignored.Add("algo sobre o que falar"); + Assert.Equal(modal, new PortugueseConverter(localWordList: words).Convert(modal, reference: hint)); + Assert.Equal(relative, new PortugueseConverter(localWordList: words).Convert(relative)); + Assert.Equal("Ela falaria.", new PortugueseConverter(new ConversionOptions(YouToTu: false)) + .Convert(modal, reference: hint! with { FormalAddress = true })); + } + + [Fact] + public void DisabledRelativeRuleStillGetsSentenceReview() + { + var blocks = new[] + { + new SrtBlock { StartMs = 0, EndMs = 1000, Text = "Havia algo" }, + new SrtBlock { StartMs = 1005, EndMs = 2000, Text = "sobre o que esperava falar contigo." }, + }; + var result = SubtitleConversion.Convert(blocks, + new ConversionOptions(SentenceContext: true, DisabledRules: "topic-relatives")); + Assert.Equal(blocks[1].Text, result[1].Text); + Assert.Contains("explicit antecedent", result[1].ContextNote); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void PreviousCueDoesNotRequireEnglishAndHonorsIgnoredAntecedents(bool context, bool ignore) + { + var blocks = new[] + { + new SrtBlock { StartMs = 0, EndMs = 1000, Text = "Havia algo" }, + new SrtBlock { StartMs = 1005, EndMs = 2000, Text = "sobre o que esperava falar contigo." }, + }; + var words = new PortugueseLocalWordList(); + if (ignore) words.Ignored.Add("algo"); + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: context), words); + Assert.Equal(ignore ? blocks[1].Text : "de que esperava falar contigo.", result[1].Text); + } + + [Theory] + [InlineData("Havia algo.", "sobre o que esperava falar contigo.")] + [InlineData("Havia algo", "Sobre o que esperava falar contigo?")] + [InlineData("Havia algo", "sobre o que esperava falar contigo.")] + [InlineData("ANA: Havia algo", "JOÃO: sobre o que esperava falar contigo.")] + public void PreviousCueNeedsAnUnbrokenRelativeContinuation(string previous, string text) + { + var result = SubtitleConversion.Convert(new[] + { + new SrtBlock { StartMs = 0, EndMs = 1000, Text = previous }, + new SrtBlock { StartMs = 1005, EndMs = 2000, Text = text }, + }, new ConversionOptions()); + Assert.Equal(text, result[1].Text); + } + + [Theory] + [InlineData(.8f, .8f, false)] + [InlineData(.2f, .2f, true)] + [InlineData(.8f, .75f, true)] + [InlineData(.8f, .78f, false)] + [InlineData(.4501f, .4501f, false)] + [InlineData(.4499f, .4499f, true)] + [InlineData(.5999f, .54f, false)] + [InlineData(.6001f, .54f, true)] + [InlineData(.8f, .7601f, false)] + [InlineData(.8f, .7599f, true)] + public async Task SentenceMeaningUsesActualCombinedTextAndExistingThresholds(float before, float after, bool flagged) + { + var sentence = new SentenceMeaning(35, 36, "Havia algo sobre o que falar.", "There was something to talk about."); + const string converted = "Havia algo de que falar."; + var seen = new List(); + Task Embed(string? text) + { + seen.Add(text); + return Task.FromResult(text == sentence.Reference + ? new[] { 1f, 0f, 0f } + : new[] { text == sentence.Original ? before : after, + MathF.Sqrt(1 - MathF.Pow(text == sentence.Original ? before : after, 2)), 0f }); + } + var note = await LocalChecks.SentenceMeaningNoteAsync(sentence, converted, Embed); + Assert.Equal(flagged, note != null); + Assert.Equal(new[] { sentence.Original, converted, sentence.Reference }, seen); + if (flagged) + { + Assert.Contains("cues 36-37", note); + Assert.Contains("does not validate relative pronouns or verb agreement", note); + } + } + + [Fact] + public async Task SentenceMeaningCanFlagOriginalChangeWithoutEnglishAndDoesNotHideFailures() + { + var sentence = new SentenceMeaning(0, 1, "Havia algo para falar.", null); + var calls = 0; + Task Embed(string? text) + { + calls++; + return Task.FromResult(text == sentence.Original ? new[] { 1f, 0f } : new[] { .69f, .7f }); + } + Assert.NotNull(await LocalChecks.SentenceMeaningNoteAsync(sentence, "Não havia nada para falar.", Embed)); + Assert.Equal(2, calls); + await Assert.ThrowsAsync(() => LocalChecks.SentenceMeaningNoteAsync(sentence, "Mudou.", + _ => throw new IOException("Embedding failed."))); + Assert.Null(await LocalChecks.SentenceMeaningNoteAsync(sentence, sentence.Original, + _ => throw new InvalidOperationException("Unchanged unreferenced text needs no scoring."))); + } + + [Fact] + public async Task UnchangedSentenceStillChecksEnglishAndReusesOriginalEmbedding() + { + var sentence = new SentenceMeaning(0, 1, "Há algo para falar.", "There is something to talk about."); + var calls = 0; + Task Embed(string? text) + { + calls++; + return Task.FromResult(text == sentence.Reference ? new[] { 1f, 0f } : new[] { .3f, .95f }); + } + Assert.NotNull(await LocalChecks.SentenceMeaningNoteAsync(sentence, sentence.Original, Embed)); + Assert.Equal(2, calls); + } + + private static void Check(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ReviewedDialogueTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ReviewedDialogueTests.cs new file mode 100644 index 00000000..f899ab6a --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ReviewedDialogueTests.cs @@ -0,0 +1,270 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class ReviewedDialogueTests +{ + [Theory] + [InlineData("Os médicos disseram\nque ficará bem.", "Doc says you're gonna be okay.", "Os médicos disseram\nque ficarás bem.")] + [InlineData("Acho que está se enganando.", "I think you're fooling yourself.", "Acho que estás a enganar-te.")] + [InlineData("Acho que está a divertir-se.", "I think you're having fun.", "Acho que estás a divertir-te.")] + [InlineData("Estavas a divertir-se.", "You were having fun.", "Estavas a divertir-te.")] + [InlineData("Ela disse que vai melhorar.", "She said you'll get better.", "Ela disse que vais melhorar.")] + [InlineData("Disseram que estava certo.", "They said you'd be right.", "Disseram que estavas certo.")] + [InlineData("Se importa responder\nalgumas perguntas?", "Would you mind answering a few questions for me?", + "Importas-te de responder\na algumas perguntas?")] + [InlineData("Se importa abrir a porta?", "Do you mind opening the door?", "Importas-te de abrir a porta?")] + [InlineData("Importa-se de esperar?", "Would you mind waiting?", "Importas-te de esperar?")] + [InlineData("Se importa responder a estas perguntas?", "Would you mind answering these questions?", "Importas-te de responder a estas perguntas?")] + [InlineData("Não se importa esperar?", "Would you mind waiting?", "Não te importas de esperar?")] + [InlineData("Se importaria esperar?", "Would you mind waiting?", "Importar-te-ias de esperar?")] + [InlineData("Se importa esperar?", "Would you mind waiting?", "Importas-te de esperar?")] + [InlineData("SE IMPORTA ESPERAR?", "Would you mind waiting?", "IMPORTAS-TE DE ESPERAR?")] + public void EmbeddedContractionsAndMindRequests(string source, string english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Os médicos disseram que ele ficará bem.", "The doctors said he'll be okay.", "ficarás")] + [InlineData("Os médicos disseram que ficará bem.", null, "ficarás")] + [InlineData("A pessoa que ela disse que ficará bem.", "The person she said would be okay.", "ficarás")] + [InlineData("Ela disse que ficará bem.", "She said she'll be fine after you leave.", "ficarás")] + [InlineData("Os médicos disseram que é necessário descansar.", "The doctors said you're supposed to rest.", "és necessário")] + [InlineData("Alguém que pensa que você é esperto.", "Someone who thinks you're smart.", "pensas")] + [InlineData("Se importa, responda.", "If it matters, answer.", "Importas-te")] + [InlineData("Não importa responder.", "Would you mind answering?", "importas-te")] + [InlineData("Ele se importa de responder?", "Would he mind answering?", "Importas-te")] + [InlineData("Se importa responder?", null, "Importas-te")] + public void EmbeddedAndRequestSafeguards(string source, string? english, string absent) => + Assert.DoesNotContain(absent, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void HospitalDialogueUsesInformalAgreementWithoutChangingOtherSubjects() + { + var source = new[] + { + Cue(1046265, 1048391, "Que engraçado."), + Cue(1048393, 1050087, "Os médicos disseram\nque ficará bem."), + Cue(1050098, 1051493, "Então por que\nestá aqui?"), + Cue(1051603, 1053230, "Porque\npeguei o caso."), + Cue(1054507, 1056400, "Se importa responder\nalgumas perguntas?"), + Cue(1056453, 1057848, "Sim, me importo."), + }; + string?[] english = ["That's funny.", "Doc says you're gonna be okay.", "Yeah", + "'Cause I caught the case.", "Would you mind answering a few questions for me?", "Yeah, I do mind."]; + foreach (var context in new[] { false, true }) + { + var result = SubtitleConversion.Convert(source, new ConversionOptions(SentenceContext: context), english: english); + Assert.Equal("Os médicos disseram\nque ficarás bem.", result[1].Text); + Assert.Equal("Então porque\nestás aqui?", result[2].Text); + Assert.Equal("Importas-te de responder\na algumas perguntas?", result[4].Text); + Assert.Equal("Sim, importo-me.", result[5].Text); + } + } + + [Fact] + public void HospitalFixesRespectFormalAddressAndDisabledOptions() + { + var reported = ReferenceHint.FromText("Doc says you're gonna be okay.")!; + foreach (var options in new[] { new ConversionOptions(YouToTu: false), + new ConversionOptions(DisabledRules: "address") }) + Assert.Equal("Disseram que ficará bem.", new PortugueseConverter(options) + .Convert("Disseram que ficará bem.", reference: reported)); + Assert.Equal("Disseram que ficará bem.", new PortugueseConverter() + .Convert("Disseram que ficará bem.", reference: reported with { FormalAddress = true })); + var request = ReferenceHint.FromText("Would you mind answering a few questions?")!; + Assert.Equal("Importa-se de responder a algumas perguntas.", new PortugueseConverter() + .Convert("Se importa responder algumas perguntas.", reference: request with { FormalAddress = true })); + Assert.Equal("Importa-se de responder a algumas perguntas?", new PortugueseConverter(new ConversionOptions(YouToTu: false)) + .Convert("Se importa responder algumas perguntas?", reference: request)); + Assert.Equal("Importa-se de responder a algumas perguntas?", new PortugueseConverter(new ConversionOptions(DisabledRules: "address")) + .Convert("Se importa responder algumas perguntas?", reference: request)); + Assert.DoesNotContain(" de ", new PortugueseConverter(new ConversionOptions(DisabledRules: "dialogue-objects")) + .Convert("Se importa responder algumas perguntas?", reference: request)); + Assert.DoesNotContain(" de ", new PortugueseConverter(new ConversionOptions(Pronouns: false)) + .Convert("Se importa responder algumas perguntas?", reference: request)); + var local = new PortugueseLocalWordList(); + local.Ignored.Add("importa"); + Assert.DoesNotContain(" de ", new PortugueseConverter(localWordList: local) + .Convert("Se importa responder algumas perguntas?", reference: request)); + } + + [Theory] + [InlineData("Vi a coletiva de imprensa.", "I saw the press conference.", "Vi a conferência de imprensa.")] + [InlineData("Organizaram coletivas de imprensa.", null, "Organizaram conferências de imprensa.")] + [InlineData("Vi a colectiva de imprensa.", null, "Vi a conferência de imprensa.")] + [InlineData("VI AS COLETIVAS DE IMPRENSA.", null, "VI AS CONFERÊNCIAS DE IMPRENSA.")] + [InlineData("Ela quer uma coletiva\nde imprensa.", null, "Ela quer uma conferência\nde imprensa.")] + [InlineData("Não numa coletiva do\ndepartamento.", "Not at a department press conference.", "Não numa conferência de imprensa do\ndepartamento.")] + [InlineData("Cancelei minha coletiva.", "I canceled my presser.", "Cancelei a minha conferência de imprensa.")] + [InlineData("De coletivas ou exclusivas?", "Press conference or select interviews?", "De conferências de imprensa ou entrevistas exclusivas?")] + [InlineData("Acontece que esta dona é a presidente.", "This broad happens to be the mayor.", "Acontece que esta tipa é a presidente.")] + [InlineData("Estas donas chegaram.", "These broads arrived.", "Estas tipas chegaram.")] + [InlineData("Vai deixar esta dona\npô-lo para fora?", "You're just gonna let this broad push you out the door?", + "Vais deixar esta tipa\npôr-te para fora?")] + [InlineData("Ela tentou pô-lo para fora.", "She tried to push you out.", "Ela tentou pôr-te para fora.")] + [InlineData("Esta dona chegou.", "This broad arrived.", "Esta tipa chegou.")] + [InlineData("E um duramente aprendido.", "And a hard-earned one.", "E um aprendido a duras penas.")] + [InlineData("Uma lição duramente aprendida.", "A hard-learned lesson.", "Uma lição aprendida a duras penas.")] + [InlineData("Foram factos duramente aprendidos.", "Hard-earned facts.", "Foram factos aprendidos a duras penas.")] + [InlineData("DURAMENTE APRENDIDAS.", "Hard-earned.", "APRENDIDAS A DURAS PENAS.")] + [InlineData("Vai fazer negócio\nou o quê?", "Doing business here or what?", "Vamos fazer negócio\nou quê?")] + [InlineData("Vamos fazer negócios aqui ou o quê?", "Are we doing business here or what?", "Vamos fazer negócios aqui ou quê?")] + [InlineData("Quatro custarão $ 200.", "Four will cost you... $200.", "Quatro custam 200 dólares.")] + [InlineData("Um custará 20 euros.", "One will cost you 20 euros.", "Um custa 20 euros.")] + [InlineData("Devo lembrar que Nicky está se formando.", "I need to remind you that Nicky is graduating.", "Devo lembrar-te que Nicky está a formar-se.")] + [InlineData("Preciso lembrá-la que ele chegou.", "I need to remind you that he arrived.", "Preciso de lembrar-te que ele chegou.")] + [InlineData("Deve isso a ela.", "You owe that to her.", "Deves-lhe isso.")] + [InlineData("Eu devo isto a eles.", "I owe this to them.", "Eu devo-lhes isto.")] + [InlineData("Não devo isso a ela.", "I don't owe that to her.", "Não lhe devo isso.")] + [InlineData("Eu deverei isso a ela.", "I will owe that to her.", "Eu dever-lhe-ei isso.")] + [InlineData("Eu deveria isso a ela.", "I would owe that to her.", "Eu dever-lhe-ia isso.")] + [InlineData("Acho que eu devo isso a ela.", "I think I owe that to her.", "Acho que eu lhe devo isso.")] + public void ReviewedSensesAndConstructions(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Esta dona de casa chegou.", "This broad arrived.", "tipa")] + [InlineData("A Dona Maria chegou.", "This broad arrived.", "tipa")] + [InlineData("Ela é a dona.", "She is the owner.", "tipa")] + [InlineData("Esta dona chegou.", null, "tipa")] + [InlineData("Uma decisão coletiva.", "A collective decision at the press conference.", "conferência")] + [InlineData("Uma coletiva de arte.", null, "conferência")] + [InlineData("O dinheiro duramente conquistado.", "Hard-earned money.", "penas")] + [InlineData("Ele foi julgado duramente.", "He was judged harshly.", "penas")] + [InlineData("Vai fazer negócio ou o quê?", "Are you doing business or what?", "Vamos")] + [InlineData("Quatro custarão 200 euros amanhã.", "Four will cost you 200 euros tomorrow.", "custam")] + [InlineData("Se encomendar, quatro custarão 200 euros.", "If you order, four will cost you 200 euros.", "custam")] + [InlineData("Quatro custarão 200 euros.", null, "custam")] + [InlineData("Quatro custariam 200 euros.", "Four would cost you 200 euros.", "custam")] + [InlineData("Devo lembrar que ele chegou.", "I must remember that he arrived.", "lembrar-te")] + [InlineData("Devo lembrar que ele chegou.", null, "lembrar-te")] + [InlineData("Deve isso a ela, não a ele.", "You owe that to her, not to him.", "-lhe")] + [InlineData("Ela tentou pô-lo para fora.", "She tried to push him out.", "pôr-te")] + public void OtherSensesAndUnconfirmedInferencesArePreserved(string source, string? english, string absent) => + Assert.DoesNotContain(absent, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void GenuineFuturePriceInPreviousCueIsPreserved() => + Assert.Contains("custarão", new PortugueseConverter().Convert("Quatro custarão 200 euros.", + "A entrega será amanhã.", ReferenceHint.FromText("Four will cost you 200 euros."))); + + [Fact] + public void FormalAndDisabledAddressKeepReminderRecipientFormal() + { + var hint = ReferenceHint.FromText("I need to remind you that she is graduating.")! with { FormalAddress = true }; + Assert.Equal("Devo lembrar-lhe que ela está a formar-se.", new PortugueseConverter().Convert( + "Devo lembrar que ela está se formando.", reference: hint)); + Assert.Equal("Ela tentou pô-lo para fora.", new PortugueseConverter().Convert("Ela tentou pô-lo para fora.", + reference: ReferenceHint.FromText("She tried to push you out.")! with { FormalAddress = true })); + Assert.Contains("lembrar-lhe", new PortugueseConverter(new ConversionOptions(YouToTu: false)).Convert( + "Devo lembrar que ela chegou.", reference: ReferenceHint.FromText("I remind you that she arrived."))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void NewNamedSummonsDoesNotMakeTheEarlierFamilyMonologueFormal(bool sentenceContext) + { + var blocks = FamilyScene(); + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: sentenceContext), english: FamilyEnglish()); + Assert.All(result.Take(10), r => Assert.False(r.Hint?.FormalAddress == true)); + Assert.True(result[10].Hint!.FormalAddress); + Assert.Equal("Devo lembrar-te que Nicky\nestá a formar-se este ano", result[2].Text); + Assert.Equal("e precisas de estar lá.", result[3].Text); + Assert.Equal("é o teu trabalho levá-la\npara o altar.", result[5].Text); + Assert.Equal("Percebeste?", result[6].Text); + Assert.Equal("Precisas de sair desta, Jack.", result[7].Text); + Assert.Equal("Deves-lhe isso.", result[8].Text); + Assert.Equal("Ouviste?", result[9].Text); + Assert.Equal("O seu marido precisa de cirurgia.", result[11].Text); + Assert.Equal(741375, blocks[2].StartMs); + } + + [Theory] + [InlineData("Precisa sair desta, Jack.", "You've got to pull through, Jack.", false)] + [InlineData("Precisa sair desta, Reagan.", "You've got to pull through, Reagan.", true)] + [InlineData("Precisa sair desta, senhor.", "You've got to pull through, sir.", true)] + public void NamedSummonsBoundaryRequiresADifferentUntitledRecipient(string previous, string english, bool formal) + { + var blocks = new[] { Cue(0, 1000, previous), Cue(1100, 2000, "Ouviu?"), Cue(2100, 3000, "Sra. Reagan?") }; + Assert.Equal(formal, DialogueRegister.IsFormal(blocks, 1, new string?[] { english, "You hear me?", "Ms. Reagan?" })); + Assert.True(DialogueRegister.IsFormal(blocks, 2)); + } + + [Fact] + public void LocalExceptionsAndStageSwitchesAreRespected() + { + var words = new PortugueseLocalWordList(); + foreach (var word in new[] { "dona", "coletiva", "duramente", "custar", "lembrar", "dever", "fazer" }) + words.Ignored.Add(word); + var converter = new PortugueseConverter(localWordList: words); + Assert.Contains("dona", converter.Convert("Esta dona chegou.", reference: ReferenceHint.FromText("This broad arrived."))); + Assert.Contains("coletiva", converter.Convert("Uma coletiva.", reference: ReferenceHint.FromText("A presser."))); + Assert.Contains("Duramente", converter.Convert("Duramente aprendido.", reference: ReferenceHint.FromText("Hard-earned."))); + Assert.Contains("custarão", converter.Convert("Quatro custarão 200 euros.", reference: ReferenceHint.FromText("Four will cost you 200 euros."))); + Assert.DoesNotContain("lembrar-te", converter.Convert("Devo lembrar que ela chegou.", reference: ReferenceHint.FromText("I remind you that she arrived."))); + Assert.DoesNotContain("-lhe", converter.Convert("Eu devo isso a ela.", reference: ReferenceHint.FromText("I owe that to her."))); + foreach (var (stage, source, english) in new[] + { + ("dialogue-objects", "Devo lembrar que ela chegou.", "I remind you that she arrived."), + ("dialogue-vocabulary", "Esta dona chegou.", "This broad arrived."), + ("dialogue-idioms", "E um duramente aprendido.", "And a hard-earned one."), + }) + { + var rules = new List(); + new PortugueseConverter(new ConversionOptions(DisabledRules: stage)).Convert(source, + reference: ReferenceHint.FromText(english), trace: rules); + Assert.DoesNotContain(rules, r => r.Id == stage); + } + } + + [Theory] + [InlineData("Sim, ajuda a encontrar seu carro se foi roubado.", "Yeah, it helps you find your car if it was stolen.", "foi-se")] + [InlineData("Como está Jack?", "How is Jack?", "estás")] + [InlineData("Daremos a volta completa depois.", "We'll give you the full tour after.", "volta-a")] + [InlineData("Ela mudou de turno por minha causa.", "She changed her shift because of me.", "pela minha causa")] + public void AdditionalEpisodeScreeningDoesNotIntroduceGrammarErrors(string source, string english, string absent) + { + var rules = new List(); + var result = new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english), trace: rules); + Assert.True(!result.Contains(absent), result + "\n" + string.Join(", ", rules.Select(r => r.Id))); + } + + private static SrtBlock Cue(int start, int end, string text) => new() { StartMs = start, EndMs = end, Text = text }; + + private static SrtBlock[] FamilyScene() => + [ + Cue(722857, 726142, "Sério, Jack?"), + Cue(726181, 728343, "Nossa foto de casamento?"), + Cue(741375, 744730, "Devo lembrar que Nicky\nestá se formando este ano"), + Cue(744787, 747012, "e precisa estar lá."), + Cue(747014, 749416, "E quando ela se casar,"), + Cue(749416, 751542, "é seu trabalho levá-la\npara o altar."), + Cue(751544, 753406, "Entendeu?"), + Cue(754354, 757311, "Precisa sair desta, Jack."), + Cue(757313, 759671, "Deve isso a ela."), + Cue(759700, 762100, "Ouviu?"), + Cue(762150, 764200, "Sra. Reagan?"), + Cue(764250, 767200, "Seu marido precisa de cirurgia."), + ]; + + private static string?[] FamilyEnglish() => + [ + "Really, Jack?", "Our wedding photo?", + "I need to remind you that Nicky is graduating this year", "and you need to be there.", + "And when she gets married,", "that's your job to walk her down the aisle.", + "You got that?", "You've got to pull through, Jack.", "You owe that to her.", "You hear me?", + "Ms. Reagan?", "Your husband needs surgery.", + ]; +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ShakingVocabularyTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ShakingVocabularyTests.cs new file mode 100644 index 00000000..758a6083 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ShakingVocabularyTests.cs @@ -0,0 +1,191 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class ShakingVocabularyTests +{ + [Theory] + [InlineData("Chacoalhar a garrafa antes de beber.", null, "Agitar a garrafa antes de beber.")] + [InlineData("Ele chacoalhou o casaco para tirar a poeira.", null, "Ele sacudiu o casaco para tirar a poeira.")] + [InlineData("Chacoalhe bem o remédio.", null, "Agita bem o medicamento.")] + [InlineData("Chacoalhou o suco.", null, "Agitou o sumo.")] + [InlineData("Chacoalha a cabeça.", null, "Abana a cabeça.")] + [InlineData("Chacoalha os braços.", null, "Sacode os braços.")] + [InlineData("Chacoalhas as mãos.", null, "Sacodes as mãos.")] + [InlineData("Chacoalham o tapete.", null, "Sacodem o tapete.")] + [InlineData("Chacoalhe o lençol.", null, "Sacuda o lençol.")] + [InlineData("Chacoalhando o casaco.", null, "A sacudir o casaco.")] + [InlineData("O casaco foi chacoalhado.", null, "O casaco foi sacudido.")] + [InlineData("As garrafas foram chacoalhadas.", null, "As garrafas foram agitadas.")] + [InlineData("Não me chacoalhe!", "Don't shake me!", "Não me abanes!")] + [InlineData("Chacoalhou-me.", "He shook me.", "Abanou-me.")] + [InlineData("Vou chacoalhá-lo.", "I'm going to shake him.", "Vou abaná-lo.")] + [InlineData("Chacoalhá-lo-ei.", "I will shake him.", "Abaná-lo-ei.")] + [InlineData("Chacoalhar-me-ia.", "He would shake me.", "Abanar-me-ia.")] + [InlineData("Não chacoalhe o frasco.", null, "Não agites o frasco.")] + [InlineData("CHACOALHOU O CASACO.", null, "SACUDIU O CASACO.")] + [InlineData("Chacoalhe a garrafa.", null, "Agita a garrafa.")] + [InlineData("Chacoalhou\no casaco.", null, "Sacudiu\no casaco.")] + [InlineData("Levei um chacoalhão.", null, "Levei um abanão.")] + [InlineData("Dois chacoalhões.", null, "Dois abanões.")] + [InlineData("Deu uma chacoalhada.", null, "Deu uma sacudidela.")] + [InlineData("Duas chacoalhadas.", null, "Duas sacudidelas.")] + [InlineData("CHACOALHÃO!", null, "ABANÃO!")] + [InlineData("Ele chacoalhou o funcionário.", "He insulted the employee.", "Ele insultou o funcionário.")] + [InlineData("Ele chacoalhou o funcionário.", "He scolded the employee.", "Ele repreendeu o funcionário.")] + [InlineData("Pare de chacoalhar.", "Stop pestering.", "Para de incomodar.")] + [InlineData("Ficou chacoalhado.", "He was shaken by the news.", "Ficou abalado.")] + [InlineData("Foi chacoalhado.", "He was shaken by his kidnapper.", "Foi abanado.")] + [InlineData("Chacoalhamos a garrafa.", "We shook the bottle.", "Agitámos a garrafa.")] + [InlineData("Chacoalhamos a garrafa.", "We shake the bottle.", "Agitamos a garrafa.")] + [InlineData("Chacoalhamos a garrafa.", null, "Agitamos a garrafa.")] + [InlineData("Mas chacoalhada\nalém do normal,", "But shaken up more than usual.", "Mas abalada\nalém do normal,")] + [InlineData("se eu estiver chacoalhando\nrolamentos de esferas", "if I'm shaking ball bearings", "se eu estiver a chocalhar\nrolamentos de esferas")] + [InlineData("Chacoalhou as chaves.", null, "Chocalhou as chaves.")] + [InlineData("Chacoalha as moedas.", null, "Chocalha as moedas.")] + [InlineData("Não chacoalhem os guizos.", null, "Não chocalhem os guizos.")] + [InlineData("Os chocalhos foram chacoalhados.", null, "Os chocalhos foram chocalhados.")] + [InlineData("Chacoalhar as chaves.", null, "Chocalhar as chaves.")] + [InlineData("CHACOALHOU OS ROLAMENTOS.", null, "CHOCALHOU OS ROLAMENTOS.")] + public void PhysicalAndFigurativeContexts(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var reference = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: reference)); + Assert.Equal(expected, converter.Convert(expected, reference: reference)); + } + + [Theory] + [InlineData("O chacoalho está aqui.")] + [InlineData("microchacoalhar")] + [InlineData("chacoalhar_uma_coisa")] + [InlineData("Chocalhar as chaves.")] + [InlineData("Chocalhou as moedas.")] + [InlineData("Vou chocalhar a garrafa.")] + public void NounsAmbiguousTensesAndOtherWordsAreProtected(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Theory] + [InlineData("chacoalhar", "sacudir")] + [InlineData("chacoalho", "sacudo")] + [InlineData("chacoalhas", "sacodes")] + [InlineData("chacoalha", "sacode")] + [InlineData("chacoalhamos", "sacudimos")] + [InlineData("chacoalham", "sacodem")] + [InlineData("chacoalhei", "sacudi")] + [InlineData("chacoalhaste", "sacudiste")] + [InlineData("chacoalhou", "sacudiu")] + [InlineData("chacoalharam", "sacudiram")] + [InlineData("chacoalhava", "sacudia")] + [InlineData("chacoalhavas", "sacudias")] + [InlineData("chacoalhávamos", "sacudíamos")] + [InlineData("chacoalhavam", "sacudiam")] + [InlineData("chacoalharei", "sacudirei")] + [InlineData("chacoalharás", "sacudirás")] + [InlineData("chacoalhará", "sacudirá")] + [InlineData("chacoalharemos", "sacudiremos")] + [InlineData("chacoalharão", "sacudirão")] + [InlineData("chacoalharia", "sacudiria")] + [InlineData("chacoalharias", "sacudirias")] + [InlineData("chacoalharíamos", "sacudiríamos")] + [InlineData("chacoalhariam", "sacudiriam")] + [InlineData("chacoalhe", "sacuda")] + [InlineData("chacoalhes", "sacudas")] + [InlineData("chacoalhemos", "sacudamos")] + [InlineData("chacoalhem", "sacudam")] + [InlineData("chacoalhasse", "sacudisse")] + [InlineData("chacoalhasses", "sacudisses")] + [InlineData("chacoalhássemos", "sacudíssemos")] + [InlineData("chacoalhassem", "sacudissem")] + [InlineData("chacoalhares", "sacudires")] + [InlineData("chacoalharmos", "sacudirmos")] + [InlineData("chacoalharem", "sacudirem")] + [InlineData("chacoalhando", "sacudindo")] + [InlineData("chacoalhado", "sacudido")] + [InlineData("chacoalhada", "sacudida")] + [InlineData("chacoalhados", "sacudidos")] + [InlineData("chacoalhadas", "sacudidas")] + [InlineData("chacoalhara", "sacudira")] + [InlineData("chacoalharas", "sacudiras")] + [InlineData("chacoalháramos", "sacudíramos")] + [InlineData("chacoalhámos", "sacudimos")] + [InlineData("chacoalhais", "sacudis")] + [InlineData("chacoalhastes", "sacudistes")] + [InlineData("chacoalháveis", "sacudíeis")] + [InlineData("chacoalhareis", "sacudireis")] + [InlineData("chacoalharíeis", "sacudiríeis")] + [InlineData("chacoalheis", "sacudais")] + [InlineData("chacoalhásseis", "sacudísseis")] + [InlineData("chacoalhardes", "sacudirdes")] + [InlineData("chacoalháreis", "sacudíreis")] + [InlineData("chacoalhai", "sacudi")] + public void ReviewedSacudirParadigm(string source, string expected) => + Assert.Equal(expected + " o casaco.", + new PortugueseConverter(new ConversionOptions(Progressive: false, YouToTu: false)).Convert(source + " o casaco.")); + + [Theory] + [InlineData("Ele pentelhou a professora.", "Ele chateou a professora.")] + [InlineData("Não pentelhe.", "Não chateie.")] + [InlineData("Pentelhar.", "Chatear.")] + [InlineData("Pentelhando.", "A chatear.")] + [InlineData("Vou pentelhá-lo.", "Vou chateá-lo.")] + [InlineData("Não xerete nas minhas coisas.", "Não bisbilhotes nas minhas coisas.")] + [InlineData("Xeretou os meus segredos.", "Bisbilhotou os meus segredos.")] + [InlineData("Xeretando na vida.", "A bisbilhotar na vida.")] + [InlineData("Xeretando meu caso?", "A bisbilhotar o meu caso?")] + [InlineData("Futucou nas gavetas.", "Remexeu nas gavetas.")] + [InlineData("Não futuque na minha bolsa.", "Não remexas na minha bolsa.")] + [InlineData("Futucava os bolsos.", "Remexia os bolsos.")] + public void OtherReviewedMissingVerbs(string source, string expected) + { + var converter = new PortugueseConverter(); + Assert.Equal(expected, converter.Convert(source)); + Assert.Equal(expected, converter.Convert(expected)); + } + + [Theory] + [InlineData("Xeretou o chefe.")] + [InlineData("Futucou a memória.")] + [InlineData("Futucou a camisa.")] + [InlineData("Futucou o braço.")] + [InlineData("Futucou o chefe.")] + public void PolysemousVerbsRequireReviewedComplements(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Fact] + public void SettingsExceptionsAndTraceAreRespected() + { + var source = "Chacoalhou o casaco."; + foreach (var options in new[] + { + new ConversionOptions(Vocabulary: false), + new ConversionOptions(DisabledRules: "shaking-vocabulary"), + }) + Assert.Equal(source, new PortugueseConverter(options).Convert(source)); + foreach (var word in new[] { "chacoalhar", "chacoalhou" }) + { + var ignored = new PortugueseLocalWordList(); + ignored.Ignored.Add(word); + Assert.Equal(source, new PortugueseConverter(localWordList: ignored).Convert(source)); + } + var custom = new PortugueseLocalWordList(); + custom.Words.Add(("chacoalhar", "mexer")); + Assert.Equal(source, new PortugueseConverter(localWordList: custom).Convert(source)); + custom.Words.Add(("chacoalhou", "mexeu")); + Assert.Equal("Mexeu o casaco.", new PortugueseConverter(localWordList: custom).Convert(source)); + var trace = new List(); + new PortugueseConverter().Convert(source, trace: trace); + Assert.Contains(trace, r => r.Id == "shaking-vocabulary"); + Assert.Equal("Ele pentelhou a professora.", new PortugueseConverter(new ConversionOptions(Vocabulary: false)) + .Convert("Ele pentelhou a professora.")); + Assert.Equal("Não xeretes nas minhas coisas.", new PortugueseConverter(new ConversionOptions(DisabledRules: "verb-vocabulary")) + .Convert("Não xerete nas minhas coisas.")); + foreach (var verb in new[] { "pentelhar", "xeretar", "futucar" }) + { + var ignored = new PortugueseLocalWordList(); + ignored.Ignored.Add(verb); + Assert.Equal($"Vamos {verb} nas coisas.", + new PortugueseConverter(localWordList: ignored).Convert($"Vamos {verb} nas coisas.")); + } + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/SpeechContentTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/SpeechContentTests.cs new file mode 100644 index 00000000..62d171e8 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/SpeechContentTests.cs @@ -0,0 +1,139 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class SpeechContentTests +{ + [Theory] + [InlineData("Eu falei algo.", null, "Eu disse alguma coisa.")] + [InlineData("Tu falaste algo?", null, "Tu disseste alguma coisa?")] + [InlineData("Ele falou alguma coisa?", null, "Ele disse alguma coisa?")] + [InlineData("Eles falaram qualquer coisa.", null, "Eles disseram qualquer coisa.")] + [InlineData("Não falei nada.", null, "Não disse nada.")] + [InlineData("Eu falo a verdade.", null, "Eu digo a verdade.")] + [InlineData("Tu falas mentiras.", null, "Tu dizes mentiras.")] + [InlineData("Ela fala uma mentira.", null, "Ela diz uma mentira.")] + [InlineData("Eles falam muitas coisas.", null, "Eles dizem muitas coisas.")] + [InlineData("Eu falava isto.", null, "Eu dizia isto.")] + [InlineData("Tu falavas aquilo.", null, "Tu dizias aquilo.")] + [InlineData("Nós falávamos a verdade.", null, "Nós dizíamos a verdade.")] + [InlineData("Eu falarei tudo.", null, "Eu direi tudo.")] + [InlineData("Ele falará a verdade.", null, "Ele dirá a verdade.")] + [InlineData("Ela falaria isso.", null, "Ela diria isso.")] + [InlineData("Nós falaremos algumas palavras.", null, "Nós diremos algumas palavras.")] + [InlineData("Se falassem algo.", null, "Se dissessem alguma coisa.")] + [InlineData("Se falar algo, avisa.", null, "Se disser alguma coisa, avisa.")] + [InlineData("Quando falarem algo.", null, "Quando disserem alguma coisa.")] + [InlineData("É melhor falar algo.", null, "É melhor dizer alguma coisa.")] + [InlineData("Não quero falar nada.", null, "Não quero dizer nada.")] + [InlineData("Ela está falando algo.", null, "Ela está a dizer alguma coisa.")] + [InlineData("Ele tinha falado algo.", null, "Ele tinha dito alguma coisa.")] + [InlineData("A verdade foi falada.", null, "A verdade foi falada.")] + [InlineData("Nós falamos algo.", "We say something.", "Nós dizemos alguma coisa.")] + [InlineData("Nós falamos algo.", "We said something.", "Nós dissemos alguma coisa.")] + [InlineData("Nós falámos algo.", null, "Nós dissemos alguma coisa.")] + [InlineData("Fala-me alguma coisa.", null, "Diz-me alguma coisa.")] + [InlineData("Falar-lhe algo.", null, "Dizer-lhe alguma coisa.")] + [InlineData("Ele falou-me algo.", null, "Ele disse-me alguma coisa.")] + [InlineData("-Falei algo.", null, "-Disse alguma coisa.")] + [InlineData("ELE FALOU ALGO?", null, "ELE DISSE ALGUMA COISA?")] + [InlineData("Ele falou algo?", null, "Ele disse alguma coisa?")] + [InlineData("Ele falou\nalgo.", null, "Ele disse\nalguma coisa.")] + [InlineData("O que você falou?", "What did you say?", "O que disseste?")] + [InlineData("O que ele falava?", "What was he saying?", "O que ele dizia?")] + [InlineData("Ele falava que era inocente.", "He said he was innocent.", "Ele dizia que era inocente.")] + [InlineData("Se falar que eu trabalho aqui, morre.", "If he says I work here, he dies.", + "Se disser que eu trabalho aqui, morre.")] + [InlineData("Ela falou: \"Não\".", "She said no.", "Ela disse: \"Não\".")] + [InlineData("O que está falando?", "What are you saying?", "O que estás a dizer?")] + [InlineData("É o que está falando?", "Is that what you're saying?", "É o que estás a dizer?")] + [InlineData("O que me falou?", "What did he tell me?", "O que me disse?")] + [InlineData("Ele falou para mim algo.", null, "Ele disse-me alguma coisa.")] + [InlineData("Ela falava para ele a verdade.", null, "Ela dizia-lhe a verdade.")] + [InlineData("Depois que falei para ela.", "After I told her.", "Depois que lhe disse.")] + [InlineData("Eu falarei-lhe tudo.", null, "Eu dir-lhe-ei tudo.")] + [InlineData("Se o agente falar algo.", null, "Se o agente disser alguma coisa.")] + [InlineData("Quando a Maria falar algo.", null, "Quando a Maria disser alguma coisa.")] + [InlineData("Se alguém falar algo.", null, "Se alguém disser alguma coisa.")] + [InlineData("Se quiser falar algo.", null, "Se quiser dizer alguma coisa.")] + [InlineData("Se me falar algo.", null, "Se me disser alguma coisa.")] + [InlineData("Se eles falarem para mim algo.", null, "Se eles me disserem alguma coisa.")] + [InlineData("Não há como falar\nisso para ela", "But one that there's no way you can bring up to her", + "Não há como\nlhe dizer isso")] + [InlineData("- Johnny, o que falou para ele?\n- Nada.", "Johnny, what did you tell him? Nothing.", + "- Johnny, o que lhe disseste?\n- Nada.")] + [InlineData("Ele tinha falado alguma coisa.", null, "Ele tinha dito alguma coisa.")] + [InlineData("Vós falais a verdade.", null, "Vós dizeis a verdade.")] + [InlineData("Vós falastes a verdade.", null, "Vós dissestes a verdade.")] + [InlineData("Vós falaríeis a verdade.", null, "Vós diríeis a verdade.")] + [InlineData("Se vós falardes a verdade.", null, "Se vós disserdes a verdade.")] + public void ContentUsesDizerWithMatchingTenseAndPerson(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } + + [Theory] + [InlineData("Ele fala português.", "He speaks Portuguese.")] + [InlineData("Ela falou com ele.", "She talked to him.")] + [InlineData("Ele falou sobre isso.", "He spoke about that.")] + [InlineData("Ela falou de alguma coisa.", "She talked about something.")] + [InlineData("Ele fala muito alto.", "He speaks very loudly.")] + [InlineData("Ele não consegue falar.", "He cannot speak.")] + [InlineData("Ele falou durante horas.", "He talked for hours.")] + [InlineData("Nós falamos algo.", null)] + [InlineData("O que ele falou?", null)] + [InlineData("Sobre o que ele falou?", "What did he talk about?")] + [InlineData("Ele falava que era inocente.", null)] + [InlineData("Ele falava que era inocente.", "He spoke as if he was innocent.")] + [InlineData("Ele falou com ela. Ela falava que era inocente.", "He spoke to her. She said she was innocent.")] + [InlineData("falei_algo", "I said something.")] + [InlineData("falou2 algo", "He said something.")] + [InlineData("refalou algo", "He said something.")] + [InlineData("bem-falado algo", "He said something.")] + [InlineData("A fala \"não\".", "He said no.")] + [InlineData("Sem falar que custa muito.", "Not to mention that it costs a lot, he said.")] + [InlineData("O que ele falou?", "Tell me what he was talking about.")] + [InlineData("Se o agente mais velho falar alguma coisa.", null)] + public void OtherSensesAndUnresolvedReferencesAreProtected(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Theory] + [InlineData(AddressMode.Formal, "- Disse alguma coisa?\n- Nada.")] + [InlineData(AddressMode.Informal, "- Disseste alguma coisa?\n- Nada.")] + public void EpisodeCueRespectsRegisterAndPreservesOriginal(AddressMode address, string expected) + { + const string source = "- Você falou algo?\n- Nada."; + var blocks = new[] { new SrtBlock { StartMs = 1262422, EndMs = 1265703, Text = source } }; + var result = Assert.Single(SubtitleConversion.Convert(blocks, new ConversionOptions(Address: address), + english: ["Did you say anything? Not a peep."])); + Assert.Equal(expected, result.Text); + Assert.Equal(source, blocks[0].Text); + Assert.Equal(1262422, blocks[0].StartMs); + Assert.Equal(1265703, blocks[0].EndMs); + } + + [Fact] + public void OptionsAndLocalVocabularyAreRespected() + { + foreach (var options in new[] { new ConversionOptions(Vocabulary: false), + new ConversionOptions(DisabledRules: "reviewed-verb-senses") }) + Assert.Equal("Ele falou algo.", new PortugueseConverter(options).Convert("Ele falou algo.")); + Assert.Equal("Ele disse alguma coisa.", new PortugueseConverter(new ConversionOptions(Colloquial: false)) + .Convert("Ele falou algo.")); + foreach (var word in new[] { "falar", "falou", "algo" }) + { + var local = new PortugueseLocalWordList(); + local.Words.Add((word, word)); + Assert.Equal("Ele falou algo.", new PortugueseConverter(localWordList: local).Convert("Ele falou algo.")); + } + var ignored = new PortugueseLocalWordList(); + ignored.Ignored.Add("falou algo"); + Assert.Equal("Ele falou algo.", new PortugueseConverter(localWordList: ignored).Convert("Ele falou algo.")); + Assert.Equal("O que estás a falar?", new PortugueseConverter().Convert("O que está falando?", + reference: ReferenceHint.FromText("What are you talking about?"))); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/SpokenIdiomTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/SpokenIdiomTests.cs new file mode 100644 index 00000000..cb53772e --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/SpokenIdiomTests.cs @@ -0,0 +1,138 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class SpokenIdiomTests +{ + [Theory] + [InlineData("ele sempre fala bobagens\ntoda vez que fica doidão.", + "'cause he always talking crazy every time he gets high.", + "ele diz sempre tolices\nquando fica pedrado.")] + [InlineData("Ele fala bobagens.", null, "Ele diz tolices.")] + [InlineData("Ela fala tolices.", null, "Ela diz tolices.")] + [InlineData("Eles falam disparates.", null, "Eles dizem disparates.")] + [InlineData("Eu falo bobagens.", null, "Eu digo tolices.")] + [InlineData("Tu falas bobagens.", null, "Tu dizes tolices.")] + [InlineData("Eu falei bobagens.", null, "Eu disse tolices.")] + [InlineData("Tu falaste bobagens.", null, "Tu disseste tolices.")] + [InlineData("Ela falou bobagens.", null, "Ela disse tolices.")] + [InlineData("Eles falaram bobagens.", null, "Eles disseram tolices.")] + [InlineData("Eu falava bobagens.", null, "Eu dizia tolices.")] + [InlineData("Nós falávamos bobagens.", null, "Nós dizíamos tolices.")] + [InlineData("Eu falarei bobagens.", null, "Eu direi tolices.")] + [InlineData("Ele falaria bobagens.", null, "Ele diria tolices.")] + [InlineData("Não fale bobagens.", null, "Não digas tolices.")] + [InlineData("Se falassem bobagens.", null, "Se dissessem tolices.")] + [InlineData("Se eles falarem bobagens.", null, "Se eles disserem tolices.")] + [InlineData("Se eles não falarem bobagens.", null, "Se eles não disserem tolices.")] + [InlineData("Se não falar bobagens.", null, "Se não disser tolices.")] + [InlineData("Quando falar bobagens.", null, "Quando disser tolices.")] + [InlineData("É melhor falar bobagens.", null, "É melhor dizer tolices.")] + [InlineData("Não quero falar bobagens.", null, "Não quero dizer tolices.")] + [InlineData("Nós falamos bobagens.", "We talk nonsense.", "Nós dizemos tolices.")] + [InlineData("Nós falamos bobagens.", "We talked nonsense.", "Nós dissemos tolices.")] + [InlineData("Ele fala muitas bobagens.", null, "Ele diz muitas tolices.")] + [InlineData("Ele fala muitos disparates.", null, "Ele diz muitos disparates.")] + [InlineData("Ele fala besteiras.", null, "Ele diz asneiras.")] + [InlineData("Ele só fala bobagens.", null, "Ele só diz tolices.")] + [InlineData("ELE SEMPRE FALA BOBAGENS.", null, "ELE DIZ SEMPRE TOLICES.")] + [InlineData("Ele sempre fala bobagens.", null, "Ele diz sempre tolices.")] + public void NonsenseUsesDizerWithMatchingPersonAndTense(string source, string? english, string expected) => + AssertConversion(source, english, expected); + + [Theory] + [InlineData("Toda vez que chove, fico em casa.", "Sempre que chove, fico em casa.")] + [InlineData("Toda a vez que chove, fico em casa.", "Sempre que chove, fico em casa.")] + [InlineData("Ele sorri toda vez que chove.", "Ele sorri sempre que chove.")] + [InlineData("Ele sempre sorri toda vez que chove.", "Ele sempre sorri quando chove.")] + [InlineData("Ele não sorri sempre toda vez que chove.", "Ele não sorri sempre de cada vez que chove.")] + [InlineData("Ele sempre diz que toda vez que chove, fica em casa.", "Ele diz sempre que sempre que chove, fica em casa.")] + [InlineData("TODA VEZ QUE CHOVE.", "SEMPRE QUE CHOVE.")] + [InlineData("Todas as vezes que chove.", "Todas as vezes que chove.")] + [InlineData("De cada vez que chove.", "De cada vez que chove.")] + public void HabitualClausesKeepUniversalMeaning(string source, string expected) => + AssertConversion(source, null, expected); + + [Theory] + [InlineData("Ele fica doidão.", "He gets high.", "Ele fica pedrado.")] + [InlineData("Ela ficou doidona.", "She got high.", "Ela ficou pedrada.")] + [InlineData("Eles ficaram doidões.", "They got stoned.", "Eles ficaram pedrados.")] + [InlineData("Elas estão doidonas.", "They are high.", "Elas estão pedradas.")] + [InlineData("Eu estou muito doidão.", "I'm really high.", "Eu estou muito pedrado.")] + [InlineData("Ele estava doidão.", "He was stoned.", "Ele estava pedrado.")] + [InlineData("Ele vai ficar doidão.", "He will get high.", "Ele vai ficar pedrado.")] + [InlineData("Ele ficou doidão de cocaína.", null, "Ele ficou pedrado de cocaína.")] + [InlineData("Ele ficou doidão de muita cocaína.", null, "Ele ficou pedrado de muita cocaína.")] + [InlineData("Ele ficou doidão de erva.", null, "Ele ficou pedrado de erva.")] + [InlineData("ELE FICA DOIDÃO.", "He gets high.", "ELE FICA PEDRADO.")] + [InlineData("Ela ficou doidona.", "She got high.", "Ela ficou pedrada.")] + public void DrugEvidenceSelectsGenderedIntoxicationSlang(string source, string? english, string expected) => + AssertConversion(source, english, expected); + + [Theory] + [InlineData("Ele fica doidão.", null, "Ele fica doidão.")] + [InlineData("Ele fica doidão.", "He goes crazy.", "Ele fica doidão.")] + [InlineData("Ele fica doidão.", "He gets high scores.", "Ele fica doidão.")] + [InlineData("Ele fica doidão.", "He gets high up in the mountains.", "Ele fica doidão.")] + [InlineData("Ele fica doidão.", "He is high on life.", "Ele fica doidão.")] + [InlineData("Ele fica doidão de alegria.", "He gets high on life.", "Ele fica doidão de alegria.")] + [InlineData("Ele combate as drogas e fica doidão.", "He fights drugs and goes crazy.", "Ele combate as drogas e fica doidão.")] + [InlineData("Ele fica doidão por ela.", "He gets high. He's crazy about her.", "Ele fica doidão por ela.")] + [InlineData("Ele fala português.", "He speaks Portuguese.", "Ele fala português.")] + [InlineData("Ele fala de bobagens.", null, "Ele fala de tolices.")] + [InlineData("Nós falamos bobagens.", null, "Nós falamos tolices.")] + [InlineData("Toda a vez foi emocionante.", null, "Toda a vez foi emocionante.")] + public void UnrelatedAndAmbiguousSensesAreProtected(string source, string? english, string expected) => + Assert.Equal(expected, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void OptionsAndLocalChoicesWin() + { + var high = ReferenceHint.FromText("He gets high."); + foreach (var options in new[] + { + new ConversionOptions(Colloquial: false), + new ConversionOptions(DisabledRules: "spoken-idioms"), + }) + { + Assert.Equal("Ele fica doidão.", new PortugueseConverter(options).Convert("Ele fica doidão.", reference: high)); + Assert.Equal("Toda vez que chove.", new PortugueseConverter(options).Convert("Toda vez que chove.")); + } + var noVocabulary = new PortugueseConverter(new ConversionOptions(Vocabulary: false)); + Assert.Equal("Ele fica doidão.", noVocabulary.Convert("Ele fica doidão.", reference: high)); + Assert.Equal("Ele fala bobagens.", noVocabulary.Convert("Ele fala bobagens.")); + Assert.Equal("Sempre que chove.", noVocabulary.Convert("Toda vez que chove.")); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("fala bobagens"); + words.Ignored.Add("toda vez que"); + words.Words.Add(("doidão", "ganzado")); + var converter = new PortugueseConverter(localWordList: words); + Assert.Equal("Ele fala bobagens.", converter.Convert("Ele fala bobagens.")); + Assert.Equal("toda vez que chove.", converter.Convert("toda vez que chove.")); + Assert.Equal("Ele fica ganzado.", converter.Convert("Ele fica doidão.", reference: high)); + var speechWords = new PortugueseLocalWordList(); + speechWords.Words.Add(("falar", "conversar")); + Assert.Equal("Ele fala tolices.", new PortugueseConverter(localWordList: speechWords).Convert("Ele fala bobagens.")); + } + + [Fact] + public void ExactCuePreservesTimingAndLineCount() + { + var cue = new SrtBlock { StartMs = 1667747, EndMs = 1670344, + Text = "ele sempre fala bobagens\ntoda vez que fica doidão." }; + var result = Assert.Single(SubtitleConversion.Convert(new[] { cue }, ConversionOptions.Default, + english: new string?[] { "'cause he always talking crazy every time he gets high." })); + Assert.Equal("ele diz sempre tolices\nquando fica pedrado.", result.Text); + Assert.Equal(1667747, cue.StartMs); + Assert.Equal(1670344, cue.EndMs); + } + + private static void AssertConversion(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var hint = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: hint)); + Assert.Equal(expected, converter.Convert(expected, reference: hint)); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/VehiclePlateTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/VehiclePlateTests.cs new file mode 100644 index 00000000..87258715 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/VehiclePlateTests.cs @@ -0,0 +1,113 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class VehiclePlateTests +{ + [Theory] + [InlineData("Anotei a placa do carro.", null, "Anotei a matrícula do carro.")] + [InlineData("Anotei as placas dos carros.", null, "Anotei as matrículas dos carros.")] + [InlineData("A placa da carrinha.", null, "A matrícula da carrinha.")] + [InlineData("As placas de um caminhão.", null, "As matrículas de um camião.")] + [InlineData("A placa da moto.", null, "A matrícula da moto.")] + [InlineData("O veículo estava sem placa.", null, "O veículo estava sem matrícula.")] + [InlineData("O carro tinha placas falsas.", null, "O carro tinha matrículas falsas.")] + [InlineData("Anotei o número da placa.", null, "Anotei o número da matrícula.")] + [InlineData("Anotei os números das placas.", null, "Anotei os números das matrículas.")] + [InlineData("com placas de fora.", null, "com matrículas de fora.")] + [InlineData("Ele usa placas clonadas.", null, "Ele usa matrículas clonadas.")] + [InlineData("com placas de Jersey.", "with Jersey plates.", "com matrículas de Jersey.")] + [InlineData("com placas de Lisboa.", "with Lisbon license plates.", "com matrículas de Lisboa.")] + [InlineData("Duas das placas.", "Two of the plates we could ID, both stolen.", "Duas das matrículas.")] + [InlineData("- Sobre a placa", "Plates came back registered to him.", "- Sobre a matrícula")] + [InlineData("Verifique essa placa?", "Call in these plates?", "Verifique esta matrícula?")] + [InlineData("A placa não foi confirmada.", "Registration not confirmed.", "A matrícula não foi confirmada.")] + [InlineData("Anotou a placa?", "Did he get the license plate number?", "Anotou a matrícula?")] + [InlineData("Anotei a placa.", "I wrote down the licence plate.", "Anotei a matrícula.")] + [InlineData("A placa.", "The number plate.", "A matrícula.")] + [InlineData("A placa.", "The registration plate.", "A matrícula.")] + [InlineData("A placa.", "Run the plate.", "A matrícula.")] + [InlineData("As placas.", "The car has plates.", "As matrículas.")] + [InlineData("ANOTEI AS PLACAS.", "I wrote down the license plates.", "ANOTEI AS MATRÍCULAS.")] + [InlineData("com placas de Nova Jersey", "with New Jersey registration", "com matrículas de Nova Jersey")] + [InlineData("Anotei a placa\ndo carro.", null, "Anotei a matrícula\ndo carro.")] + public void VehicleRegistrationUsesMatricula(string source, string? english, string expected) + { + var converter = new PortugueseConverter(); + var reference = ReferenceHint.FromText(english); + Assert.Equal(expected, converter.Convert(source, reference: reference)); + Assert.Equal(expected, converter.Convert(expected, reference: reference)); + } + + [Theory] + [InlineData("A placa de rua.", "The street sign.")] + [InlineData("A placa na parede.", null)] + [InlineData("As placas tectónicas.", null)] + [InlineData("A placa bacteriana.", null)] + [InlineData("A placa dentária.", null)] + [InlineData("A placa de rede.", null)] + [InlineData("A placa de som.", null)] + [InlineData("A placa gráfica.", null)] + [InlineData("A placa de indução.", null)] + [InlineData("A placa de Petri.", null)] + [InlineData("A placa de metal.", null)] + [InlineData("O número da placa de rede.", null)] + [InlineData("O edifício está sem placa.", null)] + [InlineData("Está sem placa.", null)] + [InlineData("A placa.", "The dinner plate.")] + [InlineData("As placas.", null)] + [InlineData("A placa comemorativa.", "The car has license plates. This is a memorial plaque.")] + [InlineData("O carro atingiu a placa de sinalização.", null)] + public void NonVehicleAndUnresolvedUsesRemainUnchanged(string source, string? english) => + Assert.Equal(source, new PortugueseConverter().Convert(source, reference: ReferenceHint.FromText(english))); + + [Fact] + public void DistinctPlateSensesInOneCueRemainSeparate() + { + Assert.Equal("Anotei a matrícula do carro e vi a placa de rua.", new PortugueseConverter() + .Convert("Anotei a placa do carro e vi a placa de rua.", + reference: ReferenceHint.FromText("I noted the license plate and saw the street sign."))); + Assert.Equal("Uma chapa de matrícula.", new PortugueseConverter() + .Convert("Uma placa de matrícula.")); + Assert.Equal("Uma chapa de matrícula.", new PortugueseConverter() + .Convert("Uma placa de matrícula.", reference: ReferenceHint.FromText("A license plate."))); + } + + [Fact] + public void VocabularyStageAndLocalChoicesWin() + { + const string source = "A placa do carro."; + foreach (var options in new[] + { + new ConversionOptions(Vocabulary: false), + new ConversionOptions(DisabledRules: "vehicle-plates"), + }) + Assert.Equal(source, new PortugueseConverter(options).Convert(source)); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("placa"); + Assert.Equal(source, new PortugueseConverter(localWordList: words).Convert(source)); + var custom = new PortugueseLocalWordList(); + custom.Words.Add(("placa do carro", "chapa do carro")); + Assert.Equal("A chapa do carro.", new PortugueseConverter(localWordList: custom).Convert(source)); + custom.Words.Clear(); + custom.Words.Add(("placa", "identificação")); + Assert.Equal("A identificação do carro.", new PortugueseConverter(localWordList: custom).Convert(source)); + } + + [Fact] + public void VehicleContextCanContinueAcrossCuesWithoutChangingTimings() + { + var blocks = new[] + { + new SrtBlock { StartMs = 1000, EndMs = 2000, Text = "Era um veículo parecido," }, + new SrtBlock { StartMs = 2010, EndMs = 3000, Text = "em marca e modelo,\ncom uma placa semelhante." }, + }; + var result = SubtitleConversion.Convert(blocks, ConversionOptions.Default); + Assert.Equal("em marca e modelo,\ncom uma matrícula semelhante.", result[1].Text); + Assert.Equal(2010, blocks[1].StartMs); + Assert.Equal(3000, blocks[1].EndMs); + Assert.Equal("a placa de rua.", new PortugueseConverter().Convert("a placa de rua.", + previousText: "Era um veículo parecido,")); + } +} diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/WaitingArrivalTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/WaitingArrivalTests.cs new file mode 100644 index 00000000..c60d4f7c --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese.Tests/WaitingArrivalTests.cs @@ -0,0 +1,138 @@ +using SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; +using SubtitleEdit.Plugins.Shared; + +namespace BrazilianToEuropeanPortuguese.Tests; + +public sealed class WaitingArrivalTests +{ + [Theory] + [InlineData("temos de esperar o Jamie\nantes de comer.", "temos de esperar pelo Jamie\nantes de comer.")] + [InlineData("Esperamos a Ana.", "Esperamos pela Ana.")] + [InlineData("Esperava o João.", "Esperava pelo João.")] + [InlineData("Esperava João.", "Esperava pelo João.")] + [InlineData("Esperava Maria.", "Esperava pela Maria.")] + [InlineData("Vou esperar Jamie.", "Vou esperar por Jamie.")] + [InlineData("Vou esperar os meus amigos.", "Vou esperar pelos meus amigos.")] + [InlineData("Vou esperar as minhas amigas.", "Vou esperar pelas minhas amigas.")] + [InlineData("Temos de esperar o médico.", "Temos de esperar pelo médico.")] + [InlineData("Precisamos esperar a Maria.", "Precisamos de esperar pela Maria.")] + [InlineData("Não vou esperar o Jamie.", "Não vou esperar pelo Jamie.")] + [InlineData("TEMOS DE ESPERAR O JAMIE.", "TEMOS DE ESPERAR PELO JAMIE.")] + [InlineData("Temos de esperar o Jamie.", "Temos de esperar pelo Jamie.")] + public void ReviewedPersonalObjectsUsePor(string source, string expected) + { + var converter = new PortugueseConverter(); + Assert.Equal(expected, converter.Convert(source)); + Assert.Equal(expected, converter.Convert(expected)); + } + + [Theory] + [InlineData("o", "pelo")] + [InlineData("a", "pela")] + [InlineData("none", "por")] + public void BareNamesRespectTheSelectedArticle(string article, string preposition) => + Assert.Equal($"Vamos esperar {preposition} Jamie.", new PortugueseConverter( + new ConversionOptions(NameArticles: $"Jamie = {article}")).Convert("Vamos esperar Jamie.")); + + [Theory] + [InlineData("Espero que o Jamie chegue.")] + [InlineData("Vou esperar o resultado.")] + [InlineData("Temos de esperar pelo Jamie.")] + [InlineData("Temos de esperar o Jamie chegar.")] + [InlineData("Vou esperar por alguém.")] + public void OtherComplementsStayUnchanged(string source) => + Assert.Equal(source, new PortugueseConverter().Convert(source)); + + [Theory] + [InlineData("Jamie", "JAMIE", false)] + [InlineData("Maria", "MARIA", false)] + [InlineData("Jamie", "JAMIE", true)] + public void NamedArrivalApologizesToTheWaitingGroup(string name, string speaker, bool sentenceContext) + { + var blocks = Scene(name); + var result = SubtitleConversion.Convert(blocks, new ConversionOptions(SentenceContext: sentenceContext), + english: English(name, speaker)); + Assert.Equal($"temos de esperar {(name == "Maria" ? "pela" : "pelo")} {name}\nantes de comer.", result[0].Text); + Assert.Equal("- Não.\n- Ei! Desculpem!", result[2].Text); + Assert.Contains(result[2].Rules, r => r.Id == "arrival-group-apology"); + Assert.Contains("verify the recipients", result[2].ContextNote); + Assert.Equal(2265703, blocks[2].StartMs); + Assert.Equal(2267695, blocks[2].EndMs); + } + + [Theory] + [InlineData(null)] + [InlineData("No. Hey! Sorry!")] + [InlineData("No. JOHN: Hey! Sorry!")] + public void AlguemAloneDoesNotEstablishPluralRecipients(string? arrival) + { + var english = English("Jamie", "JAMIE"); + english[2] = arrival; + var result = SubtitleConversion.Convert(Scene("Jamie"), ConversionOptions.Default, english: english); + Assert.Equal("- Não.\n- Ei! Desculpa!", result[2].Text); + Assert.DoesNotContain(result[2].Rules, r => r.Id == "arrival-group-apology"); + } + + [Theory] + [InlineData("temos que esperar o Jamie\nantes de comer.", "we have to wait\ntill Jamie gets home to eat.", true)] + [InlineData("esperamos o Jamie.", "We are waiting for Jamie.", true)] + [InlineData("Não temos que esperar o Jamie.", "We don't have to wait for Jamie.", false)] + [InlineData("temos que esperar o Jamie.", "We don't have to wait for Jamie.", false)] + [InlineData("Tenho que esperar o Jamie.", "I have to wait for Jamie.", false)] + public void ActualMarkupModalVariantsAndNegation(string waiting, string reference, bool plural) + { + var blocks = Scene("Jamie"); + blocks[0].Text = waiting; + blocks[2].Text = "- Não.\n- Ei! Desculpe!"; + var english = English("Jamie", "JAMIE"); + english[0] = reference; + english[2] = "No.JAMIE: Hey! Sorry!"; + var result = SubtitleConversion.Convert(blocks, ConversionOptions.Default, english: english); + Assert.Equal($"- Não.\n- Ei! {(plural ? "Desculpem" : "Desculpa")}!", result[2].Text); + } + + [Fact] + public void SingularVocativeDistantSceneAndOptionsPreventInference() + { + var blocks = Scene("Jamie"); + var english = English("Jamie", "JAMIE"); + blocks[2].Text = "- Não.\n- Ei! Desculpa, pai!"; + Assert.Equal("- Não.\n- Ei! Desculpa, pai!", + SubtitleConversion.Convert(blocks, ConversionOptions.Default, english: english)[2].Text); + blocks = Scene("Jamie"); + blocks[2].StartMs += 20000; + blocks[2].EndMs += 20000; + Assert.DoesNotContain(SubtitleConversion.Convert(blocks, ConversionOptions.Default, english: english)[2].Rules, + r => r.Id == "arrival-group-apology"); + foreach (var options in new[] + { + new ConversionOptions(Colloquial: false), + new ConversionOptions(Pronouns: false), + new ConversionOptions(DisabledRules: "arrival-group-apology"), + }) + Assert.Equal("- Não.\n- Ei! Desculpa!", + SubtitleConversion.Convert(Scene("Jamie"), options, english: english)[2].Text); + var words = new PortugueseLocalWordList(); + words.Ignored.Add("Desculpa"); + words.Ignored.Add("esperar"); + var result = SubtitleConversion.Convert(Scene("Jamie"), ConversionOptions.Default, words, english); + Assert.Equal("- Não.\n- Ei! Desculpa!", result[2].Text); + Assert.Equal("temos de esperar o Jamie\nantes de comer.", result[0].Text); + Assert.Equal("Temos de esperar o Jamie.", new PortugueseConverter( + new ConversionOptions(DisabledRules: "waiting-for-people")).Convert("Temos de esperar o Jamie.")); + } + + private static SrtBlock[] Scene(string name) => + [ + new() { StartMs = 2260257, EndMs = 2262581, Text = $"temos de esperar {(name == "Maria" ? "a" : "o")} {name}\nantes de comer." }, + new() { StartMs = 2262583, EndMs = 2265571, Text = "- Não vai mudar nada.\n- Alguém sabe dele?" }, + new() { StartMs = 2265703, EndMs = 2267695, Text = "- Não.\n- Ei! Desculpa!" }, + ]; + + private static string?[] English(string name, string speaker) => + [ + $"we have to wait till {name} gets home to eat.", + "No, you're not. Anybody heard from him?", + $"No. {speaker}: Hey! Sorry!", + ]; +} diff --git a/se5/BrazilianToEuropeanPortuguese/BatchMode.cs b/se5/BrazilianToEuropeanPortuguese/BatchMode.cs index 00f196c7..7d199a3b 100644 --- a/se5/BrazilianToEuropeanPortuguese/BatchMode.cs +++ b/se5/BrazilianToEuropeanPortuguese/BatchMode.cs @@ -54,7 +54,8 @@ internal static PluginResponse BuildBatchResponse(PluginRequest request, string changesByLine[i] = converted; } checkLines.Add(new CheckLine(i, blocks[i].Text, changed ? converted : blocks[i].Text, - i == 0 ? null : changesByLine.TryGetValue(i - 1, out var previous) ? previous : blocks[i - 1].Text, alignment?.Texts[i])); + i == 0 ? null : changesByLine.TryGetValue(i - 1, out var previous) ? previous : blocks[i - 1].Text, + alignment?.Texts[i], conversions[i].MeaningSentence)); } referenceNote += Judge(blocks, checkLines, changesByLine, embedding); diff --git a/se5/BrazilianToEuropeanPortuguese/ClauseAgreementAnalysis.cs b/se5/BrazilianToEuropeanPortuguese/ClauseAgreementAnalysis.cs index bd854207..4387c357 100644 --- a/se5/BrazilianToEuropeanPortuguese/ClauseAgreementAnalysis.cs +++ b/se5/BrazilianToEuropeanPortuguese/ClauseAgreementAnalysis.cs @@ -25,7 +25,9 @@ internal sealed class ClauseAgreementAnalysis private static readonly HashSet ClauseLinks = new(StringComparer.OrdinalIgnoreCase) { "que", "se", "quando", "porque", "embora", "enquanto", "caso", "mas" }; private static readonly HashSet Determiners = new(StringComparer.OrdinalIgnoreCase) - { "o", "a", "os", "as", "um", "uma", "uns", "umas", "este", "esta", "aquele", "aquela" }; + { "o", "a", "os", "as", "um", "uma", "uns", "umas", "este", "esta", "estes", "estas", + "esse", "essa", "esses", "essas", "aquele", "aquela", "aqueles", "aquelas", + "todo", "toda", "todos", "todas", "cada", "qualquer", "algum", "alguma", "nenhum", "nenhuma" }; private static readonly HashSet Adverbs = new(StringComparer.OrdinalIgnoreCase) { "não", "nunca", "já", "ainda", "sempre", "só", "apenas", "também", "talvez", "bem", "então", "pois", "provavelmente", "certamente", "possivelmente", "realmente", "sim", "ora", "bom", "agora", "claro", @@ -109,12 +111,19 @@ private SubjectOwner Owner(int first, int last) { var word = _tokens[i].Text; if (word == "," || Adverbs.Contains(word) || AddresseeAdverb.IsMatch(word)) continue; + if (word.Equals("o", StringComparison.OrdinalIgnoreCase) && i + 2 < last && + _tokens[i + 1].Text.Equals("quê", StringComparison.OrdinalIgnoreCase) && _tokens[i + 2].Text == ",") + { + i += 2; + continue; + } if (word.Equals("tu", StringComparison.OrdinalIgnoreCase) || word.Equals("você", StringComparison.OrdinalIgnoreCase)) return SubjectOwner.Addressee; if (Regex.IsMatch(word, @"^(?:eu|ele|ela|eles|elas|nós|vocês|isto|isso|aquilo|alguém|ninguém)$", Ci)) return SubjectOwner.Other; if (Determiners.Contains(word) && i + 1 < last && - !PortugueseVerbs.IsKnownVerbForm(_tokens[i + 1].Text)) + (!PortugueseVerbs.IsKnownVerbForm(_tokens[i + 1].Text) || + Determiners.Contains(_tokens[i + 1].Text))) return SubjectOwner.Other; if (PortugueseVerbs.IsSecondPersonForm(word)) return SubjectOwner.Addressee; if (Candidates(word).HasFlag(VerbCandidate.ThirdPerson)) return SubjectOwner.Unknown; @@ -183,6 +192,11 @@ internal AgreementDecision Decide(int verbOffset, ReferenceHint? reference) if (clause.Parent >= 0 && reference is not { RepeatedYouSubject: true }) { var parent = _clauses[clause.Parent]; + if (!clause.Conditional && _tokens[clause.First - 1].Text.Equals("que", StringComparison.OrdinalIgnoreCase) && + parent.Last > parent.First && + Regex.IsMatch(_tokens[parent.Last - 1].Text, @"^(?:alguém|ninguém|aquele|aquela|aqueles|aquelas)$", Ci) && + EnglishRelativeSubject.IsMatch(reference?.EnglishText ?? "")) + return new(false, AgreementBlock.RelativeSubject, SubjectOwner.RelativeAntecedent, candidate); if (!clause.Conditional && _tokens[clause.First - 1].Text.Equals("que", StringComparison.OrdinalIgnoreCase) && parent.Last - parent.First >= 2 && Regex.IsMatch(_tokens[parent.Last - 2].Text, @"^(?:o|a|os|as|um|uma|uns|umas|do|da|dos|das|dum|duma)$", Ci) && diff --git a/se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs b/se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs index 4d447786..3c7c7c0d 100644 --- a/se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs +++ b/se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs @@ -60,9 +60,59 @@ public static string FerrarSlangVocabulary(string text, ReferenceHint? reference }); private static readonly Regex HabitualAlways = new( - @"(?(?:^|[.!?;:]\s+|\n|(?(?:^|[.!?;:]\s+|\n|[\uE000-\uF8FF]\s*|(?sempre)(?\s+)(?\p{L}+)" + After + @"(?!-)", Ci); + @"(?sempre)(?\s+)(?(?:me|te|se|nos|vos|lhe|lhes|o|a|os|as)\s+)?" + + @"(?\p{L}+)" + After + @"(?!-)", Ci); + + private static readonly Dictionary PolitePluralCommands = new[] + { + "avisar", "informar", "comunicar", "telefonar", "ligar", "chamar", "contactar", "contatar", + "notificar", "alertar", "aguardar", "esperar", "comparecer", "procurar", "verificar", "confirmar", + "enviar", "responder", "entrar", "sair", "parar", "regressar", "voltar", "continuar", + "seguir", "entregar", "ajudar", "apoiar", "proteger", "trazer", "fazer", "dizer", "dar", + "ir", "ser", "estar", "ter", "vir", "ver", "pedir", "manter", + }.ToDictionary(verb => verb, verb => verb switch + { + "seguir" => "sigam", "trazer" => "tragam", "fazer" => "façam", "dizer" => "digam", + "dar" => "deem", "ir" => "vão", "ser" => "sejam", "estar" => "estejam", "ter" => "tenham", + "vir" => "venham", "ver" => "vejam", "pedir" => "peçam", "manter" => "mantenham", "sair" => "saiam", + "contatar" => "contactem", _ => Forms(verb)[26], + }, StringComparer.OrdinalIgnoreCase); + private static readonly Regex PoliteInfinitive = new( + Before + @"(?por\s+favor)(?\s*,?\s+)(?não\s+)?" + + @"(?" + Alternatives(PolitePluralCommands.Keys) + @")" + + @"(?-(?:me|te|se|nos|vos|lhe|lhes|o|a|os|as))?" + After, Ci); + private static readonly Regex PluralConditionalAddressee = new( + @"^\s+(?:(?:o|a|os|as|ao|à|aos|às)\s+(?:central|polícia|comando|equipa|autoridades|reforços|documentos|relatório)\s+)?" + + @"(?:se|quando|assim\s+que|logo\s+que)(?:\s+vocês)?\s+(?:(?:não|já|ainda|também)\s+){0,2}" + + @"(?[\p{L}\p{M}]+)\b", Ci); + + public static string PoliteInfinitiveCommands(string text) => PoliteInfinitive.Replace(text, m => + { + var after = m.Result("$'"); + var conditional = PluralConditionalAddressee.Match(after); + if (!conditional.Success) return m.Value; + var conditionalVerb = conditional.Groups["verb"].Value.ToLowerInvariant(); + if (Regex.IsMatch(conditionalVerb, @"^(?:houverem|choverem|nevarem|acontecerem|existirem|ocorrerem)$", Ci)) + return m.Value; + var plural = Regex.IsMatch(conditionalVerb, + @"^(?:virem|forem|tiverem|estiverem|fizerem|disserem|derem|souberem|puderem|quiserem|trouxerem|mantiverem)$", Ci); + if (!plural && Regex.IsMatch(conditionalVerb, @"(?:arem|erem|irem)$", Ci)) + { + var infinitive = conditionalVerb[..^2]; + plural = PortugueseVerbs.IsKnownInfinitive(infinitive) || NaturaDictionary.Load(true).Check(infinitive); + } + if (!plural) return m.Value; + var verb = m.Groups["verb"].Value; + var target = CaseHelper.MatchCase(verb, PolitePluralCommands[verb]); + var clitic = m.Groups["clitic"].Value; + if (clitic.Length > 0) + target = m.Groups["neg"].Success + ? (CaseHelper.IsAllUpper(verb) ? clitic[1..].ToUpperInvariant() : clitic[1..].ToLowerInvariant()) + " " + target + : PortugueseVerbs.AttachEnclitic(target, clitic[1..]); + return m.Groups["please"].Value + m.Groups["gap"].Value + m.Groups["neg"].Value + target; + }); private static readonly HashSet HabitualVerbs = BuildHabitualVerbs(); private static HashSet BuildHabitualVerbs() @@ -73,7 +123,7 @@ private static HashSet BuildHabitualVerbs() "saber", "perceber", "reconhecer", "descobrir", "notar", "encontrar", "chegar", "voltar", "falar", "dizer", "responder", "perguntar", "contar", "pensar", "achar", "acreditar", "trabalhar", "estudar", "comprar", "pagar", "comer", "beber", "ajudar", "esquecer", - "ganhar", "perder", "mentir", "ouvir", "ver", "ler", "fazer", + "ganhar", "perder", "mentir", "ouvir", "ver", "ler", "fazer", "apoiar", "defender", "proteger", "acompanhar", }.SelectMany(Forms).Where(form => (PortugueseVerbs.IsKnownVerbForm(form) || dictionary.Check(form)) && !PortugueseVerbs.IsInfinitive(form) && !Regex.IsMatch(form, @"(?:ndo|[ai]d[oa]s?)$", Ci)) .Concat(new[] { "sei", "sabes", "sabe", "sabemos", "sabem", "soube", "soubeste", "soubemos", "souberam", @@ -92,7 +142,179 @@ private static HashSet BuildHabitualVerbs() @"poderia|poderias|poderíamos|poderiam|devo|deves|deve|devemos|devem|devia|devias|devíamos|deviam)" + @"(?=\s+\p{L}+(?:ar|er|ir|ôr)" + After + @")", Ci); - public static string NaturalWordOrder(string text, ReferenceHint? reference, PersonalNameArticles names) + private static readonly Dictionary SayForms = BuildSayForms(); + private static Dictionary BuildSayForms() + { + var dizer = new[] + { + "dizer", "digo", "dizes", "diz", "dizemos", "dizem", + "disse", "disseste", "disse", "disseram", + "dizia", "dizias", "dizíamos", "diziam", + "direi", "dirás", "dirá", "diremos", "dirão", + "diria", "dirias", "diríamos", "diriam", + "diga", "digas", "digamos", "digam", + "dissesse", "dissesses", "disséssemos", "dissessem", + "dizeres", "dizermos", "dizerem", + "dizendo", "dito", "dita", "ditos", "ditas", + "dissera", "disseras", "disséramos", "dissemos", + }; + var result = Forms("falar").Select((form, index) => (form, target: dizer[index])) + .GroupBy(pair => pair.form, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First().target, StringComparer.OrdinalIgnoreCase); + foreach (var (source, target) in new[] + { + ("falámos", "dissemos"), ("falais", "dizeis"), ("falastes", "dissestes"), + ("faláveis", "dizíeis"), ("falareis", "direis"), ("falaríeis", "diríeis"), + ("faleis", "digais"), ("falásseis", "dissésseis"), ("falardes", "dizerdes"), + ("faláreis", "disséreis"), ("falai", "dizei"), + }) + result[source] = target; + return result; + } + + private static string? SayForm(string verb, string before, string english) + { + var target = SayForms[verb]; + if (verb.Equals("falamos", StringComparison.OrdinalIgnoreCase)) + { + if (Regex.IsMatch(english, @"\bwe\s+(?:(?:always|just|only)\s+)?(?:said|talked|spoke)\b", Ci)) + target = "dissemos"; + else if (!Regex.IsMatch(english, @"\bwe\s+(?:(?:always|just|only)\s+)?(?:say|talk|speak)\b", Ci)) + return null; + } + if (Regex.IsMatch(verb, @"^falar(?:es|mos|des|em)?$", Ci)) + { + var conditional = Regex.IsMatch(before, @"\b(?:se|quando|assim\s+que|logo\s+que|sempre\s+que|toda\s+(?:a\s+)?vez\s+que)" + + @"\s+(?:(?:eu|tu|ele|ela|nós|vós|vocês|eles|elas|alguém|ninguém|todos|todas|" + + @"(?:(?:o|a|os|as|um|uma|uns|umas|meu|minha|teu|tua|seu|sua)\s+){1,2}[\p{L}\p{M}]+|" + + @"(?-i:\p{Lu}[\p{L}\p{M}]+(?:\s+\p{Lu}[\p{L}\p{M}]+){0,2}))\s+)?" + + Adverbs + @"(?:(?:me|te|lhe|lhes|nos|vos)\s+)?$", Ci); + if (conditional) + target = "disser" + verb[5..].ToLowerInvariant(); + else if (Regex.IsMatch(before, @"\b(?:se|quando|assim\s+que|logo\s+que)\s+[^,;.!?]*$", Ci) && + !Regex.IsMatch(before, @"\b(?:a|de|para|quiser(?:es|mos|em)?|puder(?:es|mos|em)?|vou|vais|vai|vamos|vão)\s+$", Ci)) + return null; + } + return CaseHelper.MatchCase(verb, target); + } + + private static readonly Regex ContentSpeechVerb = new( + @"(?" + Alternatives(SayForms.Keys) + @")" + + @"(?-(?:me|te|lhe|lhes|nos|vos))?(?![\p{L}\p{M}\p{N}_-])" + + @"(?\s+(?:a|para)\s+(?:mim|ti|si|ele|ela|eles|elas|nós|vós|você|vocês)" + + @"(?![\p{L}\p{M}\p{N}_-]))?" + + @"(?:(?\s+)(?algo)(?![\p{L}\p{M}\p{N}_-]))?", Ci); + private static readonly Regex SpeechContent = new( + @"^\s+(?alguma\s+coisa|coisa\s+alguma|qualquer\s+coisa|nada|algo|tudo|isto|isso|aquilo|" + + @"(?:a\s+)?verdade|mentiras|(?:uma|a|as|muitas)\s+mentiras?|" + + @"(?:uma|duas|três|algumas|muitas|estas|essas)\s+(?:coisas?|palavras?))" + + @"(?![\p{L}\p{M}\p{N}_-])", Ci); + private static readonly Regex EnglishSay = new(@"\b(?:say|says|said|saying|tell|tells|told|telling)\b", Ci); + private static readonly Regex EnglishSpeechActivity = new(@"\b(?:talk(?:s|ed|ing)?|speak(?:s|ing)?|spoke|spoken)\b", Ci); + private static readonly Regex SpeechQuestionBefore = new( + @"(?? isBlocked) + { + var matches = ContentSpeechVerb.Matches(text); + return ContentSpeechVerb.Replace(text, m => + { + var verb = m.Groups["verb"].Value; + if (isBlocked?.Invoke("falar") == true || isBlocked?.Invoke(verb) == true) return m.Value; + var before = Regex.Match(m.Result("$`"), @"[^.!?;:]*$").Value; + if (Regex.IsMatch(verb, @"^falas?$", Ci) && PortugueseConverter.HasNominalDeterminerBefore(before)) + return m.Value; + var after = m.Groups["gap"].Value + m.Groups["algo"].Value + m.Result("$'"); + var content = SpeechContent.Match(after); + var question = SpeechQuestionBefore.Match(before); + var confirmed = matches.Count == 1 && EnglishSay.IsMatch(english) && !EnglishSpeechActivity.IsMatch(english); + var contentQuestion = question.Success && + !Regex.IsMatch(before[..question.Index], @"\b(?:de|do|da|sobre|com|por|em|no|na)\s+$", Ci); + var reported = !Regex.IsMatch(before, @"\bsem\s+$", Ci) && + Regex.IsMatch(after, @"^\s+(?:que|o\s+que)\b|^\s*[:,]?\s*[""“«]", Ci); + var addressed = m.Groups["recipient"].Success && Regex.IsMatch(after, @"^\s*[,.;!?]|^\s*$", Ci); + if (!content.Success && !(confirmed && (contentQuestion || reported || addressed))) return m.Value; + if (content.Success && isBlocked?.Invoke(content.Groups["content"].Value) == true) return m.Value; + var target = SayForm(verb, before, english); + if (target is null) return m.Value; + var clitic = m.Groups["clitic"].Value; + if (clitic.Length > 0) + target = PortugueseVerbs.AttachMesoclitic(target, clitic[1..]) ?? + PortugueseVerbs.AttachEnclitic(target, clitic[1..]); + return target + m.Groups["recipient"].Value + (m.Groups["algo"].Success + ? m.Groups["gap"].Value + CaseHelper.MatchCase(m.Groups["algo"].Value, "alguma coisa") : ""); + }); + } + + private const string NonsenseModifiers = @"(?:(?:sempre|só|apenas|muit[oa]s|tant[oa]s|algum[ao]s|ess[ae]s|est[ae]s)\s+){0,2}"; + private static readonly Regex SpokenNonsenseBefore = new( + DialogueBefore + @"(?:" + Alternatives(SayForms.Keys) + "|" + + PortugueseConverter.PronounRules.DizerVerbForms + @")\s+" + NonsenseModifiers + "$", Ci); + internal static bool KeepSpokenTolices(string before) => SpokenNonsenseBefore.IsMatch(before); + private static readonly Regex SayNonsense = new( + DialogueBefore + @"(?" + Alternatives(SayForms.Keys) + @")" + + @"(?\s+" + NonsenseModifiers + @")" + + @"(?bobagens|tolices|disparates|besteiras|asneiras)" + After, Ci); + private static readonly Regex EveryTime = new( + DialogueBefore + @"(?toda\s+(?:a\s+)?vez\s+que)" + After, Ci); + private static readonly Regex IntoxicatedSlang = new( + DialogueBefore + @"(?estou|estás|está|estamos|estão|estava|estavas|estávamos|estavam|estar|estiver|estiverem|" + + Alternatives(Forms("ficar").Concat(Forms("andar"))) + @")" + + @"(?\s+(?:(?:muito|mesmo|completamente|totalmente|bem|todo|toda|todos|todas)\s+){0,2})" + + @"(?doid[ãa]o|doidona|doidões|doidonas)" + After, Ci); + private static readonly Regex EnglishIntoxication = new( + @"\b(?:stoned|drugged)\b|\b(?:get|gets|got|getting|be|been|being|am|is|are|was|were|feel|feels|felt|" + + @"I'm|he's|she's|you're|we're|they're)\s+(?:(?:really|very|so|too|completely)\s+){0,2}high\b" + + @"(?!\s+(?:scores?|grades?|marks?|hopes?|expectations?|praise|school|heels?|up|above|blood|temperature|priority|risk|in)\b)" + + @"(?!\s+on\s+(?:life|love|adrenaline|the\s+list)\b)", Ci); + private static readonly Regex DrugIntoxicationComplement = new( + @"^\s+(?:de|com)\s+(?:(?:a|as|o|os|muita|muitas|muito|muitos)\s+)?" + + @"(?:drogas?|cocaína|heroína|canábis|cannabis|maconha|marijuana|erva|ganza|haxixe|ecstasy|mdma|" + + @"metanfetamina|cetamina|ketamina)\b", Ci); + + public static string SpokenIdioms(string text, ReferenceHint? reference, bool vocabulary, Func isBlocked) + { + if (vocabulary) + { + text = SayNonsense.Replace(text, m => + { + var verb = m.Groups["verb"].Value; + if (isBlocked("falar") || isBlocked(verb) || + new[] { "bobagens", "tolices", "disparates", "besteiras", "asneiras" }.Any(isBlocked)) return m.Value; + var before = Regex.Match(m.Result("$`"), @"[^.!?;]*$").Value; + var target = SayForm(verb, before, reference?.EnglishText ?? ""); + return target is null ? m.Value : target + m.Groups["gap"].Value + m.Groups["noun"].Value; + }); + text = IntoxicatedSlang.Replace(text, m => + { + if (isBlocked(m.Groups["adj"].Value)) return m.Value; + var after = Regex.Match(m.Result("$'"), @"^[^.!?;]*").Value; + if (Regex.IsMatch(after, @"^\s+(?:por|de\s+(?:alegria|felicidade|amor|entusiasmo))\b", Ci) || + !EnglishIntoxication.IsMatch(reference?.EnglishText?.Replace('’', '\'') ?? "") && + !DrugIntoxicationComplement.IsMatch(after)) return m.Value; + var target = m.Groups["adj"].Value.ToLowerInvariant() switch + { + "doidona" => "pedrada", "doidonas" => "pedradas", "doidões" => "pedrados", _ => "pedrado", + }; + return m.Groups["state"].Value + m.Groups["gap"].Value + + CaseHelper.MatchCase(m.Groups["adj"].Value, target); + }); + } + return EveryTime.Replace(text, m => + { + var before = Regex.Match(m.Result("$`"), @"[^.!?;]*$").Value; + var alreadyHabitual = Regex.IsMatch(before, + @"\bsempre\b(?!.*\b(?:que|se|quando|porque|enquanto|mas|e)\b)", Ci | RegexOptions.Singleline); + var negative = Regex.IsMatch(before, @"\b(?:não|nem|nunca|raramente)\b", Ci); + return CaseHelper.MatchCase(m.Value, + alreadyHabitual ? negative ? "de cada vez que" : "quando" : "sempre que"); + }); + } + + public static string NaturalWordOrder(string text, ReferenceHint? reference, PersonalNameArticles names, bool pronouns) { text = EchoTopicQuestion.Replace(text, m => { @@ -113,10 +335,29 @@ public static string NaturalWordOrder(string text, ReferenceHint? reference, Per text = ModalAlways.Replace(text, m => CaseHelper.MatchCase(m.Groups["always"].Value, m.Groups["modal"].Value) + m.Groups["gap"].Value + (CaseHelper.IsAllUpper(m.Groups["always"].Value) ? "SEMPRE" : "sempre")); - text = HabitualAlways.Replace(text, m => HabitualVerbs.Contains(m.Groups["v"].Value) - ? m.Groups["head"].Value + CaseHelper.MatchCase(m.Groups["adv"].Value, m.Groups["v"].Value) + - m.Groups["space"].Value + (CaseHelper.IsAllUpper(m.Groups["adv"].Value) ? "SEMPRE" : "sempre") - : m.Value); + text = HabitualAlways.Replace(text, m => + { + var verb = m.Groups["v"].Value; + if (!HabitualVerbs.Contains(verb)) return m.Value; + var head = m.Groups["head"].Value; + var clitic = m.Groups["clitic"].Value.Trim(); + var target = CaseHelper.MatchCase(m.Groups["adv"].Value, verb); + if (clitic.Length > 0) + { + if (!pronouns || PortugueseVerbs.IsFutureOrConditional(verb)) return m.Value; + if (Forms("achar").Contains(verb, StringComparer.OrdinalIgnoreCase)) + return m.Value; + var before = m.Result("$`") + head; + var attracts = PortugueseConverter.PronounRules.AfterSubordinateSubject(before) || + Regex.IsMatch(before, @"\b(?:que|quando|porque|enquanto|se)\s+$", Ci); + target = attracts + ? CaseHelper.MatchCase(m.Groups["adv"].Value, clitic) + " " + verb.ToLowerInvariant() + : PortugueseVerbs.AttachEnclitic(target, clitic); + if (CaseHelper.IsAllUpper(m.Value)) target = target.ToUpperInvariant(); + } + return head + target + m.Groups["space"].Value + + (CaseHelper.IsAllUpper(m.Groups["adv"].Value) ? "SEMPRE" : "sempre"); + }); return RecoverableProgressiveSubject.Replace(text, m => m.Groups["link"].Value + m.Groups["space"].Value + m.Groups["verb"].Value); } @@ -382,6 +623,58 @@ public static string FamilySubjects(string text, ReferenceHint? reference) }); } + private const string SurnameToken = @"(?-i:\p{Lu}[\p{L}\p{M}'’-]+)"; + private static readonly Regex FamilySurnameSubject = new( + @"(?(?:^|[.!?;:]\s*|\n|[\uE000-\uF8FF]\s*|" + + Before + @"(?:que|como|quando|porque|se|embora|enquanto)\s+)[\s\uE000-\uF8FF-]*)" + + @"(?" + SurnameToken + @"(?:\s+(?:(?:de|da|do|dos|das)\s+)?" + SurnameToken + @"){0,2}?)" + + @"(?\s+(?:(?:não|nunca|já|ainda|sempre|também|só|todos|todas)\s+){0,3})" + + @"(?têm|vêm|\p{L}+(?:am|em|ão))" + After, Ci); + private static readonly Regex PluralFamilyAuxiliary = new( + @"^(?:são|eram|foram|serão|seriam|sejam|fossem|estão|estavam|estiveram|estarão|estariam|estejam|estivessem|" + + @"têm|tinham|tiveram|terão|teriam|tenham|tivessem|vêm)$", Ci); + private static readonly Regex RaisedFamilyPredicate = new( + @"^\s+(?:(?:sempre|assim|bem|mal|todos|todas|muito)\s+){0,3}(?criad[oa]s|educad[oa]s)" + After, Ci); + + public static string FamilySurnameSubjects(string text, ReferenceHint? reference, PersonalNameArticles names) => + FamilySurnameSubject.Replace(text, m => + { + var name = m.Groups["name"].Value; + var first = name.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)[0]; + if (Regex.IsMatch(first, @"^(?:o|a|os|as|um|uma|uns|umas|eu|tu|ele|ela|nós|vós|vocês|eles|elas|" + + @"todos|todas|estes|estas|esses|essas|aqueles|aquelas|quem|quais|quantos|quantas)$", Ci) || + names.TryGet(first, out var selected) && selected == "none") return m.Value; + var verb = m.Groups["verb"].Value; + var pluralVerb = PluralFamilyAuxiliary.IsMatch(verb) || + Regex.IsMatch(verb, @"(?:am|em|ão)$", Ci) && PortugueseVerbs.IsKnownVerbForm(verb) && + !PortugueseVerbs.IsInfinitive(verb); + if (!pluralVerb) return m.Value; + var english = reference?.EnglishText ?? ""; + var escaped = Regex.Escape(Regex.Replace(name, @"\s+", " ")); + var confirmed = Regex.IsMatch(english, @"(? 0 || !raised.Success || + !Regex.IsMatch(verb, @"^(?:são|eram|foram|serão|seriam|sejam|fossem)$", Ci) || + name.Contains(' ') || NaturaDictionary.Load(true).Check(name.ToLowerInvariant())) + return m.Value; + } + var copula = Regex.IsMatch(verb, @"^(?:são|eram|foram|serão|seriam|sejam|fossem|" + + @"estão|estavam|estiveram|estarão|estariam|estejam|estivessem)$", Ci); + var feminine = copula && Regex.IsMatch(predicate, + @"^\s+(?:(?:sempre|assim|bem|mal|todas|muito|já|ainda)\s+){0,3}" + + @"(?:\p{L}+(?:adas|idas)|mortas|feitas|vistas|ditas|postas|abertas|mulheres|irmãs|filhas|meninas|raparigas)\b", Ci); + var article = feminine ? "as" : "os"; + var initial = Regex.IsMatch(m.Groups["head"].Value, @"^(?:[\s\uE000-\uF8FF-]*|[.!?;:]\s*)$"); + if (CaseHelper.IsAllUpper(name)) article = article.ToUpperInvariant(); + else if (initial) article = CaseHelper.Capitalize(article); + return m.Groups["head"].Value + article + " " + name + m.Groups["gap"].Value + verb; + }); + private static string FamilyArticle(string noun, bool initial) { var feminine = Regex.IsMatch(noun, @"^(?:mãe|mamã|avó|tia|irmã)$", Ci); @@ -447,6 +740,55 @@ public static string ReferenceObjectClitics(string text, ReferenceHint? referenc }); } + private static readonly Regex PlateNoun = new(DialogueBefore + @"placas?" + After, Ci); + private static readonly Regex VehicleContext = new( + @"\b(?:carros?|automóveis|automóvel|veículos?|caminh(?:ão|ões)|cami(?:ão|ões)|carrinhas?|carr(?:os?|inhas?)-patrulha|" + + @"motos?|motocicletas?|motociclos?|autocarros?|ônibus|sedans?|sedãs?|carruagens?|táxis?|reboques?|" + + @"SUV|BMW|Nissan|Ford|Toyota|Chevrolet|Honda|Mercedes|Volkswagen|Renault|Peugeot)\b", Ci); + private static readonly Regex PlateOtherSense = new( + @"^\s+(?:de\s+(?:(?:a|o|uma|um)\s+)?(?:rua|sinalizaç(?:ão|ões)|trânsito|pare|stop|aviso|publicidade|homenagem|" + + @"identificaç(?:ão|ões)\s+da\s+porta|metal|aço|alumínio|madeira|vidro|gesso|betão|concreto|rede|som|vídeo|video|" + + @"memória|circuito|Petri|cultura|induç(?:ão|ões)|gás|cozinha|parede|matrícula)|" + + @"tectónicas?|tectônicas?|bacterianas?|dentárias?|dentais|comemorativas?|publicitárias?|indicativas?|" + + @"gráficas?|mãe|solares?|fotovoltaicas?|vitrocerâmicas?)\b", Ci); + private static readonly Regex EnglishVehiclePlate = new( + @"\b(?:licen[cs]e|number|registration|vanity|temporary)\s*(?:-\s*)?plates?\b|" + + @"\b(?:plates?\s+(?:numbers?|came\s+back|we\s+could\s+ID)|(?:run|running|ran|check|checking|checked|" + + @"call\s+in|called\s+in|stolen|out-of-state|Jersey|Pennsylvania)\s+(?:(?:the|these|those|a|his|her)\s+)?plates?)\b|" + + @"\bregistration\b", Ci); + private static readonly Regex EnglishOtherPlate = new( + @"\b(?:(?:dinner|dining|food|ceramic|tectonic|dental|commemorative|memorial|circuit)\s+plates?|" + + @"(?:road|street|warning)\s+signs?|motherboards?|graphics\s+cards?|school\s+registration)\b", Ci); + + public static string VehiclePlates(string text, string? previous, ReferenceHint? reference, Func isBlocked) => + PlateNoun.Replace(text, m => + { + if (isBlocked(m.Value) || isBlocked("placa") || isBlocked("placas") || + new[] { "placa do carro", "número da placa", "sem placa" }.Any(isBlocked)) return m.Value; + var before = Regex.Match(m.Result("$`"), @"[^.!?;]*$").Value; + var after = Regex.Match(m.Result("$'"), @"^[^.!?;]*").Value; + if (PlateOtherSense.IsMatch(after) || + Regex.IsMatch(before, @"\b(?:rua|parede|porta|prédio|edifício|dentes|dentista|computador|fogão)" + + @"\b[^.!?;]{0,20}$", Ci)) return m.Value; + var english = reference?.EnglishText ?? ""; + var confirmedEnglish = EnglishVehiclePlate.IsMatch(english) || + Regex.IsMatch(english, @"\bplates?\b", Ci) && Regex.IsMatch(english, + @"\b(?:car|vehicle|truck|van|sedan|SUV|motorcycle|bus|stolen|DMV)\b", Ci); + if (EnglishOtherPlate.IsMatch(english)) confirmedEnglish = false; + var vehicleAfter = Regex.IsMatch(after, @"^\s+(?:d[oa]s?|de)\s+(?:(?:um|uma|uns|umas)\s+)?", Ci) && + VehicleContext.IsMatch(Regex.Match(after, @"^\s+(?:d[oa]s?|de)\s+(?:\p{L}+\s+){0,2}\p{L}+").Value); + var regional = Regex.IsMatch(after, @"^\s+(?:de\s+(?:fora|outro\s+estado)|estrangeiras?|clonadas?)\b", Ci); + var number = Regex.IsMatch(before, @"\bnúmeros?\s+d[ae]s?\s+$", Ci) && + !EnglishOtherPlate.IsMatch(english); + var localVehicle = VehicleContext.IsMatch(before) || vehicleAfter; + var previousVehicle = Regex.IsMatch(text[..m.Index], + @"^[\s\uE000-\uF8FF-]*(?:(?:e|mas|com|sem|a|as|o|os|uma|umas|número|da|das|em|marca|modelo,?|semelhante)\s+)*$", Ci) && + VehicleContext.IsMatch(previous ?? ""); + if (!confirmedEnglish && !localVehicle && !regional && !number && !previousVehicle) return m.Value; + return CaseHelper.MatchCase(m.Value, + m.Value.EndsWith("s", StringComparison.OrdinalIgnoreCase) ? "matrículas" : "matrícula"); + }); + public static string TravelStops(string text, ReferenceHint? reference) => Regex.IsMatch(reference?.EnglishText ?? "", @"\b(?:(?:a|one|two|three|some|the|this|that|another|our)\s+" + @"(?:(?:little|small|short|brief|quick|last|next|more|bus|train|scheduled)\s+){0,3}|" + @@ -460,6 +802,53 @@ public static string TravelStops(string text, ReferenceHint? reference) => @"^(?:(?:tenho|temos|há|são|e|quais\s+são)\s+)?(?:(?:o|a|os|as|um|uma|uns|umas|ess[ea]s?|est[ea]s?)\s+)?" + @"(?:(?:boas?|más?|últimas?|novas?)\s+)?(?:notícias|novidades|informações|detalhes|verdade|explicação)" + @"\s*[.!?…]?$", Ci); + private static readonly Regex GiveRequestReference = new( + @"(?:^|[.!?]\s+)[\s]*(?:(?:boys|girls|guys|kids|folks|everyone|gentlemen|ladies|sir|ma['’]am|" + + @"(?-i:[A-Z][a-z]+))\s*,\s*)?(?:please\s*,?\s*)?(?:give|grant)\s+(?me|us)\s+" + + @"(?(?:(?:a|one|two|three|a\s+few|some|another|your)\s+)" + + @"(?:(?:little|brief|short|quick|more|undivided|full)\s+){0,2}" + + @"(?:seconds?|minutes?|moments?|time|break|favou?r|chance|opportunity|hand|help|attention))" + + @"(?=\s*[,!.?…]|$)", Ci); + private static readonly Regex MissingGiveRecipient = new( + @"(?(?:^|[.!?…]\s+|\n)[\s\uE000-\uF8FF–—-]*" + + @"(?:(?:rapazes|raparigas|miúdos|miúdas|pessoal|meninos|meninas|senhores|senhoras|" + + @"senhor|senhora|detetive|agente|doutor|doutora|(?-i:\p{Lu}[\p{Ll}\p{M}]+))" + + @"(?:\s+(?-i:\p{Lu}[\p{Ll}\p{M}]+))?\s*,\s*)?(?:por\s+favor\s*,?\s*)?)" + + @"(?dêem|deem|dai|dê|dá)(?\s+)" + + @"(?(?:um|uma|dois|duas|três|alguns|algumas|mais|o|a)\s+" + + @"(?:(?:pouco|bocado)\s+de\s+)?(?:(?:breve|pequeno|pequena|toda|total|vossa|sua)\s+){0,2}" + + @"(?:segundos?|minutos?|momentos?|tempo|pausa|descanso|folga|favor|hipótese|chance|oportunidade|mão|ajuda|atenção))" + + @"(?=\s*[,!.?…\uE000-\uF8FF]|$)", Ci); + + public static string GiveRequestRecipients(string text, string? english) + { + if (string.IsNullOrWhiteSpace(english)) return text; + var requests = GiveRequestReference.Matches(english); + var candidates = MissingGiveRecipient.Matches(text); + if (requests.Count != 1 || candidates.Count != 1) return text; + var request = requests[0]; + var candidate = candidates[0]; + var category = Regex.Match(request.Groups["object"].Value, + @"(?:seconds?|minutes?|moments?|time|break|favou?r|chance|opportunity|hand|help|attention)$", Ci).Value.ToLowerInvariant(); + var objectPattern = category switch + { + "second" or "seconds" => @"segundos?$", + "minute" or "minutes" => @"minutos?$", + "moment" or "moments" => @"momentos?$", + "time" => @"tempo$", + "break" => @"(?:pausa|descanso|folga)$", + "favor" or "favour" => @"favor$", + "chance" or "opportunity" => @"(?:hipótese|chance|oportunidade)$", + "hand" or "help" => @"(?:mão|ajuda)$", + _ => @"atenção$", + }; + if (!Regex.IsMatch(candidate.Groups["object"].Value, objectPattern, Ci)) return text; + var verb = candidate.Groups["verb"]; + var clitic = request.Groups["recipient"].Value.Equals("me", StringComparison.OrdinalIgnoreCase) ? "me" : "nos"; + return text[..verb.Index] + PortugueseVerbs.AttachEnclitic(verb.Value, clitic) + + text[(verb.Index + verb.Length)..]; + } + private static readonly Regex GiveInformationReference = new( @"(?:^|[.!?]\s+)(?gimme|give\s+(?:it\s+to\s+me|me))" + @"(?\s+(?:the\s+)?(?:(?:good|bad)\s+)?(?:news|details|truth|information|explanation|update))?" + @@ -785,6 +1174,37 @@ public static string SpatialPosition(string text, ReferenceHint? reference) }); } + private static readonly Regex WaitForPerson = new( + DialogueBefore + @"(?" + Alternatives(Forms("esperar")) + @")" + + @"(?\s+(?:(?:ainda|sempre|também|só|apenas|muito)\s+){0,2})" + + @"(?:(?
o|a|os|as)(?\s+)" + + @"(?(?:(?:meu|minha|teu|tua|seu|sua|nosso|nossa|meus|minhas|teus|tuas|seus|suas|nossos|nossas)\s+)?" + + @"(?:pai|mãe|pais|irmãos?|irmãs?|filhos?|filhas?|amigos?|amigas?|colegas?|polícias?|agentes?|médicos?|médicas?)|" + + @"(?-i:\p{Lu}[\p{L}\p{M}]+))|(?(?-i:\p{Lu}[\p{L}\p{M}]+)))" + After, Ci); + + public static string WaitingForPeople(string text, PersonalNameArticles names, Func isBlocked) => WaitForPerson.Replace(text, m => + { + if (isBlocked("esperar") || isBlocked(m.Groups["verb"].Value)) return m.Value; + var after = m.Result("$'"); + if (Regex.IsMatch(after, @"^\s+(?:\p{L}+(?:ar|er|ir|arem|erem|irem)|que)\b", Ci)) return m.Value; + var person = m.Groups["person"].Value; + if (!person.Contains(' ') && char.IsUpper(person[0]) && + NaturaDictionary.Load(true).Check(person.ToLowerInvariant()) && + !FemininePersonalNames.Contains(person) && !MasculinePersonalNames.Contains(person)) return m.Value; + if (!m.Groups["article"].Success) + { + var article = PersonalNameArticle(person, names); + var preposition = article == "o" ? "pelo" : article == "a" ? "pela" : "por"; + return m.Groups["verb"].Value + m.Groups["gap"].Value + + (CaseHelper.IsAllUpper(m.Value) ? preposition.ToUpperInvariant() : preposition) + " " + person; + } + var prep = m.Groups["article"].Value.ToLowerInvariant() switch + { "a" => "pela", "os" => "pelos", "as" => "pelas", _ => "pelo" }; + return m.Groups["verb"].Value + m.Groups["gap"].Value + + (CaseHelper.IsAllUpper(m.Value) ? prep.ToUpperInvariant() : CaseHelper.MatchCase(m.Groups["article"].Value, prep)) + + m.Groups["space"].Value + person; + }); + public static string WaitingComplements(string text, ReferenceHint? reference) { if (!Regex.IsMatch(reference?.EnglishText ?? "", @"\b(?:keep|kept|keeping)\s+you\s+waiting\b", Ci)) @@ -970,6 +1390,69 @@ private static Dictionary MapForms(string source, string target) .ToDictionary(group => group.Key, group => group.First().target, StringComparer.OrdinalIgnoreCase); } + private const string ExamObjectPattern = + @"(?\s+)(?o|a|os|as|um|uma|uns|umas|este|esta|estes|estas|esse|essa|esses|essas|aquele|aquela)\s+" + + @"(?testes?|provas?|exames?|concursos?|vestibulares?|questionários?|avaliaç(?:ão|ões))"; + private static readonly Regex PerfectScoreVerb = new( + DialogueBefore + @"(?" + Alternatives(Forms("gabaritar").Concat(new[] + { + "gabaritais", "gabaritastes", "gabaritáveis", "gabaritareis", "gabaritaríeis", + "gabariteis", "gabaritásseis", "gabaritardes", "gabaritáreis", "gabaritai", + })) + @")" + After + @"(?![\d_])(?:(?:" + ExamObjectPattern + + @"|(?\s+)(?:todas\s+)?(?:as\s+)?(?perguntas|questões|respostas))" + After + @")?", Ci); + private static readonly Regex ExamKey = new( + DialogueBefore + @"(?o|os|um|uns)(?\s+)(?gabaritos?)" + After + + @"(?=\s+(?:de|do|da|dos|das)\s+(?:(?:um|uma|uns|umas|este|esta|estes|estas)\s+)?(?:testes?|provas?|exames?|questionários?|questões)\b)", Ci); + + private static string PerfectScores(string text, string english, Func? blocked) + { + text = PerfectScoreVerb.Replace(text, m => + { + var verb = m.Groups["v"].Value; + if (blocked?.Invoke("gabaritar") == true || blocked?.Invoke(verb) == true || + PortugueseConverter.HasNominalDeterminerBefore(m.Result("$`"))) return m.Value; + if (verb.Equals("gabarito", StringComparison.OrdinalIgnoreCase) && + !Regex.IsMatch(m.Result("$`"), @"\beu\s+(?:(?:não|nunca|sempre|já|também|só)\s+){0,3}$", Ci)) + return m.Value; + var after = m.Result("$'"); + var exam = m.Groups["exam"]; + var questions = m.Groups["questions"]; + if (!exam.Success && !questions.Success && + Regex.IsMatch(after, @"^\s+(?!(?:em|no|na|nos|nas|num|numa|sempre|nunca|também|hoje|ontem|amanhã|novamente|outra\s+vez)\b)\p{L}", Ci)) + return m.Value; + if (Regex.IsMatch(english, @"\b(?:qualif(?:y|ies|ied|ying)|qualifications?|templates?|gauges?)\b", Ci)) + return m.Value; + var ending = verb["gabarit".Length..]; + var participle = Regex.IsMatch(ending, @"^ad[oa]s?$", Ci); + if (participle && !Regex.IsMatch(m.Result("$`"), @"\b(?:ter|tenho|tens|tem|temos|têm|tinha|tinham|tínhamos|" + + @"tive|teve|tivemos|tiveram|tiver|tiverem|tenha|tenham|havia|haviam|tivesse|tivessem)\s+$", Ci)) + return Regex.IsMatch(m.Result("$`"), @"\b(?:testes?|provas?|exames?|questionários?|avaliaç(?:ão|ões))" + + @"\s+(?:(?:foi|foram|está|estão|estava|estavam|tinha\s+sido|tem\s+sido)\s+)?$", Ci) + ? CaseHelper.MatchCase(verb, "resolvid" + ending[2..] + " sem erros") : m.Value; + var result = "acert" + ending; + if (questions.Success) + result += m.Groups["gap"].Value + "todas as " + questions.Value; + else + { + result += " em tudo"; + if (exam.Success) + { + var det = m.Groups["det"].Value.ToLowerInvariant(); + result += m.Groups["gap"].Value + "n" + det + " " + exam.Value; + } + } + return CaseHelper.MatchCase(verb, result); + }); + return ExamKey.Replace(text, m => + { + var noun = m.Groups["noun"].Value; + if (blocked?.Invoke("gabarito") == true || blocked?.Invoke(noun) == true) return m.Value; + var det = m.Groups["det"].Value; + var target = (det.StartsWith("u", StringComparison.OrdinalIgnoreCase) ? "umas" : "as") + m.Groups["gap"].Value; + return CaseHelper.MatchCase(m.Value, target + "soluções"); + }); + } + private static readonly Dictionary WiretapForms = MapForms("grampear", "colocar"); private static readonly Dictionary InterceptForms = MapForms("grampear", "intercetar"); private static readonly Regex InterceptConversation = new( @@ -987,11 +1470,99 @@ private static Dictionary MapForms(string source, string target) private static readonly Dictionary DozeForms = MapForms("cochilar", "dormitar"); private static readonly Regex DozeVerb = new(Before + @"(?" + FinitePattern("cochilar") + @")" + After, Ci); + private static readonly Dictionary WidenForms = MapForms("lacear", "alargar"); + private static readonly Dictionary YieldForms = MapForms("lacear", "ceder"); + private static readonly Dictionary SoftenForms = MapForms("lacear", "amolecer"); + private static readonly Regex LacearVerb = new( + DialogueBefore + @"(?" + Alternatives(WidenForms.Keys.Append("laceá")) + @")" + + @"(?-(?:me|te|se|nos|vos|o|a|os|as|lo|la|los|las))?" + After, Ci); + + private static string LacearVocabulary(string text, ReferenceHint? reference, Func? isBlocked) => + LacearVerb.Replace(text, m => + { + var verb = m.Groups["v"].Value; + if (isBlocked?.Invoke("lacear") == true || isBlocked?.Invoke(verb) == true) return m.Value; + var before = Regex.Match(m.Result("$`"), @"[^.!?;]*$").Value; + var after = Regex.Match(m.Result("$'"), @"^[^.!?;]*").Value; + var local = before + m.Value + after; + if (Regex.IsMatch(local, @"\b(?:laços?|fitas?|enfeites?|ornamentos?)\b", Ci) || + Regex.IsMatch(reference?.EnglishText ?? "", @"\b(?:ribbons?|bows?|decorate|adorn)\b", Ci)) + return m.Value; + var cliticInfinitive = verb.Equals("laceá", StringComparison.OrdinalIgnoreCase); + var key = cliticInfinitive ? "lacear" : verb; + var directObject = Regex.IsMatch(after, + @"^\s+(?:(?:um\s+pouco|mais|bem|ligeiramente)\s+)?(?:(?:o|a|os|as|um|uma|uns|umas|este|esta|estes|estas|esse|essa|esses|essas|" + + @"meus?|minhas?|seus?|suas?|teus?|tuas?|nossos?|nossas?)\s+\p{L}|(?:sapatos?|botas?|calças?|luvas?|cintos?|algemas?|couro|pele)\b)", Ci) || + Regex.IsMatch(m.Groups["cl"].Value, @"^-(?:o|a|os|as|lo|la|los|las)$", Ci); + var material = Regex.IsMatch(local, @"\b(?:couro|pele|material)\b", Ci) && + !Regex.IsMatch(local, @"\b(?:sapatos?|botas?|calças?|luvas?|algemas?|cintos?)\b", Ci); + var soften = material || Regex.IsMatch(reference?.EnglishText ?? "", @"\bsoften(?:s|ed|ing)?\b", Ci); + var target = (soften ? SoftenForms : directObject || m.Groups["cl"].Success || Regex.IsMatch(verb, @"ad[oa]s?$", Ci) + ? WidenForms : YieldForms)[key]; + if (cliticInfinitive) + target = VerbConjugator.CliticInfinitive(soften ? "amolecer" : "alargar"); + return CaseHelper.MatchCase(verb, target) + m.Groups["cl"].Value; + }); + + private static readonly Dictionary PromisePersons = BuildPromisePersons(); + private static Dictionary BuildPromisePersons() + { + var forms = Forms("prometer"); + // Same slots as VerbConjugator, through personal infinitives; ambiguous finite forms need an explicit subject. + var persons = new[] + { + "", "me", "te", "se", "nos", "se", "me", "te", "se", "se", + "", "te", "nos", "se", "me", "te", "se", "nos", "se", + "", "te", "nos", "se", "", "te", "nos", "se", + "", "te", "nos", "se", "te", "nos", "se", + }; + return forms.Take(persons.Length).Select((form, i) => (form, person: persons[i])) + .GroupBy(item => item.form, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, + group => group.Select(item => item.person).Distinct().Count() == 1 ? group.First().person : "", + StringComparer.OrdinalIgnoreCase); + } + private static readonly Regex PromiseLaugh = new( + Before + @"(?" + Alternatives(PromisePersons.Keys) + @")(?\s+)(?não\s+|nunca\s+)?" + + @"(?:se\s+)?(?rir)(?:-se)?" + After + @"(?!-)", Ci); + private static readonly Regex TellNobody = new( + Before + @"(?" + Alternatives(Forms("contar")) + "|" + + PortugueseConverter.PronounRules.DizerVerbForms + ")" + + @"(?\s+)(?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(StringComparer.OrdinalIgnoreCase);
+            foreach (var (infinitive, english) in new[]
+            {
+                ("respeitar", @"respect(?:s|ed|ing)?"),
+                ("admirar", @"admir(?:e|es|ed|ing)"),
+                ("amar", @"lov(?:e|es|ed|ing)"),
+                ("odiar", @"hat(?:e|es|ed|ing)"),
+                ("apoiar", @"support(?:s|ed|ing)?"),
+                ("acompanhar", @"accompan(?:y|ies|ied|ying)"),
+                ("abraçar", @"hug(?:s|ged|ging)?"),
+                ("cumprimentar", @"greet(?:s|ed|ing)?"),
+                ("elogiar", @"prais(?:e|es|ed|ing)"),
+                ("convidar", @"invit(?:e|es|ed|ing)"),
+                ("proteger", @"protect(?:s|ed|ing)?"),
+                ("defender", @"defend(?:s|ed|ing)?"),
+                ("conhecer", @"know(?:s|n|ing)?|knew|meet|meets|meeting|met"),
+                ("reconhecer", @"recogni[sz](?:e|es|ed|ing)"),
+            })
+            {
+                if (!VerbConjugator.TryConjugate(infinitive, out var forms))
+                    throw new InvalidOperationException($"Cannot conjugate direct-object verb: {infinitive}");
+                foreach (var form in forms.Where((_, index) => index is not (34 or 35 or 36 or 37 or 38)))
+                    result[form] = (infinitive, english);
+            }
+            return result;
+        }
+
+        private static readonly Regex DirectLhe = new(
+            NotWordBefore + @"(?:(?lhe|lhes)(?\s+)(?" + Alternation(DirectLheForms.Keys) +
+            @")|(?" + Alternation(DirectLheForms.Keys) + @")-(?lhe|lhes))" +
+            @"(?-(?:ei|ás|á|emos|eis|ão|ia|ias|íamos|íeis|iam))?" +
+            @"(?![\p{L}\p{M}\d_-])", Ci);
+
+        internal static string CorrectDirectLhe(string text, ReferenceHint? reference, bool informal, Func blocked)
+        {
+            var english = reference?.EnglishText ?? "";
+            if (DirectLhe.Matches(text).Count != 1 ||
+                Regex.Matches(english, @"\b(?:you|him|her|them)\b", Ci).Count != 1)
+                return text;
+            return DirectLhe.Replace(text, m =>
+            {
+                var verb = m.Groups["v"].Value;
+                var sense = DirectLheForms[verb];
+                if (blocked(sense.Infinitive) || blocked(verb)) return m.Value;
+                var objectMatch = Regex.Match(english, @"\b(?:" + sense.English +
+                    @")\s+(?:(?:really|greatly|deeply|so\s+much|very\s+much|always|truly)\s+){0,2}" +
+                    @"(?you|him|her|them)\b", Ci);
+                if (!objectMatch.Success && sense.Infinitive == "respeitar")
+                    objectMatch = Regex.Match(english, @"\brespect\s+for\s+(?you|him|her|them)\b", Ci);
+                if (!objectMatch.Success) return m.Value;
+                var after = m.Result("$'");
+                if (!Regex.IsMatch(after,
+                    @"^(?:\s+(?:imensamente|muito|profundamente|verdadeiramente|realmente|sempre|ainda|também|já|só|bem|tanto|demasiado))*" +
+                    @"(?:\s*(?:[,.;:!?…""»”]|[\uE000-\uF8FF]|$)|\s+(?:como|por|desde|há|quando|porque|mas|e|que)\b)", Ci))
+                    return m.Value;
+                var person = objectMatch.Groups["object"].Value.ToLowerInvariant();
+                var plural = m.Groups["cl"].Value.Equals("lhes", StringComparison.OrdinalIgnoreCase);
+                if (person != "you" && plural != (person == "them")) return m.Value;
+                var pronoun = person switch
+                {
+                    "her" => "a", "them" => "os",
+                    "you" when informal => plural ? "vos" : "te",
+                    "you" when Regex.IsMatch(text, @"\b(?:senhora|sra|doutora|dra)\b", Ci) ||
+                        Regex.IsMatch(english, @"\b(?:ma['’]am|madam)\b", Ci) => plural ? "as" : "a",
+                    _ => plural ? "os" : "o",
+                };
+                verb += m.Groups["tail"].Value.Replace("-", "");
+                var before = m.Result("$`");
+                var proclisis = CompoundProclisis.IsMatch(before) || AfterSubordinateSubject(before);
+                var target = proclisis ? pronoun + " " + verb.ToLowerInvariant()
+                    : PortugueseVerbs.AttachMesoclitic(verb, pronoun) ?? PortugueseVerbs.AttachEnclitic(verb, pronoun);
+                var first = m.Groups["gap"].Success ? m.Groups["cl"].Value : m.Groups["v"].Value;
+                return CaseHelper.MatchCase(first, target);
+            });
+        }
+
         // "Para eles usarem você" / "para eles usarem-te" → "para eles te usarem": a preposition with its own subject attracts the clitic.
         private static readonly Regex PersonalInfinitiveObject = new(
             @"(?(? part[..^3] + "ar",
 
         public static string Apply(string s, ReferenceHint? reference = null)
         {
+            s = ExplicitGovernedReflexive.Replace(s, m =>
+            {
+                var modal = m.Groups["modal"].Value;
+                var infinitive = m.Groups["inf"].Value;
+                if ((!Modals.Contains(modal) && !Regex.IsMatch(modal, @"^(?:estou|estamos|estávamos|estais|podeis|quereis|deveis|ides)$", Ci)) ||
+                    Regex.IsMatch(modal, @"^(?:deix|mand|vejo|vemos|vi|vimos|ouço|ouvimos)", Ci) ||
+                    !PortugueseVerbs.IsKnownInfinitive(infinitive) && !NaturaDictionary.Load(true).Check(infinitive))
+                    return m.Value;
+                var clitic = m.Groups["subject"].Value.ToLowerInvariant() switch
+                {
+                    "eu" => "me", "nós" => "nos", _ => "vos",
+                };
+                return m.Groups["head"].Value + infinitive + "-" + CaseHelper.MatchCase(m.Groups["cl"].Value, clitic);
+            });
             s = ReplaceCompoundObjects(s, includeYou: false);
             s = ReplacePersonalInfinitiveObjects(s, includeYou: false);
             s = BarePossessive.Replace(s, AddArticle);
@@ -5049,11 +5318,28 @@ public static string Apply(string s, ReferenceHint? reference = null)
             s = ThankPerson.Replace(s, ReplaceThankPerson);
             s = StartPossessive.Replace(s, AddStartArticle);
             s = IntroductoryPossessive.Replace(s, AddIntroductoryArticle);
-            s = NosVemos.Replace(s, m => PortugueseVerbs.IsKnownVerbForm(m.Groups["v"].Value)
-                ? m.Groups["pre"].Value + CaseHelper.Capitalize(PortugueseVerbs.AttachEnclitic(m.Groups["v"].Value, "nos"))
+            s = NosVemos.Replace(s, m => PortugueseVerbs.IsFiniteCliticForm(m.Groups["v"].Value)
+                ? m.Groups["pre"].Value +
+                    CaseHelper.MatchCase(m.Groups["cl"].Value,
+                        PortugueseVerbs.AttachMesoclitic(m.Groups["v"].Value, "nos", requireTransitive: false) ??
+                        PortugueseVerbs.AttachEnclitic(m.Groups["v"].Value, "nos"))
                 : m.Value);
             s = JustCommand.Replace(s, ReplaceJustCommand);
             s = StartClitic.Replace(s, m => ReplaceLeading(m, capitalize: true));
+            if (Regex.IsMatch(reference?.EnglishText ?? "",
+                    @"^(?:(?:he|she|they|you|we|I|it)(?:['’](?:s|re|ve|d|ll))?\b|(?:did|does|do|has|have|had|was|were|is|are)\s+(?:he|she|they|you|we|I|it)\b)", Ci) &&
+                !Regex.IsMatch(reference?.EnglishText ?? "", @"\b(?:if|whether|unless)\b", Ci))
+                s = ReferenceLeadingSe.Replace(s, m =>
+                {
+                    var rest = Regex.Match(m.Result("$'"), @"^[^.!?…]*").Value;
+                    var continuation = m.Groups["pre"].Value.Contains('\n') &&
+                        !Regex.IsMatch(m.Result("$`"), @"[.!?…:;]\s*$") &&
+                        !Regex.IsMatch(m.Groups["pre"].Value, @"[-–—]");
+                    return continuation || rest.Contains(',') || rest.Contains(';') ||
+                        !ReflexiveInfinitive.IsMatch(m.Groups["v"].Value) ||
+                        Regex.IsMatch(m.Groups["v"].Value, @"(?:sse|sses|ssem|ssemos|sseis)$", Ci) ||
+                        !PortugueseVerbs.IsFiniteCliticForm(m.Groups["v"].Value) ? m.Value : ReplaceLeading(m, capitalize: true);
+                });
             s = ReflexiveCommand.Replace(s, ReplaceReflexiveCommand);
             s = StartFutureReflexive.Replace(s, m => PortugueseVerbs.IsFutureOrConditional(m.Groups["v"].Value) &&
                 PortugueseVerbs.AttachMesoclitic(m.Groups["v"].Value, "se", requireTransitive: false) != null
@@ -5084,6 +5370,30 @@ public static string Apply(string s, ReferenceHint? reference = null)
                 return converted == m.Value ? m.Value : converted + " " + m.Groups["np"].Value;
             });
             s = DativeRecipient.Replace(s, ReplaceDativeRecipient);
+            if (reference is { SecondPerson: true } &&
+                EnglishDativeYou.IsMatch(reference.EnglishText ?? "") &&
+                !Regex.IsMatch(reference.EnglishText ?? "", @"\b(?:himself|herself|itself|themselves|yourself|yourselves|" +
+                    @"him|her|them)\b|\byou\s+(?:and|or|alone|only)\b", Ci) &&
+                FormalDativeRecipient.Matches(s).Count == 1)
+                s = FormalDativeRecipient.Replace(s, ReplaceDativeRecipient);
+            s = AttachedReflexive.Replace(s, m =>
+            {
+                var verb = m.Groups["v"].Value;
+                var clitic = m.Groups["cl"].Value.ToLowerInvariant();
+                if (clitic == "nos" && verb.EndsWith("mo", StringComparison.OrdinalIgnoreCase) &&
+                    PortugueseVerbs.IsFiniteCliticForm(verb + "s"))
+                    verb += CaseHelper.IsAllUpper(verb) ? "S" : "s";
+                verb += m.Groups["tail"].Value.Replace("-", "");
+                if (!PortugueseVerbs.IsFiniteCliticForm(verb)) return m.Value;
+                var before = m.Result("$`");
+                var justCommand = Regex.IsMatch(before,
+                    @"(?:^|[.!?…,:]\s*|\n|[-–—]\s*)[\s\uE000-\uF8FF]*(?:só|apenas)\s+$", Ci);
+                if (!justCommand && (CompoundProclisis.IsMatch(before) || AfterSubordinateSubject(before)))
+                    return CaseHelper.IsAllUpper(verb) ? clitic.ToUpperInvariant() + " " + verb
+                        : clitic + " " + verb.ToLowerInvariant();
+                return PortugueseVerbs.AttachMesoclitic(verb, clitic, requireTransitive: false) ??
+                    PortugueseVerbs.AttachEnclitic(verb, clitic);
+            });
             s = InfinitiveProclisis.Replace(s, ReplaceInfinitiveProclisis);
             return s;
         }
@@ -5100,6 +5410,10 @@ private static string ReplaceLeading(Match m, bool capitalize)
             var verb = m.Groups["v"].Value;
             var clitic = m.Groups["cl"].Value;
             var pronoun = CaseHelper.IsAllUpper(clitic) && CaseHelper.IsAllUpper(verb) ? clitic : clitic.ToLowerInvariant();
+            if (capitalize && char.IsLower(clitic[0]) &&
+                (!NaturaDictionary.Load(true).Check(verb) ||
+                    Regex.IsMatch(verb, @"[aei]ndo$", Ci) && !PortugueseVerbs.IsFiniteCliticForm(verb)))
+                return m.Value;
             // "nos" is also "in the": only a finite verb form that cannot be a plural noun counts ("Nos diga", not "Nos casos").
             if (pronoun.Equals("nos", StringComparison.OrdinalIgnoreCase) &&
                 !(Regex.IsMatch(verb, @"(?:[ea]|em|am)$", Ci) && PortugueseVerbs.IsKnownVerbForm(verb)))
@@ -5119,7 +5433,8 @@ private static string ReplaceLeading(Match m, bool capitalize)
             }
 
             var pre = m.Groups["pre"].Value;
-            if (capitalize && !Regex.IsMatch(pre, @"(?:E|Mas|Então|Ah|Oh|Ok|Bem|Agora|Por favor),?\s+$"))
+            if (capitalize && char.IsUpper(clitic[0]) &&
+                !Regex.IsMatch(pre, @"(?:E|Mas|Então|Ah|Oh|Ok|Bem|Agora|Por favor),?\s+$"))
             {
                 attached = CaseHelper.Capitalize(attached);
             }
@@ -5153,6 +5468,9 @@ private static string ReplaceSubject(Match m, ReferenceHint? reference)
         {
             var verb = m.Groups["v"].Value;
             var clitic = m.Groups["cl"].Value;
+            if (AfterSubordinateSubject(m.Result("$`") + m.Groups["subj"].Value) ||
+                Regex.IsMatch(m.Result("$`") + m.Groups["pre"].Value, @"\bque\s+se\s+[^.!?;]*,\s*$", Ci))
+                return m.Value;
             var mesoclitic = PortugueseVerbs.AttachMesoclitic(verb, CaseHelper.IsAllUpper(verb) ? clitic : clitic.ToLowerInvariant(), requireTransitive: false);
             if (mesoclitic != null)
             {
@@ -5210,6 +5528,17 @@ private static string ReplaceNounSubject(Match m)
             var reflexive = clitic.Equals("se", StringComparison.OrdinalIgnoreCase);
             if (reflexive)
             {
+                if (Regex.IsMatch(m.Result("$`"),
+                    @"\b(?:à|ao|aos|às|de|do|da|dos|das|com|para|em|no|na|nos|nas|pelo|pela|pelos|pelas)" +
+                    @"(?:\s+(?:o|a|os|as|um|uma|uns|umas))?\s+$", Ci))
+                    return m.Value;
+                // After a predicate, se + imperfect subjunctive is a conditional, not an enclitic.
+                if (Regex.IsMatch(verb, @"(?:sse|sses|ssem|ssemos|sseis)$", Ci))
+                    return m.Value;
+                if (Regex.IsMatch(verb, @"^(?:é|era|foi|será|seria|for|seja|são|eram|foram|serão|seriam|forem|sejam)$", Ci) &&
+                    Regex.IsMatch(m.Result("$'"), @"^\s+(?:\p{L}+(?:ad[oa]s?|id[oa]s?)|abert[oa]s?|feit[oa]s?|dit[oa]s?|" +
+                        @"vist[oa]s?|post[oa]s?|pres[oa]s?|mort[oa]s?|escrit[oa]s?|pag[oa]s?|ganh[oa]s?|aceites?|entregues?)\b", Ci))
+                    return m.Value;
                 var subject = m.Groups["w"].Value;
                 var nominalDeterminer = Regex.IsMatch(m.Result("$`"), @"\b(?:o|a|os|as|um|uma|uns|umas)\s+$", Ci);
                 if (!nominalDeterminer && (PortugueseVerbs.IsKnownVerbForm(subject) ||
@@ -5222,6 +5551,7 @@ private static string ReplaceNounSubject(Match m)
                 var subject = m.Groups["w"].Value;
                 if (ClauseAgreementAnalysis.Candidates(verb).HasFlag(VerbCandidate.NominalHomograph) ||
                     PortugueseVerbs.IsKnownVerbForm(subject) || PortugueseVerbs.IsKnownInfinitive(subject) ||
+                    PortugueseVerbs.TrySplitFuture(subject, out _, out _, requireTransitive: false) ||
                     PortugueseVerbs.IsSecondPersonForm(subject) ||
                     Regex.IsMatch(subject, @"(?:ou|eu|iu)$", Ci) && PortugueseVerbs.TryGuessThirdToSecond(subject, out _) ||
                     Regex.IsMatch(subject, @"(?:aram|eram|iram)$", Ci) &&
@@ -5232,7 +5562,8 @@ private static string ReplaceNounSubject(Match m)
                     verb.EndsWith("o", StringComparison.OrdinalIgnoreCase))
                     return m.Value;
             }
-            var knownVerb = PortugueseVerbs.IsKnownVerbForm(verb) || PortugueseVerbs.ThirdToSecond.ContainsKey(verb.ToLowerInvariant());
+            var knownVerb = PortugueseVerbs.IsKnownVerbForm(verb) || PortugueseVerbs.ThirdToSecond.ContainsKey(verb.ToLowerInvariant()) ||
+                reflexive && PortugueseVerbs.IsFiniteCliticForm(verb);
             var validatedPast = (directObject || reflexive) && Regex.IsMatch(verb, @"(?:ou|eu|iu)$", Ci) &&
                 PortugueseVerbs.TryGuessThirdToSecond(verb, out _);
             if (InfinitiveAdjectives.Contains(m.Groups["w"].Value) && PortugueseVerbs.IsInfinitive(verb) && !PortugueseVerbs.IsFutureOrConditional(verb))
@@ -5246,6 +5577,8 @@ private static string ReplaceNounSubject(Match m)
             if (ProclisisAttractors.Contains(m.Groups["w"].Value) || PortugueseVerbs.IsInfinitive(verb) ||
                 infinitiveSubject.Success && PortugueseVerbs.IsInfinitive(infinitiveSubject.Groups["inf"].Value.ToLowerInvariant()) ||
                 AfterSubordinateSubject(m.Result("$`") + m.Groups["w"].Value) ||
+                reflexive && Regex.IsMatch(m.Result("$`") + m.Groups["w"].Value,
+                    @"\b(?:que|se|quando|porque|embora|caso|enquanto)\s+(?:\p{Lu}[\p{L}\p{M}]+\s+){0,2}\p{Lu}[\p{L}\p{M}]+$") ||
                 PortugueseVerbs.IsFutureOrConditional(verb) || !(knownVerb || validatedPast))
             {
                 return m.Value;
@@ -5330,10 +5663,14 @@ private static string ReplaceObject(Match m)
             "o|es|e|emos|em|a|as|am|i|este|eu|eram|ia|ias|íamos|iam|er|eres|ermos|erem|erá|erás|erei|eremos|erão|eria|erias|eríamos|eriam|esse|esses|êssemos|essem|endo";
 
         // Reviewed verbs with a neutral recipient: "Liga para ela" → "Liga-lhe", "Não contou a ela" → "Não lhe contou".
+        internal const string DizerVerbForms =
+            "(?:digo|dizes|diz|dizemos|dizem|diga|digas|digamos|digam|disse|disseste|dissemos|disseram|" +
+            "dizia|dizias|dizíamos|diziam|dizer|dizeres|dizermos|dizerem|direi|dirá|dirás|diremos|dirão|" +
+            "diria|dirias|diríamos|diriam|dissesse|dissesses|disséssemos|dissessem|disser|disseres|dissermos|disserem|dizendo)";
         internal const string DativeVerbForms =
             "(?:(?:lig|ligu|telefon|pergunt|cont|expli(?:c|qu)|mostr|mand|envi|ensin|pag|pagu|facilit)(?:" + ArEndings + ")|" +
             "(?:escrev|respond)(?:" + ErEndings + ")|" +
-            "digo|dizes|diz|dizemos|dizem|diga|digas|digam|disse|disseste|dissemos|disseram|dizia|dizias|diziam|dizer|dizeres|dizerem|direi|dirá|dirás|diremos|dirão|diria|dirias|diriam|dissesse|dissessem|dizendo|" +
+            DizerVerbForms + "|" +
             "peço|pedes|pede|pedimos|pedem|peça|peças|peçam|pedi|pediste|pediu|pediram|pedia|pedias|pediam|pedir|pedires|pedirem|pedirei|pedirá|pedirás|pediremos|pedirão|pediria|pedirias|pediriam|pedisse|pedissem|pedindo|" +
             "minto|mentes|mente|mentimos|mentem|minta|mintas|mintam|menti|mentiste|mentiu|mentiram|mentia|mentias|mentiam|mentir|mentires|mentirem|mentirei|mentirá|mentirás|mentiremos|mentirão|mentiria|mentiriam|mentisse|mentindo)";
 
@@ -5352,6 +5689,18 @@ private static string ReplaceObject(Match m)
             @"(?=\s+a\s))\s+(?para|a)\s+(?

" + DativePronouns + ")" + NotWordAfter + DativeEmphasis, Ci); + private static readonly Regex FormalDativeRecipient = new( + DativeTrigger + NotWordBefore + @"(?" + DativeVerbForms + @")\s+(?para|a)\s+(?

si)" + + NotWordAfter + DativeEmphasis + + @"(?!\s*,?\s*(?:(?:e|ou|mas|nem)(?:\s+não)?|não)\s+(?:(?:para|a)\s+)?si\b)", Ci); + private static readonly Regex EnglishDativeYou = new( + @"\b(?:(?:lie|lies|lied|lying|speak|speaks|spoke|speaking|talk|talks|talked|talking|" + + @"respond|responds|responded|responding|reply|replies|replied|replying|explain|explains|explained|explaining|" + + @"write|writes|wrote|written|writing)\s+to\s+you|" + + @"(?:tell|tells|told|telling|ask|asks|asked|asking|call|calls|called|calling|" + + @"phone|phones|phoned|phoning|pay|pays|paid|paying|send|sends|sent|sending|" + + @"show|shows|showed|shown|showing|teach|teaches|taught|teaching)\s+you)\b", Ci); + // A direct object can intervene: "Vou mostrar a casa a ele" → "Vou mostrar-lhe a casa". private const string DitransitiveVerbForms = "(?:(?:mostr|mand|envi|ensin|expli(?:c|qu)|cont|entreg|emprest)(?:" + ArEndings + ")|" + @@ -5427,7 +5776,7 @@ private static string ReplaceDativeRecipient(Match m) var clitic = m.Groups["p"].Value.ToLowerInvariant() switch { - "ele" or "ela" or "você" => "lhe", + "ele" or "ela" or "você" or "si" => "lhe", "eles" or "elas" => "lhes", "mim" => "me", "ti" => "te", diff --git a/se5/BrazilianToEuropeanPortuguese/PortugueseOrthography.cs b/se5/BrazilianToEuropeanPortuguese/PortugueseOrthography.cs index eea4591a..9deb83bd 100644 --- a/se5/BrazilianToEuropeanPortuguese/PortugueseOrthography.cs +++ b/se5/BrazilianToEuropeanPortuguese/PortugueseOrthography.cs @@ -59,6 +59,8 @@ internal static string Apply(string text, bool postAgreement) return Word.Replace(text, match => { var word = match.Value; + if (word.Equals(postAgreement ? "dêem" : "deem", StringComparison.OrdinalIgnoreCase)) + return CaseHelper.MatchCase(word, postAgreement ? "deem" : "dêem"); if (dictionary.Check(word)) return word; var candidates = Stems .Select(pair => (From: postAgreement ? pair.Before : pair.After, To: postAgreement ? pair.After : pair.Before)) diff --git a/se5/BrazilianToEuropeanPortuguese/PortugueseVerbs.cs b/se5/BrazilianToEuropeanPortuguese/PortugueseVerbs.cs index 18ae3362..97af912f 100644 --- a/se5/BrazilianToEuropeanPortuguese/PortugueseVerbs.cs +++ b/se5/BrazilianToEuropeanPortuguese/PortugueseVerbs.cs @@ -488,6 +488,43 @@ public static bool TryGuessThirdToSecond(string word, out string second) public static bool IsKnownVerbForm(string word) => AllForms.Contains(word.ToLowerInvariant()); + private static readonly (string Ending, string InfinitiveEnding)[] FiniteEndings = new[] + { + ("ar", new[] { "ávamos", "ássemos", "áramos", "ámos", "amos", "avam", "aram", "ava", "avas", "aste", "asse", "asses", "assem", "ara", "aras", "ou", "ei", "em", "es", "am", "as", "a", "o", "e" }), + ("er", new[] { "íamos", "êssemos", "êramos", "emos", "iam", "eram", "ia", "ias", "este", "esse", "esses", "essem", "era", "eras", "eu", "i", "am", "as", "em", "es", "a", "e", "o" }), + ("ir", new[] { "íamos", "íssemos", "íramos", "imos", "iam", "iram", "ia", "ias", "iste", "isse", "isses", "issem", "ira", "iras", "iu", "i", "am", "as", "em", "es", "a", "e", "o" }), + }.SelectMany(group => group.Item2.Select(ending => (ending, group.Item1))) + .OrderByDescending(pair => pair.ending.Length).ToArray(); + + internal static bool IsFiniteCliticForm(string word) + { + var lower = word.ToLowerInvariant(); + if (lower.Length < 2 || !lower.All(char.IsLetter) || GuessStopWords.Contains(lower) || IsInfinitive(lower)) + return false; + if (IsKnownVerbForm(lower) || IsSecondPersonForm(lower)) return true; + if (Regex.IsMatch(lower, @"[ai]d[oa]s?$")) return false; + if (TrySplitFuture(lower, out _, out _, requireTransitive: false)) return true; + var dictionary = NaturaDictionary.Load(true); + if (!dictionary.Check(lower)) return false; + foreach (var (ending, infinitiveEnding) in FiniteEndings) + { + if (lower.Length <= ending.Length + 1 || !lower.EndsWith(ending, StringComparison.Ordinal)) continue; + var stem = lower[..^ending.Length]; + var stems = new List { stem }; + if (stem.EndsWith("qu", StringComparison.Ordinal)) stems.Add(stem[..^2] + "c"); + if (stem.EndsWith("gu", StringComparison.Ordinal)) stems.Add(stem[..^1]); + if (stem.EndsWith('c')) stems.Add(stem[..^1] + "ç"); + if (stem.EndsWith('ç')) stems.Add(stem[..^1] + "c"); + if (stem.EndsWith('j')) stems.Add(stem[..^1] + "g"); + foreach (var candidate in stems.Select(s => s + infinitiveEnding)) + if (dictionary.Check(candidate) && VerbConjugator.TryConjugate(candidate, out var forms) && + forms.Where((_, index) => index is not (0 or 31 or 32 or 33 or 34 or 35 or 36 or 37 or 38)) + .Contains(lower, StringComparer.Ordinal)) + return true; + } + return false; + } + ///

Looks like an infinitive: ends in -ar/-er/-ir/-por/-pôr and is not a common noun/adjective. public static bool IsInfinitive(string word) { @@ -535,7 +572,7 @@ public static bool IsFutureOrConditional(string word) private static readonly HashSet ExtraMesoclisisInfinitives = new(StringComparer.Ordinal) { "manter", "dizer", "contar", "informar", "pagar", "contactar", "telefonar", "devolver", "mandar", "enviar", "dar", - "levar", "buscar", "ensinar", "lembrar", "agradecer", "explicar", "mostrar", "arranjar", "apanhar", "seguir", + "levar", "buscar", "ensinar", "lembrar", "agradecer", "explicar", "mostrar", "arranjar", "apanhar", "seguir", "mentir", }; // Only real infinitives: "queriam" (imperfect of querer) must not split into "quer" + "iam". @@ -650,7 +687,10 @@ public static bool TrySplitFuture(string word, out string stem, out string endin var root = w[..^e.Length]; var known = root is "dir" or "far" or "trar" || TransitiveInfinitives.Contains(root) || AdditionalObjectInfinitives.Contains(root) || ExtraMesoclisisInfinitives.Contains(root) || - (!requireTransitive && Infinitives.Contains(root)); + (!requireTransitive && (Infinitives.Contains(root) || + !AllForms.Contains(w) && !AllForms.Contains(root) && + IsInfinitive(root) && NaturaDictionary.Load(true).Check(root) && + NaturaDictionary.Load(true).Check(w))); if (!known) { continue; diff --git a/se5/BrazilianToEuropeanPortuguese/PortugueseWordList.xml b/se5/BrazilianToEuropeanPortuguese/PortugueseWordList.xml index b8540fb5..274b5d5f 100644 --- a/se5/BrazilianToEuropeanPortuguese/PortugueseWordList.xml +++ b/se5/BrazilianToEuropeanPortuguese/PortugueseWordList.xml @@ -368,9 +368,6 @@ - - - @@ -451,6 +448,9 @@ + + + @@ -1012,6 +1012,8 @@ + + @@ -1132,6 +1134,10 @@ + + + + diff --git a/se5/BrazilianToEuropeanPortuguese/README.md b/se5/BrazilianToEuropeanPortuguese/README.md index 08b7fb7b..c2000209 100644 --- a/se5/BrazilianToEuropeanPortuguese/README.md +++ b/se5/BrazilianToEuropeanPortuguese/README.md @@ -431,6 +431,467 @@ These rules change Portuguese constructions, rather than merely adding review wa labels the harmful sense Brazilian; [lixar](https://dicionario.priberam.org/pt-pt/lixar) includes both *prejudicar/tramar* and *estragar*. *Lixar* preserves the informal register rather than automatically substituting neutral *estragar*. +- **Physical break-in:** Brazilian *lacear* uses *ceder* when something gives + with use (*Elas vão lacear* → *Elas vão ceder*), *alargar* with a direct object + (*lacear os sapatos* → *alargar os sapatos*) and *amolecer* for reviewed + leather/material softening contexts. Generated forms retain tense/person, + participles and supported clitics. Decorating with bows/ribbons is protected. + Uses **Vocabulary** and `reviewed-verb-senses`, respecting local/ignored entries. + See Priberam for [ceder](https://dicionario.priberam.org/pt-pt/ceder), + [alargar](https://dicionario.priberam.org/pt-pt/alargar) and + [amolecer](https://dicionario.priberam.org/pt-pt/amolecer), and the Brazilian + [lacear senses](https://www.dicio.com.br/lacear/). +- **Bare police personnel and doorways:** existential *há/havia/houve/haverá/ + haveria/haja/houvesse/houver policiais* uses *polícias*, alongside existing + quantified personnel rules. Adjectives in *operações/carros policiais* stay + unchanged; English *officers* can still select *agentes*, while *cops* retains + *polícias*. Uses **Vocabulary**/`police-nouns`. + Bare plural personnel after *para/pra, com, sem, entre, perante, por, a* + and *em* are also covered when punctuation, a reviewed personnel + qualifier or a relative/coordination confirms the noun phrase: + *Não para policiais. Para policiais, …* → *Não para polícias. Para + polícias, …*. The earlier existential/quantifier rules did not cover + this prepositional pattern. This is a PT-PT vocabulary preference, not + a claim that every occurrence of *policial* is ungrammatical. + Sources: Priberam [polícia](https://dicionario.priberam.org/pt-pt/pol%C3%ADcia) + and [policial](https://dicionario.priberam.org/pt-pt/policial). + Possessive *na/nas minha(s), tua(s), sua(s), nossa(s), vossa(s) porta(s)* + becomes *à/às … porta(s)* for reviewed human-presence constructions or + English-confirmed *at … door*: *Porque há policiais na minha porta?* → + *Porque há polícias à minha porta?*. Reviewed surface uses (blood, notices, + keys, paint, etc.) and unresolved locations are protected. Uses + **Colloquial**/`construction-vocabulary`; original line breaks are retained. +- **Polite plural requests:** *Por favor, avisar se avistarem o suspeito* + becomes *Por favor, avisem se avistarem o suspeito*. A reviewed request + infinitive uses the plural imperative when an immediately following + *se/quando/assim que/logo que* clause identifies the same implicit plural + addressees. Covers regular and reviewed irregular commands, short reviewed + recipient complements, negation and personal clitics. + Singular/ambiguous conditionals, explicit third-person conditional subjects, + impersonal/existential conditions and ordinary infinitive instructions + remain unchanged. No English reference is required; valid *avistar* is + retained rather than obligatorily replaced by *ver*. + Uses **Colloquial**, **Pronouns** and `polite-infinitive-commands`. + Source: [Ciberdúvidas on polite imperatives and infinitives](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/por-favor--imperativo-formal/34520). +- **Formal dative recipients:** English-confirmed *He lied to you* selects + *Ele mentiu-lhe*, not *Ele mentiu para si*. The existing reviewed recipient + verbs also handle formal *a/para si* with their usual clitic placement: + *não lhe mentiu*, *mentir-lhe-á*, *escreveu-lhe*, *telefonou-lhe*. + Informal address still uses *te*. Unconfirmed *si*, genuine self-reference, + emphasis, coordination and multiple ambiguous recipients are protected. + Requires an aligned English recipient and **Pronouns**/`pronouns`; + this does not globally replace *para si*. +- **Direct objects are not datives:** reviewed personal-object senses of + *respeitar, admirar, amar, odiar, apoiar, acompanhar, abraçar, cumprimentar, + elogiar, convidar, proteger, defender, conhecer* and *reconhecer* correct + Brazilian object *lhe/lhes* when a single aligned English object confirms + the role. *Eu lhe respeito imensamente* becomes formal *Eu respeito-o + imensamente* or informal *Eu respeito-te imensamente*. Explicit feminine + address uses *a*; English *him/her/them* preserves the third-person object + (unmarked plural defaults to *os*). Without gender evidence, formal *you* + uses the conventional masculine object, not an inferred identity. + Negation/subordination, enclisis and mesoclisis use the shared helpers. + Possessive datives (*admiro-lhe a coragem*), actual indirect objects, + ambiguous recipients and local exceptions are protected. + Uses **Pronouns**/`pronouns`; informal objects also require **YouToTu**/`address`. + Source: [Priberam: respeitar](https://dicionario.priberam.org/pt-pt/respeitar). +- **Collective subjects and greetings:** address shortcuts now check the + subject across subtitle line breaks before changing bare questions, + *precisa/consegue/merece*, *sabe como* and negative commands. + *Toda a equipa / precisa de uma mascote* keeps third-person *precisa*: + the verb agrees with *equipa*, not with the object *mascote*. Reviewed + *todo/toda + collective noun* expressions also receive the definite + article (*toda equipa* → *toda a equipa*, *todo grupo* → *todo o grupo*), + under **Colloquial**/`collective-articles`. This is a bounded PT-PT + wording preference, not a rewrite of every universal quantifier. + Sentence punctuation and dialogue dashes still permit a new addressee. + Valid *ei* (greeting or call) is no longer globally replaced by *olha*, + independently of whether English is available; actual *olha* is retained. + With an aligned English *Hey/Hi/Hello, Name, how … you …?*, a reviewed + named greeting can use informal *como vais/estás/andas/tens estado?*. + Requires **YouToTu**/`address`; formal settings/titles, different names, + plural recipients and *como vai o trabalho?* remain protected. + A greeting is evidence of conversational familiarity, not proof that + every following cue must use *tu*, and never overrides explicit formal + address. Local ignored phrases remain authoritative. + Sources: [Ciberdúvidas: equipa agreement](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/concordancia-a-equipa-da-empresa-y-o-nosso-colega-z/37613), + [todo and articles](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/uso-do-artigo-definido-com-o-quantificador-todo/28407) + and [Priberam: ei](https://dicionario.priberam.org/pt-pt/ei). +- **Missing recipients in dar requests:** a single aligned English + *give/grant me/us* request can recover omitted *-me/-nos* in a bounded + Portuguese imperative asking for seconds, minutes, a moment, time, + a break, a favour, a chance, help or attention. + *Rapazes, dêem um segundo, certo?* becomes + *Rapazes, deem-me um segundo, certo?* with post-agreement spelling, + or *dêem-me* with pre-agreement spelling. The plural *deem/dêem* is + valid for both informal and formal groups; it must not become singular + *dá*. Singular/plural conjugation and existing orthography options are + preserved; dar's *deem/dêem* spelling now follows that option even when + the dictionary accepts both. Uses **Pronouns**/`give-request-recipient`, + independently of informal address. It requires a matching object + category and does not invent a recipient without English or overwrite + existing clitics, explicit recipients, negation, subordinate clauses, + competing requests or local ignored phrases. + Source: [Ciberdúvidas: dar imperatives and clitics](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/da-ma-da-mo/25033) + (the article uses pre-agreement *dêem*). +- **Detached conversational tags:** *sabe?*, *entende/percebe/compreende?* + and reviewed *viu/ouviu/entendeu/percebeu?* tags agree with an established + informal addressee (*sabes?*, *percebes?*, *ouviste?*, etc.). + The evidence may be in the current cue or the converted immediately + preceding continuation: *Ouve, isto nunca vai ficar mais fácil, / mas + temos de começar a caminhar, sabe?* ends in *sabes?*. + The clause subject *nós* still controls *temos*; the detached tag addresses + *tu*. Requires **YouToTu**, `address` and `discourse-tags`. + Previous-cue evidence requires a continuing sentence, a gap of at most + 1.5 seconds, compatible speakers/register and no partial selection. + If aligned English exists, its detached *you know/understand/see…?* + must confirm the tag. Literal questions, named subjects, plural English + tags, formal settings/titles, dialogue boundaries and local ignored + phrases are protected. MiniLM meaning similarity alone cannot establish + grammatical person or guarantee a mixed-register warning. + Sources: [Ciberdúvidas: address and verbal agreement](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/pronomes-pessoais-brasil/19843) + and [discourse markers](https://ciberduvidas.iscte-iul.pt/pergunta.php?id=17970). +- **Topic relatives and reference-confirmed conditionals:** an explicit + reviewed antecedent (*algo, nada, uma coisa*, or a short list of topic + nouns) followed by *sobre o que* and a form of *falar* can use the simpler + relative complement *de que*: *havia algo / sobre o que esperava falar + contigo* → *havia algo / de que esperava falar contigo*. + A narrowly checked immediate continuation can recover that antecedent + without enabling broader sentence rewriting (same speaker, at most + 1.5 seconds apart, no sentence/dialogue boundary or partial selection). + **Colloquial**/`topic-relatives` controls this wording preference. + **SentenceContext**/`sentence-context` additionally enables whole-sentence + conversion and MiniLM review across safely joined cues. + It does not remove *esperava*, invent an infinitive, or + rewrite questions, free relatives (*falou sobre o que aconteceu*), + other lexical verbs, or already valid *sobre o qual* constructions. + A single aligned English *would* or unambiguous *'d + reviewed base verb* + can repair a dictionary-validated Portuguese future/conditional with an + explicit matching pronominal subject: *ela falarei* → *ela falaria*, + *eles farão* → *eles fariam*. This is `reference-conditional`, independent + of informal address. It preserves negation and does not infer a subject, + turn every future into a conditional, or resolve competing modals, + ambiguous *'d* past-perfect readings, or mesoclitics. + Without sufficient evidence, wrong future/conditional person is + review-only. Grammar review also checks the joined relative construction + when its automatic wording rule is disabled. + Sources: [Ciberdúvidas: relative prepositions](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/ainda-a-diferenca-entre-que-e-o-qual/28803), + [Ciberdúvidas: falar complements](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/dizer--falar/7465) + and [Ciberdúvidas: conditional tense](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/condicional-simples-e-condicional-composto/38090). +- **Omitted subjects in questions and complement mood:** initial English + auxiliary questions (*Did/Have/Were/Can you…*) now confirm the omitted + addressee, just like *You…*. With informal address, *Já viveu…?* becomes + *Já viveste…?* and reviewed pronominal *Já se envolveu…* becomes + *Já te envolveste…*. Without an attractor, the existing placement rules + produce *Envolveste-te…*. Past forms reuse dictionary-backed conjugation; + formal settings, explicit other subjects, plural addressees and relative + subjects remain protected. Uses **YouToTu**/`address`; English *you* confirms + the subject, not a mandatory informal register. + Present forms after reviewed *recear, temer, desejar* and affirmative + *duvidar + que* use the present subjunctive: *Receio que estás preso* → + *Receio que estejas preso*; formal *esteja*, third-person *ele esteja*. + Reuses the existing mood helper and attracts attached clitics before the + verb. Negated hosts, nominal homographs, past/future forms and unresolved + subjects are not freely rewritten. Uses `complement-subjunctive`, + independently of the address setting; this is not a blanket rule for + every *que* clause. *Roubo de carro* and *roubo de carros* are both + possible; this rule does not invent the number of stolen vehicles. + Sources: [Ciberdúvidas: address agreement](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/pronomes-pessoais-brasil/19843), + [subjunctive complements](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/o-conjuntivo-em-subordinadas-completivas/21317) + and [proclisis attractors](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/os-casos-especificos-da-proclise/10650). +- **Reflexive, reciprocal and inherent pronouns:** shared placement is not a + blanket verb replacement or an exhaustive lexical list. It covers existing + *me/te/se/nos/vos*, including quotations and lowercase subtitle fragments, + with dictionary-validated regular finite forms beyond the small verb tables. + Examples include *lavar-se, pentear-se, barbear-se, deitar-se, levantar-se, + sentar-se, afastar-se, aproximar-se, preparar-se, apresentar-se, lembrar-se, + esquecer-se, queixar-se, arrepender-se, esforçar-se, atrever-se, resignar-se, + reconciliar-se, preocupar-se, interessar-se, importar-se, comportar-se, + concentrar-se, habituar-se, adaptar-se, recusar-se, dedicar-se, enganar-se, + magoar-se, proteger-se, defender-se, recolher-se* and *esconder-se*. + *"Nos conhecemos há muito tempo"* becomes *"Conhecemo-nos há muito tempo"*: + the final *s* of *-mos* drops before enclitic *nos*. *Há muito tempo* is + valid European Portuguese; *imenso* is not forced as an intensifier. + Future/conditional forms use mesoclisis (*conhecer-nos-emos*); negatives + and subordinate attractors retain proclisis (*não nos conhecemos*, + *disse que nos conhecemos*). Explicit *eu/nós/vós* controllers correct + *quero vestir-se* → *quero vestir-me*, *podemos esquecer-se* → + *podemos esquecer-nos*. Existing attached pronouns are also normalized: + *conhecemos-nos* → *conhecemo-nos*, *não lava-se* → *não se lava*, + *resignarei-me* → *resignar-me-ei*. Ambiguous initial *só/apenas* commands + retain their existing placement. Leading *se* statements require a reviewed + pronominal stem and unambiguous English subject evidence; conditional *se*, + prepositional *nos*, passives and + non-reflexive meanings are not indiscriminately reinterpreted. + Uses **Pronouns**/`pronouns`, without inserting missing pronouns based + solely on a verb's presence in a list or requiring a model. + Sources: [Ciberdúvidas: pronominal verb types](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/verbos-pronominais-pronomes-reflexos-pronomes-inerentes/37446), + [Conhecemo-nos](https://portuguesaletra.com/duvidas/conhecemo-nos-ou-conhecemos-nos/). +- **English-confirmed praise idioms:** with one matching personal English + predicate and one reviewed literal Portuguese candidate, *Sim, verdadeiro, + sim* aligned with *Yeah, he's the real deal, yeah* becomes *Sim, ele é mesmo + bom, sim*. Explicit *ele/ela/eles/elas* retain gender, number and present/ + imperfect tense. *The genuine article* and *the real McCoy* select + *autêntico/autêntica/autênticos/autênticas*; explicit fake/impostor + contrasts also retain authenticity rather than inventing skill. + English *know one's stuff* corrects literal *conhecer/saber as suas coisas* + to *saber o que faz*, preserving the Portuguese person, tense and emphasis: + *Estes tipos conhecem mesmo suas coisas* → *Estes tipos sabem mesmo o que + fazem*. Possessive mismatch, belongings, multiple candidates, mismatching + personal subjects and unrelated/literal senses are protected. + Does not retranslate already natural Portuguese or insert a missing + English sentence; English *the real deal* about information is not praise + of a person. Requires **Vocabulary**, **Colloquial** and + `english-praise-idioms`; ignored phrases and local vocabulary choices win. + Sources: Cambridge [the real deal](https://dictionary.cambridge.org/dictionary/english/real-deal), + [the genuine article](https://dictionary.cambridge.org/dictionary/english/genuine-article) + and [know your stuff](https://dictionary.cambridge.org/dictionary/learner-english/know-your-stuff). +- **Speech content:** Brazilian *falar algo/alguma coisa/nada/a verdade* uses + *dizer*: *falou algo?* → *disse alguma coisa?*, informal *disseste alguma + coisa?*. Tense/person, compound tenses and recipient clitics are preserved. + English-confirmed *say/tell* also covers content questions, reported + statements and quotations. Speaking languages, speaking with someone, + discussing topics and ambiguous *falamos* without tense evidence remain + protected. Uses **Vocabulary**/`reviewed-verb-senses`, respecting local + exceptions; no model is required. + Sources: [European Portuguese falar/dizer/contar](https://portuguesepedia.com/falar-dizer-contar/), + [Priberam: dizer](https://dicionario.priberam.org/pt-pt/dizer). +- **Perfect scores:** Brazilian informal *gabaritar* → *acertar em tudo*, + retaining tense/person: *Na verdade, gabaritei* → *Na verdade, acertei em + tudo*. Reviewed exam objects get the appropriate preposition (*gabaritou o + exame* → *acertou em tudo no exame*); question objects use *acertou todas + as perguntas*. Exam passives use *resolvido sem erros*. Explicit exam + answer keys (*o gabarito da prova*) become *as soluções da prova*. + Qualification senses, physical templates, unresolved complements and local + exceptions remain protected. English *nailed it* alone is not treated as + proof of a maximum score: this rule converts the Brazilian *gabaritar*. + Uses **Vocabulary**/`reviewed-verb-senses`, without requiring a model. + Source: [Priberam: gabaritar](https://dicionario.priberam.org/pt-pt/gabaritar). +- **Romantic partners:** aligned English *girlfriend/boyfriend* selects + *namorada/namorado* instead of generic *miúda/rapariga/rapaz*, covering + *garota/garoto*, the reviewed young-person synonyms and plurals. For example, + *Por favor, minha garota. Ela precisa de ajuda.* becomes *Por favor, minha + namorada. Ela precisa de ajuda.* No English means no inferred relationship. + Competing people of the same gender, mismatched numbers and multiple + possible referents are left unresolved rather than globally replacing nouns. + Local overrides/ignores and **Vocabulary**/`verb-vocabulary` are respected. + Source: [Priberam: namorada](https://dicionario.priberam.org/pt-pt/namorada). +- **Reviewed dialogue:** *coletiva/colectiva de imprensa* → *conferência de + imprensa* (including plurals), without English; *press conference/presser* + also disambiguates bare *coletiva*. English depreciative *this/that broad* + selects *esta/essa tipa*, preserving honorific and ownership *dona*. + Uses **Vocabulary**/`dialogue-vocabulary`. English-confirmed *duramente + aprendido* → *aprendido a duras penas*, retaining gender/number; elliptical + *Doing business here or what?* → *Vamos fazer negócio ou quê?*. Numeric + quoted prices use *custa/custam* only with English *will cost you* and a + monetary amount; explicit future/conditional context in this cue or the + preceding Portuguese cue is protected. These idioms also require + **Colloquial**/`dialogue-idioms`. Local exceptions remain authoritative. + English *remind you that* restores *lembrar-te que* (formal *lembrar-lhe*); + English *owe* permits neutral *Deves isso a ela* → *Deves-lhe isso* while + preserving explicit contrast (*a ela, não a ele*). + Uses **Pronouns**/`dialogue-objects`, retaining the configured address. + English-confirmed *push/throw/kick you out* also keeps an informal object + consistent: *Vais deixar esta tipa pôr-te para fora?*, not *pô-lo*. + A standalone named summons (*Sra. Silva?*) no longer makes earlier dialogue + formal across a recent different untitled vocative; its own and following + formal dialogue remain protected. First names do not universally imply *tu*. + Grammar guards preserve conditional *se foi roubado*, articles after future + verbs (*Daremos a volta*), named subjects (*Como está o João?*) and + article-free *por minha causa*. + Embedded English contractions (*Doc says you're gonna be okay*) now supply + subject evidence: *Os médicos disseram que ficarás bem*. Reviewed reporting + clauses also distinguish the reporter (*She said*) from the addressee + (*you'll*), while preserving explicit third-person and relative subjects. + Progressive reflexives follow the agreed subject: *estás a enganar-te*, + not *estás a enganar-se*. + English-confirmed *Would/Do you mind …ing?* repairs reflexive requests: + *Se importa responder algumas perguntas?* → *Importas-te de responder a + algumas perguntas?* (formal *Importa-se de …*). Covers reviewed finite + forms, dictionary-validated infinitives, negation, markup and line breaks. + Uses **Pronouns**/`dialogue-objects`; informal agreement also requires + **YouToTu**/`address`. *Que engraçado* alone does not establish a register. + Sources: [Infopédia: coletiva](https://www.infopedia.pt/dicionarios/lingua-portuguesa/coletiva), + [Priberam: dona](https://dicionario.priberam.org/pt-pt/dona), + [Priberam: tipa](https://dicionario.priberam.org/pt-pt/tipa), + [Cambridge: hard-earned](https://dictionary.cambridge.org/dictionary/english-portuguese/hard-earned), + [Ciberdúvidas: lembrar](https://ciberduvidas.iscte-iul.pt/pergunta.php?id=3657). +- **Following an explanation and continuing address:** English-confirmed + *stay with me / follow me / bear with me* in a reasoning context restores + the missing object in a bare *acompanhe*: *Sim, mas acompanhe, porque esse + é o pulo* → *Sim, mas acompanhe-me, porque este é o salto*. + Existing address rules retain or select *acompanha-me/acompanhe-me/ + acompanhem-me* as appropriate; this rule does not impose a new register. + English *leap* uses *salto* in these reviewed reasoning constructions, + not a blanket substitution of every *pulo*. Physical jumps, leaps of faith, + complete objects and negated commands do not acquire an invented *-me*. + Uses **Colloquial**/`explanation-constructions`; object restoration also + requires **Pronouns**, and *pulo* conversion also requires **Vocabulary**. + Local exceptions remain authoritative. + The **YouToTu**/`address` stage now retains a reviewed addressee across a + short unfinished cue boundary: *E se você meteu na sua cabeça / que teria + sua vingança?* → *E se meteste na tua cabeça / que terias a tua vingança?*. + Covers reviewed forms of *meter na cabeça* and, with explicit English + second-person subject evidence, reviewed reporting/thinking predicates. + Works with **SentenceContext** on or off. Formal address, changed speakers, + dialogue dashes, gaps over 1.5 seconds, complete preceding sentences, + explicit third-person English subjects, postposed subjects and ambiguous + impersonal *se* do not inherit this agreement. Unresolved *você pensa / + que teria…* without English evidence is not assumed to share its subject. + MiniLM is a meaning-similarity check, not a grammatical agreement corrector. + Sources: Priberam [acompanhar](https://dicionario.priberam.org/pt-pt/acompanhar), + [salto](https://dicionario.priberam.org/pt-pt/salto), and + [Ciberdúvidas on tu as subject and verbal agreement](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/null/34507). +- **Hypothetical reactions:** *Eu também ficaria muito zangada se fosse ele* + → *Eu também ficaria muito zangada se fosse comigo*. Reviewed emotional + reactions with conditional *ficar/estar* can express the hypothetical + situation happening to the person reacting: *comigo, contigo, connosco, + com ele/ela/eles/elas/vocês*. Supports preceding or following *se* clauses, + explicit matching subjects, unambiguous conditional forms and English-backed + omitted first-person subjects. Identity predicates, advice, changed inner + subjects and unresolved persons are preserved. This is a contextual stylistic + reformulation, not a claim that *se eu fosse ele* is invalid in PT-PT. + Uses **Colloquial**, **Pronouns** and `hypothetical-reactions`. + Independently, *se + imperfect subjunctive* after a predicate is protected + from the noun-subject clitic rule: *zangada se fosse* must never become + *zangada fosse-se*. This safeguard remains active when the stylistic rule + is disabled. + Sources: [Ciberdúvidas on legitimate hypothetical identity constructions](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/se-eu-fosse-tu-e-se-eu-fosse-a-ti/20017) + and [Priberam comigo](https://dicionario.priberam.org/pt-pt/comigo). +- **Shaking vocabulary:** *chacoalhar* uses *abanar* for general shaking, + *agitar* for reviewed liquids/containers, *sacudir* for reviewed clothing, + dust and body movements, and *chocalhar* for reviewed rattling objects + (keys, coins, bells, rattles and bearings). Existing PT-PT *chocalhar* + remains unchanged; it is also valid for liquids in a container. + English-confirmed insulting, scolding, pestering and emotional-shock senses + use *insultar*, *repreender*, *incomodar* and *abalar*, rather than a physical + shaking verb. Reviewed passive subjects also steer clothing/container senses. + Covers finite forms (including vós), infinitives, gerunds, participles, + enclisis and mesoclisis; *sacudir* has reviewed irregular *sacodes/sacode/sacodem*. + *Chacoalhamos* normally uses the unaccented target; English past evidence can + select *abanámos/agitámos/chocalhámos*. *Chacoalhão/chacoalhões* → + *abanão/abanões*; nominal *uma chacoalhada* → *uma sacudidela*. + Uses **Vocabulary**/`shaking-vocabulary` (the nouns *chacoalhão/chacoalhões* + use the word list); local overrides and ignored vocabulary remain authoritative. + Other reviewed gaps use the existing conjugated `verb-vocabulary` stage: + *pentelhar* → *chatear*; *xeretar* → *bisbilhotar* only with reviewed + things/drawers/secrets/life/affairs/case complements; *futucar* → *remexer* + only with reviewed drawers/bags/pockets/things/cupboards. Unresolved + flattery, memories, bodily touching, wrinkling and other senses are not + replaced by these contextual rules. This is a bounded vocabulary review, + not a claim that every missing Brazilian verb has been identified. + Sources: Priberam [chacoalhar](https://dicionario.priberam.org/pt-pt/chacoalhar), + [chocalhar](https://dicionario.priberam.org/pt-pt/chocalhar), + [sacudir conjugation](https://dicionario.priberam.org/pt-pt/conjugar/sacudir), + [pentelhar](https://dicionario.priberam.org/pt-pt/pentelhar), + [xeretar](https://dicionario.priberam.org/pt-pt/xeretar) and + [futucar](https://dicionario.priberam.org/pt-pt/futucar). +- **Waiting for people and arrival apologies:** reviewed personal complements + use *por*: *esperar o Jamie* → *esperar pelo Jamie*; *esperar os meus amigos* + → *esperar pelos meus amigos*. Covers regular forms of *esperar*, reviewed + family/professional nouns and capitalized names. Bare names use the saved + article choice, reviewed name lists or *por* with unresolved-name reporting; + no gender is guessed for an unknown name. Clausal *espero que…*, inanimate + complements and infinitival *esperar o Jamie chegar* are preserved. + Both *esperar alguém* and *esperar por alguém* are grammatical PT-PT; + this is a waiting-style preference, not a blanket grammatical correction. + Uses **Colloquial**, **Pronouns** and `waiting-for-people`. + A brief arrival apology can become *desculpem/perdoem* when the English + identifies the arriving speaker by name, a nearby collective says it is + waiting for that same person, and an intervening cue asks about them. + This bounded inference requires aligned English and emits a recipient-review + note; singular vocatives and distant scenes do not qualify. Uses + **Colloquial**, **Pronouns** and `arrival-group-apology`. + *Alguém* is grammatically singular and never establishes plural recipients + on its own. MiniLM checks meaning similarity, not recipient agreement, and + is not a generative grammar corrector. + Source: [Ciberdúvidas on esperar alguém / esperar por alguém](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/esperar-alguem-e-esperar-por-alguem/39183). +- **Vehicle registration plates:** contextual *placa/placas* uses + *matrícula/matrículas*, not only the former fixed *placa do carro* phrase. + Covers reviewed vehicle complements, plate numbers, cloned/out-of-state + plates, English *license/licence/number/registration plate*, vehicle-qualified + *plates*, police lookup phrases and *registration*. A short continuation + can reuse vehicle context from the preceding cue. Case, number, markup and + line breaks are retained. Technical, medical, geological and sign/board + uses remain *placa*; unresolved *placa* and *sem placa* are no longer blanket + substitutions. Explicit *placa de matrícula* still uses *chapa de matrícula*. + Uses **Vocabulary**/`vehicle-plates`; local overrides and ignored phrases win. + Sources: Priberam [matrícula](https://dicionario.priberam.org/pt-pt/matr%C3%ADcula) + and [placa](https://dicionario.priberam.org/pt-pt/placa). *Placa* is also + dictionary-attested for vehicles; *matrícula* is the selected PT-PT output. +- **Annoyance slang:** reviewed indefinite *um/uma/uns/umas pé(s) no saco* + (also hyphenated) uses *chato/chata/chatos/chatas* for people, *uma chatice* + for reviewed impersonal subjects and *um incómodo* when unresolved. + Example: *um pé no saco da polícia* → *um chato da polícia*. + This interprets the Brazilian phrase, not English *dick*, which may also + mean *detective*. No blanket English insult mapping is added. + Conjugated *encher/torrar (o) saco* becomes *chatear* with reviewed personal + clitics or *meu/teu/nosso/vosso*, stop-command constructions or English + annoyance evidence: *Ele me encheu o saco* → *Ele chateou-me*; + *Para de torrar meu saco* → *Para de me chatear*. + Literal sack contents, filling/roasting evidence, financial uses and + unresolved future/conditional placement are protected. Existing + *que saco* → *que seca*, *de saco cheio* → *farto* and + *puxa-saco* → *lambe-botas* remain available. + Uses **Colloquial**, **Vocabulary** and `annoyance-slang`; verbal/clitic + rewriting additionally requires **Pronouns**. Local overrides take priority. + Sources: [Priberam saco](https://dicionario.priberam.org/pt-pt/saco), + [chatear](https://dicionario.priberam.org/pt-pt/chatear), + [Brazilian phrase meaning](https://pt.wiktionary.org/wiki/p%C3%A9_no_saco) + and [Collins dick](https://www.collinsdictionary.com/dictionary/english/dick). +- **Habitual clitics and family surnames:** the existing habitual word-order + rule now recognizes an intervening personal clitic and reviewed support + verbs: *Sempre me apoiam* → *Apoiam-me sempre*, *porque sempre me apoiam* + → *porque me apoiam sempre*. Pre-verbal *sempre* with proclisis is itself + grammatical; this is a stylistic normalization. Negative clauses, pauses + and unresolved future/conditional clitic placement are not reordered. + Clitic constructions with ambiguous *achar* retain pre-verbal *sempre*, + protecting continuing opinions such as *sempre te achei…*. + Uses **Colloquial**, **Pronouns** and `natural-word-order`. + A singular family topic may be resumed by its plural members in the next cue: + *A minha família… Apoiam-me sempre.* No singular agreement is forced across + that boundary; a direct subject retains *A minha família apoia-me sempre*. + Bare surnames used as plural family subjects receive *os/as*: *É como Reagan + são criados* → *É como os Reagan são criados*. Plural finite agreement plus + English plural surname/family evidence supports other predicates; without + English, only an unknown single-word surname with plural *ser* and + *criados/educados* is inferred. Existing articles, singular names, coordinated + names, common nouns and local `Name = none` choices are protected. Surname + spelling is retained rather than automatically adding an *s*. + Uses **Pronouns**/`family-subjects`. Sources: + [Ciberdúvidas on proclisis-attracting adverbs](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/null/35696) + and [family surname plurals](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/plural-de-apelidos-sobrenomes/32915). +- **Nonsense, habitual clauses and intoxication:** *falar bobagens/tolices/ + disparates/besteiras/asneiras* uses conjugated *dizer*, retaining the noun's + reviewed lexical equivalent (including existing *falar abobrinha* → + *dizer disparates*): *ele sempre fala bobagens toda vez que fica + doidão* → *ele diz sempre tolices quando fica pedrado*. Tense/person are + preserved; ambiguous *falamos* needs English tense evidence. + *Toda (a) vez que* normally becomes *sempre que*. A preceding affirmative + *sempre* in the same clause permits *quando* without repeating it; negative + clauses retain universal frequency with *de cada vez que*. + *Doidão/doidona/doidões/doidonas* in reviewed state constructions becomes + *pedrado/pedrada/pedrados/pedradas* only with English drug-intoxication evidence + or a direct Portuguese drug complement. Eccentricity, passion and literal + *high* readings are protected. Uses **Colloquial**/`spoken-idioms`; speech + vocabulary and intoxication additionally require **Vocabulary**. Existing + habitual word order uses `natural-word-order`. Local overrides still win. + Sources: [Priberam pedrado](https://dicionario.priberam.org/pt-pt/pedrado), + [tolice](https://dicionario.priberam.org/pt-pt/tolice) and + [Ciberdúvidas on habitual time clauses](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/todas-as-vezes-que/13308). +- **Promises and secrecy:** elliptical English *Promise not to…?* identifies + the addressee; a subjectless Portuguese *Promete…?* request with an infinitive + can also use informal *Prometes…?* without a reference. Explicit other subjects, + contrary references and formal address are protected. With **Pronouns**, + promise-governed *rir* uses the idiomatic pronominal construction and the + governor's person: *Prometes não rir?* → *Prometes não te rir?*, + *Prometo não rir* → *Prometo não me rir*. Existing reflexives are not doubled. + Ambiguous first-/third-person forms need an explicit subject before adding a clitic. + Bare *rir* remains valid elsewhere ([Priberam rir](https://dicionario.priberam.org/pt-pt/rir)); + this is not a blanket claim that every non-pronominal use is incorrect. + Reviewed forms of *contar/dizer para ninguém* use *a ninguém*, preserving + negation. Agreement uses **YouToTu**/`address`; these complement adjustments + use **Pronouns**/`construction-vocabulary`. - **Conflict vocabulary by meaning:** *briga/brigas* no longer always mean *zanga/zangas*. Physical or purposeful struggle evidence selects *luta/lutas*: *A briga estourou* + *Fighting broke out* → *A luta estourou*. @@ -1536,11 +1997,37 @@ names differ after the episode code (`Show.S01E01.720p.pt-br.srt` and (*Está bem?* / *Is he okay?* stays). English *your* never forces *teu*, because it is the same for *tu* and the formal *o senhor* (*We need to talk to your husband* → *o seu marido*). -- It also picks the right word for *garoto/garota*: English *boy/guy* → *rapaz*, *girl* → - *rapariga* (*I hear your boy is lead detective?* → *o seu rapaz*), while *kid/child/little* - keeps *miúdo/miúda* (*coffee, kid?* → *miúdo*; *little girl* → *miudinha/miúda*). Without a - reference, *meu/seu/teu/nosso garoto* (“my boy”) also becomes *rapaz*. Your own word list - entries for these words always win. +- Explicit English *boy(s)/girl(s)* selects *rapaz(es)/rapariga(s)* for source + *menino/menina*, *garoto/garota*, *miúdo/miúda*, *moço/moça*, *moleque/moleca* + and *guri/guria*, retaining source gender, number and case. This lexical choice + also applies to *little boys/girls*; *little* or an unrelated *children* elsewhere + in the cue no longer suppresses explicit *boys/girls*. Without those English + nouns, existing *garoto/garota* behavior remains: *guy* can select *rapaz*, + *kid/child* keeps *miúdo/miúda*, and possessive *meu/seu/teu/nosso garoto* + can select *rapaz* without a reference. Unconfirmed *meninos/meninas* remain + unchanged. Local custom/ignored entries take precedence. Uses **Vocabulary** + and `verb-vocabulary`. +- Figurative English *rich in/with partners/friends/companions/colleagues/allies/family* + resolves Portuguese *ser rico de/em* those people to *ter sorte com*: + *que sou rico de parceiros* → *que tenho sorte com parceiros*. This adapts a + metaphor about valued support, not a financial statement or a fixed idiom. + Supported copula forms map to the corresponding *ter* forms, preserving + subjects, negation, people descriptors and formatting. No English confirmation + means no rewrite; money, nutrients and resources are not converted to luck. + Uses **Vocabulary**, **Colloquial** and `reference-idioms`. + See Priberam's senses of [rico](https://dicionario.priberam.org/pt-pt/rico) + and [sorte](https://dicionario.priberam.org/pt-pt/sorte). +- English *life's rich pageant/tapestry* confirms the resigned life-experience + metaphor: reviewed source phrases such as *Rica trama da vida* become + *O rico espetáculo da vida*. Supported definite articles and contractions + follow the masculine noun (*parte da rica trama da vida* → + *parte do rico espetáculo da vida*), without replacing the surrounding + clause or adding a new assertion. Literal contests, unconfirmed phrases, + unsupported determiners and already idiomatic alternatives are retained. + Uses **Vocabulary**, **Colloquial** and `reference-idioms`. + [Cambridge's idiom entry](https://dictionary.cambridge.org/dictionary/english/be-all-part-of-life-s-rich-tapestry-pageant) + confirms the difficult/unpleasant-experience reading; the Portuguese wording + is an adaptation, not a quoted dictionary translation. - When the English line says *this/these* (and not *that/those*), *isso/disso/nisso* and *esse/essa/nesse/dessa...* become *isto/disto/nisto*, *este/esta/neste/desta...* (*I got this one.* → *Eu cuido disto*; *this morning* → *esta manhã*). *Por isso* and the verb *desse* @@ -1625,6 +2112,26 @@ cached results; run checks again to obtain results for the current selection. meaning despite its missing infinitive ending. Vulkan accelerates the same model, not its grammar detection. Grammar errors are handled by the conversion rules and the separate all-line grammar review, and MiniLM only flags: it cannot write a new translation. The English cue is shown next to the line. + The installed [MiniLM checkpoint](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2) + maps text to 384-dimensional vectors; it has no correction/generation + interface. Changing similarity thresholds or adding context does not + make it a grammar corrector. Correct and incorrect person/tense or + PT-BR/PT-PT vocabulary variants can have similar meanings. + The separate Gervásio scorer can choose only the predefined alternatives + listed below, not arbitrary grammatical rewrites. Broader model-based + grammar correction would require a separately validated grammar or + generative model pipeline, not a MiniLM setting. + **Sentence-context supplementary review:** with sentence context enabled, + safely joined continuations (up to three cues, at most 1200 characters) + also receive a combined before/after meaning check. When every cue has + aligned English, the combined sentence is compared with that English too, + using the same 0.70/0.45 similarity and 0.10/0.04 drop thresholds. + A warning names all participating cues and appears on each; individual-cue + checks remain active. Speaker changes, sentence boundaries, long gaps, + markup, conflicting register and partial selections retain the existing + joining safeguards. These scores cannot certify *o que/o qual/de que* + choices or distinguish *falarei/falaria* reliably; deterministic grammar + rules and review handle those cases even without MiniLM. **Idiom-aware supplementary review:** the supplied `american_british_english_expressions_pt_PT.xlsx` (237 rows, including repeated expressions) was reviewed against the existing hints. Selected missing expressions diff --git a/se5/BrazilianToEuropeanPortuguese/README.pt-PT.md b/se5/BrazilianToEuropeanPortuguese/README.pt-PT.md index cab750aa..d4bd1f14 100644 --- a/se5/BrazilianToEuropeanPortuguese/README.pt-PT.md +++ b/se5/BrazilianToEuropeanPortuguese/README.pt-PT.md @@ -443,6 +443,493 @@ acrescentar avisos de revisão: este sentido como brasileiro; [lixar](https://dicionario.priberam.org/pt-pt/lixar) inclui *prejudicar/tramar* e *estragar*. Mantém-se o registo informal de *lixar*, em vez de impor a alternativa neutra *estragar*. +- **Ceder com o uso:** o brasileiro *lacear* passa a *ceder* quando algo dá de si + (*Elas vão lacear* → *Elas vão ceder*), a *alargar* com complemento direto + (*lacear os sapatos* → *alargar os sapatos*) e a *amolecer* em contextos + revistos de couro/material. As formas geradas mantêm o tempo, a pessoa, + os particípios e os clíticos suportados. Preserva-se o sentido de enfeitar + com laços/fitas. Usa **Vocabulary** e `reviewed-verb-senses`, respeitando + entradas locais/ignoradas. Ver Priberam: + [ceder](https://dicionario.priberam.org/pt-pt/ceder), + [alargar](https://dicionario.priberam.org/pt-pt/alargar) e + [amolecer](https://dicionario.priberam.org/pt-pt/amolecer), e os + [sentidos brasileiros de lacear](https://www.dicio.com.br/lacear/). +- **Polícias sem determinante e localização à porta:** as construções + existenciais *há/havia/houve/haverá/haveria/haja/houvesse/houver policiais* + usam *polícias*, além dos quantificadores já suportados. Preservam-se os + adjetivos em *operações/carros policiais*. O inglês *officers* continua a + permitir *agentes*, enquanto *cops* mantém *polícias*. + Usa **Vocabulary**/`police-nouns`. + Abrangem-se também nomes plurais sem determinante depois de *para/pra, + com, sem, entre, perante, por, a* e *em*, quando a pontuação, um + qualificador pessoal revisto ou uma relativa/coordenação confirmam o + grupo nominal: *Não para policiais. Para policiais, …* → *Não para + polícias. Para polícias, …*. As regras anteriores de existenciais e + quantificadores não abrangiam esta construção preposicional. + Trata-se de uma preferência vocabular PT-PT, não de afirmar que todas + as ocorrências de *policial* são agramaticais. + Fontes: Priberam [polícia](https://dicionario.priberam.org/pt-pt/pol%C3%ADcia) + e [policial](https://dicionario.priberam.org/pt-pt/policial). + A localização *na/nas minha(s), tua(s), sua(s), nossa(s), vossa(s) porta(s)* + passa a *à/às … porta(s)* em construções revistas de presença humana ou + com confirmação inglesa de *at … door*: + *Porque há policiais na minha porta?* → *Porque há polícias à minha porta?*. + Protegem-se usos revistos da superfície da porta (sangue, avisos, chaves, + tinta, etc.) e localizações ambíguas. Usa + **Colloquial**/`construction-vocabulary`, mantendo as quebras de linha. +- **Pedidos no imperativo plural:** *Por favor, avisar se avistarem o suspeito* + passa a *Por favor, avisem se avistarem o suspeito*. O infinitivo revisto de + pedido passa ao imperativo plural quando uma oração imediatamente seguinte + com *se/quando/assim que/logo que* identifica os mesmos interlocutores + implícitos no plural. Abrange verbos regulares e irregulares revistos, + complementos curtos revistos de destinatário, negação e clíticos pessoais. + Preservam-se condições singulares/ambíguas, sujeitos de terceira pessoa + explícitos na condição, condições impessoais/existenciais e instruções + comuns no infinitivo. Não exige inglês; mantém-se o verbo válido *avistar*, + sem o substituir obrigatoriamente por *ver*. + Usa **Colloquial**, **Pronouns** e `polite-infinitive-commands`. + Fonte: [Ciberdúvidas sobre «por favor», imperativo e infinitivo](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/por-favor--imperativo-formal/34520). +- **Orações reportadas e pedidos reflexos:** contrações inglesas internas, + como *Doc says you're gonna be okay*, permitem *Os médicos disseram que + ficarás bem*. Distingue-se o sujeito que relata (*She said*) do interlocutor + (*you'll*), preservando sujeitos explícitos de terceira pessoa e relativos. + Os reflexos progressivos acompanham o sujeito: *estás a enganar-te*, não + *estás a enganar-se*. + Com confirmação inglesa de *Would/Do you mind …ing?*, *Se importa responder + algumas perguntas?* passa a *Importas-te de responder a algumas perguntas?* + (formal: *Importa-se de …*). Abrange formas finitas revistas, infinitivos + validados no dicionário, negação, etiquetas e quebras de linha. + Usa **Pronouns**/`dialogue-objects`; a concordância informal exige também + **YouToTu**/`address`. *Que engraçado* não determina, por si só, o tratamento. +- **Destinatários formais no dativo:** com confirmação inglesa de *He lied + to you*, usa-se *Ele mentiu-lhe*, não *Ele mentiu para si*. Os verbos de + destinatário já revistos também tratam *a/para si* formal, respeitando a + colocação pronominal: *não lhe mentiu*, *mentir-lhe-á*, *escreveu-lhe*, + *telefonou-lhe*. O tratamento informal mantém *te*. Preservam-se *si* + sem confirmação, referência reflexa, ênfase, coordenação e destinatários + múltiplos ambíguos. Exige destinatário inglês alinhado e + **Pronouns**/`pronouns`; não substitui globalmente *para si*. +- **Complemento direto não é dativo:** os sentidos pessoais revistos de + *respeitar, admirar, amar, odiar, apoiar, acompanhar, abraçar, cumprimentar, + elogiar, convidar, proteger, defender, conhecer* e *reconhecer* corrigem + *lhe/lhes* usado como complemento direto, com um único objeto inglês + alinhado que confirme a função. *Eu lhe respeito imensamente* passa a + *Eu respeito-o imensamente* formal ou *Eu respeito-te imensamente* informal. + O tratamento feminino explícito usa *a*; *him/her/them* mantém o objeto na + terceira pessoa (plural sem género marcado usa *os*). Sem prova do género, + *you* formal usa o masculino convencional, sem inferir a identidade. + Negação, subordinação, ênclise e mesóclise usam os mecanismos partilhados. + Preservam-se dativos possessivos (*admiro-lhe a coragem*), complementos + indiretos legítimos, destinatários ambíguos e exceções locais. + Usa **Pronouns**/`pronouns`; o objeto informal exige **YouToTu**/`address`. + Fonte: [Priberam: respeitar](https://dicionario.priberam.org/pt-pt/respeitar). +- **Sujeitos coletivos e cumprimentos:** os atalhos de tratamento verificam + o sujeito através das quebras de linha antes de alterar perguntas, + *precisa/consegue/merece*, *sabe como* e ordens negativas. + *Toda a equipa / precisa de uma mascote* mantém *precisa* na terceira + pessoa: o verbo concorda com *equipa*, não com o complemento *mascote*. + Expressões revistas de *todo/toda + nome coletivo* recebem também artigo + definido (*toda equipa* → *toda a equipa*, *todo grupo* → *todo o grupo*), + com **Colloquial**/`collective-articles`. É uma preferência limitada de + redação em PT-PT, não uma reformulação de todos os quantificadores + universais. A pontuação de frase e os travessões de diálogo continuam + a permitir um novo interlocutor. + O válido *ei*, cumprimento ou chamamento, deixa de ser globalmente + substituído por *olha*, mesmo sem inglês; preserva-se *olha* existente. + Com inglês alinhado *Hey/Hi/Hello, Nome, how … you …?*, um cumprimento + nominal revisto pode usar *como vais/estás/andas/tens estado?*. + Exige **YouToTu**/`address`; preservam-se opções formais, títulos, + nomes diferentes, destinatários plurais e *como vai o trabalho?*. + O cumprimento é indício de familiaridade conversacional, não prova de + que todas as legendas seguintes devam usar *tu*, e nunca prevalece sobre + tratamento formal explícito. Mantêm-se as expressões locais ignoradas. + Fontes: [Ciberdúvidas: concordância com equipa](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/concordancia-a-equipa-da-empresa-y-o-nosso-colega-z/37613), + [todo e artigo](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/uso-do-artigo-definido-com-o-quantificador-todo/28407) + e [Priberam: ei](https://dicionario.priberam.org/pt-pt/ei). +- **Destinatários omitidos em pedidos com dar:** um único pedido inglês + alinhado *give/grant me/us* permite recuperar *-me/-nos* omitido num + imperativo português limitado a segundos, minutos, um momento, tempo, + uma pausa, um favor, uma oportunidade, ajuda ou atenção. + *Rapazes, dêem um segundo, certo?* passa a + *Rapazes, deem-me um segundo, certo?* com grafia pós-acordo, + ou *dêem-me* com grafia pré-acordo. O plural *deem/dêem* é válido + para grupos informais e formais; não deve passar ao singular *dá*. + Preservam-se a conjugação singular/plural e as opções ortográficas; + a grafia *deem/dêem* passa a seguir a opção mesmo quando o dicionário + aceita ambas. Usa **Pronouns**/`give-request-recipient`, + independentemente do tratamento informal. Exige correspondência da + categoria do objeto e não inventa destinatário sem inglês nem substitui + clíticos existentes, destinatários explícitos, negação, subordinadas, + pedidos concorrentes ou expressões locais ignoradas. + Fonte: [Ciberdúvidas: imperativo de dar e clíticos](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/da-ma-da-mo/25033) + (o artigo usa a grafia pré-acordo *dêem*). +- **Marcas conversacionais destacadas:** *sabe?*, *entende/percebe/compreende?* + e as formas revistas *viu/ouviu/entendeu/percebeu?* concordam com o + interlocutor informal estabelecido (*sabes?*, *percebes?*, *ouviste?*, etc.). + A prova pode estar na própria legenda ou na continuação imediatamente + anterior já convertida: *Ouve, isto nunca vai ficar mais fácil, / mas + temos de começar a caminhar, sabe?* termina em *sabes?*. + O sujeito *nós* continua a determinar *temos*; a expressão destacada + dirige-se a *tu*. Exige **YouToTu**, `address` e `discourse-tags`. + A prova anterior exige continuação de frase, intervalo até 1,5 segundos, + falantes/tratamento compatíveis e ausência de seleção parcial. + Se houver inglês alinhado, a expressão destacada *you know/understand/see…?* + tem de confirmar a função. Preservam-se perguntas literais, sujeitos + nominais, marcas inglesas plurais, opções/títulos formais, fronteiras + de diálogo e expressões locais ignoradas. A semelhança de sentido MiniLM, + por si só, não determina a pessoa gramatical nem garante aviso de + tratamento misto. + Fontes: [Ciberdúvidas: tratamento e concordância verbal](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/pronomes-pessoais-brasil/19843) + e [marcadores discursivos](https://ciberduvidas.iscte-iul.pt/pergunta.php?id=17970). +- **Relativas de assunto e condicional confirmado pela referência:** com + antecedente explícito revisto (*algo, nada, uma coisa* ou uma lista + limitada de nomes de assunto), *sobre o que + falar* pode usar a + construção mais simples *de que*: *havia algo / sobre o que esperava + falar contigo* → *havia algo / de que esperava falar contigo*. + Uma continuação imediata verificada permite recuperar o antecedente sem + ativar a reformulação geral da frase (mesmo falante, intervalo até + 1,5 segundos, sem fim de frase, fronteira de diálogo ou seleção parcial). + **Colloquial**/`topic-relatives` controla esta preferência de redação. + **SentenceContext**/`sentence-context` permite adicionalmente a conversão + e revisão MiniLM da frase completa entre legendas ligadas em segurança. + Não elimina *esperava*, não inventa + um infinitivo nem altera perguntas, relativas livres (*falou sobre o que + aconteceu*), outros verbos ou construções válidas com *sobre o qual*. + Um único *would* inglês alinhado, ou *'d + verbo de base revisto* não + ambíguo, permite corrigir futuro/condicional português validado no + dicionário, com sujeito pronominal explícito correspondente: + *ela falarei* → *ela falaria*, *eles farão* → *eles fariam*. + Usa `reference-conditional`, independentemente do tratamento informal. + Preserva a negação; não infere sujeitos, não transforma todos os futuros + em condicionais nem resolve modais concorrentes, leituras ambíguas de + *'d* como *had* ou mesóclises. + Sem prova suficiente, a pessoa errada no futuro/condicional gera apenas + aviso de revisão. A revisão gramatical verifica também a relativa + completa quando a regra automática de redação está desativada. + Fontes: [Ciberdúvidas: preposições nas relativas](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/ainda-a-diferenca-entre-que-e-o-qual/28803), + [Ciberdúvidas: complementos de falar](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/dizer--falar/7465) + e [Ciberdúvidas: condicional](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/condicional-simples-e-condicional-composto/38090). +- **Sujeitos omitidos em perguntas e modo nas completivas:** perguntas + inglesas iniciadas por auxiliar (*Did/Have/Were/Can you…*) confirmam + o interlocutor omitido, tal como *You…*. No tratamento informal, + *Já viveu…?* passa a *Já viveste…?* e o pronominal revisto + *Já se envolveu…* passa a *Já te envolveste…*. Sem atrator, a colocação + existente produz *Envolveste-te…*. O passado reutiliza a conjugação + validada no dicionário; preservam-se o tratamento formal, sujeitos + explícitos diferentes, interlocutores plurais e sujeitos de relativas. + Usa **YouToTu**/`address`; *you* confirma o sujeito, não obriga a tratar + alguém por *tu*. + Formas presentes depois de *recear, temer, desejar* e de *duvidar + que* + afirmativo usam o presente do conjuntivo: *Receio que estás preso* → + *Receio que estejas preso*; formal *esteja*, terceira pessoa *ele esteja*. + Reutiliza-se o mecanismo de modo existente e passam-se clíticos + enclíticos para antes do verbo. Não se reformulam indiscriminadamente + subordinantes negadas, homógrafos nominais, passado/futuro ou sujeitos + por resolver. Usa `complement-subjunctive`, independentemente da opção + de tratamento; não se aplica a todas as orações com *que*. + Tanto *roubo de carro* como *roubo de carros* são possíveis; esta regra + não inventa o número de veículos roubados. + Fontes: [Ciberdúvidas: concordância no tratamento](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/pronomes-pessoais-brasil/19843), + [conjuntivo em completivas](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/o-conjuntivo-em-subordinadas-completivas/21317) + e [atratores de próclise](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/os-casos-especificos-da-proclise/10650). +- **Expressões de elogio confirmadas pelo inglês:** com um único predicado + pessoal inglês e uma única tradução literal portuguesa revista, + *Sim, verdadeiro, sim* alinhado com *Yeah, he's the real deal, yeah* + passa a *Sim, ele é mesmo bom, sim*. Sujeitos explícitos + *ele/ela/eles/elas* mantêm género, número e presente/imperfeito. + *The genuine article* e *the real McCoy* usam + *autêntico/autêntica/autênticos/autênticas*; contrastes explícitos com + falsificações ou impostores também mantêm o sentido de autenticidade, + sem inventar competência. *Know one's stuff* corrige a tradução literal + *conhecer/saber as suas coisas* para *saber o que faz*, preservando pessoa, + tempo e ênfase: *Estes tipos conhecem mesmo suas coisas* → + *Estes tipos sabem mesmo o que fazem*. Protegem-se possessivos + discordantes, pertences, múltiplos candidatos, sujeitos pessoais + incompatíveis e sentidos literais ou diferentes. Não se retraduz texto + já natural nem se insere uma frase inglesa ausente; *the real deal* + relativo a informação não é tratado como elogio pessoal. + Exige **Vocabulary**, **Colloquial** e `english-praise-idioms`; + preservam-se expressões ignoradas e escolhas locais de vocabulário. + Fontes: Cambridge [the real deal](https://dictionary.cambridge.org/dictionary/english/real-deal), + [the genuine article](https://dictionary.cambridge.org/dictionary/english/genuine-article) + e [know your stuff](https://dictionary.cambridge.org/dictionary/learner-english/know-your-stuff). +- **Pronomes reflexos, recíprocos e inerentes:** a colocação partilhada não + consiste numa substituição indiscriminada nem numa lista lexical exaustiva. + Abrange *me/te/se/nos/vos* existentes, incluindo citações e fragmentos em + minúsculas, com flexões regulares finitas validadas pelo dicionário para + além das pequenas tabelas de verbos. Incluem-se, por exemplo, *lavar-se, + pentear-se, barbear-se, deitar-se, levantar-se, sentar-se, afastar-se, + aproximar-se, preparar-se, apresentar-se, lembrar-se, esquecer-se, + queixar-se, arrepender-se, esforçar-se, atrever-se, resignar-se, + reconciliar-se, preocupar-se, interessar-se, importar-se, comportar-se, + concentrar-se, habituar-se, adaptar-se, recusar-se, dedicar-se, enganar-se, + magoar-se, proteger-se, defender-se, recolher-se* e *esconder-se*. + *"Nos conhecemos há muito tempo"* passa a *"Conhecemo-nos há muito tempo"*: + cai o *s* de *-mos* antes de *nos* enclítico. *Há muito tempo* é português + europeu correto; não se impõe o intensificador *imenso*. + Futuro e condicional usam mesóclise (*conhecer-nos-emos*); negação e + elementos atrativos mantêm próclise (*não nos conhecemos*, *disse que nos + conhecemos*). Sujeitos explícitos *eu/nós/vós* corrigem *quero vestir-se* → + *quero vestir-me*, *podemos esquecer-se* → *podemos esquecer-nos*. + Normalizam-se também pronomes já ligados ao verbo: *conhecemos-nos* → + *conhecemo-nos*, *não lava-se* → *não se lava*, *resignarei-me* → + *resignar-me-ei*. Comandos iniciais ambíguos com *só/apenas* mantêm a + colocação existente. Declarações com *se* inicial exigem radical pronominal + revisto e prova inglesa inequívoca do sujeito; + não se reinterpretam indiscriminadamente *se* condicional, *nos* + preposicional, passivas ou sentidos não reflexos. + Usa **Pronouns**/`pronouns`, sem inserir pronomes ausentes só por o verbo + constar de uma lista e sem exigir um modelo. + Fontes: [Ciberdúvidas: tipos de verbos pronominais](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/verbos-pronominais-pronomes-reflexos-pronomes-inerentes/37446), + [Conhecemo-nos](https://portuguesaletra.com/duvidas/conhecemo-nos-ou-conhecemos-nos/). +- **Conteúdo do que se diz:** o uso brasileiro *falar algo/alguma coisa/nada/a + verdade* passa a *dizer*: *falou algo?* → *disse alguma coisa?*, com tratamento + informal *disseste alguma coisa?*. Preservam-se tempo, pessoa, tempos compostos + e clíticos de destinatário. A confirmação inglesa de *say/tell* permite também + perguntas sobre o conteúdo, discurso indireto e citações. Preservam-se línguas, + conversas com alguém, assuntos de conversa e *falamos* sem prova do tempo + verbal. Usa **Vocabulary**/`reviewed-verb-senses`, respeitando exceções locais; + não exige um modelo. + Fontes: [Falar/dizer/contar em português europeu](https://portuguesepedia.com/falar-dizer-contar/), + [Priberam: dizer](https://dicionario.priberam.org/pt-pt/dizer). +- **Resultados sem erros:** o brasileirismo informal *gabaritar* passa a + *acertar em tudo*, mantendo tempo/pessoa: *Na verdade, gabaritei* → + *Na verdade, acertei em tudo*. Complementos de exame revistos recebem + preposição (*gabaritou o exame* → *acertou em tudo no exame*); com perguntas, + usa-se *acertou todas as perguntas*. Passivas de exames usam *resolvido sem + erros*. *O gabarito da prova* passa a *as soluções da prova*. + Preservam-se qualificações profissionais, moldes físicos, complementos + não resolvidos e exceções locais. *Nailed it* não prova, por si só, uma + classificação máxima: esta regra converte o verbo brasileiro *gabaritar*. + Usa **Vocabulary**/`reviewed-verb-senses`, sem exigir um modelo. + Fonte: [Priberam: gabaritar](https://dicionario.priberam.org/pt-pt/gabaritar). +- **Relações amorosas:** o inglês alinhado *girlfriend/boyfriend* permite + *namorada/namorado*, em vez de *miúda/rapariga/rapaz*, abrangendo + *garota/garoto*, os sinónimos de jovens revistos e os plurais. Por exemplo, + *Por favor, minha garota. Ela precisa de ajuda.* passa a *Por favor, minha + namorada. Ela precisa de ajuda.* Sem inglês, não se infere uma relação. + Outras pessoas do mesmo género, números incompatíveis e múltiplos + referentes possíveis impedem a substituição automática. + Respeita exceções locais e **Vocabulary**/`verb-vocabulary`. + Fonte: [Priberam: namorada](https://dicionario.priberam.org/pt-pt/namorada). +- **Diálogo revisto:** *coletiva/colectiva de imprensa* → *conferência de + imprensa*, incluindo plurais, sem exigir inglês; *press conference/presser* + também desambigua *coletiva* isolada. O inglês depreciativo *this/that broad* + permite *esta/essa tipa*, preservando títulos e propriedade expressos por + *dona*. Usa **Vocabulary**/`dialogue-vocabulary`. + Com confirmação inglesa, *duramente aprendido* passa a *aprendido a duras + penas*, mantendo género/número; a pergunta elíptica *Doing business here or + what?* permite *Vamos fazer negócio ou quê?*. Preços numéricos usam + *custa/custam* apenas com *will cost you* e um montante monetário no inglês; + preserva-se contexto explícito futuro/condicional na fala ou na anterior + portuguesa. Estas expressões também exigem **Colloquial**/`dialogue-idioms`. + Respeitam-se exceções locais. + *Remind you that* recupera *lembrar-te que* (formal *lembrar-lhe*); o inglês + *owe* permite *Deves isso a ela* → *Deves-lhe isso*, preservando contraste + explícito (*a ela, não a ele*). Usa **Pronouns**/`dialogue-objects`, mantendo + o tratamento configurado. Com *push/throw/kick you out*, mantém-se também + o complemento informal: *Vais deixar esta tipa pôr-te para fora?*, não + *pô-lo*. Uma chamada isolada (*Sra. Silva?*) já não torna + formal o diálogo anterior através de um vocativo recente dirigido a outro + nome sem título; a própria chamada e o diálogo formal seguinte mantêm o + registo. O primeiro nome não implica universalmente tratamento por *tu*. + Protegem-se ainda o condicional *se foi roubado*, artigos depois de futuros + (*Daremos a volta*), sujeitos nominais (*Como está o João?*) e a expressão + sem artigo *por minha causa*. + Fontes: [Infopédia: coletiva](https://www.infopedia.pt/dicionarios/lingua-portuguesa/coletiva), + [Priberam: dona](https://dicionario.priberam.org/pt-pt/dona), + [Priberam: tipa](https://dicionario.priberam.org/pt-pt/tipa), + [Cambridge: hard-earned](https://dictionary.cambridge.org/dictionary/english-portuguese/hard-earned), + [Ciberdúvidas: lembrar](https://ciberduvidas.iscte-iul.pt/pergunta.php?id=3657). +- **Acompanhar uma explicação e manter o tratamento:** com confirmação inglesa + de *stay with me / follow me / bear with me* num contexto de raciocínio, + recupera-se o complemento omitido de *acompanhe*: *Sim, mas acompanhe, + porque esse é o pulo* → *Sim, mas acompanhe-me, porque este é o salto*. + As regras de tratamento existentes mantêm ou selecionam + *acompanha-me/acompanhe-me/acompanhem-me*, conforme o contexto; + esta regra não impõe um novo registo. O inglês *leap* passa a *salto* + nestas construções revistas de raciocínio, sem substituir todos os usos + de *pulo*. Saltos físicos, saltos de fé, complementos completos e ordens + negadas não recebem um *-me* inventado. + Usa **Colloquial**/`explanation-constructions`; a recuperação do complemento + exige também **Pronouns**, e a conversão de *pulo* exige **Vocabulary**. + Mantêm-se as exceções locais. + A etapa **YouToTu**/`address` mantém agora o destinatário revisto numa frase + repartida por legendas próximas: *E se você meteu na sua cabeça / que teria + sua vingança?* → *E se meteste na tua cabeça / que terias a tua vingança?*. + Abrange formas revistas de *meter na cabeça* e, com sujeito de segunda + pessoa explícito no inglês, predicados revistos de relato/pensamento. + Funciona com **SentenceContext** ativo ou inativo. Tratamento formal, + mudança de falante, travessões de diálogo, intervalos superiores a + 1,5 segundos, frases anteriores completas, sujeitos ingleses explícitos + de terceira pessoa, sujeitos pospostos e *se* impessoal ambíguo impedem + esta herança de concordância. Sem inglês, não se presume o mesmo sujeito + em *você pensa / que teria…* quando permanece por resolver. + O MiniLM verifica semelhança de sentido, não corrige concordância gramatical. + Fontes: Priberam [acompanhar](https://dicionario.priberam.org/pt-pt/acompanhar), + [salto](https://dicionario.priberam.org/pt-pt/salto) e + [Ciberdúvidas sobre tu como sujeito e concordância verbal](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/null/34507). +- **Reações hipotéticas:** *Eu também ficaria muito zangada se fosse ele* + → *Eu também ficaria muito zangada se fosse comigo*. As reações emocionais + revistas com *ficar/estar* no condicional podem apresentar a situação como + acontecendo a quem reage: *comigo, contigo, connosco, + com ele/ela/eles/elas/vocês*. Abrange condições anteriores ou posteriores, + sujeitos explícitos concordantes, formas condicionais inequívocas e sujeitos + omitidos de primeira pessoa com confirmação inglesa. Preservam-se predicados + de identidade, conselhos, mudança de sujeito na condição e pessoas ambíguas. + É uma reformulação estilística contextual: *se eu fosse ele* não é, por si, + incorreto em PT-PT. Usa **Colloquial**, **Pronouns** e `hypothetical-reactions`. + Independentemente desta preferência, protege-se *se + imperfeito do + conjuntivo* depois de um predicado contra a regra de ênclise com sujeito + nominal: *zangada se fosse* nunca deve passar a *zangada fosse-se*. + Esta proteção mantém-se com a reformulação estilística desativada. + Fontes: [Ciberdúvidas sobre construções hipotéticas legítimas](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/se-eu-fosse-tu-e-se-eu-fosse-a-ti/20017) + e [Priberam comigo](https://dicionario.priberam.org/pt-pt/comigo). +- **Vocabulário de agitação:** *chacoalhar* passa a *abanar* no sentido físico + geral, *agitar* com líquidos/recipientes revistos, *sacudir* com roupa, + pó e movimentos corporais revistos, e *chocalhar* com objetos revistos + que produzem ruído (chaves, moedas, sinos, guizos, chocalhos e rolamentos). + Preserva-se o verbo PT-PT *chocalhar*, também válido para líquidos num + recipiente. Com confirmação inglesa dos sentidos de insulto, repreensão, + incómodo ou abalo emocional, usam-se *insultar*, *repreender*, *incomodar* + ou *abalar*, em vez de um verbo de agitação física. Os sujeitos revistos + de construções passivas também distinguem roupa de recipientes. + Abrange formas finitas (incluindo vós), infinitivos, gerúndios, particípios, + ênclise e mesóclise; *sacudir* inclui as formas irregulares revistas + *sacodes/sacode/sacodem*. *Chacoalhamos* usa normalmente a forma sem acento; + o passado inglês pode selecionar *abanámos/agitámos/chocalhámos*. + *Chacoalhão/chacoalhões* → *abanão/abanões*; + o nome em *uma chacoalhada* → *uma sacudidela*. + Usa **Vocabulary**/`shaking-vocabulary`; os nomes *chacoalhão/chacoalhões* + usam a lista de palavras. Mantêm-se as exceções e substituições locais. + Outras lacunas revistas usam a etapa conjugada existente `verb-vocabulary`: + *pentelhar* → *chatear*; *xeretar* → *bisbilhotar* apenas com complementos + revistos de coisas/gavetas/segredos/vida/assuntos/casos; + *futucar* → *remexer* apenas com gavetas/bolsas/malas/bolsos/coisas/armários + revistos. Não se aplica esta substituição contextual aos sentidos por + resolver de adulação, recordação, toque corporal, amarrotamento ou outros. + Esta revisão é limitada, não um levantamento de todos os verbos brasileiros + ainda em falta. + Fontes: Priberam [chacoalhar](https://dicionario.priberam.org/pt-pt/chacoalhar), + [chocalhar](https://dicionario.priberam.org/pt-pt/chocalhar), + [conjugação de sacudir](https://dicionario.priberam.org/pt-pt/conjugar/sacudir), + [pentelhar](https://dicionario.priberam.org/pt-pt/pentelhar), + [xeretar](https://dicionario.priberam.org/pt-pt/xeretar) e + [futucar](https://dicionario.priberam.org/pt-pt/futucar). +- **Esperar por pessoas e desculpas à chegada:** os complementos pessoais + revistos usam *por*: *esperar o Jamie* → *esperar pelo Jamie*; + *esperar os meus amigos* → *esperar pelos meus amigos*. Abrange formas + regulares de *esperar*, nomes revistos de familiares/profissionais e nomes + próprios com maiúscula. Os nomes sem artigo usam a escolha guardada, + as listas revistas ou *por* com sinalização do nome por resolver; não se + adivinha o género de nomes desconhecidos. Preservam-se *espero que…*, + complementos inanimados e *esperar o Jamie chegar*. + Tanto *esperar alguém* como *esperar por alguém* são corretos em PT-PT; + trata-se de uma preferência neste sentido de espera, não de uma correção + gramatical universal. Usa **Colloquial**, **Pronouns** e `waiting-for-people`. + Uma breve desculpa à chegada pode passar a *desculpem/perdoem* quando + o inglês identifica nominalmente quem chega, um grupo próximo diz estar + à espera dessa pessoa e uma legenda intermédia pergunta por ela. + Esta inferência limitada exige inglês alinhado e acrescenta um aviso para + rever os destinatários; vocativos singulares e cenas distantes não + qualificam. Usa **Colloquial**, **Pronouns** e `arrival-group-apology`. + *Alguém* é gramaticalmente singular e não determina, por si só, destinatários + no plural. O MiniLM verifica semelhança de sentido, não a concordância com + os destinatários, e não é um corretor gramatical generativo. + Fonte: [Ciberdúvidas sobre esperar alguém / esperar por alguém](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/esperar-alguem-e-esperar-por-alguem/39183). +- **Matrículas de veículos:** *placa/placas*, em contexto de identificação + automóvel, passa a *matrícula/matrículas*, além da antiga expressão fixa + *placa do carro*. Abrange complementos revistos de veículos, números, + matrículas clonadas/de fora, o inglês *license/licence/number/registration + plate*, *plates* com contexto automóvel, consultas policiais e *registration*. + Uma continuação curta pode aproveitar o contexto automóvel da legenda + anterior. Mantêm-se maiúsculas, número, formatação e quebras de linha. + Os sentidos técnicos, médicos, geológicos e de tabuleta/sinal mantêm + *placa*; deixam de se substituir indiscriminadamente *placa* e *sem placa* + sem contexto. A expressão explícita *placa de matrícula* continua a passar + a *chapa de matrícula*. Usa **Vocabulary**/`vehicle-plates`, respeitando as + escolhas e exceções locais. + Fontes: Priberam, [matrícula](https://dicionario.priberam.org/pt-pt/matr%C3%ADcula) + e [placa](https://dicionario.priberam.org/pt-pt/placa). *Placa* também está + dicionarizada no sentido automóvel; *matrícula* é a opção de saída PT-PT. +- **Gíria de incómodo:** *um/uma/uns/umas pé(s) no saco* (também com hífen) + passa a *chato/chata/chatos/chatas* para pessoas, *uma chatice* com sujeitos + impessoais revistos e *um incómodo* quando não é possível determinar o + referente. Exemplo: *um pé no saco da polícia* → *um chato da polícia*. + Interpreta-se a expressão brasileira, não o inglês *dick*, que também pode + significar *detetive*. Não se acrescenta uma tradução inglesa indiscriminada. + *Encher/torrar (o) saco*, conjugado, passa a *chatear* com clíticos pessoais + revistos ou *meu/teu/nosso/vosso*, pedidos para parar ou confirmação inglesa + de incómodo: *Ele me encheu o saco* → *Ele chateou-me*; + *Para de torrar meu saco* → *Para de me chatear*. + Protegem-se conteúdos literais do saco, confirmação de enchimento/torrefação, + sentidos financeiros e a colocação não resolvida no futuro/condicional. + Mantêm-se *que saco* → *que seca*, *de saco cheio* → *farto* e + *puxa-saco* → *lambe-botas*. + Usa **Colloquial**, **Vocabulary** e `annoyance-slang`; a reescrita + verbal/com clíticos exige também **Pronouns**. As escolhas locais prevalecem. + Fontes: Priberam, [saco](https://dicionario.priberam.org/pt-pt/saco) e + [chatear](https://dicionario.priberam.org/pt-pt/chatear), + [sentido da expressão brasileira](https://pt.wiktionary.org/wiki/p%C3%A9_no_saco) + e [Collins dick](https://www.collinsdictionary.com/dictionary/english/dick). +- **Clíticos habituais e apelidos familiares:** a regra existente da posição + habitual de *sempre* passa a reconhecer um clítico intercalado e verbos + revistos de apoio: *Sempre me apoiam* → *Apoiam-me sempre*; *porque sempre + me apoiam* → *porque me apoiam sempre*. A posição pré-verbal de *sempre* + com próclise também é gramatical: trata-se de uma normalização estilística. + Preservam-se a negação, as pausas e a colocação não resolvida dos clíticos + no futuro/condicional. Usa **Colloquial**, **Pronouns** e `natural-word-order`. + Nas construções com clítico e o verbo ambíguo *achar*, mantém-se *sempre* + antes do verbo, protegendo opiniões continuadas como *sempre te achei…*. + Um tópico familiar singular pode ser retomado pelos membros no plural na + legenda seguinte: *A minha família… Apoiam-me sempre.* Não se força o singular + nessa retoma; o sujeito direto mantém *A minha família apoia-me sempre*. + Os apelidos sem artigo, usados como sujeitos familiares plurais, recebem + *os/as*: *É como Reagan são criados* → *É como os Reagan são criados*. + A concordância verbal plural e a confirmação inglesa do apelido plural ou + da família permitem outros predicados. Sem inglês, a inferência limita-se + a um apelido desconhecido de uma palavra com *ser* no plural e + *criados/educados*. Preservam-se artigos existentes, nomes singulares, + nomes coordenados, nomes comuns e escolhas locais `Nome = none`. + Mantém-se a grafia do apelido, sem acrescentar automaticamente um *s*. + Usa **Pronouns**/`family-subjects`. Fontes: + [Ciberdúvidas sobre advérbios atratores de próclise](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/null/35696) + e [plural de apelidos](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/plural-de-apelidos-sobrenomes/32915). +- **Tolices, frequência habitual e drogas:** *falar bobagens/tolices/disparates/ + besteiras/asneiras* passa a usar *dizer*, conjugado, mantendo o equivalente + lexical revisto do nome (incluindo *falar abobrinha* → *dizer disparates*): + *ele sempre fala bobagens toda vez que fica doidão* + → *ele diz sempre tolices quando fica pedrado*. Preservam-se o tempo e a + pessoa; *falamos*, ambíguo, exige confirmação inglesa do tempo. + *Toda (a) vez que* passa normalmente a *sempre que*. Havendo um *sempre* + afirmativo na mesma oração, usa-se *quando*, evitando a repetição; em + orações negativas, *de cada vez que* preserva a frequência universal. + *Doidão/doidona/doidões/doidonas* em construções revistas de estado passa a + *pedrado/pedrada/pedrados/pedradas* apenas com confirmação inglesa do efeito + de drogas ou um complemento português direto que as identifique. + Preservam-se a excentricidade, a paixão e os sentidos literais de *high*. + Usa **Colloquial**/`spoken-idioms`; o vocabulário de fala e de intoxicação + exige também **Vocabulary**. A posição habitual de *sempre* continua em + `natural-word-order`. Respeitam-se as escolhas locais. + Fontes: Priberam, [pedrado](https://dicionario.priberam.org/pt-pt/pedrado) e + [tolice](https://dicionario.priberam.org/pt-pt/tolice), e + [Ciberdúvidas sobre orações temporais habituais](https://ciberduvidas.iscte-iul.pt/consultorio/perguntas/todas-as-vezes-que/13308). +- **Promessas e segredo:** o inglês elíptico *Promise not to…?* identifica o + interlocutor; um pedido português sem sujeito explícito, *Promete…?* seguido + de infinitivo, também admite o informal *Prometes…?* sem referência. + Preservam-se outros sujeitos explícitos, referências contraditórias e o + tratamento formal. Com **Pronouns**, *rir* dependente de *prometer* usa a + construção pronominal idiomática, concordando com o sujeito: + *Prometes não rir?* → *Prometes não te rir?*, + *Prometo não rir* → *Prometo não me rir*. Não duplica clíticos existentes. + As formas ambíguas entre primeira e terceira pessoa exigem sujeito explícito + antes de se acrescentar um clítico. + *Rir* sem pronome continua válido noutros contextos + ([Priberam rir](https://dicionario.priberam.org/pt-pt/rir)); não se trata + de uma proibição geral. As formas revistas de *contar/dizer para ninguém* + usam *a ninguém*, sem retirar a negação. A concordância usa + **YouToTu**/`address`; os complementos usam + **Pronouns**/`construction-vocabulary`. - **Regência verbal:** no sentido de recordar, *o que/isso/isto/aquilo* e complementos de memória revistos (*nome, data, número, morada…*) recebem *de* e o pronome reflexo adequado: *Você lembra o que aconteceu?* → *Lembras-te do @@ -1834,13 +2321,37 @@ local models (optional)** e use **Browse…** para o escolher: (*Está bem?* / *Is he okay?* mantém-se). O inglês *your* nunca força *teu*, pois é igual para *tu* e para o formal *o senhor* (*We need to talk to your husband* → *o seu marido*). -- Também escolhe a palavra adequada para *garoto/garota*: o inglês - *boy/guy* → *rapaz*, *girl* → *rapariga* - (*I hear your boy is lead detective?* → *o seu rapaz*), enquanto - *kid/child/little* mantém *miúdo/miúda* (*coffee, kid?* → *miúdo*; - *little girl* → *miudinha/miúda*). - Sem referência, *meu/seu/teu/nosso garoto* (“my boy”) também passa a *rapaz*. - As suas entradas na lista local para estas palavras têm sempre prioridade. +- O inglês explícito *boy(s)/girl(s)* seleciona *rapaz(es)/rapariga(s)* para + *menino/menina*, *garoto/garota*, *miúdo/miúda*, *moço/moça*, *moleque/moleca* + e *guri/guria*, mantendo o género, o número e as maiúsculas do original. + Esta escolha lexical também se aplica a *little boys/girls*; *little* ou + *children* noutra parte da legenda não anulam os substantivos explícitos. + Sem esses substantivos ingleses, mantém-se o comportamento anterior de + *garoto/garota*: *guy* pode selecionar *rapaz*, *kid/child* mantém *miúdo/miúda*, + e *meu/seu/teu/nosso garoto* pode passar a *rapaz* sem referência. + *Meninos/meninas* sem confirmação mantêm-se. As entradas locais/ignoradas têm + prioridade. Usa **Vocabulary** e `verb-vocabulary`. +- O inglês figurado *rich in/with partners/friends/companions/colleagues/allies/family* + permite adaptar *ser rico de/em* essas pessoas para *ter sorte com*: + *que sou rico de parceiros* → *que tenho sorte com parceiros*. É uma + metáfora sobre apoio valioso, não riqueza financeira nem uma expressão fixa. + As formas suportadas de *ser* passam às formas correspondentes de *ter*, + preservando sujeitos, negação, qualificadores e formatação. Sem confirmação + inglesa não há reformulação; dinheiro, nutrientes e recursos não passam a + sorte. Usa **Vocabulary**, **Colloquial** e `reference-idioms`. + Ver os sentidos de [rico](https://dicionario.priberam.org/pt-pt/rico) + e [sorte](https://dicionario.priberam.org/pt-pt/sorte) no Priberam. +- O inglês *life's rich pageant/tapestry* confirma a metáfora resignada sobre + as experiências da vida: expressões revistas como *Rica trama da vida* + passam a *O rico espetáculo da vida*. Os artigos definidos e as contrações + suportadas acompanham o nome masculino (*parte da rica trama da vida* → + *parte do rico espetáculo da vida*), sem substituir a oração envolvente + nem acrescentar uma afirmação. Preservam-se concursos literais, frases sem + confirmação, determinantes não suportados e alternativas já idiomáticas. + Usa **Vocabulary**, **Colloquial** e `reference-idioms`. + A [entrada idiomática de Cambridge](https://dictionary.cambridge.org/dictionary/english/be-all-part-of-life-s-rich-tapestry-pageant) + confirma o sentido de experiência difícil/desagradável; a formulação + portuguesa é uma adaptação, não uma tradução citada do dicionário. - Quando a legenda inglesa diz *this/these*, e não *that/those*, *isso/disso/nisso* e *esse/essa/nesse/dessa...* passam a *isto/disto/nisto* e *este/esta/neste/desta...* (*I got this one.* → *Eu cuido disto*; @@ -1956,6 +2467,30 @@ seleção atual. revisão gramatical separada de todas as legendas; o MiniLM apenas assinala: não consegue escrever uma tradução nova. A legenda inglesa aparece junto da linha. + O [modelo MiniLM instalado](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2) + transforma texto em vetores de 384 dimensões; não dispõe de uma + interface de correção/geração. Alterar limiares ou acrescentar contexto + não o transforma num corretor gramatical. Formas certas e erradas de + pessoa/tempo, ou variantes vocabulares PT-BR/PT-PT, podem manter sentido + semelhante. O avaliador separado Gervásio só escolhe as alternativas + predefinidas indicadas abaixo, não reformulações gramaticais arbitrárias. + Uma correção gramatical mais ampla baseada em modelos exigiria um + mecanismo separado e validado com modelo gramatical ou generativo, + não uma opção do MiniLM. + + **Revisão suplementar da frase completa:** com o contexto de frase ativo, + as continuações ligadas em segurança (até três legendas e 1200 carateres) + recebem também uma comparação conjunta do sentido antes/depois. + Se todas as legendas tiverem inglês alinhado, compara-se também a frase + completa com esse inglês, usando os mesmos limiares de semelhança + 0,70/0,45 e de descida 0,10/0,04. + O aviso identifica todas as legendas e aparece em cada uma; mantêm-se as + verificações individuais. Continuam a aplicar-se as proteções existentes + para mudanças de falante, fim de frase, intervalos longos, marcação, + tratamento contraditório e seleções parciais. + Estes valores não certificam escolhas entre *o que/o qual/de que* nem + distinguem *falarei/falaria* de forma fiável; as regras e a revisão + gramatical determinísticas tratam esses casos mesmo sem MiniLM. **Revisão suplementar sensível a expressões idiomáticas:** o ficheiro fornecido `american_british_english_expressions_pt_PT.xlsx`, com diff --git a/se5/BrazilianToEuropeanPortuguese/ReferenceSubtitle.cs b/se5/BrazilianToEuropeanPortuguese/ReferenceSubtitle.cs index e1fe2abe..42564b6c 100644 --- a/se5/BrazilianToEuropeanPortuguese/ReferenceSubtitle.cs +++ b/se5/BrazilianToEuropeanPortuguese/ReferenceSubtitle.cs @@ -21,6 +21,10 @@ public sealed record ReferenceHint(bool SecondPerson, bool SecondPossessive, boo internal bool ConditionalYouSubject { get; init; } internal bool SelfActionQuestion { get; init; } internal bool SecondPersonWhatSubject { get; init; } + internal bool GroupArrivalApology { get; init; } + internal bool AddresseeContinuation { get; init; } + internal bool TopicRelativeContinuation { get; init; } + internal bool InformalTagContinuation { get; init; } internal string? EnglishText { get; init; } public static ReferenceHint? FromContext(IReadOnlyList source, int index, IReadOnlyList? english = null, @@ -76,6 +80,9 @@ public sealed record ReferenceHint(bool SecondPerson, bool SecondPossessive, boo /// The English says "girl" (not "kid"): "garota" is "rapariga", not "miúda". public bool Girl { get; init; } + public bool ExplicitBoy { get; init; } + public bool ExplicitGirl { get; init; } + /// The English says "kid/child/little": "miúdo" is right. public bool Kid { get; init; } @@ -131,7 +138,13 @@ public sealed record ReferenceHint(bool SecondPerson, bool SecondPossessive, boo private static readonly Regex InitialYouSubject = new( @"^\s*(?:(?:no|yes)\s*[.,!]\s*)?" + @"(?:(?:well|so|then|but|and|now|hey|look|listen|okay|ok|all\s+right)\b[,!?;:]?\s*)?" + - @"(?:(?:maybe|perhaps|probably|surely|certainly|at\s+least)\s+){0,2}(?:you\b|you['’](?:re|ll|ve|d)\b|y['’]all\b)", + @"(?:(?:maybe|perhaps|probably|surely|certainly|at\s+least)\s+){0,2}" + + @"(?:(?:do|did|are|were|have|had|will|would|can|could|should|must)(?:n['’]t)?\s+)?" + + @"(?:you\b(?!\s+(?:and|or|all|both|guys|two|people)\b)|you['’](?:re|ll|ve|d)\b|y['’]all\b)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private static readonly Regex PromiseQuestion = new( + @"^\s*(?:(?:well|so|and|please)\s*[,!]?\s+)?(?:(?:do|will|would|can|could)\s+you\s+)?" + + @"promise\s+(?:not\s+to|to)\s+[^?!.]+\?\s*$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex InitialConditionalYouSubject = new( @"^\s*(?:(?:well|so|then|but|and|or)\b[,!?;:]?\s*)?" + @@ -139,7 +152,7 @@ public sealed record ReferenceHint(bool SecondPerson, bool SecondPossessive, boo @"you(?=\s+(?!(?:and|or|all|both|guys)\b)\p{L})", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex YouSubject = new( - @"(? source, int index, IReadOn var labelChange = speaker.Length > 0 && candidateSpeaker.Length > 0 && speaker != candidateSpeaker || SpeakerOf(left.Text).Length > 0 && SpeakerOf(right.Text).Length > 0 && SpeakerOf(left.Text) != SpeakerOf(right.Text); if (labelChange) break; + if (direction > 0 && StartsNewRecipient(source, candidate, english)) break; if (Address(candidate)) return true; if (candidateSpeaker.Length > 0) speaker = candidateSpeaker; previous = candidate; @@ -295,6 +313,31 @@ bool Address(int i) => HasAddress(Plain(source[i].Text), PortugueseAddress) || english != null && HasAddress(Plain(english[i] ?? string.Empty), EnglishAddress); } + private static bool StartsNewRecipient(IReadOnlyList source, int index, IReadOnlyList? english) + { + var summons = Regex.Match(Plain(source[index].Text), + @"^\s*(?:Sr|Sra|Srta|Dr|Dra|senhor|senhora|doutor|doutora)\.?\s+" + + @"(?\p{Lu}[\p{L}\p{M}'’-]+)\s*\?\s*$"); + if (!summons.Success) return false; + var previous = index; + for (var earlier = index - 1; earlier >= Math.Max(0, index - 12); earlier--) + { + if (source[previous].StartMs - source[earlier].EndMs > 5000 || + source[index].StartMs - source[earlier].StartMs > 30000 || + DifferentSpeaker(source[previous].Text, source[earlier].Text)) break; + var vocative = Regex.Match(Plain(source[earlier].Text), + @",\s*(?\p{Lu}[\p{Ll}\p{M}'’-]+)\s*[.!?…]*\s*$"); + if (vocative.Success && !vocative.Groups["name"].Value.Equals(summons.Groups["name"].Value, + StringComparison.OrdinalIgnoreCase) && + !PortugueseConverter.YouToTuRules.IsFormal(vocative.Value) && + (english == null || Regex.IsMatch(Plain(english[earlier] ?? ""), + @",\s*" + Regex.Escape(vocative.Groups["name"].Value) + @"\s*[.!?…]*\s*$"))) + return true; + previous = earlier; + } + return false; + } + private static string Plain(string text) => Regex.Replace(text, @"<[^<>]*>|\{[^{}]*\}", " "); internal static bool DifferentSpeaker(string left, string right) @@ -688,6 +731,7 @@ public static class GrammarReview { var plain = Regex.Replace(text, @"<[^<>]*>|\{[^{}]*\}", " "); var notes = new List(); + notes.AddRange(RelativeAndModalGrammar.Review(plain)); foreach (Match m in ExplicitSubject.Matches(plain)) { var subject = m.Groups["subject"].Value; diff --git a/se5/BrazilianToEuropeanPortuguese/RelativeAndModalGrammar.cs b/se5/BrazilianToEuropeanPortuguese/RelativeAndModalGrammar.cs new file mode 100644 index 00000000..6561c680 --- /dev/null +++ b/se5/BrazilianToEuropeanPortuguese/RelativeAndModalGrammar.cs @@ -0,0 +1,126 @@ +using System.Text.RegularExpressions; + +namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese; + +internal static class RelativeAndModalGrammar +{ + private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + private const string TopicAntecedent = + @"algo|nada|(?:alguma|uma|esta|essa|aquela)\s+coisa|" + + @"(?:o|um|este|esse|aquele)\s+(?:assunto|tema|problema)|" + + @"(?:a|uma|esta|essa|aquela)\s+(?:questão|situação|ideia|proposta)"; + private static readonly Regex PreviousTopic = new( + @"(?" + TopicAntecedent + @")\s*$", Ci); + private static readonly Regex TopicRelative = new( + @"(?" + TopicAntecedent + @")(?\s+)" + + @"(?sobre\s+o\s+que)(?\s+(?:(?:eu|tu|ele|ela|nós|eles|elas|você|vocês|" + + @"não|nunca|já|ainda|também|me|te|lhe|lhes|nos|vos|se|de|" + + @"queria|querias|queríamos|queriam|quero|queres|quer|queremos|querem|" + + @"esperava|esperavas|esperávamos|esperavam|espero|esperas|espera|esperamos|esperam|" + + @"gostaria|gostarias|gostaríamos|gostariam|podia|podias|podíamos|podiam|" + + @"posso|podes|pode|podemos|podem|possa|possas|possamos|possam|" + + @"tenho|tens|tem|temos|têm|tinha|tinhas|tínhamos|tinham|ia|ias|íamos|iam)\s+){0,6}" + + @"(?fal\p{L}+))(?![\p{L}\p{M}\d_-])", Ci); + private static readonly HashSet FalarForms = BuildFalarForms(); + private static readonly Regex SubjectVerb = new( + @"(?
^|[.!?;]\s*|\b(?:que|mas)\s+)[\s""“«'\uE000-\uF8FF–—-]*(?:(?:e|então)\s+)?" +
+        @"(?eu|tu|você|vocês|ele|ela|nós|eles|elas)(?\s+)" +
+        @"(?(?:(?:não|nunca|jamais|já|ainda|sempre|também|só|apenas|provavelmente|" +
+        @"me|te|se|nos|vos|lhe|lhes|o|a|os|as)\s+){0,3})(?\p{L}+)(?![\p{L}\p{M}\d_-])", Ci);
+    private static readonly Regex EnglishConditional = new(
+        @"(?:^|[.!?;]\s*)(?:(?:yes|yeah|no|well|and|but|so|then)[,.]?\s+){0,3}" +
+        @"(?I|you|he|she|it|we|they)(?:\s+would(?n't)?|(?['’]d))" +
+        @"\s+(?:(?not|never)\s+)?(?:(?:always|still|really|also|just|probably)\s+){0,2}" +
+        @"(?[a-z]+)\b", Ci);
+    private static readonly Regex EnglishModal = new(
+        @"\b(?:would|wouldn't|will|won't|could|couldn't|should|shouldn't|might|must|can|can't)\b|['’](?:d|ll)\b", Ci);
+    private static readonly HashSet EnglishBaseVerbs = new(StringComparer.OrdinalIgnoreCase)
+    {
+        "be", "have", "do", "go", "come", "bring", "take", "give", "make", "get", "keep",
+        "talk", "speak", "say", "tell", "mention", "ask", "answer", "call", "help", "work",
+        "leave", "stay", "wait", "try", "need", "want", "like", "love", "hate", "prefer",
+        "know", "think", "believe", "feel", "see", "hear", "find", "look", "meet", "remember",
+        "forget", "understand", "buy", "sell", "pay", "eat", "drink", "sleep", "drive", "write",
+        "send", "open", "close", "start", "stop", "continue", "agree", "accept", "refuse", "return",
+    };
+
+    private static HashSet BuildFalarForms()
+    {
+        if (!VerbConjugator.TryConjugate("falar", out var forms))
+            throw new InvalidOperationException("Cannot conjugate falar.");
+        return new HashSet(forms, StringComparer.OrdinalIgnoreCase);
+    }
+
+    internal static string TopicRelatives(string text, string? previous = null)
+    {
+        text = TopicRelative.Replace(text, m => FalarForms.Contains(m.Groups["verb"].Value)
+            ? m.Groups["ante"].Value + m.Groups["space"].Value +
+              CaseHelper.MatchCase(m.Groups["relative"].Value, "de que") + m.Groups["tail"].Value
+            : m.Value);
+        if (previous == null || !Regex.IsMatch(text, @"^\s*sobre\s+o\s+que\b", Ci)) return text;
+        var antecedent = PreviousTopic.Match(previous);
+        if (!antecedent.Success) return text;
+        var prefix = antecedent.Groups["ante"].Value + " ";
+        return TopicRelatives(prefix + text)[prefix.Length..];
+    }
+
+    internal static bool HasTopicContinuation(string previous, string text) =>
+        PreviousTopic.IsMatch(previous) && Regex.IsMatch(text, @"^\s*sobre\s+o\s+que\b", Ci);
+
+    private static string EnglishSubject(string subject) => subject.ToLowerInvariant() switch
+    {
+        "eu" => "i", "tu" or "você" or "vocês" => "you", "ele" => "he", "ela" => "she",
+        "nós" => "we", _ => "they",
+    };
+
+    private static string ConditionalEnding(string subject) => subject.ToLowerInvariant() switch
+    {
+        "tu" => "ias", "nós" => "íamos", "vocês" or "eles" or "elas" => "iam", _ => "ia",
+    };
+
+    internal static string Conditionals(string text, string? english)
+    {
+        if (string.IsNullOrWhiteSpace(english) || EnglishModal.Matches(english).Count != 1) return text;
+        var predicates = EnglishConditional.Matches(english);
+        if (predicates.Count != 1 || !EnglishBaseVerbs.Contains(predicates[0].Groups["verb"].Value)) return text;
+        if (predicates[0].Groups["contracted"].Success &&
+            predicates[0].Groups["verb"].Value.Equals("come", StringComparison.OrdinalIgnoreCase)) return text;
+        var candidates = SubjectVerb.Matches(text).Where(m =>
+            PortugueseVerbs.TrySplitFuture(m.Groups["verb"].Value, out _, out _, requireTransitive: false)).ToArray();
+        if (candidates.Length != 1) return text;
+        var candidate = candidates[0];
+        var predicate = predicates[0];
+        if (!EnglishSubject(candidate.Groups["subject"].Value).Equals(predicate.Groups["subject"].Value,
+                StringComparison.OrdinalIgnoreCase) ||
+            Regex.IsMatch(candidate.Groups["adverbs"].Value, @"\b(?:não|nunca|jamais)\b", Ci) !=
+            predicate.Groups["negative"].Success)
+            return text;
+        var verb = candidate.Groups["verb"];
+        PortugueseVerbs.TrySplitFuture(verb.Value, out var stem, out _, requireTransitive: false);
+        var replacement = CaseHelper.MatchCase(verb.Value, stem + ConditionalEnding(candidate.Groups["subject"].Value));
+        return text[..verb.Index] + replacement + text[(verb.Index + verb.Length)..];
+    }
+
+    internal static IEnumerable Review(string text)
+    {
+        foreach (Match m in TopicRelative.Matches(text))
+            if (FalarForms.Contains(m.Groups["verb"].Value))
+                yield return $"Review: explicit antecedent “{m.Groups["ante"].Value}” before “sobre o que”; " +
+                    "check the relative complement of falar (for example, de que).";
+        foreach (Match m in SubjectVerb.Matches(text))
+        {
+            if (!PortugueseVerbs.TrySplitFuture(m.Groups["verb"].Value, out _, out var ending, requireTransitive: false))
+                continue;
+            var subject = m.Groups["subject"].Value.ToLowerInvariant();
+            var expected = subject switch
+            {
+                "eu" => new[] { "ei", "ia" }, "tu" => new[] { "ás", "ias" },
+                "nós" => new[] { "emos", "íamos" }, "vocês" or "eles" or "elas" => new[] { "ão", "iam" },
+                _ => new[] { "á", "ia" },
+            };
+            if (!expected.Contains(ending, StringComparer.OrdinalIgnoreCase))
+                yield return $"Review: explicit subject “{m.Groups["subject"].Value}” disagrees with " +
+                    $"future/conditional “{m.Groups["verb"].Value}”; verify person and tense against the context.";
+        }
+    }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/ReviewedDialogue.cs b/se5/BrazilianToEuropeanPortuguese/ReviewedDialogue.cs
new file mode 100644
index 00000000..9a6bc613
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/ReviewedDialogue.cs
@@ -0,0 +1,143 @@
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal static class ReviewedDialogue
+{
+    private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+    private const string Before = @"(?(?:re)?lembrar|(?:re)?lembrá-l[oa])(?=\s+que\b)", Ci);
+    private static readonly Regex OwedObject = new(
+        Before + @"(?[\p{L}\p{M}]+)(?\s+)(?isto|isso|aquilo|uma\s+explicação|um\s+favor)" +
+        @"\s+a\s+(?ele|ela|eles|elas)(?=\s*(?:[.!?…]|[\uE000-\uF8FF]|$))", Ci);
+    private static readonly HashSet OweForms = new(
+        VerbConjugator.TryConjugate("dever", out var forms) ? forms : throw new InvalidOperationException("Cannot conjugate dever."),
+        StringComparer.OrdinalIgnoreCase);
+    private static readonly Regex PressNoun = new(
+        Before + @"(?(?:a|as|uma|umas|numa|numas|esta|estas|essa|essas|minha|minhas|sua|suas|tua|tuas|nossa|nossas|de)\s+)" +
+        @"(?colec?tivas?)" + After + @"(?!\s+de\s+imprensa\b)", Ci);
+    private static readonly Regex Broad = new(
+        Before + @"(?(?:esta|essa|aquela|estas|essas|aquelas|uma|umas)\s+)(?donas?)" +
+        After + @"(?!\s+(?:de|do|da|dos|das)\b|\s+(?-i:\p{Lu}))", Ci);
+    private static readonly Regex Learned = new(
+        Before + @"duramente\s+(?aprendid[oa]s?)" + After, Ci);
+    private static readonly Regex Business = new(
+        Before + @"(?vai|vamos)\s+fazer\s+(?negócios?)(?\s+aqui)?\s+ou\s+(?:o\s+)?quê(?=\s*\?)", Ci);
+    private static readonly Regex Quote = new(
+        Before + @"(?(?:um|uma|dois|duas|três|quatro|cinco|seis|sete|oito|nove|dez|isto|isso|\d+)\s+)" +
+        @"(?custará|custarão)(?=\s+(?:[$€£]\s*)?\d[\d., ]*(?:\s*(?:dólares?|euros?|libras?)\b|[$€£]|[.!?…]|$))", Ci);
+    private static readonly Regex FutureContext = new(
+        @"\b(?:amanhã|depois|futuro|futura|próxim[oa]s?|quando|se|daqui\s+a|previsão|previsto|" +
+        @"tomorrow|later|future|next|when|if|forecast|predicted|eventually)\b", Ci);
+    private static readonly Regex MindRequest = new(
+        @"(?^[\s\uE000-\uF8FF]*(?:[-–—]\s*)?)(?:(?não)\s+)?" +
+        @"(?:(?você|tu)\s+)?(?:(?se|te)\s+)?(?importa|importas|importaria|importarias)" +
+        @"(?:-(?se|te))?(?\s+)(?:de\s+)?(?[\p{L}\p{M}]+)" + After, Ci);
+
+    internal static string Objects(string text, ReferenceHint? reference, bool informal, Func blocked)
+    {
+        var english = reference?.EnglishText ?? "";
+        if (Regex.IsMatch(english, @"^(?:(?:would|do|could|will)\s+you\s+mind|you\s+mind)\s+\p{L}+ing\b", Ci) &&
+            !blocked("importar"))
+            text = MindRequest.Replace(text, m =>
+            {
+                var verb = m.Groups["verb"].Value;
+                var inf = m.Groups["inf"].Value;
+                if (!PortugueseVerbs.IsKnownInfinitive(inf) || blocked(verb) ||
+                    !m.Groups["clitic"].Success && !m.Groups["enclitic"].Success) return m.Value;
+                var conditional = verb.StartsWith("importari", StringComparison.OrdinalIgnoreCase);
+                var target = informal ? conditional ? "importarias" : "importas" : conditional ? "importaria" : "importa";
+                var clitic = informal ? "te" : "se";
+                var negative = m.Groups["negative"].Success;
+                var result = negative ? "não " + clitic + " " + target
+                    : conditional ? PortugueseVerbs.AttachMesoclitic(target, clitic, requireTransitive: false)
+                    : PortugueseVerbs.AttachEnclitic(target, clitic);
+                if (result == null) return m.Value;
+                var gap = m.Groups["gap"].Value;
+                var rest = m.Result("$'");
+                var complement = inf.Equals("responder", StringComparison.OrdinalIgnoreCase) &&
+                    Regex.IsMatch(rest, @"^\s+(?:(?:algumas|umas|várias|estas|essas|duas|três|poucas)\s+)?(?:perguntas|questões)\b", Ci)
+                    ? " a" : "";
+                var first = new[] { "negative", "subject", "clitic", "verb" }
+                    .Select(name => m.Groups[name]).First(group => group.Success).Value;
+                var replacement = CaseHelper.MatchCase(first, result) + " de" + gap + inf + complement;
+                if (CaseHelper.IsAllUpper(verb) && CaseHelper.IsAllUpper(inf))
+                    replacement = replacement.ToUpperInvariant();
+                return m.Groups["head"].Value + replacement;
+            });
+        if (informal && Regex.IsMatch(english, @"\b(?:push|throw|kick|put|turn|force|shove)\s+you\s+out\b", Ci) &&
+            !Regex.IsMatch(english, @"\b(?:him|her|them)\b", Ci) && !blocked("pôr"))
+            text = Regex.Replace(text, Before + @"(?pô-l[oa])(?=\s+para\s+fora\b)", m =>
+                blocked(m.Value) ? m.Value : CaseHelper.MatchCase(m.Value, "pôr-te"), Ci);
+        if (Regex.IsMatch(english, @"\bremind\s+you\s+that\b", Ci) &&
+            !Regex.IsMatch(text, @"\b(?:vocês|vós)\b", Ci))
+            text = Reminder.Replace(text, m =>
+            {
+                var verb = m.Groups["verb"].Value;
+                var infinitive = verb.StartsWith("re", StringComparison.OrdinalIgnoreCase) ? "relembrar" : "lembrar";
+                return blocked(infinitive) || blocked(verb) ? m.Value :
+                    CaseHelper.MatchCase(verb, infinitive + (informal ? "-te" : "-lhe"));
+            });
+        if (Regex.IsMatch(english, @"\bow(?:e|es|ed|ing)\b", Ci) && !blocked("dever"))
+            text = OwedObject.Replace(text, m =>
+            {
+                var verb = m.Groups["verb"].Value;
+                if (!OweForms.Contains(verb) || PortugueseVerbs.IsInfinitive(verb) ||
+                    Regex.IsMatch(verb, @"id[oa]s?$", Ci) || blocked(verb)) return m.Value;
+                var clitic = m.Groups["person"].Value.EndsWith("s", StringComparison.OrdinalIgnoreCase) ? "lhes" : "lhe";
+                if (CaseHelper.IsAllUpper(verb)) clitic = clitic.ToUpperInvariant();
+                var attached = PortugueseVerbs.IsFutureOrConditional(verb)
+                    ? PortugueseVerbs.AttachMesoclitic(verb, clitic, requireTransitive: false)
+                    : PortugueseVerbs.AttachEnclitic(verb, clitic);
+                if (attached == null) return m.Value;
+                var before = m.Result("$`");
+                if (Regex.IsMatch(before, @"\b(?:não|nunca|jamais|que|se|quando)\s*$", Ci))
+                    attached = CaseHelper.MatchCase(verb, clitic) + " " + verb.ToLowerInvariant();
+                return attached + m.Groups["gap"].Value + m.Groups["object"].Value;
+            });
+        return text;
+    }
+
+    internal static string Vocabulary(string text, ReferenceHint? reference, Func blocked)
+    {
+        var english = reference?.EnglishText ?? "";
+        if (Regex.IsMatch(english, @"\b(?:press\s+conferences?|pressers?)\b", Ci))
+        {
+            text = PressNoun.Replace(text, m =>
+            {
+                var noun = m.Groups["noun"].Value;
+                return blocked(noun) ? m.Value : m.Groups["head"].Value +
+                    CaseHelper.MatchCase(noun, noun.EndsWith("s", StringComparison.OrdinalIgnoreCase) ? "conferências de imprensa" : "conferência de imprensa");
+            });
+            if (Regex.IsMatch(english, @"\b(?:select|exclusive)\s+interviews?\b", Ci))
+                text = Regex.Replace(text, @"(?<=conferências de imprensa\s+ou\s+)exclusivas\b", "entrevistas exclusivas", Ci);
+        }
+        if (Regex.IsMatch(english, @"\b(?:this|that|these|those|a|the)\s+broads?\b", Ci) && !blocked("dona"))
+            text = Broad.Replace(text, m => blocked(m.Groups["noun"].Value) ? m.Value :
+                m.Groups["head"].Value + CaseHelper.MatchCase(m.Groups["noun"].Value,
+                    m.Groups["noun"].Value.EndsWith("s", StringComparison.OrdinalIgnoreCase) ? "tipas" : "tipa"));
+        return text;
+    }
+
+    internal static string Idioms(string text, string? previous, ReferenceHint? reference, Func blocked)
+    {
+        var english = reference?.EnglishText ?? "";
+        if (Regex.IsMatch(english, @"\bhard[- ](?:earned|learned|learnt)\b|\b(?:learned|learnt)\b[^.!?]{0,40}\bthe\s+hard\s+way\b", Ci) &&
+            !blocked("duramente") && !blocked("aprender"))
+            text = Learned.Replace(text, m => blocked(m.Groups["part"].Value) ? m.Value :
+                CaseHelper.MatchCase(m.Value, m.Groups["part"].Value + " a duras penas"));
+        if (Regex.IsMatch(english, @"^(?:are\s+we\s+)?doing\s+(?:some\s+)?business(?:\s+here)?\s+or\s+what\s*\?$", Ci) &&
+            !blocked("fazer") && !blocked("negócio"))
+            text = Business.Replace(text, m => CaseHelper.MatchCase(m.Groups["verb"].Value, "vamos") +
+                " fazer " + m.Groups["noun"].Value + m.Groups["place"].Value + " ou quê");
+        if (Regex.IsMatch(english, @"\bwill\s+cost\s+you\b", Ci) &&
+            Regex.IsMatch(english, @"[$€£]\s*\d|\d\s*(?:dollars?|euros?|pounds?)\b", Ci) &&
+            !FutureContext.IsMatch(english + " " + text + " " + previous) && !blocked("custar"))
+            text = Quote.Replace(text, m => blocked(m.Groups["verb"].Value) ? m.Value : m.Groups["subject"].Value +
+                CaseHelper.MatchCase(m.Groups["verb"].Value,
+                    m.Groups["verb"].Value.Equals("custarão", StringComparison.OrdinalIgnoreCase) ? "custam" : "custa"));
+        return text;
+    }
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/ShakingVocabulary.cs b/se5/BrazilianToEuropeanPortuguese/ShakingVocabulary.cs
new file mode 100644
index 00000000..86cb1362
--- /dev/null
+++ b/se5/BrazilianToEuropeanPortuguese/ShakingVocabulary.cs
@@ -0,0 +1,117 @@
+using System.Text.RegularExpressions;
+
+namespace SubtitleEdit.Plugins.BrazilianToEuropeanPortuguese;
+
+internal static class ShakingVocabulary
+{
+    private const RegexOptions Ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+    private static readonly string[] Source = Paradigm("chacoalhar");
+    private static readonly Dictionary Targets = new(StringComparer.Ordinal)
+    {
+        ["abanar"] = Paradigm("abanar"),
+        ["agitar"] = Paradigm("agitar"),
+        ["chocalhar"] = Paradigm("chocalhar"),
+        ["sacudir"] = Sacudir(),
+        ["insultar"] = Paradigm("insultar"),
+        ["repreender"] = Paradigm("repreender"),
+        ["incomodar"] = Paradigm("incomodar"),
+        ["abalar"] = Paradigm("abalar"),
+    };
+    private static readonly Regex Verb = new(
+        @"(?" +
+        string.Join("|", Source.Append("chacoalhá").Distinct().OrderByDescending(s => s.Length).Select(Regex.Escape)) +
+        @")(?-(?:me|te|se|nos|vos|lhe|lhes|lo|la|los|las|o|a|os|as))?" +
+        @"(?-(?:ei|ás|á|emos|eis|ão|ia|ias|íamos|íeis|iam))?(?![\p{L}\p{M}\d_-])", Ci);
+    private static readonly Regex Complement = new(
+        @"^\s+(?:(?:bem|muito|levemente|ligeiramente|vigorosamente|violentamente|sempre|ainda)\s+){0,2}" +
+        @"(?:(?:o|a|os|as|um|uma|uns|umas|este|esta|estes|estas|esse|essa|esses|essas|" +
+        @"meu|minha|meus|minhas|teu|tua|teus|tuas|seu|sua|seus|suas)\s+){0,2}(?[\p{L}\p{M}]+)", Ci);
+    private static readonly Regex Liquid = new(
+        @"^(?:garrafas?|frascos?|recipientes?|líquidos?|remédios?|medicamentos?|xaropes?|sumos?|sucos?|leite|misturas?|soluções?)$", Ci);
+    private static readonly Regex DustOrClothes = new(
+        @"^(?:poeira|pó|casacos?|tapetes?|roupas?|mantas?|cobertores?|lençóis|lençol|toalhas?|braços?|pernas?|mãos?|corpo)$", Ci);
+    private static readonly Regex RattlingObjects = new(
+        @"^(?:chaves?|moedas?|guizos?|sinos?|chocalhos?|rolamentos?)$", Ci);
+    private static readonly Regex PassiveSubject = new(
+        @"(?[\p{L}\p{M}]+)\s+(?:foi|foram|era|eram|está|estão|estava|estavam|ficou|ficaram|tem\s+sido|tinha\s+sido)" +
+        @"\s+(?:(?:bem|muito|demasiado|vigorosamente)\s+)?$", Ci);
+
+    private static string[] Paradigm(string infinitive)
+    {
+        if (!VerbConjugator.TryConjugate(infinitive, out var forms))
+            throw new InvalidOperationException($"Cannot conjugate reviewed shaking vocabulary: {infinitive}.");
+        var stem = infinitive[..^2];
+        var extra = infinitive.EndsWith("ar", StringComparison.Ordinal)
+            ? new[] { "ais", "astes", "áveis", "areis", "aríeis", "eis", "ásseis", "ardes", "áreis", "ai" }
+            : infinitive.EndsWith("er", StringComparison.Ordinal)
+                ? new[] { "eis", "estes", "íeis", "ereis", "eríeis", "ais", "êsseis", "erdes", "êreis", "ei" }
+                : new[] { "is", "istes", "íeis", "ireis", "iríeis", "ais", "ísseis", "irdes", "íreis", "i" };
+        return forms.Concat(extra.Select(ending => stem + ending)).ToArray();
+    }
+
+    private static string[] Sacudir()
+    {
+        var forms = Paradigm("sacudir");
+        forms[2] = "sacodes";
+        forms[3] = "sacode";
+        forms[5] = "sacodem";
+        return forms;
+    }
+
+    internal static string Apply(string text, ReferenceHint? reference, Func isBlocked) =>
+        Verb.Replace(text, m =>
+        {
+            var source = m.Groups["verb"].Value;
+            if (isBlocked("chacoalhar") || isBlocked(source)) return m.Value;
+            var cliticInfinitive = source.Equals("chacoalhá", StringComparison.OrdinalIgnoreCase);
+            if (m.Groups["tail"].Success && !m.Groups["clitic"].Success ||
+                cliticInfinitive && !Regex.IsMatch(m.Groups["clitic"].Value, @"^-l[oa]s?$", Ci)) return m.Value;
+            var before = m.Result("$`");
+            if (PortugueseConverter.HasNominalDeterminerBefore(before) ||
+                Regex.IsMatch(before, @"\b(?:duas|três|quatro|cinco|seis|algumas|várias|muitas|\d+)\s+$", Ci))
+            {
+                if (source.Equals("chacoalho", StringComparison.OrdinalIgnoreCase)) return m.Value;
+                if (Regex.IsMatch(source, @"^chacoalhadas?$", Ci))
+                    return CaseHelper.MatchCase(source, source.EndsWith("s", StringComparison.OrdinalIgnoreCase)
+                        ? "sacudidelas" : "sacudidela");
+            }
+            var english = reference?.EnglishText ?? "";
+            var after = m.Result("$'");
+            var noun = Complement.Match(after).Groups["noun"].Value;
+            if (Regex.IsMatch(source, @"^chacoalhad[oa]s?$", Ci))
+            {
+                var subject = PassiveSubject.Match(before);
+                if (subject.Success) noun = subject.Groups["noun"].Value;
+            }
+            var target = Liquid.IsMatch(noun) ? "agitar" :
+                DustOrClothes.IsMatch(noun) ? "sacudir" :
+                RattlingObjects.IsMatch(noun) ? "chocalhar" :
+                Regex.IsMatch(english, @"\b(?:insult(?:s|ed|ing)?|abus(?:e|es|ed|ing))\b", Ci) ? "insultar" :
+                Regex.IsMatch(english, @"\b(?:scold(?:s|ed|ing)?|tell(?:ing)?\s+\w+\s+off|told\s+\w+\s+off)\b", Ci) ? "repreender" :
+                Regex.IsMatch(english, @"\b(?:bother(?:s|ed|ing)?|annoy(?:s|ed|ing)?|pester(?:s|ed|ing)?)\b", Ci) ? "incomodar" :
+                Regex.IsMatch(english, @"\b(?:(?:shaken|rocked)\s+(?:by\s+(?:(?:the|his|her|their|our|my)\s+)?" +
+                    @"(?:news|events?|ordeal|experience|death|what)|beyond|more)|" +
+                    @"(?:shaken|shook)\s+up|rattled)\b", Ci) ? "abalar" :
+                "abanar";
+            var key = cliticInfinitive ? "chacoalhar" : source;
+            var candidates = Source.Select((form, index) => (form, index))
+                .Where(item => item.form.Equals(key, StringComparison.OrdinalIgnoreCase)).Select(item => item.index).ToArray();
+            if (candidates.Length == 0) return m.Value;
+            if (source.Equals("chacoalhamos", StringComparison.OrdinalIgnoreCase) &&
+                (reference is { Past: true, Present: false } || Regex.IsMatch(english, @"\bshook\b", Ci)))
+                candidates = [42];
+            var outputs = candidates.Select(index => Targets[target][index]).Distinct().ToArray();
+            string converted;
+            if (outputs.Length == 1) converted = outputs[0];
+            else if (reference is { Past: true, Present: false }) converted = Targets[target][candidates[^1]];
+            else if (reference is { Present: true, Past: false }) converted = Targets[target][candidates[0]];
+            else return m.Value;
+            if (m.Groups["clitic"].Success)
+            {
+                var clitic = m.Groups["clitic"].Value[1..].ToLowerInvariant();
+                if (cliticInfinitive) clitic = clitic[1..];
+                converted = PortugueseVerbs.AttachEnclitic(converted, clitic);
+            }
+            return CaseHelper.MatchCase(source, converted + m.Groups["tail"].Value);
+        });
+}
diff --git a/se5/BrazilianToEuropeanPortuguese/SubtitleConversion.cs b/se5/BrazilianToEuropeanPortuguese/SubtitleConversion.cs
index 2e92cb0f..d3ebb795 100644
--- a/se5/BrazilianToEuropeanPortuguese/SubtitleConversion.cs
+++ b/se5/BrazilianToEuropeanPortuguese/SubtitleConversion.cs
@@ -15,6 +15,20 @@ public static IReadOnlyList Convert(IReadOnlyList block
         for (var i = 0; i < blocks.Count; i++)
         {
             var hint = Hint(blocks, i, english, options.Address, speakers);
+            if (i > 0 && Continues(blocks[i - 1], blocks[i]) &&
+                converter.HasInformalTagEvidence(result[i - 1].Text) &&
+                (speakers == null || string.IsNullOrWhiteSpace(speakers[i - 1]) || string.IsNullOrWhiteSpace(speakers[i]) ||
+                 speakers[i - 1] == speakers[i]) &&
+                (selected is not { Count: > 0 } || selected.Contains(i - 1) && selected.Contains(i)) &&
+                result[i - 1].Hint is not { FormalAddress: true } && hint is not { FormalAddress: true })
+                hint = (hint ?? new ReferenceHint(false, false, false, false)) with { InformalTagContinuation = true };
+            if (i > 0 && RelativeAndModalGrammar.HasTopicContinuation(blocks[i - 1].Text, blocks[i].Text) &&
+                Continues(blocks[i - 1], blocks[i]) &&
+                (speakers == null || string.IsNullOrWhiteSpace(speakers[i - 1]) || string.IsNullOrWhiteSpace(speakers[i]) ||
+                 speakers[i - 1] == speakers[i]) &&
+                (selected is not { Count: > 0 } || selected.Contains(i - 1) && selected.Contains(i)) &&
+                (result[i - 1].Hint?.FormalAddress == true) == (hint?.FormalAddress == true))
+                hint = (hint ?? new ReferenceHint(false, false, false, false)) with { TopicRelativeContinuation = true };
             var rules = new List();
             var conflict = options.Address == AddressMode.Automatic && DialogueRegister.ConflictingAddress(blocks[i].Text);
             var cueConverter = conflict ? new PortugueseConverter(options with { YouToTu = false }, words, unresolvedNameArticles) : converter;
@@ -25,6 +39,10 @@ public static IReadOnlyList Convert(IReadOnlyList block
                 english?[i] == null && rules.Any(r => r.Id is "address" or "progressive-command") &&
                 !Regex.IsMatch(blocks[i].Text, @"(? r.Id == "arrival-group-apology"))
+                note = string.Join("\n", new[] { note,
+                    "Address review: plural apology inferred from a named arrival and a nearby waiting group; verify the recipients." }
+                    .Where(n => n != null));
             result[i] = new CueConversion(text, hint, rules.Distinct().ToArray(), -1, note);
         }
 
@@ -36,7 +54,7 @@ public static IReadOnlyList Convert(IReadOnlyList block
                    (speakers == null || string.IsNullOrWhiteSpace(speakers[last]) || string.IsNullOrWhiteSpace(speakers[last + 1]) ||
                     speakers[last] == speakers[last + 1]) &&
                    (selected is not { Count: > 0 } || selected.Contains(last) && selected.Contains(last + 1)) &&
-                   result[last].Hint?.FormalAddress == result[last + 1].Hint?.FormalAddress)
+                   (result[last].Hint?.FormalAddress == true) == (result[last + 1].Hint?.FormalAddress == true))
                 last++;
             if (last == first) continue;
             if (Enumerable.Range(first, last - first + 1).Any(i => options.Address == AddressMode.Automatic &&
@@ -53,6 +71,11 @@ public static IReadOnlyList Convert(IReadOnlyList block
             }
             var reference = english == null ? null : string.Join(" ",
                 english.Skip(first).Take(last - first + 1).Where(t => t != null).Distinct());
+            var meaningSentence = new SentenceMeaning(first, last, string.Join(" ", originals),
+                english == null || english.Skip(first).Take(last - first + 1).Any(string.IsNullOrWhiteSpace)
+                    ? null : reference);
+            for (var index = first; index <= last; index++)
+                result[index] = result[index] with { MeaningSentence = meaningSentence };
             var combinedHint = reference == null ? result[first].Hint : ReferenceHint.FromText(reference);
             if (combinedHint != null)
                 combinedHint = combinedHint with
@@ -95,7 +118,7 @@ public static IReadOnlyList Convert(IReadOnlyList block
                 result[index] = new CueConversion(text, result[index].Hint,
                     result[index].Rules.Concat(changedSentence
                         ? rules.Append(new RuleChange("sentence-context", "Complete sentence across adjacent cues", "Context-sensitive")) : rules)
-                    .Distinct().ToArray(), anyChange ? first : -1, note);
+                    .Distinct().ToArray(), anyChange ? first : -1, note) { MeaningSentence = meaningSentence };
             }
             first = last;
         }
@@ -120,6 +143,17 @@ public static IReadOnlyList Convert(IReadOnlyList block
         IReadOnlyList? speakers)
     {
         var hint = ReferenceHint.FromContext(blocks, index, english, speakers);
+        if (NamedArrivalToWaitingGroup(blocks, index, english))
+            hint = (hint ?? new ReferenceHint(false, false, false, false)) with { GroupArrivalApology = true };
+        if (index > 0 && blocks[index].StartMs >= blocks[index - 1].StartMs &&
+            blocks[index].StartMs - blocks[index - 1].EndMs <= 1500 &&
+            !DialogueRegister.DifferentSpeaker(blocks[index - 1].Text, blocks[index].Text) &&
+            (speakers == null || string.IsNullOrWhiteSpace(speakers[index - 1]) || string.IsNullOrWhiteSpace(speakers[index]) ||
+             speakers[index - 1] == speakers[index]) &&
+            !Regex.IsMatch(blocks[index - 1].Text + blocks[index].Text, @"(?m)^\s*[-–—]|\p{Lu}{2,}:") &&
+            !Regex.IsMatch(english?[index] ?? "", @"\b(?:he|she|they|we|I)\b", RegexOptions.IgnoreCase) &&
+            ExplanationConstructions.HasGoverningAddressee(blocks[index - 1].Text, hint))
+            hint = (hint ?? new ReferenceHint(false, false, false, false)) with { AddresseeContinuation = true };
         if (index > 0 && (DialogueRegister.DifferentSpeaker(blocks[index - 1].Text, blocks[index].Text) ||
             speakers != null && !string.IsNullOrWhiteSpace(speakers[index - 1]) && !string.IsNullOrWhiteSpace(speakers[index]) &&
             speakers[index - 1] != speakers[index]))
@@ -132,6 +166,33 @@ public static IReadOnlyList Convert(IReadOnlyList block
         };
     }
 
+    private static bool NamedArrivalToWaitingGroup(IReadOnlyList blocks, int index, IReadOnlyList? english)
+    {
+        const RegexOptions ci = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+        if (english == null || index < 2) return false;
+        var arrival = Regex.Match(english[index] ?? "",
+            @"\b(?(?-i:[A-Z][A-Z'-]+)):\s*(?:hey|hi|hello)?[!. ,]*sorry[!. ]*$", ci);
+        if (!arrival.Success) return false;
+        var name = Regex.Escape(arrival.Groups["name"].Value);
+        var previous = Regex.Replace(blocks[index - 1].Text, @"<[^>]*>", "");
+        if (!Regex.IsMatch(previous, @"\balguém\s+(?:sabe|soube|ouviu|falou|teve)\b", ci)) return false;
+        for (var earlier = index - 2; earlier >= Math.Max(0, index - 3); earlier--)
+        {
+            if (blocks[index].StartMs < blocks[earlier].EndMs ||
+                blocks[index].StartMs - blocks[earlier].EndMs > 15000) continue;
+            var waiting = Regex.Replace(blocks[earlier].Text, @"<[^>]*>", "");
+            var collective = Regex.Match(waiting,
+                @"\b(?:(?:temos|tínhamos|vamos|devemos|precisamos)\s+(?:(?:de|que)\s+)?esperar|esperamos|esperávamos|esperaremos)" +
+                @"\s+(?:(?:por|pelo|pela|o|a)\s+)?" + name + @"\b", ci);
+            if (collective.Success &&
+                !Regex.IsMatch(waiting[..collective.Index], @"\b(?:não|nunca|jamais|nem)\b[^.!?]*$", ci) &&
+                Regex.IsMatch(english[earlier] ?? "", @"\bwe\s+(?:(?:have|need)\s+to\s+|(?:are|were)\s+(?:still\s+)?|" +
+                    @"(?:will|must|should)\s+)?wait(?:ing)?\b[^.!?]{0,30}\b" + name + @"\b", ci))
+                return true;
+        }
+        return false;
+    }
+
     private static bool Continues(SrtBlock left, SrtBlock right) =>
         right.StartMs >= left.StartMs && right.StartMs - left.EndMs <= 1500 &&
         !DialogueRegister.DifferentSpeaker(left.Text, right.Text) &&
diff --git a/se5/BrazilianToEuropeanPortuguese/plugin.json b/se5/BrazilianToEuropeanPortuguese/plugin.json
index 1a086790..c37415ed 100644
--- a/se5/BrazilianToEuropeanPortuguese/plugin.json
+++ b/se5/BrazilianToEuropeanPortuguese/plugin.json
@@ -2,7 +2,7 @@
   "apiVersion": 1,
   "name": "Brazilian to European Portuguese",
   "description": "Converts Brazilian Portuguese (pt-BR) to European Portuguese (pt-PT), with complement-aware vocabulary, personal infinitives, guarded clause agreement and idioms, optional conversational style, dialogue controls and grammar review. Shows a checkable preview of changes and their rule stages.",
-  "version": "1.6.0",
+  "version": "1.7.0",
   "author": "Subtitle Edit",
   "url": "https://github.com/SubtitleEdit/plugins/tree/main/se5/BrazilianToEuropeanPortuguese",
   "menu": "Tools",