Repository: syncthing/syncthing (language: go)

Issue:
The Web UI defaults to Filipino on Finnish browsers.
### What happened?

On a browser with the language set to Finnish (fi), Syncthing's Web UI mistakenly sets the language to Filipino (fil).

Reproduction steps:
- Clear the cookies for the web UI or use a blank browser profile.
- Set the browser's primary language to Finnish.
- Open the Web UI.

### Syncthing version

v1.27.10

### Platform & operating system

Linux (64-bit Intel/AMD)

### Browser version

Mozilla Firefox 129.0.1 and Chromium 127.0.6533.99

### Relevant log output

_No response_


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build-syncthing.yaml b/.github/workflows/build-syncthing.yaml
index 42e76cd18fc..54d65a08aa2 100644
--- a/.github/workflows/build-syncthing.yaml
+++ b/.github/workflows/build-syncthing.yaml
@@ -238,7 +238,9 @@ jobs:
         uses: actions/upload-artifact@v4
         with:
           name: packages-linux
-          path: syncthing-linux-*.tar.gz
+          path: |
+            syncthing-linux-*.tar.gz
+            compat.json
 
   #
   # macOS
diff --git a/.gitignore b/.gitignore
index 80bccc598df..89eba4edd3b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ deb
 /repos
 /proto/scripts/protoc-gen-gosyncthing
 /gui/next-gen-gui
+/compat.json
diff --git a/build.go b/build.go
index 5f9968485cf..d3d0b9ddd3d 100644
--- a/build.go
+++ b/build.go
@@ -4,8 +4,8 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-//go:build ignore
-// +build ignore
+//go:build tools
+// +build tools
 
 package main
 
@@ -34,6 +34,8 @@ import (
 	"time"
 
 	buildpkg "github.com/syncthing/syncthing/lib/build"
+	"github.com/syncthing/syncthing/lib/upgrade"
+	"sigs.k8s.io/yaml"
 )
 
 var (
@@ -342,9 +344,11 @@ func runCommand(cmd string, target target) {
 
 	case "tar":
 		buildTar(target, tags)
+		writeCompatJSON()
 
 	case "zip":
 		buildZip(target, tags)
+		writeCompatJSON()
 
 	case "deb":
 		buildDeb(target)
@@ -1557,3 +1561,29 @@ func nextPatchVersion(ver string) string {
 	digits[len(digits)-1] = strconv.Itoa(n + 1)
 	return strings.Join(digits, ".")
 }
+
+func writeCompatJSON() {
+	bs, err := os.ReadFile("compat.yaml")
+	if err != nil {
+		log.Fatal("Reading compat.yaml:", err)
+	}
+
+	var entries []upgrade.ReleaseCompatibility
+	if err := yaml.Unmarshal(bs, &entries); err != nil {
+		log.Fatal("Parsing compat.yaml:", err)
+	}
+
+	rt := runtime.Version()
+	for _, e := range entries {
+		if !strings.HasPrefix(rt, e.Runtime) {
+			continue
+		}
+		bs, _ := json.MarshalIndent(e, "", "  ")
+		if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
+			log.Fatal("Writing compat.json:", err)
+		}
+		return
+	}
+
+	log.Fatalf("runtime %v not found in compat.yaml", rt)
+}
diff --git a/compat.yaml b/compat.yaml
new file mode 100644
index 00000000000..655b378911a
--- /dev/null
+++ b/compat.yaml
@@ -0,0 +1,28 @@
+- runtime: go1.21
+  requirements:
+    # See https://en.wikipedia.org/wiki/MacOS_version_history#Releases
+    #
+    # macOS 10.15 (Catalina) per https://go.dev/doc/go1.22#darwin
+    darwin: "19"
+    # Per https://go.dev/doc/go1.23#linux
+    linux: "2.6.32"
+    # Windows 10's initial release was 10.0.10240.16405, per
+    # https://learn.microsoft.com/en-us/windows/release-health/release-information
+    # and Windows 11's initial release was 10.0.22000.194 per
+    # https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information
+    #
+    # Windows 10/Windows Server 2016 per https://go.dev/doc/go1.21#windows
+    windows: "10.0"
+
+- runtime: go1.22
+  requirements:
+    darwin: "19"
+    linux: "2.6.32"
+    windows: "10.0"
+
+- runtime: go1.23
+  requirements:
+    # macOS 11 (Big Sur) per https://tip.golang.org/doc/go1.23#darwin
+    darwin: "20"
+    linux: "2.6.32"
+    windows: "10.0"
diff --git a/go.mod b/go.mod
index 51b611b9f94..89503f1384e 100644
--- a/go.mod
+++ b/go.mod
@@ -46,6 +46,7 @@ require (
 	golang.org/x/time v0.6.0
 	golang.org/x/tools v0.24.0
 	google.golang.org/protobuf v1.34.2
+	sigs.k8s.io/yaml v1.4.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index 635211c3920..7dc7397a07a 100644
--- a/go.sum
+++ b/go.sum
@@ -81,6 +81,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
@@ -385,3 +386,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/lib/upgrade/upgrade_common.go b/lib/upgrade/upgrade_common.go
index fae85145b95..f7589213b96 100644
--- a/lib/upgrade/upgrade_common.go
+++ b/lib/upgrade/upgrade_common.go
@@ -38,6 +38,13 @@ type Asset struct {
 	BrowserURL string `json:"browser_download_url,omitempty"`
 }
 
+// ReleaseCompatibility defines the structure of compat.json, which is
+// included with each elease.
+type ReleaseCompatibility struct {
+	Runtime      string            `json:"runtime,omitempty"`
+	Requirements map[string]string `json:"requirements,omitempty"`
+}
+
 var (
 	ErrNoReleaseDownload  = errors.New("couldn't find a release to download")
 	ErrNoVersionToSelect  = errors.New("no version to select")


Test patch: