Add console GraphQL schema and resolvers for i18n

Add CookieBannerTranslation type, defaultLanguage field on
CookieBanner, and upsert/delete mutations for managing
per-language translations from the console.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-23 10:49:19 +04:00
parent ef60f080f1
commit 04001c0d90
4 changed files with 154 additions and 1 deletions

View File

@@ -64,6 +64,7 @@ type (
PrivacyPolicyURL *string
ConsentExpiryDays *int
ConsentMode *coredata.CookieConsentMode
DefaultLanguage *string
}
UpdateCookieCategoryRequest struct {
@@ -706,7 +707,7 @@ func (s *Service) UpdateCookieBanner(
return fmt.Errorf("cannot load cookie banner: %w", err)
}
consentChanged := req.PrivacyPolicyURL != nil || req.ConsentExpiryDays != nil || req.ConsentMode != nil
consentChanged := req.PrivacyPolicyURL != nil || req.ConsentExpiryDays != nil || req.ConsentMode != nil || req.DefaultLanguage != nil
if req.Name != nil {
banner.Name = *req.Name
@@ -723,6 +724,9 @@ func (s *Service) UpdateCookieBanner(
if req.ConsentMode != nil {
banner.ConsentMode = *req.ConsentMode
}
if req.DefaultLanguage != nil {
banner.DefaultLanguage = *req.DefaultLanguage
}
banner.UpdatedAt = time.Now()

View File

@@ -7,6 +7,7 @@ package console_v1
import (
"context"
"encoding/json"
"errors"
"go.gearno.de/kit/log"
@@ -66,6 +67,24 @@ func (r *cookieBannerResolver) Categories(ctx context.Context, obj *types.Cookie
return types.NewCookieCategoryConnection(p, r, obj.ID), nil
}
// Translations is the resolver for the translations field.
func (r *cookieBannerResolver) Translations(ctx context.Context, obj *types.CookieBanner) ([]*types.CookieBannerTranslation, error) {
scope := coredata.NewScopeFromObjectID(obj.ID)
translations, err := r.cookieBanner.ListCookieBannerTranslations(ctx, scope, obj.ID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list cookie banner translations", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
result := make([]*types.CookieBannerTranslation, len(translations))
for i, t := range translations {
result[i] = types.NewCookieBannerTranslation(t)
}
return result, nil
}
// LatestVersion is the resolver for the latestVersion field.
func (r *cookieBannerResolver) LatestVersion(ctx context.Context, obj *types.CookieBanner) (*types.CookieBannerVersion, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionCookieBannerVersionList); err != nil {
@@ -254,6 +273,7 @@ func (r *mutationResolver) UpdateCookieBanner(ctx context.Context, input types.U
PrivacyPolicyURL: input.PrivacyPolicyURL,
ConsentExpiryDays: input.ConsentExpiryDays,
ConsentMode: input.ConsentMode,
DefaultLanguage: input.DefaultLanguage,
},
)
if err != nil {
@@ -717,6 +737,85 @@ func (r *mutationResolver) DeleteCookie(ctx context.Context, input types.DeleteC
}, nil
}
// UpsertCookieBannerTranslation is the resolver for the upsertCookieBannerTranslation field.
func (r *mutationResolver) UpsertCookieBannerTranslation(ctx context.Context, input types.UpsertCookieBannerTranslationInput) (*types.UpsertCookieBannerTranslationPayload, error) {
if err := r.authorize(ctx, input.CookieBannerID, probo.ActionCookieBannerUpdate); err != nil {
return nil, err
}
scope := coredata.NewScopeFromObjectID(input.CookieBannerID)
translation, err := r.cookieBanner.UpsertCookieBannerTranslation(
ctx,
scope,
cookiebanner.UpsertCookieBannerTranslationRequest{
CookieBannerID: input.CookieBannerID,
Language: input.Language,
Translations: json.RawMessage(input.Translations),
},
)
if err != nil {
if errors.Is(err, cookiebanner.ErrBannerNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
r.logger.ErrorCtx(ctx, "cannot upsert cookie banner translation", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
banner, err := r.cookieBanner.GetCookieBanner(ctx, scope, input.CookieBannerID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load cookie banner", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UpsertCookieBannerTranslationPayload{
CookieBannerTranslation: types.NewCookieBannerTranslation(translation),
CookieBanner: types.NewCookieBanner(banner),
}, nil
}
// DeleteCookieBannerTranslation is the resolver for the deleteCookieBannerTranslation field.
func (r *mutationResolver) DeleteCookieBannerTranslation(ctx context.Context, input types.DeleteCookieBannerTranslationInput) (*types.DeleteCookieBannerTranslationPayload, error) {
if err := r.authorize(ctx, input.CookieBannerID, probo.ActionCookieBannerUpdate); err != nil {
return nil, err
}
scope := coredata.NewScopeFromObjectID(input.CookieBannerID)
err := r.cookieBanner.DeleteCookieBannerTranslation(
ctx,
scope,
cookiebanner.DeleteCookieBannerTranslationRequest{
CookieBannerID: input.CookieBannerID,
Language: input.Language,
},
)
if err != nil {
if errors.Is(err, cookiebanner.ErrTranslationNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if errors.Is(err, cookiebanner.ErrBannerNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot delete cookie banner translation", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
banner, err := r.cookieBanner.GetCookieBanner(ctx, scope, input.CookieBannerID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load cookie banner", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.DeleteCookieBannerTranslationPayload{
DeletedLanguage: input.Language,
CookieBanner: types.NewCookieBanner(banner),
}, nil
}
// Cookie returns schema.CookieResolver implementation.
func (r *Resolver) Cookie() schema.CookieResolver { return &cookieResolver{r} }

View File

@@ -85,6 +85,7 @@ type CookieBanner implements Node {
consentExpiryDays: Int!
consentMode: CookieConsentMode!
showBranding: Boolean!
defaultLanguage: String!
organization: Organization @goField(forceResolver: true)
@@ -96,6 +97,8 @@ type CookieBanner implements Node {
orderBy: CookieCategoryOrder
): CookieCategoryConnection @goField(forceResolver: true)
translations: [CookieBannerTranslation!]! @goField(forceResolver: true)
latestVersion: CookieBannerVersion @goField(forceResolver: true)
createdAt: Datetime!
@@ -104,6 +107,14 @@ type CookieBanner implements Node {
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type CookieBannerTranslation implements Node {
id: ID!
language: String!
translations: String!
createdAt: Datetime!
updatedAt: Datetime!
}
type CookieCategory implements Node {
id: ID!
cookieBanner: CookieBanner @goField(forceResolver: true)
@@ -243,6 +254,12 @@ extend type Mutation {
createCookie(input: CreateCookieInput!): CreateCookiePayload!
updateCookie(input: UpdateCookieInput!): UpdateCookiePayload!
deleteCookie(input: DeleteCookieInput!): DeleteCookiePayload!
upsertCookieBannerTranslation(
input: UpsertCookieBannerTranslationInput!
): UpsertCookieBannerTranslationPayload!
deleteCookieBannerTranslation(
input: DeleteCookieBannerTranslationInput!
): DeleteCookieBannerTranslationPayload!
}
input CreateCookieBannerInput {
@@ -261,6 +278,7 @@ input UpdateCookieBannerInput {
privacyPolicyUrl: String
consentExpiryDays: Int
consentMode: CookieConsentMode
defaultLanguage: String
}
input DeleteCookieBannerInput {
@@ -387,3 +405,24 @@ type DeleteCookiePayload {
deletedCookieId: ID!
cookieBanner: CookieBanner!
}
input UpsertCookieBannerTranslationInput {
cookieBannerId: ID!
language: String!
translations: String!
}
input DeleteCookieBannerTranslationInput {
cookieBannerId: ID!
language: String!
}
type UpsertCookieBannerTranslationPayload {
cookieBannerTranslation: CookieBannerTranslation!
cookieBanner: CookieBanner!
}
type DeleteCookieBannerTranslationPayload {
deletedLanguage: String!
cookieBanner: CookieBanner!
}

View File

@@ -73,7 +73,18 @@ func NewCookieBanner(b *coredata.CookieBanner) *CookieBanner {
ConsentExpiryDays: b.ConsentExpiryDays,
ConsentMode: b.ConsentMode,
ShowBranding: b.ShowBranding,
DefaultLanguage: b.DefaultLanguage,
CreatedAt: b.CreatedAt,
UpdatedAt: b.UpdatedAt,
}
}
func NewCookieBannerTranslation(t *coredata.CookieBannerTranslation) *CookieBannerTranslation {
return &CookieBannerTranslation{
ID: t.ID,
Language: t.Language,
Translations: string(t.Translations),
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
}