Skip to content

Commit 2249456

Browse files
donislawdevclaude
andcommitted
gui: the window's words come from a catalogue, so a second language can arrive
D9 gives this window translations and keeps the command line English forever. The text package has been the seam for that since 2026-08-10, and there was nothing underneath it: every word was a constant, so the language was chosen when the binary was compiled. Each of the 75 entries is now a lookup with its English beside it as the default, and a catalogue is embedded with go:embed. English is written once, in the code, next to the reason it is worded that way - en.json is that same English written out again by tools/gen-locale.py so a translator has a complete file to copy, and a guard holds the two together. Nothing about the window changed: all 25 stored screens render byte for byte as they did, without regenerating a single reference. The library is go-i18n rather than the toolkit's wrapper around go-i18n, and that is measured rather than preferred. fyne.io/fyne/v2/lang writes a "Translation failure" line for every message missing from the system language even when handed a fallback - 33 lines from building one screen on a machine set to pl-PL, with correct English on the screen throughout. go-i18n answers with the default and says nothing: the same measurement gives zero. It is not a new module either. The toolkit already requires it, so go.sum did not move. Not here yet, on purpose: asking the machine which language it wants, and a switch in the window. Screens are built once so that a run in flight and everything typed into a form survive moving between tabs, so changing language without a restart is its own piece of work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ccbfe02 commit 2249456

52 files changed

