diff --git a/app/Http/Controllers/Base/LocaleController.php b/app/Http/Controllers/Base/LocaleController.php new file mode 100644 index 000000000..f29ba23d0 --- /dev/null +++ b/app/Http/Controllers/Base/LocaleController.php @@ -0,0 +1,43 @@ +translator = $translator; + } + + /** + * Returns translation data given a specific locale and namespace. + * + * @param \Illuminate\Http\Request $request + * @param string $locale + * @param string $namespace + * @return \Illuminate\Http\JsonResponse + */ + public function __invoke(Request $request, string $locale, string $namespace) + { + $data = $this->translator->getLoader()->load($locale, str_replace('.', '/', $namespace)); + + return JsonResponse::create($data, 200, [ + 'E-Tag' => md5(json_encode($data)), + ]); + } +} diff --git a/package.json b/package.json index dd9a1b168..72af69148 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,10 @@ "easy-peasy": "^3.0.2", "events": "^3.0.0", "formik": "^1.5.7", + "i18next": "^19.0.0", + "i18next-chained-backend": "^2.0.0", + "i18next-localstorage-backend": "^3.0.0", + "i18next-xhr-backend": "^3.2.2", "jquery": "^3.3.1", "lodash-es": "^4.17.15", "path": "^0.12.7", @@ -21,6 +25,7 @@ "react": "^16.8.6", "react-dom": "^16.8.6", "react-hot-loader": "^4.12.13", + "react-i18next": "^11.2.1", "react-redux": "^7.1.0", "react-router-dom": "^5.0.1", "react-transition-group": "^4.1.0", diff --git a/resources/scripts/components/server/users/PermissionEditor.tsx b/resources/scripts/components/server/users/PermissionEditor.tsx index d2525681a..bf72fe344 100644 --- a/resources/scripts/components/server/users/PermissionEditor.tsx +++ b/resources/scripts/components/server/users/PermissionEditor.tsx @@ -1,10 +1,46 @@ import React from 'react'; import { SubuserPermission } from '@/state/server/subusers'; +import { useStoreState } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import { useTranslation } from 'react-i18next'; interface Props { defaultPermissions: SubuserPermission[]; } export default ({ defaultPermissions }: Props) => { - return null; + const { t } = useTranslation('server.users'); + const permissions = useStoreState((state: ApplicationStore) => state.permissions.data); + + return ( +
+ { + permissions.map(permission => ( +
+ = 0} + /> + +
+ )) + } +
+ +
+
+ ); }; diff --git a/resources/scripts/components/server/users/UsersContainer.tsx b/resources/scripts/components/server/users/UsersContainer.tsx index 8a3aa77bb..01e42f741 100644 --- a/resources/scripts/components/server/users/UsersContainer.tsx +++ b/resources/scripts/components/server/users/UsersContainer.tsx @@ -9,6 +9,7 @@ import classNames from 'classnames'; import PermissionEditor from '@/components/server/users/PermissionEditor'; import { Actions, useStoreActions, useStoreState } from 'easy-peasy'; import { ApplicationStore } from '@/state'; +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft'; export default () => { const [ loading, setLoading ] = useState(true); @@ -45,10 +46,12 @@ export default () => {

Subusers

-
+
{(loading || !permissions.length) ?
@@ -93,11 +96,21 @@ export default () => { {editSubuser &&
-

Edit {editSubuser.email}

-
- +

+ setEditSubuser(null)}> + + + Edit {editSubuser.email} +

+
+ + +
diff --git a/resources/scripts/i18n.ts b/resources/scripts/i18n.ts new file mode 100644 index 000000000..ceab3cffc --- /dev/null +++ b/resources/scripts/i18n.ts @@ -0,0 +1,32 @@ +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'; + +i18n + .use(Backend) + .use(initReactI18next) + .init({ + debug: process.env.NODE_ENV !== 'production', + 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', + }], + }, + }); + +// i18n.loadNamespaces(['validation']); + +export default i18n; diff --git a/resources/scripts/index.tsx b/resources/scripts/index.tsx index ebbb9d880..a39f33d8f 100644 --- a/resources/scripts/index.tsx +++ b/resources/scripts/index.tsx @@ -1,5 +1,6 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import React from 'react'; +import ReactDOM from 'react-dom'; import App from '@/components/App'; +import './i18n'; ReactDOM.render(, document.getElementById('app')); diff --git a/resources/styles/components/forms.css b/resources/styles/components/forms.css index 584a9163a..5021f8d5a 100644 --- a/resources/styles/components/forms.css +++ b/resources/styles/components/forms.css @@ -219,3 +219,26 @@ a.btn { cursor: default; } } + +input[type="checkbox"], input[type="radio"] { + @apply .appearance-none .inline-block .align-middle .select-none .flex-no-shrink .w-4 .h-4 .text-primary-400 .border .border-neutral-300 .rounded-sm; + color-adjust: exact; + background-origin: border-box; + transition: all 75ms linear, box-shadow 25ms linear; + + &:checked { + @apply .border-transparent .bg-no-repeat .bg-center; + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e"); + background-color: currentColor; + background-size: 100% 100%; + } + + &:focus { + @apply .outline-none .border-primary-300; + box-shadow: 0 0 0 1px rgba(9, 103, 210, 0.25); + } +} + +input[type="radio"] { + @apply .rounded-full; +} diff --git a/routes/base.php b/routes/base.php index e90fa7ef8..517aa5223 100644 --- a/routes/base.php +++ b/routes/base.php @@ -3,6 +3,9 @@ Route::get('/', 'IndexController@index')->name('index')->fallback(); Route::get('/account', 'IndexController@index')->name('account'); +Route::get('/locales/{locale}/{namespace}.json', 'LocaleController') + ->where('namespace', '.*'); + /* |-------------------------------------------------------------------------- | Account API Controller Routes diff --git a/yarn.lock b/yarn.lock index 63dbbcbe7..feed4cc31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -747,6 +747,12 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.4.2": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" @@ -4141,6 +4147,12 @@ html-minifier@^3.2.3: relateurl "0.2.x" uglify-js "3.3.x" +html-parse-stringify2@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a" + dependencies: + void-elements "^2.0.1" + html-webpack-plugin@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" @@ -4224,6 +4236,30 @@ humps@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" +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" + dependencies: + "@babel/runtime" "^7.4.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-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" + 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" + iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -6909,6 +6945,13 @@ react-hot-loader@^4.12.13: shallowequal "^1.1.0" source-map "^0.7.3" +react-i18next@^11.2.1: + version "11.2.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.2.1.tgz#a56d9f1f52d003eb4fa8f1c7d6752123827160f0" + dependencies: + "@babel/runtime" "^7.3.1" + html-parse-stringify2 "2.0.1" + react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" @@ -8394,6 +8437,10 @@ vm-browserify@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" +void-elements@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + watchpack@^1.5.0, watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"