Improve support for use of i18next; rely on browser caching to keep things simple

This commit is contained in:
DaneEveritt 2022-06-11 14:04:09 -04:00
parent 8e02966935
commit 986c375052
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
9 changed files with 152 additions and 75 deletions

View file

@ -6,20 +6,15 @@ use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Translation\Translator;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Translation\Loader;
class LocaleController extends Controller
{
/**
* @var \Illuminate\Translation\Translator
*/
private $translator;
protected Loader $loader;
/**
* LocaleController constructor.
*/
public function __construct(Translator $translator)
{
$this->translator = $translator;
$this->loader = $translator->getLoader();
}
/**
@ -27,12 +22,45 @@ class LocaleController extends Controller
*
* @return \Illuminate\Http\JsonResponse
*/
public function __invoke(Request $request, string $locale, string $namespace)
public function __invoke(Request $request)
{
$data = $this->translator->getLoader()->load($locale, str_replace('.', '/', $namespace));
$locales = explode(' ', $request->input('locale') ?? '');
$namespaces = explode(' ', $request->input('namespace') ?? '');
return new JsonResponse($data, 200, [
'E-Tag' => md5(json_encode($data)),
$response = [];
foreach ($locales as $locale) {
$response[$locale] = [];
foreach ($namespaces as $namespace) {
$response[$locale][$namespace] = $this->i18n(
$this->loader->load($locale, str_replace('.', '/', $namespace))
);
}
}
return new JsonResponse($response, 200, [
// Cache this in the browser for an hour, and allow the browser to use a stale
// cache for up to a day after it was created while it fetches an updated set
// of translation keys.
'Cache-Control' => 'public, max-age=3600, stale-while-revalidate=86400',
'ETag' => md5(json_encode($response, JSON_THROW_ON_ERROR)),
]);
}
/**
* Convert standard Laravel translation keys that look like ":foo"
* into key structures that are supported by the front-end i18n
* library, like "{{foo}}".
*/
protected function i18n(array $data): array
{
foreach ($data as $key => $value) {
if (is_array($value)) {
$data[$key] = $this->i18n($value);
} else {
$data[$key] = preg_replace('/:([\w-]+)(\W?|$)/m', '{{$1}}$2', $value);
}
}
return $data;
}
}

View file

@ -20,10 +20,9 @@
"events": "^3.0.0",
"formik": "^2.2.6",
"framer-motion": "^6.3.10",
"i18next": "^19.0.0",
"i18next-chained-backend": "^2.0.0",
"i18next-localstorage-backend": "^3.0.0",
"i18next-xhr-backend": "^3.2.2",
"i18next": "^21.8.9",
"i18next-http-backend": "^1.4.1",
"i18next-multiload-backend-adapter": "^1.0.0",
"qrcode.react": "^1.0.1",
"query-string": "^6.7.0",
"react": "^16.14.0",

View file

@ -8,6 +8,8 @@
*/
return [
'auth' => [
'fail' => 'Failed login attempt',
'success' => 'Successfully logged in',
'password-reset' => 'Reset account password',
'reset-password' => 'Sending password reset email',
'checkpoint' => 'Prompting for second factor authentication',

View file

@ -1,32 +1,35 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LocalStorageBackend from 'i18next-localstorage-backend';
import XHR from 'i18next-xhr-backend';
import Backend from 'i18next-chained-backend';
import I18NextHttpBackend, { BackendOptions } from 'i18next-http-backend';
import I18NextMultiloadBackendAdapter from 'i18next-multiload-backend-adapter';
// If we're using HMR use a unique hash per page reload so that we're always
// doing cache busting. Otherwise just use the builder provided hash value in
// the URL to allow cache busting to occur whenever the front-end is rebuilt.
const hash = module.hot ? Date.now().toString(16) : process.env.WEBPACK_BUILD_HASH;
i18n
.use(Backend)
.use(I18NextMultiloadBackendAdapter)
.use(initReactI18next)
.init({
debug: process.env.NODE_ENV !== 'production',
debug: process.env.DEBUG === 'true',
lng: 'en',
fallbackLng: 'en',
keySeparator: '.',
backend: {
backends: [
LocalStorageBackend,
XHR,
],
backendOptions: [ {
prefix: 'pterodactyl_lng__',
expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days, in milliseconds
store: window.localStorage,
}, {
loadPath: '/locales/{{lng}}/{{ns}}.json',
} ],
backend: I18NextHttpBackend,
backendOption: {
loadPath: `/locales/locale.json?locale={{lng}}&namespace={{ns}}&hash=${hash}`,
allowMultiLoading: true,
} as BackendOptions,
} as Record<string, any>,
interpolation: {
// Per i18n-react documentation: this is not needed since React is already
// handling escapes for us.
escapeValue: false,
},
});
// i18n.loadNamespaces(['validation']);
i18n.loadNamespaces([ 'validation' ]).catch(console.error);
export default i18n;

View file

@ -1,9 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from '@/components/App';
import './i18n';
import { setConfig } from 'react-hot-loader';
// Enable language support.
import './i18n';
// Prevents page reloads while making component changes which
// also avoids triggering constant loading indicators all over
// the place in development.

View file

@ -10,6 +10,7 @@ import SubNavigation from '@/components/elements/SubNavigation';
import AccountSSHContainer from '@/components/dashboard/ssh/AccountSSHContainer';
import { useLocation } from 'react-router';
import ActivityLogContainer from '@/components/dashboard/activity/ActivityLogContainer';
import Spinner from '@/components/elements/Spinner';
export default () => {
const location = useLocation();
@ -28,26 +29,28 @@ export default () => {
</SubNavigation>
}
<TransitionRouter>
<Switch location={location}>
<Route path={'/'} exact>
<DashboardContainer/>
</Route>
<Route path={'/account'} exact>
<AccountOverviewContainer/>
</Route>
<Route path={'/account/api'} exact>
<AccountApiContainer/>
</Route>
<Route path={'/account/ssh'} exact>
<AccountSSHContainer/>
</Route>
<Route path={'/account/activity'} exact>
<ActivityLogContainer />
</Route>
<Route path={'*'}>
<NotFound/>
</Route>
</Switch>
<React.Suspense fallback={<Spinner centered/>}>
<Switch location={location}>
<Route path={'/'} exact>
<DashboardContainer/>
</Route>
<Route path={'/account'} exact>
<AccountOverviewContainer/>
</Route>
<Route path={'/account/api'} exact>
<AccountApiContainer/>
</Route>
<Route path={'/account/ssh'} exact>
<AccountSSHContainer/>
</Route>
<Route path={'/account/activity'} exact>
<ActivityLogContainer/>
</Route>
<Route path={'*'}>
<NotFound/>
</Route>
</Switch>
</React.Suspense>
</TransitionRouter>
</>
);

View file

@ -8,7 +8,7 @@ Route::get('/account', [Base\IndexController::class, 'index'])
->withoutMiddleware(RequireTwoFactorAuthentication::class)
->name('account');
Route::get('/locales/{locale}/{namespace}.json', Base\LocaleController::class)
Route::get('/locales/locale.json', Base\LocaleController::class)
->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class])
->where('namespace', '.*');

View file

@ -1,4 +1,5 @@
const path = require('path');
const webpack = require('webpack');
const AssetsManifestPlugin = require('webpack-assets-manifest');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
@ -94,6 +95,11 @@ module.exports = {
moment: 'moment',
},
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
DEBUG: process.env.NODE_ENV !== 'production',
WEBPACK_BUILD_HASH: Date.now().toString(16),
}),
new AssetsManifestPlugin({ writeToDisk: true, publicPath: true, integrity: true, integrityHashes: ['sha384'] }),
new ForkTsCheckerWebpackPlugin({
typescript: {

View file

@ -1010,7 +1010,7 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-transform-typescript" "^7.12.1"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3":
version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.5.tgz#4b087f183f5d83647744d4157f66199081d17a00"
dependencies:
@ -1023,6 +1023,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.17.2":
version "7.18.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.7.2", "@babel/runtime@^7.9.6":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99"
@ -3011,6 +3018,13 @@ cross-env@^7.0.2:
dependencies:
cross-spawn "^7.0.1"
cross-fetch@3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
dependencies:
node-fetch "2.6.7"
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -4701,29 +4715,24 @@ https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
i18next-chained-backend@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/i18next-chained-backend/-/i18next-chained-backend-2.0.0.tgz#faf2e8b5f081a01e74fbec1fe580c184bc64e25b"
i18next-http-backend@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz#d8d308e7d8c5b89988446d0b83f469361e051bc0"
integrity sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==
dependencies:
"@babel/runtime" "^7.4.5"
cross-fetch "3.1.5"
i18next-localstorage-backend@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/i18next-localstorage-backend/-/i18next-localstorage-backend-3.0.0.tgz#19b4e836e9a79e564631b88b8ba1c738375e636f"
dependencies:
"@babel/runtime" "^7.4.5"
i18next-multiload-backend-adapter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/i18next-multiload-backend-adapter/-/i18next-multiload-backend-adapter-1.0.0.tgz#3cc3ea102814273bb9059a317d04a3b6e4316121"
integrity sha512-rZd/Qmr7KkGktVgJa78GPLXEnd51OyB2I9qmbI/mXKPm3MWbXwplIApqmZgxkPC9ce+b8Jnk227qX62W9SaLPQ==
i18next-xhr-backend@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/i18next-xhr-backend/-/i18next-xhr-backend-3.2.2.tgz#769124441461b085291f539d91864e3691199178"
i18next@^21.8.9:
version "21.8.9"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.8.9.tgz#c79edd5bba61e0a0d5b43a93d52e2d13a526de82"
integrity sha512-PY9a/8ADVmnju1tETeglbbVQi+nM5pcJQWm9kvKMTE3GPgHHtpDsHy5HQ/hccz2/xtW7j3vuso23JdQSH0EttA==
dependencies:
"@babel/runtime" "^7.5.5"
i18next@^19.0.0:
version "19.0.0"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.0.0.tgz#5418207d7286128e6cfe558e659fa8c60d89794b"
dependencies:
"@babel/runtime" "^7.3.1"
"@babel/runtime" "^7.17.2"
iconv-lite@0.4.24, iconv-lite@^0.4.4:
version "0.4.24"
@ -5833,6 +5842,13 @@ node-emoji@^1.11.0:
dependencies:
lodash "^4.17.21"
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-forge@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
@ -8394,6 +8410,11 @@ toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tryer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
@ -8704,6 +8725,11 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webpack-assets-manifest@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-3.1.1.tgz#39bbc3bf2ee57fcd8ba07cda51c9ba4a3c6ae1de"
@ -8869,6 +8895,14 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"