Merge pull request #69 from m-horky/is-translated

Runtime translation detection
This commit is contained in:
Leonel Quinteros 2023-04-13 08:53:40 -03:00 committed by GitHub
commit 4829902c8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 332 additions and 31 deletions

View file

@ -35,9 +35,9 @@ type Domain struct {
pluralforms plurals.Expression
// Storage
translations map[string]*Translation
contexts map[string]map[string]*Translation
pluralTranslations map[string]*Translation
translations map[string]*Translation
contextTranslations map[string]map[string]*Translation
pluralTranslations map[string]*Translation
// Sync Mutex
trMutex sync.RWMutex
@ -84,7 +84,7 @@ func NewDomain() *Domain {
domain.Headers = make(HeaderMap)
domain.headerComments = make([]string, 0)
domain.translations = make(map[string]*Translation)
domain.contexts = make(map[string]map[string]*Translation)
domain.contextTranslations = make(map[string]map[string]*Translation)
domain.pluralTranslations = make(map[string]*Translation)
return domain
@ -183,14 +183,14 @@ func (do *Domain) DropStaleTranslations() {
defer do.trMutex.Unlock()
defer do.pluralMutex.Unlock()
for name, ctx := range do.contexts {
for name, ctx := range do.contextTranslations {
for id, trans := range ctx {
if trans.IsStale() {
delete(ctx, id)
}
}
if len(ctx) == 0 {
delete(do.contexts, name)
delete(do.contextTranslations, name)
}
}
@ -312,7 +312,7 @@ func (do *Domain) SetC(id, ctx, str string) {
defer do.trMutex.Unlock()
defer do.pluralMutex.Unlock()
if context, ok := do.contexts[ctx]; ok {
if context, ok := do.contextTranslations[ctx]; ok {
if trans, hasTrans := context[id]; hasTrans {
trans.Set(str)
} else {
@ -325,7 +325,7 @@ func (do *Domain) SetC(id, ctx, str string) {
trans := NewTranslation()
trans.ID = id
trans.Set(str)
do.contexts[ctx] = map[string]*Translation{
do.contextTranslations[ctx] = map[string]*Translation{
id: trans,
}
}
@ -337,11 +337,11 @@ func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.contexts != nil {
if _, ok := do.contexts[ctx]; ok {
if do.contexts[ctx] != nil {
if _, ok := do.contexts[ctx][str]; ok {
return Printf(do.contexts[ctx][str].Get(), vars...)
if do.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return Printf(do.contextTranslations[ctx][str].Get(), vars...)
}
}
}
@ -361,7 +361,7 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
defer do.trMutex.Unlock()
defer do.pluralMutex.Unlock()
if context, ok := do.contexts[ctx]; ok {
if context, ok := do.contextTranslations[ctx]; ok {
if trans, hasTrans := context[id]; hasTrans {
trans.SetN(pluralForm, str)
} else {
@ -374,7 +374,7 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
trans := NewTranslation()
trans.ID = id
trans.SetN(pluralForm, str)
do.contexts[ctx] = map[string]*Translation{
do.contextTranslations[ctx] = map[string]*Translation{
id: trans,
}
}
@ -386,11 +386,11 @@ func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.contexts != nil {
if _, ok := do.contexts[ctx]; ok {
if do.contexts[ctx] != nil {
if _, ok := do.contexts[ctx][str]; ok {
return Printf(do.contexts[ctx][str].GetN(do.pluralForm(n)), vars...)
if do.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return Printf(do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
}
}
}
@ -402,7 +402,51 @@ func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface
return Printf(plural, vars...)
}
//GetTranslations returns a copy of every translation in the domain. It does not support contexts.
// IsTranslated reports whether a string is translated
func (do *Domain) IsTranslated(str string) bool {
return do.IsTranslatedN(str, 0)
}
// IsTranslatedN reports whether a plural string is translated
func (do *Domain) IsTranslatedN(str string, n int) bool {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations == nil {
return false
}
tr, ok := do.translations[str]
if !ok {
return false
}
return tr.IsTranslatedN(n)
}
// IsTranslatedC reports whether a context string is translated
func (do *Domain) IsTranslatedC(str, ctx string) bool {
return do.IsTranslatedNC(str, 0, ctx)
}
// IsTranslatedNC reports whether a plural context string is translated
func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.contextTranslations == nil {
return false
}
translations, ok := do.contextTranslations[ctx]
if !ok {
return false
}
tr, ok := translations[str]
if !ok {
return false
}
return tr.IsTranslatedN(n)
}
// GetTranslations returns a copy of every translation in the domain. It does not support contexts.
func (do *Domain) GetTranslations() map[string]*Translation {
all := make(map[string]*Translation, len(do.translations))
@ -511,7 +555,7 @@ func (do *Domain) MarshalText() ([]byte, error) {
// Just as with headers, output translations in consistent order (to minimise diffs between round-trips), with (first) source reference taking priority, followed by context and finally ID
references := make([]SourceReference, 0)
for name, ctx := range do.contexts {
for name, ctx := range do.contextTranslations {
for id, trans := range ctx {
if id == "" {
continue
@ -623,7 +667,7 @@ func (do *Domain) MarshalBinary() ([]byte, error) {
obj.Nplurals = do.nplurals
obj.Plural = do.plural
obj.Translations = do.translations
obj.Contexts = do.contexts
obj.Contexts = do.contextTranslations
var buff bytes.Buffer
encoder := gob.NewEncoder(&buff)
@ -649,7 +693,7 @@ func (do *Domain) UnmarshalBinary(data []byte) error {
do.nplurals = obj.Nplurals
do.plural = obj.Plural
do.translations = obj.Translations
do.contexts = obj.Contexts
do.contextTranslations = obj.Contexts
if expr, err := plurals.Compile(do.plural); err == nil {
do.pluralforms = expr

View file

@ -6,6 +6,7 @@ import (
const (
enUSFixture = "fixtures/en_US/default.po"
arFixture = "fixtures/ar/categories.po"
)
//since both Po and Mo just pass-through to Domain for MarshalBinary and UnmarshalBinary, test it here
@ -72,6 +73,58 @@ func TestDomain_GetTranslations(t *testing.T) {
}
}
func TestDomain_IsTranslated(t *testing.T) {
englishPo := NewPo()
englishPo.ParseFile(enUSFixture)
english := englishPo.GetDomain()
// singular and plural
if english.IsTranslated("My Text") {
t.Error("'My text' should be reported as translated.")
}
if english.IsTranslated("Another string") {
t.Error("'Another string' should be reported as not translated.")
}
if !english.IsTranslatedN("Empty plural form singular", 0) {
t.Error("'Empty plural form singular' should be reported as translated for n=0.")
}
if english.IsTranslatedN("Empty plural form singular", 1) {
t.Error("'Empty plural form singular' should be reported as not translated for n=1.")
}
arabicPo := NewPo()
arabicPo.ParseFile(arFixture)
arabic := arabicPo.GetDomain()
// multiple plurals
if !arabic.IsTranslated("Load %d more document") {
t.Error("Arabic singular should be reported as translated.")
}
if !arabic.IsTranslatedN("Load %d more document", 0) {
t.Error("Arabic plural should be reported as translated for n=0.")
}
if !arabic.IsTranslatedN("Load %d more document", 1) {
t.Error("Arabic plural should be reported as translated for n=1.")
}
if !arabic.IsTranslatedN("Load %d more document", 5) {
t.Error("Arabic plural should be reported as translated for n=5.")
}
if arabic.IsTranslatedN("Load %d more document", 6) {
t.Error("Arabic plural should be reported as not translated for n=6.")
}
// context
if !english.IsTranslatedC("One with var: %s", "Ctx") {
t.Error("Context singular should be reported as translated.")
}
if !english.IsTranslatedNC("One with var: %s", 0, "Ctx") {
t.Error("Context plural should be reported as translated for n=0")
}
if english.IsTranslatedNC("One with var: %s", 2, "Ctx") {
t.Error("Context plural should be reported as translated for n=2")
}
}
func TestDomain_CheckExportFormatting(t *testing.T) {
po := NewPo()
po.Set("myid", "test string\nwith \"newline\"")

View file

@ -245,3 +245,57 @@ func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) str
return tr
}
// IsTranslated reports whether a string is translated
func IsTranslated(str string) bool {
return IsTranslatedND(GetDomain(), str, 0)
}
// IsTranslatedN reports whether a plural string is translated
func IsTranslatedN(str string, n int) bool {
return IsTranslatedND(GetDomain(), str, n)
}
// IsTranslatedD reports whether a domain string is translated
func IsTranslatedD(dom, str string) bool {
return IsTranslatedND(dom, str, 0)
}
// IsTranslatedND reports whether a plural domain string is translated
func IsTranslatedND(dom, str string, n int) bool {
loadStorage(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
if _, ok := globalConfig.storage.Domains[dom]; !ok {
globalConfig.storage.AddDomain(dom)
}
return globalConfig.storage.IsTranslatedND(dom, str, n)
}
// IsTranslatedC reports whether a context string is translated
func IsTranslatedC(str, ctx string) bool {
return IsTranslatedNDC(GetDomain(), str, 0, ctx)
}
// IsTranslatedNC reports whether a plural context string is translated
func IsTranslatedNC(str string, n int, ctx string) bool {
return IsTranslatedNDC(GetDomain(), str, n, ctx)
}
// IsTranslatedDC reports whether a domain context string is translated
func IsTranslatedDC(dom, str, ctx string) bool {
return IsTranslatedNDC(dom, str, 0, ctx)
}
// IsTranslatedNDC reports whether a plural domain context string is translated
func IsTranslatedNDC(dom, str string, n int, ctx string) bool {
loadStorage(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
return globalConfig.storage.IsTranslatedNDC(dom, str, n, ctx)
}

View file

@ -176,6 +176,33 @@ msgstr "Another text on another domain"
if tr != "Another text on another domain" {
t.Errorf("Expected 'Another text on another domain' but got '%s'", tr)
}
// Test IsTranslation functions
if !IsTranslated("My text") {
t.Error("'My text' should be reported as translated.")
}
if IsTranslated("Another string") {
t.Error("'Another string' should be reported as not translated.")
}
plural := "One with var: %s"
if !IsTranslated(plural) {
t.Errorf("'%s' should be reported as translated for singular.", plural)
}
if !IsTranslatedN(plural, 0) {
t.Errorf("'%s' should be reported as translated for n=0.", plural)
}
if !IsTranslatedN(plural, 2) {
t.Errorf("'%s' should be reported as translated for n=2.", plural)
}
if !IsTranslatedC("Some random in a context", "Ctx") {
t.Errorf("'Some random in a context' should be reported as translated under context.")
}
if !IsTranslatedC(plural, "Ctx") {
t.Errorf("'%s' should be reported as translated for singular under context.", plural)
}
if !IsTranslatedNC(plural, 0, "Ctx") {
t.Errorf("'%s' should be reported as translated for n=0 under context.", plural)
}
}
func TestUntranslated(t *testing.T) {

25
introspector.go Normal file
View file

@ -0,0 +1,25 @@
package gotext
// IsTranslatedIntrospector is able to determine whether a given string is translated.
// Examples of this introspector are Po and Mo, which are specific to their domain.
// Locale holds multiple domains and also implements IsTranslatedDomainIntrospector.
type IsTranslatedIntrospector interface {
IsTranslated(str string) bool
IsTranslatedN(str string, n int) bool
IsTranslatedC(str, ctx string) bool
IsTranslatedNC(str string, n int, ctx string) bool
}
// IsTranslatedDomainIntrospector is able to determine whether a given string is translated.
// Example of this introspector is Locale, which holds multiple domains.
// Simpler objects that are domain-specific, like Po or Mo, implement IsTranslatedIntrospector.
type IsTranslatedDomainIntrospector interface {
IsTranslated(str string) bool
IsTranslatedN(str string, n int) bool
IsTranslatedD(dom, str string) bool
IsTranslatedND(dom, str string, n int) bool
IsTranslatedC(str, ctx string) bool
IsTranslatedNC(str string, n int, ctx string) bool
IsTranslatedDC(dom, str, ctx string) bool
IsTranslatedNDC(dom, str string, n int, ctx string) bool
}

View file

@ -311,6 +311,66 @@ func (l *Locale) GetTranslations() map[string]*Translation {
return all
}
// IsTranslated reports whether a string is translated
func (l *Locale) IsTranslated(str string) bool {
return l.IsTranslatedND(l.GetDomain(), str, 0)
}
// IsTranslatedN reports whether a plural string is translated
func (l *Locale) IsTranslatedN(str string, n int) bool {
return l.IsTranslatedND(l.GetDomain(), str, n)
}
// IsTranslatedD reports whether a domain string is translated
func (l *Locale) IsTranslatedD(dom, str string) bool {
return l.IsTranslatedND(dom, str, 0)
}
// IsTranslatedND reports whether a plural domain string is translated
func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
l.RLock()
defer l.RUnlock()
if l.Domains == nil {
return false
}
translator, ok := l.Domains[dom]
if !ok {
return false
}
return translator.GetDomain().IsTranslatedN(str, n)
}
// IsTranslatedC reports whether a context string is translated
func (l *Locale) IsTranslatedC(str, ctx string) bool {
return l.IsTranslatedNDC(l.GetDomain(), str, 0, ctx)
}
// IsTranslatedNC reports whether a plural context string is translated
func (l *Locale) IsTranslatedNC(str string, n int, ctx string) bool {
return l.IsTranslatedNDC(l.GetDomain(), str, n, ctx)
}
// IsTranslatedDC reports whether a domain context string is translated
func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
return l.IsTranslatedNDC(dom, str, 0, ctx)
}
// IsTranslatedNDC reports whether a plural domain context string is translated
func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
l.RLock()
defer l.RUnlock()
if l.Domains == nil {
return false
}
translator, ok := l.Domains[dom]
if !ok {
return false
}
return translator.GetDomain().IsTranslatedNC(str, n, ctx)
}
// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
type LocaleEncoding struct {
Path string

19
mo.go
View file

@ -92,6 +92,19 @@ func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{})
return mo.domain.GetNC(str, plural, n, ctx, vars...)
}
func (mo *Mo) IsTranslated(str string) bool {
return mo.domain.IsTranslated(str)
}
func (mo *Mo) IsTranslatedN(str string, n int) bool {
return mo.domain.IsTranslatedN(str, n)
}
func (mo *Mo) IsTranslatedC(str, ctx string) bool {
return mo.domain.IsTranslatedC(str, ctx)
}
func (mo *Mo) IsTranslatedNC(str string, n int, ctx string) bool {
return mo.domain.IsTranslatedNC(str, n, ctx)
}
func (mo *Mo) MarshalBinary() ([]byte, error) {
return mo.domain.MarshalBinary()
}
@ -263,10 +276,10 @@ func (mo *Mo) addTranslation(msgid, msgstr []byte) {
if len(msgctxt) > 0 {
// With context...
if _, ok := mo.domain.contexts[string(msgctxt)]; !ok {
mo.domain.contexts[string(msgctxt)] = make(map[string]*Translation)
if _, ok := mo.domain.contextTranslations[string(msgctxt)]; !ok {
mo.domain.contextTranslations[string(msgctxt)] = make(map[string]*Translation)
}
mo.domain.contexts[string(msgctxt)][translation.ID] = translation
mo.domain.contextTranslations[string(msgctxt)][translation.ID] = translation
} else {
mo.domain.translations[translation.ID] = translation
}

19
po.go
View file

@ -114,6 +114,19 @@ func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{})
return po.domain.GetNC(str, plural, n, ctx, vars...)
}
func (po *Po) IsTranslated(str string) bool {
return po.domain.IsTranslated(str)
}
func (po *Po) IsTranslatedN(str string, n int) bool {
return po.domain.IsTranslatedN(str, n)
}
func (po *Po) IsTranslatedC(str, ctx string) bool {
return po.domain.IsTranslatedC(str, ctx)
}
func (po *Po) IsTranslatedNC(str string, n int, ctx string) bool {
return po.domain.IsTranslatedNC(str, n, ctx)
}
func (po *Po) MarshalText() ([]byte, error) {
return po.domain.MarshalText()
}
@ -223,10 +236,10 @@ func (po *Po) saveBuffer() {
po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer
} else {
// With context...
if _, ok := po.domain.contexts[po.domain.ctxBuffer]; !ok {
po.domain.contexts[po.domain.ctxBuffer] = make(map[string]*Translation)
if _, ok := po.domain.contextTranslations[po.domain.ctxBuffer]; !ok {
po.domain.contextTranslations[po.domain.ctxBuffer] = make(map[string]*Translation)
}
po.domain.contexts[po.domain.ctxBuffer][po.domain.trBuffer.ID] = po.domain.trBuffer
po.domain.contextTranslations[po.domain.ctxBuffer][po.domain.trBuffer.ID] = po.domain.trBuffer
// Cleanup current context buffer if needed
if po.domain.trBuffer.ID != "" {

View file

@ -78,3 +78,15 @@ func (t *Translation) GetN(n int) string {
// Return untranslated plural by default
return t.PluralID
}
// IsTranslated reports whether a string is translated
func (t *Translation) IsTranslated() bool {
tr, ok := t.Trs[0]
return tr != "" && ok
}
// IsTranslatedN reports whether a plural string is translated
func (t *Translation) IsTranslatedN(n int) bool {
tr, ok := t.Trs[n]
return tr != "" && ok
}

View file

@ -61,7 +61,7 @@ func (te *TranslatorEncoding) GetTranslator() Translator {
po.domain.nplurals = te.Nplurals
po.domain.plural = te.Plural
po.domain.translations = te.Translations
po.domain.contexts = te.Contexts
po.domain.contextTranslations = te.Contexts
return po
}