feat: storage, language rules and migration

This commit is contained in:
Florian Sylvain committed 2026-11-01 17:50:28 +01:00
1 parent 8c4a1825ab
commit 0806733d5c
70 files changed
+1771 -132

No files matched your search

+4 -1
View File
@@ -17,7 +17,7 @@ var (
ErrArticleOffline = errors.New("article is offline")
ErrInvalidSlug = errors.New("invalid slug")
ErrSlugTaken = errors.New("another article on this site already uses that slug")
ErrSlugTaken = errors.New("another article on this site in that language already uses that slug")
ErrInvalidCanonicalURL = errors.New("invalid canonical url")
)
@@ -134,6 +134,9 @@ type Article struct {
SEODismissed []string `json:"seo_dismissed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Lang string `json:"lang"`
StoryID uint32 `json:"story_id"`
ContentRevision uint32 `json:"-"`
}
func (a Article) UsesImage(imageID uint32) bool {
@@ -102,7 +102,7 @@ type Repository interface {
Delete(id uint32) error
FindByBodyContaining(filename string) ([]Article, error)
FindByImage(imageID uint32) ([]Article, error)
SlugTaken(siteID uint32, slug string, exceptID uint32) (bool, error)
SlugTaken(siteID uint32, lang string, slug string, exceptID uint32) (bool, error)
ClearImage(imageID uint32) (ImageReferences, error)
RestoreImage(references ImageReferences, imageID uint32) error
RepointImage(fromImageIDs []uint32, toImageID uint32) ([]uint32, error)
@@ -0,0 +1,74 @@
package language
import (
"errors"
"regexp"
"slices"
"strings"
)
var (
ErrInvalidLanguage = errors.New("invalid language tag")
ErrUnsupportedLanguage = errors.New("unsupported language")
ErrNoDefaultLanguage = errors.New("the site has no default language")
)
const (
Unassigned = ""
MaxTagLength = 35
)
var supported = []string{"en", "en-GB", "en-US", "fr", "fr-CA", "pt", "pt-BR", "es", "de", "ar"}
var tagPattern = regexp.MustCompile(`^([A-Za-z]{2,3})(?:-([A-Za-z]{2}))?$`)
func Supported() []string {
return slices.Clone(supported)
}
func Normalize(tag string) (string, error) {
parts := tagPattern.FindStringSubmatch(strings.TrimSpace(tag))
if parts == nil {
return "", ErrInvalidLanguage
}
normalized := strings.ToLower(parts[1])
if parts[2] != "" {
normalized += "-" + strings.ToUpper(parts[2])
}
if !slices.Contains(supported, normalized) {
return "", ErrUnsupportedLanguage
}
return normalized, nil
}
type Suggestion struct {
Tag string
Found bool
Substituted bool
}
func Suggest(locale *string) Suggestion {
if locale == nil {
return Suggestion{}
}
tag, err := Normalize(*locale)
if err == nil {
return Suggestion{Tag: tag, Found: true}
}
if !errors.Is(err, ErrUnsupportedLanguage) {
return Suggestion{}
}
base, _, _ := strings.Cut(strings.TrimSpace(*locale), "-")
if fallback, err := Normalize(base); err == nil {
return Suggestion{Tag: fallback, Found: true, Substituted: true}
}
return Suggestion{}
}
type SiteLanguage struct {
SiteID uint32
Tag string
IsDefault bool
Enabled bool
Confirmed bool
}
@@ -0,0 +1,7 @@
package language
type Repository interface {
ListBySite(siteID uint32) ([]SiteLanguage, error)
LockStartingLanguage(siteID uint32) (string, error)
ConfirmDefault(siteID uint32) error
}
@@ -5,16 +5,18 @@ import (
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/language"
"RenewCMS/internal/domain/site"
"context"
)
type Repositories struct {
Articles article.Repository
Sites site.Repository
Authors author.Repository
Entries entry.Repository
Images image.Repository
Articles article.Repository
Sites site.Repository
Authors author.Repository
Entries entry.Repository
Images image.Repository
Languages language.Repository
}
type Transactor interface {