Skip to content

Commit 986c375

Browse files
committed
Improve support for use of i18next; rely on browser caching to keep things simple
1 parent 8e02966 commit 986c375

File tree

9 files changed

+152
-75
lines changed

9 files changed

+152
-75
lines changed

app/Http/Controllers/Base/LocaleController.php

+40-12
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,61 @@
66
use Illuminate\Http\JsonResponse;
77
use Illuminate\Translation\Translator;
88
use Pterodactyl\Http\Controllers\Controller;
9+
use Illuminate\Contracts\Translation\Loader;
910

1011
class LocaleController extends Controller
1112
{
12-
/**
13-
* @var \Illuminate\Translation\Translator
14-
*/
15-
private $translator;
13+
protected Loader $loader;
1614

17-
/**
18-
* LocaleController constructor.
19-
*/
2015
public function __construct(Translator $translator)
2116
{
22-
$this->translator = $translator;
17+
$this->loader = $translator->getLoader();
2318
}
2419

2520
/**
2621
* Returns translation data given a specific locale and namespace.
2722
*
2823
* @return \Illuminate\Http\JsonResponse
2924
*/
30-
public function __invoke(Request $request, string $locale, string $namespace)
25+
public function __invoke(Request $request)
3126
{
32-
$data = $this->translator->getLoader()->load($locale, str_replace('.', '/', $namespace));
27+
$locales = explode(' ', $request->input('locale') ?? '');
28+
$namespaces = explode(' ', $request->input('namespace') ?? '');
29+
30+
$response = [];
31+
foreach ($locales as $locale) {
32+
$response[$locale] = [];
33+
foreach ($namespaces as $namespace) {
34+
$response[$locale][$namespace] = $this->i18n(
35+
$this->loader->load($locale, str_replace('.', '/', $namespace))
36+
);
37+
}
38+
}
3339

34-
return new JsonResponse($data, 200, [
35-
'E-Tag' => md5(json_encode($data)),
40+
return new JsonResponse($response, 200, [
41+
// Cache this in the browser for an hour, and allow the browser to use a stale
42+
// cache for up to a day after it was created while it fetches an updated set
43+
// of translation keys.
44+
'Cache-Control' => 'public, max-age=3600, stale-while-revalidate=86400',
45+
'ETag' => md5(json_encode($response, JSON_THROW_ON_ERROR)),
3646
]);
3747
}
48+
49+
/**
50+
* Convert standard Laravel translation keys that look like ":foo"
51+
* into key structures that are supported by the front-end i18n
52+
* library, like "{{foo}}".
53+
*/
54+
protected function i18n(array $data): array
55+
{
56+
foreach ($data as $key => $value) {
57+
if (is_array($value)) {
58+
$data[$key] = $this->i18n($value);
59+
} else {
60+
$data[$key] = preg_replace('/:([\w-]+)(\W?|$)/m', '{{$1}}$2', $value);
61+
}
62+
}
63+
64+
return $data;
65+
}
3866
}

package.json

+3-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@
2020
"events": "^3.0.0",
2121
"formik": "^2.2.6",
2222
"framer-motion": "^6.3.10",
23-
"i18next": "^19.0.0",
24-
"i18next-chained-backend": "^2.0.0",
25-
"i18next-localstorage-backend": "^3.0.0",
26-
"i18next-xhr-backend": "^3.2.2",
23+
"i18next": "^21.8.9",
24+
"i18next-http-backend": "^1.4.1",
25+
"i18next-multiload-backend-adapter": "^1.0.0",
2726
"qrcode.react": "^1.0.1",
2827
"query-string": "^6.7.0",
2928
"react": "^16.14.0",

resources/lang/en/activity.php

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99
return [
1010
'auth' => [
11+
'fail' => 'Failed login attempt',
12+
'success' => 'Successfully logged in',
1113
'password-reset' => 'Reset account password',
1214
'reset-password' => 'Sending password reset email',
1315
'checkpoint' => 'Prompting for second factor authentication',

resources/scripts/i18n.ts

+20-17
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
import i18n from 'i18next';
22
import { initReactI18next } from 'react-i18next';
3-
import LocalStorageBackend from 'i18next-localstorage-backend';
4-
import XHR from 'i18next-xhr-backend';
5-
import Backend from 'i18next-chained-backend';
3+
import I18NextHttpBackend, { BackendOptions } from 'i18next-http-backend';
4+
import I18NextMultiloadBackendAdapter from 'i18next-multiload-backend-adapter';
5+
6+
// If we're using HMR use a unique hash per page reload so that we're always
7+
// doing cache busting. Otherwise just use the builder provided hash value in
8+
// the URL to allow cache busting to occur whenever the front-end is rebuilt.
9+
const hash = module.hot ? Date.now().toString(16) : process.env.WEBPACK_BUILD_HASH;
610

711
i18n
8-
.use(Backend)
12+
.use(I18NextMultiloadBackendAdapter)
913
.use(initReactI18next)
1014
.init({
11-
debug: process.env.NODE_ENV !== 'production',
15+
debug: process.env.DEBUG === 'true',
1216
lng: 'en',
1317
fallbackLng: 'en',
1418
keySeparator: '.',
1519
backend: {
16-
backends: [
17-
LocalStorageBackend,
18-
XHR,
19-
],
20-
backendOptions: [ {
21-
prefix: 'pterodactyl_lng__',
22-
expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days, in milliseconds
23-
store: window.localStorage,
24-
}, {
25-
loadPath: '/locales/{{lng}}/{{ns}}.json',
26-
} ],
20+
backend: I18NextHttpBackend,
21+
backendOption: {
22+
loadPath: `/locales/locale.json?locale={{lng}}&namespace={{ns}}&hash=${hash}`,
23+
allowMultiLoading: true,
24+
} as BackendOptions,
25+
} as Record<string, any>,
26+
interpolation: {
27+
// Per i18n-react documentation: this is not needed since React is already
28+
// handling escapes for us.
29+
escapeValue: false,
2730
},
2831
});
2932

30-
// i18n.loadNamespaces(['validation']);
33+
i18n.loadNamespaces([ 'validation' ]).catch(console.error);
3134

3235
export default i18n;

resources/scripts/index.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
33
import App from '@/components/App';
4-
import './i18n';
54
import { setConfig } from 'react-hot-loader';
65

6+
// Enable language support.
7+
import './i18n';
8+
79
// Prevents page reloads while making component changes which
810
// also avoids triggering constant loading indicators all over
911
// the place in development.

resources/scripts/routers/DashboardRouter.tsx

+23-20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import SubNavigation from '@/components/elements/SubNavigation';
1010
import AccountSSHContainer from '@/components/dashboard/ssh/AccountSSHContainer';
1111
import { useLocation } from 'react-router';
1212
import ActivityLogContainer from '@/components/dashboard/activity/ActivityLogContainer';
13+
import Spinner from '@/components/elements/Spinner';
1314

1415
export default () => {
1516
const location = useLocation();
@@ -28,26 +29,28 @@ export default () => {
2829
</SubNavigation>
2930
}
3031
<TransitionRouter>
31-
<Switch location={location}>
32-
<Route path={'/'} exact>
33-
<DashboardContainer/>
34-
</Route>
35-
<Route path={'/account'} exact>
36-
<AccountOverviewContainer/>
37-
</Route>
38-
<Route path={'/account/api'} exact>
39-
<AccountApiContainer/>
40-
</Route>
41-
<Route path={'/account/ssh'} exact>
42-
<AccountSSHContainer/>
43-
</Route>
44-
<Route path={'/account/activity'} exact>
45-
<ActivityLogContainer />
46-
</Route>
47-
<Route path={'*'}>
48-
<NotFound/>
49-
</Route>
50-
</Switch>
32+
<React.Suspense fallback={<Spinner centered/>}>
33+
<Switch location={location}>
34+
<Route path={'/'} exact>
35+
<DashboardContainer/>
36+
</Route>
37+
<Route path={'/account'} exact>
38+
<AccountOverviewContainer/>
39+
</Route>
40+
<Route path={'/account/api'} exact>
41+
<AccountApiContainer/>
42+
</Route>
43+
<Route path={'/account/ssh'} exact>
44+
<AccountSSHContainer/>
45+
</Route>
46+
<Route path={'/account/activity'} exact>
47+
<ActivityLogContainer/>
48+
</Route>
49+
<Route path={'*'}>
50+
<NotFound/>
51+
</Route>
52+
</Switch>
53+
</React.Suspense>
5154
</TransitionRouter>
5255
</>
5356
);

routes/base.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
->withoutMiddleware(RequireTwoFactorAuthentication::class)
99
->name('account');
1010

11-
Route::get('/locales/{locale}/{namespace}.json', Base\LocaleController::class)
11+
Route::get('/locales/locale.json', Base\LocaleController::class)
1212
->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class])
1313
->where('namespace', '.*');
1414

webpack.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const path = require('path');
2+
const webpack = require('webpack');
23
const AssetsManifestPlugin = require('webpack-assets-manifest');
34
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
45
const TerserPlugin = require('terser-webpack-plugin');
@@ -94,6 +95,11 @@ module.exports = {
9495
moment: 'moment',
9596
},
9697
plugins: [
98+
new webpack.EnvironmentPlugin({
99+
NODE_ENV: 'development',
100+
DEBUG: process.env.NODE_ENV !== 'production',
101+
WEBPACK_BUILD_HASH: Date.now().toString(16),
102+
}),
97103
new AssetsManifestPlugin({ writeToDisk: true, publicPath: true, integrity: true, integrityHashes: ['sha384'] }),
98104
new ForkTsCheckerWebpackPlugin({
99105
typescript: {

yarn.lock

+54-20
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@
10101010
"@babel/helper-plugin-utils" "^7.10.4"
10111011
"@babel/plugin-transform-typescript" "^7.12.1"
10121012

1013-
"@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":
1013+
"@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":
10141014
version "7.7.5"
10151015
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.5.tgz#4b087f183f5d83647744d4157f66199081d17a00"
10161016
dependencies:
@@ -1023,6 +1023,13 @@
10231023
dependencies:
10241024
regenerator-runtime "^0.13.4"
10251025

1026+
"@babel/runtime@^7.17.2":
1027+
version "7.18.3"
1028+
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
1029+
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
1030+
dependencies:
1031+
regenerator-runtime "^0.13.4"
1032+
10261033
"@babel/runtime@^7.7.2", "@babel/runtime@^7.9.6":
10271034
version "7.10.4"
10281035
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99"
@@ -3011,6 +3018,13 @@ cross-env@^7.0.2:
30113018
dependencies:
30123019
cross-spawn "^7.0.1"
30133020

3021+
3022+
version "3.1.5"
3023+
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
3024+
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
3025+
dependencies:
3026+
node-fetch "2.6.7"
3027+
30143028
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
30153029
version "6.0.5"
30163030
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -4701,29 +4715,24 @@ https-browserify@^1.0.0:
47014715
version "1.0.0"
47024716
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
47034717

4704-
i18next-chained-backend@^2.0.0:
4705-
version "2.0.0"
4706-
resolved "https://registry.yarnpkg.com/i18next-chained-backend/-/i18next-chained-backend-2.0.0.tgz#faf2e8b5f081a01e74fbec1fe580c184bc64e25b"
4707-
dependencies:
4708-
"@babel/runtime" "^7.4.5"
4709-
4710-
i18next-localstorage-backend@^3.0.0:
4711-
version "3.0.0"
4712-
resolved "https://registry.yarnpkg.com/i18next-localstorage-backend/-/i18next-localstorage-backend-3.0.0.tgz#19b4e836e9a79e564631b88b8ba1c738375e636f"
4718+
i18next-http-backend@^1.4.1:
4719+
version "1.4.1"
4720+
resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz#d8d308e7d8c5b89988446d0b83f469361e051bc0"
4721+
integrity sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==
47134722
dependencies:
4714-
"@babel/runtime" "^7.4.5"
4723+
cross-fetch "3.1.5"
47154724

4716-
i18next-xhr-backend@^3.2.2:
4717-
version "3.2.2"
4718-
resolved "https://registry.yarnpkg.com/i18next-xhr-backend/-/i18next-xhr-backend-3.2.2.tgz#769124441461b085291f539d91864e3691199178"
4719-
dependencies:
4720-
"@babel/runtime" "^7.5.5"
4725+
i18next-multiload-backend-adapter@^1.0.0:
4726+
version "1.0.0"
4727+
resolved "https://registry.yarnpkg.com/i18next-multiload-backend-adapter/-/i18next-multiload-backend-adapter-1.0.0.tgz#3cc3ea102814273bb9059a317d04a3b6e4316121"
4728+
integrity sha512-rZd/Qmr7KkGktVgJa78GPLXEnd51OyB2I9qmbI/mXKPm3MWbXwplIApqmZgxkPC9ce+b8Jnk227qX62W9SaLPQ==
47214729

4722-
i18next@^19.0.0:
4723-
version "19.0.0"
4724-
resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.0.0.tgz#5418207d7286128e6cfe558e659fa8c60d89794b"
4730+
i18next@^21.8.9:
4731+
version "21.8.9"
4732+
resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.8.9.tgz#c79edd5bba61e0a0d5b43a93d52e2d13a526de82"
4733+
integrity sha512-PY9a/8ADVmnju1tETeglbbVQi+nM5pcJQWm9kvKMTE3GPgHHtpDsHy5HQ/hccz2/xtW7j3vuso23JdQSH0EttA==
47254734
dependencies:
4726-
"@babel/runtime" "^7.3.1"
4735+
"@babel/runtime" "^7.17.2"
47274736

47284737
[email protected], iconv-lite@^0.4.4:
47294738
version "0.4.24"
@@ -5833,6 +5842,13 @@ node-emoji@^1.11.0:
58335842
dependencies:
58345843
lodash "^4.17.21"
58355844

5845+
5846+
version "2.6.7"
5847+
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
5848+
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
5849+
dependencies:
5850+
whatwg-url "^5.0.0"
5851+
58365852
58375853
version "0.9.0"
58385854
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
@@ -8394,6 +8410,11 @@ toposort@^2.0.2:
83948410
version "2.0.2"
83958411
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
83968412

8413+
tr46@~0.0.3:
8414+
version "0.0.3"
8415+
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
8416+
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
8417+
83978418
tryer@^1.0.1:
83988419
version "1.0.1"
83998420
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
@@ -8704,6 +8725,11 @@ wbuf@^1.1.0, wbuf@^1.7.3:
87048725
dependencies:
87058726
minimalistic-assert "^1.0.0"
87068727

8728+
webidl-conversions@^3.0.0:
8729+
version "3.0.1"
8730+
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
8731+
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
8732+
87078733
webpack-assets-manifest@^3.1.1:
87088734
version "3.1.1"
87098735
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:
88698895
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
88708896
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
88718897

8898+
whatwg-url@^5.0.0:
8899+
version "5.0.0"
8900+
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
8901+
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
8902+
dependencies:
8903+
tr46 "~0.0.3"
8904+
webidl-conversions "^3.0.0"
8905+
88728906
which-boxed-primitive@^1.0.2:
88738907
version "1.0.2"
88748908
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"

0 commit comments

Comments
 (0)