• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fyne-io / fyne / 18808454873

25 Oct 2025 09:01PM UTC coverage: 61.62% (+0.6%) from 61.061%
18808454873

Pull #5776

github

Jacalz
Merge remote-tracking branch 'fyne/develop' into cleanup-cmd-fyne
Pull Request #5776: cmd/fyne: Remove everything but the public APIs

0 of 4 new or added lines in 2 files covered. (0.0%)

167 existing lines in 3 files now uncovered.

25475 of 41342 relevant lines covered (61.62%)

703.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/cmd/fyne/internal/commands/translate.go
1
package commands
2

3
import (
4
        "encoding/json"
5
        "errors"
6
        "fmt"
7
        "go/ast"
8
        "go/importer"
9
        "go/parser"
10
        "go/token"
11
        "go/types"
12
        "io"
13
        "io/fs"
14
        "os"
15
        "path/filepath"
16
        "strconv"
17

18
        "github.com/natefinch/atomic"
19
        "github.com/urfave/cli/v2"
20
)
21

22
// Translate returns the cli command to scan for new translation strings.
23
//
24
// Since: 2.6
UNCOV
25
func Translate() *cli.Command {
×
UNCOV
26
        return &cli.Command{
×
UNCOV
27
                Name:      "translate",
×
UNCOV
28
                Usage:     "Scans for new translation strings.",
×
UNCOV
29
                ArgsUsage: "translationsFile [source ...]",
×
UNCOV
30
                Description: "Recursively scans the current or given directories/files for \n" +
×
UNCOV
31
                        "translation strings, and creates or updates the translations file.",
×
UNCOV
32
                Flags: []cli.Flag{
×
UNCOV
33
                        &cli.BoolFlag{
×
UNCOV
34
                                Name:    "imports",
×
UNCOV
35
                                Aliases: []string{"i"},
×
UNCOV
36
                                Usage:   "Additionally scan all imports (slow).",
×
UNCOV
37
                        },
×
UNCOV
38
                        &cli.BoolFlag{
×
UNCOV
39
                                Name:    "update",
×
UNCOV
40
                                Aliases: []string{"u"},
×
UNCOV
41
                                Usage:   "Update existing translations (use with care).",
×
UNCOV
42
                        },
×
UNCOV
43
                        &cli.BoolFlag{
×
UNCOV
44
                                Name:    "verbose",
×
UNCOV
45
                                Aliases: []string{"v"},
×
UNCOV
46
                                Usage:   "Show files that are being scanned etc.",
×
UNCOV
47
                        },
×
UNCOV
48
                },
×
UNCOV
49
                Action: func(ctx *cli.Context) error {
×
50
                        opts := translateOpts{
×
51
                                Imports: ctx.Bool("imports"),
×
52
                                Update:  ctx.Bool("update"),
×
53
                                Verbose: ctx.Bool("verbose"),
×
54
                        }
×
55

×
56
                        usage := func() {
×
57
                                fmt.Fprintf(os.Stderr, "usage: %s %s [command options] %s\n",
×
58
                                        ctx.App.Name,
×
59
                                        ctx.Command.Name,
×
60
                                        ctx.Command.ArgsUsage,
×
61
                                )
×
62
                                os.Exit(1)
×
63
                        }
×
64

65
                        translationsFile := ctx.Args().First()
×
66
                        if translationsFile == "" {
×
67
                                fmt.Fprintln(os.Stderr, "Missing argument: translationsFile")
×
68
                                usage()
×
69
                        }
×
70

71
                        if translationsFile != "-" && filepath.Ext(translationsFile) != ".json" {
×
72
                                fmt.Fprintln(os.Stderr, "Need .json extension for translationsFile")
×
73
                                usage()
×
74
                        }
×
75

76
                        sources, err := findSources(ctx.Args().Tail(), ".go", ".")
×
77
                        if err != nil {
×
78
                                return err
×
79
                        }
×
80

81
                        return updateTranslationsFile(translationsFile, sources, &opts)
×
82
                },
83
        }
84
}
85

86
// Takes the files/directories, recursively scans them for files with the given extension,
87
// and returns them. Uses the current directory when used with an empty list.
UNCOV
88
func findSources(files []string, ext, cur string) ([]string, error) {
×
UNCOV
89
        if len(files) == 0 {
×
UNCOV
90
                return findFilesExt(cur, ext)
×
UNCOV
91
        }
×
92

93
        sources := []string{}
×
94
        for _, file := range files {
×
95
                if filepath.Ext(file) == ext {
×
96
                        sources = append(sources, file)
×
97
                        continue
×
98
                }
99
                files, err := findFilesExt(file, ext)
×
100
                if err != nil {
×
101
                        return nil, err
×
102
                }
×
103
                sources = append(sources, files...)
×
104
        }
105

106
        return sources, nil
×
107
}
108

109
// Recursively walk the given directory and return all files with the matching extension
UNCOV
110
func findFilesExt(dir, ext string) ([]string, error) {
×
UNCOV
111
        files := []string{}
×
UNCOV
112
        err := filepath.Walk(dir, func(path string, fi fs.FileInfo, err error) error {
×
UNCOV
113
                if err != nil {
×
UNCOV
114
                        if errors.Is(err, os.ErrNotExist) {
×
UNCOV
115
                                return nil
×
UNCOV
116
                        }
×
117
                        return err
×
118
                }
119

UNCOV
120
                if filepath.Ext(path) != ext || fi.IsDir() || !fi.Mode().IsRegular() {
×
UNCOV
121
                        return nil
×
UNCOV
122
                }
×
123

UNCOV
124
                files = append(files, path)
×
UNCOV
125

×
UNCOV
126
                return nil
×
127
        })
UNCOV
128
        return files, err
×
129
}
130

131
type translateOpts struct {
132
        Imports bool
133
        Update  bool
134
        Verbose bool
135
}
136

137
// Create or add to translations file by scanning the given files for translation calls.
138
// Works with and without existing translations file.
UNCOV
139
func updateTranslationsFile(file string, files []string, opts *translateOpts) error {
×
UNCOV
140
        translations := make(map[string]any)
×
UNCOV
141

×
UNCOV
142
        f, err := os.Open(file)
×
UNCOV
143
        if err != nil && !errors.Is(err, os.ErrNotExist) {
×
144
                return err
×
145
        }
×
146

UNCOV
147
        if f != nil {
×
UNCOV
148
                dec := json.NewDecoder(f)
×
UNCOV
149
                err := dec.Decode(&translations)
×
UNCOV
150
                f.Close()
×
UNCOV
151
                if err != nil && err != io.EOF {
×
152
                        return err
×
153
                }
×
154
        }
155

UNCOV
156
        if opts.Verbose {
×
157
                fmt.Fprintf(os.Stderr, "scanning files: %v\n", files)
×
158
        }
×
159

UNCOV
160
        if err := updateTranslationsHash(translations, files, opts); err != nil {
×
161
                return err
×
162
        }
×
163

UNCOV
164
        if len(translations) == 0 {
×
165
                if opts.Verbose {
×
166
                        fmt.Fprintln(os.Stderr, "no translations found")
×
167
                }
×
168
                return nil
×
169
        }
170

UNCOV
171
        b, err := json.MarshalIndent(translations, "", "    ")
×
UNCOV
172
        if err != nil {
×
173
                return err
×
174
        }
×
175

UNCOV
176
        if file == "-" {
×
177
                fmt.Printf("%s\n", string(b))
×
178
                return nil
×
179
        }
×
180

UNCOV
181
        return writeTranslationsFile(b, file)
×
182
}
183

184
// Write data to given file and rename atomically to prevent file corruption
UNCOV
185
func writeTranslationsFile(b []byte, file string) error {
×
UNCOV
186
        nf, err := os.CreateTemp(filepath.Dir(file), filepath.Base(file)+"-*")
×
UNCOV
187
        if err != nil {
×
188
                return err
×
189
        }
×
190

UNCOV
191
        n, err := nf.Write(b)
×
UNCOV
192
        if err != nil {
×
193
                return err
×
194
        }
×
195

UNCOV
196
        if n < len(b) {
×
197
                return io.ErrShortWrite
×
198
        }
×
199

UNCOV
200
        if err := nf.Close(); err != nil {
×
201
                return err
×
202
        }
×
203

UNCOV
204
        return atomic.ReplaceFile(nf.Name(), file)
×
205
}
206