Lines changed: 1327 additions & 560 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ require (
4949
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
5050
github.com/mattn/go-runewidth v0.0.24 // indirect
5151
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
52-
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
52+
github.com/nicksnyder/go-i18n/v2 v2.5.1
5353
github.com/pmezard/go-difflib v1.0.0 // indirect
5454
github.com/rymdport/portal v0.4.2 // indirect
5555
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect

‎internal/guard/actionbar_test.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ import (
3030
func TestTheButtonsThatRunSomethingSitInTheMiddle(t *testing.T) {
3131
_, content := screenOnACanvas(t)
3232

33-
run := buttonNamed(content, text.ButtonGenerate)
33+
run := buttonNamed(content, text.ButtonGenerate())
3434
if run == nil {
35-
t.Fatalf("there is no %q button, so this guard read the wrong tree", text.ButtonGenerate)
35+
t.Fatalf("there is no %q button, so this guard read the wrong tree", text.ButtonGenerate())
3636
}
3737
row := rowHolding(content, run)
3838
if row == nil {
@@ -43,9 +43,9 @@ func TestTheButtonsThatRunSomethingSitInTheMiddle(t *testing.T) {
4343
// layout's arithmetic rather than a statement about where the button is.
4444
const slack = 8
4545

46-
first := buttonNamed(content, text.ButtonPreview)
46+
first := buttonNamed(content, text.ButtonPreview())
4747
if first == nil {
48-
t.Fatalf("there is no %q button, so this guard read the wrong tree", text.ButtonPreview)
48+
t.Fatalf("there is no %q button, so this guard read the wrong tree", text.ButtonPreview())
4949
}
5050

5151
// The group is measured rather than one button, because being centred is a

‎internal/guard/actionbarheight_test.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestTheFormDoesNotMoveWhenARunStarts(t *testing.T) {
7171
},
7272
}
7373

74-
for _, tab := range []string{text.TabOneTarget, text.TabPresets, text.TabRecipe} {
74+
for _, tab := range []string{text.TabOneTarget(), text.TabPresets(), text.TabRecipe()} {
7575
for _, state := range restingStates {
7676
t.Run(tab+"/"+state.name, func(t *testing.T) {
7777
content, w := screenInAWindow(t, tab)
@@ -86,7 +86,7 @@ func TestTheFormDoesNotMoveWhenARunStarts(t *testing.T) {
8686
}
8787

8888
if state.clearTheBox {
89-
box := entryUnder(t, content, text.FieldOutputDir)
89+
box := entryUnder(t, content, text.FieldOutputDir())
9090
if box == nil {
9191
t.Fatalf("the %s screen has no output directory box, so this guard read the wrong tree", tab)
9292
}
@@ -166,11 +166,11 @@ func TestTheFormDoesNotMoveWhenARunStarts(t *testing.T) {
166166
// Asked through Preview because it goes down the same path and writes nothing,
167167
// so this stays a guard about words rather than a guard that generates files.
168168
func TestWhatARunSaysComesBeforeWhatSettlingSaid(t *testing.T) {
169-
content, w, host := screenInAWindowWithHost(t, text.TabPresets)
169+
content, w, host := screenInAWindowWithHost(t, text.TabPresets())
170170

171171
// Nothing is filled in. A note is what the run says about a value nobody
172172
// gave it, so leaving the settings alone is what produces one at all.
173-
press(t, content, text.ButtonPreview)
173+
press(t, content, text.ButtonPreview())
174174
// This preview is ACCEPTED, so it answers from a worker. Joined before the
175175
// status line is read - see join.
176176
join(host)

‎internal/guard/actionrail_test.go‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ import (
3131
// that is on all four - see TestTheDonateButtonIsOnEveryScreen.
3232
func TestWhatIsNotAboutTheRunStandsAtTheEdgeOfTheBar(t *testing.T) {
3333
for _, tab := range []string{
34-
text.TabOneTarget, text.TabPresets, text.TabRecipe, text.TabAbout,
34+
text.TabOneTarget(), text.TabPresets(), text.TabRecipe(), text.TabAbout(),
3535
} {
3636
t.Run(tab, func(t *testing.T) {
3737
content, w := screenInAWindow(t, tab)
3838

39-
donate := buttonNamed(content, text.ButtonDonate)
39+
donate := buttonNamed(content, text.ButtonDonate())
4040
if donate == nil {
4141
t.Fatalf("this screen has no %q button, so this guard read the wrong tree",
42-
text.ButtonDonate)
42+
text.ButtonDonate())
4343
}
4444

4545
atOpening := fyne.CurrentApp().Driver().AbsolutePositionForObject(donate).X
@@ -57,7 +57,7 @@ func TestWhatIsNotAboutTheRunStandsAtTheEdgeOfTheBar(t *testing.T) {
5757
"standing at the edge of the bar.\n"+
5858
"What to do: it belongs in the rail argument of parts.ActionBar, which is laid "+
5959
"outside the form's column. Put back inside that column it moves with it.",
60-
text.ButtonDonate, atOpening, window.OpenSize.Width, whenWider, wider.Width)
60+
text.ButtonDonate(), atOpening, window.OpenSize.Width, whenWider, wider.Width)
6161
}
6262

6363
// The bar's own padding is the only thing that should stand between
@@ -66,7 +66,7 @@ func TestWhatIsNotAboutTheRunStandsAtTheEdgeOfTheBar(t *testing.T) {
6666
if room := float32(parts.ColumnWidth) / 2; atOpening > room {
6767
t.Errorf("%q starts %.0f px from the left edge, which is further in than half a "+
6868
"form column (%.0f px) - it does not read as standing at the edge.",
69-
text.ButtonDonate, atOpening, room)
69+
text.ButtonDonate(), atOpening, room)
7070
}
7171
})
7272
}

‎internal/guard/alwaysvisible_test.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ import (
2323
// It is also checked to go out of use during a run, because pressing it then
2424
// rebuilds the form underneath a run that is writing files.
2525
func TestAddingABatchIsReachableWithoutScrolling(t *testing.T) {
26-
content, _ := screenInAWindow(t, text.TabRecipe)
26+
content, _ := screenInAWindow(t, text.TabRecipe())
2727

28-
add := buttonNamed(content, text.ButtonAddBatch)
28+
add := buttonNamed(content, text.ButtonAddBatch())
2929
if add == nil {
3030
t.Fatalf("the batch screen has no %q button. It has: %v",
31-
text.ButtonAddBatch, buttonNames(content))
31+
text.ButtonAddBatch(), buttonNames(content))
3232
}
3333

3434
scroll := scrollIn(content)
@@ -38,7 +38,7 @@ func TestAddingABatchIsReachableWithoutScrolling(t *testing.T) {
3838
if holds(scroll.Content, add) {
3939
t.Errorf("%q is inside the scrolling part of the batch screen, so it is only reachable "+
4040
"after scrolling past a batch taller than the window - and it is the one control "+
41-
"this screen exists for.", text.ButtonAddBatch)
41+
"this screen exists for.", text.ButtonAddBatch())
4242
}
4343
}
4444

@@ -55,10 +55,10 @@ func TestAddingABatchIsOutOfUseWhileARunIsGoing(t *testing.T) {
5555
body := screen.Object()
5656
fields := screen.Fields()
5757

58-
add := buttonNamed(body, text.ButtonAddBatch)
58+
add := buttonNamed(body, text.ButtonAddBatch())
5959
if add == nil {
6060
t.Fatalf("the batch screen has no %q button. It has: %v",
61-
text.ButtonAddBatch, buttonNames(body))
61+
text.ButtonAddBatch(), buttonNames(body))
6262
}
6363
if add.Disabled() {
6464
t.Fatal("the add button starts disabled, so this guard cannot tell a run from a fresh screen")

‎internal/guard/boxwidth_test.go‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func TestABoxIsWideEnoughForItsOwnPlaceholder(t *testing.T) {
101101
// drift apart in what they mean.
102102
func placeholderShownFor(p format.Property) string {
103103
if p.Default == "" {
104-
return text.PlaceholderWorkedOut
104+
return text.PlaceholderWorkedOut()
105105
}
106106
return text.PlaceholderLeftEmpty(p.Default)
107107
}
@@ -126,8 +126,8 @@ func formatsLaidOut(t *testing.T) (fyne.CanvasObject, *formatChooser) {
126126
t.Cleanup(w.Close)
127127
w.Resize(fyne.NewSize(window.OpenSize.Width, 1600))
128128

129-
generate := tabNamed(t, host.content, text.TabOneTarget)
130-
picker, ok := controlUnder(generate, text.FieldFormat).(*parts.Chooser)
129+
generate := tabNamed(t, host.content, text.TabOneTarget())
130+
picker, ok := controlUnder(generate, text.FieldFormat()).(*parts.Chooser)
131131
if !ok {
132132
t.Fatal("the format field is not a list to choose from, so this guard read the wrong tree")
133133
}
@@ -193,12 +193,12 @@ func TestABoxForANumberIsNotAsWideAsTheFormOnTheBatchScreen(t *testing.T) {
193193
if host.content == nil {
194194
t.Fatal("opening the window put no screen in it")
195195
}
196-
batches := selectTab(t, host.content, text.TabRecipe)
196+
batches := selectTab(t, host.content, text.TabRecipe())
197197
w := test.NewWindow(host.content)
198198
t.Cleanup(w.Close)
199199
w.Resize(fyne.NewSize(window.OpenSize.Width, 1600))
200200

201-
picker, ok := controlUnder(batches, text.FieldFormat).(*parts.Chooser)
201+
picker, ok := controlUnder(batches, text.FieldFormat()).(*parts.Chooser)
202202
if !ok {
203203
t.Fatal("the first batch has no format list, so this guard read the wrong tree")
204204
}
@@ -262,11 +262,11 @@ func TestTwoNarrowSettingsShareARowOnEveryScreenThatDrawsThem(t *testing.T) {
262262
ourTheme(t)
263263
content, _ := laidOutWindow(t)
264264

265-
for _, tab := range []string{text.TabOneTarget, text.TabRecipe} {
265+
for _, tab := range []string{text.TabOneTarget(), text.TabRecipe()} {
266266
t.Run(tab, func(t *testing.T) {
267267
screen := tabContent(t, content, tab)
268268

269-
picker, ok := controlUnder(screen, text.FieldFormat).(*parts.Chooser)
269+
picker, ok := controlUnder(screen, text.FieldFormat()).(*parts.Chooser)
270270
if !ok {
271271
t.Fatal("this screen has no format list, so this guard read the wrong tree")
272272
}

‎internal/guard/bytecount_test.go‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
func TestABoxHoldingASizeSaysWhatItComesTo(t *testing.T) {
2828
_, content := screen(t)
2929

30-
count := byteCountBeside(t, content, text.FieldSize)
30+
count := byteCountBeside(t, content, text.FieldSize())
3131
if count.Text == "" {
3232
t.Fatal("the size box opens with 10mb in it and nothing says what that comes to")
3333
}
@@ -42,14 +42,14 @@ func TestABoxHoldingASizeSaysWhatItComesTo(t *testing.T) {
4242
// It follows the box rather than being set once. A count that stopped at
4343
// the opening value would be worse than none: it would be a wrong number
4444
// beside a right one.
45-
fill(t, content, text.FieldSize, "1kb")
45+
fill(t, content, text.FieldSize(), "1kb")
4646
if want := text.ExactBytes(1024); count.Text != want {
4747
t.Errorf("1kb was typed and the count beside the box reads %q, not %q", count.Text, want)
4848
}
4949

5050
// And says nothing at all about something that is not a size, which is
5151
// every box somebody is halfway through typing.
52-
fill(t, content, text.FieldSize, "abc")
52+
fill(t, content, text.FieldSize(), "abc")
5353
if count.Text != "" {
5454
t.Errorf("the size box holds something that is not a size and the count reads %q, "+
5555
"so it is showing a number for a value the run will refuse", count.Text)
@@ -68,15 +68,15 @@ func TestABoxHoldingASizeSaysWhatItComesTo(t *testing.T) {
6868
func TestOnlyABoxHoldingASizeCarriesACount(t *testing.T) {
6969
_, content := screen(t)
7070

71-
for _, label := range []string{text.FieldCount, text.FieldSeed, text.FieldTargetID, text.FieldNameTemplate} {
71+
for _, label := range []string{text.FieldCount(), text.FieldSeed(), text.FieldTargetID(), text.FieldNameTemplate()} {
7272
if count := byteCountIn(fieldBox(content, label)); count != nil {
7373
t.Errorf("%q does not hold a size and carries a count reading %q", label, count.Text)
7474
}
7575
}
7676
// And the one that does still has it, so this cannot pass by there being no
7777
// counts anywhere.
78-
if byteCountIn(fieldBox(content, text.FieldSize)) == nil {
79-
t.Errorf("%q holds a size and carries no count, so the check above proved nothing", text.FieldSize)
78+
if byteCountIn(fieldBox(content, text.FieldSize())) == nil {
79+
t.Errorf("%q holds a size and carries no count, so the check above proved nothing", text.FieldSize())
8080
}
8181
}
8282

@@ -118,7 +118,7 @@ func byteCountIn(o fyne.CanvasObject) *parts.ByteCount {
118118
func TestADeclaredSizeSaysWhatItComesToOnEveryScreenThatDrawsOne(t *testing.T) {
119119
t.Run("a format setting", func(t *testing.T) {
120120
_, content := screen(t)
121-
picker, ok := controlUnder(content, text.FieldFormat).(*parts.Chooser)
121+
picker, ok := controlUnder(content, text.FieldFormat()).(*parts.Chooser)
122122
if !ok {
123123
t.Fatal("this screen has no format list, so this guard read the wrong tree")
124124
}

0 commit comments

Comments
 (0)