Repository: cucumber/godog (go)

Issue:
Localisation supports
### 🤔 What's the problem you're trying to solve?

I was trying to write Gherkin syntax in Indonesian language as defined [in the docs.](https://cucumber.io/docs/gherkin/languages/). But I got `Parser errors` when I ran the test. Here is the example of the feature file:

```
Fitur: login

  Skenario: Login sukses
    Jika username gw adalah dokter_jakbar1
    Ketika gw login pake password 12345678
    Maka access_token ga boleh kosong
    Dan http status code harus 200

  Skenario: Username ngaco
    Jika username gw adalah dukun_santet
    Ketika gw login pake password 12345678
    Maka http status code harus 401
```
This should be correct according to the docs. And if I switched the feature file using english like this:

```
Feature: login

  Scenario: Successful login
    Given my username is dokter_jakbar1
    When I login using the password 12345678
    Then access_token should not be empty
    And http status code should be 200

  Scenario: Invalid username
    Given my username is dukun_santet
    When I login using the password 12345678
    Then http status code should be 401
```
It works as expected.

When I take a peek on the code, I couldn't find anything to switch the language to call [this gherkin go function](https://github.com/cucumber/gherkin/blob/2f55c59d59aa8db8956be82e8e30f63804b6407e/go/gherkin.go#L131). Or, am I missing something?

### ✨ What's your proposed solution?

Adding 'Language' or 'Dialect' field in [flag options](https://github.com/cucumber/godog/blob/main/internal/flags/options.go), then use it to called [this function](https://github.com/cucumber/godog/blob/c5a88f62c2a610e4581fd033f1d6f6cd05912df5/internal/parser/parser.go#L36), would be safe implementation on a slight glance. Although, I'm not inspecting the [ParseFeatures](https://github.com/cucumber/godog/blob/c5a88f62c2a610e4581fd033f1d6f6cd05912df5/internal/parser/parser.go#L149) reference yet, whether adding parameters there would cause breaking change or not.

### ⛏ Have you considered any alternatives or workarounds?

_No response_

### 📚 Any additional context?

_No response_

Pull request:
Localisation support
<!---
Thanks for helping to make Cucumber better! 💖

You can feel free to open a "Draft" status pull request if you're not ready for feedback yet.

Don't worry about getting everything perfect! We're here to help and will coach you through
to getting your pull request ready to merge.

The prompts below are for guidance to help you describe your change in a way that is most 
likely to make sense to other people when they are reviewing it. Still, it's just a guide, 
so feel free to delete anything that doesn't feel appropriate, and add anything additional 
that seems like it would provide useful context. 👏🏻
-->

### 🤔 What's changed?

<!-- Describe your changes in detail -->

Added `Dialect` option to switch between language for the feature file. 

### ⚡️ What's your motivation? 

<!-- 
What motivated you to propose this change? Does it fix a bug? Add a new feature?
If it fixes an open issue, you can link to the issue here, e.g. "Fixes #99"
-->

New feature - Resolves #663 

### 🏷️ What kind of change is this?

<!--- Delete any options that are not relevant -->

- :zap: New feature (non-breaking change which adds new behaviour)

### ♻️ Anything particular you want feedback on?

<!-- 
Is there anything in this change you're unsure about, or would 
particularly like reviewers to give you feedback on?
-->

### 📋 Checklist:

<!--- 
This is to help you remember all the little things we often forget to do!

Feel free to delete any tasks that are not relevant, or add new ones.
-->

- [x] I agree to respect and uphold the [Cucumber Community Code of Conduct](https://github.com/cucumber/.github/tree/main?tab=coc-ov-file)
- [x] I've changed the behaviour of the code
  - [x] I have added/updated tests to cover my changes.
- [ ] My change requires a change to the documentation.
  - [ ] I have updated the documentation accordingly.
- [x] Users should know about my change
  - [ ] I have added an entry to the "Unreleased" section of the [**CHANGELOG**](../blob/main/CHANGELOG.md), linking to this pull request.

----

*This text was originally generated from a [template](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/about-issue-and-pull-request-templates), then edited by hand. [You can modify the template here.](https://github.com/cucumber/.github/edit/main/.github/PULL_REQUEST_TEMPLATE.md)*


Patch:
diff --git a/internal/flags/options.go b/internal/flags/options.go
index 5ca40f9..40acea6 100644
--- a/internal/flags/options.go
+++ b/internal/flags/options.go
@@ -48,6 +48,9 @@ type Options struct {
 	// from feature files
 	Tags string
 
+	// Dialect to be used to parse feature files. If not set, default to "en".
+	Dialect string
+
 	// The formatter name
 	Format string
 
diff --git a/internal/parser/parser.go b/internal/parser/parser.go
index 3db1b52..f607000 100644
--- a/internal/parser/parser.go
+++ b/internal/parser/parser.go
@@ -33,7 +33,7 @@ func ExtractFeaturePathLine(p string) (string, int) {
 	return retPath, line
 }
 
-func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models.Feature, error) {
+func parseFeatureFile(fsys fs.FS, path, dialect string, newIDFunc func() string) (*models.Feature, error) {
 	reader, err := fsys.Open(path)
 	if err != nil {
 		return nil, err
@@ -42,7 +42,7 @@ func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models
 	defer reader.Close()
 
 	var buf bytes.Buffer
-	gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc)
+	gherkinDocument, err := gherkin.ParseGherkinDocumentForLanguage(io.TeeReader(reader, &buf), dialect, newIDFunc)
 	if err != nil {
 		return nil, fmt.Errorf("%s - %v", path, err)
 	}
@@ -54,11 +54,11 @@ func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models
 	return &f, nil
 }
 
-func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.Feature, error) {
+func parseBytes(path string, feature []byte, dialect string, newIDFunc func() string) (*models.Feature, error) {
 	reader := bytes.NewReader(feature)
 
 	var buf bytes.Buffer
-	gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc)
+	gherkinDocument, err := gherkin.ParseGherkinDocumentForLanguage(io.TeeReader(reader, &buf), dialect, newIDFunc)
 	if err != nil {
 		return nil, fmt.Errorf("%s - %v", path, err)
 	}
@@ -70,7 +70,7 @@ func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.F
 	return &f, nil
 }
 
-func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models.Feature, error) {
+func parseFeatureDir(fsys fs.FS, dir, dialect string, newIDFunc func() string) ([]*models.Feature, error) {
 	var features []*models.Feature
 	return features, fs.WalkDir(fsys, dir, func(p string, f fs.DirEntry, err error) error {
 		if err != nil {
@@ -85,7 +85,7 @@ func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models
 			return nil
 		}
 
-		feat, err := parseFeatureFile(fsys, p, newIDFunc)
+		feat, err := parseFeatureFile(fsys, p, dialect, newIDFunc)
 		if err != nil {
 			return err
 		}
@@ -95,7 +95,7 @@ func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models
 	})
 }
 
-func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feature, error) {
+func parsePath(fsys fs.FS, path, dialect string, newIDFunc func() string) ([]*models.Feature, error) {
 	var features []*models.Feature
 
 	path, line := ExtractFeaturePathLine(path)
@@ -114,10 +114,10 @@ func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feat
 	}
 
 	if fi.IsDir() {
-		return parseFeatureDir(fsys, path, newIDFunc)
+		return parseFeatureDir(fsys, path, dialect, newIDFunc)
 	}
 
-	ft, err := parseFeatureFile(fsys, path, newIDFunc)
+	ft, err := parseFeatureFile(fsys, path, dialect, newIDFunc)
 	if err != nil {
 		return features, err
 	}
@@ -146,14 +146,18 @@ func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feat
 }
 
 // ParseFeatures ...
-func ParseFeatures(fsys fs.FS, filter string, paths []string) ([]*models.Feature, error) {
+func ParseFeatures(fsys fs.FS, filter, dialect string, paths []string) ([]*models.Feature, error) {
 	var order int
 
+	if dialect == "" {
+		dialect = gherkin.DefaultDialect
+	}
+
 	featureIdxs := make(map[string]int)
 	uniqueFeatureURI := make(map[string]*models.Feature)
 	newIDFunc := (&messages.Incrementing{}).NewId
 	for _, path := range paths {
-		feats, err := parsePath(fsys, path, newIDFunc)
+		feats, err := parsePath(fsys, path, dialect, newIDFunc)
 
 		switch {
 		case os.IsNotExist(err):
@@ -189,14 +193,18 @@ func ParseFeatures(fsys fs.FS, filter string, paths []string) ([]*models.Feature
 
 type FeatureContent = flags.Feature
 
-func ParseFromBytes(filter string, featuresInputs []FeatureContent) ([]*models.Feature, error) {
+func ParseFromBytes(filter, dialect string, featuresInputs []FeatureContent) ([]*models.Feature, error) {
 	var order int
 
+	if dialect == "" {
+		dialect = gherkin.DefaultDialect
+	}
+
 	featureIdxs := make(map[string]int)
 	uniqueFeatureURI := make(map[string]*models.Feature)
 	newIDFunc := (&messages.Incrementing{}).NewId
 	for _, f := range featuresInputs {
-		ft, err := parseBytes(f.Name, f.Contents, newIDFunc)
+		ft, err := parseBytes(f.Name, f.Contents, dialect, newIDFunc)
 		if err != nil {
 			return nil, err
 		}
diff --git a/run.go b/run.go
index 7378b2f..1231d02 100644
--- a/run.go
+++ b/run.go
@@ -247,7 +247,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int {
 	opt.FS = storage.FS{FS: opt.FS}
 
 	if len(opt.FeatureContents) > 0 {
-		features, err := parser.ParseFromBytes(opt.Tags, opt.FeatureContents)
+		features, err := parser.ParseFromBytes(opt.Tags, opt.Dialect, opt.FeatureContents)
 		if err != nil {
 			fmt.Fprintln(os.Stderr, err)
 			return exitOptionError
@@ -256,7 +256,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int {
 	}
 
 	if len(opt.Paths) > 0 {
-		features, err := parser.ParseFeatures(opt.FS, opt.Tags, opt.Paths)
+		features, err := parser.ParseFeatures(opt.FS, opt.Tags, opt.Dialect, opt.Paths)
 		if err != nil {
 			fmt.Fprintln(os.Stderr, err)
 			return exitOptionError
@@ -389,7 +389,7 @@ func (ts TestSuite) RetrieveFeatures() ([]*models.Feature, error) {
 		}
 	}
 
-	return parser.ParseFeatures(opt.FS, opt.Tags, opt.Paths)
+	return parser.ParseFeatures(opt.FS, opt.Tags, opt.Dialect, opt.Paths)
 }
 
 func getDefaultOptions() (*Options, error) {


Test patch:
diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go
index 222f317..265d323 100644
--- a/internal/parser/parser_test.go
+++ b/internal/parser/parser_test.go
@@ -54,7 +54,7 @@ Feature: eat godogs
 		{Name: "MyCoolDuplicatedFeature", Contents: []byte(eatGodogContents)},
 	}
 
-	featureFromBytes, err := parser.ParseFromBytes("", input)
+	featureFromBytes, err := parser.ParseFromBytes("", "", input)
 	require.NoError(t, err)
 	require.Len(t, featureFromBytes, 1)
 }
@@ -80,7 +80,7 @@ Feature: eat godogs
 		},
 	}
 
-	featureFromFile, err := parser.ParseFeatures(fsys, "", []string{baseDir})
+	featureFromFile, err := parser.ParseFeatures(fsys, "", "", []string{baseDir})
 	require.NoError(t, err)
 	require.Len(t, featureFromFile, 1)
 
@@ -88,7 +88,7 @@ Feature: eat godogs
 		{Name: filepath.Join(baseDir, featureFileName), Contents: []byte(eatGodogContents)},
 	}
 
-	featureFromBytes, err := parser.ParseFromBytes("", input)
+	featureFromBytes, err := parser.ParseFromBytes("", "", input)
 	require.NoError(t, err)
 	require.Len(t, featureFromBytes, 1)
 
@@ -155,7 +155,7 @@ func Test_ParseFeatures_FromMultiplePaths(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			t.Parallel()
 
-			features, err := parser.ParseFeatures(test.fsys, "", test.paths)
+			features, err := parser.ParseFeatures(test.fsys, "", "", test.paths)
 			if test.expError != nil {
 				require.Error(t, err)
 				require.EqualError(t, err, test.expError.Error())
@@ -178,3 +178,132 @@ func Test_ParseFeatures_FromMultiplePaths(t *testing.T) {
 		})
 	}
 }
+
+func Test_ParseFeatures_Localisation(t *testing.T) {
+	tests := map[string]struct {
+		dialect  string
+		contents string
+	}{
+		"english": {
+			dialect: "en",
+			contents: `
+Feature: dummy
+  Rule: dummy
+    Background: dummy
+      Given dummy
+      When dummy
+      Then dummy
+    Scenario: dummy
+      Given dummy
+      When dummy
+      Then dummy
+      And dummy
+      But dummy
+    Example: dummy
+      Given dummy
+      When dummy
+      Then dummy
+    Scenario Outline: dummy
+      Given dummy
+      When dummy
+      Then dummy
+      `,
+		},
+		"afrikaans": {
+			dialect: "af",
+			contents: `
+Funksie: dummy
+  Regel: dummy
+    Agtergrond: dummy
+      Gegewe dummy
+      Wanneer dummy
+      Dan dummy
+    Voorbeeld: dummy
+      Gegewe dummy
+      Wanneer dummy
+      Dan dummy
+      En dummy
+      Maar dummy
+    Voorbeelde: dummy
+      Gegewe dummy
+      Wanneer dummy
+      Dan dummy
+    Situasie Uiteensetting: dummy
+      Gegewe dummy
+      Wanneer dummy
+      Dan dummy
+      `,
+		},
+		"arabic": {
+			dialect: "ar",
+			contents: `
+خاصية: dummy
+  Rule: dummy
+    الخلفية: dummy
+      بفرض  dummy
+      متى  dummy
+      اذاً  dummy
+    مثال: dummy
+      بفرض  dummy
+      متى  dummy
+      اذاً  dummy
+      و dummy
+      لكن dummy
+    امثلة: dummy
+      بفرض  dummy
+      متى  dummy
+      اذاً  dummy
+    سيناريو مخطط: dummy
+      بفرض  dummy
+      متى  dummy
+      اذاً  dummy
+      `,
+		},
+		"chinese simplified": {
+			dialect: "zh-CN",
+			contents: `
+功能: dummy
+  规则: dummy
+    背景: dummy
+      假如 dummy
+      当 dummy
+      那么 dummy
+    场景: dummy
+      假如 dummy
+      当 dummy
+      那么 dummy
+      而且 dummy
+      但是 dummy
+    例子: dummy
+      假如 dummy
+      当 dummy
+      那么 dummy
+    场景大纲: dummy
+      假如 dummy
+      当 dummy
+      那么 dummy
+      `,
+		},
+	}
+
+	featureFileName := "godogs.feature"
+	baseDir := "base"
+
+	for name, test := range tests {
+		test := test
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+
+			fsys := fstest.MapFS{
+				filepath.Join(baseDir, featureFileName): {
+					Data: []byte(test.contents),
+					Mode: fs.FileMode(0o644),
+				},
+			}
+
+			featureTestDialect, err := parser.ParseFeatures(fsys, "", test.dialect, []string{baseDir})
+			require.NoError(t, err)
+			require.Len(t, featureTestDialect, 1)
+		})
+	}
+}
diff --git a/suite_context_test.go b/suite_context_test.go
index b7b5e9d..9b042d8 100644
--- a/suite_context_test.go
+++ b/suite_context_test.go
@@ -494,7 +494,7 @@ func (tc *godogFeaturesScenario) theLoggedMessagesShouldInclude(ctx context.Cont
 }
 
 func (tc *godogFeaturesScenario) followingStepsShouldHave(status string, steps *DocString) error {
-	var expected = strings.Split(steps.Content, "\n")
+	expected := strings.Split(steps.Content, "\n")
 	var actual, unmatched, matched []string
 
 	storage := tc.testedSuite.storage
@@ -673,7 +673,7 @@ func (tc *godogFeaturesScenario) featurePath(path string) {
 }
 
 func (tc *godogFeaturesScenario) parseFeatures() error {
-	fts, err := parser.ParseFeatures(storage.FS{}, "", tc.paths)
+	fts, err := parser.ParseFeatures(storage.FS{}, "", "", tc.paths)
 	if err != nil {
 		return err
 	}
@@ -1226,7 +1226,6 @@ func TestTestSuite_Run(t *testing.T) {
 					s.Step("^multistep has ambiguous$", func() Steps {
 						return Steps{"step is ambiguous"}
 					})
-
 				},
 				Options: &Options{
 					Format:   "pretty",