207
// Update translations hash by scanning the given files, then parsing and walking the AST
UNCOV
208
func updateTranslationsHash(m map[string]any, srcs []string, opts *translateOpts) error {
×
UNCOV
209
        fset := token.NewFileSet()
×
UNCOV
210
        specs := []*ast.ImportSpec{}
×
UNCOV
211

×
UNCOV
212
        for _, src := range srcs {
×
UNCOV
213
                af, err := parser.ParseFile(fset, src, nil, parser.AllErrors)
×
UNCOV
214
                if err != nil {
×
215
                        return err
×
216
                }
×
217

UNCOV
218
                specs = append(specs, af.Imports...)
×
219
        }
220

UNCOV
221
        if opts.Imports {
×
222
                if opts.Verbose {
×
223
                        fmt.Fprintf(os.Stderr, "loading imports ...\n")
×
224
                }
×
225

226
                imp := importer.ForCompiler(fset, "source", nil)
×
227
                for _, spec := range specs {
×
228
                        if err := handleImport(imp, spec, opts); err != nil {
×
229
                                return err
×
230
                        }
×
231
                }
232
        }
233

UNCOV
234
        if opts.Verbose {
×
235
                fmt.Fprintf(os.Stderr, "scanning code ...\n")
×
236
        }
×
237

UNCOV
238
        var r error
×
UNCOV
239
        seen := make(map[string]bool)
×
UNCOV
240
        fset.Iterate(func(f *token.File) bool {
×
UNCOV
241
                fname := f.Name()
×
UNCOV
242
                if seen[fname] {
×
UNCOV
243
                        return false
×
UNCOV
244
                }
×
UNCOV
245
                seen[fname] = true
×
UNCOV
246

×
UNCOV
247
                af, err := parser.ParseFile(fset, fname, nil, parser.AllErrors)
×
UNCOV
248
                if err != nil {
×
249
                        if filepath.Base(fname) == "_cgo_gotypes.go" {
×
250
                                return true
×
251
                        }
×
252
                        r = err
×
253
                        return false
×
254
                }
255

UNCOV
256
                ast.Walk(&visitor{opts: opts, m: m}, af)
×
UNCOV
257

×
UNCOV
258
                return true
×
259
        })
260

UNCOV
261
        return r
×
262
}
263

264
// Imports given paths to add to fset
265
func handleImport(imp types.Importer, spec *ast.ImportSpec, opts *translateOpts) error {
×
266
        path, err := strconv.Unquote(spec.Path.Value)
×
267
        if err != nil {
×
268
                return err
×
269
        }
×
270

271
        if opts.Verbose {
×
272
                fmt.Fprintf(os.Stderr, "importing: %s\n", path)
×
273
        }
×
274

275
        _, err = imp.Import(path)
×
276
        if err != nil {
×
277
                return err
×
278
        }
×
279

280
        return nil
×
281
}
282

283
// Visitor pattern with state machine to find translations fallback (and key)
284
type visitor struct {
285
        opts     *translateOpts
286
        state    stateFn
287
        name     string
288
        key      string
289
        fallback string
290
        m        map[string]any
291
}
292

293
// Method to walk AST using interface for ast.Walk
UNCOV
294
func (v *visitor) Visit(node ast.Node) ast.Visitor {
×
UNCOV
295
        if node == nil {
×
UNCOV
296
                return nil
×
UNCOV
297
        }
×
298

UNCOV
299
        if v.state == nil {
×
UNCOV
300
                v.state = translateNew
×
UNCOV
301
                v.name = ""
×
UNCOV
302
                v.key = ""
×
UNCOV
303
                v.fallback = ""
×
UNCOV
304
        }
×
305

UNCOV
306
        v.state = v.state(v, node)
×
UNCOV
307

×
UNCOV
308
        return v
×
309
}
310

311
// State machine to pick out translation key and fallback from AST
312
type stateFn func(*visitor, ast.Node) stateFn
313

314
// All translation calls need to start with the literal "lang"
UNCOV
315
func translateNew(v *visitor, node ast.Node) stateFn {
×
UNCOV
316
        ident, ok := node.(*ast.Ident)
×
UNCOV
317
        if !ok {
×
UNCOV
318
                return nil
×
UNCOV
319
        }
×
320

UNCOV
321
        if ident.Name != "lang" {
×
UNCOV
322
                return nil
×
UNCOV
323
        }
×
324

UNCOV
325
        return translateCall
×
326
}
327

328
// A known translation method needs to be used. The two supported cases are:
329
// - simple cases (L, N): only the first argument is relevant
330
// - more complex cases (X, XN): first two arguments matter
UNCOV
331
func translateCall(v *visitor, node ast.Node) stateFn {
×
UNCOV
332
        ident, ok := node.(*ast.Ident)
×
UNCOV
333
        if !ok {
×
UNCOV
334
                return nil
×
UNCOV
335
        }
×
336

UNCOV
337
        v.name = ident.Name
×
UNCOV
338

×
UNCOV
339
        switch ident.Name {
×
340
        case "L", "Localize":
×
341
                return translateLocalize
×
342
        case "N", "LocalizePlural":
×
343
                return translateLocalize
×
UNCOV
344
        case "X", "LocalizeKey":
×
UNCOV
345
                return translateKey
×
346
        case "XN", "LocalizePluralKey":
×
347
                return translateKey
×
348
        }
349

350
        return nil
×
351
}
352

353
// Parse first argument, use string as key and fallback, and finish
UNCOV
354
func translateLocalize(v *visitor, node ast.Node) stateFn {
×
UNCOV
355
        basiclit, ok := node.(*ast.BasicLit)
×
UNCOV
356
        if !ok {
×
UNCOV
357
                return nil
×
UNCOV
358
        }
×
359

UNCOV
360
        val, err := strconv.Unquote(basiclit.Value)
×
UNCOV
361
        if err != nil {
×
362
                return nil
×
363
        }
×
364

UNCOV
365
        v.key = val
×
UNCOV
366
        v.fallback = val
×
UNCOV
367

×
UNCOV
368
        return translateFinish(v)
×
369
}
370

371
// Parse first argument and use as key
UNCOV
372
func translateKey(v *visitor, node ast.Node) stateFn {
×
UNCOV
373
        basiclit, ok := node.(*ast.BasicLit)
×
UNCOV
374
        if !ok {
×
375
                return nil
×
376
        }
×
377

UNCOV
378
        val, err := strconv.Unquote(basiclit.Value)
×
UNCOV
379
        if err != nil {
×
380
                return nil
×
381
        }
×
382

UNCOV
383
        v.key = val
×
UNCOV
384

×
UNCOV
385
        return translateKeyFallback
×
386
}
387

388
// Parse second argument and use as fallback, and finish
UNCOV
389
func translateKeyFallback(v *visitor, node ast.Node) stateFn {
×
UNCOV
390
        basiclit, ok := node.(*ast.BasicLit)
×
UNCOV
391
        if !ok {
×
392
                return nil
×
393
        }
×
394

UNCOV
395
        val, err := strconv.Unquote(basiclit.Value)
×
UNCOV
396
        if err != nil {
×
397
                return nil
×
398
        }
×
399

UNCOV
400
        v.fallback = val
×
UNCOV
401

×
UNCOV
402
        return translateFinish(v)
×
403
}
404

405
// Finish scan for translation and add to translation hash with the right type (singular or plural).
406
// Only adding new keys, ignoring changed or removed ones.
407
// Removing is potentially dangerous as there could be dynamic keys that get removed.
408
// By default ignore existing translations to prevent accidental overwriting.
UNCOV
409
func translateFinish(v *visitor) stateFn {
×
UNCOV
410
        _, found := v.m[v.key]
×
UNCOV
411
        if found {
×
UNCOV
412
                if !v.opts.Update {
×
UNCOV
413
                        if v.opts.Verbose {
×
414
                                fmt.Fprintf(os.Stderr, "ignoring: %s\n", v.key)
×
415
                        }
×
UNCOV
416
                        return nil
×
417
                }
418
                if v.opts.Verbose {
×
419
                        fmt.Fprintf(os.Stderr, "updating: %s\n", v.key)
×
420
                }
×
UNCOV
421
        } else {
×
UNCOV
422
                if v.opts.Verbose {
×
423
                        fmt.Fprintf(os.Stderr, "adding: %s\n", v.key)
×
424
                }
×
425
        }
426

UNCOV
427
        switch v.name {
×
428
        case "LocalizePlural", "LocalizePluralKey", "N", "XN":
×
429
                m := make(map[string]string)
×
430
                m["other"] = v.fallback
×
431
                v.m[v.key] = m
×
UNCOV
432
        default:
×
UNCOV
433
                v.m[v.key] = v.fallback
×
434
        }
435

UNCOV
436
        return nil
×
437
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc