mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 06:03:34 +01:00
Feat/admin react (#6211)
* Added vite react admin ui. * Added react i18next. * Added pads manager. * Fixed docker build. * Fixed windows build. * Fixed installOnWindows script. * Install only if path exists.
This commit is contained in:
parent
d34b964cc2
commit
db46ffb63b
112 changed files with 3327 additions and 946 deletions
|
@ -24,3 +24,4 @@ Dockerfile
|
|||
|
||||
settings.json
|
||||
src/node_modules
|
||||
admin/node_modules
|
||||
|
|
24
.github/workflows/backend-tests.yml
vendored
24
.github/workflows/backend-tests.yml
vendored
|
@ -54,6 +54,12 @@ jobs:
|
|||
-
|
||||
name: Install all dependencies and symlink for ep_etherpad-lite
|
||||
run: bin/installDeps.sh
|
||||
- name: Install admin ui
|
||||
working-directory: admin
|
||||
run: pnpm install
|
||||
- name: Build admin ui
|
||||
working-directory: admin
|
||||
run: pnpm build
|
||||
-
|
||||
name: Run the backend tests
|
||||
run: pnpm test
|
||||
|
@ -105,6 +111,12 @@ jobs:
|
|||
-
|
||||
name: Install all dependencies and symlink for ep_etherpad-lite
|
||||
run: bin/installDeps.sh
|
||||
- name: Install admin ui
|
||||
working-directory: admin
|
||||
run: pnpm install
|
||||
- name: Build admin ui
|
||||
working-directory: admin
|
||||
run: pnpm build
|
||||
-
|
||||
name: Install Etherpad plugins
|
||||
run: >
|
||||
|
@ -163,6 +175,12 @@ jobs:
|
|||
-
|
||||
name: Install all dependencies and symlink for ep_etherpad-lite
|
||||
run: bin/installOnWindows.bat
|
||||
- name: Install admin ui
|
||||
working-directory: admin
|
||||
run: pnpm install
|
||||
- name: Build admin ui
|
||||
working-directory: admin
|
||||
run: pnpm build
|
||||
-
|
||||
name: Fix up the settings.json
|
||||
run: |
|
||||
|
@ -207,6 +225,12 @@ jobs:
|
|||
${{ runner.os }}-pnpm-store-
|
||||
- name: Only install direct dependencies
|
||||
run: pnpm config set auto-install-peers false
|
||||
- name: Install admin ui
|
||||
working-directory: admin
|
||||
run: pnpm install
|
||||
- name: Build admin ui
|
||||
working-directory: admin
|
||||
run: pnpm build
|
||||
-
|
||||
name: Install Etherpad plugins
|
||||
# The --legacy-peer-deps flag is required to work around a bug in npm
|
||||
|
|
5
.github/workflows/frontend-admin-tests.yml
vendored
5
.github/workflows/frontend-admin-tests.yml
vendored
|
@ -12,7 +12,6 @@ jobs:
|
|||
name: with plugins
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# node: [16, 19, 20] >> Disabled node 16 and 18 because they do not work
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
@ -83,11 +82,11 @@ jobs:
|
|||
run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json"
|
||||
-
|
||||
name: increase maxHttpBufferSize
|
||||
run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 100000/' settings.json"
|
||||
run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 10000000/' settings.json"
|
||||
-
|
||||
name: Disable import/export rate limiting
|
||||
run: |
|
||||
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 1000000/' -i settings.json
|
||||
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json
|
||||
-
|
||||
name: Remove standard frontend test files, so only admin tests are run
|
||||
run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -23,4 +23,5 @@ out/
|
|||
/src/bin/etherpad-1.deb
|
||||
/src/bin/node.exe
|
||||
plugin_packages
|
||||
pnpm-lock.yaml
|
||||
pnpm-lock.yaml
|
||||
/src/templates/admin
|
||||
|
|
11
Dockerfile
11
Dockerfile
|
@ -4,6 +4,13 @@
|
|||
#
|
||||
# Author: muxator
|
||||
|
||||
FROM node:alpine as adminBuild
|
||||
|
||||
WORKDIR /opt/etherpad-lite
|
||||
COPY ./admin ./admin
|
||||
RUN cd ./admin && npm install -g pnpm && pnpm install && pnpm run build --outDir ./dist
|
||||
|
||||
|
||||
FROM node:alpine as build
|
||||
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
|
||||
|
||||
|
@ -99,16 +106,18 @@ COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./
|
|||
FROM build as development
|
||||
|
||||
COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/pnpm-lock.yaml ./src/
|
||||
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin
|
||||
|
||||
RUN bin/installDeps.sh && { [ -z "${ETHERPAD_PLUGINS}" ] || \
|
||||
pnpm install --workspace-root ${ETHERPAD_PLUGINS}; }
|
||||
|
||||
|
||||
FROM build as production
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV ETHERPAD_PRODUCTION=true
|
||||
|
||||
COPY --chown=etherpad:etherpad ./src ./src
|
||||
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin
|
||||
|
||||
RUN bin/installDeps.sh && { [ -z "${ETHERPAD_PLUGINS}" ] || \
|
||||
pnpm install --workspace-root ${ETHERPAD_PLUGINS}; } && \
|
||||
|
|
18
admin/.eslintrc.cjs
Normal file
18
admin/.eslintrc.cjs
Normal file
|
@ -0,0 +1,18 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
24
admin/.gitignore
vendored
Normal file
24
admin/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
30
admin/README.md
Normal file
30
admin/README.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
14
admin/index.html
Normal file
14
admin/index.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Etherpad Admin Dashboard</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="loading"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
38
admin/package.json
Normal file
38
admin/package.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"i18next": "^23.10.1",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.56",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-static-copy": "^1.0.1",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
}
|
0
admin/pnpm-workspace.yaml
Normal file
0
admin/pnpm-workspace.yaml
Normal file
28
admin/public/ep_admin_pads/ar.json
Normal file
28
admin/public/ep_admin_pads/ar.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Meno25",
|
||||
"محمد أحمد عبد الفتاح"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "فعل",
|
||||
"ep_adminpads2_autoupdate-label": "التحديث التلقائي على تغييرات الوسادة",
|
||||
"ep_adminpads2_autoupdate.title": "لتمكين أو تعطيل التحديثات التلقائية للاستعلام الحالي.",
|
||||
"ep_adminpads2_confirm": "هل تريد حقًا حذف الوسادة {{padID}}؟",
|
||||
"ep_adminpads2_delete.value": "حذف",
|
||||
"ep_adminpads2_last-edited": "آخر تعديل",
|
||||
"ep_adminpads2_loading": "جارٍ التحميل...",
|
||||
"ep_adminpads2_manage-pads": "إدارة الفوط",
|
||||
"ep_adminpads2_no-results": "لا توجد نتائج.",
|
||||
"ep_adminpads2_pad-user-count": "عدد المستخدمين الوسادة",
|
||||
"ep_adminpads2_padname": "بادنام",
|
||||
"ep_adminpads2_search-box.placeholder": "مصطلح البحث",
|
||||
"ep_adminpads2_search-button.value": "بحث",
|
||||
"ep_adminpads2_search-done": "اكتمل البحث",
|
||||
"ep_adminpads2_search-error-explanation": "واجه الخادم خطأً أثناء البحث عن منصات:",
|
||||
"ep_adminpads2_search-error-title": "فشل في الحصول على قائمة الوسادة",
|
||||
"ep_adminpads2_search-heading": "ابحث عن الفوط",
|
||||
"ep_adminpads2_title": "إدارة الوسادة",
|
||||
"ep_adminpads2_unknown-error": "خطأ غير معروف",
|
||||
"ep_adminpads2_unknown-status": "حالة غير معروفة"
|
||||
}
|
23
admin/public/ep_admin_pads/bn.json
Normal file
23
admin/public/ep_admin_pads/bn.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"আজিজ",
|
||||
"আফতাবুজ্জামান"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "কার্য",
|
||||
"ep_adminpads2_delete.value": "মুছে ফেলুন",
|
||||
"ep_adminpads2_last-edited": "সর্বশেষ সম্পাদিত",
|
||||
"ep_adminpads2_loading": "লোড হচ্ছে...",
|
||||
"ep_adminpads2_manage-pads": "প্যাড পরিচালনা করুন",
|
||||
"ep_adminpads2_no-results": "ফলাফল নেই",
|
||||
"ep_adminpads2_padname": "প্যাডের নাম",
|
||||
"ep_adminpads2_search-button.value": "অনুসন্ধান",
|
||||
"ep_adminpads2_search-done": "অনুসন্ধান সম্পূর্ণ",
|
||||
"ep_adminpads2_search-error-explanation": "প্যাড অনুসন্ধান করার সময় সার্ভার একটি ত্রুটির সম্মুখীন হয়েছে:",
|
||||
"ep_adminpads2_search-error-title": "প্যাডের তালিকা পেতে ব্যর্থ",
|
||||
"ep_adminpads2_search-heading": "প্যাড অনুসন্ধান করুন",
|
||||
"ep_adminpads2_title": "প্যাড প্রশাসন",
|
||||
"ep_adminpads2_unknown-error": "অজানা ত্রুটি",
|
||||
"ep_adminpads2_unknown-status": "অজানা অবস্থা"
|
||||
}
|
27
admin/public/ep_admin_pads/ca.json
Normal file
27
admin/public/ep_admin_pads/ca.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Mguix"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Acció",
|
||||
"ep_adminpads2_autoupdate-label": "Actualització automàtica en cas de canvis de pad",
|
||||
"ep_adminpads2_autoupdate.title": "Activa o desactiva les actualitzacions automàtiques per a la consulta actual.",
|
||||
"ep_adminpads2_confirm": "Esteu segur que voleu suprimir el pad {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Esborrar",
|
||||
"ep_adminpads2_last-edited": "Darrera modificació",
|
||||
"ep_adminpads2_loading": "S’està carregant…",
|
||||
"ep_adminpads2_manage-pads": "Gestiona els pads",
|
||||
"ep_adminpads2_no-results": "No hi ha cap resultat",
|
||||
"ep_adminpads2_pad-user-count": "Nombre d'usuaris de pads",
|
||||
"ep_adminpads2_padname": "Nom del pad",
|
||||
"ep_adminpads2_search-box.placeholder": "Terme de cerca",
|
||||
"ep_adminpads2_search-button.value": "Cercar",
|
||||
"ep_adminpads2_search-done": "Cerca completa",
|
||||
"ep_adminpads2_search-error-explanation": "El servidor ha trobat un error mentre buscava pads:",
|
||||
"ep_adminpads2_search-error-title": "No s'ha pogut obtenir la llista del pad",
|
||||
"ep_adminpads2_search-heading": "Cerca pads",
|
||||
"ep_adminpads2_title": "Administració del pad",
|
||||
"ep_adminpads2_unknown-error": "Error desconegut",
|
||||
"ep_adminpads2_unknown-status": "Estat desconegut"
|
||||
}
|
27
admin/public/ep_admin_pads/cs.json
Normal file
27
admin/public/ep_admin_pads/cs.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Spotter"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Akce",
|
||||
"ep_adminpads2_autoupdate-label": "Automatická aktualizace změn Padu",
|
||||
"ep_adminpads2_autoupdate.title": "Povolí nebo zakáže automatické aktualizace pro aktuální dotaz.",
|
||||
"ep_adminpads2_confirm": "Opravdu chcete odstranit pad {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Smazat",
|
||||
"ep_adminpads2_last-edited": "Naposledy upraveno",
|
||||
"ep_adminpads2_loading": "Načítání…",
|
||||
"ep_adminpads2_manage-pads": "Spravovat pady",
|
||||
"ep_adminpads2_no-results": "Žádné výsledky",
|
||||
"ep_adminpads2_pad-user-count": "Počet uživatelů padu",
|
||||
"ep_adminpads2_padname": "Název padu",
|
||||
"ep_adminpads2_search-box.placeholder": "Hledaný výraz",
|
||||
"ep_adminpads2_search-button.value": "Hledat",
|
||||
"ep_adminpads2_search-done": "Hledání dokončeno",
|
||||
"ep_adminpads2_search-error-explanation": "Při hledání padů došlo k chybě serveru:",
|
||||
"ep_adminpads2_search-error-title": "Seznam padů se nepodařilo získat",
|
||||
"ep_adminpads2_search-heading": "Hledat pady",
|
||||
"ep_adminpads2_title": "Správa Padu",
|
||||
"ep_adminpads2_unknown-error": "Neznámá chyba",
|
||||
"ep_adminpads2_unknown-status": "Neznámý stav"
|
||||
}
|
27
admin/public/ep_admin_pads/cy.json
Normal file
27
admin/public/ep_admin_pads/cy.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Robin Owain"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Gweithred",
|
||||
"ep_adminpads2_autoupdate-label": "Diweddaru newidiadau pad yn otomatig",
|
||||
"ep_adminpads2_autoupdate.title": "Galluogi neu analluogi diweddaru'r ymholiad cyfredol.",
|
||||
"ep_adminpads2_confirm": "Siwr eich bod am ddileu'r pad {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Dileu",
|
||||
"ep_adminpads2_last-edited": "Golygwyd ddiwethaf",
|
||||
"ep_adminpads2_loading": "Wrthi'n llwytho...",
|
||||
"ep_adminpads2_manage-pads": "Rheoli'r padiau",
|
||||
"ep_adminpads2_no-results": "Dim canlyniad",
|
||||
"ep_adminpads2_pad-user-count": "Cyfri defnyddiwr pad",
|
||||
"ep_adminpads2_padname": "Enwpad",
|
||||
"ep_adminpads2_search-box.placeholder": "Term chwilio",
|
||||
"ep_adminpads2_search-button.value": "Chwilio",
|
||||
"ep_adminpads2_search-done": "Wedi gorffen",
|
||||
"ep_adminpads2_search-error-explanation": "Nam ar y gweinydd wrth chwilio'r padiau:",
|
||||
"ep_adminpads2_search-error-title": "Methwyd a chael y rhestr pad",
|
||||
"ep_adminpads2_search-heading": "Chwilio am badiau",
|
||||
"ep_adminpads2_title": "Gweinyddiaeth y pad",
|
||||
"ep_adminpads2_unknown-error": "Nam o ryw fath",
|
||||
"ep_adminpads2_unknown-status": "Statws anhysbys"
|
||||
}
|
14
admin/public/ep_admin_pads/da.json
Normal file
14
admin/public/ep_admin_pads/da.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Saederup92"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Handling",
|
||||
"ep_adminpads2_delete.value": "Slet",
|
||||
"ep_adminpads2_last-edited": "Sidst redigeret",
|
||||
"ep_adminpads2_loading": "Indlæser...",
|
||||
"ep_adminpads2_no-results": "Ingen resultater",
|
||||
"ep_adminpads2_unknown-error": "Ukendt fejl",
|
||||
"ep_adminpads2_unknown-status": "Ukendt status"
|
||||
}
|
32
admin/public/ep_admin_pads/de.json
Normal file
32
admin/public/ep_admin_pads/de.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Brettchenweber",
|
||||
"Justman10000",
|
||||
"Lorisobi",
|
||||
"SamTV",
|
||||
"Umlaut",
|
||||
"Zunkelty"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Aktion",
|
||||
"ep_adminpads2_autoupdate-label": "Automatisch bei Pad-Änderungen updaten",
|
||||
"ep_adminpads2_autoupdate.title": "Aktiviert oder deaktiviert automatische Aktualisierungen für die aktuelle Abfrage.",
|
||||
"ep_adminpads2_confirm": "Willst du das Pad {{padID}} wirklich löschen?",
|
||||
"ep_adminpads2_delete.value": "Löschen",
|
||||
"ep_adminpads2_last-edited": "Zuletzt bearbeitet",
|
||||
"ep_adminpads2_loading": "Lädt...",
|
||||
"ep_adminpads2_manage-pads": "Pads verwalten",
|
||||
"ep_adminpads2_no-results": "Keine Ergebnisse",
|
||||
"ep_adminpads2_pad-user-count": "Nutzerzahl des Pads",
|
||||
"ep_adminpads2_padname": "Padname",
|
||||
"ep_adminpads2_search-box.placeholder": "Suchbegriff",
|
||||
"ep_adminpads2_search-button.value": "Suche",
|
||||
"ep_adminpads2_search-done": "Suche vollendet",
|
||||
"ep_adminpads2_search-error-explanation": "Der Server ist bei der Suche nach Pads auf einen Fehler gestoßen:",
|
||||
"ep_adminpads2_search-error-title": "Pad-Liste konnte nicht abgerufen werden",
|
||||
"ep_adminpads2_search-heading": "Nach Pads suchen",
|
||||
"ep_adminpads2_title": "Pad-Verwaltung",
|
||||
"ep_adminpads2_unknown-error": "Unbekannter Fehler",
|
||||
"ep_adminpads2_unknown-status": "Unbekannter Status"
|
||||
}
|
28
admin/public/ep_admin_pads/diq.json
Normal file
28
admin/public/ep_admin_pads/diq.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"1917 Ekim Devrimi",
|
||||
"Mirzali"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Hereketi",
|
||||
"ep_adminpads2_autoupdate-label": "Vurnayışanê pedi otomatik rocane kerê",
|
||||
"ep_adminpads2_autoupdate.title": "Persê mewcudi rê rocaneyışanê otomatika aktiv ke ya zi dewrê ra vecê",
|
||||
"ep_adminpads2_confirm": "Şıma qayılê pedê {{padID}} bıesternê?",
|
||||
"ep_adminpads2_delete.value": "Bestere",
|
||||
"ep_adminpads2_last-edited": "Vurnayışo peyên",
|
||||
"ep_adminpads2_loading": "Bar beno...",
|
||||
"ep_adminpads2_manage-pads": "Pedan idare kerê",
|
||||
"ep_adminpads2_no-results": "Netice çıniyo",
|
||||
"ep_adminpads2_pad-user-count": "Amarê karberanê pedi",
|
||||
"ep_adminpads2_padname": "Padname",
|
||||
"ep_adminpads2_search-box.placeholder": "termê cıgêrayış",
|
||||
"ep_adminpads2_search-button.value": "Cı geyre",
|
||||
"ep_adminpads2_search-done": "Cıgeyrayışi temam",
|
||||
"ep_adminpads2_search-error-explanation": "Server cıgeyrayışê pedan de yew xetaya raşt ame",
|
||||
"ep_adminpads2_search-error-title": "Lista pedi nêgêriye",
|
||||
"ep_adminpads2_search-heading": "Pedan cıgeyrayış",
|
||||
"ep_adminpads2_title": "İdarey pedi",
|
||||
"ep_adminpads2_unknown-error": "Xetaya nêzanıtiye",
|
||||
"ep_adminpads2_unknown-status": "Weziyeto nêzanaye"
|
||||
}
|
27
admin/public/ep_admin_pads/dsb.json
Normal file
27
admin/public/ep_admin_pads/dsb.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Michawiki"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Akcija",
|
||||
"ep_adminpads2_autoupdate-label": "Pśi změnach na zapisniku awtomatiski aktualizěrowaś",
|
||||
"ep_adminpads2_autoupdate.title": "Zmóžnja abo znjemóžnja awtomatiske aktualizacije za aktualne wótpšašowanje.",
|
||||
"ep_adminpads2_confirm": "Cośo napšawdu zapisnik {{padID}} lašowaś?",
|
||||
"ep_adminpads2_delete.value": "Lašowaś",
|
||||
"ep_adminpads2_last-edited": "Slědna změna",
|
||||
"ep_adminpads2_loading": "Zacytujo se...",
|
||||
"ep_adminpads2_manage-pads": "Zapisniki zastojaś",
|
||||
"ep_adminpads2_no-results": "Žedne wuslědki",
|
||||
"ep_adminpads2_pad-user-count": "Licba wužywarjow zapisnika",
|
||||
"ep_adminpads2_padname": "Mě zapisnika",
|
||||
"ep_adminpads2_search-box.placeholder": "Pytańske zapśimjeśe",
|
||||
"ep_adminpads2_search-button.value": "Pytaś",
|
||||
"ep_adminpads2_search-done": "Pytanje dokóńcone",
|
||||
"ep_adminpads2_search-error-explanation": "Serwer jo starcył na zmólku, mjaztym až jo pytał za zapisnikami:",
|
||||
"ep_adminpads2_search-error-title": "Lisćina zapisnikow njedajo se wobstaraś",
|
||||
"ep_adminpads2_search-heading": "Za zapisnikami pytaś",
|
||||
"ep_adminpads2_title": "Zapisnikowa administracija",
|
||||
"ep_adminpads2_unknown-error": "Njeznata zmólka",
|
||||
"ep_adminpads2_unknown-status": "Njeznaty status"
|
||||
}
|
16
admin/public/ep_admin_pads/el.json
Normal file
16
admin/public/ep_admin_pads/el.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Norhorn"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_delete.value": "Διαγραφή",
|
||||
"ep_adminpads2_last-edited": "Τελευταία απεξεργασία",
|
||||
"ep_adminpads2_loading": "Φόρτωση…",
|
||||
"ep_adminpads2_no-results": "Κανένα αποτέλεσμα",
|
||||
"ep_adminpads2_search-box.placeholder": "Αναζήτηση όρων",
|
||||
"ep_adminpads2_search-button.value": "Αναζήτηση",
|
||||
"ep_adminpads2_search-done": "Ολοκλήρωση αναζήτησης",
|
||||
"ep_adminpads2_unknown-error": "Άγνωστο σφάλμα",
|
||||
"ep_adminpads2_unknown-status": "Άγνωστη κατάσταση"
|
||||
}
|
22
admin/public/ep_admin_pads/en.json
Normal file
22
admin/public/ep_admin_pads/en.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"ep_adminpads2_action": "Action",
|
||||
"ep_adminpads2_autoupdate-label": "Auto-update on pad changes",
|
||||
"ep_adminpads2_autoupdate.title": "Enables or disables automatic updates for the current query.",
|
||||
"ep_adminpads2_confirm": "Do you really want to delete the pad {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Delete",
|
||||
"ep_adminpads2_last-edited": "Last edited",
|
||||
"ep_adminpads2_loading": "Loading…",
|
||||
"ep_adminpads2_manage-pads": "Manage pads",
|
||||
"ep_adminpads2_no-results": "No results",
|
||||
"ep_adminpads2_pad-user-count": "Pad user count",
|
||||
"ep_adminpads2_padname": "Padname",
|
||||
"ep_adminpads2_search-box.placeholder": "Search term",
|
||||
"ep_adminpads2_search-button.value": "Search",
|
||||
"ep_adminpads2_search-done": "Search complete",
|
||||
"ep_adminpads2_search-error-explanation": "The server encountered an error while searching for pads:",
|
||||
"ep_adminpads2_search-error-title": "Failed to get pad list",
|
||||
"ep_adminpads2_search-heading": "Search for pads",
|
||||
"ep_adminpads2_title": "Pad administration",
|
||||
"ep_adminpads2_unknown-error": "Unknown error",
|
||||
"ep_adminpads2_unknown-status": "Unknown status"
|
||||
}
|
27
admin/public/ep_admin_pads/eu.json
Normal file
27
admin/public/ep_admin_pads/eu.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Izendegi"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Ekintza",
|
||||
"ep_adminpads2_autoupdate-label": "Automatikoki eguneratu pad-aren aldaketak daudenean",
|
||||
"ep_adminpads2_autoupdate.title": "Oraingo kontsultarako eguneratze automatikoak gaitu edo desgaitzen du.",
|
||||
"ep_adminpads2_confirm": "Ziur zaude {{padID}} pad-a ezabatu nahi duzula?",
|
||||
"ep_adminpads2_delete.value": "Ezabatu",
|
||||
"ep_adminpads2_last-edited": "Azkenengoz editatua",
|
||||
"ep_adminpads2_loading": "Kargatzen...",
|
||||
"ep_adminpads2_manage-pads": "Kudeatu pad-ak",
|
||||
"ep_adminpads2_no-results": "Emaitzarik ez",
|
||||
"ep_adminpads2_pad-user-count": "Pad-erabiltzaile kopurua",
|
||||
"ep_adminpads2_padname": "Pad-izena",
|
||||
"ep_adminpads2_search-box.placeholder": "Bilaketa testua",
|
||||
"ep_adminpads2_search-button.value": "Bilatu",
|
||||
"ep_adminpads2_search-done": "Bilaketa osatu da",
|
||||
"ep_adminpads2_search-error-explanation": "Zerbitzariak errore bat izan du pad-ak bilatzean:",
|
||||
"ep_adminpads2_search-error-title": "Pad-zerrenda eskuratzeak huts egin du",
|
||||
"ep_adminpads2_search-heading": "Bilatu pad-ak",
|
||||
"ep_adminpads2_title": "Pad-en kudeaketa",
|
||||
"ep_adminpads2_unknown-error": "Errore ezezaguna",
|
||||
"ep_adminpads2_unknown-status": "Egoera ezezaguna"
|
||||
}
|
27
admin/public/ep_admin_pads/ff.json
Normal file
27
admin/public/ep_admin_pads/ff.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ibrahima Malal Sarr"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Baɗal",
|
||||
"ep_adminpads2_autoupdate-label": "Hesɗitin e jaajol tuma baylagol faɗo",
|
||||
"ep_adminpads2_autoupdate.title": "Hurminat walla daaƴa kesɗitine jaaje wonannde ɗaɓɓitannde wonaande.",
|
||||
"ep_adminpads2_confirm": "Aɗa yiɗi e jaati momtude faɗo {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Momtu",
|
||||
"ep_adminpads2_last-edited": "Taƴtaa sakket",
|
||||
"ep_adminpads2_loading": "Nana loowa…",
|
||||
"ep_adminpads2_manage-pads": "Toppito paɗe",
|
||||
"ep_adminpads2_no-results": "Alaa njaltudi",
|
||||
"ep_adminpads2_pad-user-count": "Limoore huutorɓe faɗo",
|
||||
"ep_adminpads2_padname": "Innde faɗo",
|
||||
"ep_adminpads2_search-box.placeholder": "Helmere njiilaw",
|
||||
"ep_adminpads2_search-button.value": "Yiylo",
|
||||
"ep_adminpads2_search-done": "Njiylaw timmii",
|
||||
"ep_adminpads2_search-error-explanation": "Sarworde ndee hawrii e juumre tuma nde yiylotoo faɗo:",
|
||||
"ep_adminpads2_search-error-title": "Horiima heɓde doggol paɗe",
|
||||
"ep_adminpads2_search-heading": "Yiylo paɗe",
|
||||
"ep_adminpads2_title": "Yiylorde paɗe",
|
||||
"ep_adminpads2_unknown-error": "Juumre nde anndaaka",
|
||||
"ep_adminpads2_unknown-status": "Ngonka ka anndaaka"
|
||||
}
|
27
admin/public/ep_admin_pads/fi.json
Normal file
27
admin/public/ep_admin_pads/fi.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Artnay",
|
||||
"Kyykaarme",
|
||||
"MITO",
|
||||
"Maantietäjä",
|
||||
"Yupik"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Toiminto",
|
||||
"ep_adminpads2_delete.value": "Poista",
|
||||
"ep_adminpads2_last-edited": "Viimeksi muokattu",
|
||||
"ep_adminpads2_loading": "Ladataan...",
|
||||
"ep_adminpads2_manage-pads": "Hallitse muistioita",
|
||||
"ep_adminpads2_no-results": "Ei tuloksia",
|
||||
"ep_adminpads2_pad-user-count": "Pad-käyttäjien määrä",
|
||||
"ep_adminpads2_padname": "Muistion nimi",
|
||||
"ep_adminpads2_search-box.placeholder": "Haettava teksti",
|
||||
"ep_adminpads2_search-button.value": "Etsi",
|
||||
"ep_adminpads2_search-done": "Haku valmis",
|
||||
"ep_adminpads2_search-error-explanation": "Palvelimessa tapahtui virhe etsiessään muistioita:",
|
||||
"ep_adminpads2_search-error-title": "Pad-luettelon hakeminen epäonnistui",
|
||||
"ep_adminpads2_search-heading": "Etsi sisältöä",
|
||||
"ep_adminpads2_unknown-error": "Tuntematon virhe",
|
||||
"ep_adminpads2_unknown-status": "Tuntematon tila"
|
||||
}
|
27
admin/public/ep_admin_pads/fr.json
Normal file
27
admin/public/ep_admin_pads/fr.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Verdy p"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Action",
|
||||
"ep_adminpads2_autoupdate-label": "Mise à jour automatique en cas de changements du bloc-notes",
|
||||
"ep_adminpads2_autoupdate.title": "Active ou désactive les mises à jour automatiques pour la requête actuelle.",
|
||||
"ep_adminpads2_confirm": "Voulez-vous vraiment supprimer le bloc-notes {{padID}} ?",
|
||||
"ep_adminpads2_delete.value": "Supprimer",
|
||||
"ep_adminpads2_last-edited": "Dernière modification",
|
||||
"ep_adminpads2_loading": "Chargement en cours...",
|
||||
"ep_adminpads2_manage-pads": "Gérer les bloc-notes",
|
||||
"ep_adminpads2_no-results": "Aucun résultat",
|
||||
"ep_adminpads2_pad-user-count": "Nombre d’utilisateurs du bloc-notes",
|
||||
"ep_adminpads2_padname": "Nom du bloc-notes",
|
||||
"ep_adminpads2_search-box.placeholder": "Terme de recherche",
|
||||
"ep_adminpads2_search-button.value": "Rechercher",
|
||||
"ep_adminpads2_search-done": "Recherche terminée",
|
||||
"ep_adminpads2_search-error-explanation": "Le serveur a rencontré une erreur en cherchant des blocs-notes :",
|
||||
"ep_adminpads2_search-error-title": "Échec d’obtention de la liste de blocs-notes",
|
||||
"ep_adminpads2_search-heading": "Rechercher des blocs-notes",
|
||||
"ep_adminpads2_title": "Administration du bloc-notes",
|
||||
"ep_adminpads2_unknown-error": "Erreur inconnue",
|
||||
"ep_adminpads2_unknown-status": "État inconnu"
|
||||
}
|
27
admin/public/ep_admin_pads/gl.json
Normal file
27
admin/public/ep_admin_pads/gl.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ghose"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Accións",
|
||||
"ep_adminpads2_autoupdate-label": "Actualización automática dos cambios",
|
||||
"ep_adminpads2_autoupdate.title": "Activa ou desactiva as actualizacións automáticas para a consulta actual.",
|
||||
"ep_adminpads2_confirm": "Tes a certeza de querer eliminar o pad {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Eliminar",
|
||||
"ep_adminpads2_last-edited": "Última edición",
|
||||
"ep_adminpads2_loading": "Cargando…",
|
||||
"ep_adminpads2_manage-pads": "Xestionar pads",
|
||||
"ep_adminpads2_no-results": "Sen resultados",
|
||||
"ep_adminpads2_pad-user-count": "Usuarias neste pad",
|
||||
"ep_adminpads2_padname": "Nome do pad",
|
||||
"ep_adminpads2_search-box.placeholder": "Buscar termo",
|
||||
"ep_adminpads2_search-button.value": "Buscar",
|
||||
"ep_adminpads2_search-done": "Busca completa",
|
||||
"ep_adminpads2_search-error-explanation": "O servidor atopou un fallo cando buscaba pads:",
|
||||
"ep_adminpads2_search-error-title": "Non se obtivo a lista de pads",
|
||||
"ep_adminpads2_search-heading": "Buscar pads",
|
||||
"ep_adminpads2_title": "Administración do pad",
|
||||
"ep_adminpads2_unknown-error": "Erro descoñecido",
|
||||
"ep_adminpads2_unknown-status": "Estado descoñecido"
|
||||
}
|
27
admin/public/ep_admin_pads/he.json
Normal file
27
admin/public/ep_admin_pads/he.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"YaronSh"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "פעולה",
|
||||
"ep_adminpads2_autoupdate-label": "לעדכן אוטומטית כשהמחברת נערכת",
|
||||
"ep_adminpads2_autoupdate.title": "הפעלה או השבתה של עדכונים אוטומטיים לשאילתה הנוכחית.",
|
||||
"ep_adminpads2_confirm": "למחוק את המחברת {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "מחיקה",
|
||||
"ep_adminpads2_last-edited": "עריכה אחרונה",
|
||||
"ep_adminpads2_loading": "בטעינה…",
|
||||
"ep_adminpads2_manage-pads": "ניהול מחברות",
|
||||
"ep_adminpads2_no-results": "אין תוצאות",
|
||||
"ep_adminpads2_pad-user-count": "ספירת משתמשים במחברת",
|
||||
"ep_adminpads2_padname": "שם המחברת",
|
||||
"ep_adminpads2_search-box.placeholder": "הביטוי לחיפוש",
|
||||
"ep_adminpads2_search-button.value": "חיפוש",
|
||||
"ep_adminpads2_search-done": "החיפוש הושלם",
|
||||
"ep_adminpads2_search-error-explanation": "השרת נתקל בשגיאה בעת חיפוש מחברות:",
|
||||
"ep_adminpads2_search-error-title": "קבלת רשימת המחברות נכשלה",
|
||||
"ep_adminpads2_search-heading": "חיפוש אחר מחברות",
|
||||
"ep_adminpads2_title": "ניהול מחברות",
|
||||
"ep_adminpads2_unknown-error": "שגיאה בלתי־ידועה",
|
||||
"ep_adminpads2_unknown-status": "מצב לא ידוע"
|
||||
}
|
27
admin/public/ep_admin_pads/hsb.json
Normal file
27
admin/public/ep_admin_pads/hsb.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Michawiki"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Akcija",
|
||||
"ep_adminpads2_autoupdate-label": "Při změnach na zapisniku awtomatisce aktualizować",
|
||||
"ep_adminpads2_autoupdate.title": "Zmóžnja abo znjemóžnja awtomatiske aktualizacije za aktualne wotprašowanje.",
|
||||
"ep_adminpads2_confirm": "Chceće woprawdźe zapisnik {{padID}} zhašeć?",
|
||||
"ep_adminpads2_delete.value": "Zhašeć",
|
||||
"ep_adminpads2_last-edited": "Poslednja změna",
|
||||
"ep_adminpads2_loading": "Začituje so...",
|
||||
"ep_adminpads2_manage-pads": "Zapisniki rjadować",
|
||||
"ep_adminpads2_no-results": "Žane wuslědki.",
|
||||
"ep_adminpads2_pad-user-count": "Ličba wužiwarjow zapisnika",
|
||||
"ep_adminpads2_padname": "Mjeno zapisnika",
|
||||
"ep_adminpads2_search-box.placeholder": "Pytanske zapřijeće",
|
||||
"ep_adminpads2_search-button.value": "Pytać",
|
||||
"ep_adminpads2_search-done": "Pytanje dokónčene",
|
||||
"ep_adminpads2_search-error-explanation": "Serwer je na zmylk storčił, mjeztym zo je za zapisnikami pytał:",
|
||||
"ep_adminpads2_search-error-title": "Lisćina zapisnikow njeda so wobstarać",
|
||||
"ep_adminpads2_search-heading": "Za zapisnikami pytać",
|
||||
"ep_adminpads2_title": "Zapisnikowa administracija",
|
||||
"ep_adminpads2_unknown-error": "Njeznaty zmylk",
|
||||
"ep_adminpads2_unknown-status": "Njeznaty status"
|
||||
}
|
25
admin/public/ep_admin_pads/hu.json
Normal file
25
admin/public/ep_admin_pads/hu.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": []
|
||||
},
|
||||
"ep_adminpads2_action": "Művelet",
|
||||
"ep_adminpads2_autoupdate-label": "Változáskor jegyzetfüzet önműködő frissítése",
|
||||
"ep_adminpads2_autoupdate.title": "Önműködő frissítése az jelenlegi lekérdezéshez be- vagy kikapcsolása.",
|
||||
"ep_adminpads2_confirm": "Biztosan törölni szeretné a(z) {{padID}} jegyzetfüzetet?",
|
||||
"ep_adminpads2_delete.value": "Törlés",
|
||||
"ep_adminpads2_last-edited": "Utoljára szerkesztve",
|
||||
"ep_adminpads2_loading": "Betöltés folyamatban…",
|
||||
"ep_adminpads2_manage-pads": "Jegyzetfüzetek kezelése",
|
||||
"ep_adminpads2_no-results": "Nincs találat",
|
||||
"ep_adminpads2_pad-user-count": "Jegyzetfüzet felhasználók száma",
|
||||
"ep_adminpads2_padname": "Jegyzetfüzet név",
|
||||
"ep_adminpads2_search-box.placeholder": "Keresési kifejezés",
|
||||
"ep_adminpads2_search-button.value": "Keresés",
|
||||
"ep_adminpads2_search-done": "Keresés befejezve",
|
||||
"ep_adminpads2_search-error-explanation": "A kiszolgáló hibát észlelt a jegyzetfüzetek keresésekor:",
|
||||
"ep_adminpads2_search-error-title": "Nem sikerült lekérni a jegyzetfüzet listát",
|
||||
"ep_adminpads2_search-heading": "Jegyzetfüzetek keresése",
|
||||
"ep_adminpads2_title": "Jegyzetfüzet felügyelete",
|
||||
"ep_adminpads2_unknown-error": "Ismeretlen hiba",
|
||||
"ep_adminpads2_unknown-status": "Ismeretlen állapot"
|
||||
}
|
27
admin/public/ep_admin_pads/ia.json
Normal file
27
admin/public/ep_admin_pads/ia.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"McDutchie"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Action",
|
||||
"ep_adminpads2_autoupdate-label": "Actualisar automaticamente le pad in caso de cambiamentos",
|
||||
"ep_adminpads2_autoupdate.title": "Activa o disactiva le actualisationes automatic pro le consulta actual.",
|
||||
"ep_adminpads2_confirm": "Es tu secur de voler deler le pad {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Deler",
|
||||
"ep_adminpads2_last-edited": "Ultime modification",
|
||||
"ep_adminpads2_loading": "Cargamento in curso…",
|
||||
"ep_adminpads2_manage-pads": "Gerer pads",
|
||||
"ep_adminpads2_no-results": "Nulle resultato",
|
||||
"ep_adminpads2_pad-user-count": "Numero de usatores del pad",
|
||||
"ep_adminpads2_padname": "Nomine del pad",
|
||||
"ep_adminpads2_search-box.placeholder": "Termino de recerca",
|
||||
"ep_adminpads2_search-button.value": "Cercar",
|
||||
"ep_adminpads2_search-done": "Recerca terminate",
|
||||
"ep_adminpads2_search-error-explanation": "Le servitor ha incontrate un error durante le recerca de pads:",
|
||||
"ep_adminpads2_search-error-title": "Non poteva obtener le lista de pads",
|
||||
"ep_adminpads2_search-heading": "Cercar pads",
|
||||
"ep_adminpads2_title": "Administration de pads",
|
||||
"ep_adminpads2_unknown-error": "Error incognite",
|
||||
"ep_adminpads2_unknown-status": "Stato incognite"
|
||||
}
|
16
admin/public/ep_admin_pads/it.json
Normal file
16
admin/public/ep_admin_pads/it.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Beta16",
|
||||
"Luca.favorido"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Azione",
|
||||
"ep_adminpads2_delete.value": "Cancella",
|
||||
"ep_adminpads2_last-edited": "Ultima modifica",
|
||||
"ep_adminpads2_loading": "Caricamento…",
|
||||
"ep_adminpads2_no-results": "Nessun risultato",
|
||||
"ep_adminpads2_search-button.value": "Cerca",
|
||||
"ep_adminpads2_unknown-error": "Errore sconosciuto",
|
||||
"ep_adminpads2_unknown-status": "Stato sconosciuto"
|
||||
}
|
13
admin/public/ep_admin_pads/kn.json
Normal file
13
admin/public/ep_admin_pads/kn.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "ಕ್ರಿಯೆ",
|
||||
"ep_adminpads2_delete.value": "ಅಳಿಸು",
|
||||
"ep_adminpads2_loading": "ತುಂಬಿಸಲಾಗುತ್ತಿದೆ…",
|
||||
"ep_adminpads2_no-results": "ಯಾವ ಫಲಿತಾಂಶಗಳೂ ಇಲ್ಲ",
|
||||
"ep_adminpads2_search-button.value": "ಹುಡುಕು",
|
||||
"ep_adminpads2_unknown-error": "ಅಪರಿಚಿತ ದೋಷ"
|
||||
}
|
28
admin/public/ep_admin_pads/ko.json
Normal file
28
admin/public/ep_admin_pads/ko.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ykhwong",
|
||||
"그냥기여자"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "동작",
|
||||
"ep_adminpads2_autoupdate-label": "패드 변경 시 자동 업데이트",
|
||||
"ep_adminpads2_autoupdate.title": "현재 쿼리의 자동 업데이트를 활성화하거나 비활성화합니다.",
|
||||
"ep_adminpads2_confirm": "{{padID}} 패드를 삭제하시겠습니까?",
|
||||
"ep_adminpads2_delete.value": "삭제",
|
||||
"ep_adminpads2_last-edited": "최근 편집",
|
||||
"ep_adminpads2_loading": "불러오는 중...",
|
||||
"ep_adminpads2_manage-pads": "패드 관리",
|
||||
"ep_adminpads2_no-results": "결과 없음",
|
||||
"ep_adminpads2_pad-user-count": "패드 사용자 수",
|
||||
"ep_adminpads2_padname": "패드 이름",
|
||||
"ep_adminpads2_search-box.placeholder": "검색어",
|
||||
"ep_adminpads2_search-button.value": "검색",
|
||||
"ep_adminpads2_search-done": "검색 완료",
|
||||
"ep_adminpads2_search-error-explanation": "패드 검색 중 서버에 오류가 발생했습니다:",
|
||||
"ep_adminpads2_search-error-title": "패드 목록 가져오기 실패",
|
||||
"ep_adminpads2_search-heading": "패드 검색",
|
||||
"ep_adminpads2_title": "패드 관리",
|
||||
"ep_adminpads2_unknown-error": "알 수 없는 오류",
|
||||
"ep_adminpads2_unknown-status": "알 수 없는 상태"
|
||||
}
|
27
admin/public/ep_admin_pads/krc.json
Normal file
27
admin/public/ep_admin_pads/krc.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Къарачайлы"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Этиу",
|
||||
"ep_adminpads2_autoupdate-label": "Блокнот тюрлендириулеринде автомат халда джангыртыу",
|
||||
"ep_adminpads2_autoupdate.title": "Баргъан излем ючюн автомат халда джангыртыуланы джандын неда джукълат.",
|
||||
"ep_adminpads2_confirm": "{{padID}} блокнотну керти да кетерирге излеймисиз?",
|
||||
"ep_adminpads2_delete.value": "Кетер",
|
||||
"ep_adminpads2_last-edited": "Ахыр тюзетиу",
|
||||
"ep_adminpads2_loading": "Джюклениу…",
|
||||
"ep_adminpads2_manage-pads": "Блокнотланы оноуун эт",
|
||||
"ep_adminpads2_no-results": "Эсебле джокъдула",
|
||||
"ep_adminpads2_pad-user-count": "Блокнот хайырланыучуланы саны",
|
||||
"ep_adminpads2_padname": "Блокнот ат",
|
||||
"ep_adminpads2_search-box.placeholder": "Терминни изле",
|
||||
"ep_adminpads2_search-button.value": "Изле",
|
||||
"ep_adminpads2_search-done": "Излеу тамамланды",
|
||||
"ep_adminpads2_search-error-explanation": "Сервер, блокнотланы излеген заманда халат табды:",
|
||||
"ep_adminpads2_search-error-title": "Блокнот тизмеси алынамады",
|
||||
"ep_adminpads2_search-heading": "Блокнотла ючюн излеу",
|
||||
"ep_adminpads2_title": "Блокнот башчылыкъ",
|
||||
"ep_adminpads2_unknown-error": "Билинмеген халат",
|
||||
"ep_adminpads2_unknown-status": "Билинмеген турум"
|
||||
}
|
16
admin/public/ep_admin_pads/lb.json
Normal file
16
admin/public/ep_admin_pads/lb.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Robby",
|
||||
"Volvox"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_confirm": "Wëllt Dir de Pad {{padID}} wierklech läschen?",
|
||||
"ep_adminpads2_delete.value": "Läschen",
|
||||
"ep_adminpads2_loading": "Lueden...",
|
||||
"ep_adminpads2_no-results": "Keng Resultater",
|
||||
"ep_adminpads2_padname": "Padnumm",
|
||||
"ep_adminpads2_search-box.placeholder": "Sichbegrëff",
|
||||
"ep_adminpads2_search-button.value": "Sichen",
|
||||
"ep_adminpads2_unknown-error": "Onbekannte Feeler"
|
||||
}
|
27
admin/public/ep_admin_pads/lt.json
Normal file
27
admin/public/ep_admin_pads/lt.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Nokeoo"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Veiksmas",
|
||||
"ep_adminpads2_autoupdate-label": "Automatinis bloknoto keitimų naujinimas",
|
||||
"ep_adminpads2_autoupdate.title": "Įjungia arba išjungia automatinius dabartinės užklausos atnaujinimus.",
|
||||
"ep_adminpads2_confirm": "Ar tikrai norite ištrinti bloknotą {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Ištrinti",
|
||||
"ep_adminpads2_last-edited": "Paskutinis pakeitimas",
|
||||
"ep_adminpads2_loading": "Įkeliama…",
|
||||
"ep_adminpads2_manage-pads": "Tvarkyti bloknotą",
|
||||
"ep_adminpads2_no-results": "Nėra rezultatų",
|
||||
"ep_adminpads2_pad-user-count": "Bloknoto naudotojų skaičius",
|
||||
"ep_adminpads2_padname": "Bloknoto pavadinimas",
|
||||
"ep_adminpads2_search-box.placeholder": "Paieškos terminas",
|
||||
"ep_adminpads2_search-button.value": "Paieška",
|
||||
"ep_adminpads2_search-done": "Paieška baigta",
|
||||
"ep_adminpads2_search-error-explanation": "Serveris susidūrė su klaida ieškant bloknotų:",
|
||||
"ep_adminpads2_search-error-title": "Nepavyko gauti bloknotų sąrašo",
|
||||
"ep_adminpads2_search-heading": "Ieškokite bloknotų",
|
||||
"ep_adminpads2_title": "Bloknotų administravimas",
|
||||
"ep_adminpads2_unknown-error": "Nežinoma klaida",
|
||||
"ep_adminpads2_unknown-status": "Nežinoma būsena"
|
||||
}
|
27
admin/public/ep_admin_pads/mk.json
Normal file
27
admin/public/ep_admin_pads/mk.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Bjankuloski06"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Дејство",
|
||||
"ep_adminpads2_autoupdate-label": "Самоподнова при измени во тетратката",
|
||||
"ep_adminpads2_autoupdate.title": "Овозможува или оневозможува самоподнова на тековното барање.",
|
||||
"ep_adminpads2_confirm": "Дали навистина сакате да ја избришете тетратката {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Избриши",
|
||||
"ep_adminpads2_last-edited": "Последно уредување",
|
||||
"ep_adminpads2_loading": "Вчитувам…",
|
||||
"ep_adminpads2_manage-pads": "Раководење со тетратки",
|
||||
"ep_adminpads2_no-results": "Нема исход",
|
||||
"ep_adminpads2_pad-user-count": "Корисници на тетратката",
|
||||
"ep_adminpads2_padname": "Назив на тетратката",
|
||||
"ep_adminpads2_search-box.placeholder": "Пребаран поим",
|
||||
"ep_adminpads2_search-button.value": "Пребарај",
|
||||
"ep_adminpads2_search-done": "Пребарувањето заврши",
|
||||
"ep_adminpads2_search-error-explanation": "Опслужувачот наиде на грешка при пребарувањето на тетратки:",
|
||||
"ep_adminpads2_search-error-title": "Не можев да го добијам списокот на тетратки",
|
||||
"ep_adminpads2_search-heading": "Пребарај по тетратките",
|
||||
"ep_adminpads2_title": "Администрација на тетратки",
|
||||
"ep_adminpads2_unknown-error": "Непозната грешка",
|
||||
"ep_adminpads2_unknown-status": "Непозната состојба"
|
||||
}
|
27
admin/public/ep_admin_pads/my.json
Normal file
27
admin/public/ep_admin_pads/my.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Andibecker"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "လုပ်ဆောင်ချက်",
|
||||
"ep_adminpads2_autoupdate-label": "pad အပြောင်းအလဲများတွင်အလိုအလျောက်အပ်ဒိတ်လုပ်ပါ",
|
||||
"ep_adminpads2_autoupdate.title": "လက်ရှိမေးမြန်းမှုအတွက်အလိုအလျောက်အပ်ဒိတ်များကိုဖွင့်ပါသို့မဟုတ်ပိတ်ပါ။",
|
||||
"ep_adminpads2_confirm": "pad {{padID}} ကိုသင်တကယ်ဖျက်ချင်လား။",
|
||||
"ep_adminpads2_delete.value": "ဖျက်ပါ",
|
||||
"ep_adminpads2_last-edited": "နောက်ဆုံးတည်းဖြတ်သည်",
|
||||
"ep_adminpads2_loading": "ဖွင့်နေသည်…",
|
||||
"ep_adminpads2_manage-pads": "pads များကိုစီမံပါ",
|
||||
"ep_adminpads2_no-results": "ရလဒ်မရှိပါ",
|
||||
"ep_adminpads2_pad-user-count": "Pad အသုံးပြုသူအရေအတွက်",
|
||||
"ep_adminpads2_padname": "Padname",
|
||||
"ep_adminpads2_search-box.placeholder": "ဝေါဟာရရှာဖွေပါ",
|
||||
"ep_adminpads2_search-button.value": "ရှာဖွေပါ",
|
||||
"ep_adminpads2_search-done": "ရှာဖွေမှုပြီးပါပြီ",
|
||||
"ep_adminpads2_search-error-explanation": "pads များကိုရှာဖွေစဉ်ဆာဗာသည်အမှားတစ်ခုကြုံခဲ့သည်။",
|
||||
"ep_adminpads2_search-error-title": "pad စာရင်းရယူရန်မအောင်မြင်ပါ",
|
||||
"ep_adminpads2_search-heading": "pads များကိုရှာဖွေပါ",
|
||||
"ep_adminpads2_title": "Pad စီမံခန့်ခွဲမှု",
|
||||
"ep_adminpads2_unknown-error": "အမည်မသိအမှား",
|
||||
"ep_adminpads2_unknown-status": "အခြေအနေမသိ"
|
||||
}
|
13
admin/public/ep_admin_pads/nb.json
Normal file
13
admin/public/ep_admin_pads/nb.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"EdoAug"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Handling",
|
||||
"ep_adminpads2_last-edited": "Sist redigert",
|
||||
"ep_adminpads2_loading": "Laster …",
|
||||
"ep_adminpads2_no-results": "Ingen resultater",
|
||||
"ep_adminpads2_search-button.value": "Søk",
|
||||
"ep_adminpads2_search-done": "Søk fullført"
|
||||
}
|
29
admin/public/ep_admin_pads/nl.json
Normal file
29
admin/public/ep_admin_pads/nl.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Aranka",
|
||||
"McDutchie",
|
||||
"Spinster"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Handeling",
|
||||
"ep_adminpads2_autoupdate-label": "Automatisch bijwerken bij aanpassingen aan de pad",
|
||||
"ep_adminpads2_autoupdate.title": "Schakelt automatische updates voor de huidige query in of uit.",
|
||||
"ep_adminpads2_confirm": "Wil je de pad {{padID}} echt verwijderen?",
|
||||
"ep_adminpads2_delete.value": "Verwijderen",
|
||||
"ep_adminpads2_last-edited": "Laatst bewerkt",
|
||||
"ep_adminpads2_loading": "Bezig met laden...",
|
||||
"ep_adminpads2_manage-pads": "Pads beheren",
|
||||
"ep_adminpads2_no-results": "Geen resultaten",
|
||||
"ep_adminpads2_pad-user-count": "Aantal gebruikers van de pad",
|
||||
"ep_adminpads2_padname": "Naam van de pad",
|
||||
"ep_adminpads2_search-box.placeholder": "Zoekterm",
|
||||
"ep_adminpads2_search-button.value": "Zoeken",
|
||||
"ep_adminpads2_search-done": "Zoekopdracht voltooid",
|
||||
"ep_adminpads2_search-error-explanation": "De server heeft een fout aangetroffen tijdens het zoeken naar pads:",
|
||||
"ep_adminpads2_search-error-title": "Kan lijst met pads niet ophalen",
|
||||
"ep_adminpads2_search-heading": "Pads zoeken",
|
||||
"ep_adminpads2_title": "Administratie van pad",
|
||||
"ep_adminpads2_unknown-error": "Onbekende fout",
|
||||
"ep_adminpads2_unknown-status": "Onbekende status"
|
||||
}
|
21
admin/public/ep_admin_pads/oc.json
Normal file
21
admin/public/ep_admin_pads/oc.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Quentí"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Accion",
|
||||
"ep_adminpads2_delete.value": "Suprimir",
|
||||
"ep_adminpads2_last-edited": "Darrièra edicion",
|
||||
"ep_adminpads2_loading": "Cargament…",
|
||||
"ep_adminpads2_manage-pads": "Gerir los pads",
|
||||
"ep_adminpads2_no-results": "Pas cap de resultat",
|
||||
"ep_adminpads2_padname": "Nom del pad",
|
||||
"ep_adminpads2_search-box.placeholder": "Tèrme de recèrca",
|
||||
"ep_adminpads2_search-button.value": "Recercar",
|
||||
"ep_adminpads2_search-done": "Recèrca acabada",
|
||||
"ep_adminpads2_search-heading": "Cercar de pads",
|
||||
"ep_adminpads2_title": "Administracion de pad",
|
||||
"ep_adminpads2_unknown-error": "Error desconeguda",
|
||||
"ep_adminpads2_unknown-status": "Estat desconegut"
|
||||
}
|
27
admin/public/ep_admin_pads/pms.json
Normal file
27
admin/public/ep_admin_pads/pms.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Borichèt"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Assion",
|
||||
"ep_adminpads2_autoupdate-label": "Agiornament automàtich an sle modìfiche ëd plancia",
|
||||
"ep_adminpads2_autoupdate.title": "Abilité o disabilité j'agiornament automàtich për l'arcesta atual.",
|
||||
"ep_adminpads2_confirm": "Veul-lo për da bon dëscancelé la plancia {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Dëscancelé",
|
||||
"ep_adminpads2_last-edited": "Modificà l'ùltima vira",
|
||||
"ep_adminpads2_loading": "Cariament…",
|
||||
"ep_adminpads2_manage-pads": "Gestì le plance",
|
||||
"ep_adminpads2_no-results": "Gnun arzultà",
|
||||
"ep_adminpads2_pad-user-count": "Conteur ëd plancia dl'utent",
|
||||
"ep_adminpads2_padname": "Nòm ëd plancia",
|
||||
"ep_adminpads2_search-box.placeholder": "Tèrmin d'arserca",
|
||||
"ep_adminpads2_search-button.value": "Arserca",
|
||||
"ep_adminpads2_search-done": "Arserca completà",
|
||||
"ep_adminpads2_search-error-explanation": "Ël servent a l'ha rancontrà n'eror an sërcand dle plance:",
|
||||
"ep_adminpads2_search-error-title": "Falì a oten-e la lista ëd plance",
|
||||
"ep_adminpads2_search-heading": "Arserca ëd plance",
|
||||
"ep_adminpads2_title": "Aministrassion ëd plance",
|
||||
"ep_adminpads2_unknown-error": "Eror nen conossù",
|
||||
"ep_adminpads2_unknown-status": "Statù nen conossù"
|
||||
}
|
30
admin/public/ep_admin_pads/pt-br.json
Normal file
30
admin/public/ep_admin_pads/pt-br.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Duke of Wikipädia",
|
||||
"Eduardo Addad de Oliveira",
|
||||
"Eduardoaddad",
|
||||
"YuriNikolai"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Ação",
|
||||
"ep_adminpads2_autoupdate-label": "Atualizar notas automaticamente",
|
||||
"ep_adminpads2_autoupdate.title": "Habilita ou desabilita atualizações automáticas para a consulta atual.",
|
||||
"ep_adminpads2_confirm": "Você realmente deseja excluir a nota {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Excluir",
|
||||
"ep_adminpads2_last-edited": "Última edição",
|
||||
"ep_adminpads2_loading": "Carregando…",
|
||||
"ep_adminpads2_manage-pads": "Gerenciar notas",
|
||||
"ep_adminpads2_no-results": "Sem resultados",
|
||||
"ep_adminpads2_pad-user-count": "Número de utilizadores na nota",
|
||||
"ep_adminpads2_padname": "Nome da nota",
|
||||
"ep_adminpads2_search-box.placeholder": "Termo de pesquisa",
|
||||
"ep_adminpads2_search-button.value": "Pesquisar",
|
||||
"ep_adminpads2_search-done": "Busca completa",
|
||||
"ep_adminpads2_search-error-explanation": "O servidor encontrou um erro enquanto procurava por notas:",
|
||||
"ep_adminpads2_search-error-title": "Falha ao buscar lista de notas",
|
||||
"ep_adminpads2_search-heading": "Pesquisar por notas",
|
||||
"ep_adminpads2_title": "Administração de notas",
|
||||
"ep_adminpads2_unknown-error": "Erro desconhecido",
|
||||
"ep_adminpads2_unknown-status": "Status desconhecido"
|
||||
}
|
27
admin/public/ep_admin_pads/pt.json
Normal file
27
admin/public/ep_admin_pads/pt.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Guilha"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Ação",
|
||||
"ep_adminpads2_autoupdate-label": "Atualizar automaticamente as notas",
|
||||
"ep_adminpads2_autoupdate.title": "Ativa ou desativa atualizações automáticas na consulta atual.",
|
||||
"ep_adminpads2_confirm": "Tencionas mesmo eliminar a nota {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Eliminar",
|
||||
"ep_adminpads2_last-edited": "Última edição",
|
||||
"ep_adminpads2_loading": "A carregar...",
|
||||
"ep_adminpads2_manage-pads": "Gerir notas",
|
||||
"ep_adminpads2_no-results": "Sem resultados",
|
||||
"ep_adminpads2_pad-user-count": "Número de utilizadores na nota",
|
||||
"ep_adminpads2_padname": "Nome da nota",
|
||||
"ep_adminpads2_search-box.placeholder": "Procurar termo",
|
||||
"ep_adminpads2_search-button.value": "Procurar",
|
||||
"ep_adminpads2_search-done": "Procura completa",
|
||||
"ep_adminpads2_search-error-explanation": "O servidor encontrou um erro enquanto procurava por notas:",
|
||||
"ep_adminpads2_search-error-title": "Falha ao obter lista de notas",
|
||||
"ep_adminpads2_search-heading": "Procurar por notas",
|
||||
"ep_adminpads2_title": "Administração da nota",
|
||||
"ep_adminpads2_unknown-error": "Erro desconhecido",
|
||||
"ep_adminpads2_unknown-status": "Estado desconhecido"
|
||||
}
|
10
admin/public/ep_admin_pads/qqq.json
Normal file
10
admin/public/ep_admin_pads/qqq.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"BryanDavis"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "{{Identical|Action}}",
|
||||
"ep_adminpads2_delete.value": "{{Identical|Delete}}",
|
||||
"ep_adminpads2_search-button.value": "{{Identical|Search}}"
|
||||
}
|
31
admin/public/ep_admin_pads/ru.json
Normal file
31
admin/public/ep_admin_pads/ru.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"DDPAT",
|
||||
"Ice bulldog",
|
||||
"Megakott",
|
||||
"Okras",
|
||||
"Pacha Tchernof"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Действие",
|
||||
"ep_adminpads2_autoupdate-label": "Автообновление при изменении документа",
|
||||
"ep_adminpads2_autoupdate.title": "Включает или отключает автоматические обновления для текущего запроса.",
|
||||
"ep_adminpads2_confirm": "Вы действительно хотите удалить документ {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Удалить",
|
||||
"ep_adminpads2_last-edited": "Последнее изменение",
|
||||
"ep_adminpads2_loading": "Загружается…",
|
||||
"ep_adminpads2_manage-pads": "Управление документами",
|
||||
"ep_adminpads2_no-results": "Нет результатов",
|
||||
"ep_adminpads2_pad-user-count": "Количество пользователей документа",
|
||||
"ep_adminpads2_padname": "Название документа",
|
||||
"ep_adminpads2_search-box.placeholder": "Искать термин",
|
||||
"ep_adminpads2_search-button.value": "Найти",
|
||||
"ep_adminpads2_search-done": "Поиск завершён",
|
||||
"ep_adminpads2_search-error-explanation": "Сервер обнаружил ошибку при поиске документов:",
|
||||
"ep_adminpads2_search-error-title": "Не удалось получить список документов",
|
||||
"ep_adminpads2_search-heading": "Поиск документов",
|
||||
"ep_adminpads2_title": "Администрирование документов",
|
||||
"ep_adminpads2_unknown-error": "Неизвестная ошибка",
|
||||
"ep_adminpads2_unknown-status": "Неизвестный статус"
|
||||
}
|
27
admin/public/ep_admin_pads/sc.json
Normal file
27
admin/public/ep_admin_pads/sc.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Adr mm"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Atzione",
|
||||
"ep_adminpads2_autoupdate-label": "Atualizatzione automàtica de is modìficas de su pad",
|
||||
"ep_adminpads2_autoupdate.title": "Ativat o disativat is atualizatziones automàticas pro sa chirca atuale.",
|
||||
"ep_adminpads2_confirm": "Seguru chi boles cantzellare su pad {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Cantzella",
|
||||
"ep_adminpads2_last-edited": "Ùrtima modìfica",
|
||||
"ep_adminpads2_loading": "Carrighende...",
|
||||
"ep_adminpads2_manage-pads": "Gesti is pads",
|
||||
"ep_adminpads2_no-results": "Nissunu resurtadu",
|
||||
"ep_adminpads2_pad-user-count": "Nùmeru de utentes de pads",
|
||||
"ep_adminpads2_padname": "Nòmine de su pad",
|
||||
"ep_adminpads2_search-box.placeholder": "Tèrmine de chirca",
|
||||
"ep_adminpads2_search-button.value": "Chirca",
|
||||
"ep_adminpads2_search-done": "Chirca cumpleta",
|
||||
"ep_adminpads2_search-error-explanation": "Su serbidore at agatadu un'errore chirchende pads:",
|
||||
"ep_adminpads2_search-error-title": "Impossìbile otènnere sa lista de pads",
|
||||
"ep_adminpads2_search-heading": "Chirca pads",
|
||||
"ep_adminpads2_title": "Amministratzione de su pad",
|
||||
"ep_adminpads2_unknown-error": "Errore disconnotu",
|
||||
"ep_adminpads2_unknown-status": "Istadu disconnotu"
|
||||
}
|
14
admin/public/ep_admin_pads/sdc.json
Normal file
14
admin/public/ep_admin_pads/sdc.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"F Samaritani"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Azioni",
|
||||
"ep_adminpads2_delete.value": "Canzella",
|
||||
"ep_adminpads2_loading": "carrigghendi...",
|
||||
"ep_adminpads2_no-results": "Nisciun risulthaddu",
|
||||
"ep_adminpads2_search-button.value": "Zercha",
|
||||
"ep_adminpads2_search-heading": "Zirchà dati",
|
||||
"ep_adminpads2_unknown-error": "Errori ischunisciddu"
|
||||
}
|
27
admin/public/ep_admin_pads/sk.json
Normal file
27
admin/public/ep_admin_pads/sk.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Yardom78"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Akcia",
|
||||
"ep_adminpads2_autoupdate-label": "Automatická aktualizácia zmien na poznámkovom bloku",
|
||||
"ep_adminpads2_autoupdate.title": "Zapne alebo vypne automatickú aktualizáciu.",
|
||||
"ep_adminpads2_confirm": "Skutočne chcete vymazať poznámkový blok {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Vymazať",
|
||||
"ep_adminpads2_last-edited": "Posledná úprava",
|
||||
"ep_adminpads2_loading": "Načítavanie...",
|
||||
"ep_adminpads2_manage-pads": "Spravovať poznámkové bloky",
|
||||
"ep_adminpads2_no-results": "Žiadne výsledky",
|
||||
"ep_adminpads2_pad-user-count": "Počet používateľov poznámkového bloku",
|
||||
"ep_adminpads2_padname": "Názov poznámkového bloku",
|
||||
"ep_adminpads2_search-box.placeholder": "Hľadať výraz",
|
||||
"ep_adminpads2_search-button.value": "Hľadať",
|
||||
"ep_adminpads2_search-done": "Hľadanie dokončené",
|
||||
"ep_adminpads2_search-error-explanation": "Pri hľadaní poznámkového bloku došlo k chybe:",
|
||||
"ep_adminpads2_search-error-title": "Nepodarilo sa získať zoznam poznámkových blokov",
|
||||
"ep_adminpads2_search-heading": "Hľadať poznámkový blok",
|
||||
"ep_adminpads2_title": "Správa poznámkového bloku",
|
||||
"ep_adminpads2_unknown-error": "Neznáma chyba",
|
||||
"ep_adminpads2_unknown-status": "Neznámy stav"
|
||||
}
|
20
admin/public/ep_admin_pads/skr-arab.json
Normal file
20
admin/public/ep_admin_pads/skr-arab.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Saraiki"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "عمل",
|
||||
"ep_adminpads2_delete.value": "مٹاؤ",
|
||||
"ep_adminpads2_last-edited": "چھیکڑی تبدیلی",
|
||||
"ep_adminpads2_loading": "لوڈ تھین٘دا پئے۔۔۔",
|
||||
"ep_adminpads2_manage-pads": "پیڈ منیج کرو",
|
||||
"ep_adminpads2_no-results": "کوئی نتیجہ کائنی",
|
||||
"ep_adminpads2_padname": "پیڈ ناں",
|
||||
"ep_adminpads2_search-box.placeholder": "ٹرم ڳولو",
|
||||
"ep_adminpads2_search-button.value": "ڳولو",
|
||||
"ep_adminpads2_search-done": "ڳولݨ پورا تھیا",
|
||||
"ep_adminpads2_search-heading": "پیڈاں دی ڳول",
|
||||
"ep_adminpads2_unknown-error": "نامعلوم غلطی",
|
||||
"ep_adminpads2_unknown-status": "نامعلوم حالت"
|
||||
}
|
28
admin/public/ep_admin_pads/sl.json
Normal file
28
admin/public/ep_admin_pads/sl.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Eleassar",
|
||||
"HairyFotr"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Dejanje",
|
||||
"ep_adminpads2_autoupdate-label": "Samodejno posodabljanje ob spremembah blokcev",
|
||||
"ep_adminpads2_autoupdate.title": "Omogoči ali onemogoči samodejne posodobitve za trenutno poizvedbo.",
|
||||
"ep_adminpads2_confirm": "Ali res želite izbrisati blokec {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Izbriši",
|
||||
"ep_adminpads2_last-edited": "Zadnje urejanje",
|
||||
"ep_adminpads2_loading": "Nalaganje ...",
|
||||
"ep_adminpads2_manage-pads": "Upravljanje blokcev",
|
||||
"ep_adminpads2_no-results": "Ni zadetkov",
|
||||
"ep_adminpads2_pad-user-count": "Število urejevalcev blokca",
|
||||
"ep_adminpads2_padname": "Ime blokca",
|
||||
"ep_adminpads2_search-box.placeholder": "Iskalni izraz",
|
||||
"ep_adminpads2_search-button.value": "Išči",
|
||||
"ep_adminpads2_search-done": "Iskanje končano",
|
||||
"ep_adminpads2_search-error-explanation": "Strežnik je med iskanjem blokcev naletel na napako:",
|
||||
"ep_adminpads2_search-error-title": "Ni bilo mogoče pridobiti seznama blokcev",
|
||||
"ep_adminpads2_search-heading": "Iskanje blokcev",
|
||||
"ep_adminpads2_title": "Upravljanje blokcev",
|
||||
"ep_adminpads2_unknown-error": "Neznana napaka",
|
||||
"ep_adminpads2_unknown-status": "Neznano stanje"
|
||||
}
|
13
admin/public/ep_admin_pads/smn.json
Normal file
13
admin/public/ep_admin_pads/smn.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Yupik"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_delete.value": "Siho",
|
||||
"ep_adminpads2_last-edited": "Majemustáá nubástittum",
|
||||
"ep_adminpads2_search-box.placeholder": "Uuccâmsääni",
|
||||
"ep_adminpads2_search-button.value": "Uusâ",
|
||||
"ep_adminpads2_unknown-error": "Tubdâmettum feilâ",
|
||||
"ep_adminpads2_unknown-status": "Tubdâmettum tile"
|
||||
}
|
16
admin/public/ep_admin_pads/sms.json
Normal file
16
admin/public/ep_admin_pads/sms.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Yupik"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_delete.value": "Jaukkâd",
|
||||
"ep_adminpads2_last-edited": "Mââimõssân muttum",
|
||||
"ep_adminpads2_no-results": "Ij käunnʼjam ni mii",
|
||||
"ep_adminpads2_padname": "Mošttʼtõspõʹmmai nõmm",
|
||||
"ep_adminpads2_search-box.placeholder": "Ooccâmsääʹnn",
|
||||
"ep_adminpads2_search-button.value": "Ooʒʒ",
|
||||
"ep_adminpads2_search-heading": "Ooʒʒ mošttʼtõspõʹmmjid",
|
||||
"ep_adminpads2_unknown-error": "Toobdteʹmes vââʹǩǩ",
|
||||
"ep_adminpads2_unknown-status": "Toobdteʹmes status"
|
||||
}
|
27
admin/public/ep_admin_pads/sq.json
Normal file
27
admin/public/ep_admin_pads/sq.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Besnik b"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Veprim",
|
||||
"ep_adminpads2_autoupdate-label": "Vetëpërditësohu, kur nga ndryshime blloku",
|
||||
"ep_adminpads2_autoupdate.title": "Aktivizon ose çaktivizon përditësim të automatizuara për kërkesën e tanishme.",
|
||||
"ep_adminpads2_confirm": "Doni vërtet të fshihet blloku {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Fshije",
|
||||
"ep_adminpads2_last-edited": "Përpunuar së fundi më",
|
||||
"ep_adminpads2_loading": "Po ngarkohet…",
|
||||
"ep_adminpads2_manage-pads": "Administroni blloqe",
|
||||
"ep_adminpads2_no-results": "S’ka përfundime",
|
||||
"ep_adminpads2_pad-user-count": "Numër përdoruesish blloku",
|
||||
"ep_adminpads2_padname": "Emër blloku",
|
||||
"ep_adminpads2_search-box.placeholder": "Term kërkimi",
|
||||
"ep_adminpads2_search-button.value": "Kërko",
|
||||
"ep_adminpads2_search-done": "Kërkim i plotë",
|
||||
"ep_adminpads2_search-error-explanation": "Shërbyesi hasi një gabim teksa kërkohej për blloqe:",
|
||||
"ep_adminpads2_search-error-title": "S’u arrit të merrej listë blloqesh",
|
||||
"ep_adminpads2_search-heading": "Kërkoni për blloqe",
|
||||
"ep_adminpads2_title": "Administrim blloku",
|
||||
"ep_adminpads2_unknown-error": "Gabim i panjohur",
|
||||
"ep_adminpads2_unknown-status": "Gjendje e panjohur"
|
||||
}
|
28
admin/public/ep_admin_pads/sv.json
Normal file
28
admin/public/ep_admin_pads/sv.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Bengtsson96",
|
||||
"WikiPhoenix"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Åtgärd",
|
||||
"ep_adminpads2_autoupdate-label": "Uppdatera automatiskt när blocket ändras",
|
||||
"ep_adminpads2_autoupdate.title": "Aktivera eller inaktivera automatiska uppdatering för nuvarande förfrågan.",
|
||||
"ep_adminpads2_confirm": "Vill du verkligen radera blocket {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Radera",
|
||||
"ep_adminpads2_last-edited": "Senast redigerad",
|
||||
"ep_adminpads2_loading": "Läser in …",
|
||||
"ep_adminpads2_manage-pads": "Hantera block",
|
||||
"ep_adminpads2_no-results": "Inga resultat",
|
||||
"ep_adminpads2_pad-user-count": "Antal blockanvändare",
|
||||
"ep_adminpads2_padname": "Blocknamn",
|
||||
"ep_adminpads2_search-box.placeholder": "Sökord",
|
||||
"ep_adminpads2_search-button.value": "Sök",
|
||||
"ep_adminpads2_search-done": "Sökning slutförd",
|
||||
"ep_adminpads2_search-error-explanation": "Servern stötte på ett fel vid sökning efter block:",
|
||||
"ep_adminpads2_search-error-title": "Misslyckades att hämta blocklista",
|
||||
"ep_adminpads2_search-heading": "Sök efter block",
|
||||
"ep_adminpads2_title": "Blockadministration",
|
||||
"ep_adminpads2_unknown-error": "Okänt fel",
|
||||
"ep_adminpads2_unknown-status": "Okänd status"
|
||||
}
|
27
admin/public/ep_admin_pads/sw.json
Normal file
27
admin/public/ep_admin_pads/sw.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Andibecker"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Hatua",
|
||||
"ep_adminpads2_autoupdate-label": "Sasisha kiotomatiki kwenye mabadiliko ya pedi",
|
||||
"ep_adminpads2_autoupdate.title": "Huwasha au kulemaza sasisho otomatiki kwa hoja ya sasa.",
|
||||
"ep_adminpads2_confirm": "Je! Kweli unataka kufuta pedi {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Futa",
|
||||
"ep_adminpads2_last-edited": "Ilihaririwa mwisho",
|
||||
"ep_adminpads2_loading": "Inapakia...",
|
||||
"ep_adminpads2_manage-pads": "Dhibiti pedi",
|
||||
"ep_adminpads2_no-results": "Hakuna matokeo",
|
||||
"ep_adminpads2_pad-user-count": "Hesabu ya mtumiaji wa pedi",
|
||||
"ep_adminpads2_padname": "Jina la utani",
|
||||
"ep_adminpads2_search-box.placeholder": "Neno la utaftaji",
|
||||
"ep_adminpads2_search-button.value": "Tafuta",
|
||||
"ep_adminpads2_search-done": "Utafutaji umekamilika",
|
||||
"ep_adminpads2_search-error-explanation": "Seva ilipata hitilafu wakati wa kutafuta pedi:",
|
||||
"ep_adminpads2_search-error-title": "Imeshindwa kupata orodha ya pedi",
|
||||
"ep_adminpads2_search-heading": "Tafuta pedi",
|
||||
"ep_adminpads2_title": "Usimamizi wa pedi",
|
||||
"ep_adminpads2_unknown-error": "Hitilafu isiyojulikana",
|
||||
"ep_adminpads2_unknown-status": "Hali isiyojulikana"
|
||||
}
|
27
admin/public/ep_admin_pads/th.json
Normal file
27
admin/public/ep_admin_pads/th.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Andibecker"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "การกระทำ",
|
||||
"ep_adminpads2_autoupdate-label": "อัปเดตอัตโนมัติเมื่อเปลี่ยนแผ่น",
|
||||
"ep_adminpads2_autoupdate.title": "เปิดหรือปิดการอัปเดตอัตโนมัติสำหรับคิวรีปัจจุบัน",
|
||||
"ep_adminpads2_confirm": "คุณต้องการลบแพด {{padID}} จริงหรือไม่",
|
||||
"ep_adminpads2_delete.value": "ลบ",
|
||||
"ep_adminpads2_last-edited": "แก้ไขล่าสุด",
|
||||
"ep_adminpads2_loading": "กำลังโหลด…",
|
||||
"ep_adminpads2_manage-pads": "จัดการแผ่นรอง",
|
||||
"ep_adminpads2_no-results": "ไม่มีผลลัพธ์",
|
||||
"ep_adminpads2_pad-user-count": "จำนวนผู้ใช้แพด",
|
||||
"ep_adminpads2_padname": "นามแฝง",
|
||||
"ep_adminpads2_search-box.placeholder": "คำที่ต้องการค้นหา",
|
||||
"ep_adminpads2_search-button.value": "ค้นหา",
|
||||
"ep_adminpads2_search-done": "ค้นหาเสร็จสมบูรณ์",
|
||||
"ep_adminpads2_search-error-explanation": "เซิร์ฟเวอร์พบข้อผิดพลาดขณะค้นหาแผ่นอิเล็กโทรด:",
|
||||
"ep_adminpads2_search-error-title": "ไม่สามารถรับรายการแผ่นรอง",
|
||||
"ep_adminpads2_search-heading": "ค้นหาแผ่นรอง",
|
||||
"ep_adminpads2_title": "การบริหารแผ่น",
|
||||
"ep_adminpads2_unknown-error": "ข้อผิดพลาดที่ไม่รู้จัก",
|
||||
"ep_adminpads2_unknown-status": "ไม่ทราบสถานะ"
|
||||
}
|
17
admin/public/ep_admin_pads/tl.json
Normal file
17
admin/public/ep_admin_pads/tl.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Mrkczr"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Kilos",
|
||||
"ep_adminpads2_delete.value": "Burahin",
|
||||
"ep_adminpads2_last-edited": "Huling binago",
|
||||
"ep_adminpads2_loading": "Naglo-load...",
|
||||
"ep_adminpads2_no-results": "Walang mga resulta",
|
||||
"ep_adminpads2_search-box.placeholder": "Mga katagang hahanapin:",
|
||||
"ep_adminpads2_search-button.value": "Hanapin",
|
||||
"ep_adminpads2_search-done": "Natapos na ang paghahanap",
|
||||
"ep_adminpads2_unknown-error": "Hindi nalalamang kamalian",
|
||||
"ep_adminpads2_unknown-status": "Hindi alam na katayuan"
|
||||
}
|
28
admin/public/ep_admin_pads/tr.json
Normal file
28
admin/public/ep_admin_pads/tr.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Hedda",
|
||||
"MuratTheTurkish"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Eylem",
|
||||
"ep_adminpads2_autoupdate-label": "Bloknot değişikliklerinde otomatik güncelleme",
|
||||
"ep_adminpads2_autoupdate.title": "Mevcut sorgu için otomatik güncellemeleri etkinleştirir veya devre dışı bırakır.",
|
||||
"ep_adminpads2_confirm": "{{padID}} bloknotunu gerçekten silmek istiyor musunuz?",
|
||||
"ep_adminpads2_delete.value": "Sil",
|
||||
"ep_adminpads2_last-edited": "Son düzenleme",
|
||||
"ep_adminpads2_loading": "Yükleniyor...",
|
||||
"ep_adminpads2_manage-pads": "Bloknotları yönet",
|
||||
"ep_adminpads2_no-results": "Sonuç yok",
|
||||
"ep_adminpads2_pad-user-count": "Bloknot kullanıcı sayısı",
|
||||
"ep_adminpads2_padname": "Bloknot adı",
|
||||
"ep_adminpads2_search-box.placeholder": "Terimi ara",
|
||||
"ep_adminpads2_search-button.value": "Ara",
|
||||
"ep_adminpads2_search-done": "Arama tamamlandı",
|
||||
"ep_adminpads2_search-error-explanation": "Sunucu, bloknotları ararken bir hatayla karşılaştı:",
|
||||
"ep_adminpads2_search-error-title": "Bloknot listesi alınamadı",
|
||||
"ep_adminpads2_search-heading": "Bloknotları ara",
|
||||
"ep_adminpads2_title": "Bloknot yönetimi",
|
||||
"ep_adminpads2_unknown-error": "Bilinmeyen hata",
|
||||
"ep_adminpads2_unknown-status": "Bilinmeyen durum"
|
||||
}
|
28
admin/public/ep_admin_pads/uk.json
Normal file
28
admin/public/ep_admin_pads/uk.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"DDPAT",
|
||||
"Ice bulldog"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "Дія",
|
||||
"ep_adminpads2_autoupdate-label": "Автоматичне оновлення при зміні майданчика",
|
||||
"ep_adminpads2_autoupdate.title": "Вмикає або вимикає автоматичне оновлення поточного запиту.",
|
||||
"ep_adminpads2_confirm": "Ви дійсно хочете видалити панель {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Видалити",
|
||||
"ep_adminpads2_last-edited": "Останнє редагування",
|
||||
"ep_adminpads2_loading": "Завантаження…",
|
||||
"ep_adminpads2_manage-pads": "Управління майданчиками",
|
||||
"ep_adminpads2_no-results": "Немає результатів",
|
||||
"ep_adminpads2_pad-user-count": "Кількість майданчиків користувача",
|
||||
"ep_adminpads2_padname": "Назва майданчика",
|
||||
"ep_adminpads2_search-box.placeholder": "Пошуковий термін",
|
||||
"ep_adminpads2_search-button.value": "Пошук",
|
||||
"ep_adminpads2_search-done": "Пошук завершено",
|
||||
"ep_adminpads2_search-error-explanation": "Під час пошуку педів сервер виявив помилку:",
|
||||
"ep_adminpads2_search-error-title": "Не вдалося отримати список панелей",
|
||||
"ep_adminpads2_search-heading": "Пошук майданчиків",
|
||||
"ep_adminpads2_title": "Введення майданчиків",
|
||||
"ep_adminpads2_unknown-error": "Невідома помилка",
|
||||
"ep_adminpads2_unknown-status": "Невідомий статус"
|
||||
}
|
29
admin/public/ep_admin_pads/zh-hans.json
Normal file
29
admin/public/ep_admin_pads/zh-hans.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"GuoPC",
|
||||
"Lakejason0",
|
||||
"沈澄心"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "操作",
|
||||
"ep_adminpads2_autoupdate-label": "在记事本更改时自动更新",
|
||||
"ep_adminpads2_autoupdate.title": "启用或禁用目前查询的自动更新",
|
||||
"ep_adminpads2_confirm": "您确定要删除记事本 {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "删除",
|
||||
"ep_adminpads2_last-edited": "上次编辑于",
|
||||
"ep_adminpads2_loading": "正在加载…",
|
||||
"ep_adminpads2_manage-pads": "管理记事本",
|
||||
"ep_adminpads2_no-results": "没有结果",
|
||||
"ep_adminpads2_pad-user-count": "记事本用户数",
|
||||
"ep_adminpads2_padname": "记事本名称",
|
||||
"ep_adminpads2_search-box.placeholder": "搜索关键词",
|
||||
"ep_adminpads2_search-button.value": "搜索",
|
||||
"ep_adminpads2_search-done": "搜索完成",
|
||||
"ep_adminpads2_search-error-explanation": "搜索记事本时服务器发生错误:",
|
||||
"ep_adminpads2_search-error-title": "获取记事本列表失败",
|
||||
"ep_adminpads2_search-heading": "搜索记事本",
|
||||
"ep_adminpads2_title": "记事本管理",
|
||||
"ep_adminpads2_unknown-error": "未知错误",
|
||||
"ep_adminpads2_unknown-status": "未知状态"
|
||||
}
|
28
admin/public/ep_admin_pads/zh-hant.json
Normal file
28
admin/public/ep_admin_pads/zh-hant.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"HellojoeAoPS",
|
||||
"Kly"
|
||||
]
|
||||
},
|
||||
"ep_adminpads2_action": "操作",
|
||||
"ep_adminpads2_autoupdate-label": "在記事本更改時自動更新",
|
||||
"ep_adminpads2_autoupdate.title": "啟用或停用目前查詢的自動更新。",
|
||||
"ep_adminpads2_confirm": "您確定要刪除記事本 {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "刪除",
|
||||
"ep_adminpads2_last-edited": "上一次編輯",
|
||||
"ep_adminpads2_loading": "載入中…",
|
||||
"ep_adminpads2_manage-pads": "管理記事本",
|
||||
"ep_adminpads2_no-results": "沒有結果",
|
||||
"ep_adminpads2_pad-user-count": "記事本使用者數",
|
||||
"ep_adminpads2_padname": "記事本名稱",
|
||||
"ep_adminpads2_search-box.placeholder": "搜尋關鍵字",
|
||||
"ep_adminpads2_search-button.value": "搜尋",
|
||||
"ep_adminpads2_search-done": "搜尋完成",
|
||||
"ep_adminpads2_search-error-explanation": "當搜尋記事本時伺服器發生錯誤:",
|
||||
"ep_adminpads2_search-error-title": "取得記事本清單失敗",
|
||||
"ep_adminpads2_search-heading": "搜尋記事本",
|
||||
"ep_adminpads2_title": "記事本管理",
|
||||
"ep_adminpads2_unknown-error": "不明錯誤",
|
||||
"ep_adminpads2_unknown-status": "不明狀態"
|
||||
}
|
BIN
admin/public/fond.jpg
Normal file
BIN
admin/public/fond.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 182 KiB |
0
admin/src/App.css
Normal file
0
admin/src/App.css
Normal file
104
admin/src/App.tsx
Normal file
104
admin/src/App.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import {useEffect} from 'react'
|
||||
import './App.css'
|
||||
import {connect} from 'socket.io-client'
|
||||
import {isJSONClean} from './utils/utils.ts'
|
||||
import {NavLink, Outlet, useNavigate} from "react-router-dom";
|
||||
import {useStore} from "./store/store.ts";
|
||||
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
|
||||
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
|
||||
export const App = ()=> {
|
||||
const setSettings = useStore(state => state.setSettings);
|
||||
const {t} = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/admin-auth/', {
|
||||
method: 'POST'
|
||||
}).then((value)=>{
|
||||
if(!value.ok){
|
||||
navigate('/login')
|
||||
}
|
||||
}).catch(()=>{
|
||||
navigate('/login')
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t('admin.page-title')
|
||||
|
||||
useStore.getState().setShowLoading(true);
|
||||
const settingSocket = connect(`${WS_URL}/settings`, {
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
|
||||
transports: ['websocket'],
|
||||
})
|
||||
|
||||
pluginsSocket.on('connect', () => {
|
||||
useStore.getState().setPluginsSocket(pluginsSocket);
|
||||
});
|
||||
|
||||
|
||||
settingSocket.on('connect', () => {
|
||||
useStore.getState().setSettingsSocket(settingSocket);
|
||||
useStore.getState().setShowLoading(false)
|
||||
settingSocket.emit('load');
|
||||
console.log('connected');
|
||||
});
|
||||
|
||||
settingSocket.on('disconnect', (reason) => {
|
||||
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
|
||||
// server disconnect".
|
||||
useStore.getState().setShowLoading(true)
|
||||
if (reason === 'io server disconnect') {
|
||||
settingSocket.connect();
|
||||
}
|
||||
});
|
||||
|
||||
settingSocket.on('settings', (settings) => {
|
||||
/* Check whether the settings.json is authorized to be viewed */
|
||||
if (settings.results === 'NOT_ALLOWED') {
|
||||
console.log('Not allowed to view settings.json')
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check to make sure the JSON is clean before proceeding */
|
||||
if (isJSONClean(settings.results)) {
|
||||
setSettings(settings.results);
|
||||
} else {
|
||||
alert('Invalid JSON');
|
||||
}
|
||||
useStore.getState().setShowLoading(false);
|
||||
});
|
||||
|
||||
settingSocket.on('saveprogress', (status)=>{
|
||||
console.log(status)
|
||||
})
|
||||
|
||||
return () => {
|
||||
settingSocket.disconnect();
|
||||
pluginsSocket.disconnect()
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div id="wrapper">
|
||||
<LoadingScreen/>
|
||||
<div className="menu">
|
||||
<h1>Etherpad</h1>
|
||||
<ul>
|
||||
<li><NavLink to="/plugins"><Trans i18nKey="admin_plugins"/></NavLink></li>
|
||||
<li><NavLink to={"/settings"}><Trans i18nKey="admin_settings"/></NavLink></li>
|
||||
<li> <NavLink to={"/help"}><Trans i18nKey="admin_plugins_info"/></NavLink></li>
|
||||
<li><NavLink to={"/pads"}><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="innerwrapper">
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default App
|
489
admin/src/index.css
Normal file
489
admin/src/index.css
Normal file
|
@ -0,0 +1,489 @@
|
|||
:root {
|
||||
--etherpad-color: #0f775b;
|
||||
}
|
||||
|
||||
|
||||
|
||||
html, body, #root {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font: 14px helvetica, sans-serif;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
div.menu {
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
width: 220px;
|
||||
border-right: 1px solid #ccc;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
div.menu ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.menu li {
|
||||
list-style: none;
|
||||
margin-left: 3px;
|
||||
line-height: 3;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.menu li:last-child {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.innerwrapper {
|
||||
padding: 15px;
|
||||
padding-left: 265px;
|
||||
}
|
||||
|
||||
div.innerwrapper-err {
|
||||
padding: 15px;
|
||||
padding-left: 265px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
background: none repeat scroll 0px 0px #FFFFFF;
|
||||
box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2);
|
||||
margin: auto;
|
||||
max-width: 1150px;
|
||||
min-height: 100%;/*always display a scrollbar*/
|
||||
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 10px 0;
|
||||
height: 1px;
|
||||
background: #aaa;
|
||||
background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
||||
background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
||||
background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
||||
background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#inner {
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
input {
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
|
||||
.sort {
|
||||
cursor: pointer;
|
||||
}
|
||||
.sort:after {
|
||||
content: '▲▼'
|
||||
}
|
||||
.sort.up:after {
|
||||
content:'▲'
|
||||
}
|
||||
.sort.down:after {
|
||||
content:'▼'
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
position:relative; /* Allows us to position the loading indicator relative to the table */
|
||||
}
|
||||
|
||||
table thead tr {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#installed-plugins td>div {
|
||||
position: relative;/* Allows us to position the loading indicator relative to this row */
|
||||
display: inline-block; /*make this fill the whole cell*/
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.messages {
|
||||
height: 5em;
|
||||
}
|
||||
.messages * {
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
.messages .fetching {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0; left: 0; bottom:0; right:0;
|
||||
padding: auto;
|
||||
|
||||
background: rgb(255,255,255);
|
||||
display: none;
|
||||
}
|
||||
|
||||
#search-progress.progress {
|
||||
padding-top: 20%;
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.progress * {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.settings {
|
||||
outline: none;
|
||||
width: 100%;
|
||||
min-height: 80vh;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
#response {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
a:link, a:visited, a:hover, a:focus {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:focus, a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.installed-results a:link,
|
||||
.search-results a:link,
|
||||
.installed-results a:visited,
|
||||
.search-results a:visited,
|
||||
.installed-results a:hover,
|
||||
.search-results a:hover,
|
||||
.installed-results a:focus,
|
||||
.search-results a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.installed-results a:focus,
|
||||
.search-results a:focus,
|
||||
.installed-results a:hover,
|
||||
.search-results a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
div.innerwrapper {
|
||||
padding: 0 15px 15px 15px;
|
||||
}
|
||||
|
||||
div.menu {
|
||||
padding: 1px 15px 0 15px;
|
||||
position: static;
|
||||
height: auto;
|
||||
border-right: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
border: none;
|
||||
}
|
||||
|
||||
table, thead, tbody, td, tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tr {
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
td {
|
||||
border: none;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: relative;
|
||||
padding-left: 50%;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.name {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
td:before {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
content: attr(data-label);
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
table input[type="button"] {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.settings-button-bar {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
background-image: url("/fond.jpg");
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
|
||||
.login-textinput {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #fffacc;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 20%;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.login-inner-box{
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
color: var(--etherpad-color);
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
padding: 10px;
|
||||
background-color: var(--etherpad-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: white;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
.dialog-confirm-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
.dialog-confirm-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background-color: white;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
|
||||
.dialog-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
color: var(--etherpad-color);
|
||||
font-size: 2em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ToastViewport {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 390px;
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
z-index: 2147483647;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ToastRootSuccess {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
|
||||
.ToastRootFailure {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.ToastRootFailure > .ToastTitle {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ToastRoot {
|
||||
border-radius: 20px;
|
||||
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
||||
padding: 15px;
|
||||
display: grid;
|
||||
grid-template-areas: 'title action' 'description action';
|
||||
grid-template-columns: auto max-content;
|
||||
column-gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
.ToastRoot[data-state='open'] {
|
||||
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.ToastRoot[data-state='closed'] {
|
||||
animation: hide 100ms ease-in;
|
||||
}
|
||||
.ToastRoot[data-swipe='move'] {
|
||||
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||
}
|
||||
.ToastRoot[data-swipe='cancel'] {
|
||||
transform: translateX(0);
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
.ToastRoot[data-swipe='end'] {
|
||||
animation: swipeOut 100ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes hide {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swipeOut {
|
||||
from {
|
||||
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||
}
|
||||
}
|
||||
|
||||
.ToastTitle {
|
||||
grid-area: title;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: var(--slate-12);
|
||||
padding: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.ToastDescription {
|
||||
grid-area: description;
|
||||
margin: 0;
|
||||
color: var(--slate-11);
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.ToastAction {
|
||||
grid-area: action;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px
|
||||
}
|
||||
|
||||
.search-field {
|
||||
width: 50%;
|
||||
padding: 5px;
|
||||
}
|
57
admin/src/localization/i18n.ts
Normal file
57
admin/src/localization/i18n.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import i18n from 'i18next'
|
||||
import {initReactI18next} from "react-i18next";
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
|
||||
|
||||
import { BackendModule } from 'i18next';
|
||||
|
||||
const LazyImportPlugin: BackendModule = {
|
||||
type: 'backend',
|
||||
init: function () {
|
||||
},
|
||||
read: async function (language, namespace, callback) {
|
||||
|
||||
let baseURL = import.meta.env.BASE_URL
|
||||
if(namespace === "translation") {
|
||||
// If default we load the translation file
|
||||
baseURL+=`/locales/${language}.json`
|
||||
} else {
|
||||
// Else we load the former plugin translation file
|
||||
baseURL+=`/${namespace}/${language}.json`
|
||||
}
|
||||
|
||||
const localeJSON = await fetch(baseURL, {
|
||||
cache: "force-cache"
|
||||
})
|
||||
let json;
|
||||
|
||||
try {
|
||||
json = JSON.parse(await localeJSON.text())
|
||||
} catch(e) {
|
||||
callback(new Error("Error loading"), null);
|
||||
}
|
||||
|
||||
|
||||
callback(null, json);
|
||||
},
|
||||
|
||||
save: function () {
|
||||
},
|
||||
|
||||
create: function () {
|
||||
/* save the missing translation */
|
||||
},
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(LazyImportPlugin)
|
||||
.use(initReactI18next)
|
||||
.init(
|
||||
{
|
||||
ns: ['translation','ep_admin_pads'],
|
||||
fallbackLng: 'en'
|
||||
}
|
||||
)
|
||||
|
||||
export default i18n
|
40
admin/src/main.tsx
Normal file
40
admin/src/main.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import {createBrowserRouter, createRoutesFromElements, Route, RouterProvider} from "react-router-dom";
|
||||
import {HomePage} from "./pages/HomePage.tsx";
|
||||
import {SettingsPage} from "./pages/SettingsPage.tsx";
|
||||
import {LoginScreen} from "./pages/LoginScreen.tsx";
|
||||
import {HelpPage} from "./pages/HelpPage.tsx";
|
||||
import * as Toast from '@radix-ui/react-toast'
|
||||
import {I18nextProvider} from "react-i18next";
|
||||
import i18n from "./localization/i18n.ts";
|
||||
import {PadPage} from "./pages/PadPage.tsx";
|
||||
import {ToastDialog} from "./utils/Toast.tsx";
|
||||
|
||||
const router = createBrowserRouter(createRoutesFromElements(
|
||||
<><Route element={<App/>}>
|
||||
<Route index element={<HomePage/>}/>
|
||||
<Route path="/plugins" element={<HomePage/>}/>
|
||||
<Route path="/settings" element={<SettingsPage/>}/>
|
||||
<Route path="/help" element={<HelpPage/>}/>
|
||||
<Route path="/pads" element={<PadPage/>}/>
|
||||
</Route><Route path="/login">
|
||||
<Route index element={<LoginScreen/>}/>
|
||||
</Route></>
|
||||
), {
|
||||
basename: import.meta.env.BASE_URL
|
||||
})
|
||||
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Toast.Provider>
|
||||
<ToastDialog/>
|
||||
<RouterProvider router={router}/>
|
||||
</Toast.Provider>
|
||||
</I18nextProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
70
admin/src/pages/HelpPage.tsx
Normal file
70
admin/src/pages/HelpPage.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import {Trans} from "react-i18next";
|
||||
import {useStore} from "../store/store.ts";
|
||||
import {useEffect, useState} from "react";
|
||||
import {HelpObj} from "./Plugin.ts";
|
||||
|
||||
export const HelpPage = () => {
|
||||
const settingsSocket = useStore(state=>state.settingsSocket)
|
||||
const [helpData, setHelpData] = useState<HelpObj>();
|
||||
|
||||
useEffect(() => {
|
||||
if(!settingsSocket) return;
|
||||
settingsSocket?.on('reply:help', (data) => {
|
||||
setHelpData(data)
|
||||
});
|
||||
|
||||
settingsSocket?.emit('help');
|
||||
}, [settingsSocket]);
|
||||
|
||||
const renderHooks = (hooks:Record<string, Record<string, string>>) => {
|
||||
return Object.keys(hooks).map((hookName, i) => {
|
||||
return <div key={hookName+i}>
|
||||
<h3>{hookName}</h3>
|
||||
<ul>
|
||||
{Object.keys(hooks[hookName]).map((hook, i) => <li>{hook}
|
||||
<ul key={hookName+hook+i}>
|
||||
{Object.keys(hooks[hookName][hook]).map((subHook, i) => <li key={i}>{subHook}</li>)}
|
||||
</ul>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (!helpData) return <div></div>
|
||||
|
||||
return <div>
|
||||
<h1><Trans i18nKey="admin_plugins_info.version"/></h1>
|
||||
<div className="help-block">
|
||||
<div><Trans i18nKey="admin_plugins_info.version_number"/></div>
|
||||
<div>{helpData?.epVersion}</div>
|
||||
<div><Trans i18nKey="admin_plugins_info.version_latest"/></div>
|
||||
<div>{helpData.latestVersion}</div>
|
||||
<div>Git sha</div>
|
||||
<div>{helpData.gitCommit}</div>
|
||||
</div>
|
||||
<h2><Trans i18nKey="admin_plugins.installed"/></h2>
|
||||
<ul>
|
||||
{helpData.installedPlugins.map((plugin, i) => <li key={i}>{plugin}</li>)}
|
||||
</ul>
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins_info.parts"/></h2>
|
||||
<ul>
|
||||
{helpData.installedParts.map((part, i) => <li key={i}>{part}</li>)}
|
||||
</ul>
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins_info.hooks"/></h2>
|
||||
{
|
||||
renderHooks(helpData.installedServerHooks)
|
||||
}
|
||||
|
||||
<h2>
|
||||
<Trans i18nKey="admin_plugins_info.hooks_client"/>
|
||||
{
|
||||
renderHooks(helpData.installedClientHooks)
|
||||
}
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
}
|
179
admin/src/pages/HomePage.tsx
Normal file
179
admin/src/pages/HomePage.tsx
Normal file
|
@ -0,0 +1,179 @@
|
|||
import {useStore} from "../store/store.ts";
|
||||
import {useEffect, useState} from "react";
|
||||
import {InstalledPlugin, PluginDef, SearchParams} from "./Plugin.ts";
|
||||
import {useDebounce} from "../utils/useDebounce.ts";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
|
||||
|
||||
export const HomePage = () => {
|
||||
const pluginsSocket = useStore(state=>state.pluginsSocket)
|
||||
const [plugins,setPlugins] = useState<PluginDef[]>([])
|
||||
const [installedPlugins, setInstalledPlugins] = useState<InstalledPlugin[]>([])
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
offset: 0,
|
||||
limit: 99999,
|
||||
sortBy: 'name',
|
||||
sortDir: 'asc',
|
||||
searchTerm: ''
|
||||
})
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const {t} = useTranslation()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if(!pluginsSocket){
|
||||
return
|
||||
}
|
||||
|
||||
pluginsSocket.on('results:installed', (data:{
|
||||
installed: InstalledPlugin[]
|
||||
})=>{
|
||||
setInstalledPlugins(data.installed)
|
||||
})
|
||||
|
||||
pluginsSocket.on('results:updatable', (data) => {
|
||||
data.updatable.forEach((pluginName: string) => {
|
||||
setInstalledPlugins(installedPlugins.map(plugin => {
|
||||
if (plugin.name === pluginName) {
|
||||
return {
|
||||
...plugin,
|
||||
updatable: true
|
||||
}
|
||||
}
|
||||
return plugin
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
pluginsSocket.on('finished:install', () => {
|
||||
pluginsSocket!.emit('getInstalled');
|
||||
})
|
||||
|
||||
pluginsSocket.on('finished:uninstall', () => {
|
||||
console.log("Finished uninstall")
|
||||
})
|
||||
|
||||
|
||||
// Reload on reconnect
|
||||
pluginsSocket.on('connect', ()=>{
|
||||
// Initial retrieval of installed plugins
|
||||
pluginsSocket.emit('getInstalled');
|
||||
pluginsSocket.emit('search', searchParams)
|
||||
})
|
||||
|
||||
pluginsSocket.emit('getInstalled');
|
||||
|
||||
// check for updates every 5mins
|
||||
const interval = setInterval(() => {
|
||||
pluginsSocket.emit('checkUpdates');
|
||||
}, 1000 * 60 * 5);
|
||||
|
||||
return ()=>{
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [pluginsSocket]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!pluginsSocket) {
|
||||
return
|
||||
}
|
||||
|
||||
pluginsSocket?.emit('search', searchParams)
|
||||
|
||||
|
||||
pluginsSocket!.on('results:search', (data: {
|
||||
results: PluginDef[]
|
||||
}) => {
|
||||
setPlugins(data.results)
|
||||
})
|
||||
|
||||
|
||||
}, [searchParams, pluginsSocket]);
|
||||
|
||||
const uninstallPlugin = (pluginName: string)=>{
|
||||
pluginsSocket!.emit('uninstall', pluginName);
|
||||
// Remove plugin
|
||||
setInstalledPlugins(installedPlugins.filter(i=>i.name !== pluginName))
|
||||
}
|
||||
|
||||
const installPlugin = (pluginName: string)=>{
|
||||
pluginsSocket!.emit('install', pluginName);
|
||||
setPlugins(plugins.filter(plugin=>plugin.name !== pluginName))
|
||||
}
|
||||
|
||||
|
||||
useDebounce(()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
offset: 0,
|
||||
searchTerm: searchTerm
|
||||
})
|
||||
}, 500, [searchTerm])
|
||||
|
||||
return <div>
|
||||
<h1><Trans i18nKey="admin_plugins"/></h1>
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins.installed"/></h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><Trans i18nKey="admin_plugins.name"/></th>
|
||||
<th><Trans i18nKey="admin_plugins.version"/></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody style={{overflow: 'auto'}}>
|
||||
{installedPlugins.map((plugin, index) => {
|
||||
return <tr key={index}>
|
||||
<td>{plugin.name}</td>
|
||||
<td>{plugin.version}</td>
|
||||
<td>
|
||||
{
|
||||
plugin.updatable ?
|
||||
<button onClick={() => installPlugin(plugin.name)}>Update</button>
|
||||
: <button disabled={plugin.name == "ep_etherpad-lite"}
|
||||
onClick={() => uninstallPlugin(plugin.name)}><Trans
|
||||
i18nKey="admin_plugins.installed_uninstall.value"/></button>
|
||||
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins.available"/></h2>
|
||||
|
||||
<input className="search-field" placeholder={t('admin_plugins.available_search.placeholder')} type="text" value={searchTerm} onChange={v=>{
|
||||
setSearchTerm(v.target.value)
|
||||
}}/>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><Trans i18nKey="admin_plugins.name"/></th>
|
||||
<th style={{width: '30%'}}><Trans i18nKey="admin_plugins.description"/></th>
|
||||
<th><Trans i18nKey="admin_plugins.version"/></th>
|
||||
<th><Trans i18nKey="admin_plugins.last-update"/></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody style={{overflow: 'auto'}}>
|
||||
{plugins.map((plugin) => {
|
||||
return <tr key={plugin.name}>
|
||||
<td><a rel="noopener noreferrer" href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
|
||||
<td>{plugin.description}</td>
|
||||
<td>{plugin.version}</td>
|
||||
<td>{plugin.time}</td>
|
||||
<td>
|
||||
<button onClick={() => installPlugin(plugin.name)}><Trans i18nKey="admin_plugins.available_install.value"/></button>
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
44
admin/src/pages/LoginScreen.tsx
Normal file
44
admin/src/pages/LoginScreen.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import {useState} from "react";
|
||||
import {useStore} from "../store/store.ts";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export const LoginScreen = ()=>{
|
||||
const navigate = useNavigate()
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const login = ()=>{
|
||||
fetch('/admin-auth/', {
|
||||
method: 'POST',
|
||||
headers:{
|
||||
Authorization: `Basic ${btoa(`${username}:${password}`)}`
|
||||
}
|
||||
}).then(r=>{
|
||||
if(!r.ok) {
|
||||
useStore.getState().setToastState({
|
||||
open: true,
|
||||
title: "Login failed",
|
||||
success: false
|
||||
})
|
||||
} else {
|
||||
navigate('/')
|
||||
}
|
||||
}).catch(e=>{
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
|
||||
return <div className="login-background">
|
||||
<div className="login-box">
|
||||
<h1 className="login-title">Login Etherpad</h1>
|
||||
<div className="login-inner-box">
|
||||
<div>Username</div>
|
||||
<input className="login-textinput" type="text" value={username} onChange={v => setUsername(v.target.value)} placeholder="Username"/>
|
||||
<div>Passwort</div>
|
||||
<input className="login-textinput" type="password" value={password}
|
||||
onChange={v => setPassword(v.target.value)} placeholder="Password"/>
|
||||
<input type="button" value="Login" onClick={login} className="login-button"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
172
admin/src/pages/PadPage.tsx
Normal file
172
admin/src/pages/PadPage.tsx
Normal file
|
@ -0,0 +1,172 @@
|
|||
import {Trans, useTranslation} from "react-i18next";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useStore} from "../store/store.ts";
|
||||
import {PadSearchQuery, PadSearchResult} from "../utils/PadSearch.ts";
|
||||
import {useDebounce} from "../utils/useDebounce.ts";
|
||||
import {determineSorting} from "../utils/sorting.ts";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
|
||||
export const PadPage = ()=>{
|
||||
const settingsSocket = useStore(state=>state.settingsSocket)
|
||||
const [searchParams, setSearchParams] = useState<PadSearchQuery>({
|
||||
offset: 0,
|
||||
limit: 12,
|
||||
pattern: '',
|
||||
sortBy: 'padName',
|
||||
ascending: true
|
||||
})
|
||||
const {t} = useTranslation()
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const pads = useStore(state=>state.pads)
|
||||
const pages = useMemo(()=>{
|
||||
if(!pads){
|
||||
return [0]
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(pads!.total / searchParams.limit)
|
||||
return Array.from({length: totalPages}, (_, i) => i+1)
|
||||
},[pads, searchParams.limit])
|
||||
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
|
||||
const [padToDelete, setPadToDelete] = useState<string>('')
|
||||
|
||||
useDebounce(()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
pattern: searchTerm
|
||||
})
|
||||
|
||||
}, 500, [searchTerm])
|
||||
|
||||
useEffect(() => {
|
||||
if(!settingsSocket){
|
||||
return
|
||||
}
|
||||
|
||||
settingsSocket.emit('padLoad', searchParams)
|
||||
|
||||
}, [settingsSocket, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!settingsSocket){
|
||||
return
|
||||
}
|
||||
|
||||
settingsSocket.on('results:padLoad', (data: PadSearchResult)=>{
|
||||
useStore.getState().setPads(data);
|
||||
})
|
||||
|
||||
|
||||
settingsSocket.on('results:deletePad', (padID: string)=>{
|
||||
const newPads = useStore.getState().pads?.results?.filter((pad)=>{
|
||||
return pad.padName !== padID
|
||||
})
|
||||
useStore.getState().setPads({
|
||||
total: useStore.getState().pads!.total-1,
|
||||
results: newPads
|
||||
})
|
||||
})
|
||||
}, [settingsSocket, pads]);
|
||||
|
||||
const deletePad = (padID: string)=>{
|
||||
settingsSocket?.emit('deletePad', padID)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return <div>
|
||||
<Dialog.Root open={deleteDialog}><Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay" />
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<div className="">
|
||||
<div className=""></div>
|
||||
<div className="">
|
||||
{t("ep_admin_pads:ep_adminpads2_confirm", {
|
||||
padID: padToDelete,
|
||||
})}
|
||||
</div>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={()=>{
|
||||
setDeleteDialog(false)
|
||||
}}>Cancel</button>
|
||||
<button onClick={()=>{
|
||||
deletePad(padToDelete)
|
||||
setDeleteDialog(false)
|
||||
}}>Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<h1><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></h1>
|
||||
<input type="text" value={searchTerm} onChange={v=>setSearchTerm(v.target.value)}
|
||||
placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'padName')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'padName',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'lastEdited')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'lastEdited',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_pad-user-count"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'userCount')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'userCount',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_last-edited"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'revisionNumber')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'revisionNumber',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}>Revision number</th>
|
||||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
pads?.results?.map((pad)=>{
|
||||
return <tr key={pad.padName}>
|
||||
<td style={{textAlign: 'center'}}>{pad.padName}</td>
|
||||
<td style={{textAlign: 'center'}}>{pad.userCount}</td>
|
||||
<td style={{textAlign: 'center'}}>{new Date(pad.lastEdited).toLocaleString()}</td>
|
||||
<td style={{textAlign: 'center'}}>{pad.revisionNumber}</td>
|
||||
<td>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={()=>{
|
||||
setPadToDelete(pad.padName)
|
||||
setDeleteDialog(true)
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_delete.value"/></button>
|
||||
<button onClick={()=>{
|
||||
window.open(`/p/${pad.padName}`, '_blank')
|
||||
}}>view</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="settings-button-bar">
|
||||
{pages.map((page)=>{
|
||||
return <button key={page} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
offset: (page-1)*searchParams.limit
|
||||
})
|
||||
}}>{page}</button>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
36
admin/src/pages/Plugin.ts
Normal file
36
admin/src/pages/Plugin.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
export type PluginDef = {
|
||||
name: string,
|
||||
description: string,
|
||||
version: string,
|
||||
time: string,
|
||||
official: boolean,
|
||||
}
|
||||
|
||||
|
||||
export type InstalledPlugin = {
|
||||
name: string,
|
||||
path: string,
|
||||
realPath: string,
|
||||
version:string,
|
||||
updatable?: boolean
|
||||
}
|
||||
|
||||
|
||||
export type SearchParams = {
|
||||
searchTerm: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
sortBy: 'name'|'version',
|
||||
sortDir: 'asc'|'desc'
|
||||
}
|
||||
|
||||
|
||||
export type HelpObj = {
|
||||
epVersion: string
|
||||
gitCommit: string
|
||||
installedClientHooks: Record<string, Record<string, string>>,
|
||||
installedParts: string[],
|
||||
installedPlugins: string[],
|
||||
installedServerHooks: Record<string, never>,
|
||||
latestVersion: string
|
||||
}
|
45
admin/src/pages/SettingsPage.tsx
Normal file
45
admin/src/pages/SettingsPage.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import {useStore} from "../store/store.ts";
|
||||
import {isJSONClean} from "../utils/utils.ts";
|
||||
import {Trans} from "react-i18next";
|
||||
|
||||
export const SettingsPage = ()=>{
|
||||
const settingsSocket = useStore(state=>state.settingsSocket)
|
||||
|
||||
const settings = useStore(state=>state.settings)
|
||||
|
||||
return <div>
|
||||
<h1><Trans i18nKey="admin_settings.current"/></h1>
|
||||
<textarea value={settings} className="settings" onChange={v => {
|
||||
useStore.getState().setSettings(v.target.value)
|
||||
}}/>
|
||||
<div className="settings-button-bar">
|
||||
<button className="settingsButton" onClick={() => {
|
||||
if (isJSONClean(settings!)) {
|
||||
// JSON is clean so emit it to the server
|
||||
settingsSocket!.emit('saveSettings', settings!);
|
||||
useStore.getState().setToastState({
|
||||
open: true,
|
||||
title: "Succesfully saved settings",
|
||||
success: true
|
||||
})
|
||||
} else {
|
||||
useStore.getState().setToastState({
|
||||
open: true,
|
||||
title: "Error saving settings",
|
||||
success: false
|
||||
})
|
||||
}
|
||||
}}><Trans i18nKey="admin_settings.current_save.value"/></button>
|
||||
<button className="settingsButton" onClick={() => {
|
||||
settingsSocket!.emit('restartServer');
|
||||
}}><Trans i18nKey="admin_settings.current_restart.value"/></button>
|
||||
</div>
|
||||
<div className="separator"/>
|
||||
<div className="settings-button-bar">
|
||||
<a rel="noopener noreferrer" target="_blank" href="https://github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON"><Trans
|
||||
i18nKey="admin_settings.current_example-prod"/></a>
|
||||
<a rel="noopener noreferrer" target="_blank" href="https://github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON"><Trans
|
||||
i18nKey="admin_settings.current_example-devel"/></a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
47
admin/src/store/store.ts
Normal file
47
admin/src/store/store.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {create} from "zustand";
|
||||
import {Socket} from "socket.io-client";
|
||||
import {PadSearchResult} from "../utils/PadSearch.ts";
|
||||
|
||||
type ToastState = {
|
||||
description?:string,
|
||||
title: string,
|
||||
open: boolean,
|
||||
success: boolean
|
||||
}
|
||||
|
||||
|
||||
type StoreState = {
|
||||
settings: string|undefined,
|
||||
setSettings: (settings: string) => void,
|
||||
settingsSocket: Socket|undefined,
|
||||
setSettingsSocket: (socket: Socket) => void,
|
||||
showLoading: boolean,
|
||||
setShowLoading: (show: boolean) => void,
|
||||
setPluginsSocket: (socket: Socket) => void
|
||||
pluginsSocket: Socket|undefined,
|
||||
toastState: ToastState,
|
||||
setToastState: (val: ToastState)=>void,
|
||||
pads: PadSearchResult|undefined,
|
||||
setPads: (pads: PadSearchResult)=>void
|
||||
}
|
||||
|
||||
|
||||
export const useStore = create<StoreState>()((set) => ({
|
||||
settings: undefined,
|
||||
setSettings: (settings: string) => set({settings}),
|
||||
settingsSocket: undefined,
|
||||
setSettingsSocket: (socket: Socket) => set({settingsSocket: socket}),
|
||||
showLoading: false,
|
||||
setShowLoading: (show: boolean) => set({showLoading: show}),
|
||||
pluginsSocket: undefined,
|
||||
setPluginsSocket: (socket: Socket) => set({pluginsSocket: socket}),
|
||||
setToastState: (val )=>set({toastState: val}),
|
||||
toastState: {
|
||||
open: false,
|
||||
title: '',
|
||||
description:'',
|
||||
success: false
|
||||
},
|
||||
pads: undefined,
|
||||
setPads: (pads)=>set({pads})
|
||||
}));
|
29
admin/src/utils/AnimationFrameHook.ts
Normal file
29
admin/src/utils/AnimationFrameHook.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {useCallback, useEffect, useRef} from "react";
|
||||
|
||||
type Args = any[]
|
||||
|
||||
export const useAnimationFrame = <Fn extends (...args: Args)=>void>(
|
||||
callback: Fn,
|
||||
wait = 0
|
||||
): ((...args: Parameters<Fn>)=>void)=>{
|
||||
const rafId = useRef(0)
|
||||
const render = useCallback(
|
||||
(...args: Parameters<Fn>)=>{
|
||||
cancelAnimationFrame(rafId.current)
|
||||
const timeStart = performance.now()
|
||||
|
||||
const renderFrame = (timeNow: number)=>{
|
||||
if(timeNow-timeStart<wait){
|
||||
rafId.current = requestAnimationFrame(renderFrame)
|
||||
return
|
||||
}
|
||||
callback(...args)
|
||||
}
|
||||
rafId.current = requestAnimationFrame(renderFrame)
|
||||
}, [callback, wait]
|
||||
)
|
||||
|
||||
|
||||
useEffect(()=>cancelAnimationFrame(rafId.current),[])
|
||||
return render
|
||||
}
|
19
admin/src/utils/LoadingScreen.tsx
Normal file
19
admin/src/utils/LoadingScreen.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {useStore} from "../store/store.ts";
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import ReactComponent from './brand.svg?react';
|
||||
export const LoadingScreen = ()=>{
|
||||
const showLoading = useStore(state => state.showLoading)
|
||||
|
||||
return <Dialog.Root open={showLoading}><Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
|
||||
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 dialog-content">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="animate-spin w-16 h-16 border-t-2 border-b-2 border-[--fg-color] rounded-full"></div>
|
||||
<div className="mt-4 text-[--fg-color]">
|
||||
<ReactComponent/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
}
|
20
admin/src/utils/PadSearch.ts
Normal file
20
admin/src/utils/PadSearch.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export type PadSearchQuery = {
|
||||
pattern: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
ascending: boolean;
|
||||
sortBy: string;
|
||||
}
|
||||
|
||||
|
||||
export type PadSearchResult = {
|
||||
total: number;
|
||||
results?: PadType[]
|
||||
}
|
||||
|
||||
export type PadType = {
|
||||
padName: string;
|
||||
lastEdited: number;
|
||||
userCount: number;
|
||||
revisionNumber: number;
|
||||
}
|
26
admin/src/utils/Toast.tsx
Normal file
26
admin/src/utils/Toast.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import * as Toast from '@radix-ui/react-toast'
|
||||
import {useStore} from "../store/store.ts";
|
||||
import {useMemo} from "react";
|
||||
|
||||
export const ToastDialog = ()=>{
|
||||
const toastState = useStore(state => state.toastState)
|
||||
const resultingClass = useMemo(()=> {
|
||||
return toastState.success?'ToastRootSuccess':'ToastRootFailure'
|
||||
}, [toastState.success])
|
||||
|
||||
console.log()
|
||||
return <>
|
||||
<Toast.Root className={"ToastRoot "+resultingClass} open={toastState && toastState.open} onOpenChange={()=>{
|
||||
useStore.getState().setToastState({
|
||||
...toastState!,
|
||||
open: !toastState?.open
|
||||
})
|
||||
}}>
|
||||
<Toast.Title className="ToastTitle">{toastState.title}</Toast.Title>
|
||||
<Toast.Description asChild>
|
||||
{toastState.description}
|
||||
</Toast.Description>
|
||||
</Toast.Root>
|
||||
<Toast.Viewport className="ToastViewport"/>
|
||||
</>
|
||||
}
|
50
admin/src/utils/brand.svg
Normal file
50
admin/src/utils/brand.svg
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg fill="#0f775b" width="355px" height="355px" viewBox="0 0 355 355" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Group 10</title>
|
||||
<defs>
|
||||
<!-- top line -->
|
||||
<rect id="path-4" x="41" y="110" width="142" height="25" rx="12.5" fill="#0f775b">
|
||||
<animate attributeName="width" from="0" to="142" dur="3s" fill="freeze"/>
|
||||
</rect>
|
||||
|
||||
<!-- middle line -->
|
||||
<rect id="path-2" x="42" y="167" width="168" height="27" rx="13.5" fill="#0f775b">
|
||||
<animate attributeName="width" from="0" to="168" dur="5s" fill="freeze"/>
|
||||
</rect>
|
||||
|
||||
<!-- bottom line -->
|
||||
<rect id="path-6" x="41" y="226" width="105" height="25" rx="12.5" fill="#0f775b">
|
||||
<animate attributeName="width" from="0" to="105" dur="2s" fill="freeze"/>
|
||||
</rect>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" >
|
||||
<g id="Group-5-Copy-2" transform="translate(-415.000000, -351.000000)">
|
||||
<g id="Group-10" transform="translate(415.000000, 351.000000)">
|
||||
<g id="Group-9" transform="translate(0.000000, 15.000000)">
|
||||
<!-- small radio wave -->
|
||||
<path stroke="0f775b" d="M237.612214,138.157654 C234.725783,135.28192 230.051254,135.279644 227.164823,138.157654 C224.278392,141.035663 224.278392,145.698831 227.164823,148.57684 C234.93988,156.329214 239.222735,166.601382 239.222735,177.499403 C239.222735,188.397423 234.93988,198.669591 227.164823,206.424696 C224.278392,209.30043 224.278392,213.965873 227.164823,216.841607 C228.608267,218.280384 230.497251,219 232.388518,219 C234.277503,219 236.16877,218.280384 237.612214,216.841607 C248.18012,206.304532 254,192.334147 254,177.499403 C254,162.665114 248.18012,148.694728 237.612214,138.157654 Z" id="Path-Copy-26" fill-opacity="0.200482" fill="#000000" fill-rule="nonzero" opacity="0.754065225">
|
||||
<animate attributeName="opacity" from="0" to="1" dur="3s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
<!-- large radio wave -->
|
||||
<path stroke="0f775b" d="M267.333026,113.158661 C264.51049,110.280446 259.939438,110.280446 257.116902,113.158661 C254.294366,116.039154 254.294366,120.709078 257.116902,123.586837 C285.703837,152.763042 285.703837,200.237641 257.116902,229.413847 C254.294366,232.292061 254.294366,236.96153 257.116902,239.839744 C258.528393,241.280219 260.375562,242 262.224964,242 C264.074365,242 265.921535,241.279763 267.333026,239.837011 C301.555658,204.912576 301.555658,148.084007 267.333026,113.158661 Z" id="Path-Copy-27" fill-opacity="0.250565" fill="#131514" fill-rule="nonzero" opacity="0.754065225">
|
||||
<animate attributeName="opacity" from="0" to="1" dur="3s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
<!-- top line -->
|
||||
<g stroke="0f775b" id="Rectangle-Copy-56">
|
||||
<use fill="#000000" fill-opacity="0.200482" fill-rule="evenodd" xlink:href="#path-4"></use>
|
||||
</g>
|
||||
<!-- middle line -->
|
||||
<g stroke="0f775b" id="Rectangle-Copy-55">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"></use>
|
||||
<use fill="#000000" fill-opacity="0.200482" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||
</g>
|
||||
<!-- bottom line -->
|
||||
<g stroke="0f775b" id="Rectangle-Copy-57">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-7)" xlink:href="#path-6"></use>
|
||||
<use fill="#000000" fill-opacity="0.200482" xlink:href="#path-6"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
6
admin/src/utils/sorting.ts
Normal file
6
admin/src/utils/sorting.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const determineSorting = (sortBy: string, ascending: boolean, currentSymbol: string) => {
|
||||
if (sortBy === currentSymbol) {
|
||||
return ascending ? 'sort up' : 'sort down';
|
||||
}
|
||||
return 'sort none';
|
||||
}
|
22
admin/src/utils/useDebounce.ts
Normal file
22
admin/src/utils/useDebounce.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {DependencyList, EffectCallback, useMemo, useRef} from "react";
|
||||
import {useAnimationFrame} from "./AnimationFrameHook";
|
||||
|
||||
const defaultDeps: DependencyList = []
|
||||
|
||||
export const useDebounce = (
|
||||
fn:EffectCallback,
|
||||
wait = 0,
|
||||
deps = defaultDeps
|
||||
):void => {
|
||||
const isFirstRender = useRef(true)
|
||||
const render = useAnimationFrame(fn, wait)
|
||||
|
||||
useMemo(()=>{
|
||||
if(isFirstRender.current){
|
||||
isFirstRender.current = false
|
||||
return
|
||||
}
|
||||
|
||||
render()
|
||||
}, deps)
|
||||
}
|
64
admin/src/utils/utils.ts
Normal file
64
admin/src/utils/utils.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
const minify = (json: string)=>{
|
||||
|
||||
let tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
|
||||
in_string = false,
|
||||
in_multiline_comment = false,
|
||||
in_singleline_comment = false,
|
||||
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
|
||||
;
|
||||
|
||||
tokenizer.lastIndex = 0;
|
||||
|
||||
while (tmp = tokenizer.exec(json)) {
|
||||
lc = RegExp.leftContext;
|
||||
rc = RegExp.rightContext;
|
||||
if (!in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.substring(from);
|
||||
if (!in_string) {
|
||||
tmp2 = tmp2.replace(/(\n|\r|\s)*/g,"");
|
||||
}
|
||||
new_str[ns++] = tmp2;
|
||||
}
|
||||
from = tokenizer.lastIndex;
|
||||
|
||||
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.match(/(\\)*$/);
|
||||
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string
|
||||
in_string = !in_string;
|
||||
}
|
||||
from--; // include " character in next catch
|
||||
rc = json.substring(from);
|
||||
}
|
||||
else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = true;
|
||||
}
|
||||
else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = false;
|
||||
}
|
||||
else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_singleline_comment = true;
|
||||
}
|
||||
else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) {
|
||||
in_singleline_comment = false;
|
||||
}
|
||||
else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) {
|
||||
new_str[ns++] = tmp[0];
|
||||
}
|
||||
}
|
||||
new_str[ns++] = rc;
|
||||
return new_str.join("");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const isJSONClean = (data: string) => {
|
||||
let cleanSettings = minify(data);
|
||||
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
|
||||
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
|
||||
try {
|
||||
return typeof JSON.parse(cleanSettings) === 'object';
|
||||
} catch (e) {
|
||||
return false; // the JSON failed to be parsed
|
||||
}
|
||||
};
|
2
admin/src/vite-env.d.ts
vendored
Normal file
2
admin/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
25
admin/tsconfig.json
Normal file
25
admin/tsconfig.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
11
admin/tsconfig.node.json
Normal file
11
admin/tsconfig.node.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
34
admin/vite.config.ts
Normal file
34
admin/vite.config.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
import {viteStaticCopy} from "vite-plugin-static-copy";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), svgr(), viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: '../src/locales',
|
||||
dest: ''
|
||||
}
|
||||
]
|
||||
})],
|
||||
base: '/admin',
|
||||
build:{
|
||||
outDir: '../src/templates/admin'
|
||||
},
|
||||
server:{
|
||||
proxy: {
|
||||
'/socket.io/*': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/admin-auth/': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/admin-prox/, '/admin/')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -41,7 +41,7 @@ try cd "${TMP_FOLDER}"
|
|||
|
||||
# setting NODE_ENV=production ensures that dev dependencies are not installed,
|
||||
# making the windows package smaller
|
||||
export NODE_ENV=production
|
||||
export NODE_ENV=development
|
||||
|
||||
rm -rf node_modules || true
|
||||
rm -rf src/node_modules || true
|
||||
|
@ -49,6 +49,15 @@ rm -rf src/node_modules || true
|
|||
#log "do a normal unix install first..."
|
||||
#$(try cd ./bin/installDeps.sh)
|
||||
|
||||
# Install admin frontend
|
||||
cd admin
|
||||
try pnpm install
|
||||
try pnpm run build
|
||||
cd ..
|
||||
|
||||
# Nuke the admin folder as it is not needed anymore :D
|
||||
rm -rf admin
|
||||
|
||||
log "copy the windows settings template..."
|
||||
try cp settings.json.template settings.json
|
||||
|
||||
|
|
|
@ -9,11 +9,17 @@ cmd /C node -e "" || ( echo "Please install node.js ( https://nodejs.org )" && e
|
|||
echo _
|
||||
echo Ensure that all dependencies are up to date... If this is the first time you have run Etherpad please be patient.
|
||||
|
||||
mkdir node_modules
|
||||
cd /D node_modules
|
||||
mklink /D "ep_etherpad-lite" "..\src"
|
||||
|
||||
cd /D ..\src
|
||||
:: Install admin ui only if available
|
||||
IF EXIST admin (
|
||||
cd /D .\admin
|
||||
dir
|
||||
cmd /C pnpm i || exit /B 1
|
||||
cmd /C pnpm run build || exit /B 1
|
||||
cd /D ..
|
||||
)
|
||||
|
||||
|
||||
cmd /C pnpm i || exit /B 1
|
||||
|
||||
cd /D "%~dp0\.."
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
packages:
|
||||
- src
|
||||
- src
|
||||
|
|
|
@ -5,8 +5,6 @@ require('eslint-config-etherpad/patch/modern-module-resolution');
|
|||
|
||||
module.exports = {
|
||||
ignorePatterns: [
|
||||
'/static/js/admin/jquery.autosize.js',
|
||||
'/static/js/admin/minify.json.js',
|
||||
'/static/js/vendors/browser.js',
|
||||
'/static/js/vendors/farbtastic.js',
|
||||
'/static/js/vendors/gritter.js',
|
||||
|
|
|
@ -92,14 +92,12 @@
|
|||
{
|
||||
"name": "adminplugins",
|
||||
"hooks": {
|
||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins",
|
||||
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "adminsettings",
|
||||
"hooks": {
|
||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings",
|
||||
"socketio": "ep_etherpad-lite/node/hooks/express/adminsettings"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
*/
|
||||
|
||||
import {MapArrayType} from "../types/MapType";
|
||||
import {PadType} from "../types/PadType";
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const Pad = require('../db/Pad');
|
||||
|
@ -105,7 +106,7 @@ const padList = new class {
|
|||
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
|
||||
* applicable).
|
||||
*/
|
||||
exports.getPad = async (id: string, text: string|null, authorId:string = '') => {
|
||||
exports.getPad = async (id: string, text?: string|null, authorId:string|null = ''):Promise<PadType> => {
|
||||
// check if this is a valid padId
|
||||
if (!exports.isValidPadId(id)) {
|
||||
throw new CustomError(`${id} is not a valid padId`, 'apierror');
|
||||
|
@ -148,6 +149,9 @@ exports.listAllPads = async () => {
|
|||
return {padIDs};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// checks if a pad exists
|
||||
exports.doesPadExist = async (padId: string) => {
|
||||
const value = await db.get(`pad:${padId}`);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
'use strict';
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
import path from "path";
|
||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||
|
||||
const eejs = require('../../eejs');
|
||||
const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin');
|
||||
|
||||
/**
|
||||
* Add the admin navigation link
|
||||
|
@ -11,9 +13,19 @@ const eejs = require('../../eejs');
|
|||
* @return {*}
|
||||
*/
|
||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => {
|
||||
args.app.get('/admin', (req:any, res:any) => {
|
||||
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req}));
|
||||
});
|
||||
args.app.get('/admin/*', (req:any, res:any, next:Function) => {
|
||||
if (req.path.includes('.')) {
|
||||
const relativPath = req.path.split('/admin/')[1];
|
||||
res.sendFile(path.join(ADMIN_PATH, relativPath));
|
||||
} else {
|
||||
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
||||
res.header('Expires', '-1');
|
||||
res.header('Pragma', 'no-cache');
|
||||
res.sendFile(path.join(ADMIN_PATH, 'index.html'));
|
||||
}
|
||||
});
|
||||
args.app.get('/admin', (req:any, res:any, next:Function) => {
|
||||
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
|
||||
})
|
||||
return cb();
|
||||
};
|
||||
|
|
|
@ -12,35 +12,7 @@ const installer = require('../../../static/js/pluginfw/installer');
|
|||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const plugins = require('../../../static/js/pluginfw/plugins');
|
||||
const semver = require('semver');
|
||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||
|
||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function) => {
|
||||
args.app.get('/admin/plugins', (req:any, res:any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', {
|
||||
plugins: pluginDefs.plugins,
|
||||
req,
|
||||
errors: [],
|
||||
}));
|
||||
});
|
||||
|
||||
args.app.get('/admin/plugins/info', (req:any, res:any) => {
|
||||
const gitCommit = settings.getGitCommit();
|
||||
const epVersion = settings.getEpVersion();
|
||||
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins-info.html', {
|
||||
gitCommit,
|
||||
epVersion,
|
||||
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
|
||||
installedParts: `<pre>${plugins.formatParts()}</pre>`,
|
||||
installedServerHooks: `<div>${plugins.formatHooks('hooks', true)}</div>`,
|
||||
installedClientHooks: `<div>${plugins.formatHooks('client_hooks', true)}</div>`,
|
||||
latestVersion: UpdateCheck.getLatestVersion(),
|
||||
req,
|
||||
}));
|
||||
});
|
||||
|
||||
return cb();
|
||||
};
|
||||
|
||||
exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||
const io = args.io.of('/pluginfw/installer');
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
import {PadQueryResult, PadSearchQuery} from "../../types/PadSearchQuery";
|
||||
import {PadType} from "../../types/PadType";
|
||||
|
||||
const eejs = require('../../eejs');
|
||||
const fsp = require('fs').promises;
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const plugins = require('../../../static/js/pluginfw/plugins');
|
||||
const settings = require('../../utils/Settings');
|
||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||
const padManager = require('../../db/PadManager');
|
||||
const api = require('../../db/API');
|
||||
|
||||
|
||||
const queryPadLimit = 12;
|
||||
|
||||
exports.expressCreateServer = (hookName:string, {app}:any) => {
|
||||
app.get('/admin/settings', (req:any, res:any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', {
|
||||
req,
|
||||
settings: '',
|
||||
errors: [],
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
exports.socketio = (hookName:string, {io}:any) => {
|
||||
io.of('/settings').on('connection', (socket: any ) => {
|
||||
|
@ -38,10 +39,160 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
});
|
||||
|
||||
socket.on('saveSettings', async (newSettings:string) => {
|
||||
console.log('Admin request to save settings through a socket on /admin/settings');
|
||||
await fsp.writeFile(settings.settingsFilename, newSettings);
|
||||
socket.emit('saveprogress', 'saved');
|
||||
});
|
||||
|
||||
|
||||
socket.on('help', ()=> {
|
||||
const gitCommit = settings.getGitCommit();
|
||||
const epVersion = settings.getEpVersion();
|
||||
|
||||
const hooks:Map<string, Map<string,string>> = plugins.getHooks('hooks', false);
|
||||
const clientHooks:Map<string, Map<string,string>> = plugins.getHooks('client_hooks', false);
|
||||
|
||||
function mapToObject(map: Map<string,any>) {
|
||||
let obj = Object.create(null);
|
||||
for (let [k,v] of map) {
|
||||
if(v instanceof Map) {
|
||||
obj[k] = mapToObject(v);
|
||||
} else {
|
||||
obj[k] = v;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
socket.emit('reply:help', {
|
||||
gitCommit,
|
||||
epVersion,
|
||||
installedPlugins: plugins.getPlugins(),
|
||||
installedParts: plugins.getParts(),
|
||||
installedServerHooks: mapToObject(hooks),
|
||||
installedClientHooks: mapToObject(clientHooks),
|
||||
latestVersion: UpdateCheck.getLatestVersion(),
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
socket.on('padLoad', async (query: PadSearchQuery) => {
|
||||
const {padIDs} = await padManager.listAllPads();
|
||||
|
||||
const data:{
|
||||
total: number,
|
||||
results?: PadQueryResult[]
|
||||
} = {
|
||||
total: padIDs.length,
|
||||
};
|
||||
let result: string[] = padIDs;
|
||||
let maxResult;
|
||||
|
||||
// Filter out matches
|
||||
if (query.pattern) {
|
||||
result = result.filter((padName: string) => padName.includes(query.pattern));
|
||||
}
|
||||
|
||||
data.total = result.length;
|
||||
|
||||
maxResult = result.length - 1;
|
||||
if (maxResult < 0) {
|
||||
maxResult = 0;
|
||||
}
|
||||
|
||||
if (query.offset && query.offset < 0) {
|
||||
query.offset = 0;
|
||||
} else if (query.offset > maxResult) {
|
||||
query.offset = maxResult;
|
||||
}
|
||||
|
||||
if (query.limit && query.limit < 0) {
|
||||
query.limit = 0;
|
||||
} else if (query.limit > queryPadLimit) {
|
||||
query.limit = queryPadLimit;
|
||||
}
|
||||
|
||||
if (query.sortBy === 'padName') {
|
||||
result = result.sort((a,b)=>{
|
||||
if(a < b) return query.ascending ? -1 : 1;
|
||||
if(a > b) return query.ascending ? 1 : -1;
|
||||
return 0;
|
||||
}).slice(query.offset, query.offset + query.limit);
|
||||
|
||||
data.results = await Promise.all(result.map(async (padName: string) => {
|
||||
const pad = await padManager.getPad(padName);
|
||||
const revisionNumber = pad.getHeadRevisionNumber()
|
||||
const userCount = api.padUsersCount(padName).padUsersCount;
|
||||
const lastEdited = await pad.getLastEdit();
|
||||
|
||||
return {
|
||||
padName,
|
||||
lastEdited,
|
||||
userCount,
|
||||
revisionNumber
|
||||
}}));
|
||||
} else {
|
||||
const currentWinners: PadQueryResult[] = []
|
||||
let queryOffsetCounter = 0
|
||||
for (let res of result) {
|
||||
|
||||
const pad = await padManager.getPad(res);
|
||||
const padType = {
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
};
|
||||
|
||||
if (currentWinners.length < query.limit) {
|
||||
if(queryOffsetCounter < query.offset){
|
||||
queryOffsetCounter++
|
||||
continue
|
||||
}
|
||||
currentWinners.push({
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
})
|
||||
} else {
|
||||
// Kick out worst pad and replace by current pad
|
||||
let worstPad = currentWinners.sort((a, b) => {
|
||||
if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
|
||||
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
|
||||
return 0;
|
||||
})
|
||||
if(worstPad[0]&&worstPad[0][query.sortBy] < padType[query.sortBy]){
|
||||
if(queryOffsetCounter < query.offset){
|
||||
queryOffsetCounter++
|
||||
continue
|
||||
}
|
||||
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
|
||||
currentWinners.push({
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
data.results = currentWinners;
|
||||
}
|
||||
|
||||
socket.emit('results:padLoad', data);
|
||||
})
|
||||
|
||||
|
||||
socket.on('deletePad', async (padId: string) => {
|
||||
const padExists = await padManager.doesPadExists(padId);
|
||||
if (padExists) {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.remove();
|
||||
socket.emit('results:deletePad', padId);
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('restartServer', async () => {
|
||||
console.log('Admin request to restart server through a socket on /admin/settings');
|
||||
settings.reloadSettings();
|
||||
|
@ -51,3 +202,10 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const searchPad = async (query:PadSearchQuery) => {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -76,10 +76,12 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
||||
})
|
||||
|
||||
io.on('connection', (socket:any) => {
|
||||
|
||||
const handleConnection = (socket:Socket) => {
|
||||
sockets.add(socket);
|
||||
socketsEvents.emit('updated');
|
||||
// https://socket.io/docs/v3/faq/index.html
|
||||
// @ts-ignore
|
||||
const session = socket.request.session;
|
||||
session.connections++;
|
||||
session.save();
|
||||
|
@ -87,15 +89,9 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
sockets.delete(socket);
|
||||
socketsEvents.emit('updated');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
io.use(socketSessionMiddleware(args));
|
||||
|
||||
// Temporary workaround so all clients go through middleware and handle connection
|
||||
io.of('/pluginfw/installer').use(socketSessionMiddleware(args))
|
||||
io.of('/settings').use(socketSessionMiddleware(args))
|
||||
|
||||
io.use((socket:any, next:Function) => {
|
||||
const renewSession = (socket:any, next:Function) => {
|
||||
socket.conn.on('packet', (packet:string) => {
|
||||
// Tell express-session that the session is still active. The session store can use these
|
||||
// touch events to defer automatic session cleanup, and if express-session is configured with
|
||||
|
@ -106,7 +102,24 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
if (socket.request.session != null) socket.request.session.touch();
|
||||
});
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
io.on('connection', handleConnection);
|
||||
|
||||
io.use(socketSessionMiddleware(args));
|
||||
|
||||
// Temporary workaround so all clients go through middleware and handle connection
|
||||
io.of('/pluginfw/installer')
|
||||
.on('connection',handleConnection)
|
||||
.use(socketSessionMiddleware(args))
|
||||
.use(renewSession)
|
||||
io.of('/settings')
|
||||
.on('connection',handleConnection)
|
||||
.use(socketSessionMiddleware(args))
|
||||
.use(renewSession)
|
||||
|
||||
io.use(renewSession);
|
||||
|
||||
// var socketIOLogger = log4js.getLogger("socket.io");
|
||||
// Debug logging now has to be set at an environment level, this is stupid.
|
||||
|
|
|
@ -50,7 +50,7 @@ exports.userCanModify = (padId: string, req: SocketClientRequest) => {
|
|||
exports.authnFailureDelayMs = 1000;
|
||||
|
||||
const checkAccess = async (req:any, res:any, next: Function) => {
|
||||
const requireAdmin = req.path.toLowerCase().startsWith('/admin');
|
||||
const requireAdmin = req.path.toLowerCase().startsWith('/admin-auth');
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin
|
||||
|
@ -126,7 +126,13 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
// completed, or maybe different credentials are required), go to the next step.
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (await authorize()) return next();
|
||||
if (await authorize()) {
|
||||
if(requireAdmin) {
|
||||
res.status(200).send('Authorized')
|
||||
return
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Step 3: Authenticate the user. (Or, if already logged in, reauthenticate with different
|
||||
|
@ -163,7 +169,7 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
if (await aCallFirst0('authnFailure', {req, res})) return;
|
||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
||||
// No plugin handled the authentication failure. Fall back to basic authentication.
|
||||
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
||||
//res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
||||
// Delay the error response for 1s to slow down brute force attacks.
|
||||
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
|
||||
res.status(401).send('Authentication Required');
|
||||
|
@ -188,7 +194,13 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
// a login page).
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (await authorize()) return next();
|
||||
const auth = await authorize()
|
||||
if (auth && !requireAdmin) return next();
|
||||
if(auth && requireAdmin) {
|
||||
res.status(200).send('Authorized')
|
||||
return
|
||||
}
|
||||
|
||||
if (await aCallFirst0('authzFailure', {req, res})) return;
|
||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
||||
// No plugin handled the authorization failure.
|
||||
|
|
15
src/node/types/PadSearchQuery.ts
Normal file
15
src/node/types/PadSearchQuery.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export type PadSearchQuery = {
|
||||
pattern: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
ascending: boolean;
|
||||
sortBy: "padName" | "lastEdited" | "userCount" | "revisionNumber";
|
||||
}
|
||||
|
||||
|
||||
export type PadQueryResult = {
|
||||
padName: string,
|
||||
lastEdited: string,
|
||||
userCount: number,
|
||||
revisionNumber: number
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue