diff --git a/CHANGELOG.md b/CHANGELOG.md index 734f6f7..8af4857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,16 @@ The format is based on [Keep a Changelog][keepachangelog] and this project adher ## UNRELEASED +### Added + +- Possibility to disable error pages auto-localization (using `--disable-l10n` flag for the `serve` & `build` commands or environment variable `DISABLE_L10N`) [#91] + ### Fixed - User UID/GID changed to the numeric values in the dockerfile [#92] [#92]:https://github.com/tarampampam/error-pages/issues/92 +[#91]:https://github.com/tarampampam/error-pages/issues/91 ## v2.12.1 diff --git a/Dockerfile b/Dockerfile index c62b6dd..6c3ce3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,7 +68,8 @@ ENV LISTEN_PORT="8080" \ TEMPLATE_NAME="ghost" \ DEFAULT_ERROR_PAGE="404" \ DEFAULT_HTTP_CODE="404" \ - SHOW_DETAILS="false" + SHOW_DETAILS="false" \ + DISABLE_L10N="false" # Docs: HEALTHCHECK --interval=7s --timeout=2s CMD ["/bin/error-pages", "healthcheck", "--log-json"] diff --git a/internal/cli/build/command.go b/internal/cli/build/command.go index 1873171..4413cbc 100644 --- a/internal/cli/build/command.go +++ b/internal/cli/build/command.go @@ -15,6 +15,7 @@ import ( func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { var ( generateIndex bool + disableL10n bool cfg *config.Config ) @@ -39,7 +40,7 @@ func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { return errors.New("wrong arguments count") } - return run(log, cfg, args[0], generateIndex) + return run(log, cfg, args[0], generateIndex, disableL10n) }, } @@ -50,6 +51,13 @@ func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { "generate index page", ) + cmd.Flags().BoolVarP( + &disableL10n, + "disable-l10n", "", + false, + "disable error pages localization", + ) + return cmd } @@ -60,7 +68,7 @@ const ( outDirPerm = os.FileMode(0775) ) -func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex bool) error { //nolint:funlen +func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex, disableL10n bool) error { //nolint:funlen,lll if len(cfg.Templates) == 0 { return errors.New("no loaded templates") } @@ -92,6 +100,7 @@ func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateI Message: page.Message(), Description: page.Description(), ShowRequestDetails: false, + L10nDisabled: disableL10n, }) if renderingErr != nil { return renderingErr diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go index 2e84b16..686bb31 100644 --- a/internal/cli/serve/command.go +++ b/internal/cli/serve/command.go @@ -30,7 +30,7 @@ func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra return errors.New("path to the config file is required for this command") } - if err = f.overrideUsingEnv(cmd.Flags()); err != nil { + if err = f.OverrideUsingEnv(cmd.Flags()); err != nil { return err } @@ -38,18 +38,18 @@ func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra return err } - return f.validate() + return f.Validate() }, - RunE: func(*cobra.Command, []string) error { return run(ctx, log, f, cfg) }, + RunE: func(*cobra.Command, []string) error { return run(ctx, log, cfg, f) }, } - f.init(cmd.Flags()) + f.Init(cmd.Flags()) return cmd } // run current command. -func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) error { //nolint:funlen +func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags) error { //nolint:funlen var ( ctx, cancel = context.WithCancel(parentCtx) // serve context creation oss = breaker.NewOSSignals(ctx) // OS signals listener @@ -70,9 +70,11 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config var ( templateNames = cfg.TemplateNames() picker interface{ Pick() string } + + opt = f.ToOptions() ) - switch f.template.name { + switch opt.Template.Name { case useRandomTemplate: log.Info("A random template will be used") @@ -99,28 +101,19 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config picker = pick.NewStringsSlice(templateNames, pick.First) default: - if t, found := cfg.Template(f.template.name); found { + if t, found := cfg.Template(opt.Template.Name); found { log.Info("We will use the requested template", zap.String("name", t.Name())) picker = pick.NewStringsSlice([]string{t.Name()}, pick.First) } else { - return errors.New("requested nonexistent template: " + f.template.name) + return errors.New("requested nonexistent template: " + opt.Template.Name) } } - var proxyHTTPHeaders = f.HeadersToProxy() - // create HTTP server server := appHttp.NewServer(log) // register server routes, middlewares, etc. - if err := server.Register( - cfg, - picker, - f.defaultErrorPage, - f.defaultHTTPCode, - f.showDetails, - proxyHTTPHeaders, - ); err != nil { + if err := server.Register(cfg, picker, opt); err != nil { return err } @@ -131,15 +124,16 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config defer close(errCh) log.Info("Server starting", - zap.String("addr", f.listen.ip), - zap.Uint16("port", f.listen.port), - zap.String("default error page", f.defaultErrorPage), - zap.Uint16("default HTTP response code", f.defaultHTTPCode), - zap.Strings("proxy headers", proxyHTTPHeaders), - zap.Bool("show request details", f.showDetails), + zap.String("addr", f.Listen.IP), + zap.Uint16("port", f.Listen.Port), + zap.String("default error page", opt.Default.PageCode), + zap.Uint16("default HTTP response code", opt.Default.HTTPCode), + zap.Strings("proxy headers", opt.ProxyHTTPHeaders), + zap.Bool("show request details", opt.ShowDetails), + zap.Bool("localization disabled", opt.L10n.Disabled), ) - if err := server.Start(f.listen.ip, f.listen.port); err != nil { + if err := server.Start(f.Listen.IP, f.Listen.Port); err != nil { errCh <- err } }(startingErrCh) diff --git a/internal/cli/serve/flags.go b/internal/cli/serve/flags.go index bb40450..457a006 100644 --- a/internal/cli/serve/flags.go +++ b/internal/cli/serve/flags.go @@ -9,60 +9,26 @@ import ( "github.com/spf13/pflag" "github.com/tarampampam/error-pages/internal/env" + "github.com/tarampampam/error-pages/internal/options" ) type flags struct { - listen struct { - ip string - port uint16 + Listen struct { + IP string + Port uint16 } template struct { name string } + l10n struct { + disabled bool + } defaultErrorPage string defaultHTTPCode uint16 showDetails bool proxyHTTPHeaders string // comma-separated } -// HeadersToProxy converts a comma-separated string with headers list into strings slice (with a sorting and without -// duplicates). -func (f *flags) HeadersToProxy() []string { - var raw = strings.Split(f.proxyHTTPHeaders, ",") - - if len(raw) == 0 { - return []string{} - } else if len(raw) == 1 { - if h := strings.TrimSpace(raw[0]); h != "" { - return []string{h} - } else { - return []string{} - } - } - - var m = make(map[string]struct{}, len(raw)) - - // make unique and ignore empty strings - for _, h := range raw { - if h = strings.TrimSpace(h); h != "" { - if _, ok := m[h]; !ok { - m[h] = struct{}{} - } - } - } - - // convert map into slice - var headers = make([]string, 0, len(m)) - for h := range m { - headers = append(headers, h) - } - - // make sort - sort.Strings(headers) - - return headers -} - const ( listenFlagName = "listen" portFlagName = "port" @@ -71,6 +37,7 @@ const ( defaultHTTPCodeFlagName = "default-http-code" showDetailsFlagName = "show-details" proxyHTTPHeadersFlagName = "proxy-headers" + disableL10nFlagName = "disable-l10n" ) const ( @@ -80,18 +47,18 @@ const ( useRandomTemplateHourly = "random-hourly" ) -func (f *flags) init(flagSet *pflag.FlagSet) { +func (f *flags) Init(flagSet *pflag.FlagSet) { flagSet.StringVarP( - &f.listen.ip, + &f.Listen.IP, listenFlagName, "l", "0.0.0.0", - fmt.Sprintf("IP address to listen on [$%s]", env.ListenAddr), + fmt.Sprintf("IP address to Listen on [$%s]", env.ListenAddr), ) flagSet.Uint16VarP( - &f.listen.port, + &f.Listen.Port, portFlagName, "p", 8080, //nolint:gomnd // must be same as default healthcheck `--port` flag value - fmt.Sprintf("TCP port number [$%s]", env.ListenPort), + fmt.Sprintf("TCP prt number [$%s]", env.ListenPort), ) flagSet.StringVarP( &f.template.name, @@ -131,22 +98,28 @@ func (f *flags) init(flagSet *pflag.FlagSet) { "", fmt.Sprintf("proxy HTTP request headers list (comma-separated) [$%s]", env.ProxyHTTPHeaders), ) + flagSet.BoolVarP( + &f.l10n.disabled, + disableL10nFlagName, "", + false, + fmt.Sprintf("disable error pages localization [$%s]", env.DisableL10n), + ) } -func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit,gocyclo +func (f *flags) OverrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit,gocyclo flagSet.VisitAll(func(flag *pflag.Flag) { // flag was NOT defined using CLI (flags should have maximal priority) if !flag.Changed { //nolint:nestif switch flag.Name { case listenFlagName: if envVar, exists := env.ListenAddr.Lookup(); exists { - f.listen.ip = strings.TrimSpace(envVar) + f.Listen.IP = strings.TrimSpace(envVar) } case portFlagName: if envVar, exists := env.ListenPort.Lookup(); exists { if p, err := strconv.ParseUint(envVar, 10, 16); err == nil { //nolint:gomnd - f.listen.port = uint16(p) + f.Listen.Port = uint16(p) } else { lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar) } @@ -182,6 +155,13 @@ func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nol if envVar, exists := env.ProxyHTTPHeaders.Lookup(); exists { f.proxyHTTPHeaders = strings.TrimSpace(envVar) } + + case disableL10nFlagName: + if envVar, exists := env.DisableL10n.Lookup(); exists { + if b, err := strconv.ParseBool(envVar); err == nil { + f.l10n.disabled = b + } + } } } }) @@ -189,9 +169,9 @@ func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nol return lastErr } -func (f *flags) validate() error { - if net.ParseIP(f.listen.ip) == nil { - return fmt.Errorf("wrong IP address [%s] for listening", f.listen.ip) +func (f *flags) Validate() error { + if net.ParseIP(f.Listen.IP) == nil { + return fmt.Errorf("wrong IP address [%s] for listening", f.Listen.IP) } if f.defaultHTTPCode > 599 { //nolint:gomnd @@ -204,3 +184,52 @@ func (f *flags) validate() error { return nil } + +// headersToProxy converts a comma-separated string with headers list into strings slice (with a sorting and without +// duplicates). +func (f *flags) headersToProxy() []string { + var raw = strings.Split(f.proxyHTTPHeaders, ",") + + if len(raw) == 0 { + return []string{} + } else if len(raw) == 1 { + if h := strings.TrimSpace(raw[0]); h != "" { + return []string{h} + } else { + return []string{} + } + } + + var m = make(map[string]struct{}, len(raw)) + + // make unique and ignore empty strings + for _, h := range raw { + if h = strings.TrimSpace(h); h != "" { + if _, ok := m[h]; !ok { + m[h] = struct{}{} + } + } + } + + // convert map into slice + var headers = make([]string, 0, len(m)) + for h := range m { + headers = append(headers, h) + } + + // make sort + sort.Strings(headers) + + return headers +} + +func (f *flags) ToOptions() (o options.ErrorPage) { + o.Default.PageCode = f.defaultErrorPage + o.Default.HTTPCode = f.defaultHTTPCode + o.L10n.Disabled = f.l10n.disabled + o.Template.Name = f.template.name + o.ShowDetails = f.showDetails + o.ProxyHTTPHeaders = f.headersToProxy() + + return o +} diff --git a/internal/env/env.go b/internal/env/env.go index e2c3dae..c1ee43b 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -14,6 +14,7 @@ const ( DefaultHTTPCode envVariable = "DEFAULT_HTTP_CODE" // default HTTP response code ShowDetails envVariable = "SHOW_DETAILS" // show request details in response ProxyHTTPHeaders envVariable = "PROXY_HTTP_HEADERS" // proxy HTTP request headers list (request -> response) + DisableL10n envVariable = "DISABLE_L10N" // disable pages localization ) // String returns environment variable name in the string representation. diff --git a/internal/env/env_test.go b/internal/env/env_test.go index 9ba607f..3032e33 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -16,6 +16,7 @@ func TestConstants(t *testing.T) { assert.Equal(t, "DEFAULT_HTTP_CODE", string(DefaultHTTPCode)) assert.Equal(t, "SHOW_DETAILS", string(ShowDetails)) assert.Equal(t, "PROXY_HTTP_HEADERS", string(ProxyHTTPHeaders)) + assert.Equal(t, "DISABLE_L10N", string(DisableL10n)) } func TestEnvVariable_Lookup(t *testing.T) { @@ -30,6 +31,7 @@ func TestEnvVariable_Lookup(t *testing.T) { {giveEnv: DefaultHTTPCode}, {giveEnv: ShowDetails}, {giveEnv: ProxyHTTPHeaders}, + {giveEnv: DisableL10n}, } for _, tt := range cases { diff --git a/internal/http/core/errorpage.go b/internal/http/core/errorpage.go index e9640c9..7337681 100644 --- a/internal/http/core/errorpage.go +++ b/internal/http/core/errorpage.go @@ -4,6 +4,7 @@ import ( "strconv" "github.com/tarampampam/error-pages/internal/config" + "github.com/tarampampam/error-pages/internal/options" "github.com/tarampampam/error-pages/internal/tpl" "github.com/valyala/fasthttp" ) @@ -24,8 +25,7 @@ func RespondWithErrorPage( //nolint:funlen,gocyclo rdr renderer, pageCode string, httpCode int, - showRequestDetails bool, - proxyHeaders []string, + opt options.ErrorPage, ) { ctx.Response.Header.Set("X-Robots-Tag", "noindex") // block Search indexing @@ -33,10 +33,14 @@ func RespondWithErrorPage( //nolint:funlen,gocyclo clientWant = ClientWantFormat(ctx) json, canJSON = cfg.JSONFormat() xml, canXML = cfg.XMLFormat() - props = tpl.Properties{Code: pageCode, ShowRequestDetails: showRequestDetails} + props = tpl.Properties{ + Code: pageCode, + ShowRequestDetails: opt.ShowDetails, + L10nDisabled: opt.L10n.Disabled, + } ) - if showRequestDetails { + if opt.ShowDetails { props.OriginalURI = string(ctx.Request.Header.Peek(OriginalURI)) props.Namespace = string(ctx.Request.Header.Peek(Namespace)) props.IngressName = string(ctx.Request.Header.Peek(IngressName)) @@ -66,7 +70,7 @@ func RespondWithErrorPage( //nolint:funlen,gocyclo } // proxy required HTTP headers from the request to the response - for _, headerToProxy := range proxyHeaders { + for _, headerToProxy := range opt.ProxyHTTPHeaders { if reqHeader := ctx.Request.Header.Peek(headerToProxy); len(reqHeader) > 0 { ctx.Response.Header.SetBytesV(headerToProxy, reqHeader) } diff --git a/internal/http/handlers/errorpage/handler.go b/internal/http/handlers/errorpage/handler.go index d9287e9..f826014 100644 --- a/internal/http/handlers/errorpage/handler.go +++ b/internal/http/handlers/errorpage/handler.go @@ -3,6 +3,7 @@ package errorpage import ( "github.com/tarampampam/error-pages/internal/config" "github.com/tarampampam/error-pages/internal/http/core" + "github.com/tarampampam/error-pages/internal/options" "github.com/tarampampam/error-pages/internal/tpl" "github.com/valyala/fasthttp" ) @@ -19,18 +20,12 @@ type ( ) // NewHandler creates handler for error pages serving. -func NewHandler( - cfg *config.Config, - p templatePicker, - rdr renderer, - showRequestDetails bool, - proxyHTTPHeaders []string, -) fasthttp.RequestHandler { +func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { core.SetClientFormat(ctx, core.PlainTextContentType) // default content type if code, ok := ctx.UserValue("code").(string); ok { - core.RespondWithErrorPage(ctx, cfg, p, rdr, code, fasthttp.StatusOK, showRequestDetails, proxyHTTPHeaders) + core.RespondWithErrorPage(ctx, cfg, p, rdr, code, fasthttp.StatusOK, opt) } else { // will never occur ctx.SetStatusCode(fasthttp.StatusInternalServerError) _, _ = ctx.WriteString("cannot extract requested code from the request") diff --git a/internal/http/handlers/index/handler.go b/internal/http/handlers/index/handler.go index 2844537..282b5e2 100644 --- a/internal/http/handlers/index/handler.go +++ b/internal/http/handlers/index/handler.go @@ -5,6 +5,7 @@ import ( "github.com/tarampampam/error-pages/internal/config" "github.com/tarampampam/error-pages/internal/http/core" + "github.com/tarampampam/error-pages/internal/options" "github.com/tarampampam/error-pages/internal/tpl" "github.com/valyala/fasthttp" ) @@ -21,23 +22,15 @@ type ( ) // NewHandler creates handler for the index page serving. -func NewHandler( - cfg *config.Config, - p templatePicker, - rdr renderer, - defaultPageCode string, - defaultHTTPCode uint16, - showRequestDetails bool, - proxyHTTPHeaders []string, -) fasthttp.RequestHandler { +func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { - pageCode, httpCode := defaultPageCode, int(defaultHTTPCode) + pageCode, httpCode := opt.Default.PageCode, int(opt.Default.HTTPCode) if returnCode, ok := extractCodeToReturn(ctx); ok { pageCode, httpCode = strconv.Itoa(returnCode), returnCode } - core.RespondWithErrorPage(ctx, cfg, p, rdr, pageCode, httpCode, showRequestDetails, proxyHTTPHeaders) + core.RespondWithErrorPage(ctx, cfg, p, rdr, pageCode, httpCode, opt) } } diff --git a/internal/http/server.go b/internal/http/server.go index e7f90a2..90a8ec9 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -15,6 +15,7 @@ import ( notfoundHandler "github.com/tarampampam/error-pages/internal/http/handlers/notfound" versionHandler "github.com/tarampampam/error-pages/internal/http/handlers/version" "github.com/tarampampam/error-pages/internal/metrics" + "github.com/tarampampam/error-pages/internal/options" "github.com/tarampampam/error-pages/internal/tpl" "github.com/tarampampam/error-pages/internal/version" "github.com/valyala/fasthttp" @@ -66,14 +67,7 @@ type templatePicker interface { // Register server routes, middlewares, etc. // Router docs: -func (s *Server) Register( - cfg *config.Config, - templatePicker templatePicker, - defaultPageCode string, - defaultHTTPCode uint16, - showDetails bool, - proxyHTTPHeaders []string, -) error { +func (s *Server) Register(cfg *config.Config, templatePicker templatePicker, opt options.ErrorPage) error { reg, m := metrics.NewRegistry(), metrics.NewMetrics() if err := m.Register(reg); err != nil { @@ -82,8 +76,9 @@ func (s *Server) Register( s.fast.Handler = common.DurationMetrics(common.LogRequest(s.router.Handler, s.log), &m) - s.router.GET("/", indexHandler.NewHandler(cfg, templatePicker, s.rdr, defaultPageCode, defaultHTTPCode, showDetails, proxyHTTPHeaders)) //nolint:lll - s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, showDetails, proxyHTTPHeaders)) //nolint:lll + s.router.GET("/", indexHandler.NewHandler(cfg, templatePicker, s.rdr, opt)) + s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, opt)) + s.router.GET("/version", versionHandler.NewHandler(version.Version())) liveHandler := healthzHandler.NewHandler(checkers.NewLiveChecker()) diff --git a/internal/options/errorpage.go b/internal/options/errorpage.go new file mode 100644 index 0000000..873c012 --- /dev/null +++ b/internal/options/errorpage.go @@ -0,0 +1,16 @@ +package options + +type ErrorPage struct { + Default struct { + PageCode string // default error page code + HTTPCode uint16 // default HTTP response code + } + L10n struct { + Disabled bool // disable error pages localization + } + Template struct { + Name string // template name + } + ShowDetails bool // show request details in response + ProxyHTTPHeaders []string // proxy HTTP request headers list +} diff --git a/internal/tpl/properties.go b/internal/tpl/properties.go index b1fd543..caae3b4 100644 --- a/internal/tpl/properties.go +++ b/internal/tpl/properties.go @@ -16,6 +16,7 @@ type Properties struct { // only string properties with a "token" tag, please RequestID string `token:"request_id"` ForwardedFor string `token:"forwarded_for"` Host string `token:"host"` + L10nDisabled bool ShowRequestDetails bool } diff --git a/internal/tpl/render.go b/internal/tpl/render.go index 821cb6d..820186d 100644 --- a/internal/tpl/render.go +++ b/internal/tpl/render.go @@ -140,8 +140,10 @@ func (tr *TemplateRenderer) Render(content []byte, props Properties) ([]byte, er } var funcMap = template.FuncMap{ - "show_details": func() bool { return props.ShowRequestDetails }, - "hide_details": func() bool { return !props.ShowRequestDetails }, + "show_details": func() bool { return props.ShowRequestDetails }, + "hide_details": func() bool { return !props.ShowRequestDetails }, + "l10n_disabled": func() bool { return props.L10nDisabled }, + "l10n_enabled": func() bool { return !props.L10nDisabled }, } // make a copy of template functions map diff --git a/internal/tpl/render_test.go b/internal/tpl/render_test.go index 1062a9f..9ddf44a 100644 --- a/internal/tpl/render_test.go +++ b/internal/tpl/render_test.go @@ -56,6 +56,17 @@ func Test_Render(t *testing.T) { giveProps: tpl.Properties{Code: "201", Message: "lorem ipsum"}, wantContent: `{"code": "201", "message": {"here":[ " Yeah " ]}}`, }, + + "fn l10n_enabled": { + giveContent: "{{ if l10n_enabled }}Y{{ else }}N{{ end }}", + giveProps: tpl.Properties{L10nDisabled: true}, + wantContent: "N", + }, + "fn l10n_disabled": { + giveContent: "{{ if l10n_disabled }}Y{{ else }}N{{ end }}", + giveProps: tpl.Properties{L10nDisabled: true}, + wantContent: "Y", + }, } { t.Run(name, func(t *testing.T) { content, err := renderer.Render([]byte(tt.giveContent), tt.giveProps) diff --git a/templates/app-down.html b/templates/app-down.html index dc89feb..010f19c 100644 --- a/templates/app-down.html +++ b/templates/app-down.html @@ -225,6 +225,7 @@ } }); + // {{ 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'; @@ -233,6 +234,7 @@ p.appendChild(s); })(document.createElement('script'), document.body); } + // {{ end }}