wip: 🔕 temporary commit

This commit is contained in:
Paramtamtam 2024-06-26 01:06:02 +04:00
parent 1682a3513f
commit 2a1fa5c108
No known key found for this signature in database
GPG Key ID: 366371698FAD0A2B
14 changed files with 1254 additions and 975 deletions

View File

@ -20,7 +20,7 @@ func TestNew(t *testing.T) {
assert.NotEmpty(t, cfg.Formats.JSON)
assert.NotEmpty(t, cfg.Formats.PlainText)
assert.True(t, len(cfg.Codes) >= 19)
assert.True(t, len(cfg.Templates) >= 2)
assert.True(t, len(cfg.Templates) >= 1)
assert.NotEmpty(t, cfg.TemplateName)
assert.True(t, cfg.Templates.Has(cfg.TemplateName))
assert.Equal(t, uint16(http.StatusNotFound), cfg.DefaultCodeToRender)

View File

@ -13,7 +13,7 @@ import (
const contentTypeHeader = "Content-Type"
// New creates a new handler that returns an error page with the specified status code and format.
func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,gocognit
func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,gocognit,gocyclo
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var code uint16
@ -86,6 +86,7 @@ func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,
tplProps.Host = r.Header.Get("Host") // the value of the `Host` header
}
// TODO: ADD SUPPORT FOR THE RANDOM TEMPLATE AND SO ON
// try to find the code message and description in the config and if not - use the standard status text or fallback
if desc, found := cfg.Codes.Find(code); found {
tplProps.Message = desc.Message
@ -140,7 +141,7 @@ func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,
}
} else {
write(w, log, `The requested content format is not supported.
Please create an issue on the project's GitHub page to request support for it.
Please create an issue on the project's GitHub page to request support for this format.
Supported formats: JSON, XML, HTML, Plain Text`)
}

View File

