From 887453fee27c23cc6808b1a995cb2256e23bebcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Sat, 2 May 2026 18:10:18 +0400 Subject: [PATCH] Exclude translations from cookie banner version snapshots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translation changes are cosmetic, not consent-contract changes, so they should not trigger a version bump. Translations are now loaded live from the database at serve time instead of being frozen in the snapshot. Signed-off-by: Émile Ré --- e2e/console/cookie_banner_versioning_test.go | 32 ++++++++++++-------- pkg/cookiebanner/service.go | 32 +++++++++++--------- pkg/cookiebanner/service_test.go | 31 +++---------------- pkg/cookiebanner/snapshot.go | 14 ++++++--- pkg/coredata/cookie_banner_version.go | 13 ++++---- 5 files changed, 58 insertions(+), 64 deletions(-) diff --git a/e2e/console/cookie_banner_versioning_test.go b/e2e/console/cookie_banner_versioning_test.go index fd4c07693..36c445310 100644 --- a/e2e/console/cookie_banner_versioning_test.go +++ b/e2e/console/cookie_banner_versioning_test.go @@ -472,7 +472,7 @@ func TestCookieBannerVersioning_ExcludedPattern(t *testing.T) { }) } -func TestCookieBannerVersioning_NoOpTranslation(t *testing.T) { +func TestCookieBannerVersioning_TranslationChangesNeverBump(t *testing.T) { t.Parallel() t.Run("re-upserting identical JSON does not bump version", func(t *testing.T) { @@ -481,13 +481,11 @@ func TestCookieBannerVersioning_NoOpTranslation(t *testing.T) { bannerID := factory.CreateCookieBanner(owner) - // Insert a custom translation, then publish. const customJSON = `{"banner_title":"Cookie Bar","button_accept_all":"Accept"}` upsertTranslation(t, owner, bannerID, "it", customJSON) published := publishBanner(t, owner, bannerID) baseline := published.Version - // Re-upsert the same JSON. upsertTranslation(t, owner, bannerID, "it", customJSON) got := latestVersion(t, owner, bannerID) @@ -495,25 +493,35 @@ func TestCookieBannerVersioning_NoOpTranslation(t *testing.T) { assert.Equal(t, "PUBLISHED", got.State) }) - t.Run("whitespace-only and key-order differences do not bump version", func(t *testing.T) { + t.Run("changing translation content does not bump version", func(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) bannerID := factory.CreateCookieBanner(owner) - const compact = `{"banner_title":"Cookie Bar","button_accept_all":"Accept"}` - const reformatted = `{ - "button_accept_all": "Accept", - "banner_title": "Cookie Bar" -}` - upsertTranslation(t, owner, bannerID, "it", compact) + upsertTranslation(t, owner, bannerID, "it", `{"banner_title":"Cookie Bar"}`) published := publishBanner(t, owner, bannerID) baseline := published.Version - upsertTranslation(t, owner, bannerID, "it", reformatted) + upsertTranslation(t, owner, bannerID, "it", `{"banner_title":"Barra dei Cookie"}`) got := latestVersion(t, owner, bannerID) - assert.Equal(t, baseline, got.Version, "JSON formatting differences should be canonicalised") + assert.Equal(t, baseline, got.Version, "translation content changes should not bump the version") + assert.Equal(t, "PUBLISHED", got.State) + }) + + t.Run("adding a new language does not bump version", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + + bannerID := factory.CreateCookieBanner(owner) + published := publishBanner(t, owner, bannerID) + baseline := published.Version + + upsertTranslation(t, owner, bannerID, "fr", `{"banner_title":"Bandeau cookies"}`) + + got := latestVersion(t, owner, bannerID) + assert.Equal(t, baseline, got.Version, "adding a new language should not bump the version") assert.Equal(t, "PUBLISHED", got.State) }) } diff --git a/pkg/cookiebanner/service.go b/pkg/cookiebanner/service.go index 01a5eadc1..7a3291842 100644 --- a/pkg/cookiebanner/service.go +++ b/pkg/cookiebanner/service.go @@ -358,9 +358,8 @@ func (s *Service) ensureDraftVersion( banner *coredata.CookieBanner, categories coredata.CookieCategories, allPatterns coredata.CookiePatterns, - translations coredata.CookieBannerTranslations, ) (*coredata.CookieBannerVersion, error) { - snapshot := buildSnapshot(banner, categories, allPatterns, translations) + snapshot := buildSnapshot(banner, categories, allPatterns) var latest coredata.CookieBannerVersion err := latest.LoadLatestByCookieBannerID(ctx, tx, scope, banner.ID) @@ -440,12 +439,7 @@ func (s *Service) ensureDraftVersionForBanner( return nil, fmt.Errorf("cannot load cookie patterns: %w", err) } - var translations coredata.CookieBannerTranslations - if err := translations.LoadAllByCookieBannerID(ctx, tx, scope, bannerID); err != nil { - return nil, fmt.Errorf("cannot load cookie banner translations: %w", err) - } - - return s.ensureDraftVersion(ctx, tx, scope, &banner, categories, allPatterns, translations) + return s.ensureDraftVersion(ctx, tx, scope, &banner, categories, allPatterns) } func (s *Service) CreateCookieBanner( @@ -1888,7 +1882,18 @@ func (s *Service) GetActiveBannerConfig( return fmt.Errorf("cannot get version snapshot: %w", err) } - config = buildBannerConfig(&banner, &version, &snapshot, lang) + var categories coredata.CookieCategories + if err := categories.LoadAllByCookieBannerID(ctx, conn, scope, banner.ID); err != nil { + return fmt.Errorf("cannot load cookie categories: %w", err) + } + + var translations coredata.CookieBannerTranslations + if err := translations.LoadAllByCookieBannerID(ctx, conn, scope, banner.ID); err != nil { + return fmt.Errorf("cannot load cookie banner translations: %w", err) + } + + resolved := resolveTranslations(translations, categories) + config = buildBannerConfig(&banner, &version, &snapshot, resolved, lang) return nil }, @@ -1904,6 +1909,7 @@ func buildBannerConfig( banner *coredata.CookieBanner, version *coredata.CookieBannerVersion, snapshot *coredata.CookieBannerVersionSnapshot, + translations map[string]coredata.CookieBannerVersionSnapshotTranslation, lang string, ) *BannerConfig { defaultLang := snapshot.DefaultLanguage @@ -1913,7 +1919,7 @@ func buildBannerConfig( resolvedLang := defaultLang if lang != "" { - if _, ok := snapshot.Translations[lang]; ok { + if _, ok := translations[lang]; ok { resolvedLang = lang } } @@ -1921,7 +1927,7 @@ func buildBannerConfig( categories := snapshot.Categories texts := make(map[string]string) - if t, ok := snapshot.Translations[resolvedLang]; ok { + if t, ok := translations[resolvedLang]; ok { maps.Copy(texts, t.UI) if len(t.Categories) == len(categories) { @@ -2038,10 +2044,6 @@ func (s *Service) UpsertCookieBannerTranslation( return fmt.Errorf("cannot load cookie banner translation: %w", err) } - if _, err := s.ensureDraftVersionForBanner(ctx, tx, scope, req.CookieBannerID); err != nil { - return fmt.Errorf("cannot ensure draft version: %w", err) - } - return nil }, ) diff --git a/pkg/cookiebanner/service_test.go b/pkg/cookiebanner/service_test.go index 8b44a0ced..d8e150381 100644 --- a/pkg/cookiebanner/service_test.go +++ b/pkg/cookiebanner/service_test.go @@ -49,14 +49,6 @@ func TestSnapshotsEqual(t *testing.T) { PostHogConsent: false, }, }, - Translations: map[string]coredata.CookieBannerVersionSnapshotTranslation{ - "fr": { - UI: map[string]string{"title": "Cookies"}, - Categories: []coredata.CookieBannerVersionSnapshotCategoryTranslation{ - {Name: "Analyse", Description: "Cookies d'analyse"}, - }, - }, - }, } } @@ -103,19 +95,6 @@ func TestSnapshotsEqual(t *testing.T) { assert.False(t, snapshotsEqual(a, b)) }) - t.Run("differing translation UI is not equal", func(t *testing.T) { - t.Parallel() - - a := baseSnapshot() - b := baseSnapshot() - b.Translations["fr"] = coredata.CookieBannerVersionSnapshotTranslation{ - UI: map[string]string{"title": "Cookies updated"}, - Categories: a.Translations["fr"].Categories, - } - - assert.False(t, snapshotsEqual(a, b)) - }) - t.Run("differing GCMConsentTypes order is not equal", func(t *testing.T) { t.Parallel() @@ -214,8 +193,8 @@ func TestBuildSnapshot_RankInvariant(t *testing.T) { t.Run("snapshot is identical regardless of rank values", func(t *testing.T) { t.Parallel() - original := buildSnapshot(banner, mkCategories(0, 1, 2, 3), nil, nil) - shuffled := buildSnapshot(banner, mkCategories(99, 50, 25, 10), nil, nil) + original := buildSnapshot(banner, mkCategories(0, 1, 2, 3), nil) + shuffled := buildSnapshot(banner, mkCategories(99, 50, 25, 10), nil) assert.True(t, snapshotsEqual(original, shuffled), "rank changes must not affect the snapshot") }) @@ -226,8 +205,8 @@ func TestBuildSnapshot_RankInvariant(t *testing.T) { ordered := mkCategories(0, 1, 2, 3) reversed := coredata.CookieCategories{ordered[3], ordered[2], ordered[1], ordered[0]} - a := buildSnapshot(banner, ordered, nil, nil) - b := buildSnapshot(banner, reversed, nil, nil) + a := buildSnapshot(banner, ordered, nil) + b := buildSnapshot(banner, reversed, nil) assert.True(t, snapshotsEqual(a, b)) }) @@ -235,7 +214,7 @@ func TestBuildSnapshot_RankInvariant(t *testing.T) { t.Run("Necessary comes first and Uncategorised comes last", func(t *testing.T) { t.Parallel() - snap := buildSnapshot(banner, mkCategories(0, 1, 2, 3), nil, nil) + snap := buildSnapshot(banner, mkCategories(0, 1, 2, 3), nil) require.Len(t, snap.Categories, 4) assert.Equal(t, coredata.CookieCategoryKindNecessary, snap.Categories[0].Kind) diff --git a/pkg/cookiebanner/snapshot.go b/pkg/cookiebanner/snapshot.go index 8c6ded86c..94048148b 100644 --- a/pkg/cookiebanner/snapshot.go +++ b/pkg/cookiebanner/snapshot.go @@ -24,6 +24,16 @@ import ( "go.probo.inc/probo/pkg/gid" ) +// resolveTranslations converts raw DB translations into the resolved map +// used by buildBannerConfig at serve time. Categories must be sorted in +// snapshot order so the positional category translations align. +func resolveTranslations( + translations coredata.CookieBannerTranslations, + categories coredata.CookieCategories, +) map[string]coredata.CookieBannerVersionSnapshotTranslation { + return buildSnapshotTranslations(translations, sortCategoriesForSnapshot(categories)) +} + // snapshotsEqual reports whether two version snapshots are visitor-identical. // buildSnapshot already normalises empty slices and nil maps, so reflect.DeepEqual // is sufficient and is the single chokepoint we'd extend if we ever wanted to @@ -67,7 +77,6 @@ func buildSnapshot( banner *coredata.CookieBanner, categories coredata.CookieCategories, allPatterns coredata.CookiePatterns, - translations coredata.CookieBannerTranslations, ) coredata.CookieBannerVersionSnapshot { categories = sortCategoriesForSnapshot(categories) @@ -104,8 +113,6 @@ func buildSnapshot( } } - snapshotTranslations := buildSnapshotTranslations(translations, categories) - return coredata.CookieBannerVersionSnapshot{ PrivacyPolicyURL: banner.PrivacyPolicyURL, CookiePolicyURL: banner.CookiePolicyURL, @@ -113,7 +120,6 @@ func buildSnapshot( ConsentMode: string(banner.ConsentMode), DefaultLanguage: banner.DefaultLanguage, Categories: snapshotCategories, - Translations: snapshotTranslations, } } diff --git a/pkg/coredata/cookie_banner_version.go b/pkg/coredata/cookie_banner_version.go index 4ce0b481c..7ea0421dc 100644 --- a/pkg/coredata/cookie_banner_version.go +++ b/pkg/coredata/cookie_banner_version.go @@ -30,13 +30,12 @@ import ( type ( CookieBannerVersionSnapshot struct { - PrivacyPolicyURL *string `json:"privacy_policy_url,omitempty"` - CookiePolicyURL string `json:"cookie_policy_url"` - ConsentExpiryDays int `json:"consent_expiry_days"` - ConsentMode string `json:"consent_mode"` - DefaultLanguage string `json:"default_language"` - Categories []CookieBannerVersionSnapshotCategory `json:"categories"` - Translations map[string]CookieBannerVersionSnapshotTranslation `json:"translations,omitempty"` + PrivacyPolicyURL *string `json:"privacy_policy_url,omitempty"` + CookiePolicyURL string `json:"cookie_policy_url"` + ConsentExpiryDays int `json:"consent_expiry_days"` + ConsentMode string `json:"consent_mode"` + DefaultLanguage string `json:"default_language"` + Categories []CookieBannerVersionSnapshotCategory `json:"categories"` } CookieBannerVersionSnapshotTranslation struct {