From 703b0780f8b0725c0a583728615cef831cbc8a33 Mon Sep 17 00:00:00 2001
From: Zack Puhl
Date: Wed, 24 Jul 2024 20:10:17 -0400
Subject: [PATCH 01/42] i18n WIP
---
.gitignore | 1 +
actions/ConnectivityAction.php | 6 +-
actions/DetectAction.php | 6 +-
actions/DisplayAction.php | 12 +--
actions/FindfeedAction.php | 8 +-
actions/FrontpageAction.php | 4 +-
actions/HealthAction.php | 2 +-
actions/ListAction.php | 4 +-
config.default.ini.php | 7 ++
index.php | 2 +-
lib/BridgeAbstract.php | 15 ++--
lib/BridgeCard.php | 22 +++--
lib/BridgeFactory.php | 4 +-
lib/CacheFactory.php | 32 +++----
lib/FeedExpander.php | 6 +-
lib/FeedParser.php | 4 +-
lib/FormatFactory.php | 4 +-
lib/bootstrap.php | 18 ++++
lib/contents.php | 2 +-
lib/url.php | 8 +-
templates/bridge-error.html.php | 8 +-
templates/connectivity.html.php | 2 +-
templates/exception.html.php | 155 +++++++++++++-------------------
templates/frontpage.html.php | 12 +--
templates/html-format.html.php | 12 +--
templates/token.html.php | 13 +--
26 files changed, 184 insertions(+), 185 deletions(-)
diff --git a/.gitignore b/.gitignore
index 6ed95489e41..b28c5e95d40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -232,6 +232,7 @@ config/*
!config/nginx.conf
!config/php-fpm.conf
!config/php.ini
+docker-compose.*
######################
## VisualStudioCode ##
diff --git a/actions/ConnectivityAction.php b/actions/ConnectivityAction.php
index 09d9c6c68ed..78f4636351a 100644
--- a/actions/ConnectivityAction.php
+++ b/actions/ConnectivityAction.php
@@ -22,7 +22,7 @@ public function __construct()
public function execute(Request $request)
{
if (!Debug::isEnabled()) {
- return new Response('This action is only available in debug mode!', 403);
+ return new Response(xlat('errors:actions:display:debug_required'), 403);
}
$bridgeName = $request->get('bridge');
@@ -31,7 +31,7 @@ public function execute(Request $request)
}
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName);
if (!$bridgeClassName) {
- return new Response('Bridge not found', 404);
+ return new Response(xlat('errors:general:not_found'), 404);
}
return $this->reportBridgeConnectivity($bridgeClassName);
}
@@ -39,7 +39,7 @@ public function execute(Request $request)
private function reportBridgeConnectivity($bridgeClassName)
{
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
- throw new \Exception('Bridge is not whitelisted!');
+ throw new \Exception(xlat('errors:general:whitelist'));
}
$bridge = $this->bridgeFactory->create($bridgeClassName);
diff --git a/actions/DetectAction.php b/actions/DetectAction.php
index 0c61f1b60d1..82fc46e3119 100644
--- a/actions/DetectAction.php
+++ b/actions/DetectAction.php
@@ -8,10 +8,10 @@ public function execute(Request $request)
$format = $request->get('format');
if (!$url) {
- return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a url']));
+ return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => xlat('errors:general:specify_url')]));
}
if (!$format) {
- return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format']));
+ return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => xlat('errors:general:specify_format')]));
}
$bridgeFactory = new BridgeFactory();
@@ -39,7 +39,7 @@ public function execute(Request $request)
}
return new Response(render(__DIR__ . '/../templates/error.html.php', [
- 'message' => 'No bridge found for given URL: ' . $url,
+ 'message' => xlat('errors:general:not_found_for_url') . ': ' . $url,
]));
}
}
diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php
index 93813004f22..4550f2560fa 100644
--- a/actions/DisplayAction.php
+++ b/actions/DisplayAction.php
@@ -32,23 +32,23 @@ public function execute(Request $request)
return new Response('', 304, ['last-modified' => $modificationTimeGMT . 'GMT']);
}
}
- return $cachedResponse->withHeader('rss-bridge', 'This is a cached response');
+ return $cachedResponse->withHeader('rss-bridge', xlat('errors:actions:display:cached'));
}
if (!$bridgeName) {
- return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Missing bridge parameter']), 400);
+ return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => xlat('errors:general:missing_parameter')]), 400);
}
$bridgeFactory = new BridgeFactory();
$bridgeClassName = $bridgeFactory->createBridgeClassName($bridgeName);
if (!$bridgeClassName) {
- return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Bridge not found']), 404);
+ return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => xlat('errors:general:not_found')]), 404);
}
if (!$format) {
- return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format']), 400);
+ return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => xlat('errors:general:format')]), 400);
}
if (!$bridgeFactory->isEnabled($bridgeClassName)) {
- return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'This bridge is not whitelisted']), 400);
+ return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => xlat('errors:general:whitelist')]), 400);
}
if (
@@ -174,7 +174,7 @@ private function createFeedItemFromException($e, BridgeAbstract $bridge): FeedIt
// Create a unique identifier every 24 hours
$uniqueIdentifier = urlencode((int)(time() / 86400));
- $title = sprintf('Bridge returned error %s! (%s)', $e->getCode(), $uniqueIdentifier);
+ $title = sprintf('%s %s! (%s)', xlat('errors:actions:display:error'), $e->getCode(), $uniqueIdentifier);
$item->setTitle($title);
$item->setURI(get_current_url());
$item->setTimestamp(time());
diff --git a/actions/FindfeedAction.php b/actions/FindfeedAction.php
index 94dc6b72b58..aa0fbbd04dc 100644
--- a/actions/FindfeedAction.php
+++ b/actions/FindfeedAction.php
@@ -13,10 +13,10 @@ public function execute(Request $request)
$format = $request->get('format');
if (!$url) {
- return new Response('You must specify a url', 400);
+ return new Response(xlat('errors:general:specify_url'), 400);
}
if (!$format) {
- return new Response('You must specify a format', 400);
+ return new Response(xlat('errors:general:specify_format'), 400);
}
$bridgeFactory = new BridgeFactory();
@@ -69,7 +69,7 @@ public function execute(Request $request)
$results[] = $content;
}
if ($results === []) {
- return new Response(Json::encode(['message' => 'No bridge found for given url']), 404, ['content-type' => 'application/json']);
+ return new Response(Json::encode(['message' => xlat('errors:general:not_found_for_url')]), 404, ['content-type' => 'application/json']);
}
return new Response(Json::encode($results), 200, ['content-type' => 'application/json']);
}
@@ -82,7 +82,7 @@ private function getParameterName($bridge, $context, $key)
} else if (isset($bridge::PARAMETERS['global'][$key]['name'])) {
$name = $bridge::PARAMETERS['global'][$key]['name'];
} else {
- $name = 'Variable "' . $key . '" (No name provided)';
+ $name = xlat('errors:actions:findfeed:no_name_var', $key);
}
return $name;
}
diff --git a/actions/FrontpageAction.php b/actions/FrontpageAction.php
index 32795c45de0..c4f92b77153 100644
--- a/actions/FrontpageAction.php
+++ b/actions/FrontpageAction.php
@@ -12,8 +12,8 @@ public function execute(Request $request)
foreach ($bridgeFactory->getMissingEnabledBridges() as $missingEnabledBridge) {
$messages[] = [
- 'body' => sprintf('Warning : Bridge "%s" not found', $missingEnabledBridge),
- 'level' => 'warning'
+ 'body' => xlat('errors:general:not_found_named', $missingEnabledBridge),
+ 'level' => 'warning',
];
}
diff --git a/actions/HealthAction.php b/actions/HealthAction.php
index a38879c2885..b9ed922d572 100644
--- a/actions/HealthAction.php
+++ b/actions/HealthAction.php
@@ -8,7 +8,7 @@ public function execute(Request $request)
{
$response = [
'code' => 200,
- 'message' => 'all is good',
+ 'message' => xlat('misc:all_is_good'),
];
return new Response(Json::encode($response), 200, ['content-type' => 'application/json']);
}
diff --git a/actions/ListAction.php b/actions/ListAction.php
index 3d9cdd738f4..66434e4f54a 100644
--- a/actions/ListAction.php
+++ b/actions/ListAction.php
@@ -14,7 +14,9 @@ public function execute(Request $request)
$bridge = $bridgeFactory->create($bridgeClassName);
$list->bridges[$bridgeClassName] = [
- 'status' => $bridgeFactory->isEnabled($bridgeClassName) ? 'active' : 'inactive',
+ 'status' => $bridgeFactory->isEnabled($bridgeClassName)
+ ? xlat('misc:active')
+ : xlat('misc:inactive'),
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'name' => $bridge->getName(),
diff --git a/config.default.ini.php b/config.default.ini.php
index 8f7de832120..3da3d64da85 100644
--- a/config.default.ini.php
+++ b/config.default.ini.php
@@ -45,6 +45,9 @@
; Whether to enable maintenance mode. If enabled, feed requests receive 503 Service Unavailable
enable_maintenance_mode = false
+; The default language to use for the application's web UI (locale and region; e.g. 'en-US').
+app_language = en-US
+
[http]
; Operation timeout in seconds
timeout = 15
@@ -58,6 +61,10 @@
; Max http response size in MB
max_filesize = 20
+; The default language to use in requests (locale and region; e.g. 'en-US').
+; Some bridges might manually override this setting to do things like bypass CloudFlare.
+accept_language = en-US
+
[cache]
; Cache type: file, sqlite, memcached, array, null
diff --git a/index.php b/index.php
index 7144ae27528..141b9214398 100644
--- a/index.php
+++ b/index.php
@@ -6,7 +6,7 @@
exit;
}
-if (! is_readable(__DIR__ . '/lib/bootstrap.php')) {
+if (!is_readable(__DIR__ . '/lib/bootstrap.php')) {
http_response_code(500);
print 'Unable to read lib/bootstrap.php. Check file permissions.';
exit;
diff --git a/lib/BridgeAbstract.php b/lib/BridgeAbstract.php
index 2467dec60e1..4dabcd9389c 100644
--- a/lib/BridgeAbstract.php
+++ b/lib/BridgeAbstract.php
@@ -131,7 +131,7 @@ public function loadConfiguration()
}
if (isset($optionValue['required']) && $optionValue['required'] === true) {
- throw new \Exception(sprintf('Missing configuration option: %s', $optionName));
+ throw new \Exception(xlat('errors:general:missing_config_option', $optionName));
} elseif (isset($optionValue['defaultValue'])) {
$this->configuration[$optionName] = $optionValue['defaultValue'];
}
@@ -152,7 +152,7 @@ public function setInput(array $input)
if (!$contexts) {
if ($input) {
- throw new \Exception('Invalid parameters value(s)');
+ throw new \Exception(xlat('errors:general:invalid_context'));
}
return;
}
@@ -163,7 +163,12 @@ public function setInput(array $input)
$errors = $validator->validateInput($input, $contexts);
if ($errors !== []) {
$invalidParameterKeys = array_column($errors, 'name');
- throw new \Exception(sprintf('Invalid parameters value(s): %s', implode(', ', $invalidParameterKeys)));
+ throw new \Exception(
+ xlat(
+ 'errors:general:invalid_context_args',
+ implode(', ', $invalidParameterKeys)
+ )
+ );
}
// Guess the context from input data
@@ -173,9 +178,9 @@ public function setInput(array $input)
}
if (is_null($this->queriedContext)) {
- throw new \Exception('Required parameter(s) missing');
+ throw new \Exception(xlat('errors:general:missing_context'));
} elseif ($this->queriedContext === false) {
- throw new \Exception('Mixed context parameters');
+ throw new \Exception(xlat('errors:general:mixed_context'));
}
$this->setInputWithContext($input, $this->queriedContext);
diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php
index d15ac865e0a..2af379628d3 100644
--- a/lib/BridgeCard.php
+++ b/lib/BridgeCard.php
@@ -16,20 +16,22 @@ public static function render(string $bridgeClassName, Request $request): string
if (Configuration::getConfig('proxy', 'url') && Configuration::getConfig('proxy', 'by_bridge')) {
$contexts['global']['_noproxy'] = [
- 'name' => 'Disable proxy (' . (Configuration::getConfig('proxy', 'name') ?: Configuration::getConfig('proxy', 'url')) . ')',
+ 'name' => xlat('bridge_card:proxy_disable') . ' ('
+ . (Configuration::getConfig('proxy', 'name') ?: Configuration::getConfig('proxy', 'url')) . ')',
'type' => 'checkbox'
];
}
if (Configuration::getConfig('cache', 'custom_timeout')) {
$contexts['global']['_cache_timeout'] = [
- 'name' => 'Cache timeout in seconds',
+ 'name' => xlat('bridge_card:cache_timeout'),
'type' => 'number',
'defaultValue' => $bridge->getCacheTimeout()
];
}
$shortName = $bridge->getShortName();
+ $showMore = ucfirst(xlat('misc:show_more')) ?: 'Show more';
$card = <<{$description}
-
+
CARD;
@@ -76,13 +78,15 @@ class="bridge-card"
}
}
- $card .= sprintf('', $bridgeClassName);
+ $showLess = ucfirst(xlat('misc:show_less')) ?: 'Show less';
+ $card .= sprintf('', $bridgeClassName, $showLess);
if (Configuration::getConfig('admin', 'donations') && $bridge->getDonationURI()) {
$card .= sprintf(
- '%s ~ Donate
',
+ '%s ~ %s
',
$bridge->getMaintainer(),
- $bridge->getDonationURI()
+ $bridge->getDonationURI(),
+ xlat('misc:donate') ?: 'Donate'
);
} else {
$card .= sprintf('%s
', $bridge->getMaintainer());
@@ -151,7 +155,8 @@ private static function renderForm(
$infoText[] = filter_var($inputEntry['title'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
}
if ($inputEntry['exampleValue'] !== '') {
- $infoText[] = "Example (right click to use):\n" . filter_var($inputEntry['exampleValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
+ $infoText[] = xlat('bridge_card:example_right_click') . ":\n"
+ . filter_var($inputEntry['exampleValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$infoTextScript = 'rssbridge_use_placeholder_value(this);';
}
@@ -165,7 +170,8 @@ private static function renderForm(
$form .= '';
}
- $form .= '';
+ $form .= '';
return $form . '' . PHP_EOL;
}
diff --git a/lib/BridgeFactory.php b/lib/BridgeFactory.php
index ad4332875de..b5ccfc4d468 100644
--- a/lib/BridgeFactory.php
+++ b/lib/BridgeFactory.php
@@ -22,7 +22,7 @@ public function __construct()
$enabledBridges = Configuration::getConfig('system', 'enabled_bridges');
if ($enabledBridges === null) {
- throw new \Exception('No bridges are enabled...');
+ throw new \Exception(xlat('errors:general:no_bridges_enabled'));
}
foreach ($enabledBridges as $enabledBridge) {
if ($enabledBridge === '*') {
@@ -34,7 +34,7 @@ public function __construct()
$this->enabledBridges[] = $bridgeClassName;
} else {
$this->missingEnabledBridges[] = $enabledBridge;
- $this->logger->info(sprintf('Bridge not found: %s', $enabledBridge));
+ $this->logger->info(xlat('errors:general:not_found_named', $enabledBridge));
}
}
}
diff --git a/lib/CacheFactory.php b/lib/CacheFactory.php
index 90aa21ba7be..9dced2d4112 100644
--- a/lib/CacheFactory.php
+++ b/lib/CacheFactory.php
@@ -16,7 +16,7 @@ public function create(string $name = null): CacheInterface
{
$name ??= Configuration::getConfig('cache', 'type');
if (!$name) {
- throw new \Exception('No cache type configured');
+ throw new \Exception(xlat('errors:cache:no_type'));
}
$cacheNames = [];
foreach (scandir(PATH_LIB_CACHES) as $file) {
@@ -35,12 +35,12 @@ public function create(string $name = null): CacheInterface
$index = array_search(strtolower($name), array_map('strtolower', $cacheNames));
if ($index === false) {
- throw new \InvalidArgumentException(sprintf('Invalid cache name: "%s"', $name));
+ throw new \InvalidArgumentException(xlat('errors:cache:bad_name', $name));
}
$className = $cacheNames[$index] . 'Cache';
if (!preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $className)) {
- throw new \InvalidArgumentException(sprintf('Invalid cache classname: "%s"', $className));
+ throw new \InvalidArgumentException(xlat('errors:cache:bad_classname', $className));
}
switch ($className) {
@@ -53,27 +53,27 @@ public function create(string $name = null): CacheInterface
'enable_purge' => Configuration::getConfig('FileCache', 'enable_purge'),
];
if (!is_dir($fileCacheConfig['path'])) {
- throw new \Exception(sprintf('The FileCache path does not exists: %s', $fileCacheConfig['path']));
+ throw new \Exception(xlat('errors:cache:filecache_path_not_found', $fileCacheConfig['path']));
}
if (!is_writable($fileCacheConfig['path'])) {
- throw new \Exception(sprintf('The FileCache path is not writable: %s', $fileCacheConfig['path']));
+ throw new \Exception(xlat('errors:cache:filecache_not_writable', $fileCacheConfig['path']));
}
return new FileCache($this->logger, $fileCacheConfig);
case SQLiteCache::class:
if (!extension_loaded('sqlite3')) {
- throw new \Exception('"sqlite3" extension not loaded. Please check "php.ini"');
+ throw new \Exception(xlat('errors:cache:not_loaded', 'sqlite'));
}
if (!is_writable(PATH_CACHE)) {
- throw new \Exception('The cache folder is not writable');
+ throw new \Exception(xlat('errors:cache:path_not_writable'));
}
$file = Configuration::getConfig('SQLiteCache', 'file');
if (!$file) {
- throw new \Exception(sprintf('Configuration for %s missing.', 'SQLiteCache'));
+ throw new \Exception(xlat('errors:cache:config_missing', 'SQLiteCache'));
}
if (dirname($file) == '.') {
$file = PATH_CACHE . $file;
} elseif (!is_dir(dirname($file))) {
- throw new \Exception(sprintf('Invalid configuration for %s', 'SQLiteCache'));
+ throw new \Exception(xlat('errors:cache:config_invalid', 'SQLiteCache'));
}
return new SQLiteCache($this->logger, [
'file' => $file,
@@ -82,31 +82,31 @@ public function create(string $name = null): CacheInterface
]);
case MemcachedCache::class:
if (!extension_loaded('memcached')) {
- throw new \Exception('"memcached" extension not loaded. Please check "php.ini"');
+ throw new \Exception(xlat('errors:cache:not_loaded', 'memcached'));
}
$section = 'MemcachedCache';
$host = Configuration::getConfig($section, 'host');
$port = Configuration::getConfig($section, 'port');
if (empty($host) && empty($port)) {
- throw new \Exception('Configuration for ' . $section . ' missing.');
+ throw new \Exception(xlat('errors:cache:config_missing', $section));
}
if (empty($host)) {
- throw new \Exception('"host" param is not set for ' . $section);
+ throw new \Exception(xlat('errors:cache:param_not_set', 'host', $section));
}
if (empty($port)) {
- throw new \Exception('"port" param is not set for ' . $section);
+ throw new \Exception(xlat('errors:cache:param_not_set', 'port', $section));
}
if (!ctype_digit($port)) {
- throw new \Exception('"port" param is invalid for ' . $section);
+ throw new \Exception(xlat('errors:cache:param_invalid', 'port', $section));
}
$port = intval($port);
if ($port < 1 || $port > 65535) {
- throw new \Exception('"port" param is invalid for ' . $section);
+ throw new \Exception(xlat('errors:cache:param_invalid', 'port', $section));
}
return new MemcachedCache($this->logger, $host, $port);
default:
if (!file_exists(PATH_LIB_CACHES . $className . '.php')) {
- throw new \Exception('Unable to find the cache file');
+ throw new \Exception(xlat('errors:cache:missing_file'));
}
return new $className();
}
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
index fe809bc259b..51273ebfb91 100644
--- a/lib/FeedExpander.php
+++ b/lib/FeedExpander.php
@@ -10,7 +10,7 @@ abstract class FeedExpander extends BridgeAbstract
public function collectExpandableDatas(string $url, $maxItems = -1)
{
if (!$url) {
- throw new \Exception('There is no $url for this RSS expander');
+ throw new \Exception(xlat('errors:expander:no_url'));
}
$maxItems = (int) $maxItems;
if ($maxItems === -1) {
@@ -20,7 +20,7 @@ public function collectExpandableDatas(string $url, $maxItems = -1)
$httpHeaders = ['Accept: ' . implode(', ', $accept)];
$xmlString = getContents($url, $httpHeaders);
if ($xmlString === '') {
- throw new \Exception(sprintf('Unable to parse xml from `%s` because we got the empty string', $url), 10);
+ throw new \Exception(xlat('errors:expander:bad_xml_url', $url), 10);
}
// prepare/massage the xml to make it more acceptable
$problematicStrings = [
@@ -35,7 +35,7 @@ public function collectExpandableDatas(string $url, $maxItems = -1)
$this->feed = $feedParser->parseFeed($xmlString);
} catch (\Exception $e) {
// FeedMergeBridge relies on this string
- throw new \Exception(sprintf('Failed to parse xml from %s: %s', $url, create_sane_exception_message($e)));
+ throw new \Exception(xlat('errors:expander:bad_xml_url_msg', $url, create_sane_exception_message($e)));
}
$items = array_slice($this->feed['items'], 0, $maxItems);
diff --git a/lib/FeedParser.php b/lib/FeedParser.php
index b774cc14e23..23e3fae8d62 100644
--- a/lib/FeedParser.php
+++ b/lib/FeedParser.php
@@ -23,7 +23,7 @@ public function parseFeed(string $xmlString): array
if ($xmlErrors) {
$firstXmlErrorMessage = $xmlErrors[0]->message;
}
- throw new \Exception(sprintf('Unable to parse xml: %s', $firstXmlErrorMessage ?? ''));
+ throw new \Exception(xlat('errors:parser:bad_xml_msg', $firstXmlErrorMessage ?? ''));
}
$feed = [
'title' => null,
@@ -79,7 +79,7 @@ public function parseFeed(string $xmlString): array
$feed['items'][] = $this->parseAtomItem($item);
}
} else {
- throw new \Exception('Unable to detect feed format');
+ throw new \Exception(xlat('errors:parser:feed_format'));
}
return $feed;
diff --git a/lib/FormatFactory.php b/lib/FormatFactory.php
index e9cbe597770..09ae7ccfd4b 100644
--- a/lib/FormatFactory.php
+++ b/lib/FormatFactory.php
@@ -18,11 +18,11 @@ public function __construct()
public function create(string $name): FormatAbstract
{
if (! preg_match('/^[a-zA-Z0-9-]*$/', $name)) {
- throw new \InvalidArgumentException('Format name invalid!');
+ throw new \InvalidArgumentException(xlat('errors:format:invalid_name', $name));
}
$sanitizedName = $this->sanitizeName($name);
if (!$sanitizedName) {
- throw new \InvalidArgumentException(sprintf('Unknown format given `%s`', $name));
+ throw new \InvalidArgumentException(xlat('errors:format:invalid_name', $name));
}
$className = '\\' . $sanitizedName . 'Format';
return new $className();
diff --git a/lib/bootstrap.php b/lib/bootstrap.php
index bfc7be39704..ac40e31e9ce 100644
--- a/lib/bootstrap.php
+++ b/lib/bootstrap.php
@@ -1,5 +1,10 @@
scheme = $scheme;
@@ -109,7 +109,7 @@ public function withPort(int $port)
public function withPath(string $path): self
{
if (!str_starts_with($path, '/')) {
- throw new UrlException(sprintf('Path must start with forward slash: %s', $path));
+ throw new UrlException(xlat('errors:url:path_slash', $path));
}
$clone = clone $this;
$clone->path = $path;
diff --git a/templates/bridge-error.html.php b/templates/bridge-error.html.php
index 8ece80be538..025f9394382 100644
--- a/templates/bridge-error.html.php
+++ b/templates/bridge-error.html.php
@@ -1,12 +1,12 @@
= raw($error) ?>
-
-
+
+
-
-
+
+
diff --git a/templates/connectivity.html.php b/templates/connectivity.html.php
index c00e8177eab..48c41042bfd 100644
--- a/templates/connectivity.html.php
+++ b/templates/connectivity.html.php
@@ -23,7 +23,7 @@
×
-
+