Skip to content

Commit

Permalink
add i18n
Browse files Browse the repository at this point in the history
  • Loading branch information
sunwei committed Dec 26, 2024
1 parent 7a27cc5 commit 5accb47
Show file tree
Hide file tree
Showing 46 changed files with 952 additions and 95 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ require (
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
Expand Down
1 change: 1 addition & 0 deletions internal/domain/contenthub/entity/contenthub.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type ContentHub struct {
TemplateExecutor contenthub.Template

*Cache
*Translator

*PageMap
*PageFinder
Expand Down
1 change: 1 addition & 0 deletions internal/domain/contenthub/entity/pagebuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func (b *PageBuilder) applyFrontMatter(p *Page) error {
p.title = b.fm.Title
p.Meta.Weight = b.fm.Weight
p.Meta.Parameters = b.fm.Params
p.Meta.Date = b.fm.Date

return nil
}
Expand Down
5 changes: 4 additions & 1 deletion internal/domain/contenthub/entity/pagecontentprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ func (c *ContentProvider) ContentSummary() (valueobject.ContentSummary, error) {
}
}

v.SummaryTruncated = c.content.summaryTruncated
v.Content = helpers.BytesToHTML(b)
if v.IsSummaryEmpty() {
v.ExtractSummary(b, c.f.MediaType)
c.content.summaryTruncated = v.SummaryTruncated
}

return &stale.Value[valueobject.ContentSummary]{
Value: v,
Expand Down
11 changes: 10 additions & 1 deletion internal/domain/contenthub/entity/pagemeta.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package entity

import "github.com/gohugonet/hugoverse/pkg/maps"
import (
"github.com/gohugonet/hugoverse/pkg/maps"
"time"
)

const (
Never = "never"
Expand All @@ -13,6 +16,8 @@ type Meta struct {
List string
Parameters maps.Params
Weight int

Date time.Time
}

func (m *Meta) Description() string {
Expand All @@ -27,6 +32,10 @@ func (m *Meta) PageWeight() int {
return m.Weight
}

func (m *Meta) PageDate() time.Time {
return m.Date
}

func (m *Meta) ShouldList(global bool) bool {
return m.shouldList(global)
}
Expand Down
112 changes: 112 additions & 0 deletions internal/domain/contenthub/entity/translator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package entity

import (
"context"
"errors"
"fmt"
"github.com/gohugoio/go-i18n/v2/i18n"
"github.com/gohugonet/hugoverse/internal/domain/contenthub"
"github.com/gohugonet/hugoverse/internal/domain/contenthub/valueobject"
"github.com/gohugonet/hugoverse/pkg/hreflect"
"github.com/gohugonet/hugoverse/pkg/loggers"
"github.com/spf13/cast"
"reflect"
"strings"
)

const ArtificialLangTagPrefix = "art-x-"

type TranslateFunc func(ctx context.Context, translationID string, templateData any) string

type Translator struct {
ContentLanguage string
TranslateFuncs map[string]TranslateFunc

Log loggers.Logger `json:"-"`
}

func (t *Translator) Translate(ctx context.Context, lang string, translationID string, templateData any) string {
if f, ok := t.TranslateFuncs[lang]; ok {
return f(ctx, translationID, templateData)
}

t.Log.Infof("Translation func for language %v not found, use default.", lang)
if f, ok := t.TranslateFuncs[t.ContentLanguage]; ok {
return f(ctx, translationID, templateData)
}

t.Log.Infoln("i18n not initialized; if you need string translations, check that you have a bundle in /i18n that matches the site language or the default language.")

return ""
}

func (t *Translator) SetupTranslateFuncs(bndl *i18n.Bundle) {
enableMissingTranslationPlaceholders := true

for _, lang := range bndl.LanguageTags() {
currentLang := lang
currentLangStr := currentLang.String()
// This may be pt-BR; make it case insensitive.
currentLangKey := strings.ToLower(strings.TrimPrefix(currentLangStr, ArtificialLangTagPrefix))
localizer := i18n.NewLocalizer(bndl, currentLangStr)
t.TranslateFuncs[currentLangKey] = func(ctx context.Context, translationID string, templateData any) string {
pluralCount := valueobject.GetPluralCount(templateData)

if templateData != nil {
tp := reflect.TypeOf(templateData)
if hreflect.IsInt(tp.Kind()) {
// This was how go-i18n worked in v1,
// and we keep it like this to avoid breaking
// lots of sites in the wild.
templateData = valueobject.IntCount(cast.ToInt(templateData))
} else {
//TODO setup with context
if _, ok := templateData.(contenthub.Page); ok {
// See issue 10782.
// The i18n has its own template handling and does not know about
// the context.Context.
// A common pattern is to pass Page to i18n, and use .ReadingTime etc.
// We need to improve this, but that requires some upstream changes.
// For now, just create a wrapper.
//templateData = page.PageWithContext{Page: p, Ctx: ctx}
}
}
}

translated, translatedLang, err := localizer.LocalizeWithTag(&i18n.LocalizeConfig{
MessageID: translationID,
TemplateData: templateData,
PluralCount: pluralCount,
})

sameLang := currentLang == translatedLang

if err == nil && sameLang {
return translated
}

if err != nil && sameLang && translated != "" {
// See #8492
// TODO(bep) this needs to be improved/fixed upstream,
// but currently we get an error even if the fallback to
// "other" succeeds.
if fmt.Sprintf("%T", err) == "i18n.pluralFormNotFoundError" {
return translated
}
}

var messageNotFoundErr *i18n.MessageNotFoundErr
if !errors.As(err, &messageNotFoundErr) {
t.Log.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err)
}

t.Log.Warnf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID)

if enableMissingTranslationPlaceholders {
return "[i18n] " + translationID
}

return translated
}
}
}
110 changes: 110 additions & 0 deletions internal/domain/contenthub/factory/hub.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,49 @@
package factory

import (
"fmt"
"github.com/gohugonet/hugoverse/internal/domain/contenthub"
"github.com/gohugonet/hugoverse/internal/domain/contenthub/entity"
"github.com/gohugonet/hugoverse/internal/domain/contenthub/valueobject"
"github.com/gohugonet/hugoverse/internal/domain/fs"
"github.com/gohugonet/hugoverse/pkg/cache/dynacache"
"github.com/gohugonet/hugoverse/pkg/cache/stale"
"github.com/gohugonet/hugoverse/pkg/doctree"
"github.com/gohugonet/hugoverse/pkg/helpers"
"github.com/gohugonet/hugoverse/pkg/herrors"
"github.com/gohugonet/hugoverse/pkg/loggers"
"github.com/gohugonet/hugoverse/pkg/paths"
"golang.org/x/text/language"
"strings"

"encoding/json"
"github.com/gohugoio/go-i18n/v2/i18n"
toml "github.com/pelletier/go-toml/v2"
yaml "gopkg.in/yaml.v2"
)

func New(services contenthub.Services) (*entity.ContentHub, error) {
log := loggers.NewDefault()
valueobject.SetupDefaultContentTypes()

cs, err := newContentSpec()
if err != nil {
return nil, err
}

t, err := newTranslator(services, log)
if err != nil {
return nil, err
}

cache := newCache()

ch := &entity.ContentHub{
Cache: cache,
Fs: services,
TemplateExecutor: nil,
Translator: t,

PageMap: &entity.PageMap{
PageTrees: newPageTree(),

Expand Down Expand Up @@ -181,3 +201,93 @@ func newConverterRegistry() (contenthub.ConverterRegistry, error) {
Converters: converters,
}, nil
}

func newTranslator(services contenthub.Services, log loggers.Logger) (*entity.Translator, error) {
defaultLangTag, err := language.Parse(services.DefaultLanguage())
if err != nil {
defaultLangTag = language.English
}
bundle := i18n.NewBundle(defaultLangTag)

bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)

if err := services.WalkI18n("", fs.WalkCallback{
HookPre: nil,
WalkFn: func(path string, info fs.FileMetaInfo) error {
if info.IsDir() {
return nil
}
file, err := valueobject.NewFileInfo(info)
if err != nil {
return err
}
return addTranslationFile(bundle, file)
},
HookPost: nil,
}, fs.WalkwayConfig{}); err != nil {
if !herrors.IsNotExist(err) {
return nil, err
}
}

t := &entity.Translator{
ContentLanguage: services.DefaultLanguage(),
TranslateFuncs: make(map[string]entity.TranslateFunc),

Log: log,
}
t.SetupTranslateFuncs(bundle)

return t, err
}

func addTranslationFile(bundle *i18n.Bundle, r *valueobject.File) error {
f, err := r.Open()
if err != nil {
return fmt.Errorf("failed to open translations file %q:: %w", r.LogicalName(), err)
}

b := helpers.ReaderToBytes(f)
f.Close()

name := r.LogicalName()
lang := paths.Filename(name)
tag := language.Make(lang)
if tag == language.Und {
try := entity.ArtificialLangTagPrefix + lang
_, err = language.Parse(try)
if err != nil {
return fmt.Errorf("%q: %s", try, err)
}
name = entity.ArtificialLangTagPrefix + name
}

_, err = bundle.ParseMessageFileBytes(b, name)
if err != nil {
if strings.Contains(err.Error(), "no plural rule") {
// https://github.com/gohugoio/hugo/issues/7798
name = entity.ArtificialLangTagPrefix + name
_, err = bundle.ParseMessageFileBytes(b, name)
if err == nil {
return nil
}
}
return errWithFileContext(fmt.Errorf("failed to load translations: %w", err), r)
}

return nil
}

func errWithFileContext(inerr error, r *valueobject.File) error {
realFilename := r.Filename()
f, err := r.Open()
if err != nil {
return inerr
}
defer f.Close()

return herrors.NewFileErrorFromName(inerr, realFilename).UpdateContent(f, nil)
}
3 changes: 3 additions & 0 deletions internal/domain/contenthub/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/spf13/afero"
goTmpl "html/template"
"io"
"time"
)

type ContentHub interface {
Expand Down Expand Up @@ -72,6 +73,7 @@ type FsService interface {
ContentFs() afero.Fs

WalkContent(start string, cb fs.WalkCallback, conf fs.WalkwayConfig) error
WalkI18n(start string, cb fs.WalkCallback, conf fs.WalkwayConfig) error
ReverseLookupContent(filename string, checkExists bool) ([]fs.ComponentPath, error)
}

Expand Down Expand Up @@ -285,6 +287,7 @@ type PageMeta interface {
Description() string
Params() maps.Params
PageWeight() int
PageDate() time.Time

ShouldList(global bool) bool
ShouldListAny() bool
Expand Down
Loading

0 comments on commit 5accb47

Please sign in to comment.