Compare commits

..

16 Commits

26 changed files with 1183 additions and 597 deletions

View File

@ -0,0 +1,18 @@
{
"$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.base.schema.json",
"name": "default",
"image": "golang:1.22-bookworm",
"features": {
"ghcr.io/guiyomh/features/golangci-lint:0": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/sshd:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"streetsidesoftware.code-spell-checker"
]
}
},
"postCreateCommand": "go mod download"
}

View File

@ -36,7 +36,7 @@ jobs:
tag: ${{ github.ref }}
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
run: mkdir ./out && ./${{ steps.values.outputs.binary-name }} build --index --target-dir ./out
run: mkdir ./out && ./${{ steps.values.outputs.binary-name }} build --index --disable-minification --target-dir ./out
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
uses: actions/upload-artifact@v4
with:
@ -93,13 +93,12 @@ jobs:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
build-args: "APP_VERSION=${{ steps.slug.outputs.version }}"
tags: ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
# tags: | # TODO: uncomment after the stable release
# tarampampam/error-pages:latest
# tarampampam/error-pages:${{ steps.slug.outputs.version }}
# tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
# tarampampam/error-pages:${{ steps.slug.outputs.version-major }}
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}
tags: |
tarampampam/error-pages:latest
tarampampam/error-pages:${{ steps.slug.outputs.version }}
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}

View File

@ -3,7 +3,7 @@
# -✂- this stage is used to develop and build the application locally -------------------------------------------------
FROM docker.io/library/golang:1.22-bookworm AS develop
# use the /var/tmp as the GOPATH to reuse the modules cache
# use the /var/tmp/go as the GOPATH to reuse the modules cache
ENV GOPATH="/var/tmp/go"
RUN set -x \
@ -32,7 +32,10 @@ FROM develop AS compile
# can be passed with any prefix (like `v1.2.3@GITHASH`), e.g.: `docker build --build-arg "APP_VERSION=v1.2.3" .`
ARG APP_VERSION="undefined@docker"
RUN --mount=type=bind,source=.,target=/src set -x \
# copy the source code
COPY . /src
RUN set -x \
&& go generate ./... \
&& CGO_ENABLED=0 LDFLAGS="-s -w -X gh.tarampamp.am/error-pages/internal/appmeta.version=${APP_VERSION}" \
go build -trimpath -ldflags "${LDFLAGS}" -o /tmp/error-pages ./cmd/error-pages/ \
@ -45,10 +48,11 @@ FROM docker.io/library/alpine:3.20 AS rootfs
WORKDIR /tmp/rootfs
# prepare rootfs for runtime
RUN --mount=type=bind,source=.,target=/src set -x \
&& mkdir -p ./etc ./bin \
RUN set -x \
&& mkdir -p ./etc/ssl/certs ./bin \
&& echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > ./etc/passwd \
&& echo 'appuser:x:10001:' > ./etc/group
&& echo 'appuser:x:10001:' > ./etc/group \
&& cp /etc/ssl/certs/ca-certificates.crt ./etc/ssl/certs/
# take the binary from the compile stage
COPY --from=compile /tmp/error-pages ./bin/error-pages
@ -69,7 +73,7 @@ ARG APP_VERSION="undefined@docker"
LABEL \
# docs: https://github.com/opencontainers/image-spec/blob/master/annotations.md
org.opencontainers.image.title="error-pages" \
org.opencontainers.image.description="Static server error pages in the docker image" \
org.opencontainers.image.description="Pretty server's error pages" \
org.opencontainers.image.url="https://github.com/tarampampam/error-pages" \
org.opencontainers.image.source="https://github.com/tarampampam/error-pages" \
org.opencontainers.image.vendor="tarampampam" \
@ -87,11 +91,12 @@ WORKDIR /opt
# to find out which environment variables and CLI arguments are supported by the application, run the app
# with the `--help` flag or refer to the documentation at https://github.com/tarampampam/error-pages#readme
ENV LOG_LEVEL="warn" \
LOG_FORMAT="json"
# docs: https://docs.docker.com/reference/dockerfile/#healthcheck
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD [\
"/bin/error-pages", "--log-format", "json", "healthcheck" \
]
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD ["/bin/error-pages", "healthcheck"]
ENTRYPOINT ["/bin/error-pages"]
CMD ["--log-format", "json", "serve"]
CMD ["serve"]

1043
README.md

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@ -4,6 +4,7 @@ go 1.22
require (
github.com/stretchr/testify v1.9.0
github.com/tdewolff/minify/v2 v2.20.35
github.com/urfave/cli-docs/v3 v3.0.0-alpha5
github.com/urfave/cli/v3 v3.0.0-alpha9
github.com/valyala/fasthttp v1.55.0
@ -19,6 +20,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tdewolff/parse/v2 v2.7.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

7
go.sum
View File

@ -26,6 +26,13 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.20.35 h1:/Vq/oivpkFyi2PViD25XHZZbJz+eO4OmPSgePex1kBU=
github.com/tdewolff/minify/v2 v2.20.35/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU=
github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw=
github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5 h1:H1oWnR2/GN0dNm2PVylws+GxSOD6YOwW/jI5l78YfPk=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5/go.mod h1:AIqom6Q60U4tiqHp41i7+/AB2XHgi1WvQ7jOFlccmZ4=
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=

View File

@ -6,7 +6,6 @@ import (
"runtime"
"strings"
_ "github.com/urfave/cli-docs/v3" // required for `go generate` to work
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/appmeta"
@ -17,7 +16,7 @@ import (
"gh.tarampamp.am/error-pages/internal/logger"
)
//go:generate go run update_readme.go
//go:generate go run app_generate.go
// NewApp creates a new console application.
func NewApp(appName string) *cli.Command { //nolint:funlen
@ -25,7 +24,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
logLevelFlag = cli.StringFlag{
Name: "log-level",
Value: logger.InfoLevel.String(),
Usage: "logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
Usage: "Logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
Sources: cli.EnvVars("LOG_LEVEL"),
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
@ -41,7 +40,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
logFormatFlag = cli.StringFlag{
Name: "log-format",
Value: logger.ConsoleFormat.String(),
Usage: "logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
Usage: "Logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
Sources: cli.EnvVars("LOG_FORMAT"),
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
@ -80,7 +79,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
serve.NewCommand(log),
build.NewCommand(log),
healthcheck.NewCommand(log, healthcheck.NewHTTPHealthChecker()),
perftest.NewCommand(log),
perftest.NewCommand(),
},
Version: fmt.Sprintf("%s (%s)", appmeta.Version(), runtime.Version()),
Flags: []cli.Flag{ // global flags

View File

@ -1,5 +1,4 @@
//go:build ignore
// +build ignore
//go:build generate
package main
@ -17,8 +16,10 @@ func main() {
if stat, err := os.Stat(readmePath); err == nil && stat.Mode().IsRegular() {
if err = cliDocs.ToTabularToFileBetweenTags(cli.NewApp(""), "error-pages", readmePath); err != nil {
panic(err)
} else {
println("✔ cli docs updated successfully")
}
} else if err != nil {
println("readme file not found, cli docs not updated:", err.Error())
println("readme file not found, cli docs not updated:", err.Error())
}
}

View File

@ -39,21 +39,24 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
cmd command
cfg = config.New()
addTplFlag = shared.AddTemplatesFlag
disableTplFlag = shared.DisableTemplateNamesFlag
addCodeFlag = shared.AddHTTPCodesFlag
disableL10nFlag = shared.DisableL10nFlag
createIndexFlag = cli.BoolFlag{
Name: "index",
Aliases: []string{"i"},
Usage: "generate index.html file with links to all error pages",
addTplFlag = shared.AddTemplatesFlag
disableTplFlag = shared.DisableTemplateNamesFlag
addCodeFlag = shared.AddHTTPCodesFlag
disableL10nFlag = shared.DisableL10nFlag
disableMinificationFlag = shared.DisableMinificationFlag
createIndexFlag = cli.BoolFlag{
Name: "index",
Aliases: []string{"i"},
Usage: "Generate index.html file with links to all error pages",
Category: shared.CategoryBuild,
}
targetDirFlag = cli.StringFlag{
Name: "target-dir",
Aliases: []string{"out", "dir", "o"},
Usage: "directory to put the built error pages into",
Usage: "Directory to put the built error pages into",
Value: ".", // current directory by default
Config: cli.StringConfig{TrimSpace: true},
Category: shared.CategoryBuild,
OnlyOnce: true,
Validator: func(dir string) error {
if dir == "" {
@ -79,6 +82,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
Usage: "Build the static error pages and put them into a specified directory",
Action: func(ctx context.Context, c *cli.Command) error {
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
cfg.DisableMinification = c.Bool(disableMinificationFlag.Name)
cmd.opt.createIndex = c.Bool(createIndexFlag.Name)
cmd.opt.targetDirAbsPath, _ = filepath.Abs(c.String(targetDirFlag.Name)) // an error checked by [os.Stat] validator
@ -138,13 +142,14 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
&disableL10nFlag,
&createIndexFlag,
&targetDirFlag,
&disableMinificationFlag,
},
}
return cmd.c
}
func (cmd *command) Run( //nolint:funlen
func (cmd *command) Run( //nolint:funlen,gocognit
ctx context.Context,
log *logger.Logger,
cfg *config.Config,
@ -170,13 +175,21 @@ func (cmd *command) Run( //nolint:funlen
var outFilePath = path.Join(cmd.opt.targetDirAbsPath, templateName, code+".html")
if content, renderErr := appTemplate.Render(templateContent, appTemplate.Props{
if content, renderErr := appTemplate.Render(templateContent, appTemplate.Props{ //nolint:nestif
Code: uint16(codeAsUint),
Message: codeDescription.Message,
Description: codeDescription.Description,
L10nDisabled: cfg.L10n.Disable,
ShowRequestDetails: false,
}); renderErr == nil {
if !cfg.DisableMinification {
if mini, minErr := appTemplate.MiniHTML(content); minErr != nil {
log.Warn("Cannot minify the content", logger.Error(minErr))
} else {
content = mini
}
}
if err := os.WriteFile(outFilePath, []byte(content), os.FileMode(0664)); err != nil { //nolint:mnd
return err
}

View File

@ -1,32 +1,59 @@
package perftest
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"math"
"os"
"os/exec"
"runtime"
"sync"
"sync/atomic"
"strconv"
"time"
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/logger"
)
const wrkOneCodeTestLua = `
local formats = { 'application/json', 'application/xml', 'text/html', 'text/plain' }
request = function()
wrk.headers["User-Agent"] = "wrk"
wrk.headers["X-Namespace"] = "NAMESPACE_" .. tostring(math.random(0, 99999999))
wrk.headers["X-Request-ID"] = "REQ_ID_" .. tostring(math.random(0, 99999999))
wrk.headers["Content-Type"] = formats[ math.random( 0, #formats - 1 ) ]
return wrk.format("GET", "/500.html?rnd=" .. tostring(math.random(0, 99999999)), nil, nil)
end
`
//nolint:lll
const bombDifferentCodes = `
local formats = { 'application/json', 'application/xml', 'text/html', 'text/plain' }
request = function()
wrk.headers["User-Agent"] = "wrk"
wrk.headers["X-Namespace"] = "NAMESPACE_" .. tostring(math.random(0, 99999999))
wrk.headers["X-Request-ID"] = "REQ_ID_" .. tostring(math.random(0, 99999999))
wrk.headers["Content-Type"] = formats[ math.random( 0, #formats - 1 ) ]
return wrk.format("GET", "/" .. tostring(math.random(400, 599)) .. ".html?rnd=" .. tostring(math.random(0, 99999999)), nil, nil)
end
`
// NewCommand creates `perftest` command.
func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
func NewCommand() *cli.Command { //nolint:funlen
var (
portFlag = shared.ListenPortFlag
durationFlag = cli.DurationFlag{
Name: "duration",
Aliases: []string{"d"},
Usage: "duration of the test",
Value: 10 * time.Second, //nolint:mnd
Usage: "Duration of test",
Value: 15 * time.Second, //nolint:mnd
Validator: func(d time.Duration) error {
if d <= time.Second {
return errors.New("duration can't be less than 1 second")
@ -38,11 +65,28 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
threadsFlag = cli.UintFlag{
Name: "threads",
Aliases: []string{"t"},
Usage: "number of threads",
Value: max(2, uint64(runtime.NumCPU()/2)), //nolint:mnd
Usage: "Number of threads to use",
Value: max(2, uint64(math.Round(float64(runtime.NumCPU())/1.3))), //nolint:mnd
Validator: func(u uint64) error {
if u == 0 {
return errors.New("threads number can't be zero")
} else if u > math.MaxUint16 {
return errors.New("threads number can't be greater than 65535")
}
return nil
},
}
connectionsFlag = cli.UintFlag{
Name: "connections",
Aliases: []string{"c"},
Usage: "Number of connections to keep open",
Value: max(16, uint64(runtime.NumCPU()*25)), //nolint:mnd
Validator: func(u uint64) error {
if u == 0 {
return errors.New("threads number can't be zero")
} else if u > math.MaxUint16 {
return errors.New("threads number can't be greater than 65535")
}
return nil
@ -52,97 +96,47 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
return &cli.Command{
Name: "perftest",
Aliases: []string{"perf", "test"},
Aliases: []string{"perf", "benchmark", "bench"},
Hidden: true,
Usage: "Simple performance (load) test for the HTTP server",
Action: func(ctx context.Context, c *cli.Command) error { // TODO: use fasthttp.Client
var (
perfCtx, cancel = context.WithTimeout(ctx, c.Duration(durationFlag.Name))
startedAt = time.Now()
wg sync.WaitGroup
success atomic.Uint64
failed atomic.Uint64
)
defer func() {
cancel()
log.Info("Summary",
logger.Uint64("success", success.Load()),
logger.Uint64("failed", failed.Load()),
logger.Duration("duration", time.Since(startedAt)),
logger.Float64("RPS", float64(success.Load()+failed.Load())/time.Since(startedAt).Seconds()),
logger.Float64("errors rate", float64(failed.Load())/float64(success.Load()+failed.Load())*100), //nolint:mnd
)
}()
log.Info("Running test",
logger.Uint64("threads", c.Uint(threadsFlag.Name)),
logger.Duration("duration", c.Duration(durationFlag.Name)),
)
var httpClient = &http.Client{
Transport: &http.Transport{MaxConnsPerHost: max(2, int(c.Uint(threadsFlag.Name))-1)}, //nolint:mnd
Timeout: c.Duration(durationFlag.Name) + time.Second,
Usage: "Performance (load) test for the HTTP server (locally installed wrk is required)",
Action: func(ctx context.Context, c *cli.Command) error {
var wrkBinPath, lErr = exec.LookPath("wrk")
if lErr != nil {
return fmt.Errorf("seems like wrk (https://github.com/wg/wrk) is not installed: %w", lErr)
}
for i := uint64(0); i < c.Uint(threadsFlag.Name); i++ {
wg.Add(1)
var runTest = func(scriptContent string) error {
if stdOut, stdErr, err := wrkRunTest(ctx,
wrkBinPath,
uint16(c.Uint(threadsFlag.Name)),
uint16(c.Uint(connectionsFlag.Name)),
c.Duration(durationFlag.Name),
uint16(c.Uint(portFlag.Name)),
scriptContent,
); err != nil {
var errData, _ = io.ReadAll(stdErr)
go func(log *logger.Logger) {
defer wg.Done()
return fmt.Errorf("failed to execute the test: %w (%s)", err, string(errData))
} else {
var outData, _ = io.ReadAll(stdOut)
if perfCtx.Err() != nil {
return
}
printf("Test completed successfully. Here is the output:\n\n%s\n", string(outData))
}
var req, rErr = makeRequest(perfCtx, uint16(c.Uint(portFlag.Name)))
if rErr != nil {
log.Error("Failed to create a new request", logger.Error(rErr))
return
}
for {
var sentAt = time.Now()
var resp, respErr = httpClient.Do(req)
if resp != nil {
if _, err := io.Copy(io.Discard, resp.Body); err != nil && !errIsDone(err) {
log.Error("Failed to read response body", logger.Error(err))
}
if err := resp.Body.Close(); err != nil && !errIsDone(err) {
log.Error("Failed to close response body", logger.Error(err))
}
}
if respErr != nil {
if errIsDone(respErr) {
return
}
log.Error("Request failed", logger.Error(respErr))
failed.Add(1)
continue
}
log.Debug("Response received",
logger.String("status", resp.Status),
logger.Duration("duration", time.Since(sentAt)),
logger.Int64("size", resp.ContentLength),
logger.Uint64("success", success.Load()),
logger.Uint64("failed", failed.Load()),
)
success.Add(1)
}
}(log.Named(fmt.Sprintf("thread-%d", i)))
return nil
}
wg.Wait()
printf("Starting the test to bomb ONE PAGE (code). Please, be patient...\n")
if err := runTest(wrkOneCodeTestLua); err != nil {
return err
}
printf("Starting the test to bomb DIFFERENT PAGES (codes). Please, be patient...\n")
if err := runTest(bombDifferentCodes); err != nil {
return err
}
return nil
},
@ -150,54 +144,51 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
&portFlag,
&durationFlag,
&threadsFlag,
&connectionsFlag,
},
}
}
// randomIntBetween returns a random integer between min and max.
func randomIntBetween(min, max int) int { return min + rand.Intn(max-min) } //nolint:gosec
func printf(format string, args ...any) { fmt.Printf(format, args...) } //nolint:forbidigo
// makeRequest creates a new HTTP request for the performance test.
func makeRequest(ctx context.Context, port uint16) (*http.Request, error) {
var req, rErr = http.NewRequestWithContext(ctx,
http.MethodGet,
fmt.Sprintf(
"http://127.0.0.1:%d/%d.html?rnd=%d", // for load testing purposes only
port,
randomIntBetween(400, 418), //nolint:mnd
randomIntBetween(1, 999_999_999), //nolint:mnd
),
http.NoBody,
func wrkRunTest(
ctx context.Context,
wrkBinPath string,
threadsCount, connectionsCount uint16,
duration time.Duration,
port uint16,
scriptContent string,
) (io.Reader, io.Reader, error) {
var tmpFile, tErr = os.CreateTemp("", "ep-perf-one-page")
if tErr != nil {
return nil, nil, fmt.Errorf("failed to create a temporary file: %w", tErr)
}
defer func() {
_ = tmpFile.Close()
_ = os.Remove(tmpFile.Name())
}()
if _, err := tmpFile.WriteString(scriptContent); err != nil {
return nil, nil, fmt.Errorf("failed to write to a temporary file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return nil, nil, err
}
var stdout, stderr bytes.Buffer
var cmd = exec.CommandContext(ctx, wrkBinPath, //nolint:gosec
"--timeout", "1s",
"--threads", strconv.FormatUint(uint64(threadsCount), 10),
"--connections", strconv.FormatUint(uint64(connectionsCount), 10),
"--duration", duration.String(),
"--script", tmpFile.Name(),
fmt.Sprintf("http://127.0.0.1:%d/", port),
)
if rErr != nil {
return nil, rErr
}
cmd.Stdout, cmd.Stderr = &stdout, &stderr
req.Header.Set("Connection", "keep-alive")
req.Header.Set("User-Agent", "perftest")
req.Header.Set("X-Namespace", fmt.Sprintf("namespace-%d", randomIntBetween(1, 999_999_999))) //nolint:mnd
req.Header.Set("X-Request-ID", fmt.Sprintf("req-id-%d", randomIntBetween(1, 999_999_999))) //nolint:mnd
var contentType string
switch randomIntBetween(1, 4) { //nolint:mnd
case 1:
contentType = "application/json"
case 2: //nolint:mnd
contentType = "application/xml"
case 3: //nolint:mnd
contentType = "text/html"
default:
contentType = "text/plain"
}
req.Header.Set("Content-Type", contentType)
return req, nil
}
// errIsDone checks if the error is a context.DeadlineExceeded or context.Canceled.
func errIsDone(err error) bool {
return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)
return &stdout, &stderr, cmd.Run() // execute
}

View File

@ -38,33 +38,37 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
)
var (
addrFlag = shared.ListenAddrFlag
portFlag = shared.ListenPortFlag
addTplFlag = shared.AddTemplatesFlag
disableTplFlag = shared.DisableTemplateNamesFlag
addCodeFlag = shared.AddHTTPCodesFlag
disableL10nFlag = shared.DisableL10nFlag
jsonFormatFlag = cli.StringFlag{
addrFlag = shared.ListenAddrFlag
portFlag = shared.ListenPortFlag
addTplFlag = shared.AddTemplatesFlag
disableTplFlag = shared.DisableTemplateNamesFlag
addCodeFlag = shared.AddHTTPCodesFlag
disableL10nFlag = shared.DisableL10nFlag
disableMinificationFlag = shared.DisableMinificationFlag
jsonFormatFlag = cli.StringFlag{
Name: "json-format",
Usage: "override the default error page response in JSON format (Go templates are supported; the error " +
Usage: "Override the default error page response in JSON format (Go templates are supported; the error " +
"page will use this template if the client requests JSON content type)",
Sources: env("RESPONSE_JSON_FORMAT"),
Category: shared.CategoryFormats,
OnlyOnce: true,
Config: trim,
}
xmlFormatFlag = cli.StringFlag{
Name: "xml-format",
Usage: "override the default error page response in XML format (Go templates are supported; the error " +
Usage: "Override the default error page response in XML format (Go templates are supported; the error " +
"page will use this template if the client requests XML content type)",
Sources: env("RESPONSE_XML_FORMAT"),
Category: shared.CategoryFormats,
OnlyOnce: true,
Config: trim,
}
plainTextFormatFlag = cli.StringFlag{
Name: "plaintext-format",
Usage: "override the default error page response in plain text format (Go templates are supported; the " +
Usage: "Override the default error page response in plain text format (Go templates are supported; the " +
"error page will use this template if the client requests plain text content type or does not specify any)",
Sources: env("RESPONSE_PLAINTEXT_FORMAT"),
Category: shared.CategoryFormats,
OnlyOnce: true,
Config: trim,
}
@ -72,17 +76,19 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
Name: "template-name",
Aliases: []string{"t"},
Value: cfg.TemplateName,
Usage: "name of the template to use for rendering error pages (built-in templates: " +
Usage: "Name of the template to use for rendering error pages (built-in templates: " +
strings.Join(cfg.Templates.Names(), ", ") + ")",
Sources: env("TEMPLATE_NAME"),
Category: shared.CategoryTemplates,
OnlyOnce: true,
Config: trim,
}
defaultCodeToRenderFlag = cli.UintFlag{
Name: "default-error-page",
Usage: "the code of the default (index page, when a code is not specified) error page to render",
Value: uint64(cfg.DefaultCodeToRender),
Sources: env("DEFAULT_ERROR_PAGE"),
Name: "default-error-page",
Usage: "The code of the default (index page, when a code is not specified) error page to render",
Value: uint64(cfg.DefaultCodeToRender),
Sources: env("DEFAULT_ERROR_PAGE"),
Category: shared.CategoryCodes,
Validator: func(code uint64) error {
if code > 999 { //nolint:mnd
return fmt.Errorf("wrong HTTP code [%d] for the default error page", code)
@ -94,17 +100,19 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
}
sendSameHTTPCodeFlag = cli.BoolFlag{
Name: "send-same-http-code",
Usage: "the HTTP response should have the same status code as the requested error page (by default, " +
Usage: "The HTTP response should have the same status code as the requested error page (by default, " +
"every response with an error page will have a status code of 200)",
Value: cfg.RespondWithSameHTTPCode,
Sources: env("SEND_SAME_HTTP_CODE"),
Category: shared.CategoryOther,
OnlyOnce: true,
}
showDetailsFlag = cli.BoolFlag{
Name: "show-details",
Usage: "show request details in the error page response (if supported by the template)",
Usage: "Show request details in the error page response (if supported by the template)",
Value: cfg.ShowDetails,
Sources: env("SHOW_DETAILS"),
Category: shared.CategoryOther,
OnlyOnce: true,
}
proxyHeadersListFlag = cli.StringFlag{
@ -122,14 +130,16 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
return nil
},
Category: shared.CategoryOther,
OnlyOnce: true,
Config: trim,
}
rotationModeFlag = cli.StringFlag{
Name: "rotation-mode",
Value: config.RotationModeDisabled.String(),
Usage: "templates automatic rotation mode (" + strings.Join(config.RotationModeStrings(), "/") + ")",
Usage: "Templates automatic rotation mode (" + strings.Join(config.RotationModeStrings(), "/") + ")",
Sources: env("TEMPLATES_ROTATION_MODE"),
Category: shared.CategoryTemplates,
OnlyOnce: true,
Config: trim,
Validator: func(s string) error {
@ -142,26 +152,27 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
}
readBufferSizeFlag = cli.UintFlag{
Name: "read-buffer-size",
Usage: "per-connection buffer size in bytes for reading requests, this also limits the maximum header size " +
Usage: "Per-connection buffer size in bytes for reading requests, this also limits the maximum header size " +
"(increase this buffer if your clients send multi-KB Request URIs and/or multi-KB headers (e.g., " +
"large cookies), note that increasing this value will increase memory consumption)",
Value: 1024 * 5, //nolint:mnd // 5 KB
Sources: env("READ_BUFFER_SIZE"),
Category: shared.CategoryOther,
OnlyOnce: true,
}
)
// override some flag usage messages
addrFlag.Usage = "the HTTP server will listen on this IP (v4 or v6) address (set 127.0.0.1 for localhost, " +
addrFlag.Usage = "The HTTP server will listen on this IP (v4 or v6) address (set 127.0.0.1/::1 for localhost, " +
"0.0.0.0 to listen on all interfaces, or specify a custom IP)"
portFlag.Usage = "the TCP port number for the HTTP server to listen on (0-65535)"
portFlag.Usage = "The TCP port number for the HTTP server to listen on (0-65535)"
disableL10nFlag.Value = cfg.L10n.Disable // set the default value depending on the configuration
cmd.c = &cli.Command{
Name: "serve",
Aliases: []string{"s", "server", "http"},
Usage: "Start HTTP server",
Usage: "Please start the HTTP server to serve the error pages. You can configure various options - please RTFM :D",
Suggest: true,
Action: func(ctx context.Context, c *cli.Command) error {
cmd.opt.http.addr = c.String(addrFlag.Name)
@ -172,6 +183,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name)
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
cfg.DisableMinification = c.Bool(disableMinificationFlag.Name)
{ // override default JSON, XML, and PlainText formats
if c.IsSet(jsonFormatFlag.Name) {
@ -293,6 +305,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
&proxyHeadersListFlag,
&rotationModeFlag,
&readBufferSizeFlag,
&disableMinificationFlag,
},
}

View File

@ -36,7 +36,7 @@ func TestCommand_Run(t *testing.T) {
"--add-template", "./testdata/foo-template.html",
"--disable-template", "ghost",
"--disable-template", "<unknown>",
"--add-http-code", "200=Code/Description",
"--add-code", "200=Code/Description",
"--json-format", "json format",
"--xml-format", "xml format",
"--plaintext-format", "plaintext format",

View File

@ -11,6 +11,15 @@ import (
"gh.tarampamp.am/error-pages/internal/config"
)
const (
CategoryHTTP = "HTTP:"
CategoryTemplates = "TEMPLATES:"
CategoryCodes = "HTTP CODES:"
CategoryFormats = "FORMATS:"
CategoryBuild = "BUILD:"
CategoryOther = "OTHER:"
)
// Note: Don't use pointers for flags, because they have own state which is not thread-safe.
// https://github.com/urfave/cli/issues/1926
@ -20,6 +29,7 @@ var ListenAddrFlag = cli.StringFlag{
Usage: "IP (v4 or v6) address to listen on",
Value: "0.0.0.0", // bind to all interfaces by default
Sources: cli.EnvVars("LISTEN_ADDR"),
Category: CategoryHTTP,
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
Validator: func(ip string) error {
@ -41,6 +51,7 @@ var ListenPortFlag = cli.UintFlag{
Usage: "TCP port number",
Value: 8080, // default port number
Sources: cli.EnvVars("LISTEN_PORT"),
Category: CategoryHTTP,
OnlyOnce: true,
Validator: func(port uint64) error {
if port == 0 || port > 65535 {
@ -53,9 +64,11 @@ var ListenPortFlag = cli.UintFlag{
var AddTemplatesFlag = cli.StringSliceFlag{
Name: "add-template",
Usage: "to add a new template, provide the path to the file using this flag (the filename without the extension " +
Usage: "To add a new template, provide the path to the file using this flag (the filename without the extension " +
"will be used as the template name)",
Config: cli.StringConfig{TrimSpace: true},
Config: cli.StringConfig{TrimSpace: true},
Sources: cli.EnvVars("ADD_TEMPLATE"),
Category: CategoryTemplates,
Validator: func(paths []string) error {
for _, path := range paths {
if path == "" {
@ -72,18 +85,19 @@ var AddTemplatesFlag = cli.StringSliceFlag{
}
var DisableTemplateNamesFlag = cli.StringSliceFlag{
Name: "disable-template",
Usage: "disable the specified template by its name (useful to disable the built-in templates and use only custom ones)",
Config: cli.StringConfig{TrimSpace: true},
Name: "disable-template",
Usage: "Disable the specified template by its name (useful to disable the built-in templates and use only custom ones)",
Config: cli.StringConfig{TrimSpace: true},
Category: CategoryTemplates,
}
var AddHTTPCodesFlag = cli.StringMapFlag{
Name: "add-http-code",
Aliases: []string{"add-code"},
Usage: "to add a new HTTP status code, provide the code and its message/description using this flag (the format " +
Name: "add-code",
Usage: "To add a new HTTP status code, provide the code and its message/description using this flag (the format " +
"should be '%code%=%message%/%description%'; the code may contain a wildcard '*' to cover multiple codes at " +
"once, for example, '4**' will cover all 4xx codes unless a more specific code is described previously)",
Config: cli.StringConfig{TrimSpace: true},
Config: cli.StringConfig{TrimSpace: true},
Category: CategoryCodes,
Validator: func(codes map[string]string) error {
for code, msgAndDesc := range codes {
if code == "" {
@ -128,7 +142,16 @@ func ParseHTTPCodes(codes map[string]string) map[string]config.CodeDescription {
var DisableL10nFlag = cli.BoolFlag{
Name: "disable-l10n",
Usage: "disable localization of error pages (if the template supports localization)",
Usage: "Disable localization of error pages (if the template supports localization)",
Sources: cli.EnvVars("DISABLE_L10N"),
Category: CategoryOther,
OnlyOnce: true,
}
var DisableMinificationFlag = cli.BoolFlag{
Name: "disable-minification",
Usage: "Disable the minification of HTML pages, including CSS, SVG, and JS (may be useful for debugging)",
Sources: cli.EnvVars("DISABLE_MINIFICATION"),
Category: CategoryOther,
OnlyOnce: true,
}

View File

@ -91,6 +91,7 @@ func TestAddTemplatesFlag(t *testing.T) {
var flag = shared.AddTemplatesFlag
assert.Equal(t, "add-template", flag.Name)
assert.Contains(t, flag.Sources.String(), "ADD_TEMPLATE")
for wantErrMsg, giveValue := range map[string][]string{
"missing template path": {""},
@ -122,7 +123,7 @@ func TestAddHTTPCodesFlag(t *testing.T) {
var flag = shared.AddHTTPCodesFlag
assert.Equal(t, "add-http-code", flag.Name)
assert.Equal(t, "add-code", flag.Name)
for name, tt := range map[string]struct {
giveValue map[string]string
@ -216,3 +217,12 @@ func TestDisableL10nFlag(t *testing.T) {
assert.Equal(t, "disable-l10n", flag.Name)
assert.Contains(t, flag.Sources.String(), "DISABLE_L10N")
}
func TestDisableMinificationFlag(t *testing.T) {
t.Parallel()
var flag = shared.DisableMinificationFlag
assert.Equal(t, "disable-minification", flag.Name)
assert.Contains(t, flag.Sources.String(), "DISABLE_MINIFICATION")
}

View File

@ -56,6 +56,9 @@ type Config struct {
// ShowDetails determines whether to show additional details in the error response, extracted from the
// incoming request (if supported by the template).
ShowDetails bool
// DisableMinification determines whether to disable minification of the rendered content (e.g., HTML, CSS) or not.
DisableMinification bool
}
const defaultJSONFormat string = `{

View File

@ -24,6 +24,7 @@ func TestNew(t *testing.T) {
assert.NotEmpty(t, cfg.TemplateName)
assert.True(t, cfg.Templates.Has(cfg.TemplateName))
assert.Equal(t, uint16(http.StatusNotFound), cfg.DefaultCodeToRender)
assert.False(t, cfg.DisableMinification)
})
t.Run("changing cfg1 should not affect cfg2", func(t *testing.T) {

View File

@ -175,6 +175,14 @@ func New(cfg *config.Config, log *logger.Logger) (_ fasthttp.RequestHandler, clo
err.Error(),
))
} else {
if !cfg.DisableMinification {
if mini, minErr := template.MiniHTML(content); minErr != nil {
log.Warn("HTML minification failed", logger.Error(minErr))
} else {
content = mini
}
}
cache.Put(tpl, tplProps, []byte(content))
write(ctx, log, content)

View File

@ -48,7 +48,7 @@ func TestHandler(t *testing.T) {
wantStatusCode: http.StatusOK,
wantHeaders: map[string]string{"Content-Type": "text/html; charset=utf-8"},
wantBodyIncludes: []string{
"<!DOCTYPE html>",
"<!doctype html>",
"<title>407: Proxy Authentication Required",
"Proxy Authentication Required",
},

View File

@ -51,7 +51,7 @@ func NewServer(log *logger.Logger, readBufferSize uint) Server {
}
// Register server handlers, middlewares, etc.
func (s *Server) Register(cfg *config.Config) error {
func (s *Server) Register(cfg *config.Config) error { //nolint:funlen
var (
liveHandler = live.New()
versionHandler = version.New(appmeta.Version())
@ -71,7 +71,7 @@ func (s *Server) Register(cfg *config.Config) error {
switch {
// live endpoints
case url == "/health/live" || url == "/health" || url == "/healthz" || url == "/live":
case url == "/healthz" || url == "/health/live" || url == "/health" || url == "/live":
liveHandler(ctx)
// version endpoint
@ -87,8 +87,9 @@ func (s *Server) Register(cfg *config.Config) error {
// - /{code}.html
// - /{code}.htm
// - /{code}
case method == fasthttp.MethodGet &&
(url == "/" || ep.URLContainsCode(url) || ep.HeadersContainCode(&ctx.Request.Header)):
//
// the HTTP method is not limited to GET and HEAD - it can be any
case url == "/" || ep.URLContainsCode(url) || ep.HeadersContainCode(&ctx.Request.Header):
errorPagesHandler(ctx)
// wrong requests handling

View File

@ -221,6 +221,16 @@ func TestRouting(t *testing.T) {
assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("other HTTP methods", func(t *testing.T) {
for _, method := range []string{http.MethodDelete, http.MethodPatch, http.MethodPost, http.MethodPut} {
var status, body, headers = sendRequest(t, method, baseUrl+"/404.html")
assert.Equal(t, http.StatusOK, status)
assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
}
})
})
t.Run("failure", func(t *testing.T) {
@ -264,15 +274,6 @@ func TestRouting(t *testing.T) {
assert.Equal(t, http.StatusNotFound, status)
assertIsNotErrorPage(t, body)
})
t.Run("invalid HTTP methods", func(t *testing.T) {
for _, method := range []string{http.MethodDelete, http.MethodPatch, http.MethodPost, http.MethodPut} {
var status, body, _ = sendRequest(t, method, baseUrl+"/404.html")
assert.Equal(t, http.StatusMethodNotAllowed, status)
assertIsNotErrorPage(t, body)
}
})
})
})

View File

@ -0,0 +1,23 @@
package template
import (
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
"github.com/tdewolff/minify/v2/js"
"github.com/tdewolff/minify/v2/svg"
)
var htmlMinify = func() *minify.M { //nolint:gochecknoglobals
var m = minify.New()
m.AddFunc("text/css", css.Minify)
m.Add("text/html", &html.Minifier{KeepDocumentTags: true, KeepEndTags: true, KeepQuotes: true})
m.AddFunc("image/svg+xml", svg.Minify)
m.AddFunc("application/javascript", js.Minify)
return m
}()
// MiniHTML minifies HTML data, including inline CSS, SVG and JS.
func MiniHTML(data string) (string, error) { return htmlMinify.String("text/html", data) }

View File

@ -0,0 +1,94 @@
package template_test
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/template"
)
func TestMiniHTML(t *testing.T) {
t.Parallel()
var wg sync.WaitGroup
for range 100 { // race condition provocation
wg.Add(1)
go func() {
defer wg.Done()
for give, want := range map[string]string{
"": "",
`<!-- Simple HTML page -->
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<h1 align="center">Test</h1>
</body>
</html>`: `<!doctype html><html><head><title>Test</title></head><body><h1 align="center">Test</h1></body></html>`,
`<!-- css styles -->
<html>
<head>
<style>
.foo:hover {
color: #f0a; /* comment */
}
</style>
</head>
<body>
<p style="color: red" class="bar">Text</p>
</body>
</html>`: `<html><head><style>.foo:hover{color:#f0a}</style></head><body><p style="color:red" class="bar">Text</p></body></html>`,
`<!-- svg -->
<svg xmlns="http://www.w3.org/2000/svg">
<g>
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
</g>
</svg>`: `<svg><g><circle cx="50" cy="50" r="40" stroke="#000" stroke-width="3" fill="red"/></g></svg>`,
`<!-- js -->
<html>
<body>
<script>
// comment
console.log('Hello, World!');
let foo = 1;
foo++;
</script>
</body>
</html>`: `<html><body><script>console.log("Hello, World!");let foo=1;foo++</script></body></html>`,
`<!-- js module not changed -->
<html>
<body>
<script type="module">
// comment
console.log('Hello, World!');
let foo = 1;
foo++;
</script>
</body>
</html>`: `<html><body><script type="module">
// comment
console.log('Hello, World!');
let foo = 1;
foo++;
</script></body></html>`,
} {
var got, err = template.MiniHTML(give)
assert.NoError(t, err)
assert.Equal(t, want, got)
}
}()
}
wg.Wait()
}

View File

@ -16,7 +16,7 @@ Object.defineProperty(window, 'l10n', {
*
* @link https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes language codes list (column `Set 1` or `ISO 639-1:2002`)
*
* @type {Map<string, Map<'fr'|'ru'|'uk'|'pt'|'nl'|'de'|'es'|'zh'|'id'|'pl', string>>}
* @type {Map<string, Map<'fr'|'ru'|'uk'|'pt'|'nl'|'de'|'es'|'zh'|'id'|'pl'|'ko', string>>}
*/
const data = Object.freeze(new Map([
[tkn('Error'), new Map([
@ -30,6 +30,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '错误'],
['id', 'Kesalahan'],
['pl', 'Błąd'],
['ko', '오류'],
])],
[tkn('Good luck'), new Map([
['fr', 'Bonne chance'],
@ -42,6 +43,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '祝好运'],
['id', 'Semoga berhasil!'],
['pl', 'Powodzenia'],
['ko', '행운을 빌어요'],
])],
[tkn('UH OH'), new Map([
['fr', 'Oups'],
@ -54,6 +56,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '哎呀'],
['id', 'Ups'],
['pl', 'Ojej'],
['ko', '헉'],
])],
[tkn('Request details'), new Map([
['fr', 'Détails de la requête'],
@ -66,6 +69,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求详情'],
['id', 'Rincian permintaan'],
['pl', 'Poproś o szczegóły'],
['ko', '요청 세부사항'],
])],
[tkn('Double-check the URL'), new Map([
['fr', 'Vérifiez lURL'],
@ -78,6 +82,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请再次检查地址'],
['id', 'Periksa URL'],
['pl', 'Sprawdź adres URL'],
['ko', 'URL을 다시 확인하세요'],
])],
[tkn('Alternatively, go back'), new Map([
['fr', 'Essayer de revenir en arrière'],
@ -90,6 +95,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '或返回上一页'],
['id', 'Atau, kembali'],
['pl', 'Alternatywnie wróć'],
['ko', '혹은, 돌아가기'],
])],
[tkn("Here's what might have happened"), new Map([
['fr', 'Voici ce qui aurait pu se passer'],
@ -102,6 +108,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '可能原因有'],
['id', 'Inilah yang bisa saja terjadi'],
['pl', 'Oto, co mogło się wydarzyć'],
['ko', '다음이 발생했을 수 있어요'],
])],
[tkn('You may have mistyped the URL'), new Map([
['fr', 'Vous avez peut-être mal tapé lURL'],
@ -114,6 +121,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '您可能输入了错误的地址'],
['id', 'Anda mungkin tersalah memasukkan URL'],
['pl', 'Być może błędnie wpisałeś adres URL'],
['ko', 'URL을 잘못 입력하셨을 수 있어요'],
])],
[tkn('The site was moved'), new Map([
['fr', 'Le site a été déplacé'],
@ -126,6 +134,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '站点已被转移'],
['id', 'Halaman dipindahkan'],
['pl', 'Witryna została przeniesiona'],
['ko', '사이트가 이동했어요'],
])],
[tkn('It was never here'), new Map([
['fr', 'Il na jamais été ici'],
@ -138,6 +147,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '站点从未存在'],
['id', 'Itu Tidak pernah di sini'],
['pl', 'Nigdy jej nie było'],
['ko', '여기에 있던 적이 없어요'],
])],
[tkn('Bad Request'), new Map([
['fr', 'Mauvaise requête'],
@ -150,6 +160,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '错误请求'],
['id', 'Permintaan yang salah'],
['pl', 'Nieprawidłowe żądanie'],
['ko', '잘못된 요청'],
])],
[tkn('The server did not understand the request'), new Map([
['fr', 'Le serveur ne comprend pas la requête'],
@ -162,6 +173,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务器不理解该请求'],
['id', 'Server tidak memahami permintaan'],
['pl', 'Serwer nie zrozumiał żądania'],
['ko', '서버가 요청을 이해하지 못했어요'],
])],
[tkn('Unauthorized'), new Map([
['fr', 'Non autorisé'],
@ -174,6 +186,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '未经授权'],
['id', 'Tidak diotorisasi'],
['pl', 'Nieautoryzowany'],
['ko', '권한 없음'],
])],
[tkn('The requested page needs a username and a password'), new Map([
['fr', 'La page demandée nécessite un nom dutilisateur et un mot de passe'],
@ -186,6 +199,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求的页面需要用户名和密码'],
['id', 'Halaman yang diminta membutuhkan nama pengguna dan kata sandi'],
['pl', 'Żądana strona wymaga podania nazwy użytkownika i hasła'],
['ko', '요청하신 페이지에는 사용자 이름과 비밀번호가 필요해요'],
])],
[tkn('Forbidden'), new Map([
['fr', 'Interdit'],
@ -198,6 +212,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '禁止访问'],
['id', 'Dilarang'],
['pl', 'Zabroniony'],
['ko', '금지됨'],
])],
[tkn('Access is forbidden to the requested page'), new Map([
['fr', 'Accès interdit à la page demandée'],
@ -210,6 +225,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '禁止访问请求的页面'],
['id', 'Akses dilarang ke halaman yang diminta'],
['pl', 'Dostęp do żądanej strony jest zabroniony'],
['ko', '요청하신 페이지에 대한 접근이 금지되어 있어요'],
])],
[tkn('Not Found'), new Map([
['fr', 'Introuvable'],
@ -222,6 +238,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '未找到'],
['id', 'Tidak ditemukan'],
['pl', 'Nie znaleziono'],
['ko', '찾을 수 없음'],
])],
[tkn('The server can not find the requested page'), new Map([
['fr', 'Le serveur ne peut trouver la page demandée'],
@ -234,6 +251,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务器找不到请求的页面'],
['id', 'Server tidak dapat menemukan halaman yang diminta'],
['pl', 'Serwer nie może znaleźć żądanej strony'],
['ko', '서버가 요청한 페이지를 찾을 수 없어요'],
])],
[tkn('Method Not Allowed'), new Map([
['fr', 'Méthode Non Autorisée'],
@ -246,6 +264,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '方法不被允许'],
['id', 'Metode tidak diizinkan'],
['pl', 'Niedozwolona metoda'],
['ko', '허용되지 않은 메소드'],
])],
[tkn('The method specified in the request is not allowed'), new Map([
['fr', 'La méthode spécifiée dans la requête nest pas autorisée'],
@ -258,6 +277,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求指定的方法不被允许'],
['id', 'Metode dalam permintaan tidak diizinkan'],
['pl', 'Metoda określona w żądaniu jest niedozwolona'],
['ko', '요청에 사용한 메소드는 허용되지 않아요'],
])],
[tkn('Proxy Authentication Required'), new Map([
['fr', 'Authentification proxy requise'],
@ -270,6 +290,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '需要代理服务器身份验证'],
['id', 'Diperlukan otentikasi proxy'],
['pl', 'Wymagane uwierzytelnianie proxy'],
['ko', '프록시 인증 필요'],
])],
[tkn('You must authenticate with a proxy server before this request can be served'), new Map([
['fr', 'Vous devez vous authentifier avec un serveur proxy avant que cette requête puisse être servie'],
@ -282,6 +303,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '您必须对代理服务器进行身份验证,然后才能让请求得到处理'],
['id', 'Anda harus mengautentikasi dengan server proxy sebelum permintaan ini dapat dilayani'],
['pl', 'Musisz uwierzytelnić się na serwerze proxy, zanim to żądanie będzie mogło zostać obsłużone'],
['ko', '이 요청을 처리하려면 프록시 서버로 인증해야 해요'],
])],
[tkn('Request Timeout'), new Map([
['fr', 'Requête expiré'],
@ -294,6 +316,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求超时'],
['id', 'Meminta batas waktu'],
['pl', 'Przekroczenie limitu czasu żądania'],
['ko', '요청 시간초과'],
])],
[tkn('The request took longer than the server was prepared to wait'), new Map([
['fr', 'La requête prend plus de temps que prévu'],
@ -306,6 +329,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求用时超过了服务器设置的最长等待时间'],
['id', 'Permintaan memakan waktu lebih lama dari yang bisa ditunggu oleh server'],
['pl', 'Żądanie trwało dłużej niż serwer był gotowy czekać'],
['ko', '요청이 서버가 기다릴 수 있는 시간보다 오래 걸렸어요'],
])],
[tkn('Conflict'), new Map([
['fr', 'Conflit'],
@ -318,6 +342,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '冲突'],
['id', 'Konflik'],
['pl', 'Konflikt'],
['ko', '상충'],
])],
[tkn('The request could not be completed because of a conflict'), new Map([
['fr', 'La requête na pas pu être complétée à cause dun conflit'],
@ -330,6 +355,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '由于冲突,请求无法完成'],
['id', 'Permintaan tidak dapat diselesaikan karena adanya konflik'],
['pl', 'Żądanie nie mogło zostać wykonane z powodu konfliktu'],
['ko', '상충으로 인해 요청을 완료할 수 없었어요'],
])],
[tkn('Gone'), new Map([
['fr', 'Supprimé'],
@ -342,6 +368,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '已移除'],
['id', 'Menghilang'],
['pl', 'Usunięto'],
['ko', '사라짐'],
])],
[tkn('The requested page is no longer available'), new Map([
['fr', 'La page demandée nest plus disponible'],
@ -354,6 +381,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求的页面不再可用'],
['id', 'Halaman yang diminta tidak lagi tersedia'],
['pl', 'Żądana strona nie jest już dostępna'],
['ko', '요청하신 페이지는 더 이상 사용할 수 없어요'],
])],
[tkn('Length Required'), new Map([
['fr', 'Longueur requise'],
@ -366,6 +394,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '需要长度'],
['id', 'Panjang yang diperlukan'],
['pl', 'Wymagana długość'],
['ko', '길이 필요'],
])],
[tkn('The "Content-Length" is not defined. The server will not accept the request without it'), new Map([
['fr', 'Le "Content-Length" nest pas défini. Le serveur ne prendra pas en compte la requête'],
@ -378,6 +407,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '未指定Content-Length(内容长度)。服务器将不接受不包含此头信息的请求'],
['id', '"Content-Length" tidak ditentukan. Server tidak akan menerima permintaan tanpa itu'],
['pl', 'Wartość "Content-Length" nie jest zdefiniowana. Serwer nie zaakceptuje żądania bez tego parametru'],
['ko', '"Content-Length"가 정의되지 않았습니다. 이 값이 없으면 서버는 요청을 수락하지 않아요'],
])],
[tkn('Precondition Failed'), new Map([
['fr', 'Échec de la condition préalable'],
@ -390,6 +420,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '前置条件判定失败'],
['id', 'Prasyarat gagal'],
['pl', 'Niespełnienie warunku wstępnego'],
['ko', '선결 조건 실패'],
])],
[tkn('The pre condition given in the request evaluated to false by the server'), new Map([
['fr', 'La précondition donnée dans la requête a été évaluée comme étant fausse par le serveur'],
@ -402,6 +433,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务器评估请求中给出的前置条件的结果为false(假)'],
['id', 'Prakondisi gagal'],
['pl', 'Warunek wstępny podany w żądaniu został oceniony przez serwer jako nieprawidłowy'],
['ko', '요청에 제공된 선결 조건을 서버는 거짓으로 평가했어요'],
])],
[tkn('Payload Too Large'), new Map([
['fr', 'Charge trop volumineuse'],
@ -414,6 +446,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求体过大'],
['id', 'Muatan terlalu besar'],
['pl', 'Żądanie jest zbyt duże'],
['ko', '콘텐츠가 너무 큼'],
])],
[tkn('The server will not accept the request, because the request entity is too large'), new Map([
['fr', 'Le serveur ne prendra pas en compte la requête, car lentité de la requête est trop volumineuse'],
@ -426,6 +459,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求体过大,服务器将不接受该请求'],
['id', 'Server tidak akan menerima permintaan, karena entitas permintaan terlalu besar'],
['pl', 'Serwer nie zaakceptuje żądania, ponieważ żądanie jest zbyt duże'],
['ko', '요청 엔터티가 너무 크기 때문에 서버가 요청을 수락하지 않았어요'],
])],
[tkn('Requested Range Not Satisfiable'), new Map([
['fr', 'Requête non satisfaisante'],
@ -438,6 +472,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '不满足请求范围'],
['id', 'Rentang yang diminta tidak dapat dipenuhi'],
['pl', 'Żądany zakres nie jest satysfakcjonujący'],
['ko', '처리할 수 없는 요청 범위'],
])],
[tkn('The requested byte range is not available and is out of bounds'), new Map([
['fr', 'Le byte range demandé nest pas disponible et est hors des limites'],
@ -450,6 +485,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求的字节范围不可用,超出边界'],
['id', 'Rentang byte yang diminta tidak tersedia dan di luar batas'],
['pl', 'Żądany zakres bajtów nie jest dostępny i znajduje się poza zakresem'],
['ko', '요청한 범위를 사용할 수 없고, 범위를 벗어났어요'],
])],
[tkn("I'm a teapot"), new Map([
['fr', 'Je suis une théière'],
@ -462,6 +498,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '我是一只茶壶'],
['id', 'Saya adalah teko'],
['pl', 'Jestem czajniczkiem'],
['ko', '저는 찻주전자에요'],
])],
[tkn('Attempt to brew coffee with a teapot is not supported'), new Map([
['fr', 'Tenter de préparer du café avec une théière nest pas pris en charge'],
@ -474,6 +511,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '用茶壶泡咖啡不受支持'],
['id', 'Upaya menyeduh kopi dengan teko tidak didukung'],
['pl', 'Próba zaparzenia kawy za pomocą czajniczka nie jest obsługiwana'],
['ko', '찻주전자로 커피를 내리는 시도는 지원되지 않아요'],
])],
[tkn('Too Many Requests'), new Map([
['fr', 'Trop de requêtes'],
@ -486,6 +524,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求过多'],
['id', 'Terlalu banyak permintaan'],
['pl', 'Zbyt wiele żądań'],
['ko', '요청이 너무 많음'],
])],
[tkn('Too many requests in a given amount of time'), new Map([
['fr', 'Trop de requêtes dans un délai donné'],
@ -498,6 +537,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '在给定的时间内发送了过多请求'],
['id', 'Terlalu banyak permintaan dalam waktu tertentu'],
['pl', 'Zbyt wiele żądań w określonym czasie'],
['ko', '지정된 시간 내에 요청이 너무 많아요'],
])],
[tkn('Internal Server Error'), new Map([
['fr', 'Erreur interne du serveur'],
@ -510,6 +550,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '内部服务器错误'],
['id', 'Kesalahan server internal'],
['pl', 'Wewnętrzny błąd serwera'],
['ko', '내부 서버 오류'],
])],
[tkn('The server met an unexpected condition'), new Map([
['fr', 'Le serveur a rencontré une condition inattendue'],
@ -522,6 +563,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务器遇到了意外情况'],
['id', 'Server mengalami kondisi yang tidak terduga'],
['pl', 'Serwer napotkał nieoczekiwany stan'],
['ko', '서버가 예상치 못한 조건이에요'],
])],
[tkn('Bad Gateway'), new Map([
['fr', 'Mauvaise passerelle'],
@ -534,6 +576,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '无效网关'],
['id', 'Gateway yang buruk'],
['pl', 'Błąd bramki'],
['ko', '게이트웨이 불량'],
])],
[tkn('The server received an invalid response from the upstream server'), new Map([
['fr', 'Le serveur a reçu une réponse invalide du serveur distant'],
@ -546,6 +589,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务器从上游服务器收到了无效的响应'],
['id', 'Server menerima respons yang tidak valid dari server induk'],
['pl', 'Serwer otrzymał nieprawidłową odpowiedź od serwera nadrzędnego'],
['ko', '게이트웨이가 업스트림 서버로부터 잘못된 응답을 받았어요'],
])],
[tkn('Service Unavailable'), new Map([
['fr', 'Service indisponible'],
@ -558,6 +602,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务不可用'],
['id', 'Layanan tidak tersedia'],
['pl', 'Serwis niedostępny'],
['ko', '서비스 불가능'],
])],
[tkn('The server is temporarily overloading or down'), new Map([
['fr', 'Le serveur est temporairement en surcharge ou indisponible'],
@ -570,6 +615,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务器暂时过载或不可用'],
['id', 'Server untuk sementara kelebihan beban atau tidak tersedia'],
['pl', 'Serwer jest tymczasowo przeciążony lub wyłączony'],
['ko', '서버가 일시적으로 과부하 상태이거나 다운되었어요'],
])],
[tkn('Gateway Timeout'), new Map([
['fr', 'Expiration Passerelle'],
@ -582,6 +628,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '网关超时'],
['id', 'Batas waktu gateway'],
['pl', 'Przekroczenie limitu czasu bramki'],
['ko', '게이트웨이 시간초과'],
])],
[tkn('The gateway has timed out'), new Map([
['fr', 'Le temps dattente de la passerelle est dépassé'],
@ -594,6 +641,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '网关响应已经超时'],
['id', 'Sambungan ke server induk telah kedaluwarsa'],
['pl', 'Bramka przekroczyła limit czasu'],
['ko', '게이트웨이 시간이 초과되었어요'],
])],
[tkn('HTTP Version Not Supported'), new Map([
['fr', 'Version HTTP non prise en charge'],
@ -606,6 +654,7 @@ Object.defineProperty(window, 'l10n', {
['zh', 'HTTP版本不受支持'],
['id', 'Versi HTTP tidak didukung'],
['pl', 'Wersja HTTP nie jest obsługiwana'],
['ko', '지원하지 않는 HTTP 버전'],
])],
[tkn('The server does not support the "http protocol" version'), new Map([
['fr', 'Le serveur ne supporte pas la version du protocole HTTP'],
@ -618,6 +667,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务器不支持该HTTP协议版本'],
['id', 'Server tidak mendukung versi HTTP ini'],
['pl', 'Serwer nie obsługuje wersji "protokołu http"'],
['ko', '서버가 해당 "HTTP 프로토콜"을 지원하지 않아요'],
])],
[tkn('Host'), new Map([
['fr', 'Hôte'],
@ -630,6 +680,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '主机'],
['id', 'Host'],
['pl', 'Host'],
['ko', '호스트'],
])],
[tkn('Original URI'), new Map([
['fr', 'URI dorigine'],
@ -642,6 +693,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '原始URI'],
['id', 'URL asli'],
['pl', 'Oryginalny URI'],
['ko', '원시 URI'],
])],
[tkn('Forwarded for'), new Map([
['fr', 'Transmis pour'],
@ -654,6 +706,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '转发自'],
['id', 'Diteruskan untuk'],
['pl', 'Przekazane do'],
['ko', '전달받은 대상'],
])],
[tkn('Namespace'), new Map([
['fr', 'Espace de noms'],
@ -666,6 +719,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '命名空间'],
['id', 'Ruang nama'],
['pl', 'Przestrzeń nazw'],
['ko', '네임스페이스'],
])],
[tkn('Ingress name'), new Map([
['fr', 'Nom ingress'],
@ -678,6 +732,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '入口名'],
['id', 'Nama ingress'],
['pl', 'Nazwa wejścia'],
['ko', '인그레스 이름'],
])],
[tkn('Service name'), new Map([
['fr', 'Nom du service'],
@ -690,6 +745,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务名'],
['id', 'Nama layanan'],
['pl', 'Nazwa usługi'],
['ko', '서비스 이름'],
])],
[tkn('Service port'), new Map([
['fr', 'Port du service'],
@ -702,6 +758,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务端口'],
['id', 'Port layanan'],
['pl', 'Port usługi'],
['ko', '서비스 포트'],
])],
[tkn('Request ID'), new Map([
['fr', 'Identifiant de la requête'],
@ -714,6 +771,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请求ID'],
['id', 'ID permintaan'],
['pl', 'Identyfikator żądania'],
['ko', '요청 ID'],
])],
[tkn('Timestamp'), new Map([
['fr', 'Horodatage'],
@ -726,6 +784,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '时间戳'],
['id', 'Cap waktu'],
['pl', 'Sygnatura czasowa'],
['ko', '시간 기록'],
])],
[tkn('client-side error'), new Map([
['fr', 'Erreur Client'],
@ -738,6 +797,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '客户端错误'],
['id', 'Kesalahan sisi klien'],
['pl', 'błąd po stronie klienta'],
['ko', '클라이언트 측 오류'],
])],
[tkn('server-side error'), new Map([
['fr', 'Erreur Serveur'],
@ -750,6 +810,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '服务端错误'],
['id', 'Kesalahan sisi server'],
['pl', 'błąd po stronie serwera'],
['ko', '서버 측 오류'],
])],
[tkn('Your Client'), new Map([
['fr', 'Votre Client'],
@ -762,6 +823,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '您的客户端'],
['id', 'Klien Anda'],
['pl', 'Klient'],
['ko', '내 클라이언트'],
])],
[tkn('Network'), new Map([
['fr', 'Réseau'],
@ -774,6 +836,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '网络'],
['id', 'Jaringan'],
['pl', 'Sieć'],
['ko', '네트워크'],
])],
[tkn('Web Server'), new Map([
['fr', 'Serveur Web'],
@ -786,6 +849,7 @@ Object.defineProperty(window, 'l10n', {
['zh', 'Web服务器'],
['id', 'Server web'],
['pl', 'Serwer WWW'],
['ko', '웹 서버'],
])],
[tkn('What happened?'), new Map([
['fr', 'Que sest-il passé ?'],
@ -798,6 +862,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '发生了什么?'],
['id', 'Apa yang terjadi?'],
['pl', 'Co się stało?'],
['ko', '어떤 일이 일어났나요?'],
])],
[tkn('What can I do?'), new Map([
['fr', 'Que puis-je faire ?'],
@ -810,6 +875,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '我能做什么?'],
['id', 'Apa yang bisa saya lakukan?'],
['pl', 'Co mogę zrobić?'],
['ko', '어떤 것을 할 수 있나요?'],
])],
[tkn('Please try again in a few minutes'), new Map([
['fr', 'Veuillez réessayer dans quelques minutes'],
@ -822,6 +888,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请在几分钟后重试'],
['id', 'Silakan coba lagi dalam beberapa menit'],
['pl', 'Spróbuj ponownie za kilka minut'],
['ko', '몇 분 후에 다시 시도해 주세요'],
])],
[tkn('Working'), new Map([
['fr', 'Opérationnel'],
@ -834,6 +901,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '正常运行'],
['id', 'Fungsi'],
['pl', 'Działa'],
['ko', '작동 중'],
])],
[tkn('Unknown'), new Map([
['fr', 'Inconnu'],
@ -846,6 +914,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '未知'],
['id', 'Tidak diketahui'],
['pl', 'Nieznany'],
['ko', '알 수 없음'],
])],
[tkn('Please try to change the request method, headers, payload, or URL'), new Map([
['fr', 'Veuillez essayer de changer la méthode de requête, les en-têtes, le contenu ou lURL'],
@ -858,6 +927,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请尝试更改请求方法、标头、有效负载或URL'],
['id', 'Coba lagi dengan metode, header, muatan, atau URL yang berbeda'],
['pl', 'Spróbuj zmienić metodę żądania, nagłówki, żądanie lub adres URL'],
['ko', '요청 방법, 헤더, 콘텐츠 또는 URL을 변경해 보세요'],
])],
[tkn('Please check your authorization data'), new Map([
['fr', 'Veuillez vérifier vos données dautorisation'],
@ -870,6 +940,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请检查您的授权数据'],
['id', 'Memeriksa detail autentikasi'],
['pl', 'Sprawdź swoje dane autoryzacyjne'],
['ko', '인증 데이터를 확인해주세요'],
])],
[tkn('Please double-check the URL and try again'), new Map([
['fr', 'Veuillez vérifier lURL et réessayer'],
@ -882,6 +953,7 @@ Object.defineProperty(window, 'l10n', {
['zh', '请再次检查URL并重试'],
['id', 'Periksa URL dan coba lagi'],
['pl', 'Sprawdź adres URL i spróbuj ponownie'],
['ko', 'URL을 다시 한번 확인해 주세요'],
])],
]));

View File

@ -118,8 +118,8 @@
const $langSwitch = document.getElementById('lang-switch');
['fr', 'ru', 'uk', 'pt', 'nl', 'de', 'es', 'zh', 'id', 'pl'].forEach((lang) => {
// ^^^ add your newly added locale here
['fr', 'ru', 'uk', 'pt', 'nl', 'de', 'es', 'zh', 'id', 'pl', 'ko' ].forEach((lang) => {
// ^^^ add your newly added locale here
const $li = document.createElement('li');
const $btn = document.createElement('button');

View File

@ -28,3 +28,4 @@ different locales, please follow these steps:
- 🇨🇳 Chinese by [@CDN18](https://github.com/CDN18)
- 🇮🇩 Indonesian by [@getwisp](https://github.com/getwisp)
- 🇵🇱 Polish by [@wielorzeczownik](https://github.com/wielorzeczownik)
- 🇰🇷 Korean by [@NavyStack](https://github.com/NavyStack)