diff --git a/internal/i18n/locale.go b/internal/i18n/locale.go new file mode 100644 index 0000000..564b3e5 --- /dev/null +++ b/internal/i18n/locale.go @@ -0,0 +1,125 @@ +package i18n + +import ( + "fmt" + "github.com/thomiceli/opengist/internal/i18n/locales" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "golang.org/x/text/language/display" + "gopkg.in/yaml.v3" + "html/template" + "io" + "io/fs" + "path/filepath" + "strings" +) + +var title = cases.Title(language.English) +var Locales = NewLocaleStore() + +type LocaleStore struct { + Locales map[string]*Locale +} + +type Locale struct { + Code string + Name string + Messages map[string]string +} + +// NewLocaleStore creates a new LocaleStore +func NewLocaleStore() *LocaleStore { + return &LocaleStore{ + Locales: make(map[string]*Locale), + } +} + +// loadLocaleFromYAML loads a single Locale from a given YAML file +func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error { + a, err := locales.Files.Open(path) + if err != nil { + return err + } + data, err := io.ReadAll(a) + if err != nil { + return err + } + + tag, err := language.Parse(localeCode) + if err != nil { + return err + } + + name := display.Self.Name(tag) + if tag == language.AmericanEnglish { + name = "English" + } + + locale := &Locale{ + Code: localeCode, + Name: title.String(name), + Messages: make(map[string]string), + } + + err = yaml.Unmarshal(data, &locale.Messages) + if err != nil { + return err + } + + store.Locales[localeCode] = locale + return nil +} + +func (store *LocaleStore) LoadAll() error { + return fs.WalkDir(locales.Files, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + localeKey := strings.TrimSuffix(path, filepath.Ext(path)) + err := store.loadLocaleFromYAML(localeKey, path) + if err != nil { + return err + } + } + return nil + }) +} + +func (store *LocaleStore) GetLocale(lang string) (*Locale, error) { + _, ok := store.Locales[lang] + if !ok { + return nil, fmt.Errorf("locale %s not found", lang) + } + + return store.Locales[lang], nil +} + +func (store *LocaleStore) HasLocale(lang string) bool { + _, ok := store.Locales[lang] + return ok +} + +func (store *LocaleStore) MatchTag(langs []language.Tag) string { + for _, lang := range langs { + if store.HasLocale(lang.String()) { + return lang.String() + } + } + + return "en-US" +} + +func (l *Locale) Tr(key string, args ...any) template.HTML { + message := l.Messages[key] + + if message == "" { + return Locales.Locales["en-US"].Tr(key, args...) + } + + if len(args) == 0 { + return template.HTML(message) + } + + return template.HTML(fmt.Sprintf(message, args...)) +} diff --git a/internal/i18n/locales/en-US.yml b/internal/i18n/locales/en-US.yml new file mode 100644 index 0000000..00d862d --- /dev/null +++ b/internal/i18n/locales/en-US.yml @@ -0,0 +1,177 @@ +gist.public: Public +gist.unlisted: Unlisted +gist.private: Private + +gist.header.like: Like +gist.header.unlike: Unlike +gist.header.fork: Fork +gist.header.edit: Edit +gist.header.delete: Delete +gist.header.forked-from: Forked from +gist.header.last-active: Last active +gist.header.select-tab: Select a tab +gist.header.code: Code +gist.header.revisions: Revisions +gist.header.revision: Revision +gist.header.clone-http: Clone via %s +gist.header.clone-http-help: Clone with Git using HTTP basic authentication. +gist.header.clone-ssh: Clone via SSH +gist.header.clone-ssh-help: Clone with Git using an SSH key. +gist.header.share: Share +gist.header.share-help: Copy shareable link for this gist. +gist.header.download-zip: Download ZIP + +gist.raw: Raw +gist.file-truncated: This file has been truncated. +gist.watch-full-file: View the full file. +gist.file-not-valid: This file is not a valid CSV file. +gist.no-content: No content + +gist.new.new_gist: New gist +gist.new.title: Title +gist.new.description: Description +gist.new.filename-with-extension: Filename with extension +gist.new.indent-mode: Indent mode +gist.new.indent-mode-space: Space +gist.new.indent-mode-tab: Tab +gist.new.indent-size: Indent size +gist.new.wrap-mode: Wrap mode +gist.new.wrap-mode-no: No wrap +gist.new.wrap-mode-soft: Soft wrap +gist.new.add-file: Add file +gist.new.create-public-button: Create public gist +gist.new.create-unlisted-button: Create unlisted gist +gist.new.create-private-button: Create private gist + +gist.edit.editing: Editing +gist.edit.change-visibility: Make +gist.edit.delete: Delete +gist.edit.cancel: Cancel +gist.edit.save: Save + +gist.list.joined: Joined +gist.list.all: All gists +gist.list.search-results: Search results +gist.list.sort: Sort +gist.list.sort-by-created: created +gist.list.sort-by-updated: updated +gist.list.order-by-asc: Least recently +gist.list.order-by-desc: Recently +gist.list.select-tab: Select a tab +gist.list.liked: Liked +gist.list.likes: likes +gist.list.forked: Forked +gist.list.forked-from: Forked from +gist.list.forks: forks +gist.list.files: files +gist.list.last-active: Last active +gist.list.no-gists: No gists + +gist.forks: Forks +gist.forks.view: View fork +gist.forks.no: No public forks + +gist.likes: Likes +gist.likes.no: No likes yet + +gist.revisions: Revisions +gist.revision.revised: revised this gist +gist.revision.go-to-revision: Go to revision +gist.revision.file-created: file created +gist.revision.file-deleted: file deleted +gist.revision.file-renamed: renamed to +gist.revision.diff-truncated: Diff truncated because it's too large to be shown +gist.revision.file-renamed-no-changes: File renamed without changes +gist.revision.empty-file: Empty file +gist.revision.no-changes: No changes +gist.revision.no-revisions: No revisions to show + +settings: Settings +settings.email: Email +settings.email-help: Used for commits and Gravatar +settings.email-set: Set email +settings.link-accounts: Link accounts +settings.link-github-account: Link GitHub account +settings.link-gitea-account: Link Gitea account +settings.unlink-github-account: Unlink GitHub account +settings.unlink-gitea-account: Unlink Gitea account +settings.delete-account: Delete account +settings.delete-account-confirm: Are you sure you want to delete your account ? +settings.add-ssh-key: Add SSH key +settings.add-ssh-key-help: Used only to pull/push gists using Git via SSH +settings.add-ssh-key-title: Title +settings.add-ssh-key-content: Key +settings.delete-ssh-key: Delete +settings.delete-ssh-key-confirm: Confirm deletion of SSH key +settings.ssh-key-added-at: Added +settings.ssh-key-never-used: Never used +settings.ssh-key-last-used: Last used + +auth.signup-disabled: Administrator has disabled signing up +auth.login: Login +auth.signup: Register +auth.new-account: New account +auth.username: Username +auth.password: Password +auth.register-instead: Register instead +auth.login-instead: Login instead +auth.github-oauth: Continue with GitHub account +auth.gitea-oauth: Continue with Gitea account + +error: Error + +header.menu.all: All +header.menu.new: New +header.menu.search: Search +header.menu.my-gists: My gists +header.menu.liked: Liked +header.menu.admin: Admin +header.menu.settings: Settings +header.menu.logout: Logout +header.menu.register: Register +header.menu.login: Login +header.menu.light: Light +header.menu.dark: Dark +header.menu.system: System +footer.powered-by: Powered by %s + +pagination.older: Older +pagination.newer: Newer +pagination.previous: Previous +pagination.next: Next + +admin.admin_panel: Admin panel +admin.general: General +admin.users: Users +admin.gists: Gists +admin.configuration: Configuration +admin.versions: Versions +admin.ssh_keys: SSH keys +admin.stats: Stats +admin.actions: Actions +admin.actions.sync-fs: Synchronize gists from filesystem +admin.actions.sync-db: Synchronize gists from database +admin.actions.git-gc: Garbage collect git repositories +admin.id: ID +admin.user: User +admin.delete: Delete +admin.created_at: Created + +admin.config-link: This configuration can be %s by a YAML config file and/or environment variables. +admin.config-link-overriden: overridden +admin.disable-signup: Disable signup +admin.disable-signup_help: Forbid the creation of new accounts. +admin.require-login: Require login +admin.require-login_help: Enforce users to be logged in to see gists. +admin.disable-login: Disable login form +admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead. +admin.disable-gravatar: Disable Gravatar +admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider. + +admin.users.delete_confirm: Do you want to delete this user ? + +admin.gists.title: Title +admin.gists.private: Private ? +admin.gists.nb-files: Nb. files +admin.gists.nb-likes: Nb. likes +admin.gists.delete_confirm: Do you want to delete this gist ? diff --git a/internal/i18n/locales/fr-FR.yml b/internal/i18n/locales/fr-FR.yml new file mode 100644 index 0000000..ced1ca9 --- /dev/null +++ b/internal/i18n/locales/fr-FR.yml @@ -0,0 +1,177 @@ +gist.public: Public +gist.unlisted: Non répertorié +gist.private: Privé + +gist.header.like: J'aime +gist.header.unlike: Je n'aime plus +gist.header.fork: Fork +gist.header.edit: Éditer +gist.header.delete: Supprimer +gist.header.forked-from: Forké de +gist.header.last-active: Dernière activité +gist.header.select-tab: Sélectionner un onglet +gist.header.code: Code +gist.header.revisions: Révisions +gist.header.revision: Révision +gist.header.clone-http: Cloner via %s +gist.header.clone-http-help: Cloner avec Git en utilisant l'authentification HTTP basic. +gist.header.clone-ssh: Cloner via SSH +gist.header.clone-ssh-help: Cloner avec Git en utilisant une clé SSH. +gist.header.share: Partager +gist.header.share-help: Copier le lien partageable de ce gist. +gist.header.download-zip: Télécharger en ZIP + +gist.raw: Brut +gist.file-truncated: Ce fichier a été tronqué. +gist.watch-full-file: Voir le fichier complet. +gist.file-not-valid: Ce fichier n'est pas un fichier CSV valide. +gist.no-content: Pas de contenu + +gist.new.new_gist: Nouveau gist +gist.new.title: Titre +gist.new.description: Description +gist.new.filename-with-extension: Nom de fichier avec extension +gist.new.indent-mode: Mode d'indentation +gist.new.indent-mode-space: Espace +gist.new.indent-mode-tab: Tabulation +gist.new.indent-size: Taille d'indentation +gist.new.wrap-mode: Mode d'enroulement +gist.new.wrap-mode-no: Sans enroulement +gist.new.wrap-mode-soft: Enroulement doux +gist.new.add-file: Ajouter un fichier +gist.new.create-public-button: Créer un gist public +gist.new.create-unlisted-button: Créer un gist non repertorié +gist.new.create-private-button: Créer un gist privé + +gist.edit.editing: Édition de +gist.edit.change-visibility: Rendre +gist.edit.delete: Supprimer +gist.edit.cancel: Annuler +gist.edit.save: Sauvegarder + +gist.list.joined: Inscrit +gist.list.all: Tous les gists +gist.list.search-results: Résultats de recherche +gist.list.sort: Trier +gist.list.sort-by-created: créé +gist.list.sort-by-updated: mis à jour +gist.list.order-by-asc: Le moins récemment +gist.list.order-by-desc: Récemment +gist.list.select-tab: Sélectionner un onglet +gist.list.liked: Aimé +gist.list.likes: j'aimes +gist.list.forked: Forké +gist.list.forked-from: Forké de +gist.list.forks: forks +gist.list.files: fichiers +gist.list.last-active: Dernière activité +gist.list.no-gists: Aucun gist + +gist.forks: Forks +gist.forks.view: Voir le fork +gist.forks.no: Pas de forks publics + +gist.likes: J'aime +gist.likes.no: Aucun j'aime pour le moment + +gist.revisions: Révisions +gist.revision.revised: a révisé ce gist +gist.revision.go-to-revision: Aller à la révision +gist.revision.file-created: fichier créé +gist.revision.file-deleted: fichier supprimé +gist.revision.file-renamed: renommé en +gist.revision.diff-truncated: Révision tronquée car trop volumineuse pour être affichée +gist.revision.file-renamed-no-changes: Fichier renommé sans modifications +gist.revision.empty-file: Fichier vide +gist.revision.no-changes: Aucun changement +gist.revision.no-revisions: Aucune révision à afficher + +settings: Paramètres +settings.email: Email +settings.email-help: Utilisé pour les commits et Gravatar +settings.email-set: Définir l'email +settings.link-accounts: Lier les comptes +settings.link-github-account: Lier le compte GitHub +settings.link-gitea-account: Lier le compte Gitea +settings.unlink-github-account: Détacher le compte GitHub +settings.unlink-gitea-account: Détacher le compte Gitea +settings.delete-account: Supprimer le compte +settings.delete-account-confirm: Êtes-vous sûr de vouloir supprimer votre compte ? +settings.add-ssh-key: Ajouter une clé SSH +settings.add-ssh-key-help: Utilisé uniquement pour pull/push des gists avec Git via SSH +settings.add-ssh-key-title: Titre +settings.add-ssh-key-content: Clé +settings.delete-ssh-key: Supprimer +settings.delete-ssh-key-confirm: Confirmer la suppression de la clé SSH +settings.ssh-key-added-at: Ajouté +settings.ssh-key-never-used: Jamais utilisé +settings.ssh-key-last-used: Dernière utilisation + +auth.signup-disabled: L'administrateur a désactivé l'inscription +auth.login: Connexion +auth.signup: Inscription +auth.new-account: Nouveau compte +auth.username: Nom d'utilisateur +auth.password: Mot de passe +auth.register-instead: Je préfère m'inscrire +auth.login-instead: Je préfère me connecter +auth.github-oauth: Continuer avec un compte GitHub +auth.gitea-oauth: Continuer avec un compte Gitea + +error: Erreur + +header.menu.all: Tous +header.menu.new: Nouveau +header.menu.search: Recherche +header.menu.my-gists: Mes gists +header.menu.liked: Aimés +header.menu.admin: Admin +header.menu.settings: Paramètres +header.menu.logout: Déconnexion +header.menu.register: Inscription +header.menu.login: Connexion +header.menu.light: Clair +header.menu.dark: Sombre +header.menu.system: Système +footer.powered-by: Propulsé par %s + +pagination.older: Plus ancien +pagination.newer: Plus récent +pagination.previous: Précédent +pagination.next: Suivant + +admin.admin_panel: Panneau d'administration +admin.general: Général +admin.users: Utilisateurs +admin.gists: Gists +admin.configuration: Configuration +admin.versions: Versions +admin.ssh_keys: Clés SSH +admin.stats: Statistiques +admin.actions: Actions +admin.actions.sync-fs: Synchroniser les gists depuis le système de fichiers +admin.actions.sync-db: Synchroniser les gists depuis la base de données +admin.actions.git-gc: Nettoyage des dépôts git +admin.id: ID +admin.user: Utilisateur +admin.delete: Supprimer +admin.created_at: Créé + +admin.config-link: Cette configuration peut être %s par un fichier de configuration YAML et/ou des variables d'environnement. +admin.config-link-overriden: remplacée +admin.disable-signup: Désactiver l'inscription +admin.disable-signup_help: Interdire la création de nouveaux comptes. +admin.require-login: Exiger la connexion +admin.require-login_help: Obliger les utilisateurs à être connectés pour voir les gists. +admin.disable-login: Désactiver le formulaire de connexion +admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place. +admin.disable-gravatar: Désactiver Gravatar +admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar. + +admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ? + +admin.gists.title: Titre +admin.gists.private: Privé ? +admin.gists.nb-files: Nb. de fichiers +admin.gists.nb-likes: Nb. de j'aime +admin.gists.delete_confirm: Voulez-vous supprimer ce gist ? diff --git a/internal/i18n/locales/fs_embed.go b/internal/i18n/locales/fs_embed.go new file mode 100644 index 0000000..f5bdafa --- /dev/null +++ b/internal/i18n/locales/fs_embed.go @@ -0,0 +1,6 @@ +package locales + +import "embed" + +//go:embed *.yml +var Files embed.FS diff --git a/internal/web/auth.go b/internal/web/auth.go index 8af6083..4a1d37d 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -27,7 +27,7 @@ import ( var title = cases.Title(language.English) func register(ctx echo.Context) error { - setData(ctx, "title", "New account") + setData(ctx, "title", tr(ctx, "auth.new-account")) setData(ctx, "htmlTitle", "New account") setData(ctx, "disableForm", getData(ctx, "DisableLoginForm")) return html(ctx, "auth_form.html") @@ -87,7 +87,7 @@ func processRegister(ctx echo.Context) error { } func login(ctx echo.Context) error { - setData(ctx, "title", "Login") + setData(ctx, "title", tr(ctx, "auth.login")) setData(ctx, "htmlTitle", "Login") setData(ctx, "disableForm", getData(ctx, "DisableLoginForm")) return html(ctx, "auth_form.html") diff --git a/internal/web/gist.go b/internal/web/gist.go index 071a93c..3b527c0 100644 --- a/internal/web/gist.go +++ b/internal/web/gist.go @@ -121,19 +121,21 @@ func allGists(ctx echo.Context) error { pageInt := getPage(ctx) sort := "created" + sortText := tr(ctx, "gist.list.sort-by-created") order := "desc" - orderText := "Recently" + orderText := tr(ctx, "gist.list.order-by-desc") if ctx.QueryParam("sort") == "updated" { sort = "updated" + sortText = tr(ctx, "gist.list.sort-by-updated") } if ctx.QueryParam("order") == "asc" { order = "asc" - orderText = "Least recently" + orderText = tr(ctx, "gist.list.order-by-asc") } - setData(ctx, "sort", sort) + setData(ctx, "sort", sortText) setData(ctx, "order", orderText) var gists []*db.Gist diff --git a/internal/web/server.go b/internal/web/server.go index a7d8be0..df29fbe 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -12,8 +12,11 @@ import ( "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/git" + "github.com/thomiceli/opengist/internal/i18n" "github.com/thomiceli/opengist/public" "github.com/thomiceli/opengist/templates" + "golang.org/x/text/language" + htmlpkg "html" "html/template" "io" "net/http" @@ -105,6 +108,13 @@ var fm = template.FuncMap{ } return s }, + "unescape": htmlpkg.UnescapeString, + "join": func(s ...string) string { + return strings.Join(s, "") + }, + "toStr": func(i interface{}) string { + return fmt.Sprint(i) + }, } type Template struct { @@ -129,7 +139,12 @@ func NewServer(isDev bool) *Server { e.HideBanner = true e.HidePort = true + if err := i18n.Locales.LoadAll(); err != nil { + log.Fatal().Err(err).Msg("Failed to load locales") + } + e.Use(dataInit) + e.Use(locale) e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ Getter: middleware.MethodFromForm("_method"), })) @@ -297,6 +312,50 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc { } } +func locale(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + + // Check URL arguments + lang := ctx.Request().URL.Query().Get("lang") + changeLang := lang != "" + + // Then check cookies + if len(lang) == 0 { + cookie, _ := ctx.Request().Cookie("lang") + if cookie != nil { + lang = cookie.Value + } + } + + // Check again in case someone changes the supported language list. + if lang != "" && !i18n.Locales.HasLocale(lang) { + lang = "" + changeLang = false + } + + //3.Then check from 'Accept-Language' header. + if len(lang) == 0 { + tags, _, _ := language.ParseAcceptLanguage(ctx.Request().Header.Get("Accept-Language")) + lang = i18n.Locales.MatchTag(tags) + } + + if changeLang { + ctx.SetCookie(&http.Cookie{Name: "lang", Value: lang, Path: "/", MaxAge: 1<<31 - 1}) + } + + localeUsed, err := i18n.Locales.GetLocale(lang) + if err != nil { + return errorRes(500, "Cannot get locale", err) + } + + setData(ctx, "localeName", localeUsed.Name) + setData(ctx, "locale", localeUsed) + setData(ctx, "allLocales", i18n.Locales.Locales) + + return next(ctx) + } +} + func sessionInit(next echo.HandlerFunc) echo.HandlerFunc { return func(ctx echo.Context) error { sess := getSession(ctx) diff --git a/internal/web/util.go b/internal/web/util.go index c9253cf..df506c7 100644 --- a/internal/web/util.go +++ b/internal/web/util.go @@ -12,6 +12,7 @@ import ( "github.com/labstack/echo/v4" "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/db" + "github.com/thomiceli/opengist/internal/i18n" "golang.org/x/crypto/argon2" "html/template" "net/http" @@ -212,11 +213,11 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp switch labels { case 1: - setData(ctx, "prevLabel", "Previous") - setData(ctx, "nextLabel", "Next") + setData(ctx, "prevLabel", tr(ctx, "pagination.previous")) + setData(ctx, "nextLabel", tr(ctx, "pagination.next")) case 2: - setData(ctx, "prevLabel", "Newer") - setData(ctx, "nextLabel", "Older") + setData(ctx, "prevLabel", tr(ctx, "pagination.newer")) + setData(ctx, "nextLabel", tr(ctx, "pagination.older")) } setData(ctx, "urlPage", urlPage) @@ -224,6 +225,11 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp return nil } +func tr(ctx echo.Context, key string) template.HTML { + l := getData(ctx, "locale").(*i18n.Locale) + return l.Tr(key) +} + type Argon2ID struct { format string version int diff --git a/public/main.ts b/public/main.ts index 88de29d..3360680 100644 --- a/public/main.ts +++ b/public/main.ts @@ -135,6 +135,11 @@ document.addEventListener('DOMContentLoaded', () => { }; } + document.getElementById('language-btn')!.onclick = () => { + document.getElementById('language-list')!.classList.toggle('hidden'); + }; + + document.querySelectorAll('.copy-gist-btn').forEach((e: HTMLElement) => { e.onclick = () => { navigator.clipboard.writeText(e.parentNode!.parentNode!.querySelector('.gist-content')!.textContent || '').catch((err) => { @@ -151,7 +156,7 @@ document.addEventListener('DOMContentLoaded', () => { } Array.from(document.querySelectorAll('.gist-visibility-option')).forEach((el) => { (el as HTMLElement).onclick = () => { - submitgistbutton.textContent = "Create " + el.textContent.toLowerCase() + " gist"; + submitgistbutton.textContent = (el as HTMLElement).dataset.btntext; submitgistbutton!.value = (el as HTMLElement).dataset.visibility || '0'; gistmenuvisibility!.classList.add('hidden'); } diff --git a/templates/base/admin_header.html b/templates/base/admin_header.html index 1613c57..2a9f0e8 100644 --- a/templates/base/admin_header.html +++ b/templates/base/admin_header.html @@ -2,7 +2,7 @@
-

Admin panel

+

{{ .locale.Tr "admin.admin_panel" }}

@@ -10,13 +10,13 @@
diff --git a/templates/base/base_footer.html b/templates/base/base_footer.html index a63f90f..c7c9816 100644 --- a/templates/base/base_footer.html +++ b/templates/base/base_footer.html @@ -4,15 +4,31 @@ {{ end }} {{ define "footer" }} -

- - - Powered by Opengist - - ⋅ - Load: {{ loadedTime .loadStartTime }} +

+

+ + + {{ .locale.Tr "footer.powered-by" "Opengist" }} + + ⋅ + Load: {{ loadedTime .loadStartTime }}⋅ +

+
+ + + + {{ .localeName }} + -

+ +
+
diff --git a/templates/base/base_header.html b/templates/base/base_header.html index a2a13c6..9ed798a 100644 --- a/templates/base/base_header.html +++ b/templates/base/base_header.html @@ -74,11 +74,11 @@ @@ -204,7 +204,7 @@ {{ if .gist.Forked }} -

Forked from {{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}

+

{{ .locale.Tr "gist.header.forked-from" }} {{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}

{{ end }} -

Last active {{ .gist.UpdatedAt }} +

{{ .locale.Tr "gist.header.last-active" }} {{ .gist.UpdatedAt }} {{ if .gist.Private }} • {{ visibilityStr .gist.Private false }} {{ end }}

{{ .gist.Description }}

@@ -100,10 +100,10 @@
- +
{{ if .revision }} {{ if ne .revision "HEAD" }} -

Revision {{ .revision }}

+

{{ .locale.Tr "gist.header.revision" }} {{ .revision }}

{{ end }} {{ end }}
diff --git a/templates/pages/admin_config.html b/templates/pages/admin_config.html index d04432b..2234b92 100644 --- a/templates/pages/admin_config.html +++ b/templates/pages/admin_config.html @@ -3,7 +3,7 @@
-

This configuration can be overridden by a YAML config file and/or environment variables.

+

{{ .locale.Tr "admin.config-link" (join "" (toStr (.locale.Tr "admin.config-link-overriden")) "") }}