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.
This commit is contained in:
Emil Lundberg 2023-11-13 21:04:24 +01:00 committed by GitHub
parent 8f1b0df74b
commit a1ad020b63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 34 additions and 11 deletions

View File

@ -538,6 +538,11 @@
"modified": "modified", "modified": "modified",
"permit": "permit", "permit": "permit",
"seconds": "seconds", "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-black": "Black",
"theme-name-dark": "Dark", "theme-name-dark": "Dark",
"theme-name-default": "Default", "theme-name-default": "Default",

View File

@ -1091,5 +1091,6 @@
<script type="text/javascript" src="syncthing/app.js"></script> <script type="text/javascript" src="syncthing/app.js"></script>
<!-- / gui application code --> <!-- / gui application code -->
<div translate="test.translation.dummy" style="display: none;">(This is just a test string for nested translation namespaces. This does not need to be translated.)</div>
</body> </body>
</html> </html>

View File

@ -26,6 +26,7 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
prefix: 'assets/lang/lang-', prefix: 'assets/lang/lang-',
suffix: '.json' suffix: '.json'
}); });
$translateProvider.fallbackLanguage('en');
LocaleServiceProvider.setAvailableLocales(validLangs); LocaleServiceProvider.setAvailableLocales(validLangs);
LocaleServiceProvider.setDefaultLocale('en'); LocaleServiceProvider.setDefaultLocale('en');

View File

@ -21,7 +21,7 @@ import (
"golang.org/x/net/html" "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 attrRe = regexp.MustCompile(`\{\{\s*'([^']+)'\s+\|\s+translate\s*\}\}`)
var attrReCond = regexp.MustCompile(`\{\{.+\s+\?\s+'([^']+)'\s+:\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) { func generalNode(n *html.Node, filename string) {
translate := false translate := false
translationId := ""
if n.Type == html.ElementNode { if n.Type == html.ElementNode {
if n.Data == "translate" { // for <translate>Text</translate> if n.Data == "translate" { // for <translate>Text</translate>
translate = true translate = true
@ -50,6 +51,7 @@ func generalNode(n *html.Node, filename string) {
for _, a := range n.Attr { for _, a := range n.Attr {
if a.Key == "translate" { if a.Key == "translate" {
translate = true translate = true
translationId = a.Val
} else if a.Key == "id" && (a.Val == "contributor-list" || } else if a.Key == "id" && (a.Val == "contributor-list" ||
a.Val == "copyright-notices") { a.Val == "copyright-notices") {
// Don't translate a list of names and // Don't translate a list of names and
@ -57,11 +59,11 @@ func generalNode(n *html.Node, filename string) {
return return
} else { } else {
for _, matches := range attrRe.FindAllStringSubmatch(a.Val, -1) { for _, matches := range attrRe.FindAllStringSubmatch(a.Val, -1) {
translation(matches[1]) translation("", matches[1])
} }
for _, matches := range attrReCond.FindAllStringSubmatch(a.Val, -1) { for _, matches := range attrReCond.FindAllStringSubmatch(a.Val, -1) {
translation(matches[1]) translation("", matches[1])
translation(matches[2]) translation("", matches[2])
} }
if a.Key == "data-content" && if a.Key == "data-content" &&
!noStringRe.MatchString(a.Val) { !noStringRe.MatchString(a.Val) {
@ -82,16 +84,16 @@ func generalNode(n *html.Node, filename string) {
} }
for c := n.FirstChild; c != nil; c = c.NextSibling { for c := n.FirstChild; c != nil; c = c.NextSibling {
if translate { if translate {
inTranslate(c, filename) inTranslate(c, translationId, filename)
} else { } else {
generalNode(c, filename) generalNode(c, filename)
} }
} }
} }
func inTranslate(n *html.Node, filename string) { func inTranslate(n *html.Node, translationId string, filename string) {
if n.Type == html.TextNode { if n.Type == html.TextNode {
translation(n.Data) translation(translationId, n.Data)
} else { } else {
log.Println("translate node with non-text child < (" + filename + ")") log.Println("translate node with non-text child < (" + filename + ")")
log.Println(n) 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) 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(v, "{%", "{{", -1)
av = strings.Replace(av, "%}", "}}", -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 s := bufio.NewScanner(fd); s.Scan(); {
for _, re := range jsRe { for _, re := range jsRe {
for _, matches := range re.FindAllStringSubmatch(s.Text(), -1) { for _, matches := range re.FindAllStringSubmatch(s.Text(), -1) {
translation(matches[1]) translation("", matches[1])
} }
} }
} }