From 98a58ed6313c38623c19c0f71ecff864a0adfd8e Mon Sep 17 00:00:00 2001 From: Karzone Date: Thu, 23 Jul 2026 12:34:05 +0100 Subject: [PATCH 1/2] Fix #1: analyze localized (non-English) feature files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structural Gherkin keywords (Feature/Background/Scenario/Scenario Outline/ Examples) were hardcoded English and the `# language:` header was never read, so a localized feature file (e.g. `# language: de` with Szenario:, Beispiele:) was silently under-parsed: its scenarios were not counted and its Examples table was never found, which also broke data-error (undefined-placeholder) detection. - Add GherkinDialect + GherkinDialectProvider, backed by the canonical gherkin-languages.json (vendored from cucumber/gherkin, embedded as a resource). The provider reads the `# language:` header (English if absent). - Route DuplicateAnalyzer's block/step keyword checks through the dialect. English files keep using the configured StepKeywords (unchanged behavior); localized files use their dialect's keywords. - Make GherkinNarrativeGuard + StepDefinitionCoverageAnalyzer dialect-aware (block-boundary + case-sensitive step-keyword matching per dialect). - Tests: German file scenarios counted + undefined placeholder detected; English regression (undefined placeholder still detected); embedded-resource load; coverage path matches a German step and rejects German narrative prose. Note: using the canonical `en` table means English now also recognizes the standard Gherkin synonyms (Example:, Scenarios:) — more spec-correct, and no sample-corpus collisions. Co-Authored-By: Claude Opus 4.8 --- src/SpecHygiene/Analysis/DuplicateAnalyzer.cs | 47 +- .../Analysis/Gherkin/gherkin-languages.json | 3899 +++++++++++++++++ .../Reqnroll/GherkinNarrativeGuard.cs | 50 + .../StepDefinitionCoverageAnalyzer.cs | 51 +- src/SpecHygiene/Models/GherkinDialect.cs | 96 + .../Services/GherkinDialectProvider.cs | 112 + src/SpecHygiene/SpecHygiene.csproj | 5 + .../GherkinDialectCoverageTests.cs | 89 + .../GherkinDialectParsingTests.cs | 129 + 9 files changed, 4428 insertions(+), 50 deletions(-) create mode 100644 src/SpecHygiene/Analysis/Gherkin/gherkin-languages.json create mode 100644 src/SpecHygiene/Models/GherkinDialect.cs create mode 100644 src/SpecHygiene/Services/GherkinDialectProvider.cs create mode 100644 tests/SpecHygiene.Tests/GherkinDialectCoverageTests.cs create mode 100644 tests/SpecHygiene.Tests/GherkinDialectParsingTests.cs diff --git a/src/SpecHygiene/Analysis/DuplicateAnalyzer.cs b/src/SpecHygiene/Analysis/DuplicateAnalyzer.cs index af4db11..d7589e2 100644 --- a/src/SpecHygiene/Analysis/DuplicateAnalyzer.cs +++ b/src/SpecHygiene/Analysis/DuplicateAnalyzer.cs @@ -14,6 +14,7 @@ public class DuplicateAnalyzer private readonly HashSet _reviewedFingerprints; private readonly StepNormalizer _normalizer; private readonly DataSourceResolver _dataSourceResolver; + private readonly GherkinDialectProvider _dialectProvider; private static readonly Regex PlaceholderRegex = new(@"<([^>]+)>", RegexOptions.Compiled); private static readonly Regex ValidPlaceholderNameRegex = new(@"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled); @@ -21,12 +22,14 @@ public class DuplicateAnalyzer public DuplicateAnalyzer( AnalyzerSettings settings, HashSet? reviewedFingerprints = null, - DataSourceResolver? dataSourceResolver = null) + DataSourceResolver? dataSourceResolver = null, + GherkinDialectProvider? dialectProvider = null) { _settings = settings; _reviewedFingerprints = reviewedFingerprints ?? new HashSet(); _normalizer = new StepNormalizer(); _dataSourceResolver = dataSourceResolver ?? new DataSourceResolver(settings.Analysis.DataSource); + _dialectProvider = dialectProvider ?? GherkinDialectProvider.Default; } /// @@ -145,6 +148,15 @@ public async Task AnalyzeAsync(bool skipDuplicateMatchi try { var lines = await File.ReadAllLinesAsync(filePath); + + // Resolve the file's Gherkin dialect from its "# language:" header (English if absent). + // For English we keep honoring the user-configured StepKeywords (unchanged behavior); + // for a localized file we use that dialect's own step keywords. + var dialect = _dialectProvider.Detect(lines); + var stepKeywords = dialect.Language.Equals(GherkinDialectProvider.DefaultLanguage, StringComparison.OrdinalIgnoreCase) + ? _settings.Analysis.StepKeywords + : dialect.StepKeywords; + var currentScenario = ""; var currentScenarioLine = 0; var isScenarioOutline = false; @@ -184,7 +196,7 @@ public async Task AnalyzeAsync(bool skipDuplicateMatchi } // Track Background - if (line.StartsWith("Background:", StringComparison.OrdinalIgnoreCase)) + if (dialect.IsBackground(line)) { inBackground = true; backgroundSteps.Clear(); @@ -196,9 +208,7 @@ public async Task AnalyzeAsync(bool skipDuplicateMatchi } // Track Scenario or Scenario Outline - if (line.StartsWith("Scenario Outline:", StringComparison.OrdinalIgnoreCase) || - line.StartsWith("Scenario Template:", StringComparison.OrdinalIgnoreCase) || - line.StartsWith("Scenario:", StringComparison.OrdinalIgnoreCase)) + if (dialect.IsScenarioStart(line)) { inBackground = false; @@ -221,8 +231,7 @@ public async Task AnalyzeAsync(bool skipDuplicateMatchi continue; } - isScenarioOutline = line.StartsWith("Scenario Outline:", StringComparison.OrdinalIgnoreCase) || - line.StartsWith("Scenario Template:", StringComparison.OrdinalIgnoreCase); + isScenarioOutline = dialect.IsScenarioOutline(line); currentScenario = line.Split(':', 2).LastOrDefault()?.Trim() ?? ""; currentScenarioLine = i + 1; @@ -247,7 +256,7 @@ public async Task AnalyzeAsync(bool skipDuplicateMatchi } // Get Examples info - use tuple deconstruction - var (placeholders, hasExamples) = FindExamplesInfo(lines, i); + var (placeholders, hasExamples) = FindExamplesInfo(lines, i, dialect); examplesPlaceholders = placeholders; hasExamplesSection = hasExamples; @@ -311,8 +320,8 @@ public async Task AnalyzeAsync(bool skipDuplicateMatchi } // Skip Examples, Feature lines etc. - if (line.StartsWith("Examples:", StringComparison.OrdinalIgnoreCase) || - line.StartsWith("Feature:", StringComparison.OrdinalIgnoreCase) || + if (dialect.IsExamples(line) || + dialect.IsFeature(line) || line.StartsWith("|") || string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) @@ -321,7 +330,7 @@ public async Task AnalyzeAsync(bool skipDuplicateMatchi } // Check if this is a step line - if (IsStepLine(line)) + if (IsStepLine(line, stepKeywords)) { var stepText = line; var dataTable = new List>(); @@ -1179,16 +1188,16 @@ private List GetProjectFolders(string root) return Directory.GetDirectories(root).ToList(); } - private (HashSet Placeholders, bool HasExamples) FindExamplesInfo(string[] lines, int startIndex) + private (HashSet Placeholders, bool HasExamples) FindExamplesInfo(string[] lines, int startIndex, GherkinDialect dialect) { var placeholders = new HashSet(StringComparer.OrdinalIgnoreCase); var hasExamples = false; - + for (int i = startIndex + 1; i < lines.Length; i++) { var line = lines[i].Trim(); - - if (line.StartsWith("Examples:", StringComparison.OrdinalIgnoreCase)) + + if (dialect.IsExamples(line)) { hasExamples = true; @@ -1215,9 +1224,7 @@ private List GetProjectFolders(string root) } // Stop at next scenario - if (line.StartsWith("Scenario:", StringComparison.OrdinalIgnoreCase) || - line.StartsWith("Scenario Outline:", StringComparison.OrdinalIgnoreCase) || - line.StartsWith("Scenario Template:", StringComparison.OrdinalIgnoreCase)) + if (dialect.IsScenarioStart(line)) { break; } @@ -1226,9 +1233,9 @@ private List GetProjectFolders(string root) return (placeholders, hasExamples); } - private bool IsStepLine(string line) + private bool IsStepLine(string line, IReadOnlyList stepKeywords) { - return _settings.Analysis.StepKeywords.Any(k => + return stepKeywords.Any(k => line.StartsWith(k + " ", StringComparison.OrdinalIgnoreCase)); } diff --git a/src/SpecHygiene/Analysis/Gherkin/gherkin-languages.json b/src/SpecHygiene/Analysis/Gherkin/gherkin-languages.json new file mode 100644 index 0000000..3777cd8 --- /dev/null +++ b/src/SpecHygiene/Analysis/Gherkin/gherkin-languages.json @@ -0,0 +1,3899 @@ +{ + "af": { + "and": [ + "* ", + "En " + ], + "background": [ + "Agtergrond" + ], + "but": [ + "* ", + "Maar " + ], + "examples": [ + "Voorbeelde" + ], + "feature": [ + "Funksie", + "Besigheid Behoefte", + "Vermoë" + ], + "given": [ + "* ", + "Gegewe " + ], + "name": "Afrikaans", + "native": "Afrikaans", + "rule": [ + "Reël", + "Reel" + ], + "scenario": [ + "Voorbeeld", + "Situasie" + ], + "scenarioOutline": [ + "Situasie Uiteensetting" + ], + "then": [ + "* ", + "Dan " + ], + "when": [ + "* ", + "Wanneer " + ] + }, + "am": { + "and": [ + "* ", + "Եվ " + ], + "background": [ + "Կոնտեքստ" + ], + "but": [ + "* ", + "Բայց " + ], + "examples": [ + "Օրինակներ" + ], + "feature": [ + "Ֆունկցիոնալություն", + "Հատկություն" + ], + "given": [ + "* ", + "Դիցուք " + ], + "name": "Armenian", + "native": "հայերեն", + "rule": [ + "Rule" + ], + "scenario": [ + "Օրինակ", + "Սցենար" + ], + "scenarioOutline": [ + "Սցենարի կառուցվացքը" + ], + "then": [ + "* ", + "Ապա " + ], + "when": [ + "* ", + "Եթե ", + "Երբ " + ] + }, + "an": { + "and": [ + "* ", + "Y ", + "E " + ], + "background": [ + "Antecedents" + ], + "but": [ + "* ", + "Pero " + ], + "examples": [ + "Eixemplos" + ], + "feature": [ + "Caracteristica" + ], + "given": [ + "* ", + "Dau ", + "Dada ", + "Daus ", + "Dadas " + ], + "name": "Aragonese", + "native": "Aragonés", + "rule": [ + "Rule" + ], + "scenario": [ + "Eixemplo", + "Caso" + ], + "scenarioOutline": [ + "Esquema del caso" + ], + "then": [ + "* ", + "Alavez ", + "Allora ", + "Antonces " + ], + "when": [ + "* ", + "Cuan " + ] + }, + "ar": { + "and": [ + "* ", + "و " + ], + "background": [ + "الخلفية" + ], + "but": [ + "* ", + "لكن " + ], + "examples": [ + "امثلة" + ], + "feature": [ + "خاصية" + ], + "given": [ + "* ", + "بفرض " + ], + "name": "Arabic", + "native": "العربية", + "rule": [ + "Rule" + ], + "scenario": [ + "مثال", + "سيناريو" + ], + "scenarioOutline": [ + "سيناريو مخطط" + ], + "then": [ + "* ", + "اذاً ", + "ثم " + ], + "when": [ + "* ", + "متى ", + "عندما " + ] + }, + "ast": { + "and": [ + "* ", + "Y ", + "Ya " + ], + "background": [ + "Antecedentes" + ], + "but": [ + "* ", + "Peru " + ], + "examples": [ + "Exemplos" + ], + "feature": [ + "Carauterística" + ], + "given": [ + "* ", + "Dáu ", + "Dada ", + "Daos ", + "Daes " + ], + "name": "Asturian", + "native": "asturianu", + "rule": [ + "Rule" + ], + "scenario": [ + "Exemplo", + "Casu" + ], + "scenarioOutline": [ + "Esbozu del casu" + ], + "then": [ + "* ", + "Entós " + ], + "when": [ + "* ", + "Cuando " + ] + }, + "az": { + "and": [ + "* ", + "Və ", + "Həm " + ], + "background": [ + "Keçmiş", + "Kontekst" + ], + "but": [ + "* ", + "Amma ", + "Ancaq " + ], + "examples": [ + "Nümunələr" + ], + "feature": [ + "Özəllik" + ], + "given": [ + "* ", + "Tutaq ki ", + "Verilir " + ], + "name": "Azerbaijani", + "native": "Azərbaycanca", + "rule": [ + "Rule" + ], + "scenario": [ + "Nümunə", + "Ssenari" + ], + "scenarioOutline": [ + "Ssenarinin strukturu" + ], + "then": [ + "* ", + "O halda " + ], + "when": [ + "* ", + "Əgər ", + "Nə vaxt ki " + ] + }, + "be": { + "and": [ + "* ", + "I ", + "Ды ", + "Таксама " + ], + "background": [ + "Кантэкст" + ], + "but": [ + "* ", + "Але ", + "Інакш " + ], + "examples": [ + "Прыклады" + ], + "feature": [ + "Функцыянальнасць", + "Фіча" + ], + "given": [ + "* ", + "Няхай ", + "Дадзена " + ], + "name": "Belarusian", + "native": "Беларуская", + "rule": [ + "Правілы" + ], + "scenario": [ + "Сцэнарый", + "Cцэнар" + ], + "scenarioOutline": [ + "Шаблон сцэнарыя", + "Узор сцэнара" + ], + "then": [ + "* ", + "Тады " + ], + "when": [ + "* ", + "Калі " + ] + }, + "bg": { + "and": [ + "* ", + "И " + ], + "background": [ + "Предистория" + ], + "but": [ + "* ", + "Но " + ], + "examples": [ + "Примери" + ], + "feature": [ + "Функционалност" + ], + "given": [ + "* ", + "Дадено " + ], + "name": "Bulgarian", + "native": "български", + "rule": [ + "Правило" + ], + "scenario": [ + "Пример", + "Сценарий" + ], + "scenarioOutline": [ + "Рамка на сценарий" + ], + "then": [ + "* ", + "То " + ], + "when": [ + "* ", + "Когато " + ] + }, + "bm": { + "and": [ + "* ", + "Dan " + ], + "background": [ + "Latar Belakang" + ], + "but": [ + "* ", + "Tetapi ", + "Tapi " + ], + "examples": [ + "Contoh" + ], + "feature": [ + "Fungsi" + ], + "given": [ + "* ", + "Diberi ", + "Bagi " + ], + "name": "Malay", + "native": "Bahasa Melayu", + "rule": [ + "Rule" + ], + "scenario": [ + "Senario", + "Situasi", + "Keadaan" + ], + "scenarioOutline": [ + "Kerangka Senario", + "Kerangka Situasi", + "Kerangka Keadaan", + "Garis Panduan Senario" + ], + "then": [ + "* ", + "Maka ", + "Kemudian " + ], + "when": [ + "* ", + "Apabila " + ] + }, + "bs": { + "and": [ + "* ", + "I ", + "A " + ], + "background": [ + "Pozadina" + ], + "but": [ + "* ", + "Ali " + ], + "examples": [ + "Primjeri" + ], + "feature": [ + "Karakteristika" + ], + "given": [ + "* ", + "Dato " + ], + "name": "Bosnian", + "native": "Bosanski", + "rule": [ + "Rule" + ], + "scenario": [ + "Primjer", + "Scenariju", + "Scenario" + ], + "scenarioOutline": [ + "Scenariju-obris", + "Scenario-outline" + ], + "then": [ + "* ", + "Zatim " + ], + "when": [ + "* ", + "Kada " + ] + }, + "ca": { + "and": [ + "* ", + "I " + ], + "background": [ + "Rerefons", + "Antecedents" + ], + "but": [ + "* ", + "Però " + ], + "examples": [ + "Exemples" + ], + "feature": [ + "Característica", + "Funcionalitat" + ], + "given": [ + "* ", + "Donat ", + "Donada ", + "Atès ", + "Atesa " + ], + "name": "Catalan", + "native": "català", + "rule": [ + "Rule" + ], + "scenario": [ + "Exemple", + "Escenari" + ], + "scenarioOutline": [ + "Esquema de l'escenari" + ], + "then": [ + "* ", + "Aleshores ", + "Cal " + ], + "when": [ + "* ", + "Quan " + ] + }, + "cs": { + "and": [ + "* ", + "A také ", + "A " + ], + "background": [ + "Pozadí", + "Kontext" + ], + "but": [ + "* ", + "Ale " + ], + "examples": [ + "Příklady" + ], + "feature": [ + "Požadavek" + ], + "given": [ + "* ", + "Pokud ", + "Za předpokladu " + ], + "name": "Czech", + "native": "Česky", + "rule": [ + "Pravidlo" + ], + "scenario": [ + "Příklad", + "Scénář" + ], + "scenarioOutline": [ + "Náčrt Scénáře", + "Osnova scénáře" + ], + "then": [ + "* ", + "Pak " + ], + "when": [ + "* ", + "Když " + ] + }, + "cy-GB": { + "and": [ + "* ", + "A " + ], + "background": [ + "Cefndir" + ], + "but": [ + "* ", + "Ond " + ], + "examples": [ + "Enghreifftiau" + ], + "feature": [ + "Arwedd" + ], + "given": [ + "* ", + "Anrhegedig a " + ], + "name": "Welsh", + "native": "Cymraeg", + "rule": [ + "Rule" + ], + "scenario": [ + "Enghraifft", + "Scenario" + ], + "scenarioOutline": [ + "Scenario Amlinellol" + ], + "then": [ + "* ", + "Yna " + ], + "when": [ + "* ", + "Pryd " + ] + }, + "da": { + "and": [ + "* ", + "Og " + ], + "background": [ + "Baggrund" + ], + "but": [ + "* ", + "Men " + ], + "examples": [ + "Eksempler" + ], + "feature": [ + "Egenskab" + ], + "given": [ + "* ", + "Givet " + ], + "name": "Danish", + "native": "dansk", + "rule": [ + "Regel" + ], + "scenario": [ + "Eksempel", + "Scenarie" + ], + "scenarioOutline": [ + "Abstrakt Scenario" + ], + "then": [ + "* ", + "Så " + ], + "when": [ + "* ", + "Når " + ] + }, + "de": { + "and": [ + "* ", + "Und " + ], + "background": [ + "Grundlage", + "Hintergrund", + "Voraussetzungen", + "Vorbedingungen" + ], + "but": [ + "* ", + "Aber " + ], + "examples": [ + "Beispiele" + ], + "feature": [ + "Funktionalität", + "Funktion" + ], + "given": [ + "* ", + "Angenommen ", + "Gegeben sei ", + "Gegeben seien " + ], + "name": "German", + "native": "Deutsch", + "rule": [ + "Rule", + "Regel" + ], + "scenario": [ + "Beispiel", + "Szenario" + ], + "scenarioOutline": [ + "Szenariogrundriss", + "Szenarien" + ], + "then": [ + "* ", + "Dann " + ], + "when": [ + "* ", + "Wenn " + ] + }, + "el": { + "and": [ + "* ", + "Και " + ], + "background": [ + "Υπόβαθρο" + ], + "but": [ + "* ", + "Αλλά " + ], + "examples": [ + "Παραδείγματα", + "Σενάρια" + ], + "feature": [ + "Δυνατότητα", + "Λειτουργία" + ], + "given": [ + "* ", + "Δεδομένου " + ], + "name": "Greek", + "native": "Ελληνικά", + "rule": [ + "Rule" + ], + "scenario": [ + "Παράδειγμα", + "Σενάριο" + ], + "scenarioOutline": [ + "Περιγραφή Σεναρίου", + "Περίγραμμα Σεναρίου" + ], + "then": [ + "* ", + "Τότε " + ], + "when": [ + "* ", + "Όταν " + ] + }, + "em": { + "and": [ + "* ", + "😂" + ], + "background": [ + "💤" + ], + "but": [ + "* ", + "😔" + ], + "examples": [ + "📓" + ], + "feature": [ + "📚" + ], + "given": [ + "* ", + "😐" + ], + "name": "Emoji", + "native": "😀", + "rule": [ + "Rule" + ], + "scenario": [ + "🥒", + "📕" + ], + "scenarioOutline": [ + "📖" + ], + "then": [ + "* ", + "🙏" + ], + "when": [ + "* ", + "🎬" + ] + }, + "en": { + "and": [ + "* ", + "And " + ], + "background": [ + "Background" + ], + "but": [ + "* ", + "But " + ], + "examples": [ + "Examples", + "Scenarios" + ], + "feature": [ + "Feature", + "Business Need", + "Ability" + ], + "given": [ + "* ", + "Given " + ], + "name": "English", + "native": "English", + "rule": [ + "Rule" + ], + "scenario": [ + "Example", + "Scenario" + ], + "scenarioOutline": [ + "Scenario Outline", + "Scenario Template" + ], + "then": [ + "* ", + "Then " + ], + "when": [ + "* ", + "When " + ] + }, + "en-Scouse": { + "and": [ + "* ", + "An " + ], + "background": [ + "Dis is what went down" + ], + "but": [ + "* ", + "Buh " + ], + "examples": [ + "Examples" + ], + "feature": [ + "Feature" + ], + "given": [ + "* ", + "Givun ", + "Youse know when youse got " + ], + "name": "Scouse", + "native": "Scouse", + "rule": [ + "Rule" + ], + "scenario": [ + "The thing of it is" + ], + "scenarioOutline": [ + "Wharrimean is" + ], + "then": [ + "* ", + "Dun ", + "Den youse gotta " + ], + "when": [ + "* ", + "Wun ", + "Youse know like when " + ] + }, + "en-au": { + "and": [ + "* ", + "Too right " + ], + "background": [ + "First off" + ], + "but": [ + "* ", + "Yeah nah " + ], + "examples": [ + "You'll wanna" + ], + "feature": [ + "Pretty much" + ], + "given": [ + "* ", + "Y'know " + ], + "name": "Australian", + "native": "Australian", + "rule": [ + "Rule" + ], + "scenario": [ + "Awww, look mate" + ], + "scenarioOutline": [ + "Reckon it's like" + ], + "then": [ + "* ", + "But at the end of the day I reckon " + ], + "when": [ + "* ", + "It's just unbelievable " + ] + }, + "en-lol": { + "and": [ + "* ", + "AN " + ], + "background": [ + "B4" + ], + "but": [ + "* ", + "BUT " + ], + "examples": [ + "EXAMPLZ" + ], + "feature": [ + "OH HAI" + ], + "given": [ + "* ", + "I CAN HAZ " + ], + "name": "LOLCAT", + "native": "LOLCAT", + "rule": [ + "Rule" + ], + "scenario": [ + "MISHUN" + ], + "scenarioOutline": [ + "MISHUN SRSLY" + ], + "then": [ + "* ", + "DEN " + ], + "when": [ + "* ", + "WEN " + ] + }, + "en-old": { + "and": [ + "* ", + "Ond ", + "7 " + ], + "background": [ + "Aer", + "Ær" + ], + "but": [ + "* ", + "Ac " + ], + "examples": [ + "Se the", + "Se þe", + "Se ðe" + ], + "feature": [ + "Hwaet", + "Hwæt" + ], + "given": [ + "* ", + "Thurh ", + "Þurh ", + "Ðurh " + ], + "name": "Old English", + "native": "Englisc", + "rule": [ + "Rule" + ], + "scenario": [ + "Swa" + ], + "scenarioOutline": [ + "Swa hwaer swa", + "Swa hwær swa" + ], + "then": [ + "* ", + "Tha ", + "Þa ", + "Ða ", + "Tha the ", + "Þa þe ", + "Ða ðe " + ], + "when": [ + "* ", + "Bæþsealf ", + "Bæþsealfa ", + "Bæþsealfe ", + "Ciricæw ", + "Ciricæwe ", + "Ciricæwa " + ] + }, + "en-pirate": { + "and": [ + "* ", + "Aye " + ], + "background": [ + "Yo-ho-ho" + ], + "but": [ + "* ", + "Avast! " + ], + "examples": [ + "Dead men tell no tales" + ], + "feature": [ + "Ahoy matey!" + ], + "given": [ + "* ", + "Gangway! " + ], + "name": "Pirate", + "native": "Pirate", + "rule": [ + "Rule" + ], + "scenario": [ + "Heave to" + ], + "scenarioOutline": [ + "Shiver me timbers" + ], + "then": [ + "* ", + "Let go and haul " + ], + "when": [ + "* ", + "Blimey! " + ] + }, + "en-tx": { + "and": [ + "Come hell or high water " + ], + "background": [ + "Lemme tell y'all a story" + ], + "but": [ + "Well now hold on, I'll you what " + ], + "examples": [ + "Now that's a story longer than a cattle drive in July" + ], + "feature": [ + "This ain’t my first rodeo", + "All gussied up" + ], + "given": [ + "Fixin' to ", + "All git out " + ], + "name": "Texas", + "native": "Texas", + "rule": [ + "Rule " + ], + "scenario": [ + "All hat and no cattle" + ], + "scenarioOutline": [ + "Serious as a snake bite", + "Busy as a hound in flea season" + ], + "then": [ + "There’s no tree but bears some fruit " + ], + "when": [ + "Quick out of the chute " + ] + }, + "eo": { + "and": [ + "* ", + "Kaj " + ], + "background": [ + "Fono" + ], + "but": [ + "* ", + "Sed " + ], + "examples": [ + "Ekzemploj" + ], + "feature": [ + "Trajto" + ], + "given": [ + "* ", + "Donitaĵo ", + "Komence " + ], + "name": "Esperanto", + "native": "Esperanto", + "rule": [ + "Regulo" + ], + "scenario": [ + "Ekzemplo", + "Scenaro", + "Kazo" + ], + "scenarioOutline": [ + "Konturo de la scenaro", + "Skizo", + "Kazo-skizo" + ], + "then": [ + "* ", + "Do " + ], + "when": [ + "* ", + "Se " + ] + }, + "es": { + "and": [ + "* ", + "Y ", + "E " + ], + "background": [ + "Antecedentes" + ], + "but": [ + "* ", + "Pero " + ], + "examples": [ + "Ejemplos" + ], + "feature": [ + "Característica", + "Necesidad del negocio", + "Requisito" + ], + "given": [ + "* ", + "Dado ", + "Dada ", + "Dados ", + "Dadas " + ], + "name": "Spanish", + "native": "español", + "rule": [ + "Regla", + "Regla de negocio" + ], + "scenario": [ + "Ejemplo", + "Escenario" + ], + "scenarioOutline": [ + "Esquema del escenario" + ], + "then": [ + "* ", + "Entonces " + ], + "when": [ + "* ", + "Cuando " + ] + }, + "et": { + "and": [ + "* ", + "Ja " + ], + "background": [ + "Taust" + ], + "but": [ + "* ", + "Kuid " + ], + "examples": [ + "Juhtumid" + ], + "feature": [ + "Omadus" + ], + "given": [ + "* ", + "Eeldades " + ], + "name": "Estonian", + "native": "eesti keel", + "rule": [ + "Reegel" + ], + "scenario": [ + "Juhtum", + "Stsenaarium" + ], + "scenarioOutline": [ + "Raamjuhtum", + "Raamstsenaarium" + ], + "then": [ + "* ", + "Siis " + ], + "when": [ + "* ", + "Kui " + ] + }, + "fa": { + "and": [ + "* ", + "و " + ], + "background": [ + "زمینه", + "پیش زمینه", + "مقدمات" + ], + "but": [ + "* ", + "اما " + ], + "examples": [ + "نمونه ها" + ], + "feature": [ + "ویژگی", + "قابلیت" + ], + "given": [ + "* ", + "با فرض ", + "فرض کنید ", + "با در نظر گرفتن " + ], + "name": "Persian", + "native": "فارسی", + "rule": [ + "قانون" + ], + "scenario": [ + "مثال", + "سناریو" + ], + "scenarioOutline": [ + "الگوی سناریو" + ], + "then": [ + "* ", + "آنگاه ", + "سپس ", + "انتظار می رود " + ], + "when": [ + "* ", + "هنگامی ", + "وقتی " + ] + }, + "fi": { + "and": [ + "* ", + "Ja " + ], + "background": [ + "Tausta" + ], + "but": [ + "* ", + "Mutta " + ], + "examples": [ + "Tapaukset" + ], + "feature": [ + "Ominaisuus" + ], + "given": [ + "* ", + "Oletetaan " + ], + "name": "Finnish", + "native": "suomi", + "rule": [ + "Rule" + ], + "scenario": [ + "Tapaus" + ], + "scenarioOutline": [ + "Tapausaihio" + ], + "then": [ + "* ", + "Niin " + ], + "when": [ + "* ", + "Kun " + ] + }, + "fr": { + "and": [ + "* ", + "Et que ", + "Et qu'", + "Et " + ], + "background": [ + "Contexte" + ], + "but": [ + "* ", + "Mais que ", + "Mais qu'", + "Mais " + ], + "examples": [ + "Exemples" + ], + "feature": [ + "Fonctionnalité" + ], + "given": [ + "* ", + "Soit ", + "Sachant que ", + "Sachant qu'", + "Sachant ", + "Etant donné que ", + "Etant donné qu'", + "Etant donné ", + "Etant donnée ", + "Etant donnés ", + "Etant données ", + "Étant donné que ", + "Étant donné qu'", + "Étant donné ", + "Étant donnée ", + "Étant donnés ", + "Étant données " + ], + "name": "French", + "native": "français", + "rule": [ + "Règle" + ], + "scenario": [ + "Exemple", + "Scénario" + ], + "scenarioOutline": [ + "Plan du scénario", + "Plan du Scénario" + ], + "then": [ + "* ", + "Alors ", + "Donc " + ], + "when": [ + "* ", + "Quand ", + "Lorsque ", + "Lorsqu'" + ] + }, + "ga": { + "and": [ + "* ", + "Agus " + ], + "background": [ + "Cúlra" + ], + "but": [ + "* ", + "Ach " + ], + "examples": [ + "Samplaí" + ], + "feature": [ + "Gné" + ], + "given": [ + "* ", + "Cuir i gcás go ", + "Cuir i gcás nach ", + "Cuir i gcás gur ", + "Cuir i gcás nár " + ], + "name": "Irish", + "native": "Gaeilge", + "rule": [ + "Riail" + ], + "scenario": [ + "Sampla", + "Cás" + ], + "scenarioOutline": [ + "Cás Achomair" + ], + "then": [ + "* ", + "Ansin " + ], + "when": [ + "* ", + "Nuair a ", + "Nuair nach ", + "Nuair ba ", + "Nuair nár " + ] + }, + "gj": { + "and": [ + "* ", + "અને " + ], + "background": [ + "બેકગ્રાઉન્ડ" + ], + "but": [ + "* ", + "પણ " + ], + "examples": [ + "ઉદાહરણો" + ], + "feature": [ + "લક્ષણ", + "વ્યાપાર જરૂર", + "ક્ષમતા" + ], + "given": [ + "* ", + "આપેલ છે " + ], + "name": "Gujarati", + "native": "ગુજરાતી", + "rule": [ + "નિયમ" + ], + "scenario": [ + "ઉદાહરણ", + "સ્થિતિ" + ], + "scenarioOutline": [ + "પરિદ્દશ્ય રૂપરેખા", + "પરિદ્દશ્ય ઢાંચો" + ], + "then": [ + "* ", + "પછી " + ], + "when": [ + "* ", + "ક્યારે " + ] + }, + "gl": { + "and": [ + "* ", + "E " + ], + "background": [ + "Contexto" + ], + "but": [ + "* ", + "Mais ", + "Pero " + ], + "examples": [ + "Exemplos" + ], + "feature": [ + "Característica" + ], + "given": [ + "* ", + "Dado ", + "Dada ", + "Dados ", + "Dadas " + ], + "name": "Galician", + "native": "galego", + "rule": [ + "Rule" + ], + "scenario": [ + "Exemplo", + "Escenario" + ], + "scenarioOutline": [ + "Esbozo do escenario" + ], + "then": [ + "* ", + "Entón ", + "Logo " + ], + "when": [ + "* ", + "Cando " + ] + }, + "he": { + "and": [ + "* ", + "וגם " + ], + "background": [ + "רקע" + ], + "but": [ + "* ", + "אבל " + ], + "examples": [ + "דוגמאות" + ], + "feature": [ + "תכונה" + ], + "given": [ + "* ", + "בהינתן " + ], + "name": "Hebrew", + "native": "עברית", + "rule": [ + "כלל" + ], + "scenario": [ + "דוגמא", + "תרחיש" + ], + "scenarioOutline": [ + "תבנית תרחיש" + ], + "then": [ + "* ", + "אז ", + "אזי " + ], + "when": [ + "* ", + "כאשר " + ] + }, + "hi": { + "and": [ + "* ", + "और ", + "तथा " + ], + "background": [ + "पृष्ठभूमि" + ], + "but": [ + "* ", + "पर ", + "परन्तु ", + "किन्तु " + ], + "examples": [ + "उदाहरण" + ], + "feature": [ + "रूप लेख" + ], + "given": [ + "* ", + "अगर ", + "यदि ", + "चूंकि " + ], + "name": "Hindi", + "native": "हिंदी", + "rule": [ + "नियम" + ], + "scenario": [ + "परिदृश्य" + ], + "scenarioOutline": [ + "परिदृश्य रूपरेखा" + ], + "then": [ + "* ", + "तब ", + "तदा " + ], + "when": [ + "* ", + "जब ", + "कदा " + ] + }, + "hr": { + "and": [ + "* ", + "I " + ], + "background": [ + "Pozadina" + ], + "but": [ + "* ", + "Ali " + ], + "examples": [ + "Primjeri", + "Scenariji" + ], + "feature": [ + "Osobina", + "Mogućnost", + "Mogucnost" + ], + "given": [ + "* ", + "Zadan ", + "Zadani ", + "Zadano ", + "Ukoliko " + ], + "name": "Croatian", + "native": "hrvatski", + "rule": [ + "Rule" + ], + "scenario": [ + "Primjer", + "Scenarij" + ], + "scenarioOutline": [ + "Skica", + "Koncept" + ], + "then": [ + "* ", + "Onda " + ], + "when": [ + "* ", + "Kada ", + "Kad " + ] + }, + "ht": { + "and": [ + "* ", + "Ak ", + "Epi ", + "E " + ], + "background": [ + "Kontèks", + "Istorik" + ], + "but": [ + "* ", + "Men " + ], + "examples": [ + "Egzanp" + ], + "feature": [ + "Karakteristik", + "Mak", + "Fonksyonalite" + ], + "given": [ + "* ", + "Sipoze ", + "Sipoze ke ", + "Sipoze Ke " + ], + "name": "Creole", + "native": "kreyòl", + "rule": [ + "Rule" + ], + "scenario": [ + "Senaryo" + ], + "scenarioOutline": [ + "Plan senaryo", + "Plan Senaryo", + "Senaryo deskripsyon", + "Senaryo Deskripsyon", + "Dyagram senaryo", + "Dyagram Senaryo" + ], + "then": [ + "* ", + "Lè sa a ", + "Le sa a " + ], + "when": [ + "* ", + "Lè ", + "Le " + ] + }, + "hu": { + "and": [ + "* ", + "És " + ], + "background": [ + "Háttér" + ], + "but": [ + "* ", + "De " + ], + "examples": [ + "Példák" + ], + "feature": [ + "Jellemző" + ], + "given": [ + "* ", + "Amennyiben ", + "Adott " + ], + "name": "Hungarian", + "native": "magyar", + "rule": [ + "Szabály" + ], + "scenario": [ + "Példa", + "Forgatókönyv" + ], + "scenarioOutline": [ + "Forgatókönyv vázlat" + ], + "then": [ + "* ", + "Akkor " + ], + "when": [ + "* ", + "Majd ", + "Ha ", + "Amikor " + ] + }, + "id": { + "and": [ + "* ", + "Dan " + ], + "background": [ + "Dasar", + "Latar Belakang" + ], + "but": [ + "* ", + "Tapi ", + "Tetapi " + ], + "examples": [ + "Contoh", + "Misal" + ], + "feature": [ + "Fitur" + ], + "given": [ + "* ", + "Dengan ", + "Diketahui ", + "Diasumsikan ", + "Bila ", + "Jika " + ], + "name": "Indonesian", + "native": "Bahasa Indonesia", + "rule": [ + "Rule", + "Aturan" + ], + "scenario": [ + "Skenario" + ], + "scenarioOutline": [ + "Skenario konsep", + "Garis-Besar Skenario" + ], + "then": [ + "* ", + "Maka ", + "Kemudian " + ], + "when": [ + "* ", + "Ketika " + ] + }, + "is": { + "and": [ + "* ", + "Og " + ], + "background": [ + "Bakgrunnur" + ], + "but": [ + "* ", + "En " + ], + "examples": [ + "Dæmi", + "Atburðarásir" + ], + "feature": [ + "Eiginleiki" + ], + "given": [ + "* ", + "Ef " + ], + "name": "Icelandic", + "native": "Íslenska", + "rule": [ + "Rule" + ], + "scenario": [ + "Atburðarás" + ], + "scenarioOutline": [ + "Lýsing Atburðarásar", + "Lýsing Dæma" + ], + "then": [ + "* ", + "Þá " + ], + "when": [ + "* ", + "Þegar " + ] + }, + "it": { + "and": [ + "* ", + "E ", + "Ed " + ], + "background": [ + "Contesto" + ], + "but": [ + "* ", + "Ma " + ], + "examples": [ + "Esempi" + ], + "feature": [ + "Funzionalità", + "Esigenza di Business", + "Abilità" + ], + "given": [ + "* ", + "Dato ", + "Data ", + "Dati ", + "Date " + ], + "name": "Italian", + "native": "italiano", + "rule": [ + "Regola" + ], + "scenario": [ + "Esempio", + "Scenario" + ], + "scenarioOutline": [ + "Schema dello scenario" + ], + "then": [ + "* ", + "Allora " + ], + "when": [ + "* ", + "Quando " + ] + }, + "ja": { + "and": [ + "* ", + "且つ", + "かつ" + ], + "background": [ + "背景" + ], + "but": [ + "* ", + "然し", + "しかし", + "但し", + "ただし" + ], + "examples": [ + "例", + "サンプル" + ], + "feature": [ + "フィーチャ", + "機能" + ], + "given": [ + "* ", + "前提" + ], + "name": "Japanese", + "native": "日本語", + "rule": [ + "ルール" + ], + "scenario": [ + "シナリオ" + ], + "scenarioOutline": [ + "シナリオアウトライン", + "シナリオテンプレート", + "テンプレ", + "シナリオテンプレ" + ], + "then": [ + "* ", + "ならば" + ], + "when": [ + "* ", + "もし" + ] + }, + "jv": { + "and": [ + "* ", + "Lan " + ], + "background": [ + "Dasar" + ], + "but": [ + "* ", + "Tapi ", + "Nanging ", + "Ananging " + ], + "examples": [ + "Conto", + "Contone" + ], + "feature": [ + "Fitur" + ], + "given": [ + "* ", + "Nalika ", + "Nalikaning " + ], + "name": "Javanese", + "native": "Basa Jawa", + "rule": [ + "Rule" + ], + "scenario": [ + "Skenario" + ], + "scenarioOutline": [ + "Konsep skenario" + ], + "then": [ + "* ", + "Njuk ", + "Banjur " + ], + "when": [ + "* ", + "Manawa ", + "Menawa " + ] + }, + "ka": { + "and": [ + "* ", + "და ", + "ასევე " + ], + "background": [ + "კონტექსტი" + ], + "but": [ + "* ", + "მაგრამ ", + "თუმცა " + ], + "examples": [ + "მაგალითები" + ], + "feature": [ + "თვისება", + "მოთხოვნა" + ], + "given": [ + "* ", + "მოცემული ", + "მოცემულია ", + "ვთქვათ " + ], + "name": "Georgian", + "native": "ქართული", + "rule": [ + "წესი" + ], + "scenario": [ + "მაგალითად", + "მაგალითი", + "მაგ", + "სცენარი" + ], + "scenarioOutline": [ + "სცენარის ნიმუში", + "სცენარის შაბლონი", + "ნიმუში", + "შაბლონი" + ], + "then": [ + "* ", + "მაშინ " + ], + "when": [ + "* ", + "როდესაც ", + "როცა ", + "როგორც კი ", + "თუ " + ] + }, + "kn": { + "and": [ + "* ", + "ಮತ್ತು " + ], + "background": [ + "ಹಿನ್ನೆಲೆ" + ], + "but": [ + "* ", + "ಆದರೆ " + ], + "examples": [ + "ಉದಾಹರಣೆಗಳು" + ], + "feature": [ + "ಹೆಚ್ಚಳ" + ], + "given": [ + "* ", + "ನೀಡಿದ " + ], + "name": "Kannada", + "native": "ಕನ್ನಡ", + "rule": [ + "Rule" + ], + "scenario": [ + "ಉದಾಹರಣೆ", + "ಕಥಾಸಾರಾಂಶ" + ], + "scenarioOutline": [ + "ವಿವರಣೆ" + ], + "then": [ + "* ", + "ನಂತರ " + ], + "when": [ + "* ", + "ಸ್ಥಿತಿಯನ್ನು " + ] + }, + "ko": { + "and": [ + "* ", + "그리고 " + ], + "background": [ + "배경" + ], + "but": [ + "* ", + "하지만 ", + "단 " + ], + "examples": [ + "예" + ], + "feature": [ + "기능" + ], + "given": [ + "* ", + "조건 ", + "먼저 " + ], + "name": "Korean", + "native": "한국어", + "rule": [ + "규칙" + ], + "scenario": [ + "시나리오" + ], + "scenarioOutline": [ + "시나리오 개요" + ], + "then": [ + "* ", + "그러면 " + ], + "when": [ + "* ", + "만일 ", + "만약 " + ] + }, + "lt": { + "and": [ + "* ", + "Ir " + ], + "background": [ + "Kontekstas" + ], + "but": [ + "* ", + "Bet " + ], + "examples": [ + "Pavyzdžiai", + "Scenarijai", + "Variantai" + ], + "feature": [ + "Savybė" + ], + "given": [ + "* ", + "Duota " + ], + "name": "Lithuanian", + "native": "lietuvių kalba", + "rule": [ + "Rule" + ], + "scenario": [ + "Pavyzdys", + "Scenarijus" + ], + "scenarioOutline": [ + "Scenarijaus šablonas" + ], + "then": [ + "* ", + "Tada " + ], + "when": [ + "* ", + "Kai " + ] + }, + "lu": { + "and": [ + "* ", + "an ", + "a " + ], + "background": [ + "Hannergrond" + ], + "but": [ + "* ", + "awer ", + "mä " + ], + "examples": [ + "Beispiller" + ], + "feature": [ + "Funktionalitéit" + ], + "given": [ + "* ", + "ugeholl " + ], + "name": "Luxemburgish", + "native": "Lëtzebuergesch", + "rule": [ + "Rule" + ], + "scenario": [ + "Beispill", + "Szenario" + ], + "scenarioOutline": [ + "Plang vum Szenario" + ], + "then": [ + "* ", + "dann " + ], + "when": [ + "* ", + "wann " + ] + }, + "lv": { + "and": [ + "* ", + "Un " + ], + "background": [ + "Konteksts", + "Situācija" + ], + "but": [ + "* ", + "Bet " + ], + "examples": [ + "Piemēri", + "Paraugs" + ], + "feature": [ + "Funkcionalitāte", + "Fīča" + ], + "given": [ + "* ", + "Kad " + ], + "name": "Latvian", + "native": "latviešu", + "rule": [ + "Rule" + ], + "scenario": [ + "Piemērs", + "Scenārijs" + ], + "scenarioOutline": [ + "Scenārijs pēc parauga" + ], + "then": [ + "* ", + "Tad " + ], + "when": [ + "* ", + "Ja " + ] + }, + "mk-Cyrl": { + "and": [ + "* ", + "И " + ], + "background": [ + "Контекст", + "Содржина" + ], + "but": [ + "* ", + "Но " + ], + "examples": [ + "Примери", + "Сценарија" + ], + "feature": [ + "Функционалност", + "Бизнис потреба", + "Можност" + ], + "given": [ + "* ", + "Дадено ", + "Дадена " + ], + "name": "Macedonian", + "native": "Македонски", + "rule": [ + "Rule" + ], + "scenario": [ + "Пример", + "Сценарио", + "На пример" + ], + "scenarioOutline": [ + "Преглед на сценарија", + "Скица", + "Концепт" + ], + "then": [ + "* ", + "Тогаш " + ], + "when": [ + "* ", + "Кога " + ] + }, + "mk-Latn": { + "and": [ + "* ", + "I " + ], + "background": [ + "Kontekst", + "Sodrzhina" + ], + "but": [ + "* ", + "No " + ], + "examples": [ + "Primeri", + "Scenaria" + ], + "feature": [ + "Funkcionalnost", + "Biznis potreba", + "Mozhnost" + ], + "given": [ + "* ", + "Dadeno ", + "Dadena " + ], + "name": "Macedonian (Latin)", + "native": "Makedonski (Latinica)", + "rule": [ + "Rule" + ], + "scenario": [ + "Scenario", + "Na primer" + ], + "scenarioOutline": [ + "Pregled na scenarija", + "Skica", + "Koncept" + ], + "then": [ + "* ", + "Togash " + ], + "when": [ + "* ", + "Koga " + ] + }, + "mn": { + "and": [ + "* ", + "Мөн ", + "Тэгээд " + ], + "background": [ + "Агуулга" + ], + "but": [ + "* ", + "Гэхдээ ", + "Харин " + ], + "examples": [ + "Тухайлбал" + ], + "feature": [ + "Функц", + "Функционал" + ], + "given": [ + "* ", + "Өгөгдсөн нь ", + "Анх " + ], + "name": "Mongolian", + "native": "монгол", + "rule": [ + "Rule" + ], + "scenario": [ + "Сценар" + ], + "scenarioOutline": [ + "Сценарын төлөвлөгөө" + ], + "then": [ + "* ", + "Тэгэхэд ", + "Үүний дараа " + ], + "when": [ + "* ", + "Хэрэв " + ] + }, + "ne": { + "and": [ + "* ", + "र ", + "अनि " + ], + "background": [ + "पृष्ठभूमी" + ], + "but": [ + "* ", + "तर " + ], + "examples": [ + "उदाहरण", + "उदाहरणहरु" + ], + "feature": [ + "सुविधा", + "विशेषता" + ], + "given": [ + "* ", + "दिइएको ", + "दिएको ", + "यदि " + ], + "name": "Nepali", + "native": "नेपाली", + "rule": [ + "नियम" + ], + "scenario": [ + "परिदृश्य" + ], + "scenarioOutline": [ + "परिदृश्य रूपरेखा" + ], + "then": [ + "* ", + "त्यसपछि ", + "अनी " + ], + "when": [ + "* ", + "जब " + ] + }, + "nl": { + "and": [ + "* ", + "En " + ], + "background": [ + "Achtergrond" + ], + "but": [ + "* ", + "Maar " + ], + "examples": [ + "Voorbeelden" + ], + "feature": [ + "Functionaliteit" + ], + "given": [ + "* ", + "Gegeven ", + "Stel " + ], + "name": "Dutch", + "native": "Nederlands", + "rule": [ + "Regel" + ], + "scenario": [ + "Voorbeeld", + "Scenario" + ], + "scenarioOutline": [ + "Abstract Scenario" + ], + "then": [ + "* ", + "Dan " + ], + "when": [ + "* ", + "Als ", + "Wanneer " + ] + }, + "no": { + "and": [ + "* ", + "Og " + ], + "background": [ + "Bakgrunn" + ], + "but": [ + "* ", + "Men " + ], + "examples": [ + "Eksempler" + ], + "feature": [ + "Egenskap" + ], + "given": [ + "* ", + "Gitt " + ], + "name": "Norwegian", + "native": "norsk", + "rule": [ + "Regel" + ], + "scenario": [ + "Eksempel", + "Scenario" + ], + "scenarioOutline": [ + "Scenariomal", + "Abstrakt Scenario" + ], + "then": [ + "* ", + "Så " + ], + "when": [ + "* ", + "Når " + ] + }, + "pa": { + "and": [ + "* ", + "ਅਤੇ " + ], + "background": [ + "ਪਿਛੋਕੜ" + ], + "but": [ + "* ", + "ਪਰ " + ], + "examples": [ + "ਉਦਾਹਰਨਾਂ" + ], + "feature": [ + "ਖਾਸੀਅਤ", + "ਮੁਹਾਂਦਰਾ", + "ਨਕਸ਼ ਨੁਹਾਰ" + ], + "given": [ + "* ", + "ਜੇਕਰ ", + "ਜਿਵੇਂ ਕਿ " + ], + "name": "Panjabi", + "native": "ਪੰਜਾਬੀ", + "rule": [ + "Rule" + ], + "scenario": [ + "ਉਦਾਹਰਨ", + "ਪਟਕਥਾ" + ], + "scenarioOutline": [ + "ਪਟਕਥਾ ਢਾਂਚਾ", + "ਪਟਕਥਾ ਰੂਪ ਰੇਖਾ" + ], + "then": [ + "* ", + "ਤਦ " + ], + "when": [ + "* ", + "ਜਦੋਂ " + ] + }, + "pl": { + "and": [ + "* ", + "Oraz ", + "I " + ], + "background": [ + "Założenia" + ], + "but": [ + "* ", + "Ale " + ], + "examples": [ + "Przykłady" + ], + "feature": [ + "Właściwość", + "Funkcja", + "Aspekt", + "Potrzeba biznesowa" + ], + "given": [ + "* ", + "Zakładając ", + "Mając ", + "Zakładając, że " + ], + "name": "Polish", + "native": "polski", + "rule": [ + "Zasada", + "Reguła" + ], + "scenario": [ + "Przykład", + "Scenariusz" + ], + "scenarioOutline": [ + "Szablon scenariusza" + ], + "then": [ + "* ", + "Wtedy " + ], + "when": [ + "* ", + "Jeżeli ", + "Jeśli ", + "Gdy ", + "Kiedy " + ] + }, + "pt": { + "and": [ + "* ", + "E " + ], + "background": [ + "Contexto", + "Cenário de Fundo", + "Cenario de Fundo", + "Fundo" + ], + "but": [ + "* ", + "Mas " + ], + "examples": [ + "Exemplos", + "Cenários", + "Cenarios" + ], + "feature": [ + "Funcionalidade", + "Característica", + "Caracteristica" + ], + "given": [ + "* ", + "Dado ", + "Dada ", + "Dados ", + "Dadas " + ], + "name": "Portuguese", + "native": "português", + "rule": [ + "Regra" + ], + "scenario": [ + "Exemplo", + "Cenário", + "Cenario" + ], + "scenarioOutline": [ + "Esquema do Cenário", + "Esquema do Cenario", + "Delineação do Cenário", + "Delineacao do Cenario" + ], + "then": [ + "* ", + "Então ", + "Entao " + ], + "when": [ + "* ", + "Quando " + ] + }, + "ro": { + "and": [ + "* ", + "Si ", + "Și ", + "Şi " + ], + "background": [ + "Context" + ], + "but": [ + "* ", + "Dar " + ], + "examples": [ + "Exemple" + ], + "feature": [ + "Functionalitate", + "Funcționalitate", + "Funcţionalitate" + ], + "given": [ + "* ", + "Date fiind ", + "Dat fiind ", + "Dată fiind", + "Dati fiind ", + "Dați fiind ", + "Daţi fiind " + ], + "name": "Romanian", + "native": "română", + "rule": [ + "Rule" + ], + "scenario": [ + "Exemplu", + "Scenariu" + ], + "scenarioOutline": [ + "Structura scenariu", + "Structură scenariu" + ], + "then": [ + "* ", + "Atunci " + ], + "when": [ + "* ", + "Cand ", + "Când " + ] + }, + "ru": { + "and": [ + "* ", + "И ", + "К тому же ", + "Также " + ], + "background": [ + "Предыстория", + "Контекст" + ], + "but": [ + "* ", + "Но ", + "А ", + "Иначе " + ], + "examples": [ + "Примеры", + "Значения" + ], + "feature": [ + "Функция", + "Функциональность", + "Функционал", + "Свойство", + "Фича" + ], + "given": [ + "* ", + "Допустим ", + "Дано ", + "Пусть " + ], + "name": "Russian", + "native": "русский", + "rule": [ + "Правило" + ], + "scenario": [ + "Пример", + "Сценарий" + ], + "scenarioOutline": [ + "Структура сценария", + "Шаблон сценария" + ], + "then": [ + "* ", + "То ", + "Затем ", + "Тогда " + ], + "when": [ + "* ", + "Когда ", + "Если " + ] + }, + "sk": { + "and": [ + "* ", + "A ", + "A tiež ", + "A taktiež ", + "A zároveň " + ], + "background": [ + "Pozadie" + ], + "but": [ + "* ", + "Ale " + ], + "examples": [ + "Príklady" + ], + "feature": [ + "Požiadavka", + "Funkcia", + "Vlastnosť" + ], + "given": [ + "* ", + "Pokiaľ ", + "Za predpokladu " + ], + "name": "Slovak", + "native": "Slovensky", + "rule": [ + "Rule" + ], + "scenario": [ + "Príklad", + "Scenár" + ], + "scenarioOutline": [ + "Náčrt Scenáru", + "Náčrt Scenára", + "Osnova Scenára" + ], + "then": [ + "* ", + "Tak ", + "Potom " + ], + "when": [ + "* ", + "Keď ", + "Ak " + ] + }, + "sl": { + "and": [ + "In ", + "Ter " + ], + "background": [ + "Kontekst", + "Osnova", + "Ozadje" + ], + "but": [ + "Toda ", + "Ampak ", + "Vendar " + ], + "examples": [ + "Primeri", + "Scenariji" + ], + "feature": [ + "Funkcionalnost", + "Funkcija", + "Možnosti", + "Moznosti", + "Lastnost", + "Značilnost" + ], + "given": [ + "Dano ", + "Podano ", + "Zaradi ", + "Privzeto " + ], + "name": "Slovenian", + "native": "Slovenski", + "rule": [ + "Rule" + ], + "scenario": [ + "Primer", + "Scenarij" + ], + "scenarioOutline": [ + "Struktura scenarija", + "Skica", + "Koncept", + "Oris scenarija", + "Osnutek" + ], + "then": [ + "Nato ", + "Potem ", + "Takrat " + ], + "when": [ + "Ko ", + "Ce ", + "Če ", + "Kadar " + ] + }, + "sr-Cyrl": { + "and": [ + "* ", + "И " + ], + "background": [ + "Контекст", + "Основа", + "Позадина" + ], + "but": [ + "* ", + "Али " + ], + "examples": [ + "Примери", + "Сценарији" + ], + "feature": [ + "Функционалност", + "Могућност", + "Особина" + ], + "given": [ + "* ", + "За дато ", + "За дате ", + "За дати " + ], + "name": "Serbian", + "native": "Српски", + "rule": [ + "Правило" + ], + "scenario": [ + "Сценарио", + "Пример" + ], + "scenarioOutline": [ + "Структура сценарија", + "Скица", + "Концепт" + ], + "then": [ + "* ", + "Онда " + ], + "when": [ + "* ", + "Када ", + "Кад " + ] + }, + "sr-Latn": { + "and": [ + "* ", + "I " + ], + "background": [ + "Kontekst", + "Osnova", + "Pozadina" + ], + "but": [ + "* ", + "Ali " + ], + "examples": [ + "Primeri", + "Scenariji" + ], + "feature": [ + "Funkcionalnost", + "Mogućnost", + "Mogucnost", + "Osobina" + ], + "given": [ + "* ", + "Za dato ", + "Za date ", + "Za dati " + ], + "name": "Serbian (Latin)", + "native": "Srpski (Latinica)", + "rule": [ + "Pravilo" + ], + "scenario": [ + "Scenario", + "Primer" + ], + "scenarioOutline": [ + "Struktura scenarija", + "Skica", + "Koncept" + ], + "then": [ + "* ", + "Onda " + ], + "when": [ + "* ", + "Kada ", + "Kad " + ] + }, + "sv": { + "and": [ + "* ", + "Och " + ], + "background": [ + "Bakgrund" + ], + "but": [ + "* ", + "Men " + ], + "examples": [ + "Exempel" + ], + "feature": [ + "Egenskap" + ], + "given": [ + "* ", + "Givet " + ], + "name": "Swedish", + "native": "Svenska", + "rule": [ + "Regel" + ], + "scenario": [ + "Scenario" + ], + "scenarioOutline": [ + "Abstrakt Scenario", + "Scenariomall" + ], + "then": [ + "* ", + "Så " + ], + "when": [ + "* ", + "När " + ] + }, + "ta": { + "and": [ + "* ", + "மேலும் ", + "மற்றும் " + ], + "background": [ + "பின்னணி" + ], + "but": [ + "* ", + "ஆனால் " + ], + "examples": [ + "எடுத்துக்காட்டுகள்", + "காட்சிகள்", + "நிலைமைகளில்" + ], + "feature": [ + "அம்சம்", + "வணிக தேவை", + "திறன்" + ], + "given": [ + "* ", + "கொடுக்கப்பட்ட " + ], + "name": "Tamil", + "native": "தமிழ்", + "rule": [ + "Rule" + ], + "scenario": [ + "உதாரணமாக", + "காட்சி" + ], + "scenarioOutline": [ + "காட்சி சுருக்கம்", + "காட்சி வார்ப்புரு" + ], + "then": [ + "* ", + "அப்பொழுது " + ], + "when": [ + "* ", + "எப்போது " + ] + }, + "th": { + "and": [ + "* ", + "และ " + ], + "background": [ + "แนวคิด" + ], + "but": [ + "* ", + "แต่ " + ], + "examples": [ + "ชุดของตัวอย่าง", + "ชุดของเหตุการณ์" + ], + "feature": [ + "โครงหลัก", + "ความต้องการทางธุรกิจ", + "ความสามารถ" + ], + "given": [ + "* ", + "กำหนดให้ " + ], + "name": "Thai", + "native": "ไทย", + "rule": [ + "Rule" + ], + "scenario": [ + "เหตุการณ์" + ], + "scenarioOutline": [ + "สรุปเหตุการณ์", + "โครงสร้างของเหตุการณ์" + ], + "then": [ + "* ", + "ดังนั้น " + ], + "when": [ + "* ", + "เมื่อ " + ] + }, + "te": { + "and": [ + "* ", + "మరియు " + ], + "background": [ + "నేపథ్యం" + ], + "but": [ + "* ", + "కాని " + ], + "examples": [ + "ఉదాహరణలు" + ], + "feature": [ + "గుణము" + ], + "given": [ + "* ", + "చెప్పబడినది " + ], + "name": "Telugu", + "native": "తెలుగు", + "rule": [ + "Rule" + ], + "scenario": [ + "ఉదాహరణ", + "సన్నివేశం" + ], + "scenarioOutline": [ + "కథనం" + ], + "then": [ + "* ", + "అప్పుడు " + ], + "when": [ + "* ", + "ఈ పరిస్థితిలో " + ] + }, + "tlh": { + "and": [ + "* ", + "'ej ", + "latlh " + ], + "background": [ + "mo'" + ], + "but": [ + "* ", + "'ach ", + "'a " + ], + "examples": [ + "ghantoH", + "lutmey" + ], + "feature": [ + "Qap", + "Qu'meH 'ut", + "perbogh", + "poQbogh malja'", + "laH" + ], + "given": [ + "* ", + "ghu' noblu' ", + "DaH ghu' bejlu' " + ], + "name": "Klingon", + "native": "tlhIngan", + "rule": [ + "Rule" + ], + "scenario": [ + "lut" + ], + "scenarioOutline": [ + "lut chovnatlh" + ], + "then": [ + "* ", + "vaj " + ], + "when": [ + "* ", + "qaSDI' " + ] + }, + "tr": { + "and": [ + "* ", + "Ve ", + "Hem de ", + "Bir de ", + "Ayrıca ", + "İlaveten ", + "Buna ek olarak " + ], + "background": [ + "Geçmiş", + "Arka Plan", + "Ön Koşul", + "Önkoşul", + "Önceki Durum", + "Giriş", + "Mukaddime", + "Mevcut Durum" + ], + "but": [ + "* ", + "Fakat ", + "Ama ", + "Ancak ", + "Yalnız ", + "Lakin ", + "Meğer ki ", + "Buna mukabil ", + "Aksi halde " + ], + "examples": [ + "Örnekler", + "Değerler" + ], + "feature": [ + "Özellik", + "İş Gereksinimi", + "Gereksinim", + "İşlev", + "Kullanıcı Hikayesi", + "Yetenek", + "Teknik Gereksinim" + ], + "given": [ + "* ", + "Mevcut ", + "Önceden ", + "Geçmişte ", + "Daha önce ", + "Halihazırda ", + "Zaten ", + "Sistemde ", + "Diyelim ki ", + "Varsayalım ki ", + "Farz edelim ki ", + "Kabul edelim ki ", + "Başlangıçta ", + "Varsayılan olarak ", + "Biliniyor ki " + ], + "name": "Turkish", + "native": "Türkçe", + "rule": [ + "Kural", + "İş Kuralı", + "Kaide", + "Hüküm", + "Madde" + ], + "scenario": [ + "Örnek", + "Senaryo", + "Durum", + "Vaka" + ], + "scenarioOutline": [ + "Senaryo taslağı", + "Senaryo şablonu" + ], + "then": [ + "* ", + "Beklenen ", + "O zaman ", + "Sonuç olarak ", + "Böylece ", + "Bunun üzerine ", + "Bu durumda ", + "O takdirde ", + "Şu halde ", + "Netice itibariyle ", + "Buna binaen " + ], + "when": [ + "* ", + "Eğer ", + "Eğer ki ", + "Ne zaman ", + "Ne zaman ki ", + "Şayet " + ] + }, + "tt": { + "and": [ + "* ", + "Һәм ", + "Вә " + ], + "background": [ + "Кереш" + ], + "but": [ + "* ", + "Ләкин ", + "Әмма " + ], + "examples": [ + "Үрнәкләр", + "Мисаллар" + ], + "feature": [ + "Мөмкинлек", + "Үзенчәлеклелек" + ], + "given": [ + "* ", + "Әйтик " + ], + "name": "Tatar", + "native": "Татарча", + "rule": [ + "Rule" + ], + "scenario": [ + "Сценарий" + ], + "scenarioOutline": [ + "Сценарийның төзелеше" + ], + "then": [ + "* ", + "Нәтиҗәдә " + ], + "when": [ + "* ", + "Әгәр " + ] + }, + "uk": { + "and": [ + "* ", + "І ", + "А також ", + "Та " + ], + "background": [ + "Передумова" + ], + "but": [ + "* ", + "Але " + ], + "examples": [ + "Приклади" + ], + "feature": [ + "Функціонал" + ], + "given": [ + "* ", + "Припустимо ", + "Припустимо, що ", + "Нехай ", + "Дано " + ], + "name": "Ukrainian", + "native": "Українська", + "rule": [ + "Rule" + ], + "scenario": [ + "Приклад", + "Сценарій" + ], + "scenarioOutline": [ + "Структура сценарію" + ], + "then": [ + "* ", + "То ", + "Тоді " + ], + "when": [ + "* ", + "Якщо ", + "Коли " + ] + }, + "ur": { + "and": [ + "* ", + "اور " + ], + "background": [ + "پس منظر" + ], + "but": [ + "* ", + "لیکن " + ], + "examples": [ + "مثالیں" + ], + "feature": [ + "صلاحیت", + "کاروبار کی ضرورت", + "خصوصیت" + ], + "given": [ + "* ", + "اگر ", + "بالفرض ", + "فرض کیا " + ], + "name": "Urdu", + "native": "اردو", + "rule": [ + "Rule" + ], + "scenario": [ + "منظرنامہ" + ], + "scenarioOutline": [ + "منظر نامے کا خاکہ" + ], + "then": [ + "* ", + "پھر ", + "تب " + ], + "when": [ + "* ", + "جب " + ] + }, + "uz": { + "and": [ + "* ", + "Ва " + ], + "background": [ + "Тарих" + ], + "but": [ + "* ", + "Лекин ", + "Бирок ", + "Аммо " + ], + "examples": [ + "Мисоллар" + ], + "feature": [ + "Функционал" + ], + "given": [ + "* ", + "Belgilangan " + ], + "name": "Uzbek", + "native": "Узбекча", + "rule": [ + "Rule" + ], + "scenario": [ + "Сценарий" + ], + "scenarioOutline": [ + "Сценарий структураси" + ], + "then": [ + "* ", + "Унда " + ], + "when": [ + "* ", + "Агар " + ] + }, + "vi": { + "and": [ + "* ", + "Và " + ], + "background": [ + "Bối cảnh" + ], + "but": [ + "* ", + "Nhưng " + ], + "examples": [ + "Dữ liệu" + ], + "feature": [ + "Tính năng" + ], + "given": [ + "* ", + "Biết ", + "Cho " + ], + "name": "Vietnamese", + "native": "Tiếng Việt", + "rule": [ + "Quy tắc" + ], + "scenario": [ + "Tình huống", + "Kịch bản" + ], + "scenarioOutline": [ + "Khung tình huống", + "Khung kịch bản" + ], + "then": [ + "* ", + "Thì " + ], + "when": [ + "* ", + "Khi " + ] + }, + "zh-CN": { + "and": [ + "* ", + "而且", + "并且", + "同时" + ], + "background": [ + "背景" + ], + "but": [ + "* ", + "但是" + ], + "examples": [ + "例子" + ], + "feature": [ + "功能" + ], + "given": [ + "* ", + "假如", + "假设", + "假定" + ], + "name": "Chinese simplified", + "native": "简体中文", + "rule": [ + "Rule", + "规则" + ], + "scenario": [ + "场景", + "剧本" + ], + "scenarioOutline": [ + "场景大纲", + "剧本大纲" + ], + "then": [ + "* ", + "那么" + ], + "when": [ + "* ", + "当" + ] + }, + "ml": { + "and": [ + "* ", + "ഒപ്പം" + ], + "background": [ + "പശ്ചാത്തലം" + ], + "but": [ + "* ", + "പക്ഷേ" + ], + "examples": [ + "ഉദാഹരണങ്ങൾ" + ], + "feature": [ + "സവിശേഷത" + ], + "given": [ + "* ", + "നൽകിയത്" + ], + "name": "Malayalam", + "native": "മലയാളം", + "rule": [ + "നിയമം" + ], + "scenario": [ + "രംഗം" + ], + "scenarioOutline": [ + "സാഹചര്യത്തിന്റെ രൂപരേഖ" + ], + "then": [ + "* ", + "പിന്നെ" + ], + "when": [ + "എപ്പോൾ" + ] + }, + "zh-TW": { + "and": [ + "* ", + "而且", + "並且", + "同時" + ], + "background": [ + "背景" + ], + "but": [ + "* ", + "但是" + ], + "examples": [ + "例子" + ], + "feature": [ + "功能" + ], + "given": [ + "* ", + "假如", + "假設", + "假定" + ], + "name": "Chinese traditional", + "native": "繁體中文", + "rule": [ + "Rule" + ], + "scenario": [ + "場景", + "劇本" + ], + "scenarioOutline": [ + "場景大綱", + "劇本大綱" + ], + "then": [ + "* ", + "那麼" + ], + "when": [ + "* ", + "當" + ] + }, + "mr": { + "and": [ + "* ", + "आणि ", + "तसेच " + ], + "background": [ + "पार्श्वभूमी" + ], + "but": [ + "* ", + "पण ", + "परंतु " + ], + "examples": [ + "उदाहरण" + ], + "feature": [ + "वैशिष्ट्य", + "सुविधा" + ], + "given": [ + "* ", + "जर", + "दिलेल्या प्रमाणे " + ], + "name": "Marathi", + "native": "मराठी", + "rule": [ + "नियम" + ], + "scenario": [ + "परिदृश्य" + ], + "scenarioOutline": [ + "परिदृश्य रूपरेखा" + ], + "then": [ + "* ", + "मग ", + "तेव्हा " + ], + "when": [ + "* ", + "जेव्हा " + ] + }, + "amh": { + "and": [ + "* ", + "እና " + ], + "background": [ + "ቅድመ ሁኔታ", + "መነሻ", + "መነሻ ሀሳብ" + ], + "but": [ + "* ", + "ግን " + ], + "examples": [ + "ምሳሌዎች", + "ሁናቴዎች" + ], + "feature": [ + "ስራ", + "የተፈለገው ስራ", + "የሚፈለገው ድርጊት" + ], + "given": [ + "* ", + "የተሰጠ " + ], + "name": "Amharic", + "native": "አማርኛ", + "rule": [ + "ህግ" + ], + "scenario": [ + "ምሳሌ", + "ሁናቴ" + ], + "scenarioOutline": [ + "ሁናቴ ዝርዝር", + "ሁናቴ አብነት" + ], + "then": [ + "* ", + "ከዚያ " + ], + "when": [ + "* ", + "መቼ " + ] + } +} diff --git a/src/SpecHygiene/Analysis/Reqnroll/GherkinNarrativeGuard.cs b/src/SpecHygiene/Analysis/Reqnroll/GherkinNarrativeGuard.cs index a3d8ff2..f6bd135 100644 --- a/src/SpecHygiene/Analysis/Reqnroll/GherkinNarrativeGuard.cs +++ b/src/SpecHygiene/Analysis/Reqnroll/GherkinNarrativeGuard.cs @@ -16,6 +16,7 @@ // error.", continuing line 3). Both were wrongly parsed as real Given step lines and reported // as "undefined step" - pure narrative text that Reqnroll never evaluates as a step at all. using System.Text.RegularExpressions; +using SpecHygiene.Models; namespace SpecHygiene.Analysis.Reqnroll { @@ -48,6 +49,32 @@ public static bool TryMatchStepLine(string line, out string rawKeyword, out stri return true; } + /// + /// Dialect-aware step-line match. Faithful to the real parser, keywords are matched + /// CASE-SENSITIVELY (a lowercase keyword is narrative, not a step), same as the English + /// . are the dialect's step + /// keywords, trimmed of any trailing space (e.g. "Angenommen", "Wenn", "*"). + /// + public static bool TryMatchStepLine(string line, IReadOnlyList stepKeywords, out string rawKeyword, out string text) + { + var trimmed = line.TrimStart(); + foreach (var kw in stepKeywords) + { + // keyword, then at least one whitespace, then the step text + if (trimmed.Length > kw.Length && + char.IsWhiteSpace(trimmed[kw.Length]) && + trimmed.StartsWith(kw, StringComparison.Ordinal)) + { + rawKeyword = kw; + text = trimmed.Substring(kw.Length).Trim(); + return text.Length > 0; + } + } + rawKeyword = ""; + text = ""; + return false; + } + /// /// For each line index, returns whether that line occurs at or after the first /// Background:/Scenario:/Scenario Outline:/Examples: header in the file. Lines before @@ -69,5 +96,28 @@ public static bool[] ComputeInsideBlockFlags(string[] lines) } return flags; } + + /// + /// Dialect-aware overload of : a block + /// boundary is any Background/Scenario/Scenario Outline/Examples header in the file's + /// dialect. Without this, a localized file's narrative preamble is never left, so its + /// steps are never parsed. + /// + public static bool[] ComputeInsideBlockFlags(string[] lines, GherkinDialect dialect) + { + var flags = new bool[lines.Length]; + bool insideBlock = false; + for (int i = 0; i < lines.Length; i++) + { + var trimmed = lines[i].TrimStart(); + if (trimmed.Length > 0 && + (dialect.IsBackground(trimmed) || dialect.IsScenarioStart(trimmed) || dialect.IsExamples(trimmed))) + { + insideBlock = true; + } + flags[i] = insideBlock; + } + return flags; + } } } diff --git a/src/SpecHygiene/Analysis/StepDefinitionCoverageAnalyzer.cs b/src/SpecHygiene/Analysis/StepDefinitionCoverageAnalyzer.cs index c3096d0..93fb90c 100644 --- a/src/SpecHygiene/Analysis/StepDefinitionCoverageAnalyzer.cs +++ b/src/SpecHygiene/Analysis/StepDefinitionCoverageAnalyzer.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using SpecHygiene.Analysis.Reqnroll; using SpecHygiene.Models; +using SpecHygiene.Services; namespace SpecHygiene.Analysis; @@ -30,14 +31,13 @@ public class StepDefinitionCoverageAnalyzer /// private List _indeterminate = new(); - private static readonly Regex ScenarioRegex = new( - @"^\s*(Scenario|Scenario Outline):\s*(.+)$", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex TagRegex = new( @"@[\w-]+", RegexOptions.Compiled); - + + // English keyword prefixes stripped by NormalizeForMatching. This runs on step text that is + // ALREADY keyword-stripped (TryMatchStepLine removed it) and on binding patterns (English-authored + // regex), so localized keywords never reach here — dialect handling is not needed at this site. private static readonly string[] StepKeywords = { "Given ", "When ", "Then ", "And ", "But ", "* " }; /// @@ -567,9 +567,13 @@ private bool TryNormalizedTextMatch(string stepText, string pattern) var projectName = ExtractProjectName(filePath); var featureFileName = Path.GetFileName(filePath); + // Resolve the file's Gherkin dialect from its "# language:" header (English if absent) so + // localized keywords (Szenario:, Beispiele:, Angenommen …) are recognized. + var dialect = GherkinDialectProvider.Default.Detect(lines); + // Lines before the first Background:/Scenario:/Examples: header are the Feature's free-text // narrative and are never steps, however they start. - var insideBlock = GherkinNarrativeGuard.ComputeInsideBlockFlags(lines); + var insideBlock = GherkinNarrativeGuard.ComputeInsideBlockFlags(lines, dialect); var featureTags = new List(); var currentTags = new List(); @@ -625,22 +629,21 @@ void AddStep(string text, string full, int line, bool ignoredScenario, StepKeywo continue; } - if (line.StartsWith("Feature:", StringComparison.OrdinalIgnoreCase)) + if (dialect.IsFeature(line)) continue; - if (line.StartsWith("Background:", StringComparison.OrdinalIgnoreCase)) + if (dialect.IsBackground(line)) { inBackground = true; inExamples = false; outlineSteps.Clear(); lastConcreteKeyword = null; currentScenario = "Background"; currentTags.Clear(); continue; } - var scenarioMatch = ScenarioRegex.Match(line); - if (scenarioMatch.Success) + if (dialect.IsScenarioStart(line)) { inBackground = false; inExamples = false; exampleHeaders = null; outlineSteps.Clear(); lastConcreteKeyword = null; - currentScenario = scenarioMatch.Groups[2].Value.Trim(); + currentScenario = line.Split(':', 2).LastOrDefault()?.Trim() ?? string.Empty; var allTags = featureTags.Concat(currentTags).ToList(); var ignoreTag = allTags.FirstOrDefault(t => _ignoreTags.Any(it => t.Equals(it, StringComparison.OrdinalIgnoreCase))); @@ -655,7 +658,7 @@ void AddStep(string text, string full, int line, bool ignoredScenario, StepKeywo continue; } - if (line.StartsWith("Examples:", StringComparison.OrdinalIgnoreCase)) + if (dialect.IsExamples(line)) { inExamples = true; exampleHeaders = null; continue; @@ -683,7 +686,7 @@ void AddStep(string text, string full, int line, bool ignoredScenario, StepKeywo // Gherkin and which our parser has always accepted. Keep it: dropping those lines would shrink // the corpus and turn live bindings dead — the one direction this change must never move. if (insideBlock[i] && !string.IsNullOrEmpty(currentScenario) - && TryMatchStepLine(line, out var stepText, out var rawKeyword)) + && TryMatchStepLine(line, dialect, out var stepText, out var rawKeyword)) { // Buffer every block's steps: an Examples: table may follow under either keyword. var resolved = ResolveKeyword(rawKeyword, ref lastConcreteKeyword); @@ -712,24 +715,12 @@ private static string Substitute(string stepText, string[] headers, string[] val } /// - /// Real Gherkin step-line grammar (case-sensitive, per ), plus the - /// "* " asterisk step the guard doesn't model. Returns the step text without its keyword. + /// Real Gherkin step-line grammar (case-sensitive, per ) for the + /// file's dialect. The dialect's step keywords already include the "* " asterisk step, so it is + /// matched here too. Returns the step text without its keyword. /// - private static bool TryMatchStepLine(string line, out string stepText, out string rawKeyword) - { - if (GherkinNarrativeGuard.TryMatchStepLine(line, out rawKeyword, out stepText)) return true; - - if (line.StartsWith("* ", StringComparison.Ordinal)) - { - stepText = line.Substring(2).Trim(); - rawKeyword = "*"; - return stepText.Length > 0; - } - - stepText = ""; - rawKeyword = ""; - return false; - } + private static bool TryMatchStepLine(string line, GherkinDialect dialect, out string stepText, out string rawKeyword) => + GherkinNarrativeGuard.TryMatchStepLine(line, dialect.StepKeywords, out rawKeyword, out stepText); /// /// Resolve a written keyword to the concrete one Reqnroll binds on. Given/When/Then are concrete diff --git a/src/SpecHygiene/Models/GherkinDialect.cs b/src/SpecHygiene/Models/GherkinDialect.cs new file mode 100644 index 0000000..656dd54 --- /dev/null +++ b/src/SpecHygiene/Models/GherkinDialect.cs @@ -0,0 +1,96 @@ +namespace SpecHygiene.Models; + +/// +/// The set of Gherkin keywords for one language dialect (e.g. en, de). Sourced from the +/// canonical gherkin-languages.json that Gherkin/Reqnroll ship, so a localized feature file +/// (# language: de with Szenario:, Beispiele:) is recognized exactly like its +/// English equivalent. +/// +/// Block keywords (Feature/Background/Scenario/Examples) are matched as keyword + ":"; +/// step keywords (Given/When/Then/And/But and their aliases) are stored trimmed and matched as +/// keyword + " ". A dialect can have several keywords per concept (German scenario = +/// Szenario or Beispiel), which is why every concept is a list. +/// +public sealed class GherkinDialect +{ + public string Language { get; } + public IReadOnlyList Feature { get; } + public IReadOnlyList Background { get; } + public IReadOnlyList Scenario { get; } + public IReadOnlyList ScenarioOutline { get; } + public IReadOnlyList Examples { get; } + public IReadOnlyList Rule { get; } + + /// Given/When/Then/And/But keywords (and aliases), trimmed of the trailing space the + /// source data carries — e.g. "Given " → "Given", "* " → "*". + public IReadOnlyList StepKeywords { get; } + + public GherkinDialect( + string language, + IReadOnlyList feature, + IReadOnlyList background, + IReadOnlyList scenario, + IReadOnlyList scenarioOutline, + IReadOnlyList examples, + IReadOnlyList rule, + IReadOnlyList stepKeywords) + { + Language = language; + Feature = feature; + Background = background; + Scenario = scenario; + ScenarioOutline = scenarioOutline; + Examples = examples; + Rule = rule; + StepKeywords = stepKeywords; + } + + public bool IsFeature(string line) => MatchesBlock(line, Feature); + public bool IsBackground(string line) => MatchesBlock(line, Background); + public bool IsScenario(string line) => MatchesBlock(line, Scenario); + public bool IsScenarioOutline(string line) => MatchesBlock(line, ScenarioOutline); + public bool IsExamples(string line) => MatchesBlock(line, Examples); + + /// True for a plain scenario OR a scenario outline/template start line. + public bool IsScenarioStart(string line) => IsScenarioOutline(line) || IsScenario(line); + + /// A block keyword must be immediately followed by a colon (Scenario:), matching + /// the historical literal checks. Outline keywords are checked before plain scenario keywords by + /// callers so Scenario Outline: is never mistaken for Scenario:. + private static bool MatchesBlock(string line, IReadOnlyList keywords) + { + foreach (var kw in keywords) + { + if (line.Length > kw.Length && + line[kw.Length] == ':' && + line.StartsWith(kw, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + /// True if the line begins with a step keyword followed by a space. Pass + /// to override (e.g. user-configured English step keywords). + public bool IsStep(string line, IReadOnlyList? keywords = null) + { + foreach (var kw in keywords ?? StepKeywords) + { + if (line.StartsWith(kw + " ", StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + /// Removes a leading step keyword from a step, returning the trimmed remainder. + public string StripStepKeyword(string stepText, IReadOnlyList? keywords = null) + { + foreach (var kw in keywords ?? StepKeywords) + { + if (stepText.StartsWith(kw + " ", StringComparison.OrdinalIgnoreCase)) + return stepText.Substring(kw.Length).Trim(); + } + return stepText.Trim(); + } +} diff --git a/src/SpecHygiene/Services/GherkinDialectProvider.cs b/src/SpecHygiene/Services/GherkinDialectProvider.cs new file mode 100644 index 0000000..c9b0046 --- /dev/null +++ b/src/SpecHygiene/Services/GherkinDialectProvider.cs @@ -0,0 +1,112 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.RegularExpressions; +using SpecHygiene.Models; + +namespace SpecHygiene.Services; + +/// +/// Loads Gherkin keyword dialects from the embedded canonical gherkin-languages.json and +/// detects a feature file's dialect from its # language: header. Absent a header, the file is +/// treated as English, so existing behavior is unchanged. +/// +/// One shared, immutable instance is enough for a whole run — use . +/// +public sealed class GherkinDialectProvider +{ + public const string DefaultLanguage = "en"; + + // Gherkin's language header: the first significant line may be "# language: xx". + private static readonly Regex LanguageHeaderRegex = + new(@"^#\s*language\s*:\s*([A-Za-z0-9_-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private readonly Dictionary _dialects; + + public static GherkinDialectProvider Default { get; } = new(); + + public GherkinDialectProvider() + { + _dialects = LoadEmbedded(); + } + + private static Dictionary LoadEmbedded() + { + var assembly = typeof(GherkinDialectProvider).Assembly; + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("gherkin-languages.json", StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidOperationException("Embedded gherkin-languages.json resource not found."); + + using var stream = assembly.GetManifestResourceStream(resourceName)!; + using var doc = JsonDocument.Parse(stream); + + var dialects = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var lang in doc.RootElement.EnumerateObject()) + { + var e = lang.Value; + dialects[lang.Name] = new GherkinDialect( + lang.Name, + Array(e, "feature"), + Array(e, "background"), + Array(e, "scenario"), + Array(e, "scenarioOutline"), + Array(e, "examples"), + Array(e, "rule"), + StepKeywords(e)); + } + return dialects; + } + + private static string[] Array(JsonElement element, string name) => + element.TryGetProperty(name, out var arr) && arr.ValueKind == JsonValueKind.Array + ? arr.EnumerateArray().Select(x => x.GetString()!).Where(s => s is not null).ToArray() + : System.Array.Empty(); + + // Given/When/Then/And/But collapsed into one distinct set, trimmed of the trailing space the + // source carries ("Given " -> "Given", "* " -> "*"). + private static string[] StepKeywords(JsonElement element) + { + var result = new List(); + foreach (var concept in new[] { "given", "when", "then", "and", "but" }) + { + foreach (var kw in Array(element, concept)) + { + var trimmed = kw.TrimEnd(); + if (trimmed.Length > 0 && !result.Contains(trimmed, StringComparer.OrdinalIgnoreCase)) + result.Add(trimmed); + } + } + return result.ToArray(); + } + + /// + /// Detects the dialect from the file's # language: header. Scans leading blank/comment + /// lines; the first # language: xx wins. If there is no header (or the code is unknown), + /// falls back to (default English) so existing files are + /// unaffected. + /// + public GherkinDialect Detect(IEnumerable lines, string? fallbackLanguage = null) + { + foreach (var raw in lines) + { + var line = raw.Trim(); + if (line.Length == 0) + continue; + + var match = LanguageHeaderRegex.Match(line); + if (match.Success) + return GetDialect(match.Groups[1].Value) + ?? GetDialect(fallbackLanguage ?? DefaultLanguage)!; + + if (line.StartsWith("#")) + continue; // some other comment before the header — keep looking + + break; // first real content line: past the header zone + } + + return GetDialect(fallbackLanguage ?? DefaultLanguage)!; + } + + /// Returns the dialect for a language code, or null if unknown. Never null for "en". + public GherkinDialect? GetDialect(string language) => + _dialects.TryGetValue(language, out var dialect) ? dialect : null; +} diff --git a/src/SpecHygiene/SpecHygiene.csproj b/src/SpecHygiene/SpecHygiene.csproj index ebfc3e7..b44e45d 100644 --- a/src/SpecHygiene/SpecHygiene.csproj +++ b/src/SpecHygiene/SpecHygiene.csproj @@ -37,6 +37,11 @@ + + + + + PreserveNewest diff --git a/tests/SpecHygiene.Tests/GherkinDialectCoverageTests.cs b/tests/SpecHygiene.Tests/GherkinDialectCoverageTests.cs new file mode 100644 index 0000000..0614fbd --- /dev/null +++ b/tests/SpecHygiene.Tests/GherkinDialectCoverageTests.cs @@ -0,0 +1,89 @@ +using SpecHygiene.Analysis; +using SpecHygiene.Models; +using Xunit; + +namespace SpecHygiene.Tests; + +/// +/// Proves the step-definition coverage path is dialect-aware (issue #1). A binding is counted USED +/// iff a matching step entered the corpus, so a German "# language: de" step matching a binding +/// demonstrates that localized keywords (Szenario:, Angenommen …) are now parsed. Before the fix the +/// German block boundaries were invisible, the step never entered the corpus, and the binding was +/// wrongly reported as unused/dead. +/// +public sealed class GherkinDialectCoverageTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "bdd-i18n-corpus-" + Guid.NewGuid().ToString("N")); + private readonly string _features; + private readonly string _stepsDir; + + public GherkinDialectCoverageTests() + { + _features = Path.Combine(_root, "Features"); + _stepsDir = Path.Combine(_root, "Steps"); + Directory.CreateDirectory(_features); + Directory.CreateDirectory(_stepsDir); + } + + public void Dispose() + { + try { Directory.Delete(_root, recursive: true); } catch { /* best-effort temp cleanup */ } + } + + private void Feature(string name, string content) => + File.WriteAllText(Path.Combine(_features, name + ".feature"), content.ReplaceLineEndings("\n")); + + private void Binding(string pattern) => + File.WriteAllText(Path.Combine(_stepsDir, "Steps.cs"), $$""" + using Reqnroll; + namespace X; + + [Binding] + public class S + { + [Given(@"{{pattern}}")] + public void TheStep() { } + } + """.ReplaceLineEndings("\n")); + + private StepDefinitionCoverageReport Run() => + new StepDefinitionCoverageAnalyzer().AnalyzeCoverage(new[] { _stepsDir }, new[] { _features }); + + [Fact] + public void GermanStep_MatchesBinding_SoBindingIsUsed() + { + Binding("etwas passiert"); + Feature("de", """ + # language: de + Funktionalität: F + + Szenario: s + Angenommen etwas passiert + """); + + var report = Run(); + + // The German "Angenommen" step entered the corpus and matched the binding -> not dead. + Assert.Equal(0, report.UnusedStepDefinitions); + } + + [Fact] + public void GermanNarrativePreamble_IsNotParsedAsStep() + { + // The prose line under Funktionalität starts with "Angenommen" but precedes any block header, + // so it is narrative, not a step. A binding matching it must stay UNUSED. + Binding("dieser Satz ist nur Prosa"); + Feature("de", """ + # language: de + Funktionalität: F + Angenommen dieser Satz ist nur Prosa + + Szenario: s + Wenn etwas anderes passiert + """); + + var report = Run(); + + Assert.Equal(1, report.UnusedStepDefinitions); + } +} diff --git a/tests/SpecHygiene.Tests/GherkinDialectParsingTests.cs b/tests/SpecHygiene.Tests/GherkinDialectParsingTests.cs new file mode 100644 index 0000000..2a62890 --- /dev/null +++ b/tests/SpecHygiene.Tests/GherkinDialectParsingTests.cs @@ -0,0 +1,129 @@ +using SpecHygiene.Analyzers; +using SpecHygiene.Models; +using SpecHygiene.Services; +using Xunit; + +namespace SpecHygiene.Tests; + +/// +/// End-to-end proof for the localized-feature-file fix (issue #1). Before the fix, structural +/// keywords were hardcoded English, so a "# language: de" file was silently under-parsed: its +/// scenarios were never counted and its Examples table was never found (breaking data-error +/// detection). These tests drive the REAL public parse path (AnalyzeAsync, parse-only mode) over +/// temp files. +/// +/// The English case is the regression guard: no existing test exercises the DuplicateAnalyzer +/// parse path, so it is asserted here alongside the German fix. +/// +public class GherkinDialectParsingTests : IDisposable +{ + private readonly string _root = + Path.Combine(Path.GetTempPath(), "spechygiene-i18n-" + Guid.NewGuid().ToString("N")); + + public GherkinDialectParsingTests() + { + Directory.CreateDirectory(Path.Combine(_root, "SampleProject")); + } + + public void Dispose() + { + try { Directory.Delete(_root, recursive: true); } catch { /* best-effort temp cleanup */ } + } + + private void WriteFeature(string name, string content) => + File.WriteAllText(Path.Combine(_root, "SampleProject", name), content.ReplaceLineEndings("\n")); + + private async Task AnalyzeAsync() + { + var settings = new AnalyzerSettings(); + settings.Analysis.SolutionPaths = new List { _root }; + var analyzer = new DuplicateAnalyzer(settings); + // Parse-only: we only care about scenarios + data errors, not cross-scenario matching. + return await analyzer.AnalyzeAsync(skipDuplicateMatching: true); + } + + private const string GermanFeature = """ + # language: de + Funktionalität: Kasse + + Szenario: Einfacher Kauf + Angenommen der Warenkorb hat 2 Artikel + Wenn ich zur Kasse gehe + Dann sehe ich die Bestellübersicht + + Szenariogrundriss: Kauf mit Menge + Angenommen der Warenkorb hat Artikel + Wenn ich eingebe + Dann sehe ich die Bestellübersicht + Beispiele: + | menge | + | 2 | + """; + + private const string EnglishFeature = """ + Feature: Checkout + + Scenario Outline: Buy with quantity + Given the cart has items + When I enter + Then I see the summary + Examples: + | count | + | 2 | + """; + + [Fact] + public async Task GermanFile_ScenariosAreCounted() + { + WriteFeature("Kasse.feature", GermanFeature); + + var report = await AnalyzeAsync(); + + // Both the plain Szenario and the Szenariogrundriss must be parsed (previously: zero). + Assert.Equal(2, report.AllScenarios.Count); + Assert.Contains(report.AllScenarios, s => s.ScenarioName == "Einfacher Kauf"); + Assert.Contains(report.AllScenarios, s => s.ScenarioName == "Kauf mit Menge"); + } + + [Fact] + public async Task GermanFile_UndefinedPlaceholderInExamplesOutline_IsDetected() + { + WriteFeature("Kasse.feature", GermanFeature); + + var report = await AnalyzeAsync(); + + // is used in a step but is not a column in Beispiele (only is). + var error = Assert.Single(report.DataErrors, e => e.ErrorType == DataErrorType.UndefinedPlaceholder); + Assert.Equal("Kauf mit Menge", error.Scenario); + Assert.Contains("fehlt", error.UndefinedPlaceholders); + Assert.DoesNotContain("menge", error.UndefinedPlaceholders); // defined -> not flagged + } + + [Fact] + public async Task EnglishFile_UndefinedPlaceholder_StillDetected_NoRegression() + { + WriteFeature("Checkout.feature", EnglishFeature); + + var report = await AnalyzeAsync(); + + Assert.Single(report.AllScenarios); + var error = Assert.Single(report.DataErrors, e => e.ErrorType == DataErrorType.UndefinedPlaceholder); + Assert.Equal("Buy with quantity", error.Scenario); + Assert.Contains("missing", error.UndefinedPlaceholders); + Assert.DoesNotContain("count", error.UndefinedPlaceholders); + } + + [Fact] + public void Provider_LoadsEmbeddedResource_AndDetectsHeader() + { + // Guards the EmbeddedResource wiring: a wrong path would throw here, not at build time. + var provider = new GherkinDialectProvider(); + + var german = provider.Detect(new[] { "# language: de", "Funktionalität: X" }); + Assert.Equal("de", german.Language); + Assert.Contains("Szenario", german.Scenario); + + var defaulted = provider.Detect(new[] { "Feature: X" }); + Assert.Equal("en", defaulted.Language); + } +} From de8068ff0104312649bdee85b8ba8279ed4f78b1 Mon Sep 17 00:00:00 2001 From: Karzone Date: Thu, 23 Jul 2026 12:37:31 +0100 Subject: [PATCH 2/2] Add THIRD-PARTY-NOTICES for vendored gherkin-languages.json (MIT) Records source, upstream commit SHA, and cucumber/gherkin's MIT license, and ships it in the NuGet package. Required since the data file is embedded and redistributed via the packed tool. Co-Authored-By: Claude Opus 4.8 --- THIRD-PARTY-NOTICES.md | 39 ++++++++++++++++++++++++++++++ src/SpecHygiene/SpecHygiene.csproj | 2 ++ 2 files changed, 41 insertions(+) create mode 100644 THIRD-PARTY-NOTICES.md diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..8952262 --- /dev/null +++ b/THIRD-PARTY-NOTICES.md @@ -0,0 +1,39 @@ +# Third-Party Notices + +SpecHygiene incorporates material from the following third-party projects. + +--- + +## Cucumber Gherkin — `gherkin-languages.json` + +- **Used in:** `src/SpecHygiene/Analysis/Gherkin/gherkin-languages.json` (embedded as a resource and + shipped in the SpecHygiene NuGet package). +- **Source:** https://github.com/cucumber/gherkin +- **File:** https://github.com/cucumber/gherkin/blob/main/gherkin-languages.json +- **Pulled from commit:** `5bfc43c74e5a1f12f40f476105de0ef48743bd80` + (to re-sync the dialect table, replace the file from this path at a newer commit and update this SHA). +- **License:** MIT + +``` +MIT License + +Copyright (c) 2017 Cucumber Ltd, Gaspar Nagy, Björn Rasmusson, Peter Sergeant, and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/src/SpecHygiene/SpecHygiene.csproj b/src/SpecHygiene/SpecHygiene.csproj index b44e45d..14c729d 100644 --- a/src/SpecHygiene/SpecHygiene.csproj +++ b/src/SpecHygiene/SpecHygiene.csproj @@ -48,6 +48,8 @@ + +