From c4b4ab18a208509131f4a71aa023a68ee9cd81d0 Mon Sep 17 00:00:00 2001 From: Andrew Pam Date: Mon, 23 Sep 2024 22:16:26 +1000 Subject: [PATCH] Implement text translations --- edits/edits.go | 33 +++++++++++++-------------- edits/edits_test.go | 3 ++- edits/help.go | 19 +++++----------- edits/help_test.go | 7 +++--- go.mod | 1 + go.sum | 2 ++ i18n/help.de | 15 +++++++++++++ i18n/help.jp | 15 +++++++++++++ i18n/i18n.go | 54 +++++++++++++++++++++++++++++++++++++++++++++ i18n/text.de | 6 +++++ i18n/text.en | 6 +++++ i18n/text.jp | 6 +++++ jotty.go | 14 +++++------- 13 files changed, 136 insertions(+), 45 deletions(-) create mode 100644 i18n/help.de create mode 100644 i18n/help.jp create mode 100644 i18n/i18n.go create mode 100644 i18n/text.de create mode 100644 i18n/text.en create mode 100644 i18n/text.jp diff --git a/edits/edits.go b/edits/edits.go index 227a856..3f77640 100644 --- a/edits/edits.go +++ b/edits/edits.go @@ -8,6 +8,7 @@ import ( "unicode" "unicode/utf8" + "git.sericyb.com.au/jotty/i18n" ps "git.sericyb.com.au/jotty/permascroll" "github.com/muesli/termenv" "github.com/rivo/uniseg" @@ -46,8 +47,7 @@ var ( ) const ( - cursorCharCap = '↑' // Capitalisation indicator character - errorLabel = "Error: " + cursorCharCap = '↑' // Capitalisation indicator character margin = 6 // Up to 4 edit marks, cursor and wrap indicator markChar = '|' // Visual representation of an edit mark moreChar = '…' // Continuation indicator character @@ -166,7 +166,7 @@ func cursorString() string { } func errorString() string { - return output.String(string(errorLabel)).Blink().Foreground(output.Color(errorColor)).String() + return output.String(i18n.Text["error"]).Blink().Foreground(output.Color(errorColor)).String() } func markString() string { @@ -216,8 +216,6 @@ func cutBuffer(cutMax int) (buf string) { // Draw the status bar that appears on the last line of the screen. func statusLine() string { - const cutLabel = "cut:" - const helpLabel = "ESC=Help" const padding = " " // Spaces between items const separators = 4 // One space after each scope var c [MaxScope]string // Counters for each scope @@ -250,14 +248,14 @@ func statusLine() string { } } - if buf := cutBuffer(ex - (w + len(padding) + uniseg.StringWidth(cutLabel) + 4)); buf != "" { - t.WriteString(padding + " " + cutLabel + " " + cutStyle(buf)) - w += len(padding) + uniseg.StringWidth(cutLabel) + uniseg.StringWidth(buf) + 2 + if buf := cutBuffer(ex - (w + len(padding) + i18n.TextWidth["cut"] + 4)); buf != "" { + t.WriteString(padding + " " + i18n.Text["cut"] + " " + cutStyle(buf)) + w += len(padding) + i18n.TextWidth["cut"] + uniseg.StringWidth(buf) + 2 } // Right-align help label - if align := ex - (w + uniseg.StringWidth(helpLabel) + len(padding)); align > 1 { - t.WriteString(strings.Repeat(" ", align) + helpStyle(helpLabel)) + if align := ex - (w + i18n.TextWidth["help"] + len(padding)); align > 1 { + t.WriteString(strings.Repeat(" ", align) + helpStyle(i18n.Text["help"])) } return t.String() @@ -687,17 +685,16 @@ func Screen() string { t = append(t, "") } - if Mode == Help { - window := helpWindow() - t = slices.Delete(t, 0, len(window)) - t = slices.Insert(t, 0, window...) - } - switch Mode { case Quit: t = append(t, confirmStyle(message)) - case Error: // Go error messages always start with a lowercase letter - t = append(t, errorString()+errorStyle(truncate(ex-(uniseg.StringWidth(errorLabel)+1), message))) + case Error: + t = append(t, errorString()+" "+errorStyle(truncate(ex-(i18n.TextWidth["error"]+1), message))) + case Help: + window := helpWindow() + t = slices.Delete(t, 0, len(window)) + t = slices.Insert(t, 0, window...) + t = append(t, statusLine()) default: t = append(t, statusLine()) } diff --git a/edits/edits_test.go b/edits/edits_test.go index 8f96ec6..5d0d549 100644 --- a/edits/edits_test.go +++ b/edits/edits_test.go @@ -5,6 +5,7 @@ import ( "slices" "testing" + "git.sericyb.com.au/jotty/i18n" ps "git.sericyb.com.au/jotty/permascroll" "github.com/muesli/termenv" "github.com/stretchr/testify/assert" @@ -470,7 +471,7 @@ func TestScreen(t *testing.T) { cursor[Para] = 3 assert.Equal("1 2 \n3 4\n\n_\n@14/14", Screen()) - HelpText = []byte("Test") + i18n.HelpText, i18n.HelpWidth = []string{"Test"}, 4 Mode = Help assert.Equal(" Test\n——————————\n\n_\n@14/14", Screen()) diff --git a/edits/help.go b/edits/help.go index 09090e8..d9b6e40 100644 --- a/edits/help.go +++ b/edits/help.go @@ -1,14 +1,12 @@ package edits import ( - "slices" "strings" + "git.sericyb.com.au/jotty/i18n" "github.com/rivo/uniseg" ) -var HelpText []byte - func dropParagraphs(w []string) []string { i := len(w) - ey for i < len(w) && len(w[i]) > 0 { @@ -24,20 +22,13 @@ func dropParagraphs(w []string) []string { // The help window. func helpWindow() (w []string) { - w = strings.Split(string(HelpText), "\n") - if len(w[len(w)-1]) == 0 { // Trim final blank line - w = slices.Delete(w, len(w)-1, len(w)) - } - - var longest int - for _, l := range w { - longest = max(longest, uniseg.StringWidth(l)) - } + w = make([]string, len(i18n.HelpText)) + copy(w, i18n.HelpText) - if longest > ex { + if i18n.HelpWidth > ex { w = rewrap(w) } else { - padding := strings.Repeat(" ", (ex-longest)/2) + padding := strings.Repeat(" ", (ex-i18n.HelpWidth)/2) for i, l := range w { if len(l) > 0 { w[i] = padding + l diff --git a/edits/help_test.go b/edits/help_test.go index 280a182..ae99836 100644 --- a/edits/help_test.go +++ b/edits/help_test.go @@ -3,6 +3,7 @@ package edits import ( "testing" + "git.sericyb.com.au/jotty/i18n" "github.com/stretchr/testify/assert" ) @@ -21,13 +22,13 @@ func TestHelpWindow(t *testing.T) { assert := assert.New(t) ResizeScreen(6, 8) - HelpText = []byte("") + i18n.HelpText, i18n.HelpWidth = []string{}, 0 assert.Equal([]string{"——————"}, helpWindow()) - HelpText = []byte("One\n\nTwo") + i18n.HelpText, i18n.HelpWidth = []string{"One", "", "Two"}, 3 assert.Equal([]string{" One", "", " Two", "——————"}, helpWindow()) - HelpText = []byte("Testing\n\nMore\n\nText") + i18n.HelpText, i18n.HelpWidth = []string{"Testing", "", "More", "", "Text"}, 7 assert.Equal([]string{"Testi-", "ng", "", "More", "", "Text", "——————"}, helpWindow()) ResizeScreen(6, 5) diff --git a/go.mod b/go.mod index cf0ef51..af55e09 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 require ( github.com/cespare/xxhash/v2 v2.3.0 github.com/charmbracelet/bubbletea v1.1.1 + github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 github.com/muesli/termenv v0.15.2 github.com/rivo/uniseg v0.4.7 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index ac05fd4..835b238 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 h1:Po+wkNdMmN+Zj1tDsJQy7mJlPlwGNQd9JZoPjObagf8= +github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49/go.mod h1:YiutDnxPRLk5DLUFj6Rw4pRBBURZY07GFr54NdV9mQg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= diff --git a/i18n/help.de b/i18n/help.de new file mode 100644 index 0000000..c19c197 --- /dev/null +++ b/i18n/help.de @@ -0,0 +1,15 @@ +Der Cursor zeigt den aktuellen Bearbeitungs- und Navigationsbereich wie folgt an: +"_"-Zeichen, "#"-Wörter, "$"-Sätze oder "¶"-Absätze. + +"|" zeigt bis zu vier Bearbeitungsmarkierungen an, die primäre und sekundäre Auswahlen definieren. +Wenn nur eine Bearbeitungsmarkierung vorhanden ist, fungiert der Cursor als zweite Bearbeitungsmarkierung. +Bei ausgewähltem Text tauscht die Leertaste die Auswahlen aus, +"Eingabetaste"/"Strg-M" und "Entf"/"Strg-X" schneiden die primäre Auswahl aus. + +Mit den folgenden Tasten lassen sich spezielle Aktionen ausführen: +"↑" Bereich vergrößern, "↓" Bereich verkleinern, "←" nach links bewegen, "→" nach rechts bewegen, +"Rücktaste"/"Strg-H" vorhergehenden Text löschen, "Eingabetaste"/"Strg-M" neuer Absatz, +"Einfügen"/"Strg-V" ausgeschnittenen Text einfügen, "Entf"/"Strg-X" Text beim Ausschneiden löschen, +"Pos1"/"Strg-U" zum Anfang bewegen, "Ende"/"Strg-D" zum Ende bewegen, +"Tab"/"Strg-I" Markierung setzen, "Umschalt-Tab" alle Markierungen abbrechen, "Strg-C" Text kopieren, +"Strg-Q"/"Strg-W" beenden, "Strg-E" exportieren, "Strg-Z" rückgängig machen, "Strg-Y" wiederherstellen diff --git a/i18n/help.jp b/i18n/help.jp new file mode 100644 index 0000000..fed661e --- /dev/null +++ b/i18n/help.jp @@ -0,0 +1,15 @@ +カーソルは、編集とナビゲーションの現在の範囲を次のように表示します: +「_」文字、「#」単語、「$」文、または「¶」段落。 + +「|」は、プライマリ選択とセカンダリ選択を定義する最大 4 つの編集マークを示します。 +編集マークが 1 つしかない場合、カーソルは 2 番目の編集マークとして機能します。 +テキストが選択されているときに「スペース」を押すと選択が交換され、 +「Enter」/「Ctrl-M」または「Delete」/「Ctrl-X」を押すと選択が切り取られます。 + +以下のキーは特別なアクションを実行します: +「↑」 範囲を拡大、「↓」 範囲を縮小、「←」 左に移動、「→」 右に移動、 +「Backspace」/「Ctrl-H」 前のテキストを消去、「Enter」/「Ctrl-M」 新しい段落、 +「Insert」/「Ctrl-V」 切り取ったテキストを挿入、「Delete」/「Ctrl-X」 切り取ったテキストの削除、 +「Home」/「Ctrl-U」 先頭に移動、「End」/「Ctrl-D」 末尾に移動、 +「Tab」/「Ctrl-I」 マークを設定、「Shift-Tab」 すべてのマークをクリア、「Ctrl-C」 テキストをコピー、 +「Ctrl-Q」/「Ctrl-W」 終了、「Ctrl-E」 エクスポート、「Ctrl-Z」 元に戻す、「Ctrl-Y」 やり直し diff --git a/i18n/i18n.go b/i18n/i18n.go new file mode 100644 index 0000000..33ba2f4 --- /dev/null +++ b/i18n/i18n.go @@ -0,0 +1,54 @@ +package i18n + +import ( + "embed" + "slices" + "strings" + + "github.com/jeandeaual/go-locale" + "github.com/rivo/uniseg" +) + +//go:embed help.* text.* +var translations embed.FS + +var ( + HelpText []string + HelpWidth int + Text = make(map[string]string) + TextWidth = make(map[string]int) +) + +func init() { + var err error + var userLanguage string + if userLanguage, err = locale.GetLanguage(); err != nil { + userLanguage = "en" + } + + var b []byte + if b, err = translations.ReadFile("help." + userLanguage); err != nil { + b, _ = translations.ReadFile("help.en") + } + + HelpText = strings.Split(string(b), "\n") + if len(HelpText[len(HelpText)-1]) == 0 { // Trim final blank line + HelpText = slices.Delete(HelpText, len(HelpText)-1, len(HelpText)) + } + + for _, l := range HelpText { + HelpWidth = max(HelpWidth, uniseg.StringWidth(l)) + } + + if b, err = translations.ReadFile("text." + userLanguage); err != nil { + b, _ = translations.ReadFile("text.en") + } + + s := strings.Split(string(b), "\n") + for _, t := range s { + k, v, _ := strings.Cut(t, "|") + v = strings.Replace(v, `\n`, "\n", -1) + Text[k] = v + TextWidth[k] = uniseg.StringWidth(v) + } +} diff --git a/i18n/text.de b/i18n/text.de new file mode 100644 index 0000000..e0d4d4b --- /dev/null +++ b/i18n/text.de @@ -0,0 +1,6 @@ +confirm|Beenden bestätigen? +cut|Ausschneiden: +error|Fehler: +help|ESC=Hilfe +usage|Verwendung:\n %s [Dateiname]\n\nWenn kein Dateiname angegeben ist, wird standardmäßig „%s“ verwendet\n\nOptionen: +version|Programmversion drucken und beenden diff --git a/i18n/text.en b/i18n/text.en new file mode 100644 index 0000000..07c7349 --- /dev/null +++ b/i18n/text.en @@ -0,0 +1,6 @@ +confirm|Confirm exit? +cut|cut: +error|Error: +help|ESC=Help +usage|Usage:\n %s [filename]\n\nIf filename is not provided, defaults to '%s'\n\nOptions: +version|print program version and exit diff --git a/i18n/text.jp b/i18n/text.jp new file mode 100644 index 0000000..7ef664c --- /dev/null +++ b/i18n/text.jp @@ -0,0 +1,6 @@ +confirm|終了を確認しますか? +cut|カット: +error|エラー: +help|ESC=ヘルプ +usage|使用方法:\n %s [ファイル名]\n\nファイル名が指定されていない場合は、デフォルトで '%s' になります\n\nオプション: +version|プログラムのバージョンを印刷して終了します diff --git a/jotty.go b/jotty.go index 705f98c..8568065 100644 --- a/jotty.go +++ b/jotty.go @@ -1,7 +1,7 @@ package main import ( - "embed" + _ "embed" "flag" "fmt" "log" @@ -11,13 +11,11 @@ import ( "time" "git.sericyb.com.au/jotty/edits" + "git.sericyb.com.au/jotty/i18n" ps "git.sericyb.com.au/jotty/permascroll" tea "github.com/charmbracelet/bubbletea" ) -//go:embed i18n -var i18n embed.FS - //go:generate sh -c "printf %s $(git describe --always --tags) > version.txt" //go:embed version.txt var version string @@ -47,12 +45,11 @@ var dispatch = map[tea.KeyType]func(){ var ( exportPath = "jotty.txt" sx, sy int // screen dimensions - vFlag = flag.Bool("version", false, "print program version and exit") ) type model struct{ timer *time.Timer } -func confirmExit() { edits.SetMode(edits.Quit, "Confirm exit?") } +func confirmExit() { edits.SetMode(edits.Quit, i18n.Text["confirm"]) } func export() { edits.Export(exportPath) } func help() { edits.SetMode(edits.Help, "") } @@ -125,13 +122,13 @@ func cleanup() { func usage() { fmt.Println("https://github.com/xanni/jotty ⓒ 2024 Andrew Pam ") - fmt.Printf("\nUsage:\n %s [filename]\n\nIf filename is not provided, defaults to '%s'\n\nOptions:\n", - filepath.Base(os.Args[0]), defaultName) + fmt.Printf("\n"+i18n.Text["usage"]+"\n", filepath.Base(os.Args[0]), defaultName) flag.PrintDefaults() } func main() { flag.Usage = usage + vFlag := flag.Bool("version", false, i18n.Text["version"]) flag.Parse() if *vFlag { println(filepath.Base(os.Args[0]) + " " + version) @@ -152,7 +149,6 @@ func main() { } defer cleanup() - edits.HelpText, _ = i18n.ReadFile("i18n/help.en") var m model m.timer = time.AfterFunc(syncDelay, func() {