forked from toolshed/abra
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1855ffcecf | ||
|
|
eb62135e3b | ||
|
|
e8a494d9f4 | ||
|
|
35c29580bb | ||
|
|
e458c531f7 | ||
|
|
d0d156dc30 | ||
|
|
14f05407f3 | ||
|
|
e611a3be38 | ||
|
|
b5f7d26c9d | ||
|
|
4b6e42171c |
@@ -1,6 +1,6 @@
|
|||||||
module coopcloud.tech/abra
|
module coopcloud.tech/abra
|
||||||
|
|
||||||
go 1.26.0
|
go 1.27
|
||||||
|
|
||||||
require (
|
require (
|
||||||
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977
|
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977
|
||||||
@@ -9,18 +9,24 @@ require (
|
|||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v1.0.0
|
github.com/charmbracelet/log v1.0.0
|
||||||
|
github.com/containers/image v3.0.2+incompatible
|
||||||
|
github.com/decentral1se/passgen v1.0.1
|
||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/docker/cli v28.4.0+incompatible
|
github.com/docker/cli v28.4.0+incompatible
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
github.com/go-git/go-git/v5 v5.19.3
|
github.com/go-git/go-git/v5 v5.19.3
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/leonelquinteros/gotext v1.7.2
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
|
github.com/leonelquinteros/gotext v1.8.0
|
||||||
github.com/moby/sys/signal v0.7.1
|
github.com/moby/sys/signal v0.7.1
|
||||||
github.com/moby/term v0.5.2
|
github.com/moby/term v0.5.2
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/regclient/regclient v0.11.5
|
github.com/regclient/regclient v0.11.5
|
||||||
github.com/schollz/progressbar/v3 v3.19.1
|
github.com/schollz/progressbar/v3 v3.19.1
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
github.com/stretchr/testify v1.12.1
|
||||||
|
golang.org/x/sys v0.49.0
|
||||||
golang.org/x/term v0.47.0
|
golang.org/x/term v0.47.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
@@ -49,15 +55,18 @@ require (
|
|||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
|
github.com/containers/storage v1.38.2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
|
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-metrics v0.0.1 // indirect
|
github.com/docker/go-metrics v0.0.1 // indirect
|
||||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.9.2 // indirect
|
github.com/go-git/go-billy/v5 v5.9.2 // indirect
|
||||||
@@ -66,7 +75,9 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
@@ -84,7 +95,9 @@ require (
|
|||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/go-archive v0.1.0 // indirect
|
github.com/moby/go-archive v0.1.0 // indirect
|
||||||
|
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
github.com/moby/sys/user v0.4.0 // indirect
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
github.com/moby/sys/userns v0.1.0 // indirect
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/morikuni/aec v1.1.0 // indirect
|
github.com/morikuni/aec v1.1.0 // indirect
|
||||||
@@ -93,19 +106,24 @@ require (
|
|||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/opencontainers/runc v1.1.13 // indirect
|
github.com/opencontainers/runc v1.1.13 // indirect
|
||||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.7.0 // indirect
|
github.com/pjbgf/sha1cd v0.7.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.5 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.20.1 // indirect
|
github.com/prometheus/procfs v0.20.1 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
@@ -136,27 +154,6 @@ require (
|
|||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/containers/image v3.0.2+incompatible
|
|
||||||
github.com/containers/storage v1.38.2 // indirect
|
|
||||||
github.com/decentral1se/passgen v1.0.1
|
|
||||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
|
||||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
|
||||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
|
||||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
|
||||||
github.com/spf13/cobra v1.10.2
|
|
||||||
github.com/stretchr/testify v1.12.1
|
|
||||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
|
||||||
golang.org/x/sys v0.49.0
|
|
||||||
)
|
|
||||||
|
|
||||||
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
|
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
|
||||||
|
|
||||||
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2
|
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2
|
||||||
|
|||||||
@@ -604,8 +604,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
|
github.com/leonelquinteros/gotext v1.8.0 h1:wwpxINWmWq3JN3WFI9BoSupBFrvAqxZTtf3qppijYkU=
|
||||||
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
|
github.com/leonelquinteros/gotext v1.8.0/go.mod h1:Wqke380+8unZq2F4oTmmqmbUY9zWQMzwImdC34XOAo8=
|
||||||
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
|
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
|
|||||||
+139
-159
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+148
-171
File diff suppressed because it is too large
Load Diff
+15
-18
@@ -593,15 +593,7 @@ func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
|
|||||||
// sortRecipeVersions sorts the recipe semver versions
|
// sortRecipeVersions sorts the recipe semver versions
|
||||||
func sortRecipeVersions(versions RecipeVersions) {
|
func sortRecipeVersions(versions RecipeVersions) {
|
||||||
sort.Slice(versions, func(i, j int) bool {
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
version1, err := tagcmp.Parse(getVersionString(versions[i]))
|
return safeCompareVersions(getVersionString(versions[i]), getVersionString(versions[j]))
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
version2, err := tagcmp.Parse(getVersionString(versions[j]))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return version1.IsLessThan(version2)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,18 +609,23 @@ func getVersionString(versionMap map[string]map[string]ServiceMeta) string {
|
|||||||
// sortVersionStrings sorts a list of semver version strings
|
// sortVersionStrings sorts a list of semver version strings
|
||||||
func sortVersionStrings(versions []string) {
|
func sortVersionStrings(versions []string) {
|
||||||
sort.Slice(versions, func(i, j int) bool {
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
version1, err := tagcmp.Parse(versions[i])
|
return safeCompareVersions(versions[i], versions[j])
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
version2, err := tagcmp.Parse(versions[j])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return version1.IsLessThan(version2)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalid versions are sorted as less than valid ones.
|
||||||
|
func safeCompareVersions(v1, v2 string) bool {
|
||||||
|
version1, err1 := tagcmp.Parse(v1)
|
||||||
|
if err1 != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
version2, err2 := tagcmp.Parse(v2)
|
||||||
|
if err2 != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return version1.IsLessThan(version2)
|
||||||
|
}
|
||||||
|
|
||||||
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
|
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
|
||||||
func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]string, error) {
|
func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]string, error) {
|
||||||
var versions []string
|
var versions []string
|
||||||
|
|||||||
@@ -123,3 +123,41 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
|
|||||||
assert.NotEqual(t, label, timeout)
|
assert.NotEqual(t, label, timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is not a full test of comparison, as that occurs in the external
|
||||||
|
// tagcmp package. This is a sunny-day integration test
|
||||||
|
func TestSortRecipeVersionsSortsTwoValidStrings(t *testing.T) {
|
||||||
|
versions := RecipeVersions{
|
||||||
|
{"2.0.0": {}}, // Index 0
|
||||||
|
{"1.0.0": {}}, // Index 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sortRecipeVersions(versions)
|
||||||
|
|
||||||
|
assert.Equal(t, "1.0.0", getVersionString(versions[0]), "Version 1.0.0 should be first")
|
||||||
|
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSortRecipeVersionsSortsInvalidVersionStringToTheBottom(t *testing.T) {
|
||||||
|
versions := RecipeVersions{
|
||||||
|
{"2.0.0": {}}, // Index 0
|
||||||
|
{"invalid": {}}, // Index 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sortRecipeVersions(versions)
|
||||||
|
|
||||||
|
assert.Equal(t, "invalid", getVersionString(versions[0]), "The invalid version should be first")
|
||||||
|
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSortRecipeVersionsSortsInvalidVersionStringToTheBottomInReverse(t *testing.T) {
|
||||||
|
versions := RecipeVersions{
|
||||||
|
{"invalid": {}}, // Index 0
|
||||||
|
{"2.0.0": {}}, // Index 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sortRecipeVersions(versions)
|
||||||
|
|
||||||
|
assert.Equal(t, "invalid", getVersionString(versions[0]), "The invalid version should be first")
|
||||||
|
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: nginx:1.31.6
|
image: nginx:1.31.7
|
||||||
secrets:
|
secrets:
|
||||||
- test_pass_one
|
- test_pass_one
|
||||||
- test_pass_two
|
- test_pass_two
|
||||||
|
|||||||
@@ -724,7 +724,8 @@ teardown(){
|
|||||||
run sed -i 's/version=0.3.5+1.21.0/version=0.4.0+1.21.5/' "$recipeDir/compose.yml"
|
run sed -i 's/version=0.3.5+1.21.0/version=0.4.0+1.21.5/' "$recipeDir/compose.yml"
|
||||||
assert_success
|
assert_success
|
||||||
_git_commit
|
_git_commit
|
||||||
run git -C "$recipeDir" tag "0.4.0+1.21.5"
|
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||||
|
-a "0.4.0+1.21.5" -m "fake: 0.4.0+1.21.5"
|
||||||
assert_success
|
assert_success
|
||||||
|
|
||||||
# Manually upgrade and pin the next release to 1.21.6.
|
# Manually upgrade and pin the next release to 1.21.6.
|
||||||
@@ -740,7 +741,8 @@ teardown(){
|
|||||||
run sed -i 's/version=0.4.0+1.21.5/version=0.4.1+1.21.6/' "$recipeDir/compose.yml"
|
run sed -i 's/version=0.4.0+1.21.5/version=0.4.1+1.21.6/' "$recipeDir/compose.yml"
|
||||||
assert_success
|
assert_success
|
||||||
_git_commit
|
_git_commit
|
||||||
run git -C "$recipeDir" tag "0.4.1+1.21.6"
|
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||||
|
-a "0.4.1+1.21.6" -m "fake: 0.4.1+1.21.6"
|
||||||
assert_success
|
assert_success
|
||||||
|
|
||||||
# Deploy recipe 0.4.0 with the exact pinned 1.21.5 image.
|
# Deploy recipe 0.4.0 with the exact pinned 1.21.5 image.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: nginx:1.31.6
|
image: nginx:1.31.7
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
# AGENTS.md - gotext Project Guidelines
|
||||||
|
|
||||||
|
This document is the **source of truth** for the `gotext` project's architecture, conventions, and development workflows. All contributors — human or AI agent — must adhere to these guidelines.
|
||||||
|
|
||||||
|
Agent-specific configuration files (e.g., `GEMINI.md`, `CLAUDE.md`) should reference this document rather than duplicating its content.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
`gotext` is a GNU Gettext implementation for Go (Golang). It provides a thread-safe, hierarchical translation system that supports PO/MO files, plural forms, and context-aware translations.
|
||||||
|
|
||||||
|
## Core Architecture
|
||||||
|
|
||||||
|
The project follows a hierarchical structure for managing translations:
|
||||||
|
|
||||||
|
1. **Global Package API (`gotext.go`)**: Provides convenient, global functions (e.g., `Get`, `GetD`, `GetN`) that rely on a shared `globalConfig`.
|
||||||
|
2. **Locales (`locale.go`)**: Represent a specific language (e.g., "en_US", "es_ES"). A `Locale` container manages multiple `Domains`.
|
||||||
|
3. **Domains (`domain.go`)**: The core translation engine for a specific set of messages. It handles:
|
||||||
|
- Message retrieval (with support for plural forms and `msgctxt`).
|
||||||
|
- Header parsing (including plural-form rules).
|
||||||
|
- Thread-safety via `sync.RWMutex`.
|
||||||
|
4. **Translators (`translator.go`)**: An interface for translation sources.
|
||||||
|
- **PO (`po.go`)**: Parsers for human-readable Gettext Portable Object files.
|
||||||
|
- **MO (`mo.go`)**: Parsers for binary Gettext Machine Object files.
|
||||||
|
5. **Plural Forms (`plurals/`)**: A dedicated package for compiling and evaluating GNU Gettext plural form expressions.
|
||||||
|
|
||||||
|
## CLI Tools
|
||||||
|
|
||||||
|
- **xgotext (`cli/xgotext/`)**: A tool to scan Go source files, extract translatable strings (matching `Get`, `GetD`, etc.), and generate/update PO files.
|
||||||
|
|
||||||
|
## Key Files & Symbols
|
||||||
|
|
||||||
|
- `gotext.go`: `Configure`, `Get`, `GetD`, `GetN`.
|
||||||
|
- `locale.go`: `Locale`, `NewLocale`, `AddDomain`.
|
||||||
|
- `domain.go`: `Domain`, `Get`, `GetN`, `parseHeaders`.
|
||||||
|
- `translator.go`: `Translator` interface.
|
||||||
|
- `plurals/compiler.go`: `Compile` function for plural expressions.
|
||||||
|
|
||||||
|
## Development Workflows
|
||||||
|
|
||||||
|
### Branching & PRs
|
||||||
|
|
||||||
|
- **Feature Branches**: All development MUST happen in dedicated feature branches (e.g., `feature/description-of-change`, `fix/bug-description`, `refactor/topic`).
|
||||||
|
- **Target Branch**: All Pull Requests MUST be made against the `master` branch.
|
||||||
|
- **Review Requirement**: No direct commits to `master` are allowed. All changes must be reviewed via a PR.
|
||||||
|
- **GitHub Integration**: If the `gh` CLI tool is installed and authenticated, it should be used for managing Pull Requests, Issues, and Reviews. Otherwise, these tasks must be performed manually through the GitHub web interface.
|
||||||
|
|
||||||
|
### Coding Standards
|
||||||
|
|
||||||
|
- **Idiomatic Go**: Follow standard Go conventions (`gofmt`, `go vet`).
|
||||||
|
- **Thread Safety**: Ensure all shared state (like in `Domain`) is protected by appropriate synchronization primitives.
|
||||||
|
- **Testing**:
|
||||||
|
- Every new feature or bug fix must include corresponding tests.
|
||||||
|
- Use the `fixtures/` directory for sample PO/MO files.
|
||||||
|
- Run `go test ./...` to ensure no regressions.
|
||||||
|
|
||||||
|
### Pre-Submit Checklist
|
||||||
|
|
||||||
|
Before submitting a Pull Request, verify:
|
||||||
|
|
||||||
|
1. All existing tests pass: `go test ./...`
|
||||||
|
2. Code passes static analysis: `go vet ./...`
|
||||||
|
3. Code is properly formatted: `gofmt`
|
||||||
|
4. New features or bug fixes include corresponding tests.
|
||||||
|
5. Commit messages are clear and descriptive.
|
||||||
+2
-45
@@ -1,46 +1,3 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
# CODE OF CONDUCT
|
||||||
|
|
||||||
## Our Pledge
|
The canonical text of this project's Code of Conduct lives at [docs/CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leonel.quinteros@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
||||||
+4
-14
@@ -1,19 +1,9 @@
|
|||||||
# CONTRIBUTING
|
# CONTRIBUTING
|
||||||
|
|
||||||
This open source project welcomes everybody that wants to contribute to it by implementing new features, fixing bugs, testing, creating documentation or simply talk about it.
|
Thank you for considering contributing to `gotext`! All kinds of contributions — features, bug fixes, tests, documentation, or discussions — are welcome.
|
||||||
|
|
||||||
Most contributions will start by creating a new Issue to discuss what is the contribution about and to agree on the steps to move forward.
|
The canonical contribution guidelines live at [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md).
|
||||||
|
|
||||||
## Issues
|
Before contributing, please also read [AGENTS.md](AGENTS.md): it is the source of truth for project architecture, conventions, and development workflows (branching, PR process, coding standards, and the pre-submit checklist).
|
||||||
|
|
||||||
All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal.
|
|
||||||
|
|
||||||
This should be your start point of contribution.
|
|
||||||
|
|
||||||
|
|
||||||
## Pull Requests
|
|
||||||
|
|
||||||
If you have any changes that can be merged, feel free to send a Pull Request.
|
|
||||||
|
|
||||||
Usually, you'd want to create a new Issue to discuss about the change you want to merge and why it's needed or what it solves.
|
|
||||||
|
|
||||||
|
Start point: open an [Issue](https://github.com/leonelquinteros/gotext/issues) to discuss your idea, then send a Pull Request against `master`.
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
# GEMINI.md - Gemini CLI Instructions for gotext
|
||||||
|
|
||||||
|
> **Important**: Read and follow [`AGENTS.md`](./AGENTS.md) first. It is the source of truth for project architecture, conventions, and development workflows. The instructions below are specific to Gemini CLI and supplement — not replace — those guidelines.
|
||||||
|
|
||||||
|
## Gemini-Specific Instructions
|
||||||
|
|
||||||
|
- Always work in a feature branch. Never commit directly to `master`.
|
||||||
|
- Before submitting, verify changes with existing and new tests (`go test ./...`).
|
||||||
|
- Propose a PR to `master` after confirmation, using the `gh` CLI when available.
|
||||||
+101
-208
@@ -9,258 +9,151 @@
|
|||||||
|
|
||||||
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
|
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
|
||||||
|
|
||||||
|
`gotext` is a native Go implementation of the GNU Gettext utilities. It provides a thread-safe, flexible, and powerful way to handle internationalization (i18n) and localization (l10n) in your Go applications.
|
||||||
|
|
||||||
# Features
|
---
|
||||||
|
|
||||||
- Implements GNU gettext support in native Go.
|
**📖 [Read the Full Documentation](https://leonelquinteros.github.io/gotext/)**
|
||||||
- Complete support for [PO files](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) including:
|
|
||||||
- Support for multiline strings and headers.
|
|
||||||
- Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/).
|
|
||||||
- Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Translating-plural-forms.html).
|
|
||||||
- Support for [message contexts](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html).
|
|
||||||
- Support for MO files.
|
|
||||||
- Thread-safe: This package is safe for concurrent use across multiple goroutines.
|
|
||||||
- It works with UTF-8 encoding as it's the default for Go language.
|
|
||||||
- Unit tests available.
|
|
||||||
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
|
|
||||||
- Ready to use inside Go templates.
|
|
||||||
- Objects are serializable to []byte to store them in cache.
|
|
||||||
- Support for Go Modules.
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# License
|
### Table of Contents
|
||||||
|
- [Features](#features)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [CLI Tool (xgotext)](#cli-tool-xgotext)
|
||||||
|
- [Advanced Usage](#advanced-usage)
|
||||||
|
- [Locale Object](#using-locale-object)
|
||||||
|
- [Plural Forms](#use-plural-forms-of-translations)
|
||||||
|
- [Dynamic Variables](#using-dynamic-variables-on-translations)
|
||||||
|
- [Directory Structure](#locales-directories-structure)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
[MIT license](LICENSE)
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Native Go implementation of Gettext (the core library has no external dependencies).
|
||||||
|
- Full support for **PO and MO files**.
|
||||||
|
- **Pluralization rules** support via GNU Gettext plural-form expressions.
|
||||||
|
- **Message context** support (`msgctxt`).
|
||||||
|
- Thread-safe for concurrent use.
|
||||||
|
- Works with UTF-8 by default.
|
||||||
|
- Integrated CLI tool (`xgotext`) for string extraction.
|
||||||
|
- Serializable objects for caching.
|
||||||
|
- Plain-string results that plug directly into Go's `text/template` and `html/template` pipelines.
|
||||||
|
|
||||||
# Documentation
|
---
|
||||||
|
|
||||||
Refer to package documentation at (https://pkg.go.dev/github.com/leonelquinteros/gotext)
|
## Installation
|
||||||
|
```bash
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
go get github.com/leonelquinteros/gotext
|
go get github.com/leonelquinteros/gotext
|
||||||
```
|
```
|
||||||
|
|
||||||
- There are no requirements or dependencies to use this package.
|
---
|
||||||
- No need to install GNU gettext utilities (unless specific needs of CLI tools).
|
|
||||||
- No need for environment variables. Some naming conventions are applied but not needed.
|
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
# Usage examples
|
For a quick start, use the package-level API:
|
||||||
|
|
||||||
## Using package for single language/domain settings
|
|
||||||
|
|
||||||
For quick/simple translations you can use the package level functions directly.
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/leonelquinteros/gotext"
|
"github.com/leonelquinteros/gotext"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Configure package
|
// Configure package: locales path, language, and domain
|
||||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
gotext.Configure("/path/to/locales", "en_US", "default")
|
||||||
|
|
||||||
// Translate text from default domain
|
// Simple translation
|
||||||
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
fmt.Println(gotext.Get("Hello, world!"))
|
||||||
|
|
||||||
// Translate text from a different domain without reconfigure
|
// Translation with variables
|
||||||
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
|
fmt.Println(gotext.Get("Hello, %s!", "Gopher"))
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using dynamic variables on translations
|
For more details, see the [Getting Started Guide](docs/GETTING_STARTED.md).
|
||||||
|
|
||||||
All translation strings support dynamic variables to be inserted without translate.
|
---
|
||||||
Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string.
|
|
||||||
|
## CLI Tool (xgotext)
|
||||||
|
`gotext` includes a command-line tool to extract translatable strings from your Go source code.
|
||||||
|
|
||||||
|
**Install xgotext:**
|
||||||
|
```bash
|
||||||
|
go install github.com/leonelquinteros/gotext/cli/xgotext@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Extract strings:**
|
||||||
|
```bash
|
||||||
|
xgotext -in . -out locales/templates
|
||||||
|
```
|
||||||
|
The command generates `.pot` templates under `locales/templates`; merge those templates into language-specific `.po` files.
|
||||||
|
|
||||||
|
See the [xgotext Documentation](docs/xgotext.md) for full usage details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Using Locale object
|
||||||
|
For managing multiple languages or domains independently:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
l := gotext.NewLocale("/path/to/locales", "es_UY")
|
||||||
"fmt"
|
l.AddDomain("default")
|
||||||
"github.com/leonelquinteros/gotext"
|
fmt.Println(l.Get("Translate this"))
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Configure package
|
|
||||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
|
||||||
|
|
||||||
// Set variables
|
|
||||||
name := "John"
|
|
||||||
|
|
||||||
// Translate text with variables
|
|
||||||
fmt.Println(gotext.Get("Hi, my name is %s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Use plural forms of translations
|
||||||
## Using Locale object
|
`gotext` handles complex pluralization rules defined in PO headers:
|
||||||
|
|
||||||
When having multiple languages/domains/libraries at the same time, you can create Locale objects for each variation
|
|
||||||
so you can handle each settings on their own.
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
// GetN(singular, plural, quantity, args...)
|
||||||
"fmt"
|
fmt.Println(gotext.GetN("I have one apple.", "I have %d apples.", 5, 5))
|
||||||
"github.com/leonelquinteros/gotext"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Create Locale with library path and language code
|
|
||||||
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
|
|
||||||
|
|
||||||
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
|
|
||||||
l.AddDomain("default")
|
|
||||||
|
|
||||||
// Translate text from default domain
|
|
||||||
fmt.Println(l.Get("Translate this"))
|
|
||||||
|
|
||||||
// Load different domain
|
|
||||||
l.AddDomain("translations")
|
|
||||||
|
|
||||||
// Translate text from domain
|
|
||||||
fmt.Println(l.GetD("translations", "Translate this"))
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
See the [Plural Forms Guide](docs/PLURALS.md) for more examples.
|
||||||
|
|
||||||
This is also helpful for using inside templates (from the "text/template" package), where you can pass the Locale object to the template.
|
### Using dynamic variables on translations
|
||||||
If you set the Locale object as "Loc" in the template, then the template code would look like:
|
Supports standard `fmt` package syntax:
|
||||||
|
|
||||||
```
|
|
||||||
{{ .Loc.Get "Translate this" }}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Using the Po object to handle .po files and PO-formatted strings
|
|
||||||
|
|
||||||
For when you need to work with PO files and strings,
|
|
||||||
you can directly use the Po object to parse it and access the translations in there in the same way.
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
name := "John"
|
||||||
"fmt"
|
fmt.Println(gotext.Get("Hi, my name is %s", name))
|
||||||
"github.com/leonelquinteros/gotext"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Set PO content
|
|
||||||
str := `
|
|
||||||
msgid "Translate this"
|
|
||||||
msgstr "Translated text"
|
|
||||||
|
|
||||||
msgid "Another string"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "One with var: %s"
|
|
||||||
msgstr "This one sets the var: %s"
|
|
||||||
`
|
|
||||||
|
|
||||||
// Create Po object
|
|
||||||
po := gotext.NewPo()
|
|
||||||
po.Parse(str)
|
|
||||||
|
|
||||||
fmt.Println(po.Get("Translate this"))
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### More advanced scenarios
|
||||||
|
- **Embedded translation files** (`embed.FS`) and named `%(name)s` formatting: see the [Advanced Usage Guide](docs/ADVANCED.md).
|
||||||
|
- **Languages fallback**: pass multiple languages to `Configure` as a colon-separated list (`gotext.Configure("/path/to/locales", "es_UY:es:en_US", "default")`).
|
||||||
|
- **Serialization/caching** of parsed locales: `Locale.MarshalBinary` / `UnmarshalBinary`.
|
||||||
|
|
||||||
## Use plural forms of translations
|
---
|
||||||
|
|
||||||
PO format supports defining one or more plural forms for the same translation.
|
|
||||||
Relying on the PO file headers, a Plural-Forms formula can be set on the translation file
|
|
||||||
as defined in (https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html)
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/leonelquinteros/gotext"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Set PO content
|
|
||||||
str := `
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
# Header below
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
||||||
|
|
||||||
msgid "Translate this"
|
|
||||||
msgstr "Translated text"
|
|
||||||
|
|
||||||
msgid "Another string"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "One with var: %s"
|
|
||||||
msgid_plural "Several with vars: %s"
|
|
||||||
msgstr[0] "This one is the singular: %s"
|
|
||||||
msgstr[1] "This one is the plural: %s"
|
|
||||||
`
|
|
||||||
|
|
||||||
// Create Po object
|
|
||||||
po := new(gotext.Po)
|
|
||||||
po.Parse(str)
|
|
||||||
|
|
||||||
fmt.Println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
|
|
||||||
// "This one is the plural: Variable"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# Locales directories structure
|
|
||||||
|
|
||||||
The package will assume a directories structure starting with a base path that will be provided to the package configuration
|
|
||||||
or to object constructors depending on the use, but either will use the same convention to lookup inside the base path.
|
|
||||||
|
|
||||||
Inside the base directory where will be the language directories named using the language and country 2-letter codes (en_US, es_AR, ...).
|
|
||||||
All package functions can lookup after the simplified version for each language in case the full code isn't present but the more general language code exists.
|
|
||||||
So if the language set is `en_UK`, but there is no directory named after that code and there is a directory named `en`,
|
|
||||||
all package functions will be able to resolve this generalization and provide translations for the more general library.
|
|
||||||
|
|
||||||
The language codes are assumed to be [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes (2-letter codes).
|
|
||||||
That said, most functions will work with any coding standard as long the directory name matches the language code set on the configuration.
|
|
||||||
|
|
||||||
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
|
|
||||||
A library directory structure can look like:
|
|
||||||
|
|
||||||
|
## Locales directories structure
|
||||||
|
The package expects a standard Gettext directory structure:
|
||||||
```
|
```
|
||||||
/path/to/locales
|
/path/to/locales
|
||||||
/path/to/locales/en_US
|
/en_US
|
||||||
/path/to/locales/en_US/LC_MESSAGES
|
/LC_MESSAGES
|
||||||
/path/to/locales/en_US/LC_MESSAGES/default.po
|
default.po
|
||||||
/path/to/locales/en_US/LC_MESSAGES/extras.po
|
/es_ES
|
||||||
/path/to/locales/en_UK
|
/LC_MESSAGES
|
||||||
/path/to/locales/en_UK/LC_MESSAGES
|
default.po
|
||||||
/path/to/locales/en_UK/LC_MESSAGES/default.po
|
|
||||||
/path/to/locales/en_UK/LC_MESSAGES/extras.po
|
|
||||||
/path/to/locales/en_AU
|
|
||||||
/path/to/locales/en_AU/LC_MESSAGES
|
|
||||||
/path/to/locales/en_AU/LC_MESSAGES/default.po
|
|
||||||
/path/to/locales/en_AU/LC_MESSAGES/extras.po
|
|
||||||
/path/to/locales/es
|
|
||||||
/path/to/locales/es/default.po
|
|
||||||
/path/to/locales/es/extras.po
|
|
||||||
/path/to/locales/es_ES
|
|
||||||
/path/to/locales/es_ES/default.po
|
|
||||||
/path/to/locales/es_ES/extras.po
|
|
||||||
/path/to/locales/fr
|
|
||||||
/path/to/locales/fr/default.po
|
|
||||||
/path/to/locales/fr/extras.po
|
|
||||||
```
|
```
|
||||||
|
Translation files can live either inside an `LC_MESSAGES` subdirectory or directly under the language code (`/es_ES/default.po`); both layouts are supported.
|
||||||
|
|
||||||
And so on...
|
It supports automatic language simplification (e.g., falling back from `en_UK` to `en`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Contribute
|
## Contributing
|
||||||
|
We welcome contributions of all kinds!
|
||||||
|
- [Contributing Guidelines](CONTRIBUTING.md)
|
||||||
|
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
|
|
||||||
- Please, contribute.
|
---
|
||||||
- Use the package on your projects.
|
|
||||||
- Report issues on Github.
|
|
||||||
- Send pull requests for bugfixes and improvements.
|
|
||||||
- Send proposals on Github issues.
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
`gotext` is released under the [MIT License](LICENSE).
|
||||||
|
|||||||
+317
-158
@@ -4,7 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"maps"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -64,9 +64,6 @@ func (m HeaderMap) Del(key string) {
|
|||||||
|
|
||||||
// Get value for key from HeaderMap
|
// Get value for key from HeaderMap
|
||||||
func (m HeaderMap) Get(key string) string {
|
func (m HeaderMap) Get(key string) string {
|
||||||
if m == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
v := m[key]
|
v := m[key]
|
||||||
if len(v) == 0 {
|
if len(v) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -81,9 +78,6 @@ func (m HeaderMap) Set(key, value string) {
|
|||||||
|
|
||||||
// Values returns all values for a given key from HeaderMap
|
// Values returns all values for a given key from HeaderMap
|
||||||
func (m HeaderMap) Values(key string) []string {
|
func (m HeaderMap) Values(key string) []string {
|
||||||
if m == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return m[key]
|
return m[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,29 +94,111 @@ func NewDomain() *Domain {
|
|||||||
return domain
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneRefs(refs []string) []string {
|
||||||
|
if refs == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
owned := make([]string, len(refs))
|
||||||
|
copy(owned, refs)
|
||||||
|
return owned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneHeaderMap(headers HeaderMap) HeaderMap {
|
||||||
|
if headers == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
owned := make(HeaderMap, len(headers))
|
||||||
|
for key, values := range headers {
|
||||||
|
owned[key] = cloneRefs(values)
|
||||||
|
}
|
||||||
|
return owned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneTranslation(trans *Translation) *Translation {
|
||||||
|
if trans == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
owned := NewTranslation()
|
||||||
|
owned.ID = trans.ID
|
||||||
|
owned.PluralID = trans.PluralID
|
||||||
|
owned.dirty = trans.dirty
|
||||||
|
owned.Refs = cloneRefs(trans.Refs)
|
||||||
|
|
||||||
|
maps.Copy(owned.Trs, trans.Trs)
|
||||||
|
|
||||||
|
return owned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneTranslations(translations map[string]*Translation) map[string]*Translation {
|
||||||
|
if translations == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
owned := make(map[string]*Translation, len(translations))
|
||||||
|
for id, trans := range translations {
|
||||||
|
owned[id] = cloneTranslation(trans)
|
||||||
|
}
|
||||||
|
return owned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneContextTranslations(contexts map[string]map[string]*Translation) map[string]map[string]*Translation {
|
||||||
|
if contexts == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
owned := make(map[string]map[string]*Translation, len(contexts))
|
||||||
|
for context, translations := range contexts {
|
||||||
|
owned[context] = cloneTranslations(translations)
|
||||||
|
}
|
||||||
|
return owned
|
||||||
|
}
|
||||||
|
|
||||||
|
func (do *Domain) hasTranslation(id string) bool {
|
||||||
|
do.trMutex.RLock()
|
||||||
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
|
return do.translations[id] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (do *Domain) hasContextTranslation(id, context string) bool {
|
||||||
|
do.trMutex.RLock()
|
||||||
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
|
return do.contextTranslations[context][id] != nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetPluralResolver sets a custom plural resolver function
|
// SetPluralResolver sets a custom plural resolver function
|
||||||
func (do *Domain) SetPluralResolver(f func(int) int) {
|
func (do *Domain) SetPluralResolver(f func(int) int) {
|
||||||
|
do.pluralMutex.Lock()
|
||||||
|
defer do.pluralMutex.Unlock()
|
||||||
|
|
||||||
do.customPluralResolver = f
|
do.customPluralResolver = f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (do *Domain) pluralForm(n int) int {
|
func (do *Domain) pluralForm(n int) int {
|
||||||
// do we really need locking here? not sure how this plurals.Expression works, so sticking with it for now
|
// Snapshot plural state before calling an expression or custom resolver.
|
||||||
do.pluralMutex.RLock()
|
do.pluralMutex.RLock()
|
||||||
defer do.pluralMutex.RUnlock()
|
pluralforms := do.pluralforms
|
||||||
|
customPluralResolver := do.customPluralResolver
|
||||||
|
do.pluralMutex.RUnlock()
|
||||||
|
|
||||||
|
if pluralforms != nil {
|
||||||
|
return pluralforms.Eval(uint32(n))
|
||||||
|
}
|
||||||
|
|
||||||
// Failure fallback
|
// Failure fallback
|
||||||
if do.pluralforms == nil {
|
if customPluralResolver != nil {
|
||||||
if do.customPluralResolver != nil {
|
return customPluralResolver(n)
|
||||||
return do.customPluralResolver(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Use the Germanic plural rule. */
|
|
||||||
if n == 1 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
return do.pluralforms.Eval(uint32(n))
|
|
||||||
|
/* Use the Germanic plural rule. */
|
||||||
|
if n == 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseHeaders retrieves data from previously parsed headers. it's called by both Mo and Po when parsing
|
// parseHeaders retrieves data from previously parsed headers. it's called by both Mo and Po when parsing
|
||||||
@@ -136,18 +212,16 @@ func (do *Domain) parseHeaders() {
|
|||||||
languageKey := "Language"
|
languageKey := "Language"
|
||||||
pluralFormsKey := "Plural-Forms"
|
pluralFormsKey := "Plural-Forms"
|
||||||
|
|
||||||
rawLines := strings.Split(raw, "\n")
|
for line := range strings.SplitSeq(raw, "\n") {
|
||||||
for _, line := range rawLines {
|
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
colonIdx := strings.Index(line, ":")
|
key, value, ok := strings.Cut(line, ":")
|
||||||
if colonIdx < 0 {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key := line[:colonIdx]
|
|
||||||
lowerKey := strings.ToLower(key)
|
lowerKey := strings.ToLower(key)
|
||||||
if lowerKey == strings.ToLower(languageKey) {
|
if lowerKey == strings.ToLower(languageKey) {
|
||||||
languageKey = key
|
languageKey = key
|
||||||
@@ -155,7 +229,7 @@ func (do *Domain) parseHeaders() {
|
|||||||
pluralFormsKey = key
|
pluralFormsKey = key
|
||||||
}
|
}
|
||||||
|
|
||||||
value := strings.TrimSpace(line[colonIdx+1:])
|
value = strings.TrimSpace(value)
|
||||||
do.Headers.Add(key, value)
|
do.Headers.Add(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,22 +242,18 @@ func (do *Domain) parseHeaders() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split plural form header value
|
for i := range strings.SplitSeq(do.PluralForms, ";") {
|
||||||
pfs := strings.Split(do.PluralForms, ";")
|
key, value, ok := strings.Cut(i, "=")
|
||||||
|
if !ok {
|
||||||
// Parse values
|
|
||||||
for _, i := range pfs {
|
|
||||||
vs := strings.SplitN(i, "=", 2)
|
|
||||||
if len(vs) != 2 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch strings.TrimSpace(vs[0]) {
|
switch strings.TrimSpace(key) {
|
||||||
case "nplurals":
|
case "nplurals":
|
||||||
do.nplurals, _ = strconv.Atoi(vs[1])
|
do.nplurals, _ = strconv.Atoi(value)
|
||||||
|
|
||||||
case "plural":
|
case "plural":
|
||||||
do.plural = vs[1]
|
do.plural = value
|
||||||
|
|
||||||
if expr, err := plurals.Compile(do.plural); err == nil {
|
if expr, err := plurals.Compile(do.plural); err == nil {
|
||||||
do.pluralforms = expr
|
do.pluralforms = expr
|
||||||
@@ -221,17 +291,19 @@ func (do *Domain) DropStaleTranslations() {
|
|||||||
|
|
||||||
// SetRefs set source references for a given translation
|
// SetRefs set source references for a given translation
|
||||||
func (do *Domain) SetRefs(str string, refs []string) {
|
func (do *Domain) SetRefs(str string, refs []string) {
|
||||||
|
ownedRefs := cloneRefs(refs)
|
||||||
|
|
||||||
do.trMutex.Lock()
|
do.trMutex.Lock()
|
||||||
do.pluralMutex.Lock()
|
do.pluralMutex.Lock()
|
||||||
defer do.trMutex.Unlock()
|
defer do.trMutex.Unlock()
|
||||||
defer do.pluralMutex.Unlock()
|
defer do.pluralMutex.Unlock()
|
||||||
|
|
||||||
if trans, ok := do.translations[str]; ok {
|
if trans, ok := do.translations[str]; ok && trans != nil {
|
||||||
trans.Refs = refs
|
trans.SetRefs(ownedRefs)
|
||||||
} else {
|
} else {
|
||||||
trans = NewTranslation()
|
trans := NewTranslation()
|
||||||
trans.ID = str
|
trans.ID = str
|
||||||
trans.SetRefs(refs)
|
trans.SetRefs(ownedRefs)
|
||||||
do.translations[str] = trans
|
do.translations[str] = trans
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,8 +315,8 @@ func (do *Domain) GetRefs(str string) []string {
|
|||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
if do.translations != nil {
|
if do.translations != nil {
|
||||||
if trans, ok := do.translations[str]; ok {
|
if trans, ok := do.translations[str]; ok && trans != nil {
|
||||||
return trans.Refs
|
return cloneRefs(trans.Refs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -268,7 +340,7 @@ func (do *Domain) Set(id, str string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves the Translation for the given string.
|
// Get retrieves the Translation for the given string.
|
||||||
func (do *Domain) Get(str string, vars ...interface{}) string {
|
func (do *Domain) Get(str string, vars ...any) string {
|
||||||
// Sync read
|
// Sync read
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
@@ -284,8 +356,8 @@ func (do *Domain) Get(str string, vars ...interface{}) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Append retrieves the Translation for the given string.
|
// Append retrieves the Translation for the given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (do *Domain) Append(b []byte, str string, vars ...interface{}) []byte {
|
func (do *Domain) Append(b []byte, str string, vars ...any) []byte {
|
||||||
// Sync read
|
// Sync read
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
@@ -322,40 +394,48 @@ func (do *Domain) SetN(id, plural string, n int, str string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetN retrieves the (N)th plural form of Translation for the given string.
|
// GetN retrieves the (N)th plural form of Translation for the given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (do *Domain) GetN(str, plural string, n int, vars ...interface{}) string {
|
func (do *Domain) GetN(str, plural string, n int, vars ...any) string {
|
||||||
|
// Get plural form before acquiring the translation lock so a custom
|
||||||
|
// resolver is never called while a project lock is held.
|
||||||
|
pluralForm := do.pluralForm(n)
|
||||||
|
|
||||||
// Sync read
|
// Sync read
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
if do.translations != nil {
|
if do.translations != nil {
|
||||||
if _, ok := do.translations[str]; ok {
|
if _, ok := do.translations[str]; ok {
|
||||||
return FormatString(do.translations[str].GetN(do.pluralForm(n)), vars...)
|
return FormatString(do.translations[str].GetN(pluralForm), vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse plural forms to distinguish between plural and singular
|
// Parse plural forms to distinguish between plural and singular
|
||||||
if do.pluralForm(n) == 0 {
|
if pluralForm == 0 {
|
||||||
return FormatString(str, vars...)
|
return FormatString(str, vars...)
|
||||||
}
|
}
|
||||||
return FormatString(plural, vars...)
|
return FormatString(plural, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendN adds the (N)th plural form of Translation for the given string.
|
// AppendN adds the (N)th plural form of Translation for the given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
|
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
|
||||||
|
// Get plural form before acquiring the translation lock so a custom
|
||||||
|
// resolver is never called while a project lock is held.
|
||||||
|
pluralForm := do.pluralForm(n)
|
||||||
|
|
||||||
// Sync read
|
// Sync read
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
if do.translations != nil {
|
if do.translations != nil {
|
||||||
if _, ok := do.translations[str]; ok {
|
if _, ok := do.translations[str]; ok {
|
||||||
return Appendf(b, do.translations[str].GetN(do.pluralForm(n)), vars...)
|
return Appendf(b, do.translations[str].GetN(pluralForm), vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse plural forms to distinguish between plural and singular
|
// Parse plural forms to distinguish between plural and singular
|
||||||
if do.pluralForm(n) == 0 {
|
if pluralForm == 0 {
|
||||||
return Appendf(b, str, vars...)
|
return Appendf(b, str, vars...)
|
||||||
}
|
}
|
||||||
return Appendf(b, plural, vars...)
|
return Appendf(b, plural, vars...)
|
||||||
@@ -388,8 +468,8 @@ func (do *Domain) SetC(id, ctx, str string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetC retrieves the corresponding Translation for a given string in the given context.
|
// GetC retrieves the corresponding Translation for a given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
|
func (do *Domain) GetC(str, ctx string, vars ...any) string {
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
@@ -408,8 +488,8 @@ func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AppendC retrieves the corresponding Translation for a given string in the given context.
|
// AppendC retrieves the corresponding Translation for a given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
|
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...any) []byte {
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
@@ -443,12 +523,14 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
|
|||||||
} else {
|
} else {
|
||||||
trans = NewTranslation()
|
trans = NewTranslation()
|
||||||
trans.ID = id
|
trans.ID = id
|
||||||
|
trans.PluralID = plural
|
||||||
trans.SetN(pluralForm, str)
|
trans.SetN(pluralForm, str)
|
||||||
context[id] = trans
|
context[id] = trans
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trans := NewTranslation()
|
trans := NewTranslation()
|
||||||
trans.ID = id
|
trans.ID = id
|
||||||
|
trans.PluralID = plural
|
||||||
trans.SetN(pluralForm, str)
|
trans.SetN(pluralForm, str)
|
||||||
do.contextTranslations[ctx] = map[string]*Translation{
|
do.contextTranslations[ctx] = map[string]*Translation{
|
||||||
id: trans,
|
id: trans,
|
||||||
@@ -457,19 +539,22 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
|
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||||
do.trMutex.RLock()
|
if do.hasContextTranslation(str, ctx) {
|
||||||
defer do.trMutex.RUnlock()
|
// Get plural form before acquiring the translation lock so a custom
|
||||||
|
// resolver is never called while a project lock is held.
|
||||||
|
pluralForm := do.pluralForm(n)
|
||||||
|
|
||||||
if do.contextTranslations != nil {
|
do.trMutex.RLock()
|
||||||
if _, ok := do.contextTranslations[ctx]; ok {
|
if translations, ok := do.contextTranslations[ctx]; ok && translations != nil {
|
||||||
if do.contextTranslations[ctx] != nil {
|
if trans, ok := translations[str]; ok && trans != nil {
|
||||||
if _, ok := do.contextTranslations[ctx][str]; ok {
|
translated := trans.GetN(pluralForm)
|
||||||
return FormatString(do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
|
do.trMutex.RUnlock()
|
||||||
}
|
return FormatString(translated, vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
do.trMutex.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
@@ -479,19 +564,22 @@ func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AppendNC retrieves the (N)th plural form of Translation for the given string in the given context.
|
// AppendNC retrieves the (N)th plural form of Translation for the given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
|
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
|
||||||
do.trMutex.RLock()
|
if do.hasContextTranslation(str, ctx) {
|
||||||
defer do.trMutex.RUnlock()
|
// Get plural form before acquiring the translation lock so a custom
|
||||||
|
// resolver is never called while a project lock is held.
|
||||||
|
pluralForm := do.pluralForm(n)
|
||||||
|
|
||||||
if do.contextTranslations != nil {
|
do.trMutex.RLock()
|
||||||
if _, ok := do.contextTranslations[ctx]; ok {
|
if translations, ok := do.contextTranslations[ctx]; ok && translations != nil {
|
||||||
if do.contextTranslations[ctx] != nil {
|
if trans, ok := translations[str]; ok && trans != nil {
|
||||||
if _, ok := do.contextTranslations[ctx][str]; ok {
|
translated := trans.GetN(pluralForm)
|
||||||
return Appendf(b, do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
|
do.trMutex.RUnlock()
|
||||||
}
|
return Appendf(b, translated, vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
do.trMutex.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
@@ -507,6 +595,14 @@ func (do *Domain) IsTranslated(str string) bool {
|
|||||||
|
|
||||||
// IsTranslatedN reports whether a plural string is translated
|
// IsTranslatedN reports whether a plural string is translated
|
||||||
func (do *Domain) IsTranslatedN(str string, n int) bool {
|
func (do *Domain) IsTranslatedN(str string, n int) bool {
|
||||||
|
if !do.hasTranslation(str) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get plural form before acquiring the translation lock so a custom
|
||||||
|
// resolver is never called while a project lock is held.
|
||||||
|
pluralForm := do.pluralForm(n)
|
||||||
|
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
@@ -514,10 +610,10 @@ func (do *Domain) IsTranslatedN(str string, n int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
tr, ok := do.translations[str]
|
tr, ok := do.translations[str]
|
||||||
if !ok {
|
if !ok || tr == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return tr.IsTranslatedN(do.pluralForm(n))
|
return tr.IsTranslatedN(pluralForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslatedC reports whether a context string is translated
|
// IsTranslatedC reports whether a context string is translated
|
||||||
@@ -527,6 +623,14 @@ func (do *Domain) IsTranslatedC(str, ctx string) bool {
|
|||||||
|
|
||||||
// IsTranslatedNC reports whether a plural context string is translated
|
// IsTranslatedNC reports whether a plural context string is translated
|
||||||
func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
|
func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
|
||||||
|
if !do.hasContextTranslation(str, ctx) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get plural form before acquiring the translation lock so a custom
|
||||||
|
// resolver is never called while a project lock is held.
|
||||||
|
pluralForm := do.pluralForm(n)
|
||||||
|
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
@@ -534,36 +638,24 @@ func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
translations, ok := do.contextTranslations[ctx]
|
translations, ok := do.contextTranslations[ctx]
|
||||||
if !ok {
|
if !ok || translations == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
tr, ok := translations[str]
|
tr, ok := translations[str]
|
||||||
if !ok {
|
if !ok || tr == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return tr.IsTranslatedN(do.pluralForm(n))
|
return tr.IsTranslatedN(pluralForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTranslations returns a copy of every translation in the domain. It does not support contexts.
|
// GetTranslations returns a copy of every translation in the domain. It does not support contexts.
|
||||||
func (do *Domain) GetTranslations() map[string]*Translation {
|
func (do *Domain) GetTranslations() map[string]*Translation {
|
||||||
all := make(map[string]*Translation, len(do.translations))
|
|
||||||
|
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
|
all := make(map[string]*Translation, len(do.translations))
|
||||||
for msgID, trans := range do.translations {
|
for msgID, trans := range do.translations {
|
||||||
newTrans := NewTranslation()
|
all[msgID] = cloneTranslation(trans)
|
||||||
newTrans.ID = trans.ID
|
|
||||||
newTrans.PluralID = trans.PluralID
|
|
||||||
newTrans.dirty = trans.dirty
|
|
||||||
if len(trans.Refs) > 0 {
|
|
||||||
newTrans.Refs = make([]string, len(trans.Refs))
|
|
||||||
copy(newTrans.Refs, trans.Refs)
|
|
||||||
}
|
|
||||||
for k, v := range trans.Trs {
|
|
||||||
newTrans.Trs[k] = v
|
|
||||||
}
|
|
||||||
all[msgID] = newTrans
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return all
|
return all
|
||||||
@@ -571,32 +663,18 @@ func (do *Domain) GetTranslations() map[string]*Translation {
|
|||||||
|
|
||||||
// GetCtxTranslations returns a copy of every translation in the domain with context
|
// GetCtxTranslations returns a copy of every translation in the domain with context
|
||||||
func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
|
func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
|
||||||
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
|
|
||||||
|
|
||||||
do.trMutex.RLock()
|
do.trMutex.RLock()
|
||||||
defer do.trMutex.RUnlock()
|
defer do.trMutex.RUnlock()
|
||||||
|
|
||||||
|
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
|
||||||
for ctx, translations := range do.contextTranslations {
|
for ctx, translations := range do.contextTranslations {
|
||||||
for msgID, trans := range translations {
|
for msgID, trans := range translations {
|
||||||
newTrans := NewTranslation()
|
|
||||||
newTrans.ID = trans.ID
|
|
||||||
newTrans.PluralID = trans.PluralID
|
|
||||||
newTrans.dirty = trans.dirty
|
|
||||||
if len(trans.Refs) > 0 {
|
|
||||||
newTrans.Refs = make([]string, len(trans.Refs))
|
|
||||||
copy(newTrans.Refs, trans.Refs)
|
|
||||||
}
|
|
||||||
for k, v := range trans.Trs {
|
|
||||||
newTrans.Trs[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if all[ctx] == nil {
|
if all[ctx] == nil {
|
||||||
all[ctx] = make(map[string]*Translation)
|
all[ctx] = make(map[string]*Translation)
|
||||||
}
|
}
|
||||||
|
|
||||||
all[ctx][msgID] = newTrans
|
all[ctx][msgID] = cloneTranslation(trans)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return all
|
return all
|
||||||
@@ -604,10 +682,11 @@ func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
|
|||||||
|
|
||||||
// SourceReference is a struct to hold source reference information
|
// SourceReference is a struct to hold source reference information
|
||||||
type SourceReference struct {
|
type SourceReference struct {
|
||||||
path string
|
path string
|
||||||
line int
|
line int
|
||||||
context string
|
context string
|
||||||
trans *Translation
|
hasContext bool
|
||||||
|
trans *Translation
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractPathAndLine(ref string) (string, int) {
|
func extractPathAndLine(ref string) (string, int) {
|
||||||
@@ -627,6 +706,11 @@ func extractPathAndLine(ref string) (string, int) {
|
|||||||
// MarshalText implements encoding.TextMarshaler interface
|
// MarshalText implements encoding.TextMarshaler interface
|
||||||
// Assists round-trip of POT/PO content
|
// Assists round-trip of POT/PO content
|
||||||
func (do *Domain) MarshalText() ([]byte, error) {
|
func (do *Domain) MarshalText() ([]byte, error) {
|
||||||
|
do.trMutex.RLock()
|
||||||
|
do.pluralMutex.RLock()
|
||||||
|
defer do.trMutex.RUnlock()
|
||||||
|
defer do.pluralMutex.RUnlock()
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if len(do.headerComments) > 0 {
|
if len(do.headerComments) > 0 {
|
||||||
buf.WriteString(strings.Join(do.headerComments, "\n"))
|
buf.WriteString(strings.Join(do.headerComments, "\n"))
|
||||||
@@ -681,31 +765,29 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
|||||||
v := do.Headers[k]
|
v := do.Headers[k]
|
||||||
|
|
||||||
for _, value := range v {
|
for _, value := range v {
|
||||||
buf.WriteString("\n\"" + k + ": " + value + "\\n\"")
|
encoded := EscapeSpecialCharacters(k + ": " + value + "\n")
|
||||||
|
buf.WriteString("\n\"" + encoded + "\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// 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)
|
references := make([]SourceReference, 0)
|
||||||
for name, ctx := range do.contextTranslations {
|
for name, ctx := range do.contextTranslations {
|
||||||
for id, trans := range ctx {
|
for _, trans := range ctx {
|
||||||
if id == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(trans.Refs) > 0 {
|
if len(trans.Refs) > 0 {
|
||||||
path, line := extractPathAndLine(trans.Refs[0])
|
path, line := extractPathAndLine(trans.Refs[0])
|
||||||
references = append(references, SourceReference{
|
references = append(references, SourceReference{
|
||||||
path,
|
path: path,
|
||||||
line,
|
line: line,
|
||||||
name,
|
context: name,
|
||||||
trans,
|
hasContext: true,
|
||||||
|
trans: trans,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
references = append(references, SourceReference{
|
references = append(references, SourceReference{
|
||||||
"",
|
context: name,
|
||||||
0,
|
hasContext: true,
|
||||||
name,
|
trans: trans,
|
||||||
trans,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,17 +801,13 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
|||||||
if len(trans.Refs) > 0 {
|
if len(trans.Refs) > 0 {
|
||||||
path, line := extractPathAndLine(trans.Refs[0])
|
path, line := extractPathAndLine(trans.Refs[0])
|
||||||
references = append(references, SourceReference{
|
references = append(references, SourceReference{
|
||||||
path,
|
path: path,
|
||||||
line,
|
line: line,
|
||||||
"",
|
trans: trans,
|
||||||
trans,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
references = append(references, SourceReference{
|
references = append(references, SourceReference{
|
||||||
"",
|
trans: trans,
|
||||||
0,
|
|
||||||
"",
|
|
||||||
trans,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -754,6 +832,9 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
|||||||
if references[i].context > references[j].context {
|
if references[i].context > references[j].context {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if references[i].hasContext != references[j].hasContext {
|
||||||
|
return !references[i].hasContext
|
||||||
|
}
|
||||||
return references[i].trans.ID < references[j].trans.ID
|
return references[i].trans.ID < references[j].trans.ID
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -765,7 +846,7 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
|||||||
buf.WriteByte(byte('\n'))
|
buf.WriteByte(byte('\n'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ref.context == "" {
|
if !ref.hasContext {
|
||||||
buf.WriteString("\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
|
buf.WriteString("\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
|
||||||
} else {
|
} else {
|
||||||
buf.WriteString("\nmsgctxt \"" + EscapeSpecialCharacters(ref.context) + "\"\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
|
buf.WriteString("\nmsgctxt \"" + EscapeSpecialCharacters(ref.context) + "\"\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
|
||||||
@@ -774,9 +855,16 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
|||||||
if trans.PluralID == "" {
|
if trans.PluralID == "" {
|
||||||
buf.WriteString("\nmsgstr \"" + EscapeSpecialCharacters(trans.Trs[0]) + "\"")
|
buf.WriteString("\nmsgstr \"" + EscapeSpecialCharacters(trans.Trs[0]) + "\"")
|
||||||
} else {
|
} else {
|
||||||
buf.WriteString("\nmsgid_plural \"" + trans.PluralID + "\"")
|
buf.WriteString("\nmsgid_plural \"" + EscapeSpecialCharacters(trans.PluralID) + "\"")
|
||||||
for i, tr := range trans.Trs {
|
|
||||||
buf.WriteString("\nmsgstr[" + EscapeSpecialCharacters(strconv.Itoa(i)) + "] \"" + tr + "\"")
|
pluralIndexes := make([]int, 0, len(trans.Trs))
|
||||||
|
for i := range trans.Trs {
|
||||||
|
pluralIndexes = append(pluralIndexes, i)
|
||||||
|
}
|
||||||
|
sort.Ints(pluralIndexes)
|
||||||
|
|
||||||
|
for _, i := range pluralIndexes {
|
||||||
|
buf.WriteString("\nmsgstr[" + strconv.Itoa(i) + "] \"" + EscapeSpecialCharacters(trans.Trs[i]) + "\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -786,8 +874,23 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
|||||||
|
|
||||||
// EscapeSpecialCharacters escapes special characters in a string
|
// EscapeSpecialCharacters escapes special characters in a string
|
||||||
func EscapeSpecialCharacters(s string) string {
|
func EscapeSpecialCharacters(s string) string {
|
||||||
s = regexp.MustCompile(`([^\\])(")`).ReplaceAllString(s, "$1\\\"") // Escape non-escaped double quotation marks
|
var escaped strings.Builder
|
||||||
|
escaped.Grow(len(s))
|
||||||
|
|
||||||
|
for i := range len(s) {
|
||||||
|
switch s[i] {
|
||||||
|
case '\\':
|
||||||
|
escaped.WriteByte('\\')
|
||||||
|
escaped.WriteByte('\\')
|
||||||
|
case '"':
|
||||||
|
escaped.WriteByte('\\')
|
||||||
|
escaped.WriteByte('"')
|
||||||
|
default:
|
||||||
|
escaped.WriteByte(s[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s = escaped.String()
|
||||||
if strings.Count(s, "\n") == 0 {
|
if strings.Count(s, "\n") == 0 {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@@ -821,16 +924,26 @@ func EscapeSpecialCharacters(s string) string {
|
|||||||
return strings.Join(data, "\n")
|
return strings.Join(data, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (do *Domain) translatorEncodingSnapshot() *TranslatorEncoding {
|
||||||
|
do.trMutex.RLock()
|
||||||
|
do.pluralMutex.RLock()
|
||||||
|
defer do.trMutex.RUnlock()
|
||||||
|
defer do.pluralMutex.RUnlock()
|
||||||
|
|
||||||
|
return &TranslatorEncoding{
|
||||||
|
Headers: cloneHeaderMap(do.Headers),
|
||||||
|
Language: do.Language,
|
||||||
|
PluralForms: do.PluralForms,
|
||||||
|
Nplurals: do.nplurals,
|
||||||
|
Plural: do.plural,
|
||||||
|
Translations: cloneTranslations(do.translations),
|
||||||
|
Contexts: cloneContextTranslations(do.contextTranslations),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalBinary implements encoding.BinaryMarshaler interface
|
// MarshalBinary implements encoding.BinaryMarshaler interface
|
||||||
func (do *Domain) MarshalBinary() ([]byte, error) {
|
func (do *Domain) MarshalBinary() ([]byte, error) {
|
||||||
obj := new(TranslatorEncoding)
|
obj := do.translatorEncodingSnapshot()
|
||||||
obj.Headers = do.Headers
|
|
||||||
obj.Language = do.Language
|
|
||||||
obj.PluralForms = do.PluralForms
|
|
||||||
obj.Nplurals = do.nplurals
|
|
||||||
obj.Plural = do.plural
|
|
||||||
obj.Translations = do.translations
|
|
||||||
obj.Contexts = do.contextTranslations
|
|
||||||
|
|
||||||
var buff bytes.Buffer
|
var buff bytes.Buffer
|
||||||
encoder := gob.NewEncoder(&buff)
|
encoder := gob.NewEncoder(&buff)
|
||||||
@@ -845,22 +958,68 @@ func (do *Domain) UnmarshalBinary(data []byte) error {
|
|||||||
obj := new(TranslatorEncoding)
|
obj := new(TranslatorEncoding)
|
||||||
|
|
||||||
decoder := gob.NewDecoder(buff)
|
decoder := gob.NewDecoder(buff)
|
||||||
err := decoder.Decode(obj)
|
if err := decoder.Decode(obj); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
do.Headers = obj.Headers
|
headers := cloneHeaderMap(obj.Headers)
|
||||||
|
if headers == nil {
|
||||||
|
headers = make(HeaderMap)
|
||||||
|
}
|
||||||
|
translations := cloneTranslations(obj.Translations)
|
||||||
|
if translations == nil {
|
||||||
|
translations = make(map[string]*Translation)
|
||||||
|
}
|
||||||
|
contextTranslations := cloneContextTranslations(obj.Contexts)
|
||||||
|
if contextTranslations == nil {
|
||||||
|
contextTranslations = make(map[string]map[string]*Translation)
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, trans := range translations {
|
||||||
|
if trans == nil {
|
||||||
|
trans = NewTranslation()
|
||||||
|
trans.ID = id
|
||||||
|
translations[id] = trans
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for context, translationsForContext := range contextTranslations {
|
||||||
|
if translationsForContext == nil {
|
||||||
|
contextTranslations[context] = make(map[string]*Translation)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for id, trans := range translationsForContext {
|
||||||
|
if trans == nil {
|
||||||
|
trans = NewTranslation()
|
||||||
|
trans.ID = id
|
||||||
|
translationsForContext[id] = trans
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluralforms plurals.Expression
|
||||||
|
if expr, err := plurals.Compile(obj.Plural); err == nil {
|
||||||
|
pluralforms = expr
|
||||||
|
}
|
||||||
|
|
||||||
|
do.trMutex.Lock()
|
||||||
|
do.pluralMutex.Lock()
|
||||||
|
|
||||||
|
do.Headers = headers
|
||||||
do.Language = obj.Language
|
do.Language = obj.Language
|
||||||
do.PluralForms = obj.PluralForms
|
do.PluralForms = obj.PluralForms
|
||||||
do.nplurals = obj.Nplurals
|
do.nplurals = obj.Nplurals
|
||||||
do.plural = obj.Plural
|
do.plural = obj.Plural
|
||||||
do.translations = obj.Translations
|
do.pluralforms = pluralforms
|
||||||
do.contextTranslations = obj.Contexts
|
do.translations = translations
|
||||||
|
do.contextTranslations = contextTranslations
|
||||||
|
do.pluralTranslations = make(map[string]*Translation)
|
||||||
|
do.headerComments = make([]string, 0)
|
||||||
|
do.trBuffer = nil
|
||||||
|
do.ctxBuffer = ""
|
||||||
|
do.refBuffer = ""
|
||||||
|
|
||||||
if expr, err := plurals.Compile(do.plural); err == nil {
|
do.pluralMutex.Unlock()
|
||||||
do.pluralforms = expr
|
do.trMutex.Unlock()
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+173
-111
@@ -55,51 +55,59 @@ func init() {
|
|||||||
domain: "default",
|
domain: "default",
|
||||||
languages: []string{FallbackLocale},
|
languages: []string{FallbackLocale},
|
||||||
library: "/usr/local/share/locale",
|
library: "/usr/local/share/locale",
|
||||||
locales: nil,
|
locales: make([]*Locale, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register Translator types for gob encoding
|
// Register Translator types for gob encoding
|
||||||
gob.Register(TranslatorEncoding{})
|
gob.Register(TranslatorEncoding{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadLocales creates a new Locale object for every language (specified using Configure)
|
// loadLocales ensures package-level locales and their configured domain are loaded.
|
||||||
// at package level based on the configuration of global configuration .
|
|
||||||
// It is called when trying to use Get or GetD methods.
|
|
||||||
func loadLocales(rebuildCache bool) {
|
func loadLocales(rebuildCache bool) {
|
||||||
globalConfig.Lock()
|
globalConfig.Lock()
|
||||||
|
|
||||||
if globalConfig.locales == nil || rebuildCache {
|
if globalConfig.locales == nil || rebuildCache {
|
||||||
var locales []*Locale
|
locales := make([]*Locale, 0, len(globalConfig.languages))
|
||||||
for _, language := range globalConfig.languages {
|
for _, language := range globalConfig.languages {
|
||||||
locales = append(locales, NewLocale(globalConfig.library, language))
|
locales = append(locales, NewLocale(globalConfig.library, language))
|
||||||
}
|
}
|
||||||
globalConfig.locales = locales
|
globalConfig.locales = locales
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, locale := range globalConfig.locales {
|
locales := append([]*Locale(nil), globalConfig.locales...)
|
||||||
if _, ok := locale.Domains[globalConfig.domain]; !ok || rebuildCache {
|
domain := globalConfig.domain
|
||||||
locale.AddDomain(globalConfig.domain)
|
|
||||||
}
|
|
||||||
locale.SetDomain(globalConfig.domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalConfig.Unlock()
|
globalConfig.Unlock()
|
||||||
|
|
||||||
|
for _, locale := range locales {
|
||||||
|
if locale == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rebuildCache || !locale.hasDomain(domain) {
|
||||||
|
locale.AddDomain(domain)
|
||||||
|
}
|
||||||
|
locale.SetDomain(domain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDomain is the domain getter for the package configuration
|
// GetDomain is the domain getter for the package configuration
|
||||||
func GetDomain() string {
|
func GetDomain() string {
|
||||||
var dom string
|
|
||||||
globalConfig.RLock()
|
globalConfig.RLock()
|
||||||
if globalConfig.locales != nil {
|
domain := globalConfig.domain
|
||||||
// All locales have the same domain
|
var locale *Locale
|
||||||
dom = globalConfig.locales[0].GetDomain()
|
for _, candidate := range globalConfig.locales {
|
||||||
}
|
if candidate != nil {
|
||||||
if dom == "" {
|
locale = candidate
|
||||||
dom = globalConfig.domain
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
globalConfig.RUnlock()
|
globalConfig.RUnlock()
|
||||||
|
|
||||||
return dom
|
if locale != nil {
|
||||||
|
if localeDomain := locale.GetDomain(); localeDomain != "" {
|
||||||
|
return localeDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDomain sets the name for the domain to be used at package level.
|
// SetDomain sets the name for the domain to be used at package level.
|
||||||
@@ -107,8 +115,8 @@ func GetDomain() string {
|
|||||||
func SetDomain(dom string) {
|
func SetDomain(dom string) {
|
||||||
globalConfig.Lock()
|
globalConfig.Lock()
|
||||||
globalConfig.domain = dom
|
globalConfig.domain = dom
|
||||||
if globalConfig.locales != nil {
|
for _, locale := range globalConfig.locales {
|
||||||
for _, locale := range globalConfig.locales {
|
if locale != nil {
|
||||||
locale.SetDomain(dom)
|
locale.SetDomain(dom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,8 +139,10 @@ func GetLanguage() string {
|
|||||||
// GetLanguages returns all languages that have been supplied.
|
// GetLanguages returns all languages that have been supplied.
|
||||||
func GetLanguages() []string {
|
func GetLanguages() []string {
|
||||||
globalConfig.RLock()
|
globalConfig.RLock()
|
||||||
defer globalConfig.RUnlock()
|
languages := make([]string, len(globalConfig.languages))
|
||||||
return globalConfig.languages
|
copy(languages, globalConfig.languages)
|
||||||
|
globalConfig.RUnlock()
|
||||||
|
return languages
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
|
// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
|
||||||
@@ -140,7 +150,7 @@ func GetLanguages() []string {
|
|||||||
func SetLanguage(lang string) {
|
func SetLanguage(lang string) {
|
||||||
globalConfig.Lock()
|
globalConfig.Lock()
|
||||||
var languages []string
|
var languages []string
|
||||||
for _, language := range strings.Split(lang, ":") {
|
for language := range strings.SplitSeq(lang, ":") {
|
||||||
languages = append(languages, SimplifiedLocale(language))
|
languages = append(languages, SimplifiedLocale(language))
|
||||||
}
|
}
|
||||||
globalConfig.languages = languages
|
globalConfig.languages = languages
|
||||||
@@ -171,17 +181,20 @@ func SetLibrary(lib string) {
|
|||||||
// GetLocales returns the locales that have been set for the package configuration.
|
// GetLocales returns the locales that have been set for the package configuration.
|
||||||
func GetLocales() []*Locale {
|
func GetLocales() []*Locale {
|
||||||
globalConfig.RLock()
|
globalConfig.RLock()
|
||||||
defer globalConfig.RUnlock()
|
locales := make([]*Locale, len(globalConfig.locales))
|
||||||
return globalConfig.locales
|
copy(locales, globalConfig.locales)
|
||||||
|
globalConfig.RUnlock()
|
||||||
|
return locales
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStorage is the locale storage getter for the package configuration.
|
// GetStorage is the locale storage getter for the package configuration.
|
||||||
//
|
//
|
||||||
// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead.
|
// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead.
|
||||||
func GetStorage() *Locale {
|
func GetStorage() *Locale {
|
||||||
locales := GetLocales()
|
for _, locale := range GetLocales() {
|
||||||
if len(locales) > 0 {
|
if locale != nil {
|
||||||
return locales[0]
|
return locale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -190,24 +203,40 @@ func GetStorage() *Locale {
|
|||||||
// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo.
|
// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo.
|
||||||
// The library, language and domain of the first Locale will set the default global configuration.
|
// The library, language and domain of the first Locale will set the default global configuration.
|
||||||
func SetLocales(locales []*Locale) {
|
func SetLocales(locales []*Locale) {
|
||||||
|
ownedLocales := make([]*Locale, len(locales))
|
||||||
|
copy(ownedLocales, locales)
|
||||||
|
|
||||||
globalConfig.Lock()
|
globalConfig.Lock()
|
||||||
defer globalConfig.Unlock()
|
globalConfig.locales = ownedLocales
|
||||||
|
|
||||||
globalConfig.locales = locales
|
languages := make([]string, 0, len(ownedLocales))
|
||||||
globalConfig.library = locales[0].path
|
var first *Locale
|
||||||
globalConfig.domain = locales[0].defaultDomain
|
for _, locale := range ownedLocales {
|
||||||
|
if locale == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var languages []string
|
locale.RLock()
|
||||||
for _, locale := range locales {
|
if first == nil {
|
||||||
|
first = locale
|
||||||
|
globalConfig.library = locale.path
|
||||||
|
globalConfig.domain = locale.defaultDomain
|
||||||
|
}
|
||||||
languages = append(languages, locale.lang)
|
languages = append(languages, locale.lang)
|
||||||
|
locale.RUnlock()
|
||||||
}
|
}
|
||||||
globalConfig.languages = languages
|
globalConfig.languages = languages
|
||||||
|
globalConfig.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetStorage allows overriding the global Locale object with one built manually with NewLocale().
|
// SetStorage allows overriding the global Locale object with one built manually with NewLocale().
|
||||||
//
|
//
|
||||||
// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead.
|
// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead.
|
||||||
func SetStorage(locale *Locale) {
|
func SetStorage(locale *Locale) {
|
||||||
|
if locale == nil {
|
||||||
|
SetLocales(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
SetLocales([]*Locale{locale})
|
SetLocales([]*Locale{locale})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +248,7 @@ func Configure(lib, lang, dom string) {
|
|||||||
globalConfig.Lock()
|
globalConfig.Lock()
|
||||||
globalConfig.library = lib
|
globalConfig.library = lib
|
||||||
var languages []string
|
var languages []string
|
||||||
for _, language := range strings.Split(lang, ":") {
|
for language := range strings.SplitSeq(lang, ":") {
|
||||||
languages = append(languages, SimplifiedLocale(language))
|
languages = append(languages, SimplifiedLocale(language))
|
||||||
}
|
}
|
||||||
globalConfig.languages = languages
|
globalConfig.languages = languages
|
||||||
@@ -230,114 +259,137 @@ func Configure(lib, lang, dom string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get uses the default domain globally set to return the corresponding Translation of a given string.
|
// Get uses the default domain globally set to return the corresponding Translation of a given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func Get(str string, vars ...interface{}) string {
|
func Get(str string, vars ...any) string {
|
||||||
return GetD(GetDomain(), str, vars...)
|
return GetD(GetDomain(), str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetN retrieves the (N)th plural form of Translation for the given string in the default domain.
|
// GetN retrieves the (N)th plural form of Translation for the given string in the default domain.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func GetN(str, plural string, n int, vars ...interface{}) string {
|
func GetN(str, plural string, n int, vars ...any) string {
|
||||||
return GetND(GetDomain(), str, plural, n, vars...)
|
return GetND(GetDomain(), str, plural, n, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetD returns the corresponding Translation in the given domain for a given string.
|
// GetD returns the corresponding Translation in the given domain for a given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func GetD(dom, str string, vars ...interface{}) string {
|
func GetD(dom, str string, vars ...any) string {
|
||||||
// Try to load default package Locales
|
// Try to load default package Locales
|
||||||
loadLocales(false)
|
loadLocales(false)
|
||||||
|
|
||||||
globalConfig.RLock()
|
locales := GetLocales()
|
||||||
defer globalConfig.RUnlock()
|
var fallback *Locale
|
||||||
|
for _, locale := range locales {
|
||||||
var tr string
|
if locale == nil {
|
||||||
for i, locale := range globalConfig.locales {
|
|
||||||
if _, ok := locale.Domains[dom]; !ok {
|
|
||||||
locale.AddDomain(dom)
|
|
||||||
}
|
|
||||||
if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tr = locale.GetD(dom, str, vars...)
|
fallback = locale
|
||||||
break
|
if !locale.hasDomain(dom) {
|
||||||
|
locale.AddDomain(dom)
|
||||||
|
}
|
||||||
|
if locale.IsTranslatedD(dom, str) {
|
||||||
|
return locale.GetD(dom, str, vars...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return tr
|
if fallback != nil {
|
||||||
|
return fallback.GetD(dom, str, vars...)
|
||||||
|
}
|
||||||
|
return FormatString(str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetND retrieves the (N)th plural form of Translation in the given domain for a given string.
|
// GetND retrieves the (N)th plural form of Translation in the given domain for a given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
|
func GetND(dom, str, plural string, n int, vars ...any) string {
|
||||||
// Try to load default package Locales
|
// Try to load default package Locales
|
||||||
loadLocales(false)
|
loadLocales(false)
|
||||||
|
|
||||||
globalConfig.RLock()
|
locales := GetLocales()
|
||||||
defer globalConfig.RUnlock()
|
var fallback *Locale
|
||||||
|
for _, locale := range locales {
|
||||||
var tr string
|
if locale == nil {
|
||||||
for i, locale := range globalConfig.locales {
|
|
||||||
if _, ok := locale.Domains[dom]; !ok {
|
|
||||||
locale.AddDomain(dom)
|
|
||||||
}
|
|
||||||
if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tr = locale.GetND(dom, str, plural, n, vars...)
|
fallback = locale
|
||||||
break
|
if !locale.hasDomain(dom) {
|
||||||
|
locale.AddDomain(dom)
|
||||||
|
}
|
||||||
|
if locale.IsTranslatedND(dom, str, n) {
|
||||||
|
return locale.GetND(dom, str, plural, n, vars...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return tr
|
if fallback != nil {
|
||||||
|
return fallback.GetND(dom, str, plural, n, vars...)
|
||||||
|
}
|
||||||
|
if n == 1 {
|
||||||
|
return FormatString(str, vars...)
|
||||||
|
}
|
||||||
|
return FormatString(plural, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetC uses the default domain globally set to return the corresponding Translation of the given string in the given context.
|
// GetC uses the default domain globally set to return the corresponding Translation of the given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func GetC(str, ctx string, vars ...interface{}) string {
|
func GetC(str, ctx string, vars ...any) string {
|
||||||
return GetDC(GetDomain(), str, ctx, vars...)
|
return GetDC(GetDomain(), str, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain.
|
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
func GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||||
return GetNDC(GetDomain(), str, plural, n, ctx, vars...)
|
return GetNDC(GetDomain(), str, plural, n, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
|
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func GetDC(dom, str, ctx string, vars ...interface{}) string {
|
func GetDC(dom, str, ctx string, vars ...any) string {
|
||||||
// Try to load default package Locales
|
// Try to load default package Locales
|
||||||
loadLocales(false)
|
loadLocales(false)
|
||||||
|
|
||||||
globalConfig.RLock()
|
locales := GetLocales()
|
||||||
defer globalConfig.RUnlock()
|
var fallback *Locale
|
||||||
|
for _, locale := range locales {
|
||||||
var tr string
|
if locale == nil {
|
||||||
for i, locale := range globalConfig.locales {
|
|
||||||
if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tr = locale.GetDC(dom, str, ctx, vars...)
|
fallback = locale
|
||||||
break
|
if !locale.hasDomain(dom) {
|
||||||
|
locale.AddDomain(dom)
|
||||||
|
}
|
||||||
|
if locale.IsTranslatedDC(dom, str, ctx) {
|
||||||
|
return locale.GetDC(dom, str, ctx, vars...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return tr
|
if fallback != nil {
|
||||||
|
return fallback.GetDC(dom, str, ctx, vars...)
|
||||||
|
}
|
||||||
|
return FormatString(str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string.
|
// GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
|
func GetNDC(dom, str, plural string, n int, ctx string, vars ...any) string {
|
||||||
// Try to load default package Locales
|
// Try to load default package Locales
|
||||||
loadLocales(false)
|
loadLocales(false)
|
||||||
|
|
||||||
// Return Translation
|
locales := GetLocales()
|
||||||
globalConfig.RLock()
|
var fallback *Locale
|
||||||
defer globalConfig.RUnlock()
|
for _, locale := range locales {
|
||||||
|
if locale == nil {
|
||||||
var tr string
|
|
||||||
for i, locale := range globalConfig.locales {
|
|
||||||
if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tr = locale.GetNDC(dom, str, plural, n, ctx, vars...)
|
fallback = locale
|
||||||
break
|
if !locale.hasDomain(dom) {
|
||||||
|
locale.AddDomain(dom)
|
||||||
|
}
|
||||||
|
if locale.IsTranslatedNDC(dom, str, n, ctx) {
|
||||||
|
return locale.GetNDC(dom, str, plural, n, ctx, vars...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return tr
|
if fallback != nil {
|
||||||
|
return fallback.GetNDC(dom, str, plural, n, ctx, vars...)
|
||||||
|
}
|
||||||
|
if n == 1 {
|
||||||
|
return FormatString(str, vars...)
|
||||||
|
}
|
||||||
|
return FormatString(plural, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslated reports whether a string is translated in given languages.
|
// IsTranslated reports whether a string is translated in given languages.
|
||||||
@@ -366,18 +418,23 @@ func IsTranslatedND(dom, str string, n int, langs ...string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadLocales(false)
|
loadLocales(false)
|
||||||
|
locales := GetLocales()
|
||||||
globalConfig.RLock()
|
|
||||||
defer globalConfig.RUnlock()
|
|
||||||
|
|
||||||
for _, lang := range langs {
|
for _, lang := range langs {
|
||||||
lang = SimplifiedLocale(lang)
|
lang = SimplifiedLocale(lang)
|
||||||
|
|
||||||
for _, supportedLocale := range globalConfig.locales {
|
for _, supportedLocale := range locales {
|
||||||
if lang != supportedLocale.GetActualLanguage(dom) {
|
if supportedLocale == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return supportedLocale.IsTranslatedND(dom, str, n)
|
if lang != supportedLocale.GetLanguage() && lang != supportedLocale.GetActualLanguage(dom) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !supportedLocale.hasDomain(dom) {
|
||||||
|
supportedLocale.AddDomain(dom)
|
||||||
|
}
|
||||||
|
if supportedLocale.IsTranslatedND(dom, str, n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -398,7 +455,7 @@ func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool {
|
|||||||
// IsTranslatedDC reports whether a domain context string is translated in given languages.
|
// IsTranslatedDC reports whether a domain context string is translated in given languages.
|
||||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||||
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
|
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
|
||||||
return IsTranslatedNDC(dom, str, 0, ctx, langs...)
|
return IsTranslatedNDC(dom, str, 1, ctx, langs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
|
// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
|
||||||
@@ -409,18 +466,23 @@ func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadLocales(false)
|
loadLocales(false)
|
||||||
|
locales := GetLocales()
|
||||||
globalConfig.RLock()
|
|
||||||
defer globalConfig.RUnlock()
|
|
||||||
|
|
||||||
for _, lang := range langs {
|
for _, lang := range langs {
|
||||||
lang = SimplifiedLocale(lang)
|
lang = SimplifiedLocale(lang)
|
||||||
|
|
||||||
for _, locale := range globalConfig.locales {
|
for _, supportedLocale := range locales {
|
||||||
if lang != locale.GetActualLanguage(dom) {
|
if supportedLocale == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return locale.IsTranslatedNDC(dom, str, n, ctx)
|
if lang != supportedLocale.GetLanguage() && lang != supportedLocale.GetActualLanguage(dom) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !supportedLocale.hasDomain(dom) {
|
||||||
|
supportedLocale.AddDomain(dom)
|
||||||
|
}
|
||||||
|
if supportedLocale.IsTranslatedNDC(dom, str, n, ctx) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
+31
-27
@@ -29,16 +29,21 @@ func SimplifiedLocale(lang string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FormatString applies text formatting only when needed to parse variables.
|
// FormatString applies text formatting only when needed to parse variables.
|
||||||
func FormatString(str string, vars ...interface{}) string {
|
func FormatString(str string, vars ...any) string {
|
||||||
if len(vars) > 0 {
|
if len(vars) > 0 {
|
||||||
return fmt.Sprintf(str, vars...)
|
return FormatStringWithArgs(str, vars)
|
||||||
}
|
}
|
||||||
|
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormatStringWithArgs applies text formatting as a proxy for fmt.Sprintf.
|
||||||
|
func FormatStringWithArgs(str string, vars []any) string {
|
||||||
|
return fmt.Sprintf(str, vars...)
|
||||||
|
}
|
||||||
|
|
||||||
// Appendf applies text formatting only when needed to parse variables.
|
// Appendf applies text formatting only when needed to parse variables.
|
||||||
func Appendf(b []byte, str string, vars ...interface{}) []byte {
|
func Appendf(b []byte, str string, vars ...any) []byte {
|
||||||
if len(vars) > 0 {
|
if len(vars) > 0 {
|
||||||
return fmt.Appendf(b, str, vars...)
|
return fmt.Appendf(b, str, vars...)
|
||||||
}
|
}
|
||||||
@@ -47,50 +52,49 @@ func Appendf(b []byte, str string, vars ...interface{}) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NPrintf support named format
|
// NPrintf support named format
|
||||||
// NPrintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
|
// NPrintf("%(name)s is Type %(type)s", map[string]any{"name": "Gotext", "type": "struct"})
|
||||||
func NPrintf(format string, params map[string]interface{}) {
|
func NPrintf(format string, params map[string]any) {
|
||||||
f, p := parseSprintf(format, params)
|
f, p := parseSprintf(format, params)
|
||||||
fmt.Printf(f, p...)
|
fmt.Printf(f, p...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sprintf support named format
|
// Sprintf support named format
|
||||||
//
|
//
|
||||||
// Sprintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
|
// Sprintf("%(name)s is Type %(type)s", map[string]any{"name": "Gotext", "type": "struct"})
|
||||||
func Sprintf(format string, params map[string]interface{}) string {
|
func Sprintf(format string, params map[string]any) string {
|
||||||
f, p := parseSprintf(format, params)
|
f, p := parseSprintf(format, params)
|
||||||
return fmt.Sprintf(f, p...)
|
return fmt.Sprintf(f, p...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSprintf(format string, params map[string]interface{}) (string, []interface{}) {
|
func parseSprintf(format string, params map[string]any) (string, []any) {
|
||||||
f, n := reformatSprintf(format)
|
f, n := reformatSprintf(format)
|
||||||
var p []interface{}
|
var p []any
|
||||||
for _, v := range n {
|
if len(n) > 0 {
|
||||||
p = append(p, params[v])
|
p = make([]any, len(n))
|
||||||
|
for i, v := range n {
|
||||||
|
p[i] = params[v]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return f, p
|
return f, p
|
||||||
}
|
}
|
||||||
|
|
||||||
func reformatSprintf(f string) (string, []string) {
|
func reformatSprintf(f string) (string, []string) {
|
||||||
m := re.FindAllStringSubmatch(f, -1)
|
matches := re.FindAllStringSubmatchIndex(f, -1)
|
||||||
i := re.FindAllStringSubmatchIndex(f, -1)
|
ord := make([]string, 0, len(matches))
|
||||||
|
pair := make([]int, 1, 1+2*len(matches))
|
||||||
|
pair[0] = 0
|
||||||
|
|
||||||
ord := []string{}
|
for _, match := range matches {
|
||||||
for _, v := range m {
|
ord = append(ord, f[match[2]:match[3]])
|
||||||
ord = append(ord, v[1])
|
pair = append(pair, match[2]-1, match[3]+1)
|
||||||
}
|
|
||||||
|
|
||||||
pair := []int{0}
|
|
||||||
for _, v := range i {
|
|
||||||
pair = append(pair, v[2]-1)
|
|
||||||
pair = append(pair, v[3]+1)
|
|
||||||
}
|
}
|
||||||
pair = append(pair, len(f))
|
pair = append(pair, len(f))
|
||||||
plen := len(pair)
|
|
||||||
|
|
||||||
out := ""
|
var out strings.Builder
|
||||||
for n := 0; n < plen; n += 2 {
|
out.Grow(len(f))
|
||||||
out += f[pair[n]:pair[n+1]]
|
for n := 0; n < len(pair); n += 2 {
|
||||||
|
out.WriteString(f[pair[n]:pair[n+1]])
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, ord
|
return out.String(), ord
|
||||||
}
|
}
|
||||||
|
|||||||
+214
-163
@@ -9,6 +9,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -22,8 +23,6 @@ multiple languages at the same time by working with this object.
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/leonelquinteros/gotext"
|
"github.com/leonelquinteros/gotext"
|
||||||
)
|
)
|
||||||
@@ -90,95 +89,114 @@ func NewLocaleFSWithPath(l string, filesystem fs.FS, p string) *Locale {
|
|||||||
return loc
|
return loc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Locale) findExt(dom, ext string) string {
|
func (l *Locale) snapshotPathLanguageFS() (string, string, fs.FS) {
|
||||||
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
|
if l == nil {
|
||||||
if l.fileExists(filename) {
|
return "", "", nil
|
||||||
return filename
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(l.lang) > 2 {
|
l.RLock()
|
||||||
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
|
p := l.path
|
||||||
if l.fileExists(filename) {
|
lang := l.lang
|
||||||
return filename
|
filesystem := l.fs
|
||||||
}
|
l.RUnlock()
|
||||||
}
|
return p, lang, filesystem
|
||||||
|
|
||||||
filename = path.Join(l.path, l.lang, dom+"."+ext)
|
|
||||||
if l.fileExists(filename) {
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(l.lang) > 2 {
|
|
||||||
filename = path.Join(l.path, l.lang[:2], dom+"."+ext)
|
|
||||||
if l.fileExists(filename) {
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActualLanguage inspects the filesystem and decides whether to strip
|
func fileExistsIn(filesystem fs.FS, filename string) bool {
|
||||||
// a CC part of the ll_CC locale string.
|
if filesystem != nil {
|
||||||
func (l *Locale) GetActualLanguage(dom string) string {
|
_, err := fs.Stat(filesystem, filename)
|
||||||
extensions := []string{"mo", "po"}
|
|
||||||
var fp string
|
|
||||||
for _, ext := range extensions {
|
|
||||||
// 'll' (or 'll_CC') exists, and it was specified as-is
|
|
||||||
fp = path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
|
|
||||||
if l.fileExists(fp) {
|
|
||||||
return l.lang
|
|
||||||
}
|
|
||||||
// 'll' exists, but 'll_CC' was specified
|
|
||||||
if len(l.lang) > 2 {
|
|
||||||
fp = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
|
|
||||||
if l.fileExists(fp) {
|
|
||||||
return l.lang[:2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
|
|
||||||
fp = path.Join(l.path, l.lang, dom+"."+ext)
|
|
||||||
if l.fileExists(fp) {
|
|
||||||
return l.lang
|
|
||||||
}
|
|
||||||
// 'll' exists outside of LC_category, but 'll_CC' was specified
|
|
||||||
if len(l.lang) > 2 {
|
|
||||||
fp = path.Join(l.path, l.lang[:2], dom+"."+ext)
|
|
||||||
if l.fileExists(fp) {
|
|
||||||
return l.lang[:2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Locale) fileExists(filename string) bool {
|
|
||||||
if l.fs != nil {
|
|
||||||
_, err := fs.Stat(l.fs, filename)
|
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
_, err := os.Stat(filename)
|
_, err := os.Stat(filename)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Locale) findExtWithFS(dom, ext string) (fs.FS, string) {
|
||||||
|
p, lang, filesystem := l.snapshotPathLanguageFS()
|
||||||
|
|
||||||
|
filename := path.Join(p, lang, "LC_MESSAGES", dom+"."+ext)
|
||||||
|
if fileExistsIn(filesystem, filename) {
|
||||||
|
return filesystem, filename
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lang) > 2 {
|
||||||
|
filename = path.Join(p, lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||||
|
if fileExistsIn(filesystem, filename) {
|
||||||
|
return filesystem, filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = path.Join(p, lang, dom+"."+ext)
|
||||||
|
if fileExistsIn(filesystem, filename) {
|
||||||
|
return filesystem, filename
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lang) > 2 {
|
||||||
|
filename = path.Join(p, lang[:2], dom+"."+ext)
|
||||||
|
if fileExistsIn(filesystem, filename) {
|
||||||
|
return filesystem, filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filesystem, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActualLanguage inspects the filesystem and decides whether to strip
|
||||||
|
// a CC part of the ll_CC locale string.
|
||||||
|
func (l *Locale) GetActualLanguage(dom string) string {
|
||||||
|
p, lang, filesystem := l.snapshotPathLanguageFS()
|
||||||
|
extensions := []string{"po", "mo"}
|
||||||
|
var fp string
|
||||||
|
for _, ext := range extensions {
|
||||||
|
// 'll' (or 'll_CC') exists, and it was specified as-is
|
||||||
|
fp = path.Join(p, lang, "LC_MESSAGES", dom+"."+ext)
|
||||||
|
if fileExistsIn(filesystem, fp) {
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
// 'll' exists, but 'll_CC' was specified
|
||||||
|
if len(lang) > 2 {
|
||||||
|
fp = path.Join(p, lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||||
|
if fileExistsIn(filesystem, fp) {
|
||||||
|
return lang[:2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
|
||||||
|
fp = path.Join(p, lang, dom+"."+ext)
|
||||||
|
if fileExistsIn(filesystem, fp) {
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
// 'll' exists outside of LC_category, but 'll_CC' was specified
|
||||||
|
if len(lang) > 2 {
|
||||||
|
fp = path.Join(p, lang[:2], dom+"."+ext)
|
||||||
|
if fileExistsIn(filesystem, fp) {
|
||||||
|
return lang[:2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// AddDomain creates a new domain for a given locale object and initializes the Po object.
|
// AddDomain creates a new domain for a given locale object and initializes the Po object.
|
||||||
// If the domain exists, it gets reloaded.
|
// If the domain exists, it gets reloaded.
|
||||||
func (l *Locale) AddDomain(dom string) {
|
func (l *Locale) AddDomain(dom string) {
|
||||||
|
if l == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var poObj Translator
|
var poObj Translator
|
||||||
|
|
||||||
file := l.findExt(dom, "po")
|
filesystem, file := l.findExtWithFS(dom, "po")
|
||||||
if file != "" {
|
if file != "" {
|
||||||
poObj = NewPoFS(l.fs)
|
poObj = NewPoFS(filesystem)
|
||||||
// Parse file.
|
// Parse file.
|
||||||
poObj.ParseFile(file)
|
poObj.ParseFile(file)
|
||||||
} else {
|
} else {
|
||||||
file = l.findExt(dom, "mo")
|
filesystem, file = l.findExtWithFS(dom, "mo")
|
||||||
if file != "" {
|
if file != "" {
|
||||||
poObj = NewMoFS(l.fs)
|
poObj = NewMoFS(filesystem)
|
||||||
// Parse file.
|
// Parse file.
|
||||||
poObj.ParseFile(file)
|
poObj.ParseFile(file)
|
||||||
} else {
|
} else {
|
||||||
// fallback return if no file found with
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,9 +230,34 @@ func (l *Locale) AddTranslator(dom string, tr Translator) {
|
|||||||
|
|
||||||
l.Unlock()
|
l.Unlock()
|
||||||
}
|
}
|
||||||
|
func (l *Locale) translatorFor(dom string) Translator {
|
||||||
|
if l == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.RLock()
|
||||||
|
translator := l.Domains[dom]
|
||||||
|
l.RUnlock()
|
||||||
|
return translator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) hasDomain(dom string) bool {
|
||||||
|
if l == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
l.RLock()
|
||||||
|
_, ok := l.Domains[dom]
|
||||||
|
l.RUnlock()
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// GetDomain is the domain getter for Locale configuration
|
// GetDomain is the domain getter for Locale configuration
|
||||||
func (l *Locale) GetDomain() string {
|
func (l *Locale) GetDomain() string {
|
||||||
|
if l == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
l.RLock()
|
l.RLock()
|
||||||
dom := l.defaultDomain
|
dom := l.defaultDomain
|
||||||
l.RUnlock()
|
l.RUnlock()
|
||||||
@@ -223,6 +266,10 @@ func (l *Locale) GetDomain() string {
|
|||||||
|
|
||||||
// SetDomain sets the name for the domain to be used.
|
// SetDomain sets the name for the domain to be used.
|
||||||
func (l *Locale) SetDomain(dom string) {
|
func (l *Locale) SetDomain(dom string) {
|
||||||
|
if l == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
l.Lock()
|
l.Lock()
|
||||||
l.defaultDomain = dom
|
l.defaultDomain = dom
|
||||||
l.Unlock()
|
l.Unlock()
|
||||||
@@ -230,6 +277,10 @@ func (l *Locale) SetDomain(dom string) {
|
|||||||
|
|
||||||
// GetLanguage is the lang getter for Locale configuration
|
// GetLanguage is the lang getter for Locale configuration
|
||||||
func (l *Locale) GetLanguage() string {
|
func (l *Locale) GetLanguage() string {
|
||||||
|
if l == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
l.RLock()
|
l.RLock()
|
||||||
lang := l.lang
|
lang := l.lang
|
||||||
l.RUnlock()
|
l.RUnlock()
|
||||||
@@ -237,51 +288,37 @@ func (l *Locale) GetLanguage() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get uses a domain "default" to return the corresponding Translation of a given string.
|
// Get uses a domain "default" to return the corresponding Translation of a given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (l *Locale) Get(str string, vars ...interface{}) string {
|
func (l *Locale) Get(str string, vars ...any) string {
|
||||||
return l.GetD(l.GetDomain(), str, vars...)
|
return l.GetD(l.GetDomain(), str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain.
|
// GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string {
|
func (l *Locale) GetN(str, plural string, n int, vars ...any) string {
|
||||||
return l.GetND(l.GetDomain(), str, plural, n, vars...)
|
return l.GetND(l.GetDomain(), str, plural, n, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetD returns the corresponding Translation in the given domain for the given string.
|
// GetD returns the corresponding Translation in the given domain for the given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
|
func (l *Locale) GetD(dom, str string, vars ...any) string {
|
||||||
// Sync read
|
translator := l.translatorFor(dom)
|
||||||
l.RLock()
|
if translator != nil {
|
||||||
defer l.RUnlock()
|
return translator.Get(str, vars...)
|
||||||
|
|
||||||
if l.Domains != nil {
|
|
||||||
if _, ok := l.Domains[dom]; ok {
|
|
||||||
if l.Domains[dom] != nil {
|
|
||||||
return l.Domains[dom].Get(str, vars...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return FormatString(str, vars...)
|
return FormatString(str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetND retrieves the (N)th plural form of Translation in the given domain for the given string.
|
// GetND retrieves the (N)th plural form of Translation in the given domain for the given string.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
|
func (l *Locale) GetND(dom, str, plural string, n int, vars ...any) string {
|
||||||
// Sync read
|
translator := l.translatorFor(dom)
|
||||||
l.RLock()
|
if translator != nil {
|
||||||
defer l.RUnlock()
|
return translator.GetN(str, plural, n, vars...)
|
||||||
|
|
||||||
if l.Domains != nil {
|
|
||||||
if _, ok := l.Domains[dom]; ok {
|
|
||||||
if l.Domains[dom] != nil {
|
|
||||||
return l.Domains[dom].GetN(str, plural, n, vars...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use western default rule (plural > 1) to handle missing domain default result.
|
// Use western default rule (n != 1) to handle missing domain default result.
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
return FormatString(str, vars...)
|
return FormatString(str, vars...)
|
||||||
}
|
}
|
||||||
@@ -289,51 +326,37 @@ func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetC uses a domain "default" to return the corresponding Translation of the given string in the given context.
|
// GetC uses a domain "default" to return the corresponding Translation of the given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
|
func (l *Locale) GetC(str, ctx string, vars ...any) string {
|
||||||
return l.GetDC(l.GetDomain(), str, ctx, vars...)
|
return l.GetDC(l.GetDomain(), str, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain.
|
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||||
return l.GetNDC(l.GetDomain(), str, plural, n, ctx, vars...)
|
return l.GetNDC(l.GetDomain(), str, plural, n, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
|
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
|
func (l *Locale) GetDC(dom, str, ctx string, vars ...any) string {
|
||||||
// Sync read
|
translator := l.translatorFor(dom)
|
||||||
l.RLock()
|
if translator != nil {
|
||||||
defer l.RUnlock()
|
return translator.GetC(str, ctx, vars...)
|
||||||
|
|
||||||
if l.Domains != nil {
|
|
||||||
if _, ok := l.Domains[dom]; ok {
|
|
||||||
if l.Domains[dom] != nil {
|
|
||||||
return l.Domains[dom].GetC(str, ctx, vars...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return FormatString(str, vars...)
|
return FormatString(str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNDC retrieves the (N)th plural form of Translation in the given domain for the given string in the given context.
|
// GetNDC retrieves the (N)th plural form of Translation in the given domain for the given string in the given context.
|
||||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
|
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...any) string {
|
||||||
// Sync read
|
translator := l.translatorFor(dom)
|
||||||
l.RLock()
|
if translator != nil {
|
||||||
defer l.RUnlock()
|
return translator.GetNC(str, plural, n, ctx, vars...)
|
||||||
|
|
||||||
if l.Domains != nil {
|
|
||||||
if _, ok := l.Domains[dom]; ok {
|
|
||||||
if l.Domains[dom] != nil {
|
|
||||||
return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use western default rule (plural > 1) to handle missing domain default result.
|
// Use western default rule (n != 1) to handle missing domain default result.
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
return FormatString(str, vars...)
|
return FormatString(str, vars...)
|
||||||
}
|
}
|
||||||
@@ -343,13 +366,26 @@ func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...inte
|
|||||||
// GetTranslations returns a copy of all translations in all domains of this locale. It does not support contexts.
|
// GetTranslations returns a copy of all translations in all domains of this locale. It does not support contexts.
|
||||||
func (l *Locale) GetTranslations() map[string]*Translation {
|
func (l *Locale) GetTranslations() map[string]*Translation {
|
||||||
all := make(map[string]*Translation)
|
all := make(map[string]*Translation)
|
||||||
|
if l == nil {
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
l.RLock()
|
l.RLock()
|
||||||
defer l.RUnlock()
|
translators := make([]Translator, 0, len(l.Domains))
|
||||||
for _, translator := range l.Domains {
|
for _, translator := range l.Domains {
|
||||||
for msgID, trans := range translator.GetDomain().GetTranslations() {
|
translators = append(translators, translator)
|
||||||
all[msgID] = trans
|
}
|
||||||
|
l.RUnlock()
|
||||||
|
|
||||||
|
for _, translator := range translators {
|
||||||
|
if translator == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
domain := translator.GetDomain()
|
||||||
|
if domain == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
maps.Copy(all, domain.GetTranslations())
|
||||||
}
|
}
|
||||||
|
|
||||||
return all
|
return all
|
||||||
@@ -357,7 +393,7 @@ func (l *Locale) GetTranslations() map[string]*Translation {
|
|||||||
|
|
||||||
// IsTranslated reports whether a string is translated
|
// IsTranslated reports whether a string is translated
|
||||||
func (l *Locale) IsTranslated(str string) bool {
|
func (l *Locale) IsTranslated(str string) bool {
|
||||||
return l.IsTranslatedND(l.GetDomain(), str, 0)
|
return l.IsTranslatedND(l.GetDomain(), str, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslatedN reports whether a plural string is translated
|
// IsTranslatedN reports whether a plural string is translated
|
||||||
@@ -367,27 +403,25 @@ func (l *Locale) IsTranslatedN(str string, n int) bool {
|
|||||||
|
|
||||||
// IsTranslatedD reports whether a domain string is translated
|
// IsTranslatedD reports whether a domain string is translated
|
||||||
func (l *Locale) IsTranslatedD(dom, str string) bool {
|
func (l *Locale) IsTranslatedD(dom, str string) bool {
|
||||||
return l.IsTranslatedND(dom, str, 0)
|
return l.IsTranslatedND(dom, str, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslatedND reports whether a plural domain string is translated
|
// IsTranslatedND reports whether a plural domain string is translated
|
||||||
func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
|
func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
|
||||||
l.RLock()
|
translator := l.translatorFor(dom)
|
||||||
defer l.RUnlock()
|
if translator == nil {
|
||||||
|
|
||||||
if l.Domains == nil {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
translator, ok := l.Domains[dom]
|
domain := translator.GetDomain()
|
||||||
if !ok {
|
if domain == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return translator.GetDomain().IsTranslatedN(str, n)
|
return domain.IsTranslatedN(str, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslatedC reports whether a context string is translated
|
// IsTranslatedC reports whether a context string is translated
|
||||||
func (l *Locale) IsTranslatedC(str, ctx string) bool {
|
func (l *Locale) IsTranslatedC(str, ctx string) bool {
|
||||||
return l.IsTranslatedNDC(l.GetDomain(), str, 0, ctx)
|
return l.IsTranslatedNDC(l.GetDomain(), str, 1, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslatedNC reports whether a plural context string is translated
|
// IsTranslatedNC reports whether a plural context string is translated
|
||||||
@@ -397,22 +431,20 @@ func (l *Locale) IsTranslatedNC(str string, n int, ctx string) bool {
|
|||||||
|
|
||||||
// IsTranslatedDC reports whether a domain context string is translated
|
// IsTranslatedDC reports whether a domain context string is translated
|
||||||
func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
|
func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
|
||||||
return l.IsTranslatedNDC(dom, str, 0, ctx)
|
return l.IsTranslatedNDC(dom, str, 1, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslatedNDC reports whether a plural domain context string is translated
|
// IsTranslatedNDC reports whether a plural domain context string is translated
|
||||||
func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
|
func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
|
||||||
l.RLock()
|
translator := l.translatorFor(dom)
|
||||||
defer l.RUnlock()
|
if translator == nil {
|
||||||
|
|
||||||
if l.Domains == nil {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
translator, ok := l.Domains[dom]
|
domain := translator.GetDomain()
|
||||||
if !ok {
|
if domain == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return translator.GetDomain().IsTranslatedNC(str, n, ctx)
|
return domain.IsTranslatedNC(str, n, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
|
// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
|
||||||
@@ -425,18 +457,35 @@ type LocaleEncoding struct {
|
|||||||
|
|
||||||
// MarshalBinary implements encoding BinaryMarshaler interface
|
// MarshalBinary implements encoding BinaryMarshaler interface
|
||||||
func (l *Locale) MarshalBinary() ([]byte, error) {
|
func (l *Locale) MarshalBinary() ([]byte, error) {
|
||||||
obj := new(LocaleEncoding)
|
var localePath, lang, defaultDomain string
|
||||||
obj.DefaultDomain = l.defaultDomain
|
domains := make(map[string]Translator)
|
||||||
obj.Domains = make(map[string][]byte)
|
if l != nil {
|
||||||
for k, v := range l.Domains {
|
l.RLock()
|
||||||
var err error
|
localePath = l.path
|
||||||
obj.Domains[k], err = v.MarshalBinary()
|
lang = l.lang
|
||||||
|
defaultDomain = l.defaultDomain
|
||||||
|
domains = make(map[string]Translator, len(l.Domains))
|
||||||
|
for name, translator := range l.Domains {
|
||||||
|
if translator != nil {
|
||||||
|
domains[name] = translator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := &LocaleEncoding{
|
||||||
|
Path: localePath,
|
||||||
|
Lang: lang,
|
||||||
|
Domains: make(map[string][]byte, len(domains)),
|
||||||
|
DefaultDomain: defaultDomain,
|
||||||
|
}
|
||||||
|
for name, translator := range domains {
|
||||||
|
data, err := translator.MarshalBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
obj.Domains[name] = data
|
||||||
}
|
}
|
||||||
obj.Lang = l.lang
|
|
||||||
obj.Path = l.path
|
|
||||||
|
|
||||||
var buff bytes.Buffer
|
var buff bytes.Buffer
|
||||||
encoder := gob.NewEncoder(&buff)
|
encoder := gob.NewEncoder(&buff)
|
||||||
@@ -445,7 +494,7 @@ func (l *Locale) MarshalBinary() ([]byte, error) {
|
|||||||
return buff.Bytes(), err
|
return buff.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalBinary implements encoding BinaryUnmarshaler interface
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
|
||||||
func (l *Locale) UnmarshalBinary(data []byte) error {
|
func (l *Locale) UnmarshalBinary(data []byte) error {
|
||||||
buff := bytes.NewBuffer(data)
|
buff := bytes.NewBuffer(data)
|
||||||
obj := new(LocaleEncoding)
|
obj := new(LocaleEncoding)
|
||||||
@@ -456,12 +505,7 @@ func (l *Locale) UnmarshalBinary(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.defaultDomain = obj.DefaultDomain
|
domains := make(map[string]Translator, len(obj.Domains))
|
||||||
l.lang = obj.Lang
|
|
||||||
l.path = obj.Path
|
|
||||||
|
|
||||||
// Decode Domains
|
|
||||||
l.Domains = make(map[string]Translator)
|
|
||||||
for k, v := range obj.Domains {
|
for k, v := range obj.Domains {
|
||||||
var tr TranslatorEncoding
|
var tr TranslatorEncoding
|
||||||
buff := bytes.NewBuffer(v)
|
buff := bytes.NewBuffer(v)
|
||||||
@@ -471,8 +515,15 @@ func (l *Locale) UnmarshalBinary(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Domains[k] = tr.GetTranslator()
|
domains[k] = tr.GetTranslator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
l.defaultDomain = obj.DefaultDomain
|
||||||
|
l.lang = obj.Lang
|
||||||
|
l.path = obj.Path
|
||||||
|
l.Domains = domains
|
||||||
|
l.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
site_name: gotext
|
||||||
|
site_description: GNU gettext utilities for Go
|
||||||
|
site_author: Leonel Quinteros
|
||||||
|
repo_url: https://github.com/leonelquinteros/gotext
|
||||||
|
edit_uri: edit/master/docs/
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
palette:
|
||||||
|
primary: indigo
|
||||||
|
accent: indigo
|
||||||
|
features:
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.sections
|
||||||
|
- navigation.top
|
||||||
|
- search.suggest
|
||||||
|
- search.highlight
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Home: index.md
|
||||||
|
- Getting Started: GETTING_STARTED.md
|
||||||
|
- User Guides:
|
||||||
|
- Plural Forms: PLURALS.md
|
||||||
|
- xgotext CLI: xgotext.md
|
||||||
|
- Character Encoding: ENCODING.md
|
||||||
|
- Advanced Usage: ADVANCED.md
|
||||||
|
- Best Practices: BEST_PRACTICES.md
|
||||||
|
- API Reference: https://pkg.go.dev/github.com/leonelquinteros/gotext
|
||||||
|
- Contributing:
|
||||||
|
- Guidelines: CONTRIBUTING.md
|
||||||
|
- Code of Conduct: CODE_OF_CONDUCT.md
|
||||||
|
- License: LICENSE.md
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.snippets
|
||||||
|
- pymdownx.superfences
|
||||||
+65
-97
@@ -76,42 +76,42 @@ func (mo *Mo) GetDomain() *Domain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the translation for the given string
|
// Get returns the translation for the given string
|
||||||
func (mo *Mo) Get(str string, vars ...interface{}) string {
|
func (mo *Mo) Get(str string, vars ...any) string {
|
||||||
return mo.domain.Get(str, vars...)
|
return mo.domain.Get(str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append a translation string into the domain
|
// Append a translation string into the domain
|
||||||
func (mo *Mo) Append(b []byte, str string, vars ...interface{}) []byte {
|
func (mo *Mo) Append(b []byte, str string, vars ...any) []byte {
|
||||||
return mo.domain.Append(b, str, vars...)
|
return mo.domain.Append(b, str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetN returns the translation for the given string and plural form
|
// GetN returns the translation for the given string and plural form
|
||||||
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
|
func (mo *Mo) GetN(str, plural string, n int, vars ...any) string {
|
||||||
return mo.domain.GetN(str, plural, n, vars...)
|
return mo.domain.GetN(str, plural, n, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendN appends a translation string for the given plural form into the domain
|
// AppendN appends a translation string for the given plural form into the domain
|
||||||
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
|
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
|
||||||
return mo.domain.AppendN(b, str, plural, n, vars...)
|
return mo.domain.AppendN(b, str, plural, n, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetC returns the translation for the given string and context
|
// GetC returns the translation for the given string and context
|
||||||
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
|
func (mo *Mo) GetC(str, ctx string, vars ...any) string {
|
||||||
return mo.domain.GetC(str, ctx, vars...)
|
return mo.domain.GetC(str, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendC appends a translation string for the given context into the domain
|
// AppendC appends a translation string for the given context into the domain
|
||||||
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
|
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...any) []byte {
|
||||||
return mo.domain.AppendC(b, str, ctx, vars...)
|
return mo.domain.AppendC(b, str, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNC returns the translation for the given string, plural form and context
|
// GetNC returns the translation for the given string, plural form and context
|
||||||
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||||
return mo.domain.GetNC(str, plural, n, ctx, vars...)
|
return mo.domain.GetNC(str, plural, n, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendNC appends a translation string for the given plural form and context into the domain
|
// AppendNC appends a translation string for the given plural form and context into the domain
|
||||||
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
|
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
|
||||||
return mo.domain.AppendNC(b, str, plural, n, ctx, vars...)
|
return mo.domain.AppendNC(b, str, plural, n, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,22 +163,18 @@ func (mo *Mo) Parse(buf []byte) {
|
|||||||
defer mo.domain.trMutex.Unlock()
|
defer mo.domain.trMutex.Unlock()
|
||||||
defer mo.domain.pluralMutex.Unlock()
|
defer mo.domain.pluralMutex.Unlock()
|
||||||
|
|
||||||
r := bytes.NewReader(buf)
|
if len(buf) < 28 {
|
||||||
|
|
||||||
var magicNumber uint32
|
|
||||||
if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil {
|
|
||||||
return
|
return
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var bo binary.ByteOrder
|
var bo binary.ByteOrder
|
||||||
switch magicNumber {
|
switch binary.LittleEndian.Uint32(buf[:4]) {
|
||||||
case MoMagicLittleEndian:
|
case MoMagicLittleEndian:
|
||||||
bo = binary.LittleEndian
|
bo = binary.LittleEndian
|
||||||
case MoMagicBigEndian:
|
case MoMagicBigEndian:
|
||||||
bo = binary.BigEndian
|
bo = binary.BigEndian
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
// return fmt.Errorf("gettext: %v", "invalid magic number")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var header struct {
|
var header struct {
|
||||||
@@ -187,82 +183,58 @@ func (mo *Mo) Parse(buf []byte) {
|
|||||||
MsgIDCount uint32
|
MsgIDCount uint32
|
||||||
MsgIDOffset uint32
|
MsgIDOffset uint32
|
||||||
MsgStrOffset uint32
|
MsgStrOffset uint32
|
||||||
HashSize uint32
|
|
||||||
HashOffset uint32
|
|
||||||
}
|
|
||||||
if err := binary.Read(r, bo, &header); err != nil {
|
|
||||||
return
|
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
|
||||||
if v := header.MajorVersion; v != 0 && v != 1 {
|
|
||||||
return
|
|
||||||
// return fmt.Errorf("gettext: %v", "invalid version number")
|
|
||||||
}
|
|
||||||
if v := header.MinorVersion; v != 0 && v != 1 {
|
|
||||||
return
|
|
||||||
// return fmt.Errorf("gettext: %v", "invalid version number")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msgIDStart := make([]uint32, header.MsgIDCount)
|
bufLen := uint64(len(buf))
|
||||||
msgIDLen := make([]uint32, header.MsgIDCount)
|
header.MajorVersion = bo.Uint16(buf[4:6])
|
||||||
if _, err := r.Seek(int64(header.MsgIDOffset), 0); err != nil {
|
header.MinorVersion = bo.Uint16(buf[6:8])
|
||||||
|
header.MsgIDCount = bo.Uint32(buf[8:12])
|
||||||
|
header.MsgIDOffset = bo.Uint32(buf[12:16])
|
||||||
|
header.MsgStrOffset = bo.Uint32(buf[16:20])
|
||||||
|
|
||||||
|
if header.MajorVersion != 0 && header.MajorVersion != 1 {
|
||||||
return
|
return
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
}
|
||||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
if header.MinorVersion != 0 && header.MinorVersion != 1 {
|
||||||
if err := binary.Read(r, bo, &msgIDLen[i]); err != nil {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
directorySize := uint64(header.MsgIDCount) * 8
|
||||||
|
if uint64(header.MsgIDOffset)+directorySize > bufLen ||
|
||||||
|
uint64(header.MsgStrOffset)+directorySize > bufLen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := uint32(0); i < header.MsgIDCount; i++ {
|
||||||
|
msgIDEntry := int(uint64(header.MsgIDOffset) + uint64(i)*8)
|
||||||
|
msgIDLen := bo.Uint32(buf[msgIDEntry:])
|
||||||
|
msgIDStart := bo.Uint32(buf[msgIDEntry+4:])
|
||||||
|
if uint64(msgIDStart) > bufLen ||
|
||||||
|
uint64(msgIDLen) > bufLen-uint64(msgIDStart) {
|
||||||
return
|
return
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
}
|
||||||
if err := binary.Read(r, bo, &msgIDStart[i]); err != nil {
|
|
||||||
|
msgStrEntry := int(uint64(header.MsgStrOffset) + uint64(i)*8)
|
||||||
|
msgStrLen := bo.Uint32(buf[msgStrEntry:])
|
||||||
|
msgStrStart := bo.Uint32(buf[msgStrEntry+4:])
|
||||||
|
if uint64(msgStrStart) > bufLen ||
|
||||||
|
uint64(msgStrLen) > bufLen-uint64(msgStrStart) {
|
||||||
return
|
return
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msgStrStart := make([]int32, header.MsgIDCount)
|
for i := uint32(0); i < header.MsgIDCount; i++ {
|
||||||
msgStrLen := make([]int32, header.MsgIDCount)
|
msgIDEntry := int(uint64(header.MsgIDOffset) + uint64(i)*8)
|
||||||
if _, err := r.Seek(int64(header.MsgStrOffset), 0); err != nil {
|
msgIDLen := bo.Uint32(buf[msgIDEntry:])
|
||||||
return
|
msgIDStart := bo.Uint32(buf[msgIDEntry+4:])
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
msgIDEnd := int(uint64(msgIDStart) + uint64(msgIDLen))
|
||||||
}
|
|
||||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
|
||||||
if err := binary.Read(r, bo, &msgStrLen[i]); err != nil {
|
|
||||||
return
|
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
|
||||||
if err := binary.Read(r, bo, &msgStrStart[i]); err != nil {
|
|
||||||
return
|
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
msgStrEntry := int(uint64(header.MsgStrOffset) + uint64(i)*8)
|
||||||
if _, err := r.Seek(int64(msgIDStart[i]), 0); err != nil {
|
msgStrLen := bo.Uint32(buf[msgStrEntry:])
|
||||||
return
|
msgStrStart := bo.Uint32(buf[msgStrEntry+4:])
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
msgStrEnd := int(uint64(msgStrStart) + uint64(msgStrLen))
|
||||||
}
|
|
||||||
msgIDData := make([]byte, msgIDLen[i])
|
|
||||||
if _, err := r.Read(msgIDData); err != nil {
|
|
||||||
return
|
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil {
|
mo.addTranslation(buf[int(msgIDStart):msgIDEnd], buf[int(msgStrStart):msgStrEnd])
|
||||||
return
|
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
|
||||||
msgStrData := make([]byte, msgStrLen[i])
|
|
||||||
if _, err := r.Read(msgStrData); err != nil {
|
|
||||||
return
|
|
||||||
// return fmt.Errorf("gettext: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(msgIDData) == 0 {
|
|
||||||
mo.addTranslation(msgIDData, msgStrData)
|
|
||||||
} else {
|
|
||||||
mo.addTranslation(msgIDData, msgStrData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse headers
|
// Parse headers
|
||||||
@@ -279,36 +251,32 @@ func (mo *Mo) addTranslation(msgid, msgstr []byte) {
|
|||||||
translation := NewTranslation()
|
translation := NewTranslation()
|
||||||
var msgctxt []byte
|
var msgctxt []byte
|
||||||
var msgidPlural []byte
|
var msgidPlural []byte
|
||||||
|
hasContext := false
|
||||||
|
|
||||||
d := bytes.Split(msgid, []byte(EotSeparator))
|
if eotIndex := bytes.IndexByte(msgid, EotSeparator[0]); eotIndex >= 0 {
|
||||||
if len(d) == 1 {
|
hasContext = true
|
||||||
msgid = d[0]
|
msgctxt = msgid[:eotIndex]
|
||||||
} else {
|
msgid = msgid[eotIndex+len(EotSeparator):]
|
||||||
msgid, msgctxt = d[1], d[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dd := bytes.Split(msgid, []byte(NulSeparator))
|
if nulIndex := bytes.IndexByte(msgid, NulSeparator[0]); nulIndex >= 0 {
|
||||||
if len(dd) > 1 {
|
msgidPlural = msgid[nulIndex+1:]
|
||||||
msgid = dd[0]
|
msgid = msgid[:nulIndex]
|
||||||
dd = dd[1:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
translation.ID = string(msgid)
|
translation.ID = string(msgid)
|
||||||
|
|
||||||
msgidPlural = bytes.Join(dd, []byte(NulSeparator))
|
|
||||||
if len(msgidPlural) > 0 {
|
if len(msgidPlural) > 0 {
|
||||||
translation.PluralID = string(msgidPlural)
|
translation.PluralID = string(msgidPlural)
|
||||||
}
|
}
|
||||||
|
|
||||||
ddd := bytes.Split(msgstr, []byte(NulSeparator))
|
i := 0
|
||||||
if len(ddd) > 0 {
|
for s := range bytes.SplitSeq(msgstr, []byte(NulSeparator)) {
|
||||||
for i, s := range ddd {
|
translation.Trs[i] = string(s)
|
||||||
translation.Trs[i] = string(s)
|
i++
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msgctxt) > 0 {
|
if hasContext {
|
||||||
// With context...
|
// With context, including an explicitly empty context...
|
||||||
if _, ok := mo.domain.contextTranslations[string(msgctxt)]; !ok {
|
if _, ok := mo.domain.contextTranslations[string(msgctxt)]; !ok {
|
||||||
mo.domain.contextTranslations[string(msgctxt)] = make(map[string]*Translation)
|
mo.domain.contextTranslations[string(msgctxt)] = make(map[string]*Translation)
|
||||||
}
|
}
|
||||||
|
|||||||
+362
-178
@@ -12,18 +12,12 @@ package plurals
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
type match struct {
|
|
||||||
openPos int
|
|
||||||
closePos int
|
|
||||||
}
|
|
||||||
|
|
||||||
var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`)
|
|
||||||
|
|
||||||
type testToken interface {
|
type testToken interface {
|
||||||
compile(tokens []string) (test test, err error)
|
compile(tokens []string) (test test, err error)
|
||||||
}
|
}
|
||||||
@@ -31,30 +25,30 @@ type testToken interface {
|
|||||||
type cmpTestBuilder func(val uint32, flipped bool) test
|
type cmpTestBuilder func(val uint32, flipped bool) test
|
||||||
type logicTestBuild func(left test, right test) test
|
type logicTestBuild func(left test, right test) test
|
||||||
|
|
||||||
var ternaryToken ternaryStruct
|
func compileTernary(tokens []string, depth int) (expr Expression, err error) {
|
||||||
|
if depth > maxParseDepth {
|
||||||
|
return nil, errors.New("expression nesting is too deep")
|
||||||
|
}
|
||||||
|
|
||||||
type ternaryStruct struct{}
|
|
||||||
|
|
||||||
func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
|
|
||||||
main, err := splitTokens(tokens, "?")
|
main, err := splitTokens(tokens, "?")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return expr, err
|
return nil, err
|
||||||
}
|
}
|
||||||
test, err := compileTest(strings.Join(main.Left, ""))
|
test, err := compileTestDepth(strings.Join(main.Left, ""), depth+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return expr, err
|
return nil, err
|
||||||
}
|
}
|
||||||
actions, err := splitTokens(main.Right, ":")
|
actions, err := splitTernaryTokens(main.Right)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return expr, err
|
return nil, err
|
||||||
}
|
}
|
||||||
trueAction, err := compileExpression(strings.Join(actions.Left, ""))
|
trueAction, err := compileExpressionDepth(strings.Join(actions.Left, ""), depth+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return expr, err
|
return nil, err
|
||||||
}
|
}
|
||||||
falseAction, err := compileExpression(strings.Join(actions.Right, ""))
|
falseAction, err := compileExpressionDepth(strings.Join(actions.Right, ""), depth+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return expr, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
return ternary{
|
return ternary{
|
||||||
test: test,
|
test: test,
|
||||||
@@ -63,93 +57,145 @@ func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxParseDepth = 1024
|
||||||
|
|
||||||
|
func splitTernaryTokens(tokens []string) (s splitted, err error) {
|
||||||
|
depth := 0
|
||||||
|
for index, token := range tokens {
|
||||||
|
switch token {
|
||||||
|
case "?":
|
||||||
|
depth++
|
||||||
|
case ":":
|
||||||
|
if depth == 0 {
|
||||||
|
return splitted{
|
||||||
|
Left: tokens[:index],
|
||||||
|
Right: tokens[index+1:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s, errors.New("':' not found for ternary expression")
|
||||||
|
}
|
||||||
|
|
||||||
var constToken constValStruct
|
var constToken constValStruct
|
||||||
|
|
||||||
type constValStruct struct{}
|
type constValStruct struct{}
|
||||||
|
|
||||||
func (constValStruct) compile(tokens []string) (expr Expression, err error) {
|
func (constValStruct) compile(tokens []string) (expr Expression, err error) {
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
return expr, errors.New("got nothing instead of constant")
|
return nil, errors.New("got nothing instead of constant")
|
||||||
}
|
}
|
||||||
if len(tokens) != 1 {
|
if len(tokens) != 1 {
|
||||||
return expr, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
|
return nil, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
|
||||||
}
|
}
|
||||||
i, err := strconv.Atoi(tokens[0])
|
i, err := strconv.Atoi(tokens[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return expr, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return constValue{value: i}, nil
|
return constValue{value: i}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileLogicTest(tokens []string, sep string, builder logicTestBuild) (test test, err error) {
|
func compileLogicTestDepth(tokens []string, sep string, builder logicTestBuild, depth int) (test test, err error) {
|
||||||
|
if depth > maxParseDepth {
|
||||||
|
return nil, errors.New("expression nesting is too deep")
|
||||||
|
}
|
||||||
split, err := splitTokens(tokens, sep)
|
split, err := splitTokens(tokens, sep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return test, err
|
return nil, err
|
||||||
}
|
}
|
||||||
left, err := compileTest(strings.Join(split.Left, ""))
|
left, err := compileTestDepth(strings.Join(split.Left, ""), depth+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return test, err
|
return nil, err
|
||||||
}
|
}
|
||||||
right, err := compileTest(strings.Join(split.Right, ""))
|
right, err := compileTestDepth(strings.Join(split.Right, ""), depth+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return test, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return builder(left, right), nil
|
return builder(left, right), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var orToken orStruct
|
|
||||||
|
|
||||||
type orStruct struct{}
|
|
||||||
|
|
||||||
func (orStruct) compile(tokens []string) (test test, err error) {
|
|
||||||
return compileLogicTest(tokens, "||", buildOr)
|
|
||||||
}
|
|
||||||
func buildOr(left test, right test) test {
|
func buildOr(left test, right test) test {
|
||||||
return or{left: left, right: right}
|
return or{left: left, right: right}
|
||||||
}
|
}
|
||||||
|
|
||||||
var andToken andStruct
|
|
||||||
|
|
||||||
type andStruct struct{}
|
|
||||||
|
|
||||||
func (andStruct) compile(tokens []string) (test test, err error) {
|
|
||||||
return compileLogicTest(tokens, "&&", buildAnd)
|
|
||||||
}
|
|
||||||
func buildAnd(left test, right test) test {
|
func buildAnd(left test, right test) test {
|
||||||
return and{left: left, right: right}
|
return and{left: left, right: right}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unwrapSingleGroup(tokens []string) ([]string, error) {
|
||||||
|
for len(tokens) == 1 && strings.HasPrefix(tokens[0], "(") {
|
||||||
|
inner, err := tokenize(tokens[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(inner) == 1 && inner[0] == tokens[0] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tokens = inner
|
||||||
|
}
|
||||||
|
return tokens, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSimpleN(tokens []string) bool {
|
||||||
|
tokens, err := unwrapSingleGroup(tokens)
|
||||||
|
if err != nil || len(tokens) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return tokens[0] == "n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLiteralTokens(tokens []string) (uint32, error) {
|
||||||
|
tokens, err := unwrapSingleGroup(tokens)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(tokens) != 1 {
|
||||||
|
return 0, errors.New("expected a single integer")
|
||||||
|
}
|
||||||
|
return parseUint32(tokens[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSimpleN(tokens []string) bool {
|
||||||
|
for _, token := range tokens {
|
||||||
|
if isSimpleN([]string{token}) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func compileMod(tokens []string) (math math, err error) {
|
func compileMod(tokens []string) (math math, err error) {
|
||||||
split, err := splitTokens(tokens, "%")
|
split, err := splitTokens(tokens, "%")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return math, err
|
return math, err
|
||||||
}
|
}
|
||||||
if len(split.Left) != 1 || split.Left[0] != "n" {
|
if !isSimpleN(split.Left) {
|
||||||
return math, errors.New("modulus operation requires 'n' as left operand")
|
return math, errors.New("modulus operation requires 'n' as left operand")
|
||||||
}
|
}
|
||||||
if len(split.Right) != 1 {
|
i, err := parseLiteralTokens(split.Right)
|
||||||
return math, errors.New("modulus operation requires simple integer as right operand")
|
|
||||||
}
|
|
||||||
i, err := parseUint32(split.Right[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return math, err
|
return math, err
|
||||||
}
|
}
|
||||||
return mod{value: uint32(i)}, nil
|
if i == 0 {
|
||||||
|
return math, errors.New("modulus operation requires a non-zero integer as right operand")
|
||||||
|
}
|
||||||
|
return mod{value: i}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder, flipped bool) (test test, err error) {
|
func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder, flipped bool) (test test, err error) {
|
||||||
modifier, err := compileMod(modTokens)
|
modifier, err := compileMod(modTokens)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return test, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(actionTokens) != 1 {
|
if len(actionTokens) == 0 {
|
||||||
return test, errors.New("can only get modulus of integer")
|
return nil, errors.New("can only get modulus of integer")
|
||||||
}
|
}
|
||||||
i, err := parseUint32(actionTokens[0])
|
i, err := parseLiteralTokens(actionTokens)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return test, err
|
return nil, err
|
||||||
}
|
}
|
||||||
action := builder(uint32(i), flipped)
|
action := builder(i, flipped)
|
||||||
return pipe{
|
return pipe{
|
||||||
modifier: modifier,
|
modifier: modifier,
|
||||||
action: action,
|
action: action,
|
||||||
@@ -159,31 +205,37 @@ func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder,
|
|||||||
func compileEquality(tokens []string, sep string, builder cmpTestBuilder) (test test, err error) {
|
func compileEquality(tokens []string, sep string, builder cmpTestBuilder) (test test, err error) {
|
||||||
split, err := splitTokens(tokens, sep)
|
split, err := splitTokens(tokens, sep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return test, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(split.Left) == 1 && split.Left[0] == "n" {
|
left, err := unwrapSingleGroup(split.Left)
|
||||||
if len(split.Right) != 1 {
|
if err != nil {
|
||||||
return test, errors.New("test can only compare n to integers")
|
return nil, err
|
||||||
}
|
}
|
||||||
i, err := parseUint32(split.Right[0])
|
right, err := unwrapSingleGroup(split.Right)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if isSimpleN(left) {
|
||||||
|
i, err := parseLiteralTokens(right)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return test, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return builder(i, false), nil
|
return builder(i, false), nil
|
||||||
} else if len(split.Right) == 1 && split.Right[0] == "n" {
|
}
|
||||||
if len(split.Left) != 1 {
|
if isSimpleN(right) {
|
||||||
return test, errors.New("test can only compare n to integers")
|
i, err := parseLiteralTokens(left)
|
||||||
}
|
|
||||||
i, err := parseUint32(split.Left[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return test, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return builder(i, true), nil
|
return builder(i, true), nil
|
||||||
} else if contains(split.Left, "n") && contains(split.Left, "%") {
|
|
||||||
return subPipe(split.Left, split.Right, builder, false)
|
|
||||||
}
|
}
|
||||||
return test, errors.New("equality test must have 'n' as one of the two tests")
|
if containsSimpleN(left) && slices.Contains(left, "%") {
|
||||||
|
return subPipe(left, right, builder, false)
|
||||||
|
}
|
||||||
|
if containsSimpleN(right) && slices.Contains(right, "%") {
|
||||||
|
return subPipe(right, left, builder, true)
|
||||||
|
}
|
||||||
|
return nil, errors.New("equality test must have 'n' as one of the two tests")
|
||||||
}
|
}
|
||||||
|
|
||||||
var eqToken eqStruct
|
var eqToken eqStruct
|
||||||
@@ -258,8 +310,8 @@ type testTokenDef struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var precedence = []testTokenDef{
|
var precedence = []testTokenDef{
|
||||||
{op: "||", token: orToken},
|
{op: "||"},
|
||||||
{op: "&&", token: andToken},
|
{op: "&&"},
|
||||||
{op: "==", token: eqToken},
|
{op: "==", token: eqToken},
|
||||||
{op: "!=", token: neqToken},
|
{op: "!=", token: neqToken},
|
||||||
{op: ">=", token: gteToken},
|
{op: ">=", token: gteToken},
|
||||||
@@ -273,20 +325,10 @@ type splitted struct {
|
|||||||
Right []string
|
Right []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find index of token in list of tokens
|
|
||||||
func index(tokens []string, sep string) int {
|
|
||||||
for index, token := range tokens {
|
|
||||||
if token == sep {
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split a list of tokens by a token into a splitted struct holding the tokens
|
// Split a list of tokens by a token into a splitted struct holding the tokens
|
||||||
// before and after the token to be split by.
|
// before and after the token to be split by.
|
||||||
func splitTokens(tokens []string, sep string) (s splitted, err error) {
|
func splitTokens(tokens []string, sep string) (s splitted, err error) {
|
||||||
index := index(tokens, sep)
|
index := slices.Index(tokens, sep)
|
||||||
if index == -1 {
|
if index == -1 {
|
||||||
return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','"))
|
return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','"))
|
||||||
}
|
}
|
||||||
@@ -296,135 +338,277 @@ func splitTokens(tokens []string, sep string) (s splitted, err error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan a string for parenthesis
|
func trimLexicalWhitespace(s string) string {
|
||||||
func scan(s string) <-chan match {
|
return strings.Map(func(r rune) rune {
|
||||||
ch := make(chan match)
|
if unicode.IsSpace(r) {
|
||||||
go func() {
|
return -1
|
||||||
depth := 0
|
|
||||||
opener := 0
|
|
||||||
for index, char := range s {
|
|
||||||
switch char {
|
|
||||||
case '(':
|
|
||||||
if depth == 0 {
|
|
||||||
opener = index
|
|
||||||
}
|
|
||||||
depth++
|
|
||||||
case ')':
|
|
||||||
depth--
|
|
||||||
if depth == 0 {
|
|
||||||
ch <- match{
|
|
||||||
openPos: opener,
|
|
||||||
closePos: index + 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
close(ch)
|
return r
|
||||||
}()
|
}, s)
|
||||||
return ch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the string into tokens
|
func lexicalToken(s string, index int) (token string, next int, err error) {
|
||||||
func split(s string) <-chan string {
|
if index >= len(s) {
|
||||||
ch := make(chan string)
|
return "", index, errors.New("unexpected end of expression")
|
||||||
go func() {
|
}
|
||||||
s = strings.ReplaceAll(s, " ", "")
|
|
||||||
if !strings.Contains(s, "(") {
|
switch s[index] {
|
||||||
ch <- s
|
case '?', ':', '%', '(', ')':
|
||||||
} else {
|
return s[index : index+1], index + 1, nil
|
||||||
last := 0
|
case '|':
|
||||||
end := len(s)
|
if index+1 >= len(s) || s[index+1] != '|' {
|
||||||
for info := range scan(s) {
|
return "", index, errors.New("logical OR must use '||'")
|
||||||
if last != info.openPos {
|
}
|
||||||
ch <- s[last:info.openPos]
|
return "||", index + 2, nil
|
||||||
}
|
case '&':
|
||||||
ch <- s[info.openPos:info.closePos]
|
if index+1 >= len(s) || s[index+1] != '&' {
|
||||||
last = info.closePos
|
return "", index, errors.New("logical AND must use '&&'")
|
||||||
|
}
|
||||||
|
return "&&", index + 2, nil
|
||||||
|
case '=':
|
||||||
|
if index+1 >= len(s) || s[index+1] != '=' {
|
||||||
|
return "", index, errors.New("equality must use '=='")
|
||||||
|
}
|
||||||
|
return "==", index + 2, nil
|
||||||
|
case '!':
|
||||||
|
if index+1 >= len(s) || s[index+1] != '=' {
|
||||||
|
return "", index, errors.New("inequality must use '!='")
|
||||||
|
}
|
||||||
|
return "!=", index + 2, nil
|
||||||
|
case '>':
|
||||||
|
if index+1 < len(s) && s[index+1] == '=' {
|
||||||
|
return ">=", index + 2, nil
|
||||||
|
}
|
||||||
|
return ">", index + 1, nil
|
||||||
|
case '<':
|
||||||
|
if index+1 < len(s) && s[index+1] == '=' {
|
||||||
|
return "<=", index + 2, nil
|
||||||
|
}
|
||||||
|
return "<", index + 1, nil
|
||||||
|
case 'n':
|
||||||
|
return "n", index + 1, nil
|
||||||
|
default:
|
||||||
|
if s[index] >= '0' && s[index] <= '9' {
|
||||||
|
next = index + 1
|
||||||
|
for next < len(s) && s[next] >= '0' && s[next] <= '9' {
|
||||||
|
next++
|
||||||
}
|
}
|
||||||
if last != end {
|
return s[index:next], next, nil
|
||||||
ch <- s[last:]
|
}
|
||||||
|
return "", index, fmt.Errorf("invalid character %q in expression", s[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateParentheses(s string) error {
|
||||||
|
depth := 0
|
||||||
|
for index := 0; index < len(s); {
|
||||||
|
token, next, err := lexicalToken(s, index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch token {
|
||||||
|
case "(":
|
||||||
|
depth++
|
||||||
|
case ")":
|
||||||
|
depth--
|
||||||
|
if depth < 0 {
|
||||||
|
return errors.New("unmatched closing parenthesis")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(ch)
|
index = next
|
||||||
}()
|
}
|
||||||
return ch
|
if depth != 0 {
|
||||||
|
return errors.New("unmatched opening parenthesis")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripRedundantOuterParens(s string) string {
|
||||||
|
if len(s) < 2 || s[0] != '(' {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
leading := 0
|
||||||
|
for leading < len(s) && s[leading] == '(' {
|
||||||
|
leading++
|
||||||
|
}
|
||||||
|
|
||||||
|
openers := make([]int, 0, leading)
|
||||||
|
outer := leading
|
||||||
|
for index := range s {
|
||||||
|
switch s[index] {
|
||||||
|
case '(':
|
||||||
|
openers = append(openers, index)
|
||||||
|
case ')':
|
||||||
|
if len(openers) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
opener := openers[len(openers)-1]
|
||||||
|
openers = openers[:len(openers)-1]
|
||||||
|
if opener < outer && index != len(s)-1-opener {
|
||||||
|
outer = opener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(openers) != 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[outer : len(s)-outer]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tokenizes a string into a list of strings, tokens grouped by parenthesis are
|
// Tokenizes a string into a list of strings, tokens grouped by parenthesis are
|
||||||
// not split! If the string starts with ( and ends in ), those are stripped.
|
// not split! If the string starts with ( and ends in ), those are stripped.
|
||||||
func tokenize(s string) []string {
|
func tokenize(s string) ([]string, error) {
|
||||||
/*
|
s = trimLexicalWhitespace(s)
|
||||||
TODO: Properly detect if the string starts with a ( and ends with a )
|
if s == "" {
|
||||||
and that those two form a matching pair.
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
if err := validateParentheses(s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s = stripRedundantOuterParens(s)
|
||||||
|
if s == "" {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
Eg: (foo) -> true; (foo)(bar) -> false;
|
|
||||||
*/
|
|
||||||
if len(s) == 0 {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
if s[0] == '(' && s[len(s)-1] == ')' {
|
|
||||||
s = s[1 : len(s)-1]
|
|
||||||
}
|
|
||||||
ret := []string{}
|
ret := []string{}
|
||||||
for chunk := range split(s) {
|
for index := 0; index < len(s); {
|
||||||
if len(chunk) != 0 {
|
if s[index] == '(' {
|
||||||
if chunk[0] == '(' && chunk[len(chunk)-1] == ')' {
|
start := index
|
||||||
ret = append(ret, chunk)
|
depth := 0
|
||||||
} else {
|
groupEnd := false
|
||||||
for _, token := range pat.FindAllStringSubmatch(chunk, -1) {
|
for index < len(s) {
|
||||||
ret = append(ret, token[0])
|
switch s[index] {
|
||||||
|
case '(':
|
||||||
|
depth++
|
||||||
|
case ')':
|
||||||
|
depth--
|
||||||
|
if depth == 0 {
|
||||||
|
index++
|
||||||
|
ret = append(ret, s[start:index])
|
||||||
|
groupEnd = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if groupEnd {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
index++
|
||||||
}
|
}
|
||||||
} else {
|
if depth != 0 {
|
||||||
fmt.Printf("Empty chunk in string '%s'\n", s)
|
return nil, errors.New("unmatched opening parenthesis")
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
token, next, err := lexicalToken(s, index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if token == ")" {
|
||||||
|
return nil, errors.New("unmatched closing parenthesis")
|
||||||
|
}
|
||||||
|
ret = append(ret, token)
|
||||||
|
index = next
|
||||||
}
|
}
|
||||||
return ret
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a string containing a plural form expression to a Expression object.
|
// Compile a string containing a plural form expression to a Expression object.
|
||||||
func Compile(s string) (expr Expression, err error) {
|
func Compile(s string) (expr Expression, err error) {
|
||||||
if s == "0" {
|
s = trimLexicalWhitespace(s)
|
||||||
return constValue{value: 0}, nil
|
if s == "" {
|
||||||
|
return nil, errors.New("empty expression")
|
||||||
}
|
}
|
||||||
if !strings.Contains(s, "?") {
|
return compileExpressionDepth(s, 0)
|
||||||
s += "?1:0"
|
|
||||||
}
|
|
||||||
return compileExpression(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a token is in a slice of strings
|
// Compiles an expression (ternary, numeric leaf, or test).
|
||||||
func contains(haystack []string, needle string) bool {
|
func compileExpressionDepth(s string, depth int) (expr Expression, err error) {
|
||||||
for _, s := range haystack {
|
if depth > maxParseDepth {
|
||||||
if s == needle {
|
return nil, errors.New("expression nesting is too deep")
|
||||||
return true
|
}
|
||||||
|
tokens, err := tokenize(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if slices.Contains(tokens, "?") {
|
||||||
|
return compileTernary(tokens, depth)
|
||||||
|
}
|
||||||
|
for _, tokenDef := range precedence {
|
||||||
|
if !slices.Contains(tokens, tokenDef.op) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
condition, err := compileTestDepth(s, depth+1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return testValue{condition: condition}, nil
|
||||||
}
|
}
|
||||||
return false
|
if isSimpleN(tokens) {
|
||||||
}
|
return variableValue{}, nil
|
||||||
|
}
|
||||||
// Compiles an expression (ternary or constant)
|
if slices.Contains(tokens, "%") {
|
||||||
func compileExpression(s string) (expr Expression, err error) {
|
modifier, err := compileMod(tokens)
|
||||||
tokens := tokenize(s)
|
if err != nil {
|
||||||
if contains(tokens, "?") {
|
return nil, err
|
||||||
return ternaryToken.compile(tokens)
|
}
|
||||||
|
return mathValue{value: modifier}, nil
|
||||||
}
|
}
|
||||||
return constToken.compile(tokens)
|
return constToken.compile(tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compiles a test (comparison)
|
// Compiles a test (comparison)
|
||||||
func compileTest(s string) (test test, err error) {
|
func compileTestDepth(s string, depth int) (test test, err error) {
|
||||||
tokens := tokenize(s)
|
if depth > maxParseDepth {
|
||||||
|
return nil, errors.New("expression nesting is too deep")
|
||||||
|
}
|
||||||
|
tokens, err := tokenize(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
for _, tokenDef := range precedence {
|
for _, tokenDef := range precedence {
|
||||||
if contains(tokens, tokenDef.op) {
|
if !slices.Contains(tokens, tokenDef.op) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch tokenDef.op {
|
||||||
|
case "||":
|
||||||
|
return compileLogicTestDepth(tokens, "||", buildOr, depth)
|
||||||
|
case "&&":
|
||||||
|
return compileLogicTestDepth(tokens, "&&", buildAnd, depth)
|
||||||
|
default:
|
||||||
return tokenDef.token.compile(tokens)
|
return tokenDef.token.compile(tokens)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return test, errors.New("cannot compile")
|
if slices.Contains(tokens, "%") {
|
||||||
|
m, err := compileMod(tokens)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pipe{
|
||||||
|
modifier: m,
|
||||||
|
action: notequal{value: 0},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if len(tokens) == 1 && strings.HasPrefix(tokens[0], "(") {
|
||||||
|
inner, err := tokenize(tokens[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(inner) == 1 && inner[0] == tokens[0] {
|
||||||
|
return nil, errors.New("cannot compile parenthesized test")
|
||||||
|
}
|
||||||
|
return compileTestDepth(tokens[0], depth+1)
|
||||||
|
}
|
||||||
|
if len(tokens) == 1 && tokens[0] == "n" {
|
||||||
|
return notequal{value: 0}, nil
|
||||||
|
}
|
||||||
|
if value, err := parseLiteralTokens(tokens); err == nil {
|
||||||
|
return literalTest{value: value}, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("cannot compile")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUint32(s string) (ui uint32, err error) {
|
func parseUint32(s string) (ui uint32, err error) {
|
||||||
|
s = trimLexicalWhitespace(s)
|
||||||
i, err := strconv.ParseUint(s, 10, 32)
|
i, err := strconv.ParseUint(s, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ui, err
|
return ui, err
|
||||||
|
|||||||
+34
@@ -20,6 +20,37 @@ func (c constValue) Eval(n uint32) int {
|
|||||||
return c.value
|
return c.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type variableValue struct{}
|
||||||
|
|
||||||
|
func (variableValue) Eval(n uint32) int {
|
||||||
|
return int(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mathValue struct {
|
||||||
|
value math
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mathValue) Eval(n uint32) int {
|
||||||
|
if m.value == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return int(m.value.calc(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
type testValue struct {
|
||||||
|
condition test
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t testValue) Eval(n uint32) int {
|
||||||
|
if t.condition == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if t.condition.test(n) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type test interface {
|
type test interface {
|
||||||
test(n uint32) bool
|
test(n uint32) bool
|
||||||
}
|
}
|
||||||
@@ -31,6 +62,9 @@ type ternary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t ternary) Eval(n uint32) int {
|
func (t ternary) Eval(n uint32) int {
|
||||||
|
if t.test == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
if t.test.test(n) {
|
if t.test.test(n) {
|
||||||
if t.trueExpr == nil {
|
if t.trueExpr == nil {
|
||||||
return -1
|
return -1
|
||||||
|
|||||||
+3
@@ -15,5 +15,8 @@ type mod struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m mod) calc(n uint32) uint32 {
|
func (m mod) calc(n uint32) uint32 {
|
||||||
|
if m.value == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return n % m.value
|
return n % m.value
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -22,6 +22,14 @@ func (e notequal) test(n uint32) bool {
|
|||||||
return n != e.value
|
return n != e.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type literalTest struct {
|
||||||
|
value uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l literalTest) test(n uint32) bool {
|
||||||
|
return l.value != 0
|
||||||
|
}
|
||||||
|
|
||||||
type gt struct {
|
type gt struct {
|
||||||
value uint32
|
value uint32
|
||||||
flipped bool
|
flipped bool
|
||||||
@@ -77,6 +85,9 @@ type and struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e and) test(n uint32) bool {
|
func (e and) test(n uint32) bool {
|
||||||
|
if e.left == nil || e.right == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if !e.left.test(n) {
|
if !e.left.test(n) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -89,6 +100,9 @@ type or struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e or) test(n uint32) bool {
|
func (e or) test(n uint32) bool {
|
||||||
|
if e.left == nil || e.right == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if e.left.test(n) {
|
if e.left.test(n) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -101,5 +115,8 @@ type pipe struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e pipe) test(n uint32) bool {
|
func (e pipe) test(n uint32) bool {
|
||||||
|
if e.modifier == nil || e.action == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return e.action.test(e.modifier.calc(n))
|
return e.action.test(e.modifier.calc(n))
|
||||||
}
|
}
|
||||||
|
|||||||
+305
-78
@@ -42,6 +42,10 @@ type Po struct {
|
|||||||
|
|
||||||
domain *Domain
|
domain *Domain
|
||||||
fs fs.FS
|
fs fs.FS
|
||||||
|
|
||||||
|
parseBufferHasID bool
|
||||||
|
parseBufferHasPlural bool
|
||||||
|
parseBufferHasContext bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type parseState int
|
type parseState int
|
||||||
@@ -52,6 +56,7 @@ const (
|
|||||||
msgID
|
msgID
|
||||||
msgIDPlural
|
msgIDPlural
|
||||||
msgStr
|
msgStr
|
||||||
|
msgStrInvalid
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewPo should always be used to instantiate a new Po object
|
// NewPo should always be used to instantiate a new Po object
|
||||||
@@ -94,7 +99,7 @@ func (po *Po) GetRefs(str string) []string {
|
|||||||
|
|
||||||
// SetPluralResolver sets the plural resolver function
|
// SetPluralResolver sets the plural resolver function
|
||||||
func (po *Po) SetPluralResolver(f func(int) int) {
|
func (po *Po) SetPluralResolver(f func(int) int) {
|
||||||
po.domain.customPluralResolver = f
|
po.domain.SetPluralResolver(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set translation
|
// Set translation
|
||||||
@@ -103,12 +108,12 @@ func (po *Po) Set(id, str string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get translation
|
// Get translation
|
||||||
func (po *Po) Get(str string, vars ...interface{}) string {
|
func (po *Po) Get(str string, vars ...any) string {
|
||||||
return po.domain.Get(str, vars...)
|
return po.domain.Get(str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append translation
|
// Append translation
|
||||||
func (po *Po) Append(b []byte, str string, vars ...interface{}) []byte {
|
func (po *Po) Append(b []byte, str string, vars ...any) []byte {
|
||||||
return po.domain.Append(b, str, vars...)
|
return po.domain.Append(b, str, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +123,12 @@ func (po *Po) SetN(id, plural string, n int, str string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetN gets the plural translation
|
// GetN gets the plural translation
|
||||||
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
|
func (po *Po) GetN(str, plural string, n int, vars ...any) string {
|
||||||
return po.domain.GetN(str, plural, n, vars...)
|
return po.domain.GetN(str, plural, n, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendN appends the plural translation
|
// AppendN appends the plural translation
|
||||||
func (po *Po) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
|
func (po *Po) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
|
||||||
return po.domain.AppendN(b, str, plural, n, vars...)
|
return po.domain.AppendN(b, str, plural, n, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +138,12 @@ func (po *Po) SetC(id, ctx, str string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetC gets the translation for a given context
|
// GetC gets the translation for a given context
|
||||||
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
|
func (po *Po) GetC(str, ctx string, vars ...any) string {
|
||||||
return po.domain.GetC(str, ctx, vars...)
|
return po.domain.GetC(str, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendC appends the translation for a given context
|
// AppendC appends the translation for a given context
|
||||||
func (po *Po) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
|
func (po *Po) AppendC(b []byte, str, ctx string, vars ...any) []byte {
|
||||||
return po.domain.AppendC(b, str, ctx, vars...)
|
return po.domain.AppendC(b, str, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,12 +153,12 @@ func (po *Po) SetNC(id, plural, ctx string, n int, str string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetNC gets the plural translation for a given context
|
// GetNC gets the plural translation for a given context
|
||||||
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||||
return po.domain.GetNC(str, plural, n, ctx, vars...)
|
return po.domain.GetNC(str, plural, n, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendNC appends the plural translation for a given context
|
// AppendNC appends the plural translation for a given context
|
||||||
func (po *Po) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
|
func (po *Po) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
|
||||||
return po.domain.AppendNC(b, str, plural, n, ctx, vars...)
|
return po.domain.AppendNC(b, str, plural, n, ctx, vars...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,16 +219,17 @@ func (po *Po) Parse(buf []byte) {
|
|||||||
defer po.domain.trMutex.Unlock()
|
defer po.domain.trMutex.Unlock()
|
||||||
defer po.domain.pluralMutex.Unlock()
|
defer po.domain.pluralMutex.Unlock()
|
||||||
|
|
||||||
// Get lines
|
|
||||||
lines := strings.Split(string(buf), "\n")
|
|
||||||
|
|
||||||
// Init buffer
|
// Init buffer
|
||||||
po.domain.trBuffer = NewTranslation()
|
po.domain.trBuffer = NewTranslation()
|
||||||
po.domain.ctxBuffer = ""
|
po.domain.ctxBuffer = ""
|
||||||
po.domain.refBuffer = ""
|
po.domain.refBuffer = ""
|
||||||
|
po.parseBufferHasID = false
|
||||||
|
po.parseBufferHasPlural = false
|
||||||
|
po.parseBufferHasContext = false
|
||||||
|
|
||||||
state := head
|
state := head
|
||||||
for _, l := range lines {
|
activeMsgStrIndex := 0
|
||||||
|
for l := range strings.Lines(string(buf)) {
|
||||||
// Trim spaces
|
// Trim spaces
|
||||||
l = strings.TrimSpace(l)
|
l = strings.TrimSpace(l)
|
||||||
|
|
||||||
@@ -233,44 +239,71 @@ func (po *Po) Parse(buf []byte) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer context and continue
|
// Multi line strings and headers
|
||||||
if strings.HasPrefix(l, "msgctxt") {
|
if strings.HasPrefix(l, "\"") {
|
||||||
po.parseContext(l)
|
if !po.parseString(l, state, activeMsgStrIndex) && state != head {
|
||||||
state = msgCtxt
|
state = msgStrInvalid
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer msgid and continue
|
// Buffer context and continue
|
||||||
if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") {
|
if po.hasKeywordBoundary(l, "msgctxt") {
|
||||||
po.parseID(l)
|
if po.parseContext(l) {
|
||||||
state = msgID
|
state = msgCtxt
|
||||||
|
} else {
|
||||||
|
state = msgStrInvalid
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for plural form
|
// Check for plural form
|
||||||
if strings.HasPrefix(l, "msgid_plural") {
|
if po.hasKeywordBoundary(l, "msgid_plural") {
|
||||||
po.parsePluralID(l)
|
if state != msgID || !po.parsePluralID(l) {
|
||||||
po.domain.pluralTranslations[po.domain.trBuffer.PluralID] = po.domain.trBuffer
|
state = msgStrInvalid
|
||||||
state = msgIDPlural
|
} else {
|
||||||
|
state = msgIDPlural
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer msgid and continue
|
||||||
|
if po.hasKeywordBoundary(l, "msgid") {
|
||||||
|
if po.parseID(l) {
|
||||||
|
state = msgID
|
||||||
|
} else {
|
||||||
|
state = msgStrInvalid
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save Translation
|
// Save Translation
|
||||||
if strings.HasPrefix(l, "msgstr") {
|
if po.hasKeywordBoundary(l, "msgstr") {
|
||||||
po.parseMessage(l)
|
if state != msgID && state != msgIDPlural && state != msgStr {
|
||||||
state = msgStr
|
activeMsgStrIndex = 0
|
||||||
continue
|
state = msgStrInvalid
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Multi line strings and headers
|
var ok bool
|
||||||
if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") {
|
activeMsgStrIndex, ok = po.parseMessage(l)
|
||||||
po.parseString(l, state)
|
if ok {
|
||||||
|
state = msgStr
|
||||||
|
} else {
|
||||||
|
activeMsgStrIndex = 0
|
||||||
|
state = msgStrInvalid
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save last Translation buffer.
|
// Save last Translation buffer, but do not synthesize an entry after an
|
||||||
po.saveBuffer()
|
// invalid directive. Preserve the historical empty-input entry.
|
||||||
|
if po.parseBufferHasID {
|
||||||
|
po.saveBuffer()
|
||||||
|
} else if len(buf) == 0 {
|
||||||
|
po.domain.translations[""] = po.domain.trBuffer
|
||||||
|
}
|
||||||
|
|
||||||
// Parse headers
|
// Parse headers
|
||||||
po.domain.parseHeaders()
|
po.domain.parseHeaders()
|
||||||
@@ -285,22 +318,30 @@ func (po *Po) Parse(buf []byte) {
|
|||||||
// saveBuffer takes the context and Translation buffers
|
// saveBuffer takes the context and Translation buffers
|
||||||
// and saves it on the translations collection
|
// and saves it on the translations collection
|
||||||
func (po *Po) saveBuffer() {
|
func (po *Po) saveBuffer() {
|
||||||
// With no context...
|
if !po.parseBufferHasID {
|
||||||
if po.domain.ctxBuffer == "" {
|
return
|
||||||
po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer
|
}
|
||||||
} else {
|
|
||||||
// With context...
|
// Store a completed plural ID after all of its continuation lines have
|
||||||
|
// been decoded.
|
||||||
|
if po.parseBufferHasPlural {
|
||||||
|
po.domain.pluralTranslations[po.domain.trBuffer.PluralID] = po.domain.trBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
if po.parseBufferHasContext {
|
||||||
if _, ok := po.domain.contextTranslations[po.domain.ctxBuffer]; !ok {
|
if _, ok := po.domain.contextTranslations[po.domain.ctxBuffer]; !ok {
|
||||||
po.domain.contextTranslations[po.domain.ctxBuffer] = make(map[string]*Translation)
|
po.domain.contextTranslations[po.domain.ctxBuffer] = make(map[string]*Translation)
|
||||||
}
|
}
|
||||||
po.domain.contextTranslations[po.domain.ctxBuffer][po.domain.trBuffer.ID] = po.domain.trBuffer
|
po.domain.contextTranslations[po.domain.ctxBuffer][po.domain.trBuffer.ID] = po.domain.trBuffer
|
||||||
|
// Context applies only to the record it prefixes, even when its ID is empty.
|
||||||
// Cleanup current context buffer if needed
|
po.domain.ctxBuffer = ""
|
||||||
if po.domain.trBuffer.ID != "" {
|
po.parseBufferHasContext = false
|
||||||
po.domain.ctxBuffer = ""
|
} else {
|
||||||
}
|
po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
po.parseBufferHasID = false
|
||||||
|
po.parseBufferHasPlural = false
|
||||||
// Flush Translation buffer
|
// Flush Translation buffer
|
||||||
if po.domain.refBuffer == "" {
|
if po.domain.refBuffer == "" {
|
||||||
po.domain.trBuffer = NewTranslation()
|
po.domain.trBuffer = NewTranslation()
|
||||||
@@ -328,68 +369,106 @@ func (po *Po) parseComment(l string, state parseState) {
|
|||||||
|
|
||||||
// parseContext takes a line starting with "msgctxt",
|
// parseContext takes a line starting with "msgctxt",
|
||||||
// saves the current Translation buffer and creates a new context.
|
// saves the current Translation buffer and creates a new context.
|
||||||
func (po *Po) parseContext(l string) {
|
func (po *Po) parseContext(l string) bool {
|
||||||
|
value, ok := po.parseQuotedDirective(l, "msgctxt")
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Save current Translation buffer.
|
// Save current Translation buffer.
|
||||||
po.saveBuffer()
|
po.saveBuffer()
|
||||||
|
|
||||||
// Buffer context
|
// Buffer context, including an explicitly empty context.
|
||||||
po.domain.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt")))
|
po.domain.ctxBuffer = value
|
||||||
|
po.parseBufferHasContext = true
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseID takes a line starting with "msgid",
|
// parseID takes a line starting with "msgid",
|
||||||
// saves the current Translation and creates a new msgid buffer.
|
// saves the current Translation and creates a new msgid buffer.
|
||||||
func (po *Po) parseID(l string) {
|
func (po *Po) parseID(l string) bool {
|
||||||
|
value, ok := po.parseQuotedDirective(l, "msgid")
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Save current Translation buffer.
|
// Save current Translation buffer.
|
||||||
po.saveBuffer()
|
po.saveBuffer()
|
||||||
|
|
||||||
// Set id
|
// Set id
|
||||||
po.domain.trBuffer.ID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
|
po.domain.trBuffer.ID = value
|
||||||
|
po.parseBufferHasID = true
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePluralID saves the plural id buffer from a line starting with "msgid_plural"
|
// parsePluralID saves the plural id buffer from a line starting with "msgid_plural"
|
||||||
func (po *Po) parsePluralID(l string) {
|
func (po *Po) parsePluralID(l string) bool {
|
||||||
po.domain.trBuffer.PluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
|
value, ok := po.parseQuotedDirective(l, "msgid_plural")
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
po.domain.trBuffer.PluralID = value
|
||||||
|
po.parseBufferHasPlural = true
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseMessage takes a line starting with "msgstr" and saves it into the current buffer.
|
// parseMessage takes a line starting with "msgstr" and saves it into the current buffer.
|
||||||
func (po *Po) parseMessage(l string) {
|
func (po *Po) parseMessage(l string) (int, bool) {
|
||||||
l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr"))
|
if !po.hasKeywordBoundary(l, "msgstr") {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
l = strings.TrimSpace(l[len("msgstr"):])
|
||||||
// Check for indexed Translation forms
|
// Check for indexed Translation forms
|
||||||
if strings.HasPrefix(l, "[") {
|
if strings.HasPrefix(l, "[") {
|
||||||
idx := strings.Index(l, "]")
|
idx := strings.IndexByte(l, ']')
|
||||||
if idx == -1 {
|
if idx <= 1 || !isASCIIPluralIndex(l[1:idx]) {
|
||||||
// Skip wrong index formatting
|
// Skip wrong index formatting
|
||||||
return
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse index
|
// Parse index
|
||||||
i, err := strconv.Atoi(l[1:idx])
|
i, err := strconv.Atoi(l[1:idx])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Skip wrong index formatting
|
// Skip wrong index formatting
|
||||||
return
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Translation string
|
// Parse Translation string
|
||||||
po.domain.trBuffer.Trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
|
clean, ok := decodePOQuotedString(strings.TrimSpace(l[idx+1:]))
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
po.domain.trBuffer.Trs[i] = clean
|
||||||
|
|
||||||
// Loop
|
return i, true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save single Translation form under 0 index
|
// Save single Translation form under 0 index
|
||||||
po.domain.trBuffer.Trs[0], _ = strconv.Unquote(l)
|
clean, ok := decodePOQuotedString(l)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
po.domain.trBuffer.Trs[0] = clean
|
||||||
|
return 0, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseString takes a well formatted string without prefix
|
// parseString takes a well formatted string without prefix
|
||||||
// and creates headers or attach multi-line strings when corresponding
|
// and creates headers or attach multi-line strings when corresponding
|
||||||
func (po *Po) parseString(l string, state parseState) {
|
func (po *Po) parseString(l string, state parseState, activeMsgStrIndex int) bool {
|
||||||
clean, _ := strconv.Unquote(l)
|
if state == msgStrInvalid {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
clean, ok := decodePOQuotedString(l)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
switch state {
|
switch state {
|
||||||
case msgStr:
|
case msgStr:
|
||||||
// Append to last Translation found
|
// Append to active Translation form
|
||||||
po.domain.trBuffer.Trs[len(po.domain.trBuffer.Trs)-1] += clean
|
po.domain.trBuffer.Trs[activeMsgStrIndex] += clean
|
||||||
|
|
||||||
case msgID:
|
case msgID:
|
||||||
// Multiline msgid - Append to current id
|
// Multiline msgid - Append to current id
|
||||||
@@ -402,26 +481,174 @@ func (po *Po) parseString(l string, state parseState) {
|
|||||||
case msgCtxt:
|
case msgCtxt:
|
||||||
// Multiline context - Append to current context
|
// Multiline context - Append to current context
|
||||||
po.domain.ctxBuffer += clean
|
po.domain.ctxBuffer += clean
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidLine checks for line prefixes to detect valid syntax.
|
|
||||||
func (po *Po) isValidLine(l string) bool {
|
func (po *Po) isValidLine(l string) bool {
|
||||||
// Check prefix
|
if strings.HasPrefix(l, "\"") {
|
||||||
valid := []string{
|
return true
|
||||||
"\"",
|
|
||||||
"msgctxt",
|
|
||||||
"msgid",
|
|
||||||
"msgid_plural",
|
|
||||||
"msgstr",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range valid {
|
for _, keyword := range []string{
|
||||||
if strings.HasPrefix(l, v) {
|
"msgctxt",
|
||||||
|
"msgid_plural",
|
||||||
|
"msgid",
|
||||||
|
"msgstr",
|
||||||
|
} {
|
||||||
|
if po.hasKeywordBoundary(l, keyword) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (po *Po) hasKeywordBoundary(l, keyword string) bool {
|
||||||
|
if !strings.HasPrefix(l, keyword) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(l) == len(keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
next := l[len(keyword)]
|
||||||
|
if keyword == "msgstr" && next == '[' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return next == ' ' || next == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
func (po *Po) parseQuotedDirective(l, keyword string) (string, bool) {
|
||||||
|
if !po.hasKeywordBoundary(l, keyword) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := decodePOQuotedString(strings.TrimSpace(l[len(keyword):]))
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodePOQuotedString decodes a GNU PO double-quoted string.
|
||||||
|
func decodePOQuotedString(source string) (string, bool) {
|
||||||
|
if len(source) < 2 || source[0] != '"' {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded []byte
|
||||||
|
for i := 1; i < len(source); {
|
||||||
|
switch source[i] {
|
||||||
|
case '"':
|
||||||
|
if i != len(source)-1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if decoded == nil {
|
||||||
|
return source[1:i], true
|
||||||
|
}
|
||||||
|
return string(decoded), true
|
||||||
|
|
||||||
|
case '\\':
|
||||||
|
if decoded == nil {
|
||||||
|
decoded = make([]byte, 0, len(source)-2)
|
||||||
|
decoded = append(decoded, source[1:i]...)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
if i >= len(source) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch source[i] {
|
||||||
|
case 'a':
|
||||||
|
decoded = append(decoded, '\a')
|
||||||
|
i++
|
||||||
|
case 'b':
|
||||||
|
decoded = append(decoded, '\b')
|
||||||
|
i++
|
||||||
|
case 'f':
|
||||||
|
decoded = append(decoded, '\f')
|
||||||
|
i++
|
||||||
|
case 'n':
|
||||||
|
decoded = append(decoded, '\n')
|
||||||
|
i++
|
||||||
|
case 'r':
|
||||||
|
decoded = append(decoded, '\r')
|
||||||
|
i++
|
||||||
|
case 't':
|
||||||
|
decoded = append(decoded, '\t')
|
||||||
|
i++
|
||||||
|
case 'v':
|
||||||
|
decoded = append(decoded, '\v')
|
||||||
|
i++
|
||||||
|
case '\\':
|
||||||
|
decoded = append(decoded, '\\')
|
||||||
|
i++
|
||||||
|
case '"':
|
||||||
|
decoded = append(decoded, '"')
|
||||||
|
i++
|
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||||
|
var value byte
|
||||||
|
for digits := 0; i < len(source) && digits < 3; digits++ {
|
||||||
|
if source[i] < '0' || source[i] > '7' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
value = value<<3 | (source[i] - '0')
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
decoded = append(decoded, value)
|
||||||
|
case 'x':
|
||||||
|
i++
|
||||||
|
if i >= len(source) || !isPOHexDigit(source[i]) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
var value byte
|
||||||
|
for i < len(source) && isPOHexDigit(source[i]) {
|
||||||
|
value = value<<4 | poHexValue(source[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
decoded = append(decoded, value)
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if decoded != nil {
|
||||||
|
decoded = append(decoded, source[i])
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPOHexDigit(c byte) bool {
|
||||||
|
return c >= '0' && c <= '9' ||
|
||||||
|
c >= 'a' && c <= 'f' ||
|
||||||
|
c >= 'A' && c <= 'F'
|
||||||
|
}
|
||||||
|
|
||||||
|
func poHexValue(c byte) byte {
|
||||||
|
switch {
|
||||||
|
case c >= '0' && c <= '9':
|
||||||
|
return c - '0'
|
||||||
|
case c >= 'a' && c <= 'f':
|
||||||
|
return c - 'a' + 10
|
||||||
|
default:
|
||||||
|
return c - 'A' + 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isASCIIPluralIndex(index string) bool {
|
||||||
|
if index == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(index); i++ {
|
||||||
|
if index[i] < '0' || index[i] > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
+6
-14
@@ -49,11 +49,8 @@ func (t *Translation) Set(str string) {
|
|||||||
|
|
||||||
// Get returns the string of the translation
|
// Get returns the string of the translation
|
||||||
func (t *Translation) Get() string {
|
func (t *Translation) Get() string {
|
||||||
// Look for Translation index 0
|
if tr := t.Trs[0]; tr != "" {
|
||||||
if _, ok := t.Trs[0]; ok {
|
return tr
|
||||||
if t.Trs[0] != "" {
|
|
||||||
return t.Trs[0]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return untranslated id by default
|
// Return untranslated id by default
|
||||||
@@ -68,11 +65,8 @@ func (t *Translation) SetN(n int, str string) {
|
|||||||
|
|
||||||
// GetN returns the string of the plural translation
|
// GetN returns the string of the plural translation
|
||||||
func (t *Translation) GetN(n int) string {
|
func (t *Translation) GetN(n int) string {
|
||||||
// Look for Translation index
|
if tr := t.Trs[n]; tr != "" {
|
||||||
if _, ok := t.Trs[n]; ok {
|
return tr
|
||||||
if t.Trs[n] != "" {
|
|
||||||
return t.Trs[n]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return untranslated singular if corresponding
|
// Return untranslated singular if corresponding
|
||||||
@@ -86,12 +80,10 @@ func (t *Translation) GetN(n int) string {
|
|||||||
|
|
||||||
// IsTranslated reports whether a string is translated
|
// IsTranslated reports whether a string is translated
|
||||||
func (t *Translation) IsTranslated() bool {
|
func (t *Translation) IsTranslated() bool {
|
||||||
tr, ok := t.Trs[0]
|
return t.Trs[0] != ""
|
||||||
return tr != "" && ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTranslatedN reports whether a plural string is translated
|
// IsTranslatedN reports whether a plural string is translated
|
||||||
func (t *Translation) IsTranslatedN(n int) bool {
|
func (t *Translation) IsTranslatedN(n int) bool {
|
||||||
tr, ok := t.Trs[n]
|
return t.Trs[n] != ""
|
||||||
return tr != "" && ok
|
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-14
@@ -9,18 +9,20 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/leonelquinteros/gotext/plurals"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Translator interface is used by Locale and Po objects.Translator
|
// Translator interface is used by Locale and Po objects.
|
||||||
// It contains all methods needed to parse translation sources and obtain corresponding translations.
|
// It contains all methods needed to parse translation sources and obtain corresponding translations.
|
||||||
// Also implements gob.GobEncoder/gob.DobDecoder interfaces to allow serialization of Locale objects.
|
// It includes encoding.BinaryMarshaler and encoding.BinaryUnmarshaler methods for Locale serialization.
|
||||||
type Translator interface {
|
type Translator interface {
|
||||||
ParseFile(f string)
|
ParseFile(f string)
|
||||||
Parse(buf []byte)
|
Parse(buf []byte)
|
||||||
Get(str string, vars ...interface{}) string
|
Get(str string, vars ...any) string
|
||||||
GetN(str, plural string, n int, vars ...interface{}) string
|
GetN(str, plural string, n int, vars ...any) string
|
||||||
GetC(str, ctx string, vars ...interface{}) string
|
GetC(str, ctx string, vars ...any) string
|
||||||
GetNC(str, plural string, n int, ctx string, vars ...interface{}) string
|
GetNC(str, plural string, n int, ctx string, vars ...any) string
|
||||||
|
|
||||||
MarshalBinary() ([]byte, error)
|
MarshalBinary() ([]byte, error)
|
||||||
UnmarshalBinary([]byte) error
|
UnmarshalBinary([]byte) error
|
||||||
@@ -31,10 +33,10 @@ type Translator interface {
|
|||||||
// It contains all methods needed to parse translation sources and to append entries to the object.
|
// It contains all methods needed to parse translation sources and to append entries to the object.
|
||||||
type AppendTranslator interface {
|
type AppendTranslator interface {
|
||||||
Translator
|
Translator
|
||||||
Append(b []byte, str string, vars ...interface{}) []byte
|
Append(b []byte, str string, vars ...any) []byte
|
||||||
AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte
|
AppendN(b []byte, str, plural string, n int, vars ...any) []byte
|
||||||
AppendC(b []byte, str, ctx string, vars ...interface{}) []byte
|
AppendC(b []byte, str, ctx string, vars ...any) []byte
|
||||||
AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte
|
AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslatorEncoding is used as intermediary storage to encode Translator objects to Gob.
|
// TranslatorEncoding is used as intermediary storage to encode Translator objects to Gob.
|
||||||
@@ -63,14 +65,58 @@ type TranslatorEncoding struct {
|
|||||||
// deserialize into a Po-compatible object.
|
// deserialize into a Po-compatible object.
|
||||||
func (te *TranslatorEncoding) GetTranslator() Translator {
|
func (te *TranslatorEncoding) GetTranslator() Translator {
|
||||||
po := NewPo()
|
po := NewPo()
|
||||||
po.domain = NewDomain()
|
if te == nil {
|
||||||
po.domain.Headers = te.Headers
|
return po
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := te.Headers
|
||||||
|
if headers == nil {
|
||||||
|
headers = make(HeaderMap)
|
||||||
|
}
|
||||||
|
translations := te.Translations
|
||||||
|
if translations == nil {
|
||||||
|
translations = make(map[string]*Translation)
|
||||||
|
}
|
||||||
|
for id, translation := range translations {
|
||||||
|
if translation == nil {
|
||||||
|
translation = NewTranslation()
|
||||||
|
translation.ID = id
|
||||||
|
translations[id] = translation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contexts := te.Contexts
|
||||||
|
if contexts == nil {
|
||||||
|
contexts = make(map[string]map[string]*Translation)
|
||||||
|
}
|
||||||
|
for context, translationsForContext := range contexts {
|
||||||
|
if translationsForContext == nil {
|
||||||
|
contexts[context] = make(map[string]*Translation)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for id, translation := range translationsForContext {
|
||||||
|
if translation == nil {
|
||||||
|
translation = NewTranslation()
|
||||||
|
translation.ID = id
|
||||||
|
translationsForContext[id] = translation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
po.domain.Headers = headers
|
||||||
po.domain.Language = te.Language
|
po.domain.Language = te.Language
|
||||||
po.domain.PluralForms = te.PluralForms
|
po.domain.PluralForms = te.PluralForms
|
||||||
po.domain.nplurals = te.Nplurals
|
po.domain.nplurals = te.Nplurals
|
||||||
po.domain.plural = te.Plural
|
po.domain.plural = te.Plural
|
||||||
po.domain.translations = te.Translations
|
po.domain.translations = translations
|
||||||
po.domain.contextTranslations = te.Contexts
|
po.domain.contextTranslations = contexts
|
||||||
|
|
||||||
|
if expr, err := plurals.Compile(te.Plural); err == nil {
|
||||||
|
po.domain.pluralforms = expr
|
||||||
|
}
|
||||||
|
|
||||||
|
po.Headers = po.domain.Headers
|
||||||
|
po.Language = po.domain.Language
|
||||||
|
po.PluralForms = po.domain.PluralForms
|
||||||
|
|
||||||
return po
|
return po
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.27@sha256:e432b43af23a9328d56a7c499be0476810aa344acbcf65fc7c455d4ff5a40602
|
FROM golang:1.27@sha256:820153096fda4de6ac1b7194d12e416b7dd8f4e9a33d17e0345b111b945162d8
|
||||||
|
|
||||||
ENV GOOS=linux
|
ENV GOOS=linux
|
||||||
ENV GOARCH=arm
|
ENV GOARCH=arm
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.27@sha256:e432b43af23a9328d56a7c499be0476810aa344acbcf65fc7c455d4ff5a40602
|
FROM golang:1.27@sha256:820153096fda4de6ac1b7194d12e416b7dd8f4e9a33d17e0345b111b945162d8
|
||||||
|
|
||||||
ENV GOOS=linux
|
ENV GOOS=linux
|
||||||
ENV GOARCH=arm64
|
ENV GOARCH=arm64
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# This is a renovate-friendly source of Docker images.
|
# This is a renovate-friendly source of Docker images.
|
||||||
FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python
|
FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python
|
||||||
FROM otel/weaver:v0.21.2@sha256:2401de985c38bdb98b43918e2f43aa36b2afed4aa5669ac1c1de0a17301cd36d AS weaver
|
FROM otel/weaver:v0.28.0@sha256:5857bb81d8cc7ed608cdcae37c4b459eb020f21dd4acade35f7933cd6762fbcc AS weaver
|
||||||
FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown
|
FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown
|
||||||
|
|||||||
+2
-2
@@ -403,8 +403,8 @@ github.com/klauspost/compress/internal/le
|
|||||||
github.com/klauspost/compress/internal/snapref
|
github.com/klauspost/compress/internal/snapref
|
||||||
github.com/klauspost/compress/zstd
|
github.com/klauspost/compress/zstd
|
||||||
github.com/klauspost/compress/zstd/internal/xxhash
|
github.com/klauspost/compress/zstd/internal/xxhash
|
||||||
# github.com/leonelquinteros/gotext v1.7.2
|
# github.com/leonelquinteros/gotext v1.8.0
|
||||||
## explicit; go 1.23.5
|
## explicit; go 1.27
|
||||||
github.com/leonelquinteros/gotext
|
github.com/leonelquinteros/gotext
|
||||||
github.com/leonelquinteros/gotext/plurals
|
github.com/leonelquinteros/gotext/plurals
|
||||||
# github.com/lucasb-eyer/go-colorful v1.4.0
|
# github.com/lucasb-eyer/go-colorful v1.4.0
|
||||||
|
|||||||
Reference in New Issue
Block a user