@ -11,6 +11,7 @@ import (
"time"
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/l10n"
)
var builtInFunctions = template.FuncMap{ //nolint:gochecknoglobals
@ -99,6 +100,10 @@ var builtInFunctions = template.FuncMap{ //nolint:gochecknoglobals
// retrieves the value of the environment variable named by the key:
// `{{ env "SHELL" }}` // `/bin/bash`
"env": os.Getenv,
// returns the content of the JS file with a script for automatic error page localization:
// `{{ l10nScript }}` // `Object.defineProperty(window, ...`
"l10nScript": l10n.L10n,
}
func Render(content string, props Props) (string, error) {

View File

@ -11,6 +11,7 @@ import (
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/internal/template"
"gh.tarampamp.am/error-pages/l10n"
)
func TestRender_BuiltInFunction(t *testing.T) {
@ -29,10 +30,6 @@ func TestRender_BuiltInFunction(t *testing.T) {
giveTemplate: `{{ now.Unix }}`,
wantResult: strconv.Itoa(int(time.Now().Unix())),
},
"now (time)": {
giveTemplate: `{{ now.Hour }}:{{ now.Minute }}:{{ now.Second }}`,
wantResult: time.Now().Format("15:4:5"),
},
"hostname": {giveTemplate: `{{ hostname }}`, wantResult: hostname},
"json (string)": {giveTemplate: `{{ json "test" }}`, wantResult: `"test"`},
"json (int)": {giveTemplate: `{{ json 42 }}`, wantResult: `42`},
@ -54,6 +51,7 @@ func TestRender_BuiltInFunction(t *testing.T) {
"strFields": {giveTemplate: `{{ strFields "foo bar baz" }}`, wantResult: `[foo bar baz]`},
"env (ok)": {giveTemplate: `{{ env "TEST_ENV_VAR" }}`, wantResult: "unit-test"},
"env (not found)": {giveTemplate: `{{ env "NOT_FOUND_ENV_VAR" }}`, wantResult: ""},
"l10nScript": {giveTemplate: `{{ l10nScript }}`, wantResult: l10n.L10n()},
} {
t.Run(name, func(t *testing.T) {
require.NoError(t, os.Setenv("TEST_ENV_VAR", "unit-test"))

View File

@ -1,11 +0,0 @@
{
"extends": [
"eslint:recommended"
],
"parserOptions": {
"ecmaVersion": 2017
},
"env": {
"browser": true
}
}

14
l10n/embed_test.go Normal file
View File

@ -0,0 +1,14 @@
package l10n_test
import (
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/l10n"
)
func TestL10n(t *testing.T) {
assert.NotEmpty(t, l10n.L10n())
assert.Contains(t, l10n.L10n(), "data-l10n")
}

9
l10n/enbed.go Normal file
View File

@ -0,0 +1,9 @@
package l10n
import _ "embed"
//go:embed l10n.js
var content string
// L10n returns the content of the JS file with a script for automatic error page localization.
func L10n() string { return content }

File diff suppressed because it is too large Load Diff

138
l10n/playground.html Normal file
View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>L10n playground</title>
<style>
:root {
--color-bg-primary: #fff;
--color-text-primary: #0e0620;
--color-ui-bg-primary: #0e0620;
--color-ui-bg-inverted: #fff;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg-primary: #212121;
--color-text-primary: #fafafa;
--color-ui-bg-primary: #fafafa;
--color-ui-bg-inverted: #212121;
}
}
html, body {
margin: 0 auto;
padding: 0;
font-family: Arial, sans-serif;
max-width: 1200px;
min-width: 400px;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
}
#lang-switch {
list-style-type: none;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
flex-grow: 4;
gap: 1em;
padding: 2em 0;
button {
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
border: 1px solid var(--color-text-primary);
padding: 0.5em 1em;
cursor: pointer;
font-size: 1.2em;
font-weight: bold;
border-radius: 1em 0 1em 0;
transition: background-color 0.3s, color 0.3s;
&:hover {
background-color: var(--color-ui-bg-primary);
color: var(--color-ui-bg-inverted);
}
}
}
#tokens-list {
list-style-type: none;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1em;
padding: 0 1em;
li {
padding: 0.5em 1em;
font-size: 1.2em;
&::first-letter {
font-weight: bold;
}
}
}
</style>
</head>
<body>
<ul id="lang-switch"></ul>
<ul id="tokens-list"></ul>
<script type="module">
const $tokensList = document.getElementById('tokens-list');
[
'Error', 'Good luck', 'UH OH', 'Request details', 'Double-check the URL', 'Alternatively, go back', 'Host',
"Here's what might have happened", 'You may have mistyped the URL', 'The site was moved', 'It was never here',
'Bad Request', 'The server did not understand the request', 'Unauthorized', 'Method Not Allowed', 'Bad Gateway',
'The requested page needs a username and a password', 'Forbidden', 'Access is forbidden to the requested page',
'Not Found', 'The server can not find the requested page', 'The method specified in the request is not allowed',
'Proxy Authentication Required', 'You must authenticate with a proxy server before this request can be served',
'Request Timeout', 'The request took longer than the server was prepared to wait', 'Conflict', "I'm a teapot",
'The request could not be completed because of a conflict', 'Gone', 'The requested page is no longer available',
'Length Required', 'The "Content-Length" is not defined. The server will not accept the request without it',
'Precondition Failed', 'The pre condition given in the request evaluated to false by the server', 'Namespace',
'Payload Too Large', 'The server will not accept the request, because the request entity is too large',
'Requested Range Not Satisfiable', 'The requested byte range is not available and is out of bounds',
'Attempt to brew coffee with a teapot is not supported', 'Too Many Requests', 'Gateway Timeout', 'Service port',
'Too many requests in a given amount of time', 'Internal Server Error', 'The server met an unexpected condition',
'The server received an invalid response from the upstream server', 'Service Unavailable', 'Service name',
'The server is temporarily overloading or down', 'The gateway has timed out', 'HTTP Version Not Supported',
'The server does not support the "http protocol" version', 'Original URI', 'Forwarded for', 'Ingress name',
'Request ID', 'Timestamp', 'client-side error', 'server-side error', 'Your Client', 'Network', 'Web Server',
'What happened?', 'What can i do?', 'Please try again in a few minutes', 'Working', 'Unknown',
'Please try to change the request method, headers, payload, or URL', 'Please check your authorization data',
'Please double-check the URL and try again',
].forEach((token) => {
const $li = document.createElement('li');
$li.textContent = token;
$li.setAttribute('data-l10n', '');
$li.title = token;
$tokensList.appendChild($li);
});
const $langSwitch = document.getElementById('lang-switch');
['fr', 'ru', 'uk', 'pt', 'nl', 'de', 'es', 'zh', 'id', 'pl'].forEach((lang) => {
// ^^^ add your newly added locale here
const $li = document.createElement('li');
const $btn = document.createElement('button');
$btn.textContent = lang;
$btn.addEventListener('click', () => {
window.l10n.setLocale(lang);
window.l10n.localizeDocument();
});
$li.appendChild($btn);
$langSwitch.appendChild($li);
});
</script>
<script src="l10n.js" defer async></script>
</body>
</html>

View File

@ -1,15 +1,22 @@
# 🔤 Localization
[![jsDelivr hits](https://img.shields.io/jsdelivr/gh/hm/tarampampam/error-pages)](https://www.jsdelivr.com/package/gh/tarampampam/error-pages)
This directory contains the file [l10n.js](l10n.js) for localizing error pages. Once the error page is loaded,
this script runs and translates the page content to the user's locale.
This directory contains file [l10n.js](l10n.js) for the error pages localization. The working logic is very simple - pages load this script using [jsdelivr.com](https://www.jsdelivr.com/) as a CDN for [versioned content from the GitHub repository](https://www.jsdelivr.com/features#gh), and it translates tag content with the special HTML attribute `data-l10n`.
> [!NOTE]
> In version `2.*`, the working logic was simpler: error pages loaded this script using
> [jsdelivr.com](https://www.jsdelivr.com/) as a CDN for
> [versioned content from the GitHub repository](https://www.jsdelivr.com/features#gh), and it translated
> tag content with the special HTML attribute `data-l10n`.
By default, pages markup contains strings in English (`en` locale). If you want to localize the error pages on the different locales, you should:
By default, the error page markup contains strings in English (`en` locale). To localize the error pages to
different locales, please follow these steps:
- Find your locale name on [this page](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (column `639-1`)
- Make a fork of this repository
- Edit file [l10n.js](l10n.js) in `data` section (append new localized strings) using locale name from the step 1
- Make a PR with your changes
1. Find your locale name on [this page](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (column `Set 1` or `ISO 639-1:2002`)
2. Fork this repository
3. Edit the file [l10n.js](l10n.js) in the `data` map (append new localized strings) using the locale name from step 1
4. Please add your locale to the [playground.html](playground.html) file to test the localization
5. Make a PR with your changes
## 👍 Translators

View File

@ -10,9 +10,10 @@ import (
//go:embed *.html
var content embed.FS
func BuiltIn() map[string]string { // error check is covered by unit tests
// BuiltIn returns a map of built-in templates. The key is the template name and the value is the template content.
func BuiltIn() map[string]string {
var (
list, _ = fs.ReadDir(content, ".")
list, _ = fs.ReadDir(content, ".") // error check is covered by unit tests
result = make(map[string]string, len(list))
)

118
templates/ghost.html Normal file
View File

@ -0,0 +1,118 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="noindex, nofollow" />
<title>{{ code }}: {{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
<style>
html,body {background-color:#1a1a1a;color:#fff;font-family:'Open Sans',sans-serif;height:100vh;margin:0;font-size:0}
.container {height:100vh;align-items:center;display:flex;justify-content:center;position:relative}
.wrap {text-align:center}
.ghost {animation:float 3s ease-out infinite}
@keyframes float { 50% {transform:translate(0,20px)}}
.shadowFrame {width:130px;margin: 10px auto 0 auto}
.shadow {animation:shrink 3s ease-out infinite;transform-origin:center center}
@keyframes shrink {0%{width:90%;margin:0 5%} 50% {width:60%;margin:0 18%} 100% {width:90%;margin:0 5%}}
h3 {font-size:17px;text-transform: uppercase;margin:0.3em auto}
.description {font-size:13px;color:#aaa}
/* {{ if show_details }} */
.details {color:#999;width:100%}
.details table {width:100%}
.details td {white-space:nowrap;font-size:11px}
.details .name {text-align:right;padding-right:.6em;width:50%}
.details .value {text-align:left;padding-left:.6em;font-family:'Lucida Console','Courier New',monospace}
/* {{ end }} */
</style>
</head>
<body>
<div class="container">
<div class="wrap">
<svg class="ghost" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="127.433px" height="132.743px" viewBox="0 0 127.433 132.743" xml:space="preserve">
<path fill="#FFF6F4" d="M116.223,125.064c1.032-1.183,1.323-2.73,1.391-3.747V54.76c0,0-4.625-34.875-36.125-44.375 s-66,6.625-72.125,44l-0.781,63.219c0.062,4.197,1.105,6.177,1.808,7.006c1.94,1.811,5.408,3.465,10.099-0.6 c7.5-6.5,8.375-10,12.75-6.875s5.875,9.75,13.625,9.25s12.75-9,13.75-9.625s4.375-1.875,7,1.25s5.375,8.25,12.875,7.875 s12.625-8.375,12.625-8.375s2.25-3.875,7.25,0.375s7.625,9.75,14.375,8.125C114.739,126.01,115.412,125.902,116.223,125.064z"></path>
<circle fill="#1a1a1a" cx="86.238" cy="57.885" r="6.667"></circle>
<circle fill="#1a1a1a" cx="40.072" cy="57.885" r="6.667"></circle>
<path fill="#1a1a1a" d="M71.916,62.782c0.05-1.108-0.809-2.046-1.917-2.095c-0.673-0.03-1.28,0.279-1.667,0.771 c-0.758,0.766-2.483,2.235-4.696,2.358c-1.696,0.094-3.438-0.625-5.191-2.137c-0.003-0.003-0.007-0.006-0.011-0.009l0.002,0.005 c-0.332-0.294-0.757-0.488-1.235-0.509c-1.108-0.049-2.046,0.809-2.095,1.917c-0.032,0.724,0.327,1.37,0.887,1.749 c-0.001,0-0.002-0.001-0.003-0.001c2.221,1.871,4.536,2.88,6.912,2.986c0.333,0.014,0.67,0.012,1.007-0.01 c3.163-0.191,5.572-1.942,6.888-3.166l0.452-0.453c0.021-0.019,0.04-0.041,0.06-0.061l0.034-0.034 c-0.007,0.007-0.015,0.014-0.021,0.02C71.666,63.771,71.892,63.307,71.916,62.782z"></path>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.614" cy="99.426" r="3.292"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="95.364" cy="28.676" r="3.291"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="24.739" cy="93.551" r="2.667"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="101.489" cy="33.051" r="2.666"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.738" cy="87.717" r="2.833"></circle>
<path fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" d="M116.279,55.814c-0.021-0.286-2.323-28.744-30.221-41.012 c-7.806-3.433-15.777-5.173-23.691-5.173c-16.889,0-30.283,7.783-37.187,15.067c-9.229,9.736-13.84,26.712-14.191,30.259 l-0.748,62.332c0.149,2.133,1.389,6.167,5.019,6.167c1.891,0,4.074-1.083,6.672-3.311c4.96-4.251,7.424-6.295,9.226-6.295 c1.339,0,2.712,1.213,5.102,3.762c4.121,4.396,7.461,6.355,10.833,6.355c2.713,0,5.311-1.296,7.942-3.962 c3.104-3.145,5.701-5.239,8.285-5.239c2.116,0,4.441,1.421,7.317,4.473c2.638,2.8,5.674,4.219,9.022,4.219 c4.835,0,8.991-2.959,11.27-5.728l0.086-0.104c1.809-2.2,3.237-3.938,5.312-3.938c2.208,0,5.271,1.942,9.359,5.936 c0.54,0.743,3.552,4.674,6.86,4.674c1.37,0,2.559-0.65,3.531-1.932l0.203-0.268L116.279,55.814z M114.281,121.405 c-0.526,0.599-1.096,0.891-1.734,0.891c-2.053,0-4.51-2.82-5.283-3.907l-0.116-0.136c-4.638-4.541-7.975-6.566-10.82-6.566 c-3.021,0-4.884,2.267-6.857,4.667l-0.086,0.104c-1.896,2.307-5.582,4.999-9.725,4.999c-2.775,0-5.322-1.208-7.567-3.59 c-3.325-3.528-6.03-5.102-8.772-5.102c-3.278,0-6.251,2.332-9.708,5.835c-2.236,2.265-4.368,3.366-6.518,3.366 c-2.772,0-5.664-1.765-9.374-5.723c-2.488-2.654-4.29-4.395-6.561-4.395c-2.515,0-5.045,2.077-10.527,6.777 c-2.727,2.337-4.426,2.828-5.37,2.828c-2.662,0-3.017-4.225-3.021-4.225l0.745-62.163c0.332-3.321,4.767-19.625,13.647-28.995 c3.893-4.106,10.387-8.632,18.602-11.504c-0.458,0.503-0.744,1.165-0.744,1.898c0,1.565,1.269,2.833,2.833,2.833 c1.564,0,2.833-1.269,2.833-2.833c0-1.355-0.954-2.485-2.226-2.764c4.419-1.285,9.269-2.074,14.437-2.074 c7.636,0,15.336,1.684,22.887,5.004c26.766,11.771,29.011,39.047,29.027,39.251V121.405z"></path>
</svg>
<p class="shadowFrame">
<svg class="shadow" xmlns="http://www.w3.org/2000/svg" x="61px" y="20px" width="122.436px" height="39.744px" viewBox="0 0 122.436 39.744" xml:space="preserve">
<ellipse fill="#262626" cx="61.128" cy="19.872" rx="49.25" ry="8.916"></ellipse>
</svg>
</p>
<h3><span data-l10n>Error</span> {{ code }}</h3>
<p class="description" data-l10n>{{ description }}</p>
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name" data-l10n>Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name" data-l10n>Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name" data-l10n>Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name" data-l10n>Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name" data-l10n>Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name" data-l10n>Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name" data-l10n>Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name" data-l10n>Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
<script>
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Template 1</title>
</head>
<body>
</body>
</html>

View File

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Template 2</title>
</head>
<body>
</body>
</html>