Skip to content

Commit

Permalink
ref:fix up translatable content (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
plastikfan committed Aug 23, 2024
1 parent f516547 commit 5631382
Show file tree
Hide file tree
Showing 32 changed files with 955 additions and 1,187 deletions.
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ tasks:

# derive a translation from the default
# ! the default active file does not contain hashes, just the extracted content
# ! the translate file contains the hashes
# ! the foreign translate file contains the hashes
# ! the active foreign file (locale/out/l10n/active.en-US.json) is empty
# ! pass the translate file(locale/out/l10n/translate.en-US.json) to your translator
#
Expand Down
7 changes: 4 additions & 3 deletions internal/nfs/ensure-path-at.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nfs

import (
"io/fs"
"os"
"path/filepath"
"strings"
Expand All @@ -22,7 +23,7 @@ import (
// If vfs is not provided, then the path is ensured directly on the native file
// system.

func EnsurePathAt(path, defaultFilename string, perm int,
func EnsurePathAt(path, defaultFilename string, perm fs.FileMode,
vfs ...MkDirAllFS,
) (at string, err error) {
var (
Expand All @@ -38,10 +39,10 @@ func EnsurePathAt(path, defaultFilename string, perm int,

if len(vfs) > 0 {
if !vfs[0].DirectoryExists(directory) {
err = vfs[0].MkDirAll(directory, os.FileMode(perm))
err = vfs[0].MkDirAll(directory, perm)
}
} else {
err = os.MkdirAll(directory, os.FileMode(perm))
err = os.MkdirAll(directory, perm)
}

return filepath.Clean(filepath.Join(directory, file)), err
Expand Down
34 changes: 18 additions & 16 deletions internal/nfs/file-systems.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ package nfs
import (
"io/fs"
"os"
"path/filepath"
"strings"
)

// NewStatFS creates a new fs.StatFS from a path
func NewStatFS(path string) fs.StatFS {
return StatFSFromFS(os.DirFS(path))
}

type readDirFS struct {
fsys fs.FS
}

// NewReadDirFS creates a ReadDirFS file system
// NewReadDirFS creates a ReadDirFS file system, which
// contains the ReadDir method that reads entries of a path
// from the native file system.
func NewReadDirFS(path string) fs.ReadDirFS {
return &readDirFS{
fsys: os.DirFS(path),
Expand All @@ -33,7 +38,8 @@ type queryStatusFS struct {
fsys fs.FS
}

func NewQueryStatusFS(fsys fs.FS) fs.StatFS {
// StatFSFromFS creates a file system upon which Stat can be invoked
func StatFSFromFS(fsys fs.FS) fs.StatFS {
return &queryStatusFS{
fsys: fsys,
}
Expand All @@ -57,25 +63,25 @@ type nativeDirFS struct {
// NewNativeDirFS creates an instance of MkDirAllFS from a path
func NewNativeDirFS(path string) MkDirAllFS {
return &nativeDirFS{
statFS: NewQueryStatusFS(NewReadDirFS(path)),
statFS: StatFSFromFS(NewReadDirFS(path)),
}
}

// FromNativeDirFS creates an instance of MkDirAllFS from a fs.FS
func FromNativeDirFS(fsys fs.FS) MkDirAllFS {
// DirFSFromFS creates a native instance of MkDirAllFS from a fs.FS
func DirFSFromFS(fsys fs.FS) MkDirAllFS {
return &nativeDirFS{
statFS: NewQueryStatusFS(fsys),
statFS: StatFSFromFS(fsys),
}
}

// FileExists return true if item at path exists as a file
func (f *nativeDirFS) FileExists(path string) bool {
fi, err := f.statFS.Stat(path)
info, err := f.statFS.Stat(path)
if err != nil {
return false
}

if fi.IsDir() {
if info.IsDir() {
return false
}

Expand All @@ -84,16 +90,12 @@ func (f *nativeDirFS) FileExists(path string) bool {

// DirectoryExists return true if item at path exists as a directory
func (f *nativeDirFS) DirectoryExists(path string) bool {
if strings.HasPrefix(path, string(filepath.Separator)) {
path = path[1:]
}

fileInfo, err := f.statFS.Stat(path)
info, err := f.statFS.Stat(path)
if err != nil {
return false
}

if !fileInfo.IsDir() {
if !info.IsDir() {
return false
}

Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

func createLocalizer(lang *LanguageInfo, sourceID string,
dirFS nfs.MkDirAllFS,
) (*Localizer, error) {
) (*i18n.Localizer, error) {
bundle := i18n.NewBundle(lang.Tag)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package translate

import "io/fs"
import (
"io/fs"

"github.com/nicksnyder/go-i18n/v2/i18n"
)

type multiplexor struct {
}

func (mx *multiplexor) invoke(localizer *Localizer, data Localisable) string {
return localizer.MustLocalize(&LocalizeConfig{
func (mx *multiplexor) invoke(localizer *i18n.Localizer, data Localisable) (string, error) {
return localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: data.Message(),
TemplateData: data,
})
Expand All @@ -21,18 +25,22 @@ type multiContainer struct {
func (mc *multiContainer) localise(data Localisable) (string, error) {
localizer, err := mc.find(data.SourceID())

return mc.invoke(localizer, data), err
if err != nil {
return "", err
}

return mc.invoke(localizer, data)
}

func (mc *multiContainer) add(info *LocalizerInfo) {
if _, found := mc.localizers[info.sourceID]; found {
if _, found := mc.localizers[info.SourceID]; found {
return
}

mc.localizers[info.sourceID] = info.Localizer
mc.localizers[info.SourceID] = info.Localizer
}

func (mc *multiContainer) find(id string) (*Localizer, error) {
func (mc *multiContainer) find(id string) (*i18n.Localizer, error) {
if loc, found := mc.localizers[id]; found {
return loc, nil
}
Expand Down
168 changes: 168 additions & 0 deletions internal/translate/translate-defs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package translate

import (
"io/fs"

"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/snivilised/li18ngo/internal/lo"
"github.com/snivilised/li18ngo/internal/nfs"
"golang.org/x/text/language"
)

// 📚 package: translate contains internal li18ngo definitions that
// client does not need direct access to.

const (
// Li18ngoSourceID the id that represents this module. If client want
// to provides translations for languages that li18ngo does not, then
// the localizer the create created for this purpose should use this
// SourceID. So whenever the Text function is used on templates defined
// inside this module, the translation process is directed to use the
// correct i18n.Localizer (identified by the SourceID). The Source is
// statically defined for all templates defined in li18ngo.
Li18ngoSourceID = "github.com/snivilised/li18ngo"
)

type (
SupportedLanguages []language.Tag

Localisable interface {
Message() *i18n.Message
SourceID() string
}

TranslationSource struct {
// Name of dependency's translation file
Name string
Path string
}

// TranslationFiles maps a source id to a TranslationSource
TranslationFiles map[string]TranslationSource

// LoadFrom denotes where to load the translation file from
LoadFrom struct {
// Path denoting where to load language file from, defaults to exe location
//
Path string

// Sources are the translation files that need to be loaded. They represent
// the client app/library its dependencies.
//
// The source id would typically be the name of a package that is the source
// of string messages that are to be translated. Actually, we could use
// the top level url of the package by convention, as that is unique.
// So li18ngo would use "github.com/snivilised/li18ngo" but clients
// are free to use whatever naming scheme they want to use for their own
// dependencies.
//
Sources TranslationFiles
}

// LocalizerCreatorFn represents the signature of the function can optionally
// provide to override how an i18n Localizer is created.
LocalizerCreatorFn func(li *LanguageInfo, sourceID string,
dirFS nfs.MkDirAllFS,
) (*i18n.Localizer, error)

// UseOptionFn functional options function required by Use.
UseOptionFn func(*UseOptions)

// UseOptions the options provided to the Use function
UseOptions struct {
// Tag sets the language to use
//
Tag language.Tag

// From denotes where to load the translation file from
//
From LoadFrom

// DefaultIsAcceptable controls whether an error is returned if the
// request language is not available. By default DefaultIsAcceptable
// is true so that the application continues in the default language
// even if the requested language is not available.
//
DefaultIsAcceptable bool

// Create allows the client to override the default function to create
// the i18n Localizer(s) (1 per language).
//
Create LocalizerCreatorFn

// Custom set-able by the client for what ever purpose is required.
//
Custom any

// FS is a file system from where translations are loaded from. This
// does not have to performed explicitly asa it will be created using
// the From field if not specified.
FS fs.StatFS
}

// LanguageInfo information pertaining to setting language. Auto detection
// is not supported. Any executable that supports i18n, should perform
// auto detection and then invoke Use, with the detected language tag
LanguageInfo struct {
UseOptions

// Default language reflects the base language. If all else fails, messages will
// be in this language. It is fixed at BritishEnglish reflecting the language this
// package is written in.
//
Default language.Tag

// Supported indicates the list of languages for which translations are available.
//
Supported SupportedLanguages
}

LocalizerInfo struct {
// Localizer by default created internally, but can be overridden by
// the client if they provide a create function to the Translator Factory
//
Localizer *i18n.Localizer

SourceID string
}

Translator interface {
Localise(data Localisable) string
LanguageInfo() *LanguageInfo
negotiate(other Translator) (Translator, error)
add(info *LocalizerInfo, source *TranslationSource)
}

localizerContainer map[string]*i18n.Localizer
)

// AddSource adds a translation source
func (lf *LoadFrom) AddSource(sourceID string, source *TranslationSource) {
if _, found := lf.Sources[sourceID]; !found {
lf.Sources[sourceID] = *source
}
}

var (
Tx Translator
DefaultLanguage = language.BritishEnglish
)

// Text is the function to use to obtain a string created from
// registered Localizers. The data parameter must be a go template
// defining the input parameters and the translatable message content.
func Text(data Localisable) string {
return Tx.Localise(data)
}

func ResetTx() {
// required only for unit tests
//
Tx = nil
}

func containsLanguage(languages SupportedLanguages, tag language.Tag) bool {
return lo.ContainsBy(languages, func(t language.Tag) bool {
return t == tag
})
}
Loading

0 comments on commit 5631382

Please sign in to comment.