From a1ad020b6387f851764cb9a6e97253df85cce12c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 13 Nov 2023 21:04:24 +0100 Subject: [PATCH] Support explicit translation ID and dotted namespaces in translation extraction (#9192) Some translations, especially single words or other short labels for buttons and the like, may not be transferable between contexts even if they happen to be equal in English. In these cases, setting an explicit translation ID is important for context separation. Angular Translate also supports nested JSON in translation tables, addressed using `.` as namespace separator; this enhancement makes use of this when extracting translation with an explicit translation ID. --- gui/default/assets/lang/lang-en.json | 5 ++++ gui/default/index.html | 1 + gui/default/syncthing/app.js | 1 + script/translate.go | 38 ++++++++++++++++++++-------- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/gui/default/assets/lang/lang-en.json b/gui/default/assets/lang/lang-en.json index 4a43240d8..0e7acfc04 100644 --- a/gui/default/assets/lang/lang-en.json +++ b/gui/default/assets/lang/lang-en.json @@ -538,6 +538,11 @@ "modified": "modified", "permit": "permit", "seconds": "seconds", + "test": { + "translation": { + "dummy": "(This is just a test string for nested translation namespaces. This does not need to be translated.)" + } + }, "theme-name-black": "Black", "theme-name-dark": "Dark", "theme-name-default": "Default", diff --git a/gui/default/index.html b/gui/default/index.html index 4c55ffd7b..186ae5321 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -1091,5 +1091,6 @@ +
(This is just a test string for nested translation namespaces. This does not need to be translated.)
diff --git a/gui/default/syncthing/app.js b/gui/default/syncthing/app.js index 6c85be499..80e9a20d7 100644 --- a/gui/default/syncthing/app.js +++ b/gui/default/syncthing/app.js @@ -26,6 +26,7 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi prefix: 'assets/lang/lang-', suffix: '.json' }); + $translateProvider.fallbackLanguage('en'); LocaleServiceProvider.setAvailableLocales(validLangs); LocaleServiceProvider.setDefaultLocale('en'); diff --git a/script/translate.go b/script/translate.go index 69622b606..1d182ebe2 100644 --- a/script/translate.go +++ b/script/translate.go @@ -21,7 +21,7 @@ import ( "golang.org/x/net/html" ) -var trans = make(map[string]string) +var trans = make(map[string]interface{}) var attrRe = regexp.MustCompile(`\{\{\s*'([^']+)'\s+\|\s+translate\s*\}\}`) var attrReCond = regexp.MustCompile(`\{\{.+\s+\?\s+'([^']+)'\s+:\s+'([^']+)'\s+\|\s+translate\s*\}\}`) @@ -41,6 +41,7 @@ var aboutRe = regexp.MustCompile(`^([^/]+/[^/]+|(The Go Pro|Font Awesome ).+|Bui func generalNode(n *html.Node, filename string) { translate := false + translationId := "" if n.Type == html.ElementNode { if n.Data == "translate" { // for Text translate = true @@ -50,6 +51,7 @@ func generalNode(n *html.Node, filename string) { for _, a := range n.Attr { if a.Key == "translate" { translate = true + translationId = a.Val } else if a.Key == "id" && (a.Val == "contributor-list" || a.Val == "copyright-notices") { // Don't translate a list of names and @@ -57,11 +59,11 @@ func generalNode(n *html.Node, filename string) { return } else { for _, matches := range attrRe.FindAllStringSubmatch(a.Val, -1) { - translation(matches[1]) + translation("", matches[1]) } for _, matches := range attrReCond.FindAllStringSubmatch(a.Val, -1) { - translation(matches[1]) - translation(matches[2]) + translation("", matches[1]) + translation("", matches[2]) } if a.Key == "data-content" && !noStringRe.MatchString(a.Val) { @@ -82,16 +84,16 @@ func generalNode(n *html.Node, filename string) { } for c := n.FirstChild; c != nil; c = c.NextSibling { if translate { - inTranslate(c, filename) + inTranslate(c, translationId, filename) } else { generalNode(c, filename) } } } -func inTranslate(n *html.Node, filename string) { +func inTranslate(n *html.Node, translationId string, filename string) { if n.Type == html.TextNode { - translation(n.Data) + translation(translationId, n.Data) } else { log.Println("translate node with non-text child < (" + filename + ")") log.Println(n) @@ -102,12 +104,26 @@ func inTranslate(n *html.Node, filename string) { } } -func translation(v string) { +func translation(id string, v string) { + namespace := trans + idParts := strings.Split(id, ".") + id = idParts[len(idParts)-1] + for _, subNamespace := range idParts[0 : len(idParts)-1] { + if _, ok := namespace[subNamespace]; !ok { + namespace[subNamespace] = make(map[string]interface{}) + } + namespace = namespace[subNamespace].(map[string]interface{}) + } + v = strings.TrimSpace(v) - if _, ok := trans[v]; !ok { + if id == "" { + id = v + } + + if _, ok := namespace[id]; !ok { av := strings.Replace(v, "{%", "{{", -1) av = strings.Replace(av, "%}", "}}", -1) - trans[v] = av + namespace[id] = av } } @@ -136,7 +152,7 @@ func walkerFor(basePath string) filepath.WalkFunc { for s := bufio.NewScanner(fd); s.Scan(); { for _, re := range jsRe { for _, matches := range re.FindAllStringSubmatch(s.Text(), -1) { - translation(matches[1]) + translation("", matches[1]) } } }