Release / Build and publish image (push) Failing after 10m6s
Reported from use: "Fetch now" said "something went wrong" and the error log was empty. Both halves were the same bug wearing two hats. `handleGmailSyncNow` answered 502 with the code `internal_error`, which the interface maps to a generic sentence — so Google's own words, which are the whole diagnosis, were replaced by one that says nothing. And `writeError` records nothing; only `s.internal` does. So there was no message to read and nowhere to look it up afterwards. `s.upstream()` is the answer: 502 because this is somebody else's service saying no rather than this server breaking, a code of its own so the interface can quote what was said, and a row in the error log — which is the entire reason that log exists. A Gmail sync failing quietly for a week is otherwise invisible: the background job writes a line the next restart erases. The same connection also left the mailbox unnamed. The address is learned at connect time and that call can fail; a warning went to the log and an empty string to the database, so the page said "an unknown account" and kept saying it. The only way back was to disconnect and reconnect, which nobody would guess. The status endpoint asks again when it finds none, so a wrong answer lasts until the next look rather than until somebody reconnects. Ten Danish strings were hiding in `confirm()` and `app.toast()` calls, including every destructive confirmation in the app — the worst possible set to leave in one language, since it is what somebody reads in the half-second before agreeing to lose something. The i18n guard reads markup and attributes, and a string argument lives in the script block. It reads those now, and found four my own grep missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
4.9 KiB
Go
121 lines
4.9 KiB
Go
package i18n
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Danish prose left in a component is the failure the dictionaries cannot see.
|
|
//
|
|
// `i18n_test.go` checks that the two dictionaries agree. It cannot check the thing
|
|
// that actually went wrong: a string that was never put in either, because nobody
|
|
// noticed it. Three survived the sweep — "Ingen nyere version fundet.", "Ikke mere
|
|
// i dag." and a token hint split across two lines — and they survived it for the
|
|
// same reason. The sweep grepped for æøå, and none of them contain one.
|
|
//
|
|
// So this reads the templates instead and looks for the words that give Danish
|
|
// away whatever letters it happens to use. A word list rather than a language
|
|
// detector: this repository ships one static binary, and a list of thirty common
|
|
// words catches the sentences a person would actually write.
|
|
// The same words, inside the code rather than the markup.
|
|
//
|
|
// `confirm()`, `alert()` and `app.toast()` take a string argument, and a string
|
|
// argument lives in the script block where the scanner above never looks. Six of
|
|
// them survived the whole i18n sweep that way — every destructive confirmation in
|
|
// the app, which is the worst possible set to leave in one language, because it is
|
|
// the text somebody reads in the half-second before they agree to lose something.
|
|
func TestNoDanishProseInDialogsAndToasts(t *testing.T) {
|
|
// The argument of one of the three, quoted or backticked. Deliberately narrow:
|
|
// a general scan of every string literal in a Svelte script block would drown
|
|
// in class names, keys and query fragments.
|
|
call := regexp.MustCompile(`(?:confirm|alert|app\.toast)\(\s*['` + "`" + `]([^'` + "`" + `]{4,})['` + "`" + `]`)
|
|
danish := regexp.MustCompile(`(?i)\b(og|er|ikke|det|den|der|til|kan|som|har|med|af|en|et|du|din|dine|vises|ingen|nyere|fundet|ude|valgt|slettet|gemt|opgave|projekt|indstillinger|fjern|opret|vælg|luk|tilføj|ryd|gem|slet|omdøb|bliver|holder|alle)\b`)
|
|
|
|
var found []string
|
|
root := filepath.Join("..", "..", "web", "src")
|
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
return err
|
|
}
|
|
if !strings.HasSuffix(path, ".svelte") && !strings.HasSuffix(path, ".js") {
|
|
return nil
|
|
}
|
|
if strings.Contains(path, "locales") {
|
|
return nil
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, _ := filepath.Rel(root, path)
|
|
for _, m := range call.FindAllStringSubmatch(string(body), -1) {
|
|
if danish.MatchString(m[1]) {
|
|
found = append(found, rel+": "+m[1])
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk %s: %v", root, err)
|
|
}
|
|
|
|
if len(found) > 0 {
|
|
t.Errorf("%d dialog or toast string(s) are written in Danish inside the code. "+
|
|
"They are what somebody reads before agreeing to lose something, and an "+
|
|
"English interface shows them in Danish:\n %s",
|
|
len(found), strings.Join(found, "\n "))
|
|
}
|
|
}
|
|
|
|
func TestNoDanishProseOutsideTheDictionaries(t *testing.T) {
|
|
// Text nodes of three characters or more, and the three attributes a person
|
|
// reads. Anything inside {…} is already an expression — usually a t() call —
|
|
// and is not what this is looking for.
|
|
text := regexp.MustCompile(`>\s*([^<>{}\n][^<>{}]{3,})\s*<`)
|
|
attrs := regexp.MustCompile(`(?:aria-label|title|placeholder)="([^"{][^"]{3,})"`)
|
|
// Words that are Danish and are not also English. "is", "for" and "to" would
|
|
// match English prose and are deliberately absent.
|
|
danish := regexp.MustCompile(`(?i)\b(og|er|ikke|det|den|der|til|kan|som|har|med|af|en|et|du|din|dine|vises|ingen|nyere|fundet|ude|valgt|slettet|gemt|opgave|projekt|indstillinger|fjern|opret|vælg|luk|tilføj|ryd|gem|slet|omdøb)\b`)
|
|
comments := regexp.MustCompile(`(?s)<!--.*?-->`)
|
|
styles := regexp.MustCompile(`(?s)<style>.*`)
|
|
|
|
var found []string
|
|
root := filepath.Join("..", "..", "web", "src")
|
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".svelte") {
|
|
return err
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
markup := string(body)
|
|
if at := strings.Index(markup, "</script>"); at > 0 {
|
|
markup = markup[at:]
|
|
}
|
|
markup = styles.ReplaceAllString(comments.ReplaceAllString(markup, ""), "")
|
|
|
|
rel, _ := filepath.Rel(root, path)
|
|
for _, m := range append(text.FindAllStringSubmatch(markup, -1),
|
|
attrs.FindAllStringSubmatch(markup, -1)...) {
|
|
phrase := strings.TrimSpace(m[1])
|
|
if danish.MatchString(phrase) {
|
|
found = append(found, rel+": "+strings.Join(strings.Fields(phrase), " "))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk %s: %v", root, err)
|
|
}
|
|
|
|
if len(found) > 0 {
|
|
t.Errorf("%d Danish string(s) are written into a component instead of going through "+
|
|
"t(). An English interface shows them in Danish, and neither dictionary knows "+
|
|
"they exist:\n %s", len(found), strings.Join(found, "\n "))
|
|
}
|
|
}
|