diff --git a/pkg/app/app.go b/pkg/app/app.go index d558ed250..3e227a395 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -9,6 +9,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui" + "github.com/jesseduffield/lazygit/pkg/i18n" ) // App struct @@ -20,6 +21,7 @@ type App struct { OSCommand *commands.OSCommand GitCommand *commands.GitCommand Gui *gui.Gui + Tr *i18n.Localizer } func newLogger(config config.AppConfigurer) *logrus.Logger { @@ -48,11 +50,17 @@ func NewApp(config config.AppConfigurer) (*App, error) { if err != nil { return nil, err } + + app.Tr, err = i18n.NewLocalizer(app.Log) + if err != nil { + return nil, err + } + app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand) if err != nil { return nil, err } - app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, config.GetVersion()) + app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config.GetVersion()) if err != nil { return nil, err } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 9f9819a5a..d4ea75e74 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -14,13 +14,6 @@ import ( gitconfig "github.com/tcnksm/go-gitconfig" ) -var ( - // ErrNoOpenCommand : When we don't know which command to use to open a file - ErrNoOpenCommand = errors.New("Unsure what command to use to open this file") - // ErrNoEditorDefined : When we can't find an editor to edit a file - ErrNoEditorDefined = errors.New("No editor defined in $VISUAL, $EDITOR, or git config") -) - // Platform stores the os state type Platform struct { os string @@ -113,7 +106,7 @@ func (c *OSCommand) GetOpenCommand() (string, string, error) { return name, trail, nil } } - return "", "", ErrNoOpenCommand + return "", "", errors.New("Unsure what command to use to open this file") } // VsCodeOpenFile opens the file in code, with the -r flag to open in the @@ -157,7 +150,7 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) { } } if editor == "" { - return nil, ErrNoEditorDefined + return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config") } return c.PrepareSubProcess(editor, filename) } diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index c4786d39f..67e0ceb07 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -12,7 +12,7 @@ import ( func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { index := gui.getItemPosition(v) if index == 0 { - return gui.createErrorPanel(g, "You have already checked out this branch") + return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch")) } branch := gui.getSelectedBranch(v) if err := gui.GitCommand.Checkout(branch.Name, false); err != nil { @@ -23,7 +23,9 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error { branch := gui.getSelectedBranch(v) - return gui.createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error { + message := gui.Tr.SLocalize("SureForceCheckout") + title := gui.Tr.SLocalize("ForceCheckoutBranch") + return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.Checkout(branch.Name, true); err != nil { gui.createErrorPanel(g, err.Error()) } @@ -32,7 +34,7 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { - gui.createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error { + gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil { return gui.createErrorPanel(g, err.Error()) } @@ -43,7 +45,13 @@ func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error { branch := gui.State.Branches[0] - gui.createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error { + message := gui.Tr.TemplateLocalize( + "NewBranchNameBranchOff", + Teml{ + "branchName": branch.Name, + }, + ) + gui.createPromptPanel(g, v, message, func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil { return gui.createErrorPanel(g, err.Error()) } @@ -57,9 +65,16 @@ func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error { checkedOutBranch := gui.State.Branches[0] selectedBranch := gui.getSelectedBranch(v) if checkedOutBranch.Name == selectedBranch.Name { - return gui.createErrorPanel(g, "You cannot delete the checked out branch!") + return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch")) } - return gui.createConfirmationPanel(g, v, "Delete Branch", "Are you sure you want delete the branch "+selectedBranch.Name+" ?", func(g *gocui.Gui, v *gocui.View) error { + message := gui.Tr.TemplateLocalize( + "DeleteBranchMessage", + Teml{ + "selectedBranchName": selectedBranch.Name, + }, + ) + title := gui.Tr.SLocalize("DeleteBranch") + return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.DeleteBranch(selectedBranch.Name); err != nil { return gui.createErrorPanel(g, err.Error()) } @@ -72,7 +87,7 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error { selectedBranch := gui.getSelectedBranch(v) defer gui.refreshSidePanels(g) if checkedOutBranch.Name == selectedBranch.Name { - return gui.createErrorPanel(g, "You cannot merge a branch into itself") + return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself")) } if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil { return gui.createErrorPanel(g, err.Error()) @@ -87,13 +102,13 @@ func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch { func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error { return gui.renderOptionsMap(g, map[string]string{ - "space": "checkout", - "f": "force checkout", - "m": "merge", - "c": "checkout by name", - "n": "new branch", - "d": "delete branch", - "← → ↑ ↓": "navigate", + "space": gui.Tr.SLocalize("checkout"), + "f": gui.Tr.SLocalize("forceCheckout"), + "m": gui.Tr.SLocalize("merge"), + "c": gui.Tr.SLocalize("checkoutByName"), + "n": gui.Tr.SLocalize("newBranch"), + "d": gui.Tr.SLocalize("deleteBranch"), + "← → ↑ ↓": gui.Tr.SLocalize("navigate"), }) } @@ -104,13 +119,13 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { } // This really shouldn't happen: there should always be a master branch if len(gui.State.Branches) == 0 { - return gui.renderString(g, "main", "No branches for this repo") + return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo")) } go func() { branch := gui.getSelectedBranch(v) diff, err := gui.GitCommand.GetBranchGraph(branch.Name) if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") { - diff = "There is no tracking for this branch" + diff = gui.Tr.SLocalize("NoTrackingThisBranch") } gui.renderString(g, "main", diff) }() diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index f765ab308..26db703f0 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -1,22 +1,24 @@ package gui -import "github.com/jesseduffield/gocui" +import ( + "github.com/jesseduffield/gocui" +) func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error { message := gui.trimmedContent(v) if message == "" { - return gui.createErrorPanel(g, "You cannot commit without a commit message") + return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr")) } sub, err := gui.GitCommand.Commit(g, message) if err != nil { // TODO need to find a way to send through this error - if err != ErrSubProcess { + if err != gui.Errors.ErrSubProcess { return gui.createErrorPanel(g, err.Error()) } } if sub != nil { gui.SubProcess = sub - return ErrSubProcess + return gui.Errors.ErrSubProcess } gui.refreshFiles(g) v.Clear() @@ -46,5 +48,12 @@ func (gui *Gui) handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error { - return gui.renderString(g, "options", "esc: close, enter: confirm") + message := gui.Tr.TemplateLocalize( + "CloseConfirm", + Teml{ + "keyBindClose": "esc", + "keyBindConfirm": "enter", + }, + ) + return gui.renderString(g, "options", message) } diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 60ae1c315..417b947a2 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -8,11 +8,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" ) -var ( - // ErrNoCommits : When no commits are found for the branch - ErrNoCommits = errors.New("No commits for this branch") -) - func (gui *Gui) refreshCommits(g *gocui.Gui) error { g.Update(func(*gocui.Gui) error { gui.State.Commits = gui.GitCommand.GetCommits() @@ -44,7 +39,7 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error { } func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error { - return gui.createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(g, commitView, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error { commit, err := gui.getSelectedCommit(g) if err != nil { panic(err) @@ -65,11 +60,11 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error { return gui.renderOptionsMap(g, map[string]string{ - "s": "squash down", - "r": "rename", - "g": "reset to this commit", - "f": "fixup commit", - "← → ↑ ↓": "navigate", + "s": gui.Tr.SLocalize("squashDown"), + "r": gui.Tr.SLocalize("rename"), + "g": gui.Tr.SLocalize("resetToThisCommit"), + "f": gui.Tr.SLocalize("fixupCommit"), + "← → ↑ ↓": gui.Tr.SLocalize("navigate"), }) } @@ -79,10 +74,10 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { } commit, err := gui.getSelectedCommit(g) if err != nil { - if err != ErrNoCommits { + if err != errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) { return err } - return gui.renderString(g, "main", "No commits for this branch") + return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch")) } commitText := gui.GitCommand.Show(commit.Sha) return gui.renderString(g, "main", commitText) @@ -90,10 +85,10 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { if gui.getItemPosition(v) != 0 { - return gui.createErrorPanel(g, "Can only squash topmost commit") + return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit")) } if len(gui.State.Commits) == 1 { - return gui.createErrorPanel(g, "You have no commits to squash with") + return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) } commit, err := gui.getSelectedCommit(g) if err != nil { @@ -121,17 +116,18 @@ func (gui *Gui) anyUnStagedChanges(files []commands.File) bool { func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { if len(gui.State.Commits) == 1 { - return gui.createErrorPanel(g, "You have no commits to squash with") + return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) } if gui.anyUnStagedChanges(gui.State.Files) { - return gui.createErrorPanel(g, "Can't fixup while there are unstaged changes") + return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges")) } branch := gui.State.Branches[0] commit, err := gui.getSelectedCommit(g) if err != nil { return err } - gui.createConfirmationPanel(g, v, "Fixup", "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", func(g *gocui.Gui, v *gocui.View) error { + message := gui.Tr.SLocalize("SureFixupThisCommit") + gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil { return gui.createErrorPanel(g, err.Error()) } @@ -145,9 +141,9 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { if gui.getItemPosition(v) != 0 { - return gui.createErrorPanel(g, "Can only rename topmost commit") + return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) } - gui.createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error { + gui.createPromptPanel(g, v, gui.Tr.SLocalize("RenameCommit"), func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil { return gui.createErrorPanel(g, err.Error()) } @@ -165,11 +161,11 @@ func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) { panic(err) } if len(gui.State.Commits) == 0 { - return commands.Commit{}, ErrNoCommits + return commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) } lineNumber := gui.getItemPosition(v) if lineNumber > len(gui.State.Commits)-1 { - gui.Log.Info("potential error in getSelected Commit (mismatched ui and state)", gui.State.Commits, lineNumber) + gui.Log.Info(gui.Tr.SLocalize("PotentialErrInGetselectedCommit"), gui.State.Commits, lineNumber) return gui.State.Commits[len(gui.State.Commits)-1], nil } return gui.State.Commits[lineNumber], nil diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 5018d0a9f..9c3d2b263 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -83,7 +83,13 @@ func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, t // delete the existing confirmation panel if it exists if view, _ := g.View("confirmation"); view != nil { if err := gui.closeConfirmationPrompt(g); err != nil { - gui.Log.Error("Could not close confirmation prompt: ", err.Error()) + errMessage := gui.Tr.TemplateLocalize( + "CantCloseConfirmationPrompt", + Teml{ + "error": err.Error(), + }, + ) + gui.Log.Error(errMessage) } } x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, prompt) @@ -117,7 +123,14 @@ func (gui *Gui) handleNewline(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { - gui.renderString(g, "options", "esc: close, enter: confirm") + actions := gui.Tr.TemplateLocalize( + "CloseConfirm", + Teml{ + "keyBindClose": "esc", + "keyBindConfirm": "enter", + }, + ) + gui.renderString(g, "options", actions) if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil { return err } @@ -135,7 +148,7 @@ func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { currentView := g.CurrentView() colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(message)) - return gui.createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil) + return gui.createConfirmationPanel(g, currentView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil) } func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error { @@ -147,7 +160,7 @@ func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error { if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 { return nil } - gui.Log.Info("resizing popup panel") + gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel")) _, err := g.SetView(v.Name(), x0, y0, x1, y1, 0) return err } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index a7e9683d2..27589bc19 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -7,7 +7,6 @@ import ( // "strings" - "errors" "os/exec" "strings" @@ -16,11 +15,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" ) -var ( - errNoFiles = errors.New("No changed files") - errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`) -) - func (gui *Gui) stagedFiles() []commands.File { files := gui.State.Files result := make([]commands.File, 0) @@ -54,7 +48,7 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error { func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error { file, err := gui.getSelectedFile(g) if err != nil { - if err == errNoFiles { + if err == gui.Errors.ErrNoFiles { return nil } return err @@ -80,28 +74,28 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error { file, err := gui.getSelectedFile(g) if err != nil { - if err == errNoFiles { + if err == gui.Errors.ErrNoFiles { return nil } return err } if !file.HasUnstagedChanges { - return gui.createErrorPanel(g, "File has no unstaged changes to add") + return gui.createErrorPanel(g, gui.Tr.SLocalize("FileHasNoUnstagedChanges")) } if !file.Tracked { - return gui.createErrorPanel(g, "Cannot git add --patch untracked files") + return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd")) } sub, err := gui.GitCommand.AddPatch(file.Name) if err != nil { return err } gui.SubProcess = sub - return ErrSubProcess + return gui.Errors.ErrSubProcess } func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) { if len(gui.State.Files) == 0 { - return commands.File{}, errNoFiles + return commands.File{}, gui.Errors.ErrNoFiles } filesView, err := g.View("files") if err != nil { @@ -114,18 +108,25 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) { func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error { file, err := gui.getSelectedFile(g) if err != nil { - if err == errNoFiles { + if err == gui.Errors.ErrNoFiles { return nil } return err } var deleteVerb string if file.Tracked { - deleteVerb = "checkout" + deleteVerb = gui.Tr.SLocalize("checkout") } else { - deleteVerb = "delete" + deleteVerb = gui.Tr.SLocalize("delete") } - return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error { + message := gui.Tr.TemplateLocalize( + "SureTo", + Teml{ + "deleteVerb": deleteVerb, + "fileName": file.Name, + }, + ) + return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", message, func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.RemoveFile(file); err != nil { panic(err) } @@ -139,7 +140,7 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(g, err.Error()) } if file.Tracked { - return gui.createErrorPanel(g, "Cannot ignore tracked files") + return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles")) } gui.GitCommand.Ignore(file.Name) return gui.refreshFiles(g) @@ -147,27 +148,27 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error { optionsMap := map[string]string{ - "← → ↑ ↓": "navigate", - "S": "stash files", - "c": "commit changes", - "o": "open", - "i": "ignore", - "d": "delete", - "space": "toggle staged", - "R": "refresh", - "t": "add patch", - "e": "edit", - "PgUp/PgDn": "scroll", + "← → ↑ ↓": gui.Tr.SLocalize("navigate"), + "S": gui.Tr.SLocalize("stashFiles"), + "c": gui.Tr.SLocalize("CommitChanges"), + "o": gui.Tr.SLocalize("open"), + "i": gui.Tr.SLocalize("ignore"), + "d": gui.Tr.SLocalize("delete"), + "space": gui.Tr.SLocalize("toggleStaged"), + "R": gui.Tr.SLocalize("refresh"), + "t": gui.Tr.SLocalize("addPatch"), + "e": gui.Tr.SLocalize("edit"), + "PgUp/PgDn": gui.Tr.SLocalize("scroll"), } if gui.State.HasMergeConflicts { - optionsMap["a"] = "abort merge" - optionsMap["m"] = "resolve merge conflicts" + optionsMap["a"] = gui.Tr.SLocalize("abortMerge") + optionsMap["m"] = gui.Tr.SLocalize("resolveMergeConflicts") } if file == nil { return gui.renderOptionsMap(g, optionsMap) } if file.Tracked { - optionsMap["d"] = "checkout" + optionsMap["d"] = gui.Tr.SLocalize("checkout") } return gui.renderOptionsMap(g, optionsMap) } @@ -175,10 +176,10 @@ func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error { func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { file, err := gui.getSelectedFile(g) if err != nil { - if err != errNoFiles { + if err != gui.Errors.ErrNoFiles { return err } - gui.renderString(g, "main", "No changed files") + gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles")) return gui.renderfilesOptions(g, nil) } gui.renderfilesOptions(g, &file) @@ -193,7 +194,7 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts { - return gui.createErrorPanel(g, "There are no staged files to commit") + return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) } commitMessageView := gui.getCommitMessageView(g) g.Update(func(g *gocui.Gui) error { @@ -208,7 +209,7 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { // their editor rather than via the popup panel func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error { if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts { - return gui.createErrorPanel(g, "There are no staged files to commit") + return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) } gui.PrepareSubProcess(g, "git", "commit") return nil @@ -222,7 +223,7 @@ func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) error { } gui.SubProcess = sub g.Update(func(g *gocui.Gui) error { - return ErrSubProcess + return gui.Errors.ErrSubProcess }) return nil } @@ -230,7 +231,7 @@ func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) error { func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (*exec.Cmd, error)) error { file, err := gui.getSelectedFile(g) if err != nil { - if err != errNoFiles { + if err != gui.Errors.ErrNoFiles { return err } return nil @@ -241,7 +242,7 @@ func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) ( } if sub != nil { gui.SubProcess = sub - return ErrSubProcess + return gui.Errors.ErrSubProcess } return nil } @@ -303,10 +304,10 @@ func (gui *Gui) renderFile(file commands.File, filesView *gocui.View) { func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { item, err := gui.getSelectedFile(g) if err != nil { - if err != errNoFiles { + if err != gui.Errors.ErrNoFiles { return "", err } - return "", gui.renderString(g, "main", "No file to display") + return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay")) } cat, err := gui.GitCommand.CatFile(item.Name) if err != nil { @@ -333,7 +334,7 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error { } func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { - gui.createMessagePanel(g, v, "", "Pulling...") + gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PullWait")) go func() { if err := gui.GitCommand.Pull(); err != nil { gui.createErrorPanel(g, err.Error()) @@ -348,7 +349,7 @@ func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error { - gui.createMessagePanel(g, v, "", "Pushing...") + gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PushWait")) go func() { branchName := gui.State.Branches[0].Name if err := gui.GitCommand.Push(branchName); err != nil { @@ -369,13 +370,13 @@ func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error { } file, err := gui.getSelectedFile(g) if err != nil { - if err != errNoFiles { + if err != gui.Errors.ErrNoFiles { return err } return nil } if !file.HasMergeConflicts { - return gui.createErrorPanel(g, "This file has no merge conflicts") + return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons")) } gui.switchFocus(g, v, mergeView) return gui.refreshMergePanel(g) @@ -385,13 +386,13 @@ func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.AbortMerge(); err != nil { return gui.createErrorPanel(g, err.Error()) } - gui.createMessagePanel(g, v, "", "Merge aborted") + gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted")) gui.refreshStatus(g) return gui.refreshFiles(g) } func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error { - return gui.createConfirmationPanel(g, v, "Clear file panel", "Are you sure you want `reset --hard HEAD`? You may lose changes", func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("ClearFilePanel"), gui.Tr.SLocalize("SureResetHardHead"), func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.ResetHard(); err != nil { gui.createErrorPanel(g, err.Error()) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 3b7876ede..a5bdb9e07 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -19,16 +19,38 @@ import ( "github.com/golang-collections/collections/stack" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/i18n" ) // OverlappingEdges determines if panel edges overlap var OverlappingEdges = false -// ErrSubProcess tells us we're switching to a subprocess so we need to -// close the Gui until it is finished -var ( - ErrSubProcess = errors.New("running subprocess") -) +// SentinelErrors are the errors that have special meaning and need to be checked +// by calling functions. The less of these, the better +type SentinelErrors struct { + ErrSubProcess error + ErrNoFiles error +} + +// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here +// because we can't do package-scoped errors with localization, and also because +// it seems like package-scoped variables are bad in general +// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables +// In the future it would be good to implement some of the recommendations of +// that article. For now, if we don't need an error to be a sentinel, we will just +// define it inline. This has implications for error messages that pop up everywhere +// in that we'll be duplicating the default values. We may need to look at +// having a default localisation bundle defined, and just using keys-only when +// localising things in the code. +func (gui *Gui) GenerateSentinelErrors() { + gui.Errors = SentinelErrors{ + ErrSubProcess: errors.New(gui.Tr.SLocalize("RunningSubprocess")), + ErrNoFiles: errors.New(gui.Tr.SLocalize("NoChangedFiles")), + } +} + +// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize +type Teml i18n.Teml // Gui wraps the gocui Gui object which handles rendering and events type Gui struct { @@ -39,6 +61,8 @@ type Gui struct { Version string SubProcess *exec.Cmd State guiState + Tr *i18n.Localizer + Errors SentinelErrors } type guiState struct { @@ -57,7 +81,7 @@ type guiState struct { } // NewGui builds a new gui handler -func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, version string) (*Gui, error) { +func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, version string) (*Gui, error) { initialState := guiState{ Files: make([]commands.File, 0), PreviousView: "files", @@ -71,13 +95,18 @@ func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *comm Version: version, } - return &Gui{ + gui := &Gui{ Log: log, GitCommand: gitCommand, OSCommand: oSCommand, Version: version, State: initialState, - }, nil + Tr: tr, + } + + gui.GenerateSentinelErrors() + + return gui, nil } func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error { @@ -133,7 +162,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - v.Title = "Not enough space to render panels" + v.Title = gui.Tr.SLocalize("NotEnoughSpace") v.Wrap = true } return nil @@ -152,7 +181,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - v.Title = "Diff" + v.Title = gui.Tr.SLocalize("DiffTitle") v.Wrap = true v.FgColor = gocui.ColorWhite } @@ -161,7 +190,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - v.Title = "Status" + v.Title = gui.Tr.SLocalize("StatusTitle") v.FgColor = gocui.ColorWhite } @@ -171,7 +200,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } filesView.Highlight = true - filesView.Title = "Files" + filesView.Title = gui.Tr.SLocalize("FilesTitle") v.FgColor = gocui.ColorWhite } @@ -179,7 +208,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - v.Title = "Branches" + v.Title = gui.Tr.SLocalize("BranchesTitle") v.FgColor = gocui.ColorWhite } @@ -187,7 +216,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - v.Title = "Commits" + v.Title = gui.Tr.SLocalize("CommitsTitle") v.FgColor = gocui.ColorWhite } @@ -195,7 +224,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - v.Title = "Stash" + v.Title = gui.Tr.SLocalize("StashTitle") v.FgColor = gocui.ColorWhite } @@ -214,7 +243,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } g.SetViewOnBottom("commitMessage") - commitMessageView.Title = "Commit message" + commitMessageView.Title = gui.Tr.SLocalize("CommitMessage") commitMessageView.FgColor = gocui.ColorWhite commitMessageView.Editable = true } @@ -310,7 +339,7 @@ func (gui *Gui) RunWithSubprocesses() { if err := gui.Run(); err != nil { if err == gocui.ErrQuit { break - } else if err == ErrSubProcess { + } else if err == gui.Errors.ErrSubProcess { gui.SubProcess.Stdin = os.Stdin gui.SubProcess.Stdout = os.Stdout gui.SubProcess.Stderr = os.Stderr diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index 81e37f593..1efea16da 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -232,11 +232,11 @@ func (gui *Gui) switchToMerging(g *gocui.Gui) error { func (gui *Gui) renderMergeOptions(g *gocui.Gui) error { return gui.renderOptionsMap(g, map[string]string{ - "↑ ↓": "select hunk", - "← →": "navigate conflicts", - "space": "pick hunk", - "b": "pick both hunks", - "z": "undo", + "↑ ↓": gui.Tr.SLocalize("selectHunk"), + "← →": gui.Tr.SLocalize("navigateConflicts"), + "space": gui.Tr.SLocalize("pickHunk"), + "b": gui.Tr.SLocalize("pickBothHunks"), + "z": gui.Tr.SLocalize("undo"), }) } diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index b4fb11902..0c323e254 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -33,10 +33,10 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry { func (gui *Gui) renderStashOptions(g *gocui.Gui) error { return gui.renderOptionsMap(g, map[string]string{ - "space": "apply", - "g": "pop", - "d": "drop", - "← → ↑ ↓": "navigate", + "space": gui.Tr.SLocalize("apply"), + "g": gui.Tr.SLocalize("pop"), + "d": gui.Tr.SLocalize("drop"), + "← → ↑ ↓": gui.Tr.SLocalize("navigate"), }) } @@ -47,7 +47,7 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { go func() { stashEntry := gui.getSelectedStashEntry(v) if stashEntry == nil { - gui.renderString(g, "main", "No stash entries") + gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries")) return } diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index) @@ -65,7 +65,9 @@ func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error { - return gui.createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry?", func(g *gocui.Gui, v *gocui.View) error { + title := gui.Tr.SLocalize("StashDrop") + message := gui.Tr.SLocalize("SureDropStashEntry") + return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error { return gui.stashDo(g, v, "drop") }, nil) } @@ -73,7 +75,13 @@ func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error { stashEntry := gui.getSelectedStashEntry(v) if stashEntry == nil { - return gui.createErrorPanel(g, "No stash to "+method) + errorMessage := gui.Tr.TemplateLocalize( + "NoStashTo", + Teml{ + "method": method, + }, + ) + return gui.createErrorPanel(g, errorMessage) } if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil { gui.createErrorPanel(g, err.Error()) @@ -84,9 +92,9 @@ func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error { func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error { if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 { - return gui.createErrorPanel(g, "You have no tracked/staged files to stash") + return gui.createErrorPanel(g, gui.Tr.SLocalize("NoTrackedStagedFilesStash")) } - gui.createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { + gui.createPromptPanel(g, filesView, gui.Tr.SLocalize("StashChanges"), func(g *gocui.Gui, v *gocui.View) error { if err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil { gui.createErrorPanel(g, err.Error()) } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 331e27975..95324ecde 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -29,7 +29,13 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { break } if i == len(cyclableViews)-1 { - gui.Log.Info(v.Name() + " is not in the list of views") + message := gui.Tr.TemplateLocalize( + "IssntListOfViews", + Teml{ + "name": v.Name(), + }, + ) + gui.Log.Info(message) return nil } } @@ -52,7 +58,13 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { break } if i == len(cyclableViews)-1 { - gui.Log.Info(v.Name() + " is not in the list of views") + message := gui.Tr.TemplateLocalize( + "IssntListOfViews", + Teml{ + "name": v.Name(), + }, + ) + gui.Log.Info(message) return nil } } @@ -87,7 +99,7 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { case "stash": return gui.handleStashEntrySelect(g, v) default: - panic("No view matching newLineFocused switch statement") + panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement")) } } @@ -105,11 +117,23 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { // we should never stack confirmation panels if oldView != nil && oldView.Name() != "confirmation" { oldView.Highlight = false - gui.Log.Info("setting previous view to:", oldView.Name()) + message := gui.Tr.TemplateLocalize( + "settingPreviewsViewTo", + Teml{ + "oldViewName": oldView.Name(), + }, + ) + gui.Log.Info(message) gui.State.PreviousView = oldView.Name() } newView.Highlight = true - gui.Log.Info("new focused view is " + newView.Name()) + message := gui.Tr.TemplateLocalize( + "newFocusedViewIs", + Teml{ + "newFocusedView": newView.Name(), + }, + ) + gui.Log.Info(message) if _, err := g.SetCurrentView(newView.Name()); err != nil { return err } diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go new file mode 100644 index 000000000..0ba562a22 --- /dev/null +++ b/pkg/i18n/dutch.go @@ -0,0 +1,291 @@ +package i18n + +import ( + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" +) + +// addDutch will add all dutch translations +func addDutch(i18nObject *i18n.Bundle) { + + // add the translations + i18nObject.AddMessages(language.Dutch, + &i18n.Message{ + ID: "NotEnoughSpace", + Other: "Niet genoeg ruimte om de panelen te renderen", + }, &i18n.Message{ + ID: "DiffTitle", + Other: "Diff", + }, &i18n.Message{ + ID: "FilesTitle", + Other: "Bestanden", + }, &i18n.Message{ + ID: "BranchesTitle", + Other: "Branches", + }, &i18n.Message{ + ID: "CommitsTitle", + Other: "Commits", + }, &i18n.Message{ + ID: "StashTitle", + Other: "Stash", + }, &i18n.Message{ + ID: "CommitMessage", + Other: "Commit Bericht", + }, &i18n.Message{ + ID: "CommitChanges", + Other: "Commit Veranderingen", + }, &i18n.Message{ + ID: "StatusTitle", + Other: "Status", + }, &i18n.Message{ + ID: "navigate", + Other: "navigeer", + }, &i18n.Message{ + ID: "stashFiles", + Other: "stash-bestanden", + }, &i18n.Message{ + ID: "open", + Other: "open", + }, &i18n.Message{ + ID: "ignore", + Other: "negeren", + }, &i18n.Message{ + ID: "delete", + Other: "verwijderen", + }, &i18n.Message{ + ID: "toggleStaged", + Other: "toggle staged", + }, &i18n.Message{ + ID: "refresh", + Other: "verversen", + }, &i18n.Message{ + ID: "addPatch", + Other: "verandering toevoegen", + }, &i18n.Message{ + ID: "edit", + Other: "veranderen", + }, &i18n.Message{ + ID: "scroll", + Other: "scroll", + }, &i18n.Message{ + ID: "abortMerge", + Other: "samenvoegen afbreken", + }, &i18n.Message{ + ID: "resolveMergeConflicts", + Other: "verhelp samenvoegen fouten", + }, &i18n.Message{ + ID: "checkout", + Other: "uitchecken", + }, &i18n.Message{ + ID: "NoChangedFiles", + Other: "Geen Bestanden verandert", + }, &i18n.Message{ + ID: "FileHasNoUnstagedChanges", + Other: "Het bestand heeft geen unstaged veranderingen om toe te voegen", + }, &i18n.Message{ + ID: "CannotGitAdd", + Other: "Kan commando niet uitvoeren git add --path untracked files", + }, &i18n.Message{ + ID: "CantIgnoreTrackFiles", + Other: "Kan gevolgde bestanden niet negeren", + }, &i18n.Message{ + ID: "NoStagedFilesToCommit", + Other: "Er zijn geen staged bestanden om te commiten", + }, &i18n.Message{ + ID: "NoFilesDisplay", + Other: "Geen bestanden om te laten zien", + }, &i18n.Message{ + ID: "PullWait", + Other: "Pulling...", + }, &i18n.Message{ + ID: "PushWait", + Other: "Pushing...", + }, &i18n.Message{ + ID: "FileNoMergeCons", + Other: "Dit bestand heeft geen merge conflicten", + }, &i18n.Message{ + ID: "SureResetHardHead", + Other: "Weet je het zeker dat je `reset --hard HEAD` wil uitvoeren? het kan dat je hierdoor bestanden verliest", + }, &i18n.Message{ + ID: "SureTo", + Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijdert)", + }, &i18n.Message{ + ID: "AlreadyCheckedOutBranch", + Other: "Je hebt uitgecheckt op deze branch", + }, &i18n.Message{ + ID: "SureForceCheckout", + Other: "Weet je zeker dat je het uitchecken wil forceren? al je locale verandering zullen worden verwijdert", + }, &i18n.Message{ + ID: "ForceCheckoutBranch", + Other: "Forceer uitchecken op deze branch", + }, &i18n.Message{ + ID: "BranchName", + Other: "Branch naam", + }, &i18n.Message{ + ID: "NewBranchNameBranchOff", + Other: "Nieuw branch naam (Branch is afgeleid van {{.branchName}})", + }, &i18n.Message{ + ID: "CantDeleteCheckOutBranch", + Other: "Je kan een uitgecheckte branch niet verwijderen!", + }, &i18n.Message{ + ID: "DeleteBranch", + Other: "Verwijder branch", + }, &i18n.Message{ + ID: "DeleteBranchMessage", + Other: "Weet je zeker dat je {{.selectedBranchName}} branch wil verwijderen?", + }, &i18n.Message{ + ID: "CantMergeBranchIntoItself", + Other: "Je kan niet een branch in zichzelf mergen", + }, &i18n.Message{ + ID: "forceCheckout", + Other: "forceer checkout", + }, &i18n.Message{ + ID: "merge", + Other: "merge", + }, &i18n.Message{ + ID: "checkoutByName", + Other: "uitchecken bij naam", + }, &i18n.Message{ + ID: "newBranch", + Other: "nieuwe branch", + }, &i18n.Message{ + ID: "deleteBranch", + Other: "verwijder branch", + }, &i18n.Message{ + ID: "NoBranchesThisRepo", + Other: "Geen branches voor deze repo", + }, &i18n.Message{ + ID: "NoTrackingThisBranch", + Other: "deze branch wordt niet gevolgd", + }, &i18n.Message{ + ID: "CommitWithoutMessageErr", + Other: "Je kan geen commit maken zonder commit bericht", + }, &i18n.Message{ + ID: "CloseConfirm", + Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen", + }, &i18n.Message{ + ID: "SureResetThisCommit", + Other: "Weet je het zeker dat je wil resetten naar deze commit?", + }, &i18n.Message{ + ID: "ResetToCommit", + Other: "Reset Naar Commit", + }, &i18n.Message{ + ID: "squashDown", + Other: "squash beneden", + }, &i18n.Message{ + ID: "rename", + Other: "hernoem", + }, &i18n.Message{ + ID: "resetToThisCommit", + Other: "reset naar deze commit", + }, &i18n.Message{ + ID: "fixupCommit", + Other: "Fixup commit", + }, &i18n.Message{ + ID: "NoCommitsThisBranch", + Other: "Er zijn geen commits voor deze branch", + }, &i18n.Message{ + ID: "OnlySquashTopmostCommit", + Other: "Kan alleen bovenste commit squashen", + }, &i18n.Message{ + ID: "YouNoCommitsToSquash", + Other: "Je hebt geen commits om mee te squashen", + }, &i18n.Message{ + ID: "CantFixupWhileUnstagedChanges", + Other: "Kan geen Fixup uitvoeren op unstaged veranderingen", + }, &i18n.Message{ + ID: "Fixup", + Other: "Fixup", + }, &i18n.Message{ + ID: "SureFixupThisCommit", + Other: "Weet je zeker dat je fixup wil uitvoeren op deze commit? De commit hieronder zol worden squashed in deze", + }, &i18n.Message{ + ID: "OnlyRenameTopCommit", + Other: "Je kan alleen de bovenste commit hernoemen", + }, &i18n.Message{ + ID: "RenameCommit", + Other: "Hernoem Commit", + }, &i18n.Message{ + ID: "PotentialErrInGetselectedCommit", + Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)", + }, &i18n.Message{ + ID: "NoCommitsThisBranch", + Other: "Geen commits voor deze branch", + }, &i18n.Message{ + ID: "Error", + Other: "Fout", + }, &i18n.Message{ + ID: "resizingPopupPanel", + Other: "resizen popup paneel", + }, &i18n.Message{ + ID: "RunningSubprocess", + Other: "subprocess lopend", + }, &i18n.Message{ + ID: "selectHunk", + Other: "selecteer Hunk", + }, &i18n.Message{ + ID: "navigateConflicts", + Other: "navigeer conflicts", + }, &i18n.Message{ + ID: "pickHunk", + Other: "kies Hunk", + }, &i18n.Message{ + ID: "pickBothHunks", + Other: "kies bijde hunks", + }, &i18n.Message{ + ID: "undo", + Other: "ongedaan maken", + }, &i18n.Message{ + ID: "pop", + Other: "pop", + }, &i18n.Message{ + ID: "drop", + Other: "drop", + }, &i18n.Message{ + ID: "apply", + Other: "toepassen", + }, &i18n.Message{ + ID: "NoStashEntries", + Other: "Geen stash items", + }, &i18n.Message{ + ID: "StashDrop", + Other: "Stash drop", + }, &i18n.Message{ + ID: "SureDropStashEntry", + Other: "Weet je het zeker dat je deze stash entry wil laten vallen?", + }, &i18n.Message{ + ID: "NoStashTo", + Other: "Geen stash voor {{.method}}", + }, &i18n.Message{ + ID: "NoTrackedStagedFilesStash", + Other: "Je hebt geen tracked/staged bestanden om te laten stashen", + }, &i18n.Message{ + ID: "StashChanges", + Other: "Stash veranderingen", + }, &i18n.Message{ + ID: "IssntListOfViews", + Other: "{{.name}} is niet in de lijst van weergaves", + }, &i18n.Message{ + ID: "NoViewMachingNewLineFocusedSwitchStatement", + Other: "Er machen geen weergave met de newLineFocused switch declaratie", + }, &i18n.Message{ + ID: "settingPreviewsViewTo", + Other: "vorige weergave instellen op: {{.oldViewName}}", + }, &i18n.Message{ + ID: "newFocusedViewIs", + Other: "nieuw gefocussed weergave is {{.newFocusedView}}", + }, &i18n.Message{ + ID: "CantCloseConfirmationPrompt", + Other: "Kon de bevestiging prompt niet sluiten: {{.error}}", + }, &i18n.Message{ + ID: "NoChangedFiles", + Other: "Geen veranderde files", + }, &i18n.Message{ + ID: "ClearFilePanel", + Other: "maak bestandsvenster leeg", + }, &i18n.Message{ + ID: "MergeAborted", + Other: "Merge afgebroken", + }, + ) +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go new file mode 100644 index 000000000..6e2af8e22 --- /dev/null +++ b/pkg/i18n/english.go @@ -0,0 +1,299 @@ +/* + +Todo list when making a new translation +- Copy this file and rename it to the language you want to translate to like someLanguage.go +- Change the addEnglish() name to the language you want to translate to like addSomeLanguage() +- change the first function argument of i18nObject.AddMessages( to the language you want to translate to like language.SomeLanguage +- Remove this todo and the about section + +*/ + +package i18n + +import ( + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" +) + +func addEnglish(i18nObject *i18n.Bundle) { + + i18nObject.AddMessages(language.English, + &i18n.Message{ + ID: "NotEnoughSpace", + Other: "Not enough space to render panels", + }, &i18n.Message{ + ID: "DiffTitle", + Other: "Diff", + }, &i18n.Message{ + ID: "FilesTitle", + Other: "Files", + }, &i18n.Message{ + ID: "BranchesTitle", + Other: "Branches", + }, &i18n.Message{ + ID: "CommitsTitle", + Other: "Commits", + }, &i18n.Message{ + ID: "StashTitle", + Other: "Stash", + }, &i18n.Message{ + ID: "CommitMessage", + Other: "Commit message", + }, &i18n.Message{ + ID: "CommitChanges", + Other: "commit changes", + }, &i18n.Message{ + ID: "StatusTitle", + Other: "Status", + }, &i18n.Message{ + ID: "navigate", + Other: "navigate", + }, &i18n.Message{ + ID: "stashFiles", + Other: "stash files", + }, &i18n.Message{ + ID: "open", + Other: "open", + }, &i18n.Message{ + ID: "ignore", + Other: "ignore", + }, &i18n.Message{ + ID: "delete", + Other: "delete", + }, &i18n.Message{ + ID: "toggleStaged", + Other: "toggle staged", + }, &i18n.Message{ + ID: "refresh", + Other: "refresh", + }, &i18n.Message{ + ID: "addPatch", + Other: "add path", + }, &i18n.Message{ + ID: "edit", + Other: "edit", + }, &i18n.Message{ + ID: "scroll", + Other: "scroll", + }, &i18n.Message{ + ID: "abortMerge", + Other: "abort merge", + }, &i18n.Message{ + ID: "resolveMergeConflicts", + Other: "resolve merge conflicts", + }, &i18n.Message{ + ID: "checkout", + Other: "checkout", + }, &i18n.Message{ + ID: "NoChangedFiles", + Other: "No changed files", + }, &i18n.Message{ + ID: "FileHasNoUnstagedChanges", + Other: "File has no unstaged changes to add", + }, &i18n.Message{ + ID: "CannotGitAdd", + Other: "Cannot git add --patch untracked files", + }, &i18n.Message{ + ID: "CantIgnoreTrackFiles", + Other: "Cannot ignore tracked files", + }, &i18n.Message{ + ID: "NoStagedFilesToCommit", + Other: "There are no staged files to commit", + }, &i18n.Message{ + ID: "NoFilesDisplay", + Other: "No file to display", + }, &i18n.Message{ + ID: "PullWait", + Other: "Pulling...", + }, &i18n.Message{ + ID: "PushWait", + Other: "Pushing...", + }, &i18n.Message{ + ID: "FileNoMergeCons", + Other: "This file has no merge conflicts", + }, &i18n.Message{ + ID: "SureResetHardHead", + Other: "Are you sure you want `reset --hard HEAD`? You may lose changes", + }, &i18n.Message{ + ID: "SureTo", + Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?", + }, &i18n.Message{ + ID: "AlreadyCheckedOutBranch", + Other: "You have already checked out this branch", + }, &i18n.Message{ + ID: "SureForceCheckout", + Other: "Are you sure you want force checkout? You will lose all local changes", + }, &i18n.Message{ + ID: "ForceCheckoutBranch", + Other: "Force Checkout Branch", + }, &i18n.Message{ + ID: "BranchName", + Other: "Branch name", + }, &i18n.Message{ + ID: "NewBranchNameBranchOff", + Other: "New Branch Name (Branch is off of {{.branchName}})", + }, &i18n.Message{ + ID: "CantDeleteCheckOutBranch", + Other: "You cannot delete the checked out branch!", + }, &i18n.Message{ + ID: "DeleteBranch", + Other: "Delete Branch", + }, &i18n.Message{ + ID: "DeleteBranchMessage", + Other: "Are you sure you want delete the branch {{.selectedBranchName}} ?", + }, &i18n.Message{ + ID: "CantMergeBranchIntoItself", + Other: "You cannot merge a branch into itself", + }, &i18n.Message{ + ID: "forceCheckout", + Other: "force checkout", + }, &i18n.Message{ + ID: "merge", + Other: "merge", + }, &i18n.Message{ + ID: "checkoutByName", + Other: "checkout by name", + }, &i18n.Message{ + ID: "newBranch", + Other: "new branch", + }, &i18n.Message{ + ID: "deleteBranch", + Other: "delete branch", + }, &i18n.Message{ + ID: "NoBranchesThisRepo", + Other: "No branches for this repo", + }, &i18n.Message{ + ID: "NoTrackingThisBranch", + Other: "There is no tracking for this branch", + }, &i18n.Message{ + ID: "CommitWithoutMessageErr", + Other: "You cannot commit without a commit message", + }, &i18n.Message{ + ID: "CloseConfirm", + Other: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm", + }, &i18n.Message{ + ID: "SureResetThisCommit", + Other: "Are you sure you want to reset to this commit?", + }, &i18n.Message{ + ID: "ResetToCommit", + Other: "Reset To Commit", + }, &i18n.Message{ + ID: "squashDown", + Other: "squash down", + }, &i18n.Message{ + ID: "rename", + Other: "rename", + }, &i18n.Message{ + ID: "resetToThisCommit", + Other: "reset to this commit", + }, &i18n.Message{ + ID: "fixupCommit", + Other: "fixup commit", + }, &i18n.Message{ + ID: "NoCommitsThisBranch", + Other: "No commits for this branch", + }, &i18n.Message{ + ID: "OnlySquashTopmostCommit", + Other: "Can only squash topmost commit", + }, &i18n.Message{ + ID: "YouNoCommitsToSquash", + Other: "You have no commits to squash with", + }, &i18n.Message{ + ID: "CantFixupWhileUnstagedChanges", + Other: "Can't fixup while there are unstaged changes", + }, &i18n.Message{ + ID: "Fixup", + Other: "Fixup", + }, &i18n.Message{ + ID: "SureFixupThisCommit", + Other: "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", + }, &i18n.Message{ + ID: "OnlyRenameTopCommit", + Other: "Can only rename topmost commit", + }, &i18n.Message{ + ID: "RenameCommit", + Other: "Rename Commit", + }, &i18n.Message{ + ID: "PotentialErrInGetselectedCommit", + Other: "potential error in getSelected Commit (mismatched ui and state)", + }, &i18n.Message{ + ID: "NoCommitsThisBranch", + Other: "No commits for this branch", + }, &i18n.Message{ + ID: "Error", + Other: "Error", + }, &i18n.Message{ + ID: "resizingPopupPanel", + Other: "resizing popup panel", + }, &i18n.Message{ + ID: "RunningSubprocess", + Other: "running subprocess", + }, &i18n.Message{ + ID: "selectHunk", + Other: "select hunk", + }, &i18n.Message{ + ID: "navigateConflicts", + Other: "navigate conflicts", + }, &i18n.Message{ + ID: "pickHunk", + Other: "pick hunk", + }, &i18n.Message{ + ID: "pickBothHunks", + Other: "pick both hunks", + }, &i18n.Message{ + ID: "undo", + Other: "undo", + }, &i18n.Message{ + ID: "pop", + Other: "pop", + }, &i18n.Message{ + ID: "drop", + Other: "drop", + }, &i18n.Message{ + ID: "apply", + Other: "apply", + }, &i18n.Message{ + ID: "NoStashEntries", + Other: "No stash entries", + }, &i18n.Message{ + ID: "StashDrop", + Other: "Stash drop", + }, &i18n.Message{ + ID: "SureDropStashEntry", + Other: "Are you sure you want to drop this stash entry?", + }, &i18n.Message{ + ID: "NoStashTo", + Other: "No stash to {{.method}}", + }, &i18n.Message{ + ID: "NoTrackedStagedFilesStash", + Other: "You have no tracked/staged files to stash", + }, &i18n.Message{ + ID: "StashChanges", + Other: "Stash changes", + }, &i18n.Message{ + ID: "IssntListOfViews", + Other: "{{.name}} is not in the list of views", + }, &i18n.Message{ + ID: "NoViewMachingNewLineFocusedSwitchStatement", + Other: "No view matching newLineFocused switch statement", + }, &i18n.Message{ + ID: "settingPreviewsViewTo", + Other: "setting previous view to: {{.oldViewName}}", + }, &i18n.Message{ + ID: "newFocusedViewIs", + Other: "new focused view is {{.newFocusedView}}", + }, &i18n.Message{ + ID: "CantCloseConfirmationPrompt", + Other: "Could not close confirmation prompt: {{.error}}", + }, &i18n.Message{ + ID: "NoChangedFiles", + Other: "No changed files", + }, &i18n.Message{ + ID: "ClearFilePanel", + Other: "Clear file panel", + }, &i18n.Message{ + ID: "MergeAborted", + Other: "Merge aborted", + }, + ) +} diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go new file mode 100644 index 000000000..9102ff0b3 --- /dev/null +++ b/pkg/i18n/i18n.go @@ -0,0 +1,84 @@ +package i18n + +import ( + "github.com/Sirupsen/logrus" + "github.com/cloudfoundry/jibber_jabber" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" +) + +// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize +type Teml map[string]interface{} + +// Localizer will translate a message into the user's language +type Localizer struct { + i18nLocalizer *i18n.Localizer + language string + Log *logrus.Logger +} + +// NewLocalizer creates a new Localizer +func NewLocalizer(log *logrus.Logger) (*Localizer, error) { + + // detect the user's language + userLang, err := jibber_jabber.DetectLanguage() + if err != nil { + return nil, err + } + log.Info("language: " + userLang) + + // create a i18n bundle that can be used to add translations and other things + i18nBundle := &i18n.Bundle{DefaultLanguage: language.English} + + addBundles(i18nBundle) + + // return the new localizer that can be used to translate text + i18nLocalizer := i18n.NewLocalizer(i18nBundle, userLang) + + localizer := &Localizer{ + i18nLocalizer: i18nLocalizer, + language: userLang, + Log: log, + } + + return localizer, nil +} + +// Localize handels the translations +// expects i18n.LocalizeConfig as input: https://godoc.org/github.com/nicksnyder/go-i18n/v2/i18n#Localizer.MustLocalize +// output: translated string +func (l *Localizer) Localize(config *i18n.LocalizeConfig) string { + return l.i18nLocalizer.MustLocalize(config) +} + +// SLocalize (short localize) is for 1 line localizations +// ID: The id that is used in the .toml translation files +// Other: the default message it needs to return if there is no translation found or the system is english +func (l *Localizer) SLocalize(ID string) string { + return l.Localize(&i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: ID, + }, + }) +} + +// TemplateLocalize allows the Other input to be dynamic +func (l *Localizer) TemplateLocalize(ID string, TemplateData map[string]interface{}) string { + return l.Localize(&i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: ID, + }, + TemplateData: TemplateData, + }) +} + +// GetLanguage returns the currently selected language, e.g 'en' +func (l *Localizer) GetLanguage() string { + return l.language +} + +// add translation file(s) +func addBundles(i18nBundle *i18n.Bundle) { + addDutch(i18nBundle) + addEnglish(i18nBundle) +}