Improve support for use of i18next; rely on browser caching to keep things simple
This commit is contained in:
parent
8e02966935
commit
986c375052
9 changed files with 152 additions and 75 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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', '.*');
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
74
yarn.lock
74
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue