Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add translator mechanism #921

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions ClickableWidgets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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()
}

Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 12 additions & 0 deletions Context.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type GIUContext struct {

InputHandler InputHandler
FontAtlas *FontAtlas
Translator Translator

textureLoadingQueue *queue.Queue
textureFreeingQueue *queue.Queue
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions FontAtlasProsessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions TextWidgets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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,
}
}
Expand Down
102 changes: 102 additions & 0 deletions Translator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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{}

// 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
}

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 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)

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
}
31 changes: 17 additions & 14 deletions Widgets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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()
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -766,7 +769,7 @@ func (t *TooltipWidget) buildTooltip() {
t.layout.Build()
imgui.EndTooltip()
} else {
imgui.SetTooltip(t.tip)
imgui.SetTooltip(Context.PrepareString(t.tip))
}
}
}
Expand Down Expand Up @@ -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),
) {
Expand Down
49 changes: 49 additions & 0 deletions examples/translation/translation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Package main shows using of translations in giu.
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
)

func loop() {
giu.SingleWindow().Layout(
giu.Combo("Select language", languageCodes[currentLang], languageCodes, &currentLang).OnChange(func() {
if err := giu.Context.Translator.SetLanguage(languageCodes[currentLang]); err != nil {
panic(err)
}
}),
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)
}

if err := translator.SetLanguage(languageCodes[currentLang]); err != nil {
panic(err)
}

giu.Context.SetTranslator(translator)

wnd.Run(loop)
}
Loading