i18n admin.tmpl, safer HTML interpolation

This commit is contained in:
Evan Goode 2025-07-09 23:51:13 -04:00
parent 0117ad3c95
commit dfab4da016
9 changed files with 63 additions and 284 deletions

View File

@ -17,7 +17,6 @@ import (
"github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/text/language"
"gorm.io/gorm"
"html"
"html/template"
"io"
"log"
@ -69,7 +68,6 @@ func NewTemplate(app *App) *Template {
funcMap := template.FuncMap{
"render": RenderHTML,
"html": func(x string) template.HTML { return template.HTML(x) },
"PrimaryPlayerSkinURL": app.PrimaryPlayerSkinURL,
"PlayerSkinURL": app.PlayerSkinURL,
"InviteURL": app.InviteURL,
@ -161,6 +159,11 @@ func NewWebError(returnURL string, message string, args ...interface{}) error {
}
func RenderHTML(templateString string, args ...interface{}) (template.HTML, error) {
// If there are no args, skip parsing and return the "template" as-is
if len(args) == 0 {
return template.HTML(templateString), nil
}
t, err := template.New("").Parse(templateString)
if err != nil {
return "", err
@ -176,7 +179,7 @@ func RenderHTML(templateString string, args ...interface{}) (template.HTML, erro
}
type baseContext struct {
T func(string, ...interface{}) template.HTML
T func(string, ...interface{}) string
App *App
L *gotext.Locale
URL string
@ -185,24 +188,9 @@ type baseContext struct {
ErrorMessage string
}
func NewT(l *gotext.Locale) func(string, ...interface{}) template.HTML {
return func(msgid string, args ...interface{}) template.HTML {
sanitized := make([]interface{}, 0, len(args))
for _, arg := range args {
switch arg.(type) {
case template.HTML:
sanitized = append(sanitized, arg)
default:
sanitized = append(sanitized, html.EscapeString(fmt.Sprint(arg)))
}
}
return template.HTML(l.Get(msgid, sanitized...))
}
}
func (app *App) NewBaseContext(c *echo.Context) baseContext {
l := (*c).Get(CONTEXT_KEY_LOCALE).(*gotext.Locale)
T := NewT((*c).Get(CONTEXT_KEY_LOCALE).(*gotext.Locale))
T := l.Get
return baseContext{
App: app,
L: l,

View File

@ -1,212 +0,0 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: es\n"
msgid "OpenID logo"
msgstr "Logo de OpenID"
msgid "Registration"
msgstr "Registro"
msgid "Register"
msgstr "Registrarse"
msgid "Admin"
msgstr "Administrador"
msgid "%s's Account"
msgstr "Cuenta de %s"
msgid "Log out"
msgstr "Cerrar sesión"
msgid "Log in"
msgstr "Iniciar sesión"
msgid "Sign in with %s"
msgstr "Iniciar sesión con %s"
msgid "or"
msgstr "o"
msgid "Username"
msgstr "Nombre de usuario"
msgid "Password"
msgstr "Contraseña"
msgid "Configuring your client"
msgstr "Configurando tu cliente"
msgid "Using %s on the client requires a third-party launcher that supports custom API servers. %s, a fork of Prism Launcher, is recommended, but %s also works. Both are free/libre."
msgstr "Usar %s en el cliente requiere un lanzador de terceros que soporte servidores API personalizados. Se recomienda %s, un fork de Prism Launcher, pero %s también funciona. Ambos son libres."
msgid "Click your account in the top right and select “Manage Accounts...”."
msgstr "Haz clic en tu cuenta en la esquina superior derecha y selecciona “Administrar cuentas...”."
msgid "Click “Add authlib-injector” in the right-hand sidebar."
msgstr "Haz clic en “Add authlib-injector” en la barra lateral derecha."
msgid "Enter your player name and your %s password or Minecraft Token, and use %s for the URL. Click “OK”."
msgstr "Introduce tu nombre de jugador y tu contraseña de %s o Token de Minecraft, y usa %s para la URL. Haz clic en “Aceptar”."
msgid "Go to the “Account List” view by clicking the account at the top of the sidebar."
msgstr "Ve a la vista “Lista de cuentas” haciendo clic en la cuenta en la parte superior de la barra lateral."
msgid "At the bottom left, click “New Auth Server” and enter %s. Click “Next” and then “Finish”."
msgstr "En la parte inferior izquierda, haz clic en “Añadir un servidor de autenticación” e ingresa %s. Haz clic en “Siguiente” y luego en “Finalizar”."
msgid "In the sidebar, click the newly-added authentication server, labeled “%s”. Enter your %s player name and password and click “Login”."
msgstr "En la barra lateral, haz clic en el servidor de autenticación recién añadido, etiquetado como “%s”. Ingresa tu nombre de jugador y contraseña de %s y haz clic en “Acceder”."
msgid "Other launchers"
msgstr "Otros lanzadores"
msgid "Use the authlib-injector URL %s."
msgstr "Usa la URL de authlib-injector %s."
msgid "Or, if your launcher supports custom API servers but not via authlib-injector, use the following URLs:"
msgstr "O, si tu lanzador soporta servidores API personalizados pero no a través de authlib-injector, usa las siguientes URLs:"
msgid "Authentication server:"
msgstr "Servidor de “Authentication”:"
msgid "Account server:"
msgstr "Servidor de “Account”:"
msgid "Session server:"
msgstr "Servidor de “Session”:"
msgid "Services server:"
msgstr "Servidor de “Services”:"
msgid "Configuring your server"
msgstr "Configurando tu servidor"
msgid "Minecraft 1.16 and later"
msgstr "Minecraft 1.16 y posteriores"
msgid "On recent versions of Minecraft, you can use %s on an unmodified Vanilla server. To do so, add the following arguments before you specify the jar file when you start the server:"
msgstr "En las versiones recientes de Minecraft, puedes usar %s en un servidor Vanilla sin modificaciones. Para hacerlo, agrega los siguientes argumentos antes de especificar el archivo jar al iniciar el servidor:"
msgid "For example, the full command you use to start the server might be:"
msgstr "Por ejemplo, el comando completo que usas para iniciar el servidor podría ser:"
msgid "Minecraft 1.7.2 through 1.15.2"
msgstr "Minecraft 1.7.2 a 1.15.2"
msgid "Refer to <a href=\"%s\">the authlib-injector documentation on setting up a server</a>."
msgstr "Consulta <a href=\"%s\">la documentación de authlib-injector sobre cómo configurar un servidor</a>."
msgid "Alternatively, you can patch your server to use a newer version of Mojang's authlib that supports the arguments for custom API servers. Replace the files under <code>com/mojang/authlib</code> in your <code>server.jar</code> with the files in <a href=\"%s\">authlib-1.6.25.jar</a>."
msgstr "Alternativamente, puedes parchear tu servidor para usar una versión más reciente de authlib de Mojang que soporte los argumentos para servidores API personalizados. Reemplaza los archivos bajo <code>com/mojang/authlib</code> en tu <code>server.jar</code> con los archivos en <a href=\"%s\">authlib-1.6.25.jar</a>."
msgid "<a href=\"%s\">Late Classic</a>, Alpha, Beta, etc. through Minecraft 1.6.4"
msgstr "<a href=\"%s\">Clásico Tardío</a>, Alpha, Beta, etc. hasta Minecraft 1.6.4"
msgid "Use %s and start the server with the <code>-Dminecraft.api.session.host</code> argument described above. For example, the full command you use to start the server might be:"
msgstr "Usa %s y arranca el servidor con el argumento <code>-Dminecraft.api.session.host</code> descrito arriba. Por ejemplo, el comando completo que usas para iniciar el servidor podría ser:"
msgid "Drasl version %s."
msgstr "Versión de Drasl %s."
msgid "Licensed as %s."
msgstr "Con licencia %s."
msgid "<a href=\"%s\">Source code</a>."
msgstr "<a href=\"%s\">Código fuente</a>."
msgid "Signing in with %s requires an invite."
msgstr "Iniciar sesión con %s requiere una invitación."
msgid "Register a new account:"
msgid "Register a new account with a random UUID:"
msgid "Player UUID (leave blank for random)"
msgstr "UUID del jugador (dejar en blanco para uno aleatorio)"
msgid "Using invite code %s"
msgstr "Usando el código de invitación %s"
msgid "Register from an existing Minecraft player"
msgstr "Registrarse como un jugador existente de Minecraft"
msgid "Registration as a new player is invite-only."
msgstr "El registro como nuevo jugador es solo por invitación."
msgid "Registration as an existing player is invite-only."
msgstr "El registro como jugador existente es solo por invitación."
msgid "Register a new account with the UUID of an existing %s account. Requires verification that you own the account."
msgstr "Registra una nueva cuenta con el UUID de una cuenta existente de %s. Se requiere verificación de que eres el propietario de la cuenta."
msgid "Register a new account with the UUID of an existing %s account"
msgstr "Registrar una nueva cuenta con el UUID de una cuenta existente de %s"
msgid "%s player name"
msgstr "Nombre de jugador de %s"
msgid "Continue"
msgstr "Continuar"
msgid "Register"
msgstr "Registrarse"
msgid "Migrate an existing user"
msgstr "Migrar un usuario existente"
msgid "You can link this identity provider to an existing %s account."
msgstr "Puedes vincular este proveedor de identidad a una cuenta existente de %s."
msgid "If you do so, you will no longer be able to log in using your %s password. You'll need to use your Minecraft Token to log in to Minecraft launchers."
msgstr "Si lo haces, ya no podrás iniciar sesión usando tu contraseña de %s. Necesitarás usar tu Token de Minecraft para iniciar sesión en los lanzadores de Minecraft."
msgid "Link account"
msgstr "Vincular cuenta"
msgid "Create a player"
msgstr "Crear un jugador"
msgid "Complete registration by creating a new player:"
msgstr "Completa el registro creando un nuevo jugador:"
msgid "Complete registration by creating a new player with a random UUID:"
msgstr "Completa el registro creando un nuevo jugador con un UUID aleatorio:"
msgid "Choosing a player name is not allowed."
msgstr "No está permitido elegir un nombre de jugador."
msgid "Player name"
msgstr "Nombre de jugador"
msgid "We need to verify that you own the %s account “%s” before you register its UUID."
msgstr "Necesitamos verificar que eres el propietario de la cuenta %s “%s” antes de registrar su UUID."
msgid "Download this image and set it as your skin on your %s account, on <a target=\"_blank\" href=\"%s\">this page</a>."
msgstr "Descarga esta imagen y configúrala como tu skin en tu cuenta de %s, en <a target=\"_blank\" href=\"%s\">esta página</a>."
msgid "Download this image and set it as your skin on your %s account."
msgstr "Descarga esta imagen y configúrala como tu skin en tu cuenta de %s."
msgid "%s verification skin"
msgstr "Skin de verificación de %s"
msgid "%s-verification-skin.png"
msgstr "%s-skin-de-verificación.png"
msgid "Download skin"
msgstr "Descargar skin"
msgid "When you are done, hit “Register”."
msgstr "Cuando termines, haz clic en “Registrarse”."
msgid "When you are done, enter a password for your %s account and hit “Register”."
msgstr "Cuando termines, ingresa una contraseña para tu cuenta de %s y haz clic en “Registrarse”."
msgid "When you are done, hit “Create player”."
msgstr "Cuando termines, haz clic en “Crear jugador”."
msgid "Create player"
msgstr "Crear jugador"

View File

@ -45,6 +45,7 @@ table {
thead {
font-weight: bold;
white-space: nowrap;
}
td:not(:last-child) {

View File

@ -1,12 +1,12 @@
{{ template "layout" . }}
{{ define "title" }}Admin - {{ .App.Config.ApplicationName }}{{ end }}
{{ define "title" }}{{ call .T "Admin" }}{{ end }}
{{ define "content" }}
{{ template "header" . }}
<h4>Pending Invites</h4>
<h4>{{ call .T "Pending Invites" }}</h4>
<div style="text-align: right">
<form
@ -15,15 +15,15 @@
method="post"
>
<input hidden name="returnUrl" value="{{ .URL }}" />
<input type="submit" value="+ New Invite" />
<input type="submit" value="+ {{ call .T "New Invite" }}" />
</form>
</div>
{{ if .Invites }}
<table>
<thead>
<tr>
<td style="width: 50%">Link</td>
<td>Date&nbsp;Generated</td>
<td style="width: 50%">{{ call .T "Link" }}</td>
<td>{{ call .T "Date Generated" }}</td>
<td></td>
</tr>
</thead>
@ -50,7 +50,7 @@
value="{{ $invite.Code }}"
hidden
/>
<input type="submit" value="× Delete" />
<input type="submit" value="× {{ call $.T "Delete" }}" />
</form>
</td>
</tr>
@ -58,10 +58,10 @@
</tbody>
</table>
{{ else }}
No invites to show.
{{ call .T "No invites to show." }}
{{ end }}
<h4>All Users</h4>
<h4>{{ call .T "All Users" }}</h4>
<div style="display: none">
{{ range $user := .Users }}
@ -69,7 +69,7 @@
id="delete-{{ $user.UUID }}"
action="{{ $.App.FrontEndURL }}/web/delete-user"
method="post"
onsubmit="return confirm('Are you sure you want to delete the account “{{ $user.Username }}”? This action is irreversible.');"
onsubmit="return confirm('{{ call $.T "Are you sure you want to delete the account “%s”? This action is irreversible." $user.Username }}');"
>
<input
hidden
@ -89,12 +89,12 @@
<table>
<thead>
<tr>
<td colspan="2">User</td>
<td>Players</td>
<td>Max&nbsp;#&nbsp;players*</td>
<td>Admin</td>
<td>Locked</td>
<td>Delete&nbsp;Account</td>
<td colspan="2">{{ call .T "User" }}</td>
<td>{{ call .T "Players" }}</td>
<td>{{ call .T "Max # players*" }}</td>
<td>{{ call .T "Admin" }}</td>
<td>{{ call .T "Locked" }}</td>
<td>{{ call .T "Delete Account" }}</td>
</tr>
</thead>
<tbody>
@ -122,7 +122,7 @@
<a href="{{ $.App.FrontEndURL }}/web/player/{{ $player.UUID }}">{{ $player.Name }}</a>
{{ end }}
{{ else if gt (len $user.Players) 1 }}
{{ len $user.Players }}&nbsp;players
{{ len $user.Players }}
{{ end }}
</td>
<td>
@ -130,7 +130,7 @@
name="max-player-count-{{ $user.UUID }}"
type="number"
{{ if $user.IsAdmin }}
title="Admins can always create unlimited players"
title="{{ call $.T "Admins can always create unlimited players" }}"
disabled
{{ end }}
value="{{ if or $user.IsAdmin (eq $user.MaxPlayerCount $.App.Constants.MaxPlayerCountUnlimited) }}-1{{ else if eq $user.MaxPlayerCount $.App.Constants.MaxPlayerCountUseDefault}}{{ else }}{{ $user.MaxPlayerCount }}{{ end }}"
@ -140,17 +140,12 @@
<td>
<input
name="admin-{{ $user.UUID }}"
title="Admin?"
title="{{ if IsDefaultAdmin $user }}{{ call $.T "To demote a default admin, edit the %s configuration." $.App.Config.ApplicationName }}{{ else }}{{ call $.T "Admin?" }}{{ end }}"
type="checkbox"
{{ if
$user.IsAdmin
}}
{{ if $user.IsAdmin }}
checked
{{ end }}
{{ if
IsDefaultAdmin
$user
}}
{{ if IsDefaultAdmin $user }}
disabled
{{ end }}
/>
@ -158,11 +153,9 @@
<td>
<input
name="locked-{{ $user.UUID }}"
title="Locked?"
title="{{ call $.T "Locked?" }}"
type="checkbox"
{{ if
$user.IsLocked
}}
{{ if $user.IsLocked }}
checked
{{ end }}
/>
@ -171,17 +164,17 @@
<input
type="submit"
form="delete-{{ $user.UUID }}"
value="× Delete"
value="× {{ call $.T "Delete" }}"
/>
</td>
</tr>
{{ end }}
</tbody>
</table>
<p><small>*Specify -1 to allow an unlimited number of players. Leave blank to use the default max number, which is {{ $.App.Config.DefaultMaxPlayerCount }}.</small></p>
<p><small>{{ call .T "*Specify -1 to allow an unlimited number of players. Leave blank to use the default max number, which is %d." $.App.Config.DefaultMaxPlayerCount }}</small></p>
<p style="text-align: center">
<input hidden name="returnUrl" value="{{ $.URL }}" />
<input type="submit" value="Save changes" />
<input type="submit" value="{{ call .T "Save changes" }}" />
</p>
</form>

View File

@ -1,4 +1,7 @@
{{ template "layout" . }}
{{ define "title" }}{{ call .T "Skin Verification" }}{{ end }}
{{ define "content" }}
{{ template "header" . }}

View File

@ -1,6 +1,6 @@
{{ template "layout" . }}
{{ define "title" }}Complete Registration - {{ .App.Config.ApplicationName }}{{ end }}
{{ define "title" }}{{ call .T "Complete Registration" }}{{ end }}
{{ define
"content"

View File

@ -3,8 +3,8 @@
<hr />
<small>
{{ call .T `Drasl version %s.` .App.Constants.Version }}
{{ call .T `Licensed as %s.` (render `<a href="{{ index . 0 }}">{{ index . 1 }}</a>` .App.Constants.LicenseURL .App.Constants.License) }}
{{ call .T `<a href="%s">Source code</a>.` .App.Constants.RepositoryURL }}
{{ render (call .T `Licensed as {{ index . 0 }}.`) (render `<a href="{{ index . 0 }}">{{ index . 1 }}</a>` .App.Constants.LicenseURL .App.Constants.License) }}
{{ render (call .T `<a href="{{ index . 0 }}">Source code</a>.`) .App.Constants.RepositoryURL }}
</small>
{{ end }}
{{ end }}

View File

@ -1,6 +1,6 @@
{{ template "layout" . }}
{{ define "title" }}Register - {{ .App.Config.ApplicationName }}{{ end }}
{{ define "title" }}{{ call .T "Registration" }}{{ end }}
{{ define
"content"

View File

@ -5,7 +5,7 @@
{{ define "content" }}
{{ template "header" . }}
{{ $authlibInjectorLink := render `<a href="{{ index . 0}}">{{ index . 1}}</a>` .App.AuthlibInjectorURL .App.AuthlibInjectorURL }}
{{ $authlibInjectorLink := render `<a href="{{ index . 0}}">{{ index . 0}}</a>` .App.AuthlibInjectorURL }}
<h3>{{ call .T "Log in" }}</h3>
@ -47,10 +47,10 @@
<h3>{{ call .T "Configuring your client" }}</h3>
<p>
{{ call .T "Using %s on the client requires a third-party launcher that supports custom API servers. %s, a fork of Prism Launcher, is recommended, but %s also works. Both are free/libre."
{{ render (call .T `Using {{ index . 0 }} on the client requires a third-party launcher that supports custom API servers. {{ index . 1 }}, a fork of Prism Launcher, is recommended, but {{ index . 2 }} also works. Both are free/libre.`)
.App.Config.ApplicationName
(html `<a href="https://github.com/unmojang/FjordLauncher">Fjord Launcher</a>`)
(html `<a href="https://github.com/huanghongxun/HMCL">HMCL</a>`) }}
(render `<a href="https://github.com/unmojang/FjordLauncher">Fjord Launcher</a>`)
(render `<a href="https://github.com/huanghongxun/HMCL">HMCL</a>` ) }}
</p>
<h4>Fjord Launcher</h4>
@ -58,8 +58,8 @@
<ol>
<li>{{ call .T "Click your account in the top right and select “Manage Accounts...”." }} </li>
<li>{{ call .T "Click “Add authlib-injector” in the right-hand sidebar." }}</li>
<li>{{ call .T
"Enter your player name and your %s password or Minecraft Token, and use %s for the URL. Click “OK”."
<li>{{ render
(call .T `Enter your player name and your {{ index . 0 }} password or Minecraft Token, and use {{ index . 1 }} for the URL. Click “OK”.`)
.App.Config.ApplicationName
$authlibInjectorLink
}}</li>
@ -70,8 +70,8 @@
<ol>
<li>{{ call .T "Go to the “Account List” view by clicking the account at the top of the sidebar." }}</li>
<li>
{{ call .T
"At the bottom left, click “New Auth Server” and enter %s. Click “Next” and then “Finish”."
{{ render
(call .T `At the bottom left, click “New Auth Server” and enter {{ index . 0 }} . Click “Next” and then “Finish”.`)
$authlibInjectorLink
}}
</li>
@ -86,7 +86,10 @@
<h4>{{ call .T "Other launchers" }}</h4>
<p>{{ call .T "Use the authlib-injector URL %s." $authlibInjectorLink }}</p>
<p>{{ render
(call .T `Use the authlib-injector URL {{ index . 0 }}.`)
$authlibInjectorLink
}}</p>
<p>{{ call .T "Or, if your launcher supports custom API servers but not via authlib-injector, use the following URLs:" }}</p>
@ -133,21 +136,24 @@ java -Xmx1024M -Xms1024M \
<h4>{{ call .T "Minecraft 1.7.2 through 1.15.2" }}</h4>
<p>{{ call .T
`Refer to <a href="%s">the authlib-injector documentation on setting up a server</a>.`
<p>{{ render
(call .T `Refer to <a href="{{ index . 0 }}">the authlib-injector documentation on setting up a server</a>.`)
"https://github.com/yushijinhun/authlib-injector/blob/develop/README.en.md#deploy"
}}</p>
<p>{{ call .T
`Alternatively, you can patch your server to use a newer version of Mojang's authlib that supports the arguments for custom API servers. Replace the files under <code>com/mojang/authlib</code> in your <code>server.jar</code> with the files in <a href="%s">authlib-1.6.25.jar</a>.`
<p>{{ render
(call .T `Alternatively, you can patch your server to use a newer version of Mojang's authlib that supports the arguments for custom API servers. Replace the files under <code>com/mojang/authlib</code> in your <code>server.jar</code> with the files in <a href="{{ index . 0 }}">authlib-1.6.25.jar</a>.`)
"https://libraries.minecraft.net/com/mojang/authlib/1.6.25/authlib-1.6.25.jar"
}}</p>
<h4>{{ call .T `<a href="%s">Late Classic</a>, Alpha, Beta, etc. through Minecraft 1.6.4` "https://minecraft.wiki/w/Java_Edition_Late_Classic" }}</h4>
<h4>{{ render (call .T
`<a href="{{ index . 0 }}">Late Classic</a>, Alpha, Beta, etc. through Minecraft 1.6.4`)
"https://minecraft.wiki/w/Java_Edition_Late_Classic"
}}</h4>
<p>{{ call .T
`Use %s and start the server with the <code>-Dminecraft.api.session.host</code> argument described above. For example, the full command you use to start the server might be:`
(html `<a href="https://github.com/craftycodie/OnlineModeFix">OnlineModeFix</a>`)
<p>{{ render (call .T
`Use {{ index . 0 }} and start the server with the <code>-Dminecraft.api.session.host</code> argument described above. For example, the full command you use to start the server might be:`)
(render `<a href="https://github.com/craftycodie/OnlineModeFix">OnlineModeFix</a>`)
}}
<pre style="word-wrap: break-word; white-space: pre-wrap; overflow-x: auto">