From 90e0c007db0fc56a02fd7026156e108029d10688 Mon Sep 17 00:00:00 2001 From: gucio321 Date: Thu, 28 Nov 2024 14:32:42 +0100 Subject: [PATCH 1/2] add translator mechanism --- ClickableWidgets.go | 14 ++-- Context.go | 12 ++++ FontAtlasProsessor.go | 1 + TextWidgets.go | 4 +- Translator.go | 100 ++++++++++++++++++++++++++++ Widgets.go | 31 +++++---- examples/translation/translation.go | 44 ++++++++++++ 7 files changed, 183 insertions(+), 23 deletions(-) create mode 100644 Translator.go create mode 100644 examples/translation/translation.go diff --git a/ClickableWidgets.go b/ClickableWidgets.go index 9c992db0..3737c3ef 100644 --- a/ClickableWidgets.go +++ b/ClickableWidgets.go @@ -69,7 +69,7 @@ func (b *ButtonWidget) Build() { defer imgui.EndDisabled() } - if imgui.ButtonV(Context.FontAtlas.RegisterString(b.id.String()), imgui.Vec2{X: b.width, Y: b.height}) && b.onClick != nil { + if imgui.ButtonV(Context.PrepareString(b.id.String()), imgui.Vec2{X: b.width, Y: b.height}) && b.onClick != nil { b.onClick() } } @@ -141,7 +141,7 @@ func (b *SmallButtonWidget) OnClick(onClick func()) *SmallButtonWidget { // Build implements Widget interface. func (b *SmallButtonWidget) Build() { - if imgui.SmallButton(Context.FontAtlas.RegisterString(b.id.String())) && b.onClick != nil { + if imgui.SmallButton(Context.PrepareString(b.id.String())) && b.onClick != nil { b.onClick() } } @@ -381,7 +381,7 @@ func (c *CheckboxWidget) OnChange(onChange func()) *CheckboxWidget { // Build implements Widget interface. func (c *CheckboxWidget) Build() { - if imgui.Checkbox(Context.FontAtlas.RegisterString(c.text.String()), c.selected) && c.onChange != nil { + if imgui.Checkbox(Context.PrepareString(c.text.String()), c.selected) && c.onChange != nil { c.onChange() } } @@ -414,7 +414,7 @@ func (r *RadioButtonWidget) OnChange(onChange func()) *RadioButtonWidget { // Build implements Widget interface. func (r *RadioButtonWidget) Build() { - if imgui.RadioButtonBool(Context.FontAtlas.RegisterString(r.text.String()), r.active) && r.onChange != nil { + if imgui.RadioButtonBool(Context.PrepareString(r.text.String()), r.active) && r.onChange != nil { r.onChange() } } @@ -489,7 +489,7 @@ func (s *SelectableWidget) Build() { s.flags |= SelectableFlagsAllowDoubleClick } - if imgui.SelectableBoolV(Context.FontAtlas.RegisterString(s.label.String()), s.selected, imgui.SelectableFlags(s.flags), imgui.Vec2{X: s.width, Y: s.height}) && s.onClick != nil { + if imgui.SelectableBoolV(Context.PrepareString(s.label.String()), s.selected, imgui.SelectableFlags(s.flags), imgui.Vec2{X: s.width, Y: s.height}) && s.onClick != nil { s.onClick() } @@ -514,7 +514,7 @@ type TreeNodeWidget struct { // TreeNode creates a new tree node widget. func TreeNode(label string) *TreeNodeWidget { return &TreeNodeWidget{ - label: Context.FontAtlas.RegisterString(label), + label: label, flags: 0, layout: nil, eventHandler: nil, @@ -554,7 +554,7 @@ func (t *TreeNodeWidget) Layout(widgets ...Widget) *TreeNodeWidget { // Build implements Widget interface. func (t *TreeNodeWidget) Build() { - open := imgui.TreeNodeExStrV(t.label, imgui.TreeNodeFlags(t.flags)) + open := imgui.TreeNodeExStrV(Context.PrepareString(t.label), imgui.TreeNodeFlags(t.flags)) if t.event != nil { t.event() diff --git a/Context.go b/Context.go index 94930fa2..96b7f990 100644 --- a/Context.go +++ b/Context.go @@ -69,6 +69,7 @@ type GIUContext struct { InputHandler InputHandler FontAtlas *FontAtlas + Translator Translator textureLoadingQueue *queue.Queue textureFreeingQueue *queue.Queue @@ -87,6 +88,7 @@ func CreateContext(b backend.Backend[glfwbackend.GLFWWindowFlags]) *GIUContext { textureLoadingQueue: queue.New(), textureFreeingQueue: queue.New(), m: &sync.Mutex{}, + Translator: &EmptyTranslator{}, } // Create font @@ -114,6 +116,16 @@ func (c *GIUContext) SetDirty() { c.dirty = true } +// PrepareString prepares string to be displayed by imgui. +// It does the following: +// - adds a string to the FontAtlas +// - translates the string with the Translator set in the context +// Not all widgets will use this. Text with user-defined input (e.g. InputText will still use FontAtlas.RegisterString) +func (c *GIUContext) PrepareString(str string) string { + str = c.Translator.Translate(str) + return c.FontAtlas.RegisterString(str) +} + // cleanStates removes all states that were not marked as valid during rendering, // then reset said flag before new usage // should always be called before first Get/Set state use in renderloop diff --git a/FontAtlasProsessor.go b/FontAtlasProsessor.go index 4ed5670e..84bebf77 100644 --- a/FontAtlasProsessor.go +++ b/FontAtlasProsessor.go @@ -218,6 +218,7 @@ func (a *FontAtlas) registerDefaultFonts(fontInfos []FontInfo) { // RegisterString register string to font atlas builder. // Note only register strings that will be displayed on the UI. +// This also calls translator before registering the string. func (a *FontAtlas) RegisterString(str string) string { for _, s := range str { if _, ok := a.stringMap.Load(s); !ok { diff --git a/TextWidgets.go b/TextWidgets.go index 99571a20..1a53343c 100644 --- a/TextWidgets.go +++ b/TextWidgets.go @@ -132,7 +132,7 @@ type BulletTextWidget struct { // BulletText creates bulletTextWidget. func BulletText(text string) *BulletTextWidget { return &BulletTextWidget{ - text: Context.FontAtlas.RegisterString(text), + text: Context.PrepareString(text), } } @@ -526,7 +526,7 @@ type LabelWidget struct { // Label constructs label widget. func Label(label string) *LabelWidget { return &LabelWidget{ - label: Context.FontAtlas.RegisterString(label), + label: Context.PrepareString(label), wrapped: false, } } diff --git a/Translator.go b/Translator.go new file mode 100644 index 00000000..d389f25a --- /dev/null +++ b/Translator.go @@ -0,0 +1,100 @@ +package giu + +import ( + "strings" +) + +// Translator should be implemented by type that can translate a string to another string. +type Translator interface { + // Translate returns tranalted string + Translate(string) string + // SetLanguage changes language of the translation. + SetLanguage(string) error +} + +// SetTranslator allows to change the default (&EmptyTranslator{}) +// This will raise a panic if t is nil. +// Note that using translator will change labels of widgets, +// so this might affect internal imgui's state. +// See also Context.RegisterString +func (c *GIUContext) SetTranslator(t Translator) { + Assert(t != nil, "Context", "SetTranslator", "Translator must not be nil.") + c.Translator = t +} + +var _ Translator = &EmptyTranslator{} + +// EmptyTranslator is the default one (to save resources). +// It does nothing +type EmptyTranslator struct{} + +func (t *EmptyTranslator) Translate(s string) string { + return s +} + +func (t *EmptyTranslator) SetLanguage(_ string) error { + return nil +} + +var _ Translator = &BasicTranslator{} + +// BasicTranslator is a simpliest implementation of translation mechanism. +// This is NOT thread-safe yet. If you need thread-safety, write your own implementation. +// +// It is supposed to be used in the following way: +// - create translator +// - add languages (tip: you can use empty map for default language) +// - set translator in Context +// - set default language +// - write your UI as always. +type BasicTranslator struct { + // language tag -> key -> value + source map[string]map[string]string + currentLanguage string +} + +// NewBasicTranslator creates a new BasicTranslator with the given language tag. +func NewBasicTranslator() *BasicTranslator { + return &BasicTranslator{} +} + +// Translate implements Translator interface. +// It translates s to the current language, under the following conditions: +// - If s is empty, an empty string is returned with no further processing. +// - If t.currentLanguage is empty, a panic will be raised. +// - If t.currentLanguage is not in a.source, BasicTranslator raises panic. +// - If s is not in source[currentLanguage], s is returned as-is. +func (t *BasicTranslator) Translate(s string) string { + s = strings.Split(s, "##")[0] + if s == "" { + return "" + } + + Assert(t.currentLanguage != "", "BasicTranslator", "Translate", "Current language is not set, so ther is no sense in using BasicTranslator.") + locale, ok := t.source[t.currentLanguage] + Assert(ok, "BasicTranslator", "Translate", "There is no language tag %s known by the translator. Did you add it?", t.currentLanguage) + + translated, ok := locale[s] + if !ok { + return s + } + + return translated +} + +// SetLanguage sets the current language of the translator. +func (t *BasicTranslator) SetLanguage(tag string) error { + t.currentLanguage = tag + return nil +} + +// AddLanguage adds a new "dictionary" to the translator. +func (t *BasicTranslator) AddLanguage(tag string, source map[string]string) *BasicTranslator { + if t.source == nil { + t.source = make(map[string]map[string]string) + } + + t.source[tag] = source + + return t +} diff --git a/Widgets.go b/Widgets.go index 4ab0e832..296aced2 100644 --- a/Widgets.go +++ b/Widgets.go @@ -149,7 +149,7 @@ func ComboCustom(label, previewValue string) *ComboCustomWidget { // Layout add combo's layout. func (cc *ComboCustomWidget) Layout(widgets ...Widget) *ComboCustomWidget { - cc.layout = Layout(widgets) + cc.layout = widgets return cc } @@ -172,7 +172,7 @@ func (cc *ComboCustomWidget) Build() { defer imgui.PopItemWidth() } - if imgui.BeginComboV(Context.FontAtlas.RegisterString(cc.label.String()), cc.previewValue, imgui.ComboFlags(cc.flags)) { + if imgui.BeginComboV(Context.PrepareString(cc.label.String()), cc.previewValue, imgui.ComboFlags(cc.flags)) { cc.layout.Build() imgui.EndCombo() } @@ -236,9 +236,9 @@ func (c *ComboWidget) Build() { defer imgui.PopItemWidth() } - if imgui.BeginComboV(Context.FontAtlas.RegisterString(c.label.String()), c.previewValue, imgui.ComboFlags(c.flags)) { + if imgui.BeginComboV(Context.PrepareString(c.label.String()), c.previewValue, imgui.ComboFlags(c.flags)) { for i, item := range c.items { - if imgui.SelectableBool(fmt.Sprintf("%s##%d", item, i)) { + if imgui.SelectableBool(fmt.Sprintf("%s##%d", Context.PrepareString(item), i)) { *c.selected = int32(i) if c.onChange != nil { c.onChange() @@ -332,7 +332,7 @@ func (d *DragIntWidget) Format(format string) *DragIntWidget { // Build implements Widget interface. func (d *DragIntWidget) Build() { - imgui.DragIntV(Context.FontAtlas.RegisterString(d.label.String()), d.value, d.speed, d.minValue, d.maxValue, d.format, 0) + imgui.DragIntV(Context.PrepareString(d.label.String()), d.value, d.speed, d.minValue, d.maxValue, d.format, 0) } var _ Widget = &ColumnWidget{} @@ -376,7 +376,7 @@ func MainMenuBar() *MainMenuBarWidget { // Layout sets layout of the menu bar. (See MenuWidget). func (m *MainMenuBarWidget) Layout(widgets ...Widget) *MainMenuBarWidget { - m.layout = Layout(widgets) + m.layout = widgets return m } @@ -471,7 +471,7 @@ func (m *MenuItemWidget) OnClick(onClick func()) *MenuItemWidget { // Build implements Widget interface. func (m *MenuItemWidget) Build() { - if imgui.MenuItemBoolV(Context.FontAtlas.RegisterString(m.label.String()), m.shortcut, m.selected, m.enabled) && m.onClick != nil { + if imgui.MenuItemBoolV(Context.PrepareString(m.label.String()), m.shortcut, m.selected, m.enabled) && m.onClick != nil { m.onClick() } } @@ -514,7 +514,7 @@ func (m *MenuWidget) Layout(widgets ...Widget) *MenuWidget { // Build implements Widget interface. func (m *MenuWidget) Build() { - if imgui.BeginMenuV(Context.FontAtlas.RegisterString(m.label.String()), m.enabled) { + if imgui.BeginMenuV(Context.PrepareString(m.label.String()), m.enabled) { m.layout.Build() imgui.EndMenu() } @@ -621,7 +621,7 @@ type TabItemWidget struct { // TabItem creates new TabItem. func TabItem(label string) *TabItemWidget { return &TabItemWidget{ - label: Context.FontAtlas.RegisterString(label), + label: label, open: nil, flags: 0, layout: nil, @@ -650,13 +650,16 @@ func (t *TabItemWidget) Flags(flags TabItemFlags) *TabItemWidget { // Layout is a layout displayed when item is opened. func (t *TabItemWidget) Layout(widgets ...Widget) *TabItemWidget { - t.layout = Layout(widgets) + t.layout = widgets return t } // BuildTabItem executes tab item build steps. func (t *TabItemWidget) BuildTabItem() { - if imgui.BeginTabItemV(t.label, t.open, imgui.TabItemFlags(t.flags)) { + if imgui.BeginTabItemV( + Context.PrepareString(t.label), + t.open, imgui.TabItemFlags(t.flags), + ) { t.layout.Build() imgui.EndTabItem() } @@ -734,7 +737,7 @@ func Tooltipf(format string, args ...any) *TooltipWidget { // Layout sets a custom layout of tooltip. func (t *TooltipWidget) Layout(widgets ...Widget) *TooltipWidget { - t.layout = Layout(widgets) + t.layout = widgets return t } @@ -766,7 +769,7 @@ func (t *TooltipWidget) buildTooltip() { t.layout.Build() imgui.EndTooltip() } else { - imgui.SetTooltip(t.tip) + imgui.SetTooltip(Context.PrepareString(t.tip)) } } } @@ -839,7 +842,7 @@ func (ce *ColorEditWidget) Build() { } if imgui.ColorEdit4V( - Context.FontAtlas.RegisterString(ce.label.String()), + Context.PrepareString(ce.label.String()), &col, imgui.ColorEditFlags(ce.flags), ) { diff --git a/examples/translation/translation.go b/examples/translation/translation.go new file mode 100644 index 00000000..2fdd0cd3 --- /dev/null +++ b/examples/translation/translation.go @@ -0,0 +1,44 @@ +package main + +import "github.com/AllenDang/giu" + +var ( + // here we define our multi-language dictionary. + // You could also do that e.g. with JSON. + languageDefs = map[string]map[string]string{ + "en": {}, // as we write our UI in english, the default value of the text will be fine (see (*BasicTranslator).Translate for more) + "pl": { + "Hello world!": "Witaj Ĺ›wiecie", + }, + "de": { + "Hello world!": "Hallo Welt!", + }, + } + + languageCodes = []string{"en", "pl", "de"} + currentLang int32 = 0 +) + +func loop() { + giu.SingleWindow().Layout( + giu.Combo("Select language", languageCodes[currentLang], languageCodes, ¤tLang).OnChange(func() { + giu.Context.Translator.SetLanguage(languageCodes[currentLang]) + }), + giu.Label("Hello world!"), + ) +} + +func main() { + wnd := giu.NewMasterWindow("Hello world", 800, 600, giu.MasterWindowFlagsNotResizable) + // initialize translation. Do that before loop. + translator := giu.NewBasicTranslator() + for k, v := range languageDefs { + translator.AddLanguage(k, v) + } + + translator.SetLanguage(languageCodes[currentLang]) + + giu.Context.SetTranslator(translator) + + wnd.Run(loop) +} From 6b5b4ec6be01824a4d345105d7fb2b296989b040 Mon Sep 17 00:00:00 2001 From: gucio321 Date: Thu, 28 Nov 2024 14:41:12 +0100 Subject: [PATCH 2/2] linting --- Context.go | 2 +- Translator.go | 8 +++++--- examples/translation/translation.go | 13 +++++++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Context.go b/Context.go index 96b7f990..ab9288d3 100644 --- a/Context.go +++ b/Context.go @@ -120,7 +120,7 @@ func (c *GIUContext) SetDirty() { // It does the following: // - adds a string to the FontAtlas // - translates the string with the Translator set in the context -// Not all widgets will use this. Text with user-defined input (e.g. InputText will still use FontAtlas.RegisterString) +// Not all widgets will use this. Text with user-defined input (e.g. InputText will still use FontAtlas.RegisterString). func (c *GIUContext) PrepareString(str string) string { str = c.Translator.Translate(str) return c.FontAtlas.RegisterString(str) diff --git a/Translator.go b/Translator.go index d389f25a..9f1a379a 100644 --- a/Translator.go +++ b/Translator.go @@ -16,7 +16,7 @@ type Translator interface { // This will raise a panic if t is nil. // Note that using translator will change labels of widgets, // so this might affect internal imgui's state. -// See also Context.RegisterString +// See also Context.RegisterString. func (c *GIUContext) SetTranslator(t Translator) { Assert(t != nil, "Context", "SetTranslator", "Translator must not be nil.") c.Translator = t @@ -25,13 +25,15 @@ func (c *GIUContext) SetTranslator(t Translator) { var _ Translator = &EmptyTranslator{} // EmptyTranslator is the default one (to save resources). -// It does nothing +// It does nothing. type EmptyTranslator struct{} +// Translate implements Translator interface. func (t *EmptyTranslator) Translate(s string) string { return s } +// SetLanguage implements Translator interface. func (t *EmptyTranslator) SetLanguage(_ string) error { return nil } @@ -70,7 +72,7 @@ func (t *BasicTranslator) Translate(s string) string { return "" } - Assert(t.currentLanguage != "", "BasicTranslator", "Translate", "Current language is not set, so ther is no sense in using BasicTranslator.") + Assert(t.currentLanguage != "", "BasicTranslator", "Translate", "Current language is not set, so there is no sense in using BasicTranslator.") locale, ok := t.source[t.currentLanguage] Assert(ok, "BasicTranslator", "Translate", "There is no language tag %s known by the translator. Did you add it?", t.currentLanguage) diff --git a/examples/translation/translation.go b/examples/translation/translation.go index 2fdd0cd3..6770c52a 100644 --- a/examples/translation/translation.go +++ b/examples/translation/translation.go @@ -1,3 +1,4 @@ +// Package main shows using of translations in giu. package main import "github.com/AllenDang/giu" @@ -15,14 +16,16 @@ var ( }, } - languageCodes = []string{"en", "pl", "de"} - currentLang int32 = 0 + languageCodes = []string{"en", "pl", "de"} + currentLang int32 ) func loop() { giu.SingleWindow().Layout( giu.Combo("Select language", languageCodes[currentLang], languageCodes, ¤tLang).OnChange(func() { - giu.Context.Translator.SetLanguage(languageCodes[currentLang]) + if err := giu.Context.Translator.SetLanguage(languageCodes[currentLang]); err != nil { + panic(err) + } }), giu.Label("Hello world!"), ) @@ -36,7 +39,9 @@ func main() { translator.AddLanguage(k, v) } - translator.SetLanguage(languageCodes[currentLang]) + if err := translator.SetLanguage(languageCodes[currentLang]); err != nil { + panic(err) + } giu.Context.SetTranslator(translator)