Exclude translations from cookie banner version snapshots

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é <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-02 18:10:18 +04:00
parent 685ff91d74
commit 887453fee2
5 changed files with 58 additions and 64 deletions

View File

@@ -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)
})
}

View File

@@ -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
},
)

View File

@@ -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)

View File

@@ -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,
}
}

View File

@@ -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 {