Repository: gohugoio/hugo (language: go)

Issue:
server: Browser does not reload after changing non-default language content
If you have some translated page, and you edit the translation, server runs refresh, and the page reloads, but it still displays the old content. Example [site](https://github.com/TiGR/hugo-testing/tree/translation-caching-issue). In that site try running `hugo server` and edit `content/_index.es.md`, notice that the rendered content stays the same. This issue does not happen in 0.122.


Discussion / hints:
Changes to content in the default content language work as expected. The reported problem is limited to the other languages.

Fix patch:
diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
index 576ee5e08ed..70e8a0bb6b3 100644
--- a/hugolib/content_map_page.go
+++ b/hugolib/content_map_page.go
@@ -125,37 +125,52 @@ type pageTrees struct {
 	resourceTrees doctree.MutableTrees
 }
 
-// collectIdentities collects all identities from in all trees matching the given key.
-// This will at most match in one tree, but may give identies from multiple dimensions (e.g. language).
-func (t *pageTrees) collectIdentities(p *paths.Path) []identity.Identity {
-	ids := t.collectIdentitiesFor(p.Base())
+// collectAndMarkStaleIdentities collects all identities from in all trees matching the given key.
+// We currently re-read all page/resources for all languages that share the same path,
+// so we mark all entries as stale (which will trigger cache invalidation), then
+// return the first.
+func (t *pageTrees) collectAndMarkStaleIdentities(p *paths.Path) []identity.Identity {
+	ids := t.collectAndMarkStaleIdentitiesFor(p.Base())
 
 	if p.Component() == files.ComponentFolderContent {
 		// It may also be a bundled content resource.
-		if n := t.treeResources.Get(p.ForBundleType(paths.PathTypeContentResource).Base()); n != nil {
+		key := p.ForBundleType(paths.PathTypeContentResource).Base()
+		tree := t.treeResources
+		if n := tree.Get(key); n != nil {
 			n.ForEeachIdentity(func(id identity.Identity) bool {
 				ids = append(ids, id)
 				return false
 			})
+			if n, ok := tree.GetRaw(key); ok {
+				n.MarkStale()
+			}
 		}
 	}
 	return ids
 }
 
-func (t *pageTrees) collectIdentitiesFor(key string) []identity.Identity {
+func (t *pageTrees) collectAndMarkStaleIdentitiesFor(key string) []identity.Identity {
 	var ids []identity.Identity
-	if n := t.treePages.Get(key); n != nil {
+	tree := t.treePages
+	if n := tree.Get(key); n != nil {
 		n.ForEeachIdentity(func(id identity.Identity) bool {
 			ids = append(ids, id)
 			return false
 		})
+		if n, ok := tree.GetRaw(key); ok {
+			n.MarkStale()
+		}
 	}
 
-	if n := t.treeResources.Get(key); n != nil {
+	tree = t.treeResources
+	if n := tree.Get(key); n != nil {
 		n.ForEeachIdentity(func(id identity.Identity) bool {
 			ids = append(ids, id)
 			return false
 		})
+		if n, ok := tree.GetRaw(key); ok {
+			n.MarkStale()
+		}
 	}
 
 	return ids
@@ -626,9 +641,7 @@ func (n contentNodeIs) resetBuildState() {
 
 func (n contentNodeIs) MarkStale() {
 	for _, nn := range n {
-		if nn != nil {
-			nn.MarkStale()
-		}
+		resource.MarkStale(nn)
 	}
 }
 
@@ -799,6 +812,7 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI {
 		if !ok {
 			panic(fmt.Sprintf("unknown type %T", new))
 		}
+		resource.MarkStale(vv[newp.s.languagei])
 		vv[newp.s.languagei] = new
 		return vv
 	case *resourceSource:
@@ -818,6 +832,7 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI {
 		if !ok {
 			panic(fmt.Sprintf("unknown type %T", new))
 		}
+		resource.MarkStale(vv[newp.LangIndex()])
 		vv[newp.LangIndex()] = newp
 		return vv
 	default:
@@ -1014,8 +1029,12 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
 	)
 
 	for _, id := range changes {
-		if staler, ok := id.(resource.Staler); ok {
-			h.Log.Trace(logg.StringFunc(func() string { return fmt.Sprintf("Marking stale: %s (%T)\n", id, id) }))
+		if staler, ok := id.(resource.Staler); ok && !staler.IsStale() {
+			var msgDetail string
+			if p, ok := id.(*pageState); ok && p.File() != nil {
+				msgDetail = fmt.Sprintf(" (%s)", p.File().Filename())
+			}
+			h.Log.Trace(logg.StringFunc(func() string { return fmt.Sprintf("Marking stale: %s (%T)%s\n", id, id, msgDetail) }))
 			staler.MarkStale()
 		}
 	}
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index 17a3f00563a..aa43d541a93 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -702,7 +702,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
 		switch pathInfo.Component() {
 		case files.ComponentFolderContent:
 			logger.Println("Source changed", pathInfo.Path())
-			if ids := h.pageTrees.collectIdentities(pathInfo); len(ids) > 0 {
+			if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 {
 				changes = append(changes, ids...)
 			}
 
diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go
index 43d0aa7864e..7a735dca5c9 100644
--- a/resources/resource/resourcetypes.go
+++ b/resources/resource/resourcetypes.go
@@ -17,6 +17,7 @@ import (
 	"context"
 
 	"github.com/gohugoio/hugo/common/maps"
+	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/media"
 
@@ -250,7 +251,10 @@ func IsStaleAny(os ...any) bool {
 // MarkStale will mark any of the oses as stale, if possible.
 func MarkStale(os ...any) {
 	for _, o := range os {
-		if s, ok := o.(Staler); ok {
+		if types.IsNil(o) {
+			continue
+		}
+		if s, ok := o.(StaleMarker); ok {
 			s.MarkStale()
 		}
 	}


Test patch:
diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go
index 06a6083f546..6e43a4570d2 100644
--- a/hugolib/rebuild_test.go
+++ b/hugolib/rebuild_test.go
@@ -1171,6 +1171,59 @@ Hello: {{ i18n "hello" }}
 	b.AssertFileContent("public/index.html", "Hello: Hugo")
 }
 
+func TestRebuildEditContentNonDefaultLanguage(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableLiveReload = true
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+weight = 1
+[languages.nn]
+weight = 2
+-- content/p1/index.en.md --
+---
+title: "P1 en"
+---
+P1 en.
+-- content/p1/b.en.md --
+---
+title: "B en"
+---
+B en.
+-- content/p1/f1.en.txt --
+F1 en
+-- content/p1/index.nn.md --
+---
+title: "P1 nn"
+---
+P1 nn.
+-- content/p1/b.nn.md --
+---
+title: "B nn"
+---
+B nn.
+-- content/p1/f1.nn.txt --
+F1 nn
+-- layouts/_default/single.html --
+Single: {{ .Title }}|{{ .Content }}|Bundled File: {{ with .Resources.GetMatch "f1.*" }}{{ .Content }}{{ end }}|Bundled Page: {{ with .Resources.GetMatch "b.*" }}{{ .Content }}{{ end }}|
+`
+
+	b := TestRunning(t, files)
+
+	b.AssertFileContent("public/nn/p1/index.html", "Single: P1 nn|<p>P1 nn.</p>", "F1 nn|")
+	b.EditFileReplaceAll("content/p1/index.nn.md", "P1 nn.", "P1 nn edit.").Build()
+	b.AssertFileContent("public/nn/p1/index.html", "Single: P1 nn|<p>P1 nn edit.</p>\n|")
+	b.EditFileReplaceAll("content/p1/f1.nn.txt", "F1 nn", "F1 nn edit.").Build()
+	b.AssertFileContent("public/nn/p1/index.html", "Bundled File: F1 nn edit.")
+	b.EditFileReplaceAll("content/p1/b.nn.md", "B nn.", "B nn edit.").Build()
+	b.AssertFileContent("public/nn/p1/index.html", "B nn edit.")
+}
+
 func TestRebuildVariationsAssetsSassImport(t *testing.T) {
 	if !htesting.IsCI() {
 		t.Skip("skip CI only")