├── .eslintrc.cjs ├── .github ├── dependabot.yml └── workflows │ ├── backend-tests.yml │ ├── frontend-tests.yml │ ├── npmpublish.yml │ └── test-and-release.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apiUtils.js ├── commentManager.js ├── ep.json ├── exportHTML.js ├── index.js ├── locales ├── ar.json ├── bn.json ├── cs.json ├── da.json ├── de.json ├── diq.json ├── dsb.json ├── el.json ├── en.json ├── es.json ├── eu.json ├── fa.json ├── ff.json ├── fi.json ├── fr.json ├── gl.json ├── gur.json ├── ha.json ├── he.json ├── hi.json ├── hsb.json ├── hu.json ├── ia.json ├── id.json ├── io.json ├── it.json ├── ja.json ├── kn.json ├── ko.json ├── krc.json ├── lb.json ├── lt.json ├── mk.json ├── nl.json ├── oc.json ├── pl.json ├── pms.json ├── pt-br.json ├── qqq.json ├── ru.json ├── sc.json ├── scn.json ├── sdc.json ├── sk.json ├── skr-arab.json ├── sl.json ├── smn.json ├── sms.json ├── sq.json ├── sv.json ├── th.json ├── tr.json ├── uk.json ├── xmf.json ├── zh-hans.json └── zh-hant.json ├── package.json ├── pnpm-lock.yaml ├── static ├── css │ ├── comment.css │ ├── commentIcon.css │ └── main.css ├── js │ ├── commentBoxes.js │ ├── commentIcons.js │ ├── commentL10n.js │ ├── copyPasteEvents.js │ ├── index.js │ ├── jquery.tmpl.min.js │ ├── moment-with-locales.min.js │ ├── newComment.js │ ├── preCommentMark.js │ └── shared.js └── tests │ ├── backend │ └── specs │ │ ├── api │ │ ├── commentReplies.js │ │ ├── comments.js │ │ ├── exportEtherpad.js │ │ ├── exportHTML.js │ │ └── test.etherpad │ │ ├── padCopy.js │ │ ├── padRemove.js │ │ └── readOnlyPad.js │ ├── frontend │ ├── specs │ │ ├── commentDelete.js │ │ ├── commentEdit.js │ │ ├── commentIcons.js │ │ ├── commentReply.js │ │ ├── commentSuggestion.js │ │ ├── comment_l10n.js │ │ ├── comment_settings.js │ │ ├── newComment.js │ │ ├── preCommentMark.js │ │ ├── timeFormat.js │ │ └── xcommentCopyPaste.js │ └── utils.js │ └── utils.js └── templates ├── commentBarButtons.ejs ├── commentIcons.html ├── comments.html ├── layout.ejs ├── menuButtons.ejs ├── settings.ejs └── styles.html /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a workaround for https://github.com/eslint/eslint/issues/3458 4 | require('eslint-config-etherpad/patch/modern-module-resolution'); 5 | 6 | module.exports = { 7 | root: true, 8 | extends: 'etherpad/plugin', 9 | ignorePatterns: [ 10 | '/static/js/jquery.tmpl.min.js', 11 | '/static/js/moment-with-locales.min.js', 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | versioning-strategy: "increase" 12 | -------------------------------------------------------------------------------- /.github/workflows/backend-tests.yml: -------------------------------------------------------------------------------- 1 | name: Backend Tests 2 | 3 | # any branch is useful for testing before a PR is submitted 4 | on: 5 | workflow_call: 6 | 7 | jobs: 8 | withplugins: 9 | # run on pushes to any branch 10 | # run on PRs from external forks 11 | if: | 12 | (github.event_name != 'pull_request') 13 | || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) 14 | name: with Plugins 15 | runs-on: ubuntu-latest 16 | steps: 17 | - 18 | name: Install libreoffice 19 | uses: awalsh128/cache-apt-pkgs-action@v1.4.2 20 | with: 21 | packages: libreoffice libreoffice-pdfimport 22 | version: 1.0 23 | - 24 | name: Install etherpad core 25 | uses: actions/checkout@v3 26 | with: 27 | repository: ether/etherpad-lite 28 | path: etherpad-lite 29 | - uses: pnpm/action-setup@v3 30 | name: Install pnpm 31 | with: 32 | version: 8 33 | run_install: false 34 | - name: Get pnpm store directory 35 | shell: bash 36 | run: | 37 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 38 | - uses: actions/cache@v4 39 | name: Setup pnpm cache 40 | with: 41 | path: ${{ env.STORE_PATH }} 42 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 43 | restore-keys: | 44 | ${{ runner.os }}-pnpm-store- 45 | - 46 | name: Checkout plugin repository 47 | uses: actions/checkout@v3 48 | with: 49 | path: plugin 50 | - 51 | name: Determine plugin name 52 | id: plugin_name 53 | working-directory: ./plugin 54 | run: | 55 | npx -c 'printf %s\\n "::set-output name=plugin_name::${npm_package_name}"' 56 | - 57 | name: Link plugin directory 58 | working-directory: ./plugin 59 | run: | 60 | pnpm link --global 61 | - name: Remove tests 62 | working-directory: ./etherpad-lite 63 | run: rm -rf ./src/tests/backend/specs 64 | - 65 | name: Install Etherpad core dependencies 66 | working-directory: ./etherpad-lite 67 | run: bin/installDeps.sh 68 | - name: Link plugin to etherpad-lite 69 | working-directory: ./etherpad-lite 70 | run: | 71 | pnpm link --global $PLUGIN_NAME 72 | pnpm run install-plugins --path ../../plugin 73 | env: 74 | PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }} 75 | - name: Link ep_etherpad-lite 76 | working-directory: ./etherpad-lite/src 77 | run: | 78 | pnpm link --global 79 | - name: Link etherpad to plugin 80 | working-directory: ./plugin 81 | run: | 82 | pnpm link --global ep_etherpad-lite 83 | - 84 | name: Run the backend tests 85 | working-directory: ./etherpad-lite 86 | run: | 87 | res=$(find .. -path "./node_modules/ep_*/static/tests/backend/specs/**" | wc -l) 88 | if [ $res -eq 0 ]; then 89 | echo "No backend tests found" 90 | else 91 | pnpm run test 92 | fi 93 | -------------------------------------------------------------------------------- /.github/workflows/frontend-tests.yml: -------------------------------------------------------------------------------- 1 | # Publicly credit Sauce Labs because they generously support open source 2 | # projects. 3 | name: Frontend Tests 4 | 5 | on: 6 | workflow_call: 7 | 8 | jobs: 9 | test-frontend: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - 14 | name: Check out Etherpad core 15 | uses: actions/checkout@v3 16 | with: 17 | repository: ether/etherpad-lite 18 | - uses: pnpm/action-setup@v3 19 | name: Install pnpm 20 | with: 21 | version: 8 22 | run_install: false 23 | - name: Get pnpm store directory 24 | shell: bash 25 | run: | 26 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 27 | - uses: actions/cache@v4 28 | name: Setup pnpm cache 29 | with: 30 | path: ${{ env.STORE_PATH }} 31 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 32 | restore-keys: | 33 | ${{ runner.os }}-pnpm-store- 34 | - 35 | name: Check out the plugin 36 | uses: actions/checkout@v3 37 | with: 38 | path: ./node_modules/__tmp 39 | - 40 | name: export GIT_HASH to env 41 | id: environment 42 | run: | 43 | cd ./node_modules/__tmp 44 | echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" 45 | - 46 | name: Determine plugin name 47 | id: plugin_name 48 | run: | 49 | cd ./node_modules/__tmp 50 | npx -c 'printf %s\\n "::set-output name=plugin_name::${npm_package_name}"' 51 | - 52 | name: Rename plugin directory 53 | env: 54 | PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }} 55 | run: | 56 | mv ./node_modules/__tmp ./node_modules/"${PLUGIN_NAME}" 57 | - 58 | name: Install plugin dependencies 59 | env: 60 | PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }} 61 | run: | 62 | cd ./node_modules/"${PLUGIN_NAME}" 63 | pnpm i 64 | # Etherpad core dependencies must be installed after installing the 65 | # plugin's dependencies, otherwise npm will try to hoist common 66 | # dependencies by removing them from src/node_modules and installing them 67 | # in the top-level node_modules. As of v6.14.10, npm's hoist logic appears 68 | # to be buggy, because it sometimes removes dependencies from 69 | # src/node_modules but fails to add them to the top-level node_modules. 70 | # Even if npm correctly hoists the dependencies, the hoisting seems to 71 | # confuse tools such as `npm outdated`, `npm update`, and some ESLint 72 | # rules. 73 | - 74 | name: Install Etherpad core dependencies 75 | run: bin/installDeps.sh 76 | - name: Create settings.json 77 | run: cp ./src/tests/settings.json settings.json 78 | - name: Run the frontend tests 79 | shell: bash 80 | run: | 81 | pnpm run dev & 82 | connected=false 83 | can_connect() { 84 | curl -sSfo /dev/null http://localhost:9001/ || return 1 85 | connected=true 86 | } 87 | now() { date +%s; } 88 | start=$(now) 89 | while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do 90 | sleep 1 91 | done 92 | cd src 93 | pnpm exec playwright install chromium --with-deps 94 | pnpm run test-ui --project=chromium 95 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to the npm registry when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | workflow_call: 8 | 9 | jobs: 10 | publish-npm: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | registry-url: https://registry.npmjs.org/ 17 | - name: Check out Etherpad core 18 | uses: actions/checkout@v3 19 | with: 20 | repository: ether/etherpad-lite 21 | - uses: pnpm/action-setup@v3 22 | name: Install pnpm 23 | with: 24 | version: 9 25 | run_install: false 26 | - name: Get pnpm store directory 27 | shell: bash 28 | run: | 29 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 30 | - uses: actions/cache@v4 31 | name: Setup pnpm cache 32 | with: 33 | path: ${{ env.STORE_PATH }} 34 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 35 | restore-keys: | 36 | ${{ runner.os }}-pnpm-store- 37 | - 38 | uses: actions/checkout@v3 39 | with: 40 | fetch-depth: 0 41 | - 42 | name: Bump version (patch) 43 | run: | 44 | LATEST_TAG=$(git describe --tags --abbrev=0) || exit 1 45 | NEW_COMMITS=$(git rev-list --count "${LATEST_TAG}"..) || exit 1 46 | [ "${NEW_COMMITS}" -gt 0 ] || exit 0 47 | git config user.name 'github-actions[bot]' 48 | git config user.email '41898282+github-actions[bot]@users.noreply.github.com' 49 | pnpm i 50 | pnpm version patch 51 | git push --follow-tags 52 | # This is required if the package has a prepare script that uses something 53 | # in dependencies or devDependencies. 54 | - 55 | run: pnpm i 56 | # `npm publish` must come after `git push` otherwise there is a race 57 | # condition: If two PRs are merged back-to-back then master/main will be 58 | # updated with the commits from the second PR before the first PR's 59 | # workflow has a chance to push the commit generated by `npm version 60 | # patch`. This causes the first PR's `git push` step to fail after the 61 | # package has already been published, which in turn will cause all future 62 | # workflow runs to fail because they will all attempt to use the same 63 | # already-used version number. By running `npm publish` after `git push`, 64 | # back-to-back merges will cause the first merge's workflow to fail but 65 | # the second's will succeed. 66 | - 67 | run: pnpm publish 68 | env: 69 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 70 | #- 71 | # name: Add package to etherpad organization 72 | # run: pnpm access grant read-write etherpad:developers 73 | # env: 74 | # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 75 | -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | on: [push] 3 | 4 | 5 | jobs: 6 | backend: 7 | uses: ./.github/workflows/backend-tests.yml 8 | secrets: inherit 9 | frontend: 10 | uses: ./.github/workflows/frontend-tests.yml 11 | secrets: inherit 12 | release: 13 | if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }} 14 | needs: 15 | - backend 16 | - frontend 17 | uses: ./.github/workflows/npmpublish.yml 18 | secrets: inherit 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .ep_initialized 4 | node_modules/ 5 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | docs/ 3 | examples/ 4 | support/ 5 | test/ 6 | testing.js 7 | .DS_Store 8 | .ep_initialized -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "lts/*" 5 | 6 | cache: false 7 | 8 | services: 9 | - docker 10 | 11 | install: 12 | - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" 13 | 14 | #script: 15 | # - "tests/frontend/travis/runner.sh" 16 | 17 | env: 18 | global: 19 | - secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec=" 20 | - secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g=" 21 | 22 | jobs: 23 | include: 24 | - name: "Lint test package-lock" 25 | install: 26 | - "npm install lockfile-lint" 27 | script: 28 | - npx lockfile-lint --path package-lock.json --validate-https --allowed-hosts npm 29 | - name: "Run the Backend tests" 30 | before_script: 31 | - "tests/frontend/travis/sauce_tunnel.sh" 32 | before_install: 33 | - sudo add-apt-repository -y ppa:libreoffice/ppa 34 | - sudo apt-get update 35 | - sudo apt-get -y install libreoffice 36 | - sudo apt-get -y install libreoffice-pdfimport 37 | install: 38 | - "npm install" 39 | - "mkdir ep_comments_page" 40 | - "mv !(ep_comments_page) ep_comments_page" 41 | - "git clone https://github.com/ether/etherpad-lite.git etherpad" 42 | - "cd etherpad" 43 | - "mkdir node_modules" 44 | - "mv ../ep_comments_page node_modules" 45 | - "bin/installDeps.sh" 46 | - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" 47 | - "cd src && npm install && cd -" 48 | script: 49 | - "tests/frontend/travis/runnerBackend.sh" 50 | - name: "Test the Frontend" 51 | before_script: 52 | - "tests/frontend/travis/sauce_tunnel.sh" 53 | install: 54 | - "npm install" 55 | - "mkdir ep_comments_page" 56 | - "mv !(ep_comments_page) ep_comments_page" 57 | - "git clone https://github.com/ether/etherpad-lite.git etherpad" 58 | - "cd etherpad" 59 | - "mkdir node_modules" 60 | - "mv ../ep_comments_page node_modules" 61 | - "bin/installDeps.sh" 62 | - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" 63 | script: 64 | - "tests/frontend/travis/runner.sh" 65 | 66 | notifications: 67 | irc: 68 | channels: 69 | - "irc.freenode.org#etherpad-lite-dev" 70 | 71 | ##ETHERPAD_TRAVIS_V=999 72 | ## Travis configuration automatically created using bin/plugins/updateAllPluginsScript.sh 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Publish Status](https://github.com/ether/ep_comments_page/workflows/Node.js%20Package/badge.svg) ![Backend Tests Status](https://github.com/ether/ep_comments_page/workflows/Backend%20tests/badge.svg) 2 | 3 | # Comments and annotations for Etherpad 4 | 5 | ![Screen shot](https://user-images.githubusercontent.com/220864/98013526-617ff900-1df2-11eb-88b6-cf259372f6ca.PNG) 6 | 7 | ## Installing this plugin with npm. 8 | ``` 9 | npm install ep_comments_page 10 | ``` 11 | 12 | ## Extra settings 13 | This plugin has some extra features that can be enabled by changing values on `settings.json` of your Etherpad instance. 14 | 15 | ### Alternative comment display 16 | There is an alternative way to display comments. Instead of having all comments visible on the right of the page, you can have just an icon on the right margin of the page. Comment details are displayed when user clicks on the comment icon: 17 | 18 | ![Screen shot](http://i.imgur.com/cEo7PdL.png) 19 | 20 | To use this way of displaying comments, add the following to your `settings.json`: 21 | ``` 22 | // Display comments as icons, not boxes 23 | "ep_comments_page": { 24 | "displayCommentAsIcon": true 25 | }, 26 | ``` 27 | 28 | ### Highlight selected text when creating a comment 29 | It is also possible to mark the text originally selected when user adds a comment: 30 | ![Screen shot](http://i.imgur.com/AhaVgRZ.png) 31 | 32 | To enable this feature, add the following code to your `settings.json`: 33 | ``` 34 | // Highlight selected text when adding comment 35 | "ep_comments_page": { 36 | "highlightSelectedText": true 37 | }, 38 | ``` 39 | 40 | **Warning**: there is a side effect when you enable this feature: a revision is created everytime the text is highlighted, resulting on apparently "empty" changes when you check your pad on the timeslider. If that is an issue for you, we don't recommend you to use this feature. 41 | 42 | ### Disable HTML export 43 | By default comments are exported to HTML, but if you don't wish to do that then you can disable it by adding the following to your `settings.json`: 44 | ``` 45 | "ep_comments_page": { 46 | "exportHtml": false 47 | }, 48 | ``` 49 | 50 | ## Creating comment via API 51 | If you need to add comments to a pad: 52 | 53 | * Call this route to create the comments on Etherpad and get the comment ids: 54 | ``` 55 | curl -X POST http://localhost:9001/p/THE_PAD_ID/comments -d "apikey=YOUR_API_KEY" -d 'data=[{"name":"AUTHOR","text":"COMMENT"}, {"name":"ANOTHER_AUTHOR","text":"ANOTHER_COMMENT"}]' 56 | ``` 57 | 58 | The response will be: 59 | ``` 60 | {"code":0,"commentIds":["c-VEtzKolgD5krJOVU","c-B8MEmAT0NJ9usUwc"]} 61 | ``` 62 | 63 | * Use the returned comment ids to set the pad HTML [via API](http://etherpad.org/doc/v1.5.6/#index_sethtml_padid_html): 64 | ``` 65 | My comment goes here. 66 | ``` 67 | 68 | Result: 69 | ![Screen shot](http://i.imgur.com/KM4lPJx.png) 70 | 71 | NOTE: Adding a comment to a pad via API will make the other editors with that pad to be alerted, but this feature is only active if your Etherpad is run in `loadTest` mode. Read [the Etherpad Guide](https://github.com/ether/etherpad-lite/wiki/Load-Testing-Etherpad) for how to enable load testing. 72 | 73 | ## License 74 | Apache 2 75 | -------------------------------------------------------------------------------- /apiUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const absolutePaths = require('ep_etherpad-lite/node/utils/AbsolutePaths'); 4 | const fs = require('fs'); 5 | const padManager = require('ep_etherpad-lite/node/db/PadManager'); 6 | const settings = require('ep_etherpad-lite/node/utils/Settings'); 7 | 8 | // ensure we have an apiKey 9 | let apiKey = ''; 10 | try { 11 | apiKey = fs.readFileSync(absolutePaths.makeAbsolute('./APIKEY.txt'), 'utf8').trim(); 12 | } catch (e) { 13 | console.warn('Could not find APIKEY'); 14 | } 15 | 16 | // Checks if api key is correct and prepare response if it is not. 17 | // Returns true if valid, false otherwise. 18 | const validateApiKey = (fields, res) => { 19 | let valid = true; 20 | 21 | const apiKeyReceived = fields.apikey || fields.api_key; 22 | if (apiKeyReceived !== apiKey) { 23 | res.statusCode = 401; 24 | res.json({code: 4, message: 'no or wrong API Key', data: null}); 25 | valid = false; 26 | } 27 | 28 | return valid; 29 | }; 30 | 31 | const validateRequiredField = 32 | (originalFields, fieldName) => typeof originalFields[fieldName] !== 'undefined'; 33 | 34 | // Checks if required fields are present, and prepare response if any of them 35 | // is not. Returns true if valid, false otherwise. 36 | const validateRequiredFields = (originalFields, requiredFields, res) => { 37 | for (const requiredField of requiredFields) { 38 | if (!validateRequiredField(originalFields, requiredField)) { 39 | const errorMessage = `${requiredField} is required`; 40 | res.json({code: 1, message: errorMessage, data: null}); 41 | return false; 42 | } 43 | } 44 | return true; 45 | }; 46 | 47 | // Sanitizes pad id and returns it: 48 | const sanitizePadId = (req) => { 49 | let padIdReceived = req.params.pad; 50 | padManager.sanitizePadId(padIdReceived, (padId) => { 51 | padIdReceived = padId; 52 | }); 53 | 54 | return padIdReceived; 55 | }; 56 | 57 | // Builds url for message broadcasting, based on settings.json and on the 58 | // given endPoint: 59 | const broadcastUrlFor = (endPoint) => { 60 | let url = ''; 61 | if (settings.ssl) { 62 | url += 'https://'; 63 | } else { 64 | url += 'http://'; 65 | } 66 | url += `${settings.ip}:${settings.port}${endPoint}`; 67 | 68 | return url; 69 | }; 70 | 71 | /* ********** Available functions/values: ********** */ 72 | 73 | exports.validateApiKey = validateApiKey; 74 | exports.validateRequiredFields = validateRequiredFields; 75 | exports.sanitizePadId = sanitizePadId; 76 | exports.broadcastUrlFor = broadcastUrlFor; 77 | -------------------------------------------------------------------------------- /commentManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('underscore'); 4 | const db = require('ep_etherpad-lite/node/db/DB'); 5 | const log4js = require('ep_etherpad-lite/node_modules/log4js'); 6 | const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; 7 | const shared = require('./static/js/shared'); 8 | 9 | const logger = log4js.getLogger('ep_comments_page'); 10 | 11 | exports.getComments = async (padId) => { 12 | // Not sure if we will encouter race conditions here.. Be careful. 13 | 14 | // get the globalComments 15 | let comments = await db.get(`comments:${padId}`); 16 | if (comments == null) comments = {}; 17 | return {comments}; 18 | }; 19 | 20 | exports.deleteComment = async (padId, commentId, authorId) => { 21 | const comments = await db.get(`comments:${padId}`); 22 | if (comments == null || comments[commentId] == null) { 23 | logger.debug(`ignoring attempt to delete non-existent comment ${commentId}`); 24 | throw new Error('no_such_comment'); 25 | } 26 | if (comments[commentId].author !== authorId) { 27 | logger.debug(`author ${authorId} attempted to delete comment ${commentId} ` + 28 | `belonging to author ${comments[commentId].author}`); 29 | throw new Error('unauth'); 30 | } 31 | delete comments[commentId]; 32 | await db.set(`comments:${padId}`, comments); 33 | }; 34 | 35 | exports.deleteComments = async (padId) => { 36 | await db.remove(`comments:${padId}`); 37 | }; 38 | 39 | exports.addComment = async (padId, data) => { 40 | const [commentIds, comments] = await exports.bulkAddComments(padId, [data]); 41 | return [commentIds[0], comments[0]]; 42 | }; 43 | 44 | exports.bulkAddComments = async (padId, data) => { 45 | // get the entry 46 | let comments = await db.get(`comments:${padId}`); 47 | 48 | // the entry doesn't exist so far, let's create it 49 | if (comments == null) comments = {}; 50 | 51 | const newComments = []; 52 | const commentIds = data.map((commentData) => { 53 | // if the comment was copied it already has a commentID, so we don't need create one 54 | const commentId = commentData.commentId || shared.generateCommentId(); 55 | 56 | const comment = { 57 | author: commentData.author || 'empty', 58 | name: commentData.name, 59 | text: commentData.text, 60 | changeTo: commentData.changeTo, 61 | changeFrom: commentData.changeFrom, 62 | timestamp: parseInt(commentData.timestamp) || new Date().getTime(), 63 | }; 64 | // add the entry for this pad 65 | comments[commentId] = comment; 66 | 67 | newComments.push(comment); 68 | return commentId; 69 | }); 70 | 71 | // save the new element back 72 | await db.set(`comments:${padId}`, comments); 73 | 74 | return [commentIds, newComments]; 75 | }; 76 | 77 | exports.copyComments = async (originalPadId, newPadID) => { 78 | // get the comments of original pad 79 | const originalComments = await db.get(`comments:${originalPadId}`); 80 | // make sure we have different copies of the comment between pads 81 | const copiedComments = _.mapObject(originalComments, (thisComment) => _.clone(thisComment)); 82 | 83 | // save the comments on new pad 84 | await db.set(`comments:${newPadID}`, copiedComments); 85 | }; 86 | 87 | exports.getCommentReplies = async (padId) => { 88 | // get the globalComments replies 89 | let replies = await db.get(`comment-replies:${padId}`); 90 | // comment does not exist 91 | if (replies == null) replies = {}; 92 | return {replies}; 93 | }; 94 | 95 | exports.deleteCommentReplies = async (padId) => { 96 | await db.remove(`comment-replies:${padId}`); 97 | }; 98 | 99 | exports.addCommentReply = async (padId, data) => { 100 | const [replyIds, replies] = await exports.bulkAddCommentReplies(padId, [data]); 101 | return [replyIds[0], replies[0]]; 102 | }; 103 | 104 | exports.bulkAddCommentReplies = async (padId, data) => { 105 | // get the entry 106 | let replies = await db.get(`comment-replies:${padId}`); 107 | // the entry doesn't exist so far, let's create it 108 | if (replies == null) replies = {}; 109 | 110 | const newReplies = []; 111 | const replyIds = data.map((replyData) => { 112 | // create the new reply id 113 | const replyId = `c-reply-${randomString(16)}`; 114 | 115 | const metadata = replyData.comment || {}; 116 | 117 | const reply = { 118 | commentId: replyData.commentId, 119 | text: replyData.reply || replyData.text, 120 | changeTo: replyData.changeTo || null, 121 | changeFrom: replyData.changeFrom || null, 122 | author: metadata.author || 'empty', 123 | name: metadata.name || replyData.name, 124 | timestamp: parseInt(replyData.timestamp) || new Date().getTime(), 125 | }; 126 | 127 | // add the entry for this pad 128 | replies[replyId] = reply; 129 | 130 | newReplies.push(reply); 131 | return replyId; 132 | }); 133 | 134 | // save the new element back 135 | await db.set(`comment-replies:${padId}`, replies); 136 | 137 | return [replyIds, newReplies]; 138 | }; 139 | 140 | exports.copyCommentReplies = async (originalPadId, newPadID) => { 141 | // get the replies of original pad 142 | const originalReplies = await db.get(`comment-replies:${originalPadId}`); 143 | // make sure we have different copies of the reply between pads 144 | const copiedReplies = _.mapObject(originalReplies, (thisReply) => _.clone(thisReply)); 145 | 146 | // save the comment replies on new pad 147 | await db.set(`comment-replies:${newPadID}`, copiedReplies); 148 | }; 149 | 150 | exports.changeAcceptedState = async (padId, commentId, state) => { 151 | // Given a comment we update that comment to say the change was accepted or reverted 152 | 153 | // If we're dealing with comment replies we need to a different query 154 | let prefix = 'comments:'; 155 | if (commentId.substring(0, 7) === 'c-reply') { 156 | prefix = 'comment-replies:'; 157 | } 158 | 159 | // get the entry 160 | const comments = await db.get(prefix + padId); 161 | 162 | // add the entry for this pad 163 | const comment = comments[commentId]; 164 | 165 | if (state) { 166 | comment.changeAccepted = true; 167 | comment.changeReverted = false; 168 | } else { 169 | comment.changeAccepted = false; 170 | comment.changeReverted = true; 171 | } 172 | 173 | comments[commentId] = comment; 174 | 175 | // save the new element back 176 | await db.set(prefix + padId, comments); 177 | }; 178 | 179 | exports.changeCommentText = async (padId, commentId, commentText, authorId) => { 180 | if (commentText.length <= 0) { 181 | logger.debug(`ignoring attempt to change comment ${commentId} to the empty string`); 182 | throw new Error('comment_cannot_be_empty'); 183 | } 184 | 185 | // Given a comment we update the comment text 186 | 187 | // If we're dealing with comment replies we need to a different query 188 | let prefix = 'comments:'; 189 | if (commentId.substring(0, 7) === 'c-reply') { 190 | prefix = 'comment-replies:'; 191 | } 192 | 193 | // get the entry 194 | const comments = await db.get(prefix + padId); 195 | if (comments == null || comments[commentId] == null) { 196 | logger.debug(`ignoring attempt to edit non-existent comment ${commentId}`); 197 | throw new Error('no_such_comment'); 198 | } 199 | if (comments[commentId].author !== authorId) { 200 | logger.debug(`author ${authorId} attempted to edit comment ${commentId} ` + 201 | `belonging to author ${comments[commentId].author}`); 202 | throw new Error('unauth'); 203 | } 204 | // update the comment text 205 | comments[commentId].text = commentText; 206 | 207 | // save the comment updated back 208 | await db.set(prefix + padId, comments); 209 | }; 210 | -------------------------------------------------------------------------------- /ep.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "name":"comments_page", 5 | "pre": ["ep_etherpad-lite/webaccess", "ep_page_view/page_view"], 6 | "post": ["ep_etherpad-lite/static"], 7 | "client_hooks": { 8 | "postToolbarInit": "ep_comments_page/static/js/index", 9 | "postAceInit": "ep_comments_page/static/js/index", 10 | "collectContentPre": "ep_comments_page/static/js/shared", 11 | "aceAttribsToClasses": "ep_comments_page/static/js/index", 12 | "aceEditorCSS": "ep_comments_page/static/js/index", 13 | "aceEditEvent": "ep_comments_page/static/js/index", 14 | "aceInitialized": "ep_comments_page/static/js/index" 15 | }, 16 | "hooks": { 17 | "padInitToolbar": "ep_comments_page/index", 18 | "padRemove": "ep_comments_page/index", 19 | "padCopy": "ep_comments_page/index", 20 | "socketio": "ep_comments_page/index", 21 | "expressCreateServer": "ep_comments_page/index", 22 | "collectContentPre": "ep_comments_page/static/js/shared", 23 | "eejsBlock_editbarMenuLeft": "ep_comments_page/index", 24 | "eejsBlock_scripts": "ep_comments_page/index", 25 | "eejsBlock_mySettings": "ep_comments_page/index", 26 | "eejsBlock_styles": "ep_comments_page/index", 27 | "clientVars": "ep_comments_page/index", 28 | "exportHtmlAdditionalTagsWithData": "ep_comments_page/exportHTML", 29 | "getLineHTMLForExport": "ep_comments_page/exportHTML", 30 | "exportEtherpadAdditionalContent": "ep_comments_page/index", 31 | "exportHTMLAdditionalContent": "ep_comments_page/exportHTML", 32 | "handleMessageSecurity": "ep_comments_page/index" 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /exportHTML.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const $ = require('cheerio').load(''); 4 | const commentManager = require('./commentManager'); 5 | const settings = require('ep_etherpad-lite/node/utils/Settings'); 6 | 7 | // Iterate over pad attributes to find only the comment ones 8 | const findAllCommentUsedOn = (pad) => { 9 | const commentsUsed = []; 10 | pad.pool.eachAttrib((key, value) => { if (key === 'comment') commentsUsed.push(value); }); 11 | return commentsUsed; 12 | }; 13 | 14 | // Add the props to be supported in export 15 | exports.exportHtmlAdditionalTagsWithData = 16 | async (hookName, pad) => findAllCommentUsedOn(pad).map((name) => ['comment', name]); 17 | 18 | exports.getLineHTMLForExport = async (hookName, context) => { 19 | if (settings.ep_comments_page && settings.ep_comments_page.exportHtml === false) return; 20 | 21 | // I'm not sure how optimal this is - it will do a database lookup for each line.. 22 | const {comments} = await commentManager.getComments(context.padId); 23 | let hasPlugin = false; 24 | // Load the HTML into a throwaway div instead of calling $.load() to avoid 25 | // https://github.com/cheeriojs/cheerio/issues/1031 26 | const content = $('
').html(context.lineContent); 27 | // include links for each comment which we will add content later. 28 | content.find('span').each(function () { 29 | const span = $(this); 30 | const commentId = span.data('comment'); 31 | if (!commentId) return; // not a comment. please optimize me in selector 32 | if (!comments[commentId]) return; // if this comment has been deleted.. 33 | hasPlugin = true; 34 | span.append( 35 | $('').append( 36 | $('').attr('href', `#${commentId}`).text('*'))); 37 | // Replace data-comment="foo" with class="comment foo". 38 | if (/^c-[0-9a-zA-Z]+$/.test(commentId)) { 39 | span.removeAttr('data-comment').addClass('comment').addClass(commentId); 40 | } 41 | }); 42 | if (hasPlugin) context.lineContent = content.html(); 43 | }; 44 | 45 | exports.exportHTMLAdditionalContent = async (hookName, {padId}) => { 46 | if (settings.ep_comments_page && settings.ep_comments_page.exportHtml === false) return; 47 | const {comments} = await commentManager.getComments(padId); 48 | if (!comments) return; 49 | const div = $('
').attr('id', 'comments'); 50 | for (const [commentId, comment] of Object.entries(comments)) { 51 | div.append( 52 | $('

') 53 | .attr('role', 'comment') 54 | .addClass('comment') 55 | .attr('id', commentId) 56 | .text(`* ${comment.text}`)); 57 | } 58 | // adds additional HTML to the body, we get this HTML from the database of comments:padId 59 | return $.html(div); 60 | }; 61 | -------------------------------------------------------------------------------- /locales/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Meno25" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "تعليق", 8 | "ep_comments_page.comments": "تعليقات", 9 | "ep_comments_page.add_comment.title": "إضافة تعليق جديد على الاختيار", 10 | "ep_comments_page.add_comment": "إضافة تعليق جديد على الاختيار", 11 | "ep_comments_page.add_comment.hint": "يرجى أولا تحديد النص للتعليق", 12 | "ep_comments_page.delete_comment.title": "احذف هذا التعليق", 13 | "ep_comments_page.edit_comment.title": "قم بتحرير هذا التعليق", 14 | "ep_comments_page.show_comments": "إظهار التعليقات", 15 | "ep_comments_page.comments_template.suggested_change": "التغيير المقترح", 16 | "ep_comments_page.comments_template.from": "من", 17 | "ep_comments_page.comments_template.accept_change.value": "قبول التغيير", 18 | "ep_comments_page.comments_template.revert_change.value": "التراجع عن التغيير", 19 | "ep_comments_page.comments_template.suggested_change_from": "التغيير المقترح من \"{{changeFrom}}\" إلى \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "اقترح التغيير من \"{{changeFrom}}\" إلى", 21 | "ep_comments_page.comments_template.to": "إلى", 22 | "ep_comments_page.comments_template.include_suggestion": "تضمين التغيير المقترح", 23 | "ep_comments_page.comments_template.comment.value": "تعليق", 24 | "ep_comments_page.comments_template.cancel.value": "إلغاء", 25 | "ep_comments_page.comments_template.reply.value": "رد", 26 | "ep_comments_page.comments_template.reply.placeholder": "رد", 27 | "ep_comments_page.comments_template.edit_comment.save": "حفظ", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "إلغاء", 29 | "ep_comments_page.error.edit_unauth": "لا يمكنك تعديل تعليقات المستخدمين الآخرين!", 30 | "ep_comments_page.error.delete_unauth": "لا يمكنك حذف تعليقات المستخدمين الآخرين!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "আজিজ", 5 | "আফতাবুজ্জামান" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "মন্তব্য", 9 | "ep_comments_page.comments": "মন্তব্য", 10 | "ep_comments_page.delete_comment.title": "এই মন্তব্যটি মুছুন", 11 | "ep_comments_page.edit_comment.title": "এই মন্তব্যটি সম্পাদনা করুন", 12 | "ep_comments_page.show_comments": "মন্তব্য দেখান", 13 | "ep_comments_page.comments_template.suggested_change": "পরামর্শকৃত পরিবর্তন", 14 | "ep_comments_page.comments_template.comment.value": "মন্তব্য", 15 | "ep_comments_page.comments_template.cancel.value": "বাতিল", 16 | "ep_comments_page.comments_template.reply.value": "উত্তর দিন", 17 | "ep_comments_page.comments_template.reply.placeholder": "উত্তর দিন", 18 | "ep_comments_page.comments_template.edit_comment.save": "সংরক্ষণ", 19 | "ep_comments_page.comments_template.edit_comment.cancel": "বাতিল", 20 | "ep_comments_page.error.edit_unauth": "আপনি অন্য ব্যবহারকারীর মন্তব্য সম্পাদনা করতে পারবেন না!", 21 | "ep_comments_page.error.delete_unauth": "আপনি অন্য ব্যবহারকারীর মন্তব্য মুছে ফেলতে পারবেন না!" 22 | } 23 | -------------------------------------------------------------------------------- /locales/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Spotter" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Komentář", 8 | "ep_comments_page.comments": "Komentáře", 9 | "ep_comments_page.add_comment.title": "Přidat nový komentář k výběru", 10 | "ep_comments_page.add_comment": "Přidat nový komentář k výběru", 11 | "ep_comments_page.add_comment.hint": "Nejprve vyberte text, který chcete komentovat", 12 | "ep_comments_page.delete_comment.title": "Smazat tento komentář", 13 | "ep_comments_page.edit_comment.title": "Upravit tento komentář", 14 | "ep_comments_page.show_comments": "Zobrazit komentáře", 15 | "ep_comments_page.comments_template.suggested_change": "Navrhovaná změna", 16 | "ep_comments_page.comments_template.from": "Od", 17 | "ep_comments_page.comments_template.accept_change.value": "Přijmout změnu", 18 | "ep_comments_page.comments_template.revert_change.value": "Vrátit změnu", 19 | "ep_comments_page.comments_template.suggested_change_from": "Navrhovaná změna z „{{changeFrom}}“ na „{{changeTo}}“", 20 | "ep_comments_page.comments_template.suggest_change_from": "Navrhněte změnu z „{{changeFrom}}“ na", 21 | "ep_comments_page.comments_template.to": "Komu", 22 | "ep_comments_page.comments_template.include_suggestion": "Zahrnout navrhovanou změnu", 23 | "ep_comments_page.comments_template.comment.value": "Komentář", 24 | "ep_comments_page.comments_template.cancel.value": "Zrušit", 25 | "ep_comments_page.comments_template.reply.value": "Odpovědět", 26 | "ep_comments_page.comments_template.reply.placeholder": "Odpovědět", 27 | "ep_comments_page.comments_template.edit_comment.save": "Uložit", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "Zrušit", 29 | "ep_comments_page.error.edit_unauth": "Komentáře ostatních uživatelů nelze upravovat!", 30 | "ep_comments_page.error.delete_unauth": "Komentáře ostatních uživatelů nelze mazat!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Antonla", 5 | "Saederup92" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Kommentar", 9 | "ep_comments_page.comments": "Kommentarer", 10 | "ep_comments_page.add_comment.title": "Tilføj ny kommentar ved markering", 11 | "ep_comments_page.add_comment": "Tilføj ny kommentar ved markering", 12 | "ep_comments_page.add_comment.hint": "Marker venligst først teksten for at kommentere", 13 | "ep_comments_page.delete_comment.title": "Slet denne kommentar", 14 | "ep_comments_page.edit_comment.title": "Rediger denne kommentar", 15 | "ep_comments_page.show_comments": "Vis kommentarer", 16 | "ep_comments_page.comments_template.suggested_change": "Foreslået ændring", 17 | "ep_comments_page.comments_template.from": "Fra", 18 | "ep_comments_page.comments_template.accept_change.value": "Godkend ændring", 19 | "ep_comments_page.comments_template.revert_change.value": "Fortryd ændring", 20 | "ep_comments_page.comments_template.suggested_change_from": "Foreslået ændring fra \"{{changeFrom}}\" til \"{{changeTo}}\"", 21 | "ep_comments_page.comments_template.suggest_change_from": "Foreslå ændring fra \"{{changeFrom}}\" til", 22 | "ep_comments_page.comments_template.to": "Til", 23 | "ep_comments_page.comments_template.include_suggestion": "Inkluder foreslået ændring", 24 | "ep_comments_page.comments_template.comment.value": "Kommentar", 25 | "ep_comments_page.comments_template.cancel.value": "Annuller", 26 | "ep_comments_page.comments_template.reply.value": "Svar", 27 | "ep_comments_page.comments_template.reply.placeholder": "Svar", 28 | "ep_comments_page.comments_template.edit_comment.save": "gem", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "annuller", 30 | "ep_comments_page.error.edit_unauth": "Du kan ikke redigere andre brugeres kommentarer!", 31 | "ep_comments_page.error.delete_unauth": "Du kan ikke slette andre brugeres kommentarer!" 32 | } 33 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Brettchenweber", 5 | "SamTV", 6 | "Tim.krieger" 7 | ] 8 | }, 9 | "ep_comments_page.comment": "Kommentar", 10 | "ep_comments_page.comments": "Kommentare", 11 | "ep_comments_page.add_comment.title": "Kommentar zur Auswahl hinzufügen", 12 | "ep_comments_page.add_comment": "Kommentar zur Auswahl hinzufügen", 13 | "ep_comments_page.add_comment.hint": "Bitte wähle zuerst den zu kommentierenden Text aus!", 14 | "ep_comments_page.delete_comment.title": "Diesen Kommentar löschen", 15 | "ep_comments_page.edit_comment.title": "Diesen Kommentar bearbeiten", 16 | "ep_comments_page.show_comments": "Kommentare anzeigen", 17 | "ep_comments_page.comments_template.suggested_change": "Vorgeschlagene Änderung", 18 | "ep_comments_page.comments_template.from": "von", 19 | "ep_comments_page.comments_template.accept_change.value": "Änderung akzeptieren", 20 | "ep_comments_page.comments_template.revert_change.value": "Änderung zurücknehmen", 21 | "ep_comments_page.comments_template.suggested_change_from": "Ersetze \"{{changeFrom}}\" durch \"{{changeTo}}\"", 22 | "ep_comments_page.comments_template.suggest_change_from": "Änderung vorschlagen von \"{{changeFrom}}\" zu", 23 | "ep_comments_page.comments_template.to": "zu", 24 | "ep_comments_page.comments_template.include_suggestion": "Änderungsvorschlag hinzufügen", 25 | "ep_comments_page.comments_template.comment.value": "Kommentar", 26 | "ep_comments_page.comments_template.cancel.value": "Abbrechen", 27 | "ep_comments_page.comments_template.reply.value": "Antworten", 28 | "ep_comments_page.comments_template.reply.placeholder": "Antworten", 29 | "ep_comments_page.comments_template.edit_comment.save": "Speichern", 30 | "ep_comments_page.comments_template.edit_comment.cancel": "Abbrechen", 31 | "ep_comments_page.error.edit_unauth": "Du kannst Kommentare anderer Benutzer nicht bearbeiten!", 32 | "ep_comments_page.error.delete_unauth": "Du kannst Kommentare anderer Benutzer nicht löschen!" 33 | } 34 | -------------------------------------------------------------------------------- /locales/diq.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "GolyatGeri", 5 | "Mirzali" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Mışewre", 9 | "ep_comments_page.comments": "Mışewrehi", 10 | "ep_comments_page.add_comment.title": "Weçinay rên yo mısewreh tede kerên", 11 | "ep_comments_page.add_comment": "Weçinay rên yo mısewreh tede kerên", 12 | "ep_comments_page.add_comment.hint": "Veri mışore kerdeye metini weçinên", 13 | "ep_comments_page.delete_comment.title": "Nê mışori bıester", 14 | "ep_comments_page.edit_comment.title": "Nê mışori bıvurne", 15 | "ep_comments_page.show_comments": "Mışori bıvine", 16 | "ep_comments_page.comments_template.suggested_change": "Wesebyaye Vurnayış", 17 | "ep_comments_page.comments_template.from": "Rıster", 18 | "ep_comments_page.comments_template.accept_change.value": "Vurnayışa qebul bıkerê", 19 | "ep_comments_page.comments_template.revert_change.value": "Vurnayışi bıgêrên pey", 20 | "ep_comments_page.comments_template.suggested_change_from": "Wesebyaye Vurnayış \"{{changeFrom}}\" ra heta \"{{changeTo}}\"", 21 | "ep_comments_page.comments_template.suggest_change_from": "Wesebyaye Vurnayış \"{{changeFrom}}\" heta", 22 | "ep_comments_page.comments_template.to": "Gırewter", 23 | "ep_comments_page.comments_template.include_suggestion": "Wesebyaye vurnayışi dexil kerên", 24 | "ep_comments_page.comments_template.comment.value": "Mışewre", 25 | "ep_comments_page.comments_template.cancel.value": "Bıtexelne", 26 | "ep_comments_page.comments_template.reply.value": "Cewab bıde", 27 | "ep_comments_page.comments_template.reply.placeholder": "Cewab bıde", 28 | "ep_comments_page.comments_template.edit_comment.save": "qeyd ke", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "bıtexelne", 30 | "ep_comments_page.error.edit_unauth": "Şoma nêeşken mısorey karkeranên binan bıvurnên", 31 | "ep_comments_page.error.delete_unauth": "Şoma nêeşken mısorey karkeranên binan esternin" 32 | } 33 | -------------------------------------------------------------------------------- /locales/dsb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Michawiki" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Komentar", 8 | "ep_comments_page.comments": "Komentary", 9 | "ep_comments_page.add_comment.title": "K wuběrkoju nowy komentar pśidaś", 10 | "ep_comments_page.add_comment": "K wuběrkoju nowy komentar pśidaś", 11 | "ep_comments_page.add_comment.hint": "Pšosym wubjeŕśo nejpjerwjej tekst, kótaryž se ma komentěrowaś", 12 | "ep_comments_page.delete_comment.title": "Toś ten komentar wulašowaś", 13 | "ep_comments_page.edit_comment.title": "Toś ten komentar wobźěłaś", 14 | "ep_comments_page.show_comments": "Komentary pokazaś", 15 | "ep_comments_page.comments_template.suggested_change": "Naraźona změna", 16 | "ep_comments_page.comments_template.from": "Wót", 17 | "ep_comments_page.comments_template.accept_change.value": "Změnu akceptěrowaś", 18 | "ep_comments_page.comments_template.revert_change.value": "Změnu anulěrowaś", 19 | "ep_comments_page.comments_template.suggested_change_from": "Naraźona změna wót \"{{changeFrom}}\" do \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Změnu naraźiś wót \"{{changeFrom}}\" do", 21 | "ep_comments_page.comments_template.to": "Do", 22 | "ep_comments_page.comments_template.include_suggestion": "Naraźonu změnu zapśimjeś", 23 | "ep_comments_page.comments_template.comment.value": "Komentar", 24 | "ep_comments_page.comments_template.cancel.value": "Pśetergnuś", 25 | "ep_comments_page.comments_template.reply.value": "Wótegroniś", 26 | "ep_comments_page.comments_template.reply.placeholder": "Wótegroniś", 27 | "ep_comments_page.comments_template.edit_comment.save": "składowaś", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "pśetergnuś", 29 | "ep_comments_page.error.edit_unauth": "Njamóžoš komentary drugich wužywarjow wobźěłaś!", 30 | "ep_comments_page.error.delete_unauth": "Njamóžoš komentary drugich wužywarjow lašowaś!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Norhorn" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Σχόλιο", 8 | "ep_comments_page.comments": "Σχόλια", 9 | "ep_comments_page.add_comment.title": "Προσθήκη νέου σχολίου στην επιλογή", 10 | "ep_comments_page.add_comment": "Προσθήκη νέου σχολίου στην επιλογή", 11 | "ep_comments_page.add_comment.hint": "Επιλέξτε πρώτα το κείμενο για να σχολιάσετε", 12 | "ep_comments_page.delete_comment.title": "Διαγραφή αυτού του σχόλιου", 13 | "ep_comments_page.edit_comment.title": "Επεξεργασία αυτού του σχόλιου", 14 | "ep_comments_page.show_comments": "Προβολή σχολίων", 15 | "ep_comments_page.comments_template.suggested_change": "Προτεινόμενη αλλαγή", 16 | "ep_comments_page.comments_template.from": "Από", 17 | "ep_comments_page.comments_template.accept_change.value": "Αποδοχή αλλαγής", 18 | "ep_comments_page.comments_template.revert_change.value": "Επαναφορά αλλαγής", 19 | "ep_comments_page.comments_template.suggested_change_from": "Προτεινόμενη αλλαγή από \"{{changeFrom}}\" σε \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Πρόταση αλλαγής από \"{{changeFrom}}\" σε", 21 | "ep_comments_page.comments_template.to": "Προς", 22 | "ep_comments_page.comments_template.include_suggestion": "Να συμπεριληφθεί η προτεινόμενη αλλαγή", 23 | "ep_comments_page.comments_template.comment.value": "Σχόλιο", 24 | "ep_comments_page.comments_template.cancel.value": "Ακύρωση", 25 | "ep_comments_page.comments_template.reply.value": "Απάντηση", 26 | "ep_comments_page.comments_template.reply.placeholder": "Απάντηση", 27 | "ep_comments_page.comments_template.edit_comment.save": "Αποθήκευση", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "ακύρωση", 29 | "ep_comments_page.error.edit_unauth": "Δεν μπορείτε να επεξεργαστείτε τα σχόλια άλλων χρηστών!", 30 | "ep_comments_page.error.delete_unauth": "Δεν μπορείτε να διαγράψετε σχόλια άλλων χρηστών!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "ep_comments_page.comment" : "Comment", 3 | "ep_comments_page.comments" : "Comments", 4 | "ep_comments_page.add_comment.title" : "Add new comment on selection", 5 | "ep_comments_page.add_comment" : "Add new comment on selection", 6 | "ep_comments_page.add_comment.hint" : "Please first select the text to comment", 7 | "ep_comments_page.delete_comment.title" : "Delete this comment", 8 | "ep_comments_page.edit_comment.title" : "Edit this comment", 9 | "ep_comments_page.show_comments" : "Show Comments", 10 | "ep_comments_page.comments_template.suggested_change" : "Suggested Change", 11 | "ep_comments_page.comments_template.from" : "From", 12 | "ep_comments_page.comments_template.accept_change.value" : "Accept Change", 13 | "ep_comments_page.comments_template.revert_change.value" : "Revert Change", 14 | "ep_comments_page.comments_template.suggested_change_from" : "Suggested change from \"{{changeFrom}}\" to \"{{changeTo}}\"", 15 | "ep_comments_page.comments_template.suggest_change_from" : "Suggest change from \"{{changeFrom}}\" to", 16 | "ep_comments_page.comments_template.to" : "To", 17 | "ep_comments_page.comments_template.include_suggestion" : "Include suggested change", 18 | "ep_comments_page.comments_template.comment.value" : "Comment", 19 | "ep_comments_page.comments_template.cancel.value" : "Cancel", 20 | "ep_comments_page.comments_template.reply.value" : "Reply", 21 | "ep_comments_page.comments_template.reply.placeholder" : "Reply", 22 | "ep_comments_page.comments_template.edit_comment.save" : "save", 23 | "ep_comments_page.comments_template.edit_comment.cancel" :"cancel", 24 | "ep_comments_page.error.edit_unauth": "You cannot edit other users comments!", 25 | "ep_comments_page.error.delete_unauth": "You cannot delete other users comments!" 26 | } 27 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Avengium", 5 | "Jakeukalane" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Comentario", 9 | "ep_comments_page.comments": "Comentarios", 10 | "ep_comments_page.add_comment.hint": "Primero selecciona el texto para comentar", 11 | "ep_comments_page.edit_comment.title": "Editar este comentario", 12 | "ep_comments_page.comments_template.accept_change.value": "Aceptar cambio", 13 | "ep_comments_page.comments_template.revert_change.value": "Revertir cambio", 14 | "ep_comments_page.comments_template.suggested_change_from": "Cambio sugerido de \"{{changeFrom}}\" a \"{{changeTo}}\"", 15 | "ep_comments_page.comments_template.suggest_change_from": "Sugerir cambio de \"{{changeFrom}}\" a", 16 | "ep_comments_page.comments_template.include_suggestion": "Incluir cambio sugerido", 17 | "ep_comments_page.comments_template.comment.value": "Comentario", 18 | "ep_comments_page.comments_template.cancel.value": "Cancelar", 19 | "ep_comments_page.comments_template.reply.value": "Responder", 20 | "ep_comments_page.comments_template.edit_comment.save": "Guardar", 21 | "ep_comments_page.comments_template.edit_comment.cancel": "cancelar", 22 | "ep_comments_page.error.edit_unauth": "¡No puedes editar los comentarios de otros usuarios!", 23 | "ep_comments_page.error.delete_unauth": "¡No puedes eliminar los comentarios de otros usuarios!" 24 | } 25 | -------------------------------------------------------------------------------- /locales/eu.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Izendegi" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Iruzkina", 8 | "ep_comments_page.comments": "Iruzkinak", 9 | "ep_comments_page.add_comment.title": "Gehitu iruzkin berria hautapenean", 10 | "ep_comments_page.add_comment": "Gehitu iruzkin berria hautapenean", 11 | "ep_comments_page.add_comment.hint": "Aukeratu ezazu lehenengo iruzkina gehitzeko testua", 12 | "ep_comments_page.delete_comment.title": "Ezabatu iruzkin hau", 13 | "ep_comments_page.edit_comment.title": "Editatu iruzkin hau", 14 | "ep_comments_page.show_comments": "Erakutsi iruzkinak", 15 | "ep_comments_page.comments_template.suggested_change": "Proposatutako aldaketa", 16 | "ep_comments_page.comments_template.from": "Jatorria", 17 | "ep_comments_page.comments_template.accept_change.value": "Onartu aldaketa", 18 | "ep_comments_page.comments_template.revert_change.value": "Leheneratu aldaketa", 19 | "ep_comments_page.comments_template.suggested_change_from": "Proposatutako aldaketa \"{{changeFrom}}\"-(e)tik \"{{changeTo}}\"-(e)ra", 20 | "ep_comments_page.comments_template.suggest_change_from": "Proposatu aldaketa \"{{changeFrom}}\"(e)tik hurrengo honetara:", 21 | "ep_comments_page.comments_template.to": "Ondorengoa", 22 | "ep_comments_page.comments_template.include_suggestion": "Gehitu proposatutako aldaketak", 23 | "ep_comments_page.comments_template.comment.value": "Iruzkina", 24 | "ep_comments_page.comments_template.cancel.value": "Utzi", 25 | "ep_comments_page.comments_template.reply.value": "Erantzun", 26 | "ep_comments_page.comments_template.reply.placeholder": "Erantzun", 27 | "ep_comments_page.comments_template.edit_comment.save": "gorde", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "utzi", 29 | "ep_comments_page.error.edit_unauth": "Ezin dituzu beste erabiltzaileen iruzkinak editatu!", 30 | "ep_comments_page.error.delete_unauth": "Ezin dituzu beste erabiltzaileen iruzkinak ezabatu!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/fa.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Darafsh", 5 | "Jeeputer" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "توضیح", 9 | "ep_comments_page.comments": "نظرات", 10 | "ep_comments_page.add_comment.title": "افزودن توضیح تازه برای منتخب‌ها", 11 | "ep_comments_page.add_comment": "افزودن توضیح تازه برای منتخب‌ها", 12 | "ep_comments_page.add_comment.hint": "لطفاً ابتدا متن را برای نظر دادن انتخاب کنید", 13 | "ep_comments_page.delete_comment.title": "حذف این نظر", 14 | "ep_comments_page.edit_comment.title": "ویرایش این نظر", 15 | "ep_comments_page.show_comments": "نمایش نظرات", 16 | "ep_comments_page.comments_template.suggested_change": "تغییر پیشنهادی", 17 | "ep_comments_page.comments_template.from": "از", 18 | "ep_comments_page.comments_template.accept_change.value": "پذیرفتن تغییر", 19 | "ep_comments_page.comments_template.revert_change.value": "واگردانی تغییر", 20 | "ep_comments_page.comments_template.suggested_change_from": "تغییر پیسنهادی از «{{changeFrom}}» به «{{changeTo}}»", 21 | "ep_comments_page.comments_template.suggest_change_from": "تغییر پیشنهاد از «{{changeFrom}}» به", 22 | "ep_comments_page.comments_template.to": "به", 23 | "ep_comments_page.comments_template.include_suggestion": "گنجاندن تغییر پیشنهادی", 24 | "ep_comments_page.comments_template.comment.value": "نظر", 25 | "ep_comments_page.comments_template.cancel.value": "لغو", 26 | "ep_comments_page.comments_template.reply.value": "پاسخ", 27 | "ep_comments_page.comments_template.reply.placeholder": "پاسخ", 28 | "ep_comments_page.comments_template.edit_comment.save": "ذخیره", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "لغو", 30 | "ep_comments_page.error.edit_unauth": "نمی‌توانید نظرات کاربران دیگر را ویرایش کنید!", 31 | "ep_comments_page.error.delete_unauth": "نمی‌توانید نظرات کاربران دیگر را حذف کنید!" 32 | } 33 | -------------------------------------------------------------------------------- /locales/ff.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ibrahima Malal Sarr" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Yowre", 8 | "ep_comments_page.comments": "Jowe", 9 | "ep_comments_page.add_comment.title": "Ɓeydu yowre hesere e labagol", 10 | "ep_comments_page.add_comment": "Ɓeydu yowre hesere e labagol", 11 | "ep_comments_page.add_comment.hint": "Tiiɗno labo tawo bonndol ngol njiɗ-ɗaa yowde e mum", 12 | "ep_comments_page.delete_comment.title": "Momtu ndee yowre", 13 | "ep_comments_page.edit_comment.title": "Taƴto ndee yowre", 14 | "ep_comments_page.show_comments": "Hollu Jowe", 15 | "ep_comments_page.comments_template.suggested_change": "Wasiyo Bayle", 16 | "ep_comments_page.comments_template.from": "Iwde e", 17 | "ep_comments_page.comments_template.accept_change.value": "Jaɓ Baylol", 18 | "ep_comments_page.comments_template.revert_change.value": "Firlit Baylol", 19 | "ep_comments_page.comments_template.suggested_change_from": "Wasiya baylol iwde e {{changeFrom}} wonta {{changeTo}}", 20 | "ep_comments_page.comments_template.suggest_change_from": "Wasiyo baylol iwde e {{changeFrom}} wonta", 21 | "ep_comments_page.comments_template.to": "Wonta", 22 | "ep_comments_page.comments_template.include_suggestion": "Waɗor heen baylol basiyangol", 23 | "ep_comments_page.comments_template.comment.value": "Ɓeydu yowre", 24 | "ep_comments_page.comments_template.cancel.value": "Haaytu", 25 | "ep_comments_page.comments_template.reply.value": "Jaabo", 26 | "ep_comments_page.comments_template.reply.placeholder": "jaabawol", 27 | "ep_comments_page.comments_template.edit_comment.save": "danndu", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "haaytu", 29 | "ep_comments_page.error.edit_unauth": "A waawaa taƴtaade jowe woɗɓe!", 30 | "ep_comments_page.error.delete_unauth": "A waawaa momtude jowe woɗɓe!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Artnay", 5 | "MITO" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Kommentoi", 9 | "ep_comments_page.comments": "Kommentit", 10 | "ep_comments_page.add_comment.title": "Lisää uusi kommentti valintaan", 11 | "ep_comments_page.add_comment": "Lisää uusi kommentti valintaan", 12 | "ep_comments_page.add_comment.hint": "Valitse ensin teksti, jota haluat kommentoida", 13 | "ep_comments_page.delete_comment.title": "Poista tämä kommentti", 14 | "ep_comments_page.edit_comment.title": "Muokkaa tätä kommenttia", 15 | "ep_comments_page.show_comments": "Näytä kommentit", 16 | "ep_comments_page.comments_template.suggested_change": "Muutosehdotus", 17 | "ep_comments_page.comments_template.from": "Lähettäjä", 18 | "ep_comments_page.comments_template.accept_change.value": "Hyväksy muutos", 19 | "ep_comments_page.comments_template.revert_change.value": "Peruuta muutos", 20 | "ep_comments_page.comments_template.to": "Vastaanottaja", 21 | "ep_comments_page.comments_template.include_suggestion": "Sisällytä ehdotettu muutos", 22 | "ep_comments_page.comments_template.comment.value": "Kommentoi", 23 | "ep_comments_page.comments_template.cancel.value": "Peruuta", 24 | "ep_comments_page.comments_template.reply.value": "Vastaa", 25 | "ep_comments_page.comments_template.reply.placeholder": "Vastaa", 26 | "ep_comments_page.comments_template.edit_comment.save": "tallenna", 27 | "ep_comments_page.comments_template.edit_comment.cancel": "peruuta", 28 | "ep_comments_page.error.edit_unauth": "Et voi muokata toisten käyttäjien kommentteja!", 29 | "ep_comments_page.error.delete_unauth": "Et voi poistaa muiden käyttäjien kommentteja!" 30 | } 31 | -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Verdy p" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Annotation", 8 | "ep_comments_page.comments": "Annotations", 9 | "ep_comments_page.add_comment.title": "Annoter la sélection", 10 | "ep_comments_page.add_comment": "Annoter la sélection", 11 | "ep_comments_page.add_comment.hint": "Vous devez d'abord sélectionner un texte à annoter", 12 | "ep_comments_page.delete_comment.title": "Supprimer cette annotation", 13 | "ep_comments_page.edit_comment.title": "Modifier cette annotation", 14 | "ep_comments_page.show_comments": "Afficher les annotations", 15 | "ep_comments_page.comments_template.suggested_change": "Modification proposée", 16 | "ep_comments_page.comments_template.from": "Remplacer", 17 | "ep_comments_page.comments_template.accept_change.value": "Appliquer la proposition", 18 | "ep_comments_page.comments_template.revert_change.value": "Annuler la proposition", 19 | "ep_comments_page.comments_template.suggested_change_from": "Propose de remplacer « {{changeFrom}} » par « {{changeTo}} »", 20 | "ep_comments_page.comments_template.suggest_change_from": "Remplacer « {{changeFrom}} » par", 21 | "ep_comments_page.comments_template.to": "Par", 22 | "ep_comments_page.comments_template.include_suggestion": "Inclure la modification suggérée", 23 | "ep_comments_page.comments_template.comment.value": "Annotation", 24 | "ep_comments_page.comments_template.cancel.value": "Annuler", 25 | "ep_comments_page.comments_template.reply.value": "Répondre", 26 | "ep_comments_page.comments_template.reply.placeholder": "Répondre", 27 | "ep_comments_page.comments_template.edit_comment.save": "enregistrer", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "annuler", 29 | "ep_comments_page.error.edit_unauth": "Vous ne pouvez pas modifier les commentaires des autres utilisatrices ou utilisateurs !", 30 | "ep_comments_page.error.delete_unauth": "Vous ne pouvez pas supprimer les commentaires des autres utilisatrices et utilisateurs !" 31 | } 32 | -------------------------------------------------------------------------------- /locales/gl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ghose" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Comentar", 8 | "ep_comments_page.comments": "Comentarios", 9 | "ep_comments_page.add_comment.title": "Engdir novo comentario na selección", 10 | "ep_comments_page.add_comment": "Engadir novo comentario na selección", 11 | "ep_comments_page.add_comment.hint": "Primeiro elixa o texto a comentar", 12 | "ep_comments_page.delete_comment.title": "Eliminar este comentario", 13 | "ep_comments_page.edit_comment.title": "Editar este comentario", 14 | "ep_comments_page.show_comments": "Mostrar comentarios", 15 | "ep_comments_page.comments_template.suggested_change": "Cambio suxerido", 16 | "ep_comments_page.comments_template.from": "Desde", 17 | "ep_comments_page.comments_template.accept_change.value": "Aceptar cambio", 18 | "ep_comments_page.comments_template.revert_change.value": "Rexeitar o cambio", 19 | "ep_comments_page.comments_template.suggested_change_from": "Cambio suxerido de \"{{changeFrom}}\" a \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Suxerir o cambio de \"{{changeFrom}}\" a", 21 | "ep_comments_page.comments_template.to": "Para", 22 | "ep_comments_page.comments_template.include_suggestion": "Incluír cambio suxerido", 23 | "ep_comments_page.comments_template.comment.value": "Comentar", 24 | "ep_comments_page.comments_template.cancel.value": "Cancelar", 25 | "ep_comments_page.comments_template.reply.value": "Responder", 26 | "ep_comments_page.comments_template.reply.placeholder": "Responder", 27 | "ep_comments_page.comments_template.edit_comment.save": "gardar", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "cancelar", 29 | "ep_comments_page.error.edit_unauth": "Non pode editar os comentarios doutras usuarias!", 30 | "ep_comments_page.error.delete_unauth": "Non pode eliminar os comentarios doutras usuarias!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/gur.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Adignyoke", 5 | "Akakiiri", 6 | "Akoonaba", 7 | "Amoramah" 8 | ] 9 | }, 10 | "ep_comments_page.comment": "Fu yelisum", 11 | "ep_comments_page.comments": "Lebesegɔ", 12 | "ep_comments_page.add_comment.title": "Pa'asɛ putɛpaalɛ loisego zuo", 13 | "ep_comments_page.add_comment": "Pa'asɛ yelesum paalega bo fu loorɛ", 14 | "ep_comments_page.add_comment.hint": "Zaam zaam loe gɔleseko ti fu yeti fu pa'asɛ la yia", 15 | "ep_comments_page.delete_comment.title": "Yesi lebesegɔ wã basɛ", 16 | "ep_comments_page.edit_comment.title": "Demese sɔsekãna wa", 17 | "ep_comments_page.show_comments": "Pa'ale fu yelesum", 18 | "ep_comments_page.comments_template.suggested_change": "Pa'asɛ putɛ̃'ɛrɛ tee", 19 | "ep_comments_page.comments_template.from": "Ze'ele", 20 | "ep_comments_page.comments_template.accept_change.value": "Sakɛ Teerɛ", 21 | "ep_comments_page.comments_template.revert_change.value": "Lebege tee", 22 | "ep_comments_page.comments_template.suggested_change_from": "Puti'irɛ teere ze'ele\"{{tee ze'ele}}\" bo \"{{ tee bo}}\"", 23 | "ep_comments_page.comments_template.suggest_change_from": "Pa'asɛ putɛ̃'ɛrɛ teere ze'ele{{ teere ze'ele}}", 24 | "ep_comments_page.comments_template.to": "Wɛ̃", 25 | "ep_comments_page.comments_template.include_suggestion": "Pa'asɛ putɛ̃'ɛrɛ teere", 26 | "ep_comments_page.comments_template.comment.value": "Lebese", 27 | "ep_comments_page.comments_template.cancel.value": "Gu basɛ", 28 | "ep_comments_page.comments_template.reply.value": "Lerege", 29 | "ep_comments_page.comments_template.reply.placeholder": "Lerege", 30 | "ep_comments_page.comments_template.edit_comment.save": "Seefe", 31 | "ep_comments_page.comments_template.edit_comment.cancel": "Saabasɛ", 32 | "ep_comments_page.error.edit_unauth": "Fu kan ta'am demese se'em sɔsega", 33 | "ep_comments_page.error.delete_unauth": "Fu kanta'am saalum basɛba putɛra basɛ" 34 | } 35 | -------------------------------------------------------------------------------- /locales/ha.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Abbaty", 5 | "Omar Ali", 6 | "Salihu aliyu" 7 | ] 8 | }, 9 | "ep_comments_page.comment": "Ra'ayi", 10 | "ep_comments_page.comments": "sharhi", 11 | "ep_comments_page.add_comment.title": "Kara sabon ra'ayi akan zaben", 12 | "ep_comments_page.add_comment": "saka sabon bayani a bangaren", 13 | "ep_comments_page.add_comment.hint": "zaba farkon sakon sharhi", 14 | "ep_comments_page.delete_comment.title": "goge wannan sharhin", 15 | "ep_comments_page.edit_comment.title": "ghera wannan sharhin", 16 | "ep_comments_page.show_comments": "nuna sharhi", 17 | "ep_comments_page.comments_template.suggested_change": "chanza shawara", 18 | "ep_comments_page.comments_template.from": "daga", 19 | "ep_comments_page.comments_template.accept_change.value": "karban chanji", 20 | "ep_comments_page.comments_template.revert_change.value": "dawo da chanji", 21 | "ep_comments_page.comments_template.to": "zuwa", 22 | "ep_comments_page.comments_template.include_suggestion": "sanya damar sauyi", 23 | "ep_comments_page.comments_template.comment.value": "Ra'ayi", 24 | "ep_comments_page.comments_template.cancel.value": "sokewa", 25 | "ep_comments_page.comments_template.reply.value": "Maidawa", 26 | "ep_comments_page.comments_template.reply.placeholder": "maidawa", 27 | "ep_comments_page.comments_template.edit_comment.save": "adanawa", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "soke", 29 | "ep_comments_page.error.edit_unauth": "bazaka iya gheran sharhin wani bah", 30 | "ep_comments_page.error.delete_unauth": "bazaka iya goge sharhin wani bah" 31 | } 32 | -------------------------------------------------------------------------------- /locales/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Amire80" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "הערה", 8 | "ep_comments_page.comments": "הערות", 9 | "ep_comments_page.add_comment.title": "הוספת הערה חדשה לבחירה", 10 | "ep_comments_page.add_comment": "הוספת הערה חדשה לבחירה", 11 | "ep_comments_page.add_comment.hint": "נא לבחור קודם טקסט כדי להעיר", 12 | "ep_comments_page.delete_comment.title": "למחוק את ההערה הזאת", 13 | "ep_comments_page.edit_comment.title": "לערוך את ההערה הזאת", 14 | "ep_comments_page.show_comments": "הצגת ההערה", 15 | "ep_comments_page.comments_template.suggested_change": "שינוי מוצע", 16 | "ep_comments_page.comments_template.from": "מאת", 17 | "ep_comments_page.comments_template.accept_change.value": "קבלת השינוי", 18 | "ep_comments_page.comments_template.revert_change.value": "שחזור השינוי", 19 | "ep_comments_page.comments_template.suggested_change_from": "שינוי מוצע מהטקסט \"{{changeFrom}}\" לטקסט \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "להציע שינוי של הטקסט \"{{changeFrom}}\" לטקסט", 21 | "ep_comments_page.comments_template.to": "לטקסט", 22 | "ep_comments_page.comments_template.include_suggestion": "לכלול את השינוי המוצע", 23 | "ep_comments_page.comments_template.comment.value": "הערה", 24 | "ep_comments_page.comments_template.cancel.value": "ביטול", 25 | "ep_comments_page.comments_template.reply.value": "להשיב", 26 | "ep_comments_page.comments_template.reply.placeholder": "להשיב", 27 | "ep_comments_page.comments_template.edit_comment.save": "שמירה", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "ביטול", 29 | "ep_comments_page.error.edit_unauth": "אין לך אפשרות לערוך הערות של משתמשים אחרים!", 30 | "ep_comments_page.error.delete_unauth": "אין לך אפשרות למחוק הערות של משתמשים אחרים!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Abijeet Patro" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "टिप्पणी", 8 | "ep_comments_page.comments": "टिप्पणियाँ", 9 | "ep_comments_page.comments_template.cancel.value": "रद्द करें", 10 | "ep_comments_page.comments_template.reply.value": "जवाब दें", 11 | "ep_comments_page.comments_template.reply.placeholder": "जवाब दें", 12 | "ep_comments_page.comments_template.edit_comment.save": "सहेजें", 13 | "ep_comments_page.comments_template.edit_comment.cancel": "रद्द करें" 14 | } 15 | -------------------------------------------------------------------------------- /locales/hsb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Michawiki" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Komentar", 8 | "ep_comments_page.comments": "Komentary", 9 | "ep_comments_page.add_comment.title": "K wuběrej nowy komentar přidać", 10 | "ep_comments_page.add_comment": "K wuběrej nowy komentar přidać", 11 | "ep_comments_page.add_comment.hint": "Prošu wubjerće najprjedy tekst, kotryž so ma komentować", 12 | "ep_comments_page.delete_comment.title": "Tutón komentar zhašeć", 13 | "ep_comments_page.edit_comment.title": "Tutón komentar wobdźěłać", 14 | "ep_comments_page.show_comments": "Komentary pokazać", 15 | "ep_comments_page.comments_template.suggested_change": "Namjetowana změna", 16 | "ep_comments_page.comments_template.from": "Wot", 17 | "ep_comments_page.comments_template.accept_change.value": "Změnu přiwzać", 18 | "ep_comments_page.comments_template.revert_change.value": "Změnu cofnyć", 19 | "ep_comments_page.comments_template.suggested_change_from": "Namjetowana změna wot \"{{changeFrom}}\" do \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Změnu namjetować wot \"{{changeFrom}}\" do", 21 | "ep_comments_page.comments_template.to": "Do", 22 | "ep_comments_page.comments_template.include_suggestion": "Namjetowanu změnu zapřijeć", 23 | "ep_comments_page.comments_template.comment.value": "Komentar", 24 | "ep_comments_page.comments_template.cancel.value": "Přetorhnyć", 25 | "ep_comments_page.comments_template.reply.value": "Wotmołwić", 26 | "ep_comments_page.comments_template.reply.placeholder": "Wotmołwić", 27 | "ep_comments_page.comments_template.edit_comment.save": "składować", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "přetorhnyć", 29 | "ep_comments_page.error.edit_unauth": "Njemóžeš komentary druhich wužiwarjow wobdźěłać!", 30 | "ep_comments_page.error.delete_unauth": "Njemóžeš komentary druhich wužiwarjow zhašeć!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [] 4 | }, 5 | "ep_comments_page.comment": "Megjegyzés", 6 | "ep_comments_page.comments": "Megjegyzések", 7 | "ep_comments_page.add_comment.title": "Új megjegyzés hozzáadása a kijelöléshez", 8 | "ep_comments_page.add_comment": "Új megjegyzés hozzáadása a kijelöléshez", 9 | "ep_comments_page.add_comment.hint": "Először jelölje meg a szöveget a megjegyzéshez", 10 | "ep_comments_page.delete_comment.title": "Megjegyzés törlése", 11 | "ep_comments_page.edit_comment.title": "Megjegyzés szerkesztése", 12 | "ep_comments_page.show_comments": "Megjegyzések megjelenítése", 13 | "ep_comments_page.comments_template.suggested_change": "Javasolt változtatás", 14 | "ep_comments_page.comments_template.from": "Feladó", 15 | "ep_comments_page.comments_template.accept_change.value": "Változtatás elfogadása", 16 | "ep_comments_page.comments_template.revert_change.value": "Változtatás visszavonása", 17 | "ep_comments_page.comments_template.suggested_change_from": "Változtatás javasolta:", 18 | "ep_comments_page.comments_template.suggest_change_from": "Változtatás javasolta:", 19 | "ep_comments_page.comments_template.to": "Címzett:", 20 | "ep_comments_page.comments_template.include_suggestion": "Javasolt változtatás tartalmazása", 21 | "ep_comments_page.comments_template.comment.value": "Megjegyzés", 22 | "ep_comments_page.comments_template.cancel.value": "Mégsem", 23 | "ep_comments_page.comments_template.reply.value": "Válasz", 24 | "ep_comments_page.comments_template.reply.placeholder": "Válasz", 25 | "ep_comments_page.comments_template.edit_comment.save": "mentés", 26 | "ep_comments_page.comments_template.edit_comment.cancel": "mégsem", 27 | "ep_comments_page.error.edit_unauth": "You cannot edit other users comments!", 28 | "ep_comments_page.error.delete_unauth": "You cannot delete other users comments!" 29 | } 30 | -------------------------------------------------------------------------------- /locales/ia.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "McDutchie" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Commento", 8 | "ep_comments_page.comments": "Commentos", 9 | "ep_comments_page.add_comment.title": "Adder un nove commento sur le selection", 10 | "ep_comments_page.add_comment": "Adder un nove commento sur le selection", 11 | "ep_comments_page.add_comment.hint": "Per favor selige primo le texto a commentar", 12 | "ep_comments_page.delete_comment.title": "Deler iste commento", 13 | "ep_comments_page.edit_comment.title": "Modificar iste commento", 14 | "ep_comments_page.show_comments": "Monstrar commentos", 15 | "ep_comments_page.comments_template.suggested_change": "Cambiamento suggerite", 16 | "ep_comments_page.comments_template.from": "De", 17 | "ep_comments_page.comments_template.accept_change.value": "Acceptar cambiamento", 18 | "ep_comments_page.comments_template.revert_change.value": "Reverter cambiamento", 19 | "ep_comments_page.comments_template.suggested_change_from": "Cambiamento suggerite de \"{{changeFrom}}\" a \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Suggerer cambiamento de \"{{changeFrom}}\" a", 21 | "ep_comments_page.comments_template.to": "A", 22 | "ep_comments_page.comments_template.include_suggestion": "Includer cambiamento suggerite", 23 | "ep_comments_page.comments_template.comment.value": "Commento", 24 | "ep_comments_page.comments_template.cancel.value": "Cancellar", 25 | "ep_comments_page.comments_template.reply.value": "Responder", 26 | "ep_comments_page.comments_template.reply.placeholder": "Responsa", 27 | "ep_comments_page.comments_template.edit_comment.save": "salveguardar", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "cancellar", 29 | "ep_comments_page.error.edit_unauth": "Tu non pote modificar le commentos de altere usatores!", 30 | "ep_comments_page.error.delete_unauth": "Tu non pote deler le commentos de altere usatores!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Atriwidada", 5 | "Veracious" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Komentar", 9 | "ep_comments_page.comments": "Komentar", 10 | "ep_comments_page.add_comment.title": "Tambahkan komentar baru pada pilihan", 11 | "ep_comments_page.add_comment": "Tambahkan komentar baru pada pilihan", 12 | "ep_comments_page.add_comment.hint": "Harap pilih terlebih dahulu teks yang akan dikomentari", 13 | "ep_comments_page.delete_comment.title": "Hapus komentar ini", 14 | "ep_comments_page.edit_comment.title": "Sunting komentar ini", 15 | "ep_comments_page.show_comments": "Tampilkan Komentar", 16 | "ep_comments_page.comments_template.suggested_change": "Sarankan Perubahan", 17 | "ep_comments_page.comments_template.from": "Dari", 18 | "ep_comments_page.comments_template.accept_change.value": "Terima Perubahan", 19 | "ep_comments_page.comments_template.revert_change.value": "Balikkan Perubahan", 20 | "ep_comments_page.comments_template.suggested_change_from": "Menyarankan perubahan dari \"{{changeFrom}}\" menjadi \"{{changeTo}}\"", 21 | "ep_comments_page.comments_template.suggest_change_from": "Menyarankan perubahan dari \"{{changeFrom}}\" menjadi", 22 | "ep_comments_page.comments_template.to": "Ke", 23 | "ep_comments_page.comments_template.include_suggestion": "Sertakan perubahan yang disarankan", 24 | "ep_comments_page.comments_template.comment.value": "Komentar", 25 | "ep_comments_page.comments_template.cancel.value": "Batalkan", 26 | "ep_comments_page.comments_template.reply.value": "Balas", 27 | "ep_comments_page.comments_template.reply.placeholder": "Balas", 28 | "ep_comments_page.comments_template.edit_comment.save": "simpan", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "batalkan", 30 | "ep_comments_page.error.edit_unauth": "Anda tidak dapat menyunting komentar pengguna lain!", 31 | "ep_comments_page.error.delete_unauth": "Anda tidak dapat menghapus komentar pengguna lain!" 32 | } 33 | -------------------------------------------------------------------------------- /locales/io.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "JSantos", 5 | "Joao Xavier" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Komento", 9 | "ep_comments_page.comments": "Komenti", 10 | "ep_comments_page.add_comment.title": "Adjuntez nova komento en l'areo selektita", 11 | "ep_comments_page.add_comment": "Adjuntez nova komento en l'areo selektita", 12 | "ep_comments_page.add_comment.hint": "Selektez l'unesma texto por komentar", 13 | "ep_comments_page.delete_comment.title": "Efacar ca komento", 14 | "ep_comments_page.edit_comment.title": "Redaktar ca komento", 15 | "ep_comments_page.show_comments": "Montrar komenti", 16 | "ep_comments_page.comments_template.suggested_change": "Sugestita modifikuro", 17 | "ep_comments_page.comments_template.from": "De", 18 | "ep_comments_page.comments_template.accept_change.value": "Aceptar modifikuro", 19 | "ep_comments_page.comments_template.revert_change.value": "Desfacar modifikuro", 20 | "ep_comments_page.comments_template.suggested_change_from": "Sugestita modifikuro de \"{{changeFrom}}\" a \"{{changeTo}}\"", 21 | "ep_comments_page.comments_template.suggest_change_from": "Modifikuro sugestita de \"{{changeFrom}}\" a", 22 | "ep_comments_page.comments_template.to": "A", 23 | "ep_comments_page.comments_template.include_suggestion": "Inkluzez chanjo propozata", 24 | "ep_comments_page.comments_template.comment.value": "Komento", 25 | "ep_comments_page.comments_template.cancel.value": "Desfacar", 26 | "ep_comments_page.comments_template.reply.value": "Respondar", 27 | "ep_comments_page.comments_template.reply.placeholder": "Respondo", 28 | "ep_comments_page.comments_template.edit_comment.save": "konservez", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "desfacar", 30 | "ep_comments_page.error.edit_unauth": "Vu ne povas redaktar la komenti di altriǃ", 31 | "ep_comments_page.error.delete_unauth": "Vu ne povas efakar la komenti di altriǃ" 32 | } 33 | -------------------------------------------------------------------------------- /locales/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ajeje Brazorf", 5 | "Beta16" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Commento", 9 | "ep_comments_page.comments": "Commenti", 10 | "ep_comments_page.add_comment.title": "Aggiungi nuovo commento sulla selezione", 11 | "ep_comments_page.add_comment": "Aggiungi nuovo commento sulla selezione", 12 | "ep_comments_page.add_comment.hint": "Devi prima selezionare un testo su cui aggiungere il commento!", 13 | "ep_comments_page.delete_comment.title": "Cancella questo commento", 14 | "ep_comments_page.edit_comment.title": "Modifica questo commento", 15 | "ep_comments_page.show_comments": "Visualizza commenti", 16 | "ep_comments_page.comments_template.suggested_change": "Modifica suggerita", 17 | "ep_comments_page.comments_template.from": "Da", 18 | "ep_comments_page.comments_template.accept_change.value": "Accetta modifica", 19 | "ep_comments_page.comments_template.revert_change.value": "Annulla modifica", 20 | "ep_comments_page.comments_template.suggested_change_from": "Modifica suggerita da \"{{changeFrom}}\" a \"{{changeTo}}\"", 21 | "ep_comments_page.comments_template.suggest_change_from": "Suggerisci modifica da \"{{changeFrom}}\" a", 22 | "ep_comments_page.comments_template.to": "A", 23 | "ep_comments_page.comments_template.include_suggestion": "Suggerisci modfica", 24 | "ep_comments_page.comments_template.comment.value": "Commenta", 25 | "ep_comments_page.comments_template.cancel.value": "Annulla", 26 | "ep_comments_page.comments_template.reply.value": "Rispondi", 27 | "ep_comments_page.comments_template.reply.placeholder": "Rispondi", 28 | "ep_comments_page.comments_template.edit_comment.save": "salva", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "Cancella", 30 | "ep_comments_page.error.edit_unauth": "Non puoi modificare i commenti di altri autori!", 31 | "ep_comments_page.error.delete_unauth": "Non puoi eliminare i commenti di altri autori!" 32 | } 33 | -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "A2y4", 5 | "Chqaz" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "コメント", 9 | "ep_comments_page.comments": "コメント", 10 | "ep_comments_page.comments_template.comment.value": "コメント", 11 | "ep_comments_page.comments_template.cancel.value": "キャンセル", 12 | "ep_comments_page.comments_template.reply.value": "返信", 13 | "ep_comments_page.comments_template.reply.placeholder": "返信", 14 | "ep_comments_page.comments_template.edit_comment.save": "保存", 15 | "ep_comments_page.comments_template.edit_comment.cancel": "キャンセル" 16 | } 17 | -------------------------------------------------------------------------------- /locales/kn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "ಟಿಪ್ಪಣಿ", 8 | "ep_comments_page.comments": "ಟಿಪ್ಪಣಿಗಳು", 9 | "ep_comments_page.delete_comment.title": "ಈ ಟಿಪ್ಪಣಿಯನ್ನು ಅಳಿಸಿ", 10 | "ep_comments_page.edit_comment.title": "ಈ ಟಿಪ್ಪಣಿಯನ್ನು ಸಂಪಾದಿಸಿ", 11 | "ep_comments_page.show_comments": "ಟಿಪ್ಪಣಿಗಳನ್ನು ತೋರಿಸಿ", 12 | "ep_comments_page.comments_template.suggested_change": "ಸೂಚಿಸಲ್ಪಟ್ಟ ಬದಲಾವಣೆ", 13 | "ep_comments_page.comments_template.from": "ಇಂದ", 14 | "ep_comments_page.comments_template.accept_change.value": "ಬದಲಾವಣೆ ಸ್ವೀಕರಿಸಿ", 15 | "ep_comments_page.comments_template.to": "ಗೆ", 16 | "ep_comments_page.comments_template.comment.value": "ಟಿಪ್ಪಣಿ", 17 | "ep_comments_page.comments_template.cancel.value": "ರದ್ದುಮಾಡಿ", 18 | "ep_comments_page.comments_template.reply.value": "ಪ್ರತಿಕ್ರಿಯೆ", 19 | "ep_comments_page.comments_template.reply.placeholder": "ಪ್ರತಿಕ್ರಿಯೆ", 20 | "ep_comments_page.comments_template.edit_comment.save": "ಉಳಿಸಿ", 21 | "ep_comments_page.comments_template.edit_comment.cancel": "ರದ್ದುಮಾಡಿ", 22 | "ep_comments_page.error.edit_unauth": "ನೀವು ಬೇರೆಯವರ ಟಿಪ್ಪಣಿಗಳನ್ನು ಸಂಪಾದಿಸಲಾಗುವುದಿಲ್ಲ!", 23 | "ep_comments_page.error.delete_unauth": "ನೀವು ಬೇರೆಯವರ ಟಿಪ್ಪಣಿಗಳನ್ನು ಅಳಿಸಲಾಗುವುದಿಲ್ಲ!" 24 | } 25 | -------------------------------------------------------------------------------- /locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Dr1t jg", 5 | "Ykhwong", 6 | "그냥기여자", 7 | "아라" 8 | ] 9 | }, 10 | "ep_comments_page.comment": "의견", 11 | "ep_comments_page.comments": "의견", 12 | "ep_comments_page.add_comment.title": "선택 영역에 새 댓글 추가", 13 | "ep_comments_page.add_comment": "선택 영역에 새 댓글 추가", 14 | "ep_comments_page.add_comment.hint": "먼저 의견을 전달할 대상 텍스트를 선택해 주십시오", 15 | "ep_comments_page.delete_comment.title": "이 의견 삭제", 16 | "ep_comments_page.edit_comment.title": "이 의견 편집하기", 17 | "ep_comments_page.show_comments": "의견 표시", 18 | "ep_comments_page.comments_template.suggested_change": "제안된 변경사항", 19 | "ep_comments_page.comments_template.from": "출발지", 20 | "ep_comments_page.comments_template.accept_change.value": "변경 수락", 21 | "ep_comments_page.comments_template.revert_change.value": "변경사항 되돌리기", 22 | "ep_comments_page.comments_template.suggested_change_from": "\"{{changeFrom}}\"에서 \"{{changeTo}}\"(으)로 변경 제안됨", 23 | "ep_comments_page.comments_template.suggest_change_from": "\"{{changeFrom}}\"에서 다음으로 변경할 것으로 제안:", 24 | "ep_comments_page.comments_template.to": "도착지", 25 | "ep_comments_page.comments_template.include_suggestion": "제안된 변경사항 포함", 26 | "ep_comments_page.comments_template.comment.value": "의견", 27 | "ep_comments_page.comments_template.cancel.value": "취소", 28 | "ep_comments_page.comments_template.reply.value": "답변", 29 | "ep_comments_page.comments_template.reply.placeholder": "답변", 30 | "ep_comments_page.comments_template.edit_comment.save": "저장", 31 | "ep_comments_page.comments_template.edit_comment.cancel": "취소", 32 | "ep_comments_page.error.edit_unauth": "다른 사용자의 의견을 편집할 수 없습니다!", 33 | "ep_comments_page.error.delete_unauth": "다른 사용자의 의견을 삭제할 수 없습니다!" 34 | } 35 | -------------------------------------------------------------------------------- /locales/krc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Къарачайлы" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Комментарий", 8 | "ep_comments_page.comments": "Комментарийле", 9 | "ep_comments_page.add_comment.title": "Сайлаугъа джангы комментарий къош", 10 | "ep_comments_page.add_comment": "Сайлаугъа джангы комментарий къош", 11 | "ep_comments_page.add_comment.hint": "Тилейбиз, алгъын комментарий этиллик текстни сайла", 12 | "ep_comments_page.delete_comment.title": "Бу комментарийни кетер", 13 | "ep_comments_page.edit_comment.title": "Бу комментарийни тюзет", 14 | "ep_comments_page.show_comments": "Комментарийлени кёргюзт", 15 | "ep_comments_page.comments_template.suggested_change": "Теджелген тюрлендириу", 16 | "ep_comments_page.comments_template.from": "Джиберген", 17 | "ep_comments_page.comments_template.accept_change.value": "Тюрлениуню къабыл эт", 18 | "ep_comments_page.comments_template.revert_change.value": "Тюрлениуню кери ал", 19 | "ep_comments_page.comments_template.suggested_change_from": "\"{{changeFrom}}\"-дан/ден, {{changeTo}}-гъа/ге теджелген тюрлениу", 20 | "ep_comments_page.comments_template.suggest_change_from": "\"{{changeFrom}}\"-ден/дан былайгъа теджелген тюрлениу", 21 | "ep_comments_page.comments_template.to": "Алыучу", 22 | "ep_comments_page.comments_template.include_suggestion": "Теджелген тюрлендириуню къош", 23 | "ep_comments_page.comments_template.comment.value": "Комментарий", 24 | "ep_comments_page.comments_template.cancel.value": "Ызына ал", 25 | "ep_comments_page.comments_template.reply.value": "Джууабла", 26 | "ep_comments_page.comments_template.reply.placeholder": "Джууабла", 27 | "ep_comments_page.comments_template.edit_comment.save": "сакъландыр", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "ызына ал", 29 | "ep_comments_page.error.edit_unauth": "Башха къошулуучуланы комментарийлерин тюзеталлыкъ тюлсюз!", 30 | "ep_comments_page.error.delete_unauth": "Башха къошулуучуланы комментарийлерин кетераллыкъ тюлсюз!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/lb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Robby" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Bemierkung", 8 | "ep_comments_page.comments": "Bemierkungen", 9 | "ep_comments_page.delete_comment.title": "Dës Bemierkung läschen", 10 | "ep_comments_page.edit_comment.title": "Dës Bemierkung änneren", 11 | "ep_comments_page.show_comments": "Bemierkunge weisen", 12 | "ep_comments_page.comments_template.suggested_change": "Proposéiert Ännerung", 13 | "ep_comments_page.comments_template.from": "Vum", 14 | "ep_comments_page.comments_template.accept_change.value": "Ännerung akzeptéieren", 15 | "ep_comments_page.comments_template.revert_change.value": "Ännerung zrécksetzen", 16 | "ep_comments_page.comments_template.to": "Fir", 17 | "ep_comments_page.comments_template.comment.value": "Bemierkung", 18 | "ep_comments_page.comments_template.cancel.value": "Ofbriechen", 19 | "ep_comments_page.comments_template.reply.value": "Äntweren", 20 | "ep_comments_page.comments_template.reply.placeholder": "Äntweren", 21 | "ep_comments_page.comments_template.edit_comment.save": "späicheren", 22 | "ep_comments_page.comments_template.edit_comment.cancel": "ofbriechen", 23 | "ep_comments_page.error.edit_unauth": "Dir kënnt d'Bemierkunge vun anere Benotzer net änneren", 24 | "ep_comments_page.error.delete_unauth": "Dir kënnt d'Bemierkunge vun anere Benotzer net läschen!" 25 | } 26 | -------------------------------------------------------------------------------- /locales/lt.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Nokeoo" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Komentaras", 8 | "ep_comments_page.comments": "Komentarai", 9 | "ep_comments_page.add_comment.title": "Pridėkite naują komentarą prie pasirinkimo", 10 | "ep_comments_page.add_comment": "Pridėkite naują komentarą prie pasirinkimo", 11 | "ep_comments_page.add_comment.hint": "Pirmiausia pasirinkite tekstą, kurį norite komentuoti", 12 | "ep_comments_page.delete_comment.title": "Ištrinti šį komentarą", 13 | "ep_comments_page.edit_comment.title": "Redaguoti šį komentarą", 14 | "ep_comments_page.show_comments": "Rodyti komentarus", 15 | "ep_comments_page.comments_template.suggested_change": "Siūlomas keitimas", 16 | "ep_comments_page.comments_template.from": "Nuo", 17 | "ep_comments_page.comments_template.accept_change.value": "Priimti keitimą", 18 | "ep_comments_page.comments_template.revert_change.value": "Atmesti keitimą", 19 | "ep_comments_page.comments_template.suggested_change_from": "Siūlomas keitimas iš „{{changeFrom}}“ į „{{changeTo}}“", 20 | "ep_comments_page.comments_template.suggest_change_from": "Siūlyti keisti iš „{{changeFrom}}“ į", 21 | "ep_comments_page.comments_template.to": "Į", 22 | "ep_comments_page.comments_template.include_suggestion": "Įtraukti pasiūlytą keitimą", 23 | "ep_comments_page.comments_template.comment.value": "Komentaras", 24 | "ep_comments_page.comments_template.cancel.value": "Atšaukti", 25 | "ep_comments_page.comments_template.reply.value": "Atsakyti", 26 | "ep_comments_page.comments_template.reply.placeholder": "Atsakyti", 27 | "ep_comments_page.comments_template.edit_comment.save": "išsaugoti", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "atšaukti", 29 | "ep_comments_page.error.edit_unauth": "Negalite redaguoti kitų naudotojų komentarų!", 30 | "ep_comments_page.error.delete_unauth": "Negalite trinti kitų naudotojų komentarų!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/mk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Bjankuloski06" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Коментар", 8 | "ep_comments_page.comments": "Коментари", 9 | "ep_comments_page.add_comment.title": "Дај нов коментар за избраното", 10 | "ep_comments_page.add_comment": "Дај нов коментар на избраното", 11 | "ep_comments_page.add_comment.hint": "Најпрвин изберете текст за коментирање", 12 | "ep_comments_page.delete_comment.title": "Избриши го коментаров", 13 | "ep_comments_page.edit_comment.title": "Измени го коментаров", 14 | "ep_comments_page.show_comments": "Прикажи коментари", 15 | "ep_comments_page.comments_template.suggested_change": "Предложена промена", 16 | "ep_comments_page.comments_template.from": "Од", 17 | "ep_comments_page.comments_template.accept_change.value": "Прифати промена", 18 | "ep_comments_page.comments_template.revert_change.value": "Отповикај промена", 19 | "ep_comments_page.comments_template.suggested_change_from": "Предложена промена од „{{changeFrom}}“ во „{{changeTo}}“", 20 | "ep_comments_page.comments_template.suggest_change_from": "Предложена промена од „{{changeFrom}}“ во", 21 | "ep_comments_page.comments_template.to": "На", 22 | "ep_comments_page.comments_template.include_suggestion": "Вклучи предложена промена", 23 | "ep_comments_page.comments_template.comment.value": "Коментирај", 24 | "ep_comments_page.comments_template.cancel.value": "Откажи", 25 | "ep_comments_page.comments_template.reply.value": "Одговори", 26 | "ep_comments_page.comments_template.reply.placeholder": "Одговори", 27 | "ep_comments_page.comments_template.edit_comment.save": "зачувај", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "откажи", 29 | "ep_comments_page.error.edit_unauth": "Не можеда те менувате туѓи коментари!", 30 | "ep_comments_page.error.delete_unauth": "Не можете да бришете туѓи коментари!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "McDutchie" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Opmerking", 8 | "ep_comments_page.comments": "Opmerkingen", 9 | "ep_comments_page.add_comment.title": "Voeg opmerking toe aan selectie", 10 | "ep_comments_page.add_comment": "Voeg opmerking toe aan selectie", 11 | "ep_comments_page.add_comment.hint": "Selecteer eerst een stuk tekst om een opmerking aan toe te voegen", 12 | "ep_comments_page.delete_comment.title": "Verwijder deze opmerking", 13 | "ep_comments_page.edit_comment.title": "Bewerk deze opmerking", 14 | "ep_comments_page.show_comments": "Toon opmerkingen", 15 | "ep_comments_page.comments_template.suggested_change": "Voorgestelde wijziging", 16 | "ep_comments_page.comments_template.from": "Van", 17 | "ep_comments_page.comments_template.accept_change.value": "Accepteer wijziging", 18 | "ep_comments_page.comments_template.revert_change.value": "Draai wijziging terug", 19 | "ep_comments_page.comments_template.suggested_change_from": "Voorgestelde wijziging van \"{{changeFrom}}\" in \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Voorgestelde wijziging van \"{{changeFrom}}\" in", 21 | "ep_comments_page.comments_template.to": "Naar", 22 | "ep_comments_page.comments_template.include_suggestion": "Voeg voorgestelde wijziging toe", 23 | "ep_comments_page.comments_template.comment.value": "Opmerking", 24 | "ep_comments_page.comments_template.cancel.value": "Annuleer", 25 | "ep_comments_page.comments_template.reply.value": "Antwoord", 26 | "ep_comments_page.comments_template.reply.placeholder": "Antwoord", 27 | "ep_comments_page.comments_template.edit_comment.save": "bewaar", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "annuleer", 29 | "ep_comments_page.error.edit_unauth": "You cannot edit other users comments!", 30 | "ep_comments_page.error.delete_unauth": "You cannot delete other users comments!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/oc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Quentí" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Comentari", 8 | "ep_comments_page.comments": "Comentaris", 9 | "ep_comments_page.add_comment.title": "Apondre un comentari novèl sus la seleccion", 10 | "ep_comments_page.comments_template.accept_change.value": "Acceptar la modificacion", 11 | "ep_comments_page.comments_template.cancel.value": "Anullar", 12 | "ep_comments_page.comments_template.reply.value": "Respondre", 13 | "ep_comments_page.comments_template.edit_comment.save": "enregistrar", 14 | "ep_comments_page.comments_template.edit_comment.cancel": "anullar" 15 | } 16 | -------------------------------------------------------------------------------- /locales/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [] 4 | }, 5 | "ep_comments_page.comment": "Komentarz", 6 | "ep_comments_page.comments": "Komentarze", 7 | "ep_comments_page.add_comment.title": "Dodaj nowy komentarz do sekcji", 8 | "ep_comments_page.add_comment": "Dodaj nowy komentarz do sekcji", 9 | "ep_comments_page.add_comment.hint": "Najpierw wybierz tekst do skomentowania", 10 | "ep_comments_page.delete_comment.title": "Usuń komentarz", 11 | "ep_comments_page.edit_comment.title": "Edit this comment", 12 | "ep_comments_page.show_comments": "Pokaż komentarze", 13 | "ep_comments_page.comments_template.suggested_change": "Sugerowane zmiany", 14 | "ep_comments_page.comments_template.from": "Od", 15 | "ep_comments_page.comments_template.accept_change.value": "Zaakceptuj zmiany", 16 | "ep_comments_page.comments_template.revert_change.value": "Przywróc zmiany", 17 | "ep_comments_page.comments_template.suggested_change_from": "Sugerowana zmiana z", 18 | "ep_comments_page.comments_template.suggest_change_from": "Zaproponuj zmiane z", 19 | "ep_comments_page.comments_template.to": "Do", 20 | "ep_comments_page.comments_template.include_suggestion": "Dołącz sugestie", 21 | "ep_comments_page.comments_template.comment.value": "Komentarz", 22 | "ep_comments_page.comments_template.cancel.value": "Anuluj", 23 | "ep_comments_page.comments_template.reply.value": "Odpowiedź", 24 | "ep_comments_page.comments_template.reply.placeholder": "Odpowiedź", 25 | "ep_comments_page.comments_template.edit_comment.save": "save", 26 | "ep_comments_page.comments_template.edit_comment.cancel": "cancel", 27 | "ep_comments_page.error.edit_unauth": "You cannot edit other users comments!", 28 | "ep_comments_page.error.delete_unauth": "You cannot delete other users comments!" 29 | } 30 | -------------------------------------------------------------------------------- /locales/pms.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Borichèt" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Coment", 8 | "ep_comments_page.comments": "Coment", 9 | "ep_comments_page.add_comment.title": "Gionté un coment neuv an sla selession", 10 | "ep_comments_page.add_comment": "Gionté un coment neuv an sla selession", 11 | "ep_comments_page.add_comment.hint": "Për piasì, prima ch'a serna ël test da comenté", 12 | "ep_comments_page.delete_comment.title": "Eliminé cost coment", 13 | "ep_comments_page.edit_comment.title": "Modifiché cost coment", 14 | "ep_comments_page.show_comments": "Smon-e ij coment", 15 | "ep_comments_page.comments_template.suggested_change": "Modìfiche proponùe", 16 | "ep_comments_page.comments_template.from": "Da", 17 | "ep_comments_page.comments_template.accept_change.value": "Aceté la modìfica", 18 | "ep_comments_page.comments_template.revert_change.value": "Anulé la modìfica", 19 | "ep_comments_page.comments_template.suggested_change_from": "Propon-e ëd rampiassé «{{changeFrom}}» con «{{changeTo}}»", 20 | "ep_comments_page.comments_template.suggest_change_from": "Propon-e ëd rampiassé «{{changeFrom}}» con", 21 | "ep_comments_page.comments_template.to": "A", 22 | "ep_comments_page.comments_template.include_suggestion": "Anclude la modìfica sugerìa", 23 | "ep_comments_page.comments_template.comment.value": "Coment", 24 | "ep_comments_page.comments_template.cancel.value": "Anulé", 25 | "ep_comments_page.comments_template.reply.value": "Rësponde", 26 | "ep_comments_page.comments_template.reply.placeholder": "Rësponde", 27 | "ep_comments_page.comments_template.edit_comment.save": "argistré", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "anulé", 29 | "ep_comments_page.error.edit_unauth": "A peul pa modifiché ij coment dj'àutri utent!", 30 | "ep_comments_page.error.delete_unauth": "A peul nen dëscancelé ij coment ëd j'àutri utent!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Eduardoaddad", 5 | "Svjatysberega", 6 | "YuriNikolai" 7 | ] 8 | }, 9 | "ep_comments_page.comment": "Comentário", 10 | "ep_comments_page.comments": "Comentários", 11 | "ep_comments_page.add_comment.title": "Adicionar um novo comentário sobre a seleção", 12 | "ep_comments_page.add_comment": "Adicionar novo comentário na seleção", 13 | "ep_comments_page.add_comment.hint": "Favor selecionar primeiro o texto a comentar", 14 | "ep_comments_page.delete_comment.title": "Apagar este comentário", 15 | "ep_comments_page.edit_comment.title": "Editar este comentário", 16 | "ep_comments_page.show_comments": "Mostrar comentários", 17 | "ep_comments_page.comments_template.suggested_change": "Alteração Sugerida", 18 | "ep_comments_page.comments_template.from": "De", 19 | "ep_comments_page.comments_template.accept_change.value": "Aceitar Alteração", 20 | "ep_comments_page.comments_template.revert_change.value": "Reverter Alteração", 21 | "ep_comments_page.comments_template.suggested_change_from": "Alteração sugerida de \"{{changeFrom}}\" para \"{{changeTo}}\"", 22 | "ep_comments_page.comments_template.suggest_change_from": "Sugerir alteração de \"{{changeFrom}}\" para", 23 | "ep_comments_page.comments_template.to": "Para", 24 | "ep_comments_page.comments_template.include_suggestion": "Incluir alteração sugerida", 25 | "ep_comments_page.comments_template.comment.value": "Comentário", 26 | "ep_comments_page.comments_template.cancel.value": "Cancelar", 27 | "ep_comments_page.comments_template.reply.value": "Responder", 28 | "ep_comments_page.comments_template.reply.placeholder": "Responder", 29 | "ep_comments_page.comments_template.edit_comment.save": "salvar", 30 | "ep_comments_page.comments_template.edit_comment.cancel": "cancelar", 31 | "ep_comments_page.error.edit_unauth": "Não é possível editar comentários de outros usuários!", 32 | "ep_comments_page.error.delete_unauth": "Não é possível apagar comentários de outros usuários!" 33 | } 34 | -------------------------------------------------------------------------------- /locales/qqq.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ajeje Brazorf" 5 | ] 6 | }, 7 | "ep_comments_page.comments_template.cancel.value": "{{Identical|Cancel}}", 8 | "ep_comments_page.comments_template.edit_comment.cancel": "{{Identical|Cancel}}" 9 | } 10 | -------------------------------------------------------------------------------- /locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "DDPAT", 5 | "Ice bulldog" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Примечание", 9 | "ep_comments_page.comments": "Примечания", 10 | "ep_comments_page.add_comment.title": "Добавьте примечание к выделенному тексту", 11 | "ep_comments_page.add_comment": "Добавьте примечание к выделенному тексту", 12 | "ep_comments_page.add_comment.hint": "Выделите текст чтобы создать примечание", 13 | "ep_comments_page.delete_comment.title": "Удалить примечание", 14 | "ep_comments_page.edit_comment.title": "Отредактировать примечание", 15 | "ep_comments_page.show_comments": "Показывать примечания", 16 | "ep_comments_page.comments_template.suggested_change": "Предлагаемое изменение", 17 | "ep_comments_page.comments_template.from": "Заменить", 18 | "ep_comments_page.comments_template.accept_change.value": "Принять изменение", 19 | "ep_comments_page.comments_template.revert_change.value": "Отменить изменение", 20 | "ep_comments_page.comments_template.suggested_change_from": "Предлагаемое изменение с «{{changeFrom}}» на «{{changeTo}}»", 21 | "ep_comments_page.comments_template.suggest_change_from": "Предложить изменение с «{{changeFrom}}» на", 22 | "ep_comments_page.comments_template.to": "на", 23 | "ep_comments_page.comments_template.include_suggestion": "Предложить правку", 24 | "ep_comments_page.comments_template.comment.value": "Отправить", 25 | "ep_comments_page.comments_template.cancel.value": "Отменить", 26 | "ep_comments_page.comments_template.reply.value": "Ответить", 27 | "ep_comments_page.comments_template.reply.placeholder": "Ответить", 28 | "ep_comments_page.comments_template.edit_comment.save": "сохранить", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "отменить", 30 | "ep_comments_page.error.edit_unauth": "You cannot edit other users comments!", 31 | "ep_comments_page.error.delete_unauth": "You cannot delete other users comments!" 32 | } 33 | -------------------------------------------------------------------------------- /locales/sc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Adr mm" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Cummentu", 8 | "ep_comments_page.comments": "Cummentos", 9 | "ep_comments_page.add_comment.title": "Agiunghe unu cummentu nou a sa seletzione", 10 | "ep_comments_page.add_comment": "Agiunghe unu cummentu a sa seletzione", 11 | "ep_comments_page.add_comment.hint": "Seletziona prima su testu chi boles cummentare", 12 | "ep_comments_page.delete_comment.title": "Cantzella custu cummentu", 13 | "ep_comments_page.edit_comment.title": "Modìfica custu cummentu", 14 | "ep_comments_page.show_comments": "Ammustra cummentos", 15 | "ep_comments_page.comments_template.suggested_change": "Modìficas sugeridas", 16 | "ep_comments_page.comments_template.from": "Dae", 17 | "ep_comments_page.comments_template.accept_change.value": "Atzeta sa modìfica", 18 | "ep_comments_page.comments_template.revert_change.value": "Refuda sa modìfica", 19 | "ep_comments_page.comments_template.suggested_change_from": "Modìfica sugerida dae \"{{changeFrom}}\" in \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Modìfica sugerida dae \"{{changeFrom}}\" in", 21 | "ep_comments_page.comments_template.to": "In", 22 | "ep_comments_page.comments_template.include_suggestion": "Include sa modìfica sugerida", 23 | "ep_comments_page.comments_template.comment.value": "Cummentu", 24 | "ep_comments_page.comments_template.cancel.value": "Annulla", 25 | "ep_comments_page.comments_template.reply.value": "Risponde", 26 | "ep_comments_page.comments_template.reply.placeholder": "Risponde", 27 | "ep_comments_page.comments_template.edit_comment.save": "sarva", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "annulla", 29 | "ep_comments_page.error.edit_unauth": "Non podes modificare is cummentos de is àteros autores.", 30 | "ep_comments_page.error.delete_unauth": "Non podes cantzellare is cummentos de is àteros autores." 31 | } 32 | -------------------------------------------------------------------------------- /locales/scn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ajeje Brazorf" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Cummentu", 8 | "ep_comments_page.comments": "Cummenti", 9 | "ep_comments_page.delete_comment.title": "Cancella stu cummento", 10 | "ep_comments_page.edit_comment.title": "Cancia stu cummentu", 11 | "ep_comments_page.show_comments": "Ammustra cummenti", 12 | "ep_comments_page.comments_template.comment.value": "Cummentu", 13 | "ep_comments_page.comments_template.cancel.value": "Annulla", 14 | "ep_comments_page.comments_template.edit_comment.save": "sarva", 15 | "ep_comments_page.comments_template.edit_comment.cancel": "annulla" 16 | } 17 | -------------------------------------------------------------------------------- /locales/sdc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "F Samaritani" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Oggettu", 8 | "ep_comments_page.comments_template.from": "Da", 9 | "ep_comments_page.comments_template.cancel.value": "Annullà", 10 | "ep_comments_page.comments_template.reply.value": "Ripundì", 11 | "ep_comments_page.comments_template.reply.placeholder": "Ripundì", 12 | "ep_comments_page.comments_template.edit_comment.save": "Saivva", 13 | "ep_comments_page.comments_template.edit_comment.cancel": "Annullà" 14 | } 15 | -------------------------------------------------------------------------------- /locales/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Yardom78" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Komentár", 8 | "ep_comments_page.comments": "Komentáre", 9 | "ep_comments_page.add_comment.title": "Pridať nový komentár", 10 | "ep_comments_page.add_comment": "Pridať nový komentár", 11 | "ep_comments_page.add_comment.hint": "Prosím vyberte text na komentár", 12 | "ep_comments_page.delete_comment.title": "Vymazať tento komentár", 13 | "ep_comments_page.edit_comment.title": "Upraviť tento komentár", 14 | "ep_comments_page.show_comments": "Zobraziť komentáre", 15 | "ep_comments_page.comments_template.suggested_change": "Navrhovaná zmena", 16 | "ep_comments_page.comments_template.from": "Od", 17 | "ep_comments_page.comments_template.accept_change.value": "Súhlasiť so zmenou", 18 | "ep_comments_page.comments_template.revert_change.value": "Vrátiť zmenu späť", 19 | "ep_comments_page.comments_template.suggested_change_from": "Navrhované zmeny od \"{{changeFrom}}\" do \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Navrhnúť zmeny od \"{{changeFrom}}\" do", 21 | "ep_comments_page.comments_template.to": "Komu", 22 | "ep_comments_page.comments_template.include_suggestion": "Zahrnúť navrhovanú zmenu", 23 | "ep_comments_page.comments_template.comment.value": "Komentár", 24 | "ep_comments_page.comments_template.cancel.value": "Zrušiť", 25 | "ep_comments_page.comments_template.reply.value": "Odpovedať", 26 | "ep_comments_page.comments_template.reply.placeholder": "Odpovedať", 27 | "ep_comments_page.comments_template.edit_comment.save": "uložiť", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "zrušiť", 29 | "ep_comments_page.error.edit_unauth": "Nemôžete upravovať komentáre iných používateľov!", 30 | "ep_comments_page.error.delete_unauth": "Nemôžete vymazať komentáre iných používateľov!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/skr-arab.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Saraiki" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "تبصرہ", 8 | "ep_comments_page.comments": "تبصرے", 9 | "ep_comments_page.delete_comment.title": "ایہ تبصرہ مٹاؤ", 10 | "ep_comments_page.edit_comment.title": "ایں تبصرے وچ تبدیلی کرو", 11 | "ep_comments_page.show_comments": "تبصرے ݙکھاؤ", 12 | "ep_comments_page.comments_template.from": "کنوں", 13 | "ep_comments_page.comments_template.accept_change.value": "تبدیلی قبول کرو", 14 | "ep_comments_page.comments_template.to": "کوں", 15 | "ep_comments_page.comments_template.comment.value": "تبصرہ", 16 | "ep_comments_page.comments_template.cancel.value": "منسوخ", 17 | "ep_comments_page.comments_template.reply.value": "جواب", 18 | "ep_comments_page.comments_template.reply.placeholder": "جواب", 19 | "ep_comments_page.comments_template.edit_comment.save": "بچاؤ", 20 | "ep_comments_page.comments_template.edit_comment.cancel": "منسوخ" 21 | } 22 | -------------------------------------------------------------------------------- /locales/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Eleassar" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Komentar", 8 | "ep_comments_page.comments": "Komentarji", 9 | "ep_comments_page.add_comment.title": "Dodaj nov komentar k izbiri", 10 | "ep_comments_page.add_comment": "Dodaj nov komentar k izbiri", 11 | "ep_comments_page.add_comment.hint": "Najprej izberite besedilo za komentiranje", 12 | "ep_comments_page.delete_comment.title": "Izbriši komentar", 13 | "ep_comments_page.edit_comment.title": "Uredi komentar", 14 | "ep_comments_page.show_comments": "Prikaži komentarje", 15 | "ep_comments_page.comments_template.suggested_change": "Predlagana sprememba", 16 | "ep_comments_page.comments_template.from": "Iz", 17 | "ep_comments_page.comments_template.accept_change.value": "Sprejmi spremembo", 18 | "ep_comments_page.comments_template.revert_change.value": "Vrni spremembo", 19 | "ep_comments_page.comments_template.suggested_change_from": "Predlagana sprememba iz »{{changeFrom}}« v »{{changeTo}}«", 20 | "ep_comments_page.comments_template.suggest_change_from": "Predlagajte spremembo iz »{{changeFrom}}« v", 21 | "ep_comments_page.comments_template.to": "V", 22 | "ep_comments_page.comments_template.include_suggestion": "Vključi predlagano spremembo", 23 | "ep_comments_page.comments_template.comment.value": "Komentar", 24 | "ep_comments_page.comments_template.cancel.value": "Prekliči", 25 | "ep_comments_page.comments_template.reply.value": "Odgovor", 26 | "ep_comments_page.comments_template.reply.placeholder": "Odgovor", 27 | "ep_comments_page.comments_template.edit_comment.save": "shrani", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "Prekliči", 29 | "ep_comments_page.error.edit_unauth": "Ne morete urejati komentarjev drugih uporabnikov!", 30 | "ep_comments_page.error.delete_unauth": "Ne morete izbrisati komentarjev drugih uporabnikov!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/smn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Yupik" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Čääli komment", 8 | "ep_comments_page.comments_template.comment.value": "Čääli komment", 9 | "ep_comments_page.comments_template.cancel.value": "Jooskâ", 10 | "ep_comments_page.comments_template.reply.value": "Västid", 11 | "ep_comments_page.comments_template.reply.placeholder": "Västid", 12 | "ep_comments_page.comments_template.edit_comment.save": "vuorkkii", 13 | "ep_comments_page.comments_template.edit_comment.cancel": "jooskâ" 14 | } 15 | -------------------------------------------------------------------------------- /locales/sms.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Yupik" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Kommentââʹst", 8 | "ep_comments_page.comments": "Kommeeʹnt", 9 | "ep_comments_page.delete_comment.title": "Jaukkâd tän kommeeʹnt", 10 | "ep_comments_page.edit_comment.title": "Muuʹtt tän kommeeʹnt", 11 | "ep_comments_page.show_comments": "Čuäʹjet kommeeʹntid", 12 | "ep_comments_page.comments_template.suggested_change": "Eʹtǩǩuum muuttâs", 13 | "ep_comments_page.comments_template.from": "Vuõltteei", 14 | "ep_comments_page.comments_template.accept_change.value": "Priim muttâz", 15 | "ep_comments_page.comments_template.revert_change.value": "Kååʹmet muttâz", 16 | "ep_comments_page.comments_template.to": "Vuâsttavaʹlddi", 17 | "ep_comments_page.comments_template.comment.value": "Kommentââʹst", 18 | "ep_comments_page.comments_template.cancel.value": "Jõõsk", 19 | "ep_comments_page.comments_template.reply.value": "Vaʹstted", 20 | "ep_comments_page.comments_template.reply.placeholder": "Vaʹstted", 21 | "ep_comments_page.comments_template.edit_comment.save": "ruõkk", 22 | "ep_comments_page.comments_template.edit_comment.cancel": "jõõsk" 23 | } 24 | -------------------------------------------------------------------------------- /locales/sq.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Besnik b" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Koment", 8 | "ep_comments_page.comments": "Komente", 9 | "ep_comments_page.add_comment.title": "Shtoni te përzgjedhja një koment të ri", 10 | "ep_comments_page.add_comment": "Shtoni te përzgjedhja një koment të ri", 11 | "ep_comments_page.add_comment.hint": "Ju lutemi, së pari përzgjidhni tekstin për komentim", 12 | "ep_comments_page.delete_comment.title": "Fshije këtë koment", 13 | "ep_comments_page.edit_comment.title": "Përpunojeni këtë koment", 14 | "ep_comments_page.show_comments": "Shfaqi Komentet", 15 | "ep_comments_page.comments_template.suggested_change": "Ndryshim i Sugjeruar", 16 | "ep_comments_page.comments_template.from": "Nga", 17 | "ep_comments_page.comments_template.accept_change.value": "Pranoje Ndryshimin", 18 | "ep_comments_page.comments_template.revert_change.value": "Prapaktheje Ndryshimin", 19 | "ep_comments_page.comments_template.suggested_change_from": "Ndryshim i sugjeruar nga “{{changeFrom}}” në “{{changeTo}}”", 20 | "ep_comments_page.comments_template.suggest_change_from": "Sugjeroni ndryshim nga “{{changeFrom}}” në", 21 | "ep_comments_page.comments_template.to": "Në", 22 | "ep_comments_page.comments_template.include_suggestion": "Përfshije ndryshimin e sugjeruar", 23 | "ep_comments_page.comments_template.comment.value": "Koment", 24 | "ep_comments_page.comments_template.cancel.value": "Anuloje", 25 | "ep_comments_page.comments_template.reply.value": "Përgjigjuni", 26 | "ep_comments_page.comments_template.reply.placeholder": "Përgjigjuni", 27 | "ep_comments_page.comments_template.edit_comment.save": "ruaje", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "anuloje", 29 | "ep_comments_page.error.edit_unauth": "S’mund të përpunoni komente përdoruesish të tjerë!", 30 | "ep_comments_page.error.delete_unauth": "S’mund të fshini komente përdoruesish të tjerë!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Sabelöga" 5 | ] 6 | }, 7 | "ep_comments_page.comment": "Kommentar", 8 | "ep_comments_page.comments": "Kommentarer", 9 | "ep_comments_page.add_comment.title": "Lägg till ny kommentar till markering", 10 | "ep_comments_page.add_comment": "Lägg till ny kommentar till markering", 11 | "ep_comments_page.add_comment.hint": "Markera först den text du vill kommentera", 12 | "ep_comments_page.delete_comment.title": "Radera den här kommentaren", 13 | "ep_comments_page.edit_comment.title": "Redigera den här kommentaren", 14 | "ep_comments_page.show_comments": "Visa kommentarer", 15 | "ep_comments_page.comments_template.suggested_change": "Föreslagen ändring", 16 | "ep_comments_page.comments_template.from": "Från", 17 | "ep_comments_page.comments_template.accept_change.value": "Godkänn ändring", 18 | "ep_comments_page.comments_template.revert_change.value": "Ångra ändring", 19 | "ep_comments_page.comments_template.suggested_change_from": "Föreslagen ändring från \"{{changeFrom}}\" till \"{{changeTo}}\"", 20 | "ep_comments_page.comments_template.suggest_change_from": "Föreslå ändring från \"{{changeFrom}}\" till", 21 | "ep_comments_page.comments_template.to": "Till", 22 | "ep_comments_page.comments_template.include_suggestion": "Bifoga ändringsförslag", 23 | "ep_comments_page.comments_template.comment.value": "Kommentera", 24 | "ep_comments_page.comments_template.cancel.value": "Avbryt", 25 | "ep_comments_page.comments_template.reply.value": "Svara", 26 | "ep_comments_page.comments_template.reply.placeholder": "Svar", 27 | "ep_comments_page.comments_template.edit_comment.save": "spara", 28 | "ep_comments_page.comments_template.edit_comment.cancel": "avbryt", 29 | "ep_comments_page.error.edit_unauth": "Du kan inte redigera andra användares kommentarer!", 30 | "ep_comments_page.error.delete_unauth": "Du kan inte radera andra användares kommentarer!" 31 | } 32 | -------------------------------------------------------------------------------- /locales/th.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Prame Tan", 5 | "Thas Tayapongsak", 6 | "Thastp", 7 | "Trisorn Triboon" 8 | ] 9 | }, 10 | "ep_comments_page.comment": "ความคิดเห็น", 11 | "ep_comments_page.comments": "แสดงความเห็น", 12 | "ep_comments_page.add_comment.title": "เพิ่มความเห็นใหม่สำหรับส่วนที่เลือก", 13 | "ep_comments_page.add_comment": "เพิ่มความคิดเห็นใหม่สำหรับส่วนที่เลือก", 14 | "ep_comments_page.add_comment.hint": "กรุณาเลือกข้อความที่จะแสดงความคิดเห็น", 15 | "ep_comments_page.delete_comment.title": "ลบความคิดเห็นนี้", 16 | "ep_comments_page.edit_comment.title": "แก้ไขความคิดเห็นนี้", 17 | "ep_comments_page.show_comments": "แสดงความคิดเห็น", 18 | "ep_comments_page.comments_template.suggested_change": "การเปลี่ยนแปลงที่เสนอ", 19 | "ep_comments_page.comments_template.from": "จาก", 20 | "ep_comments_page.comments_template.accept_change.value": "ยอมรับการเปลี่ยนแปลง", 21 | "ep_comments_page.comments_template.revert_change.value": "ย้อนคืนการเปลี่ยนแปลง", 22 | "ep_comments_page.comments_template.suggested_change_from": "เสนอให้เปลี่ยน \"{{changeFrom}}\" ไปเป็น \"{{changeTo}}\"", 23 | "ep_comments_page.comments_template.suggest_change_from": "เสนอให้เปลี่ยนจาก \"{{changeFrom}}\" ไปเป็น", 24 | "ep_comments_page.comments_template.to": "ไปเป็น", 25 | "ep_comments_page.comments_template.include_suggestion": "เพิ่มการเปลี่ยนแปลงที่เสนอ", 26 | "ep_comments_page.comments_template.comment.value": "ความคิดเห็น", 27 | "ep_comments_page.comments_template.cancel.value": "ยกเลิก", 28 | "ep_comments_page.comments_template.reply.value": "ตอบกลับ", 29 | "ep_comments_page.comments_template.reply.placeholder": "ตอบกลับ", 30 | "ep_comments_page.comments_template.edit_comment.save": "บันทึก", 31 | "ep_comments_page.comments_template.edit_comment.cancel": "ยกเลิก", 32 | "ep_comments_page.error.edit_unauth": "คุณไม่สามารถแก้ไขความคิดเห็นของผู้ใช้อื่นได้", 33 | "ep_comments_page.error.delete_unauth": "คุณไม่สามารถลบความคิดเห็นของผู้ใช้อื่นได้" 34 | } 35 | -------------------------------------------------------------------------------- /locales/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Can", 5 | "Erdemkose", 6 | "Hedda" 7 | ] 8 | }, 9 | "ep_comments_page.comment": "Yorum", 10 | "ep_comments_page.comments": "Yorumlar", 11 | "ep_comments_page.add_comment.title": "Seçime yeni yorum ekle", 12 | "ep_comments_page.add_comment": "Seçime yeni yorum ekle", 13 | "ep_comments_page.add_comment.hint": "Lütfen önce yorum yapılacak metni seçin", 14 | "ep_comments_page.delete_comment.title": "Bu yorumu sil", 15 | "ep_comments_page.edit_comment.title": "Bu yorumu düzenle", 16 | "ep_comments_page.show_comments": "Yorumları göster", 17 | "ep_comments_page.comments_template.suggested_change": "Önerilen Değişiklik", 18 | "ep_comments_page.comments_template.from": "Gönderen", 19 | "ep_comments_page.comments_template.accept_change.value": "Değişikliği Kabul Et", 20 | "ep_comments_page.comments_template.revert_change.value": "Değişikliği Geri Al", 21 | "ep_comments_page.comments_template.suggested_change_from": "\"{{changeFrom}}\"'dan/den, {{changeTo}}'ye/ya önerilen değişiklik", 22 | "ep_comments_page.comments_template.suggest_change_from": "\"{{changeFrom}}\"'den/dan şuraya önerilen değişiklik", 23 | "ep_comments_page.comments_template.to": "Alıcı", 24 | "ep_comments_page.comments_template.include_suggestion": "Önerilen değişikliği dahil et", 25 | "ep_comments_page.comments_template.comment.value": "Yorum", 26 | "ep_comments_page.comments_template.cancel.value": "İptal", 27 | "ep_comments_page.comments_template.reply.value": "Yanıtla", 28 | "ep_comments_page.comments_template.reply.placeholder": "Yanıtla", 29 | "ep_comments_page.comments_template.edit_comment.save": "kaydet", 30 | "ep_comments_page.comments_template.edit_comment.cancel": "iptal", 31 | "ep_comments_page.error.edit_unauth": "Diğer kullanıcıların yorumlarını düzenleyemezsiniz!", 32 | "ep_comments_page.error.delete_unauth": "Diğer kullanıcıların yorumlarını silemezsiniz!" 33 | } 34 | -------------------------------------------------------------------------------- /locales/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "DDPAT", 5 | "Ice bulldog" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "Коментар", 9 | "ep_comments_page.comments": "Коментарі", 10 | "ep_comments_page.add_comment.title": "Додати новий коментар до вибору", 11 | "ep_comments_page.add_comment": "Додати новий коментар до вибору", 12 | "ep_comments_page.add_comment.hint": "Будь ласка, спочатку виберіть текст для коментування", 13 | "ep_comments_page.delete_comment.title": "Видалити цей коментар", 14 | "ep_comments_page.edit_comment.title": "Редагувати цей коментар", 15 | "ep_comments_page.show_comments": "Показати коментарі", 16 | "ep_comments_page.comments_template.suggested_change": "Запропонована зміна", 17 | "ep_comments_page.comments_template.from": "Від", 18 | "ep_comments_page.comments_template.accept_change.value": "Прийняти зміну", 19 | "ep_comments_page.comments_template.revert_change.value": "Скасувати зміни", 20 | "ep_comments_page.comments_template.suggested_change_from": "Пропонована зміна з «{{changeFrom}}» на «{{changeTo}}»", 21 | "ep_comments_page.comments_template.suggest_change_from": "Запропонувати зміну з «{{changeFrom}}» на", 22 | "ep_comments_page.comments_template.to": "Кому:", 23 | "ep_comments_page.comments_template.include_suggestion": "Включити запропоновану зміну", 24 | "ep_comments_page.comments_template.comment.value": "Коментар", 25 | "ep_comments_page.comments_template.cancel.value": "Скасувати", 26 | "ep_comments_page.comments_template.reply.value": "Відповісти", 27 | "ep_comments_page.comments_template.reply.placeholder": "Відповісти", 28 | "ep_comments_page.comments_template.edit_comment.save": "зберегти", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "скасувати", 30 | "ep_comments_page.error.edit_unauth": "Ви не можете редагувати коментарі інших користувачів!", 31 | "ep_comments_page.error.delete_unauth": "Ви не можете видаляти коментарі інших користувачів!" 32 | } 33 | -------------------------------------------------------------------------------- /locales/xmf.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Narazeni" 5 | ] 6 | }, 7 | "ep_comments_page.delete_comment.title": "ათე კომენტარიშ ლასუა", 8 | "ep_comments_page.show_comments": "კომენტარეფიშ ძირაფა", 9 | "ep_comments_page.comments_template.accept_change.value": "თირუაშ მეღება", 10 | "ep_comments_page.comments_template.comment.value": "კომენტარი", 11 | "ep_comments_page.comments_template.cancel.value": "გოუქვაფა", 12 | "ep_comments_page.comments_template.reply.value": "გამაშ მეჭარუა", 13 | "ep_comments_page.comments_template.edit_comment.save": "ჩუალა", 14 | "ep_comments_page.comments_template.edit_comment.cancel": "გოუქვაფა", 15 | "ep_comments_page.error.edit_unauth": "თქვა ვეშეილებჷნა შხვა მახვარებუეფიშ კომენტარეფიშ რედაქტირაფა!", 16 | "ep_comments_page.error.delete_unauth": "თქვა ვეშეილებჷნა შხვა მახვარებუეფიშ კომენტარეფიშ ლასუა!" 17 | } 18 | -------------------------------------------------------------------------------- /locales/zh-hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "HellojoeAoPS", 5 | "TsuyaMarisa", 6 | "列维劳德" 7 | ] 8 | }, 9 | "ep_comments_page.comment": "评论", 10 | "ep_comments_page.comments": "评论", 11 | "ep_comments_page.add_comment.title": "在选项中添加新评论", 12 | "ep_comments_page.add_comment": "对选择添加新评论", 13 | "ep_comments_page.add_comment.hint": "请先选择要发表评论的文字", 14 | "ep_comments_page.delete_comment.title": "删除此评论", 15 | "ep_comments_page.edit_comment.title": "编辑此注释", 16 | "ep_comments_page.show_comments": "显示评论", 17 | "ep_comments_page.comments_template.suggested_change": "建议更改", 18 | "ep_comments_page.comments_template.from": "来自", 19 | "ep_comments_page.comments_template.accept_change.value": "接受更改", 20 | "ep_comments_page.comments_template.revert_change.value": "恢复更改", 21 | "ep_comments_page.comments_template.suggested_change_from": "建议从“{{changeFrom}}”更改为“{{changeTo}}”", 22 | "ep_comments_page.comments_template.suggest_change_from": "建议从“{{changeFrom}}”更改为", 23 | "ep_comments_page.comments_template.to": "至", 24 | "ep_comments_page.comments_template.include_suggestion": "包括建议的更改", 25 | "ep_comments_page.comments_template.comment.value": "评论", 26 | "ep_comments_page.comments_template.cancel.value": "取消", 27 | "ep_comments_page.comments_template.reply.value": "回复", 28 | "ep_comments_page.comments_template.reply.placeholder": "回复", 29 | "ep_comments_page.comments_template.edit_comment.save": "保存", 30 | "ep_comments_page.comments_template.edit_comment.cancel": "取消", 31 | "ep_comments_page.error.edit_unauth": "您不能编辑其他用户的评论!", 32 | "ep_comments_page.error.delete_unauth": "您不能删除其他用户的评论!" 33 | } 34 | -------------------------------------------------------------------------------- /locales/zh-hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "HellojoeAoPS", 5 | "Kly" 6 | ] 7 | }, 8 | "ep_comments_page.comment": "意見", 9 | "ep_comments_page.comments": "意見", 10 | "ep_comments_page.add_comment.title": "對選擇新增意見", 11 | "ep_comments_page.add_comment": "對選擇新增意見", 12 | "ep_comments_page.add_comment.hint": "請先選擇要發表評論的文字", 13 | "ep_comments_page.delete_comment.title": "刪除這則意見", 14 | "ep_comments_page.edit_comment.title": "編輯此意見", 15 | "ep_comments_page.show_comments": "顯示意見", 16 | "ep_comments_page.comments_template.suggested_change": "建議更改", 17 | "ep_comments_page.comments_template.from": "來自", 18 | "ep_comments_page.comments_template.accept_change.value": "接受更改", 19 | "ep_comments_page.comments_template.revert_change.value": "恢復更改", 20 | "ep_comments_page.comments_template.suggested_change_from": "建議從“{{changeFrom}}”更改為“{{changeTo}}”", 21 | "ep_comments_page.comments_template.suggest_change_from": "建議從“{{changeFrom}}”更改為", 22 | "ep_comments_page.comments_template.to": "至", 23 | "ep_comments_page.comments_template.include_suggestion": "包含建議更改", 24 | "ep_comments_page.comments_template.comment.value": "意見", 25 | "ep_comments_page.comments_template.cancel.value": "取消", 26 | "ep_comments_page.comments_template.reply.value": "回覆", 27 | "ep_comments_page.comments_template.reply.placeholder": "回覆", 28 | "ep_comments_page.comments_template.edit_comment.save": "儲存", 29 | "ep_comments_page.comments_template.edit_comment.cancel": "取消", 30 | "ep_comments_page.error.edit_unauth": "您無法編輯其他使用者的意見!", 31 | "ep_comments_page.error.delete_unauth": "您無法刪除其他使用者的意見!" 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Adds comments on sidebar and link it to the text. For no-skin use ep_page_view.", 3 | "name": "ep_comments_page", 4 | "version": "1.0.38", 5 | "author": { 6 | "name": "Nicolas Lescop", 7 | "email": "limplementeur@gmail.com" 8 | }, 9 | "license": "Apache-2.0", 10 | "contributors": [ 11 | { 12 | "name": "Nicolas Lescop", 13 | "email": "limplementeur@gmail.com" 14 | }, 15 | { 16 | "name": "John McLear", 17 | "email": "john@mclear.co.uk" 18 | }, 19 | { 20 | "name": "Luiza Pagliari", 21 | "email": "lpagliari@gmail.com" 22 | } 23 | ], 24 | "dependencies": { 25 | "cheerio": "^1.0.0-rc.12", 26 | "formidable": "^3.5.1", 27 | "underscore": "^1.13.6" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^8.57.0", 31 | "eslint-config-etherpad": "^4.0.4", 32 | "socket.io-client": "*", 33 | "superagent": "^8.1.2", 34 | "typescript": "^5.4.2" 35 | }, 36 | "scripts": { 37 | "lint": "eslint .", 38 | "lint:fix": "eslint --fix ." 39 | }, 40 | "engines": { 41 | "node": ">=18.0.0" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/ether/ep_comments" 46 | }, 47 | "funding": { 48 | "type": "individual", 49 | "url": "https://etherpad.org/" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /static/css/comment.css: -------------------------------------------------------------------------------- 1 | /* Text commented inside editor */ 2 | #innerdocbody .ace-line .comment { 3 | background-color: #fff382; 4 | color: #222; 5 | } 6 | #innerdocbody .ace-line .comment[data-open="true"]{ 7 | color: orange !important; 8 | } 9 | 10 | 11 | /* Comment right side container */ 12 | #comments { 13 | width: 250px; 14 | order: 3; 15 | position: relative; 16 | } 17 | #comments:not(.active) { 18 | display: none; 19 | } 20 | @media (max-width: 900px) { 21 | #commentIcons, #comments { 22 | display: none !important; 23 | } 24 | } 25 | 26 | .sidebar-comment { 27 | position: absolute; 28 | width: 100%; 29 | } 30 | 31 | /* WITH ICONS */ 32 | #comments.with-icons { 33 | display: none; 34 | } 35 | 36 | /* NEW COMMENT FORM (included both in popup and reply) */ 37 | .new-comment .comment-content { 38 | width: 100%; 39 | } 40 | .comment-reply .new-comment:not(.editing) .form-more { 41 | display: none; 42 | } 43 | input.error, textarea.error { 44 | border-color: red; 45 | } 46 | 47 | /* COMMENT GENERAL STYLE */ 48 | .comment-author-name { 49 | font-weight: bold; 50 | } 51 | .comment-created-at { 52 | font-size: .8em; 53 | opacity: .7; 54 | margin-left: 5px; 55 | } 56 | .comment-actions-wrapper { 57 | float: right; 58 | } 59 | 60 | /* COMMENT COMPACTED (Visible on right side) */ 61 | .sidebar-comment:not(.full-display) .full-display-content { 62 | display: none; 63 | } 64 | .compact-display-content { 65 | text-overflow: ellipsis; 66 | white-space: nowrap; 67 | overflow: hidden; 68 | padding: 0 10px; 69 | background-color: #eeeeed; 70 | } 71 | 72 | /* COMMENT FULL (when mouse hover) */ 73 | .sidebar-comment.full-display { 74 | z-index: 2; 75 | } 76 | .sidebar-comment.full-display .full-display-content { 77 | display: block; 78 | margin-top: -10px; 79 | } 80 | .sidebar-comment.full-display .compact-display-content { 81 | display: none; 82 | } 83 | .full-display-content { 84 | background-color: white; 85 | border-radius: 5px; 86 | overflow: hidden; 87 | box-shadow: 0 2px 4px #ddd; 88 | z-index: 99; 89 | } 90 | .full-display-content .comment-title-wrapper, 91 | .full-display-content .comment-reply { 92 | padding: 10px; 93 | } 94 | .full-display-content .comment-title-wrapper .comment-text { 95 | display: block; 96 | margin-top: 10px; 97 | white-space: normal; 98 | } 99 | .full-display-content .comment-title-wrapper .comment-text.default-text { 100 | display: none; 101 | } 102 | 103 | /* SUGGESTION */ 104 | .suggestion, .reply-suggestion { 105 | display: none; 106 | } 107 | .suggestion-display { 108 | margin-top: 5px; 109 | white-space: normal; 110 | } 111 | .suggestion-display .from-label, 112 | .suggestion-display .to-label { 113 | margin-right: 2px; 114 | } 115 | .suggestion-display .from-value, 116 | .suggestion-display .to-value { 117 | opacity: .8; 118 | font-style: italic; 119 | } 120 | .suggestion-display .from-value:after, .suggestion-display .from-value:before, 121 | .suggestion-display .to-value:after, .suggestion-display .to-value:before { 122 | content: '"'; 123 | } 124 | .suggestion-create .from-label, 125 | .suggestion-create .to-label { 126 | display: block; 127 | font-weight: bold; 128 | margin: 5px 0; 129 | } 130 | .approve-suggestion-btn, .revert-suggestion-btn { 131 | display: block; 132 | margin-bottom: 10px; 133 | } 134 | .suggestion-create textarea.to-value { 135 | width: 100%; 136 | } 137 | .comment-container .revert-suggestion-btn { display: none; } 138 | .comment-container.change-accepted .revert-suggestion-btn { display: block; } 139 | .comment-container.change-accepted .approve-suggestion-btn { display: none; } 140 | 141 | .comment-container.change-accepted .comment-replies-container .revert-suggestion-btn { display: none; } 142 | .comment-container.change-accepted .comment-replies-container .approve-suggestion-btn { display: block; } 143 | .comment-container.change-accepted .comment-replies-container .comment-container.change-accepted .revert-suggestion-btn { display: block; } 144 | .comment-container.change-accepted .comment-replies-container .comment-container.change-accepted .approve-suggestion-btn { display: none; } 145 | /* REPLIES */ 146 | .comment-reply { 147 | background-color: #f9f9f9; 148 | border-top: 1px solid #eee; 149 | } 150 | /* One previous reply */ 151 | .sidebar-comment-reply { 152 | margin-bottom: 10px; 153 | } 154 | .sidebar-comment-reply .comment-text { 155 | display: inline; 156 | margin-top: 5px; 157 | white-space: normal; 158 | } 159 | .sidebar-comment-reply .comment-edit { 160 | display: none; 161 | margin-left: 5px; 162 | font-size: 1em; 163 | opacity: .7; 164 | transition: opacity .2s; 165 | } 166 | .sidebar-comment-reply:hover:not(.editing) .comment-edit { 167 | display: inline; 168 | } 169 | .sidebar-comment-reply:hover:not(.editing) .comment-edit:hover { 170 | opacity: 1; 171 | } 172 | .reply-comment-suggest, .comment-suggest { 173 | margin-top: 10px; 174 | } 175 | 176 | /* EDITING COMMENT */ 177 | .comment-edit-text { 178 | width: 100%; 179 | } 180 | .comment-edit-form + .comment-text { 181 | display: none !important; 182 | } 183 | 184 | /* MODAL FOR MOBILES */ 185 | .comment-modal { 186 | bottom: auto !important; 187 | right: auto !important; 188 | } 189 | .comment-modal-comment { 190 | padding: 0; 191 | } 192 | .comment-modal-comment .sidebar-comment { 193 | position: relative; 194 | top: 0; 195 | } 196 | .comment-modal-comment .compact-display-content { 197 | display: none; 198 | } 199 | .comment-modal-comment .full-display-content { 200 | display: block !important; 201 | margin: 0; 202 | } 203 | .comment-modal-comment .comment-content { 204 | margin-top: 0 !important; 205 | } 206 | 207 | /* OTHER */ 208 | .hidden { 209 | display: none; 210 | } -------------------------------------------------------------------------------- /static/css/commentIcon.css: -------------------------------------------------------------------------------- 1 | #commentIcons { 2 | display: block; 3 | z-index: 1; 4 | margin-left: 15px; 5 | width: 50px; 6 | position: relative; 7 | } 8 | #commentIcons:not(.active) { 9 | display: none; 10 | } 11 | 12 | .comment-icon-line { 13 | position: absolute; 14 | margin-top: 2px; 15 | } 16 | .comment-icon { 17 | background-repeat: no-repeat; 18 | display: inline-block; 19 | height: 16px; 20 | vertical-align: middle; 21 | width: 16px; 22 | margin-right: 5px; 23 | cursor: pointer; 24 | } 25 | .comment-icon:before { 26 | font-family: "fontawesome-etherpad"; 27 | content: "\E850"; 28 | color:#666; 29 | font-size:14px; 30 | padding-top:2px; 31 | line-height:17px; 32 | } 33 | 34 | .comment-icon.with-reply:before { 35 | content: "\E82D"; 36 | } 37 | 38 | .comment-icon.active:before { 39 | color:orange; 40 | } -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | .commenticon { 2 | background-repeat: no-repeat; 3 | display: inline-block; 4 | height: 16px; 5 | vertical-align: middle; 6 | width: 16px; 7 | } 8 | .commenticon:before{ 9 | font-family: "fontawesome-etherpad"; 10 | content: "\e828"; 11 | color:#666; 12 | font-size:14px; 13 | padding-top:2px; 14 | line-height:17px; 15 | } 16 | -------------------------------------------------------------------------------- /static/js/commentBoxes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Easier access to outter pad 4 | let padOuter; 5 | const getPadOuter = () => { 6 | padOuter = padOuter || $('iframe[name="ace_outer"]').contents(); 7 | return padOuter; 8 | }; 9 | 10 | const getCommentsContainer = () => getPadOuter().find('#comments'); 11 | 12 | /* ***** Public methods: ***** */ 13 | 14 | const hideComment = (commentId, hideCommentTitle) => { 15 | const commentElm = getCommentsContainer().find(`#${commentId}`); 16 | commentElm.removeClass('full-display'); 17 | 18 | // hide even the comment title 19 | if (hideCommentTitle) commentElm.hide(); 20 | 21 | const inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); 22 | inner.contents().find('head .comment-style').remove(); 23 | 24 | getPadOuter().find('.comment-modal').removeClass('popup-show'); 25 | }; 26 | 27 | const hideAllComments = () => { 28 | getCommentsContainer().find('.sidebar-comment').removeClass('full-display'); 29 | getPadOuter().find('.comment-modal').removeClass('popup-show'); 30 | }; 31 | 32 | const highlightComment = (commentId, e, editorComment) => { 33 | const container = getCommentsContainer(); 34 | const commentElm = container.find(`#${commentId}`); 35 | const inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); 36 | 37 | if (container.is(':visible')) { 38 | // hide all other comments 39 | container.find('.sidebar-comment').each(function () { 40 | inner.contents().find('head .comment-style').remove(); 41 | $(this).removeClass('full-display'); 42 | }); 43 | 44 | // Then highlight new comment 45 | commentElm.addClass('full-display'); 46 | // now if we apply a class such as mouseover to the editor it will go shitty 47 | // so what we need to do is add CSS for the specific ID to the document... 48 | // It's fucked up but that's how we do it.. 49 | inner.contents().find('head').append( 50 | ``); 51 | } else { 52 | // make a full copy of the html, including listeners 53 | const commentElmCloned = commentElm.clone(true, true); 54 | 55 | // before of appending clear the css (like top positionning) 56 | commentElmCloned.attr('style', ''); 57 | // fix checkbox, because as we are duplicating the sidebar-comment, we lose unique input names 58 | commentElmCloned.find('.label-suggestion-checkbox').click(function () { 59 | $(this).siblings('input[type="checkbox"]').click(); 60 | }); 61 | 62 | // hovering comment view 63 | getPadOuter().find('.comment-modal-comment').html('').append(commentElmCloned); 64 | const padInner = getPadOuter().find('iframe[name="ace_inner"]'); 65 | // get modal position 66 | const containerWidth = getPadOuter().find('#outerdocbody').outerWidth(true); 67 | const modalWitdh = getPadOuter().find('.comment-modal').outerWidth(true); 68 | let targetLeft = e.clientX; 69 | let targetTop = $(e.target).offset().top; 70 | if (editorComment) { 71 | targetLeft += padInner.offset().left; 72 | targetTop += parseInt(padInner.css('padding-top').split('px')[0]); 73 | targetTop += parseInt(padOuter.find('#outerdocbody').css('padding-top').split('px')[0]); 74 | } else { 75 | // mean we are clicking from a comment Icon 76 | targetLeft = $(e.target).offset().left - 20; 77 | } 78 | 79 | // if positioning modal on target left will make part of the modal to be 80 | // out of screen, we place it closer to the middle of the screen 81 | if (targetLeft + modalWitdh > containerWidth) { 82 | targetLeft = containerWidth - modalWitdh - 25; 83 | } 84 | const editorCommentHeight = editorComment ? editorComment.outerHeight(true) : 30; 85 | getPadOuter().find('.comment-modal').addClass('popup-show').css({ 86 | left: `${targetLeft}px`, 87 | top: `${targetTop + editorCommentHeight}px`, 88 | }); 89 | } 90 | }; 91 | 92 | // Adjust position of the comment detail on the container, to be on the same 93 | // height of the pad text associated to the comment, and return the affected element 94 | const adjustTopOf = (commentId, baseTop) => { 95 | const commentElement = getPadOuter().find(`#${commentId}`); 96 | commentElement.css('top', `${baseTop}px`); 97 | 98 | return commentElement; 99 | }; 100 | 101 | // Indicates if comment is on the expected position (baseTop-5) 102 | const isOnTop = (commentId, baseTop) => { 103 | const commentElement = getPadOuter().find(`#${commentId}`); 104 | const expectedTop = `${baseTop}px`; 105 | return commentElement.css('top') === expectedTop; 106 | }; 107 | 108 | // Indicates if event was on one of the elements that does not close comment 109 | const shouldNotCloseComment = (e) => { 110 | // a comment box 111 | if ($(e.target).closest('.sidebar-comment').length || 112 | $(e.target).closest('.comment-modal').length) { // the comment modal 113 | return true; 114 | } 115 | return false; 116 | }; 117 | 118 | exports.hideComment = hideComment; 119 | exports.hideAllComments = hideAllComments; 120 | exports.highlightComment = highlightComment; 121 | exports.adjustTopOf = adjustTopOf; 122 | exports.isOnTop = isOnTop; 123 | exports.shouldNotCloseComment = shouldNotCloseComment; 124 | -------------------------------------------------------------------------------- /static/js/commentL10n.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* ***** Public methods: ***** */ 4 | 5 | const localize = (element) => { 6 | html10n.translateElement(html10n.translations, element.get(0)); 7 | }; 8 | 9 | exports.localize = localize; 10 | -------------------------------------------------------------------------------- /static/js/jquery.tmpl.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},h=0,c=0,l=[];function g(e,d,g,i){var c={data:i||(d?d.data:{}),_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};e&&a.extend(c,e,{nodes:[],parent:d});if(g){c.tmpl=g;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++h;(l.length?f:b)[h]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h0?this.clone(true):this).get();a.fn[d].apply(a(i[h]),k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,l,j){if(d[0]&&d[0].nodeType){var f=a.makeArray(arguments),g=d.length,i=0,h;while(i1)f[0]=[a.makeArray(d)];if(h&&c)f[2]=function(b){a.tmpl.afterManip(this,b,j)};r.apply(this,f)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var j,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(i(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);j=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(i(c,null,j)):j},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){_=_.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(_,$1,$2);_=[];",close:"call=$item.calls();_=call._.concat($item.wrap(call,_));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){_.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){_.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function i(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:i(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=j(c).concat(b);if(d)b=b.concat(j(d))});return b?b:j(c)}function j(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,_=[],$data=$item.data;with($data){_.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,j,d,b,c,e){var i=a.tmpl.tag[j],h,f,g;if(!i)throw"Template command not found: "+j;h=i._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=k(b);e=e?","+k(e)+")":c?")":"";f=c?b.indexOf(".")>-1?b+c:"("+b+").call($item"+e:b;g=c?f:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else g=f=h.$1||"null";d=k(d);return"');"+i[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(g).split("$1").join(f).split("$2").join(d?d.replace(/\s*([^\(]+)\s*(\((.*?)\))?/g,function(d,c,b,a){a=a?","+a+")":b?")":"";return a?"("+c+").call($item"+a:d}):h.$2||"")+"_.push('"})+"');}return _;")}function n(c,b){c._wrap=i(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function k(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,i;for(e=0,p=o.length;e=0;i--)m(j[i]);m(k)}function m(j){var p,i=j,k,e,m;if(m=j.getAttribute(d)){while(i.parentNode&&(i=i.parentNode).nodeType===1&&!(p=i.getAttribute(d)));if(p!==m){i=i.parentNode?i.nodeType===11?0:i.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[i]||f[i],null,true);e.key=++h;b[h]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;i=a.data(j.parentNode,"tmplItem");i=i?i.key:0}if(e){k=e;while(k&&k.key!=i){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent,null,true)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery) -------------------------------------------------------------------------------- /static/js/newComment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const commentL10n = require('ep_comments_page/static/js/commentL10n'); 4 | 5 | let $newComment = $(); 6 | 7 | // Create a comment object with data filled on the given form 8 | const buildCommentFrom = (form) => { 9 | const text = form.find('.comment-content').val(); 10 | const changeFrom = form.find('.from-value').text(); 11 | const changeTo = form.find('.to-value').val() || null; 12 | const comment = {}; 13 | 14 | comment.text = text; 15 | if (changeTo) { 16 | comment.changeFrom = changeFrom; 17 | comment.changeTo = changeTo; 18 | } 19 | 20 | return comment; 21 | }; 22 | 23 | // Callback for new comment Cancel 24 | const cancelNewComment = () => { 25 | hideNewCommentPopup(); 26 | }; 27 | 28 | // Callback for new comment Submit 29 | const submitNewComment = (callback) => { 30 | const index = 0; 31 | const comment = buildCommentFrom($newComment); 32 | if (comment.text.length > 0 || comment.changeTo && comment.changeTo.length > 0) { 33 | $newComment.find('.comment-content, .to-value').removeClass('error'); 34 | hideNewCommentPopup(); 35 | callback(comment, index); 36 | } else { 37 | if (comment.text.length === 0) $newComment.find('.comment-content').addClass('error'); 38 | if (comment.changeTo && comment.changeTo.length === 0) { 39 | $newComment.find('.to-value').addClass('error'); 40 | } 41 | } 42 | return false; 43 | }; 44 | 45 | /* ***** Public methods: ***** */ 46 | 47 | const localizenewCommentPopup = () => { 48 | if ($newComment.length !== 0) commentL10n.localize($newComment); 49 | }; 50 | 51 | // Insert new Comment Form 52 | const insertNewCommentPopupIfDontExist = (comment, callback) => { 53 | $newComment.remove(); 54 | 55 | comment.commentId = ''; 56 | $newComment = $('#newCommentTemplate').tmpl(comment); 57 | $newComment.appendTo($('#editorcontainerbox')); 58 | 59 | localizenewCommentPopup(); 60 | 61 | // Listen for include suggested change toggle 62 | $newComment.find('.suggestion-checkbox').change(function () { 63 | $newComment.find('.suggestion').toggle($(this).is(':checked')); 64 | }); 65 | 66 | // Cancel btn 67 | $newComment.find('#comment-reset').on('click', () => { 68 | cancelNewComment(); 69 | }); 70 | // Create btn 71 | $newComment.on('submit', (e) => { 72 | e.preventDefault(); 73 | return submitNewComment(callback); 74 | }); 75 | 76 | return $newComment; 77 | }; 78 | 79 | const showNewCommentPopup = (position) => { 80 | // position below comment icon 81 | position = position || []; 82 | let left = position[0]; 83 | if ($('.toolbar .addComment').length) { 84 | left = $('.toolbar .addComment').offset().left; 85 | } 86 | const top = position[1]; 87 | $newComment.css('left', left); 88 | if (left === position[0]) { 89 | $newComment.css('top', top); 90 | } 91 | // Reset form to make sure it is all clear 92 | $newComment.find('.suggestion-checkbox').prop('checked', false).trigger('change'); 93 | $newComment.find('textarea').val(''); 94 | $newComment.find('.comment-content, .to-value').removeClass('error'); 95 | 96 | // Show popup 97 | $newComment.addClass('popup-show'); 98 | $newComment.find('.comment-content').focus(); 99 | 100 | // mark selected text, so it is clear to user which text range the comment is being applied to 101 | pad.plugins.ep_comments_page.preCommentMarker.markSelectedText(); 102 | }; 103 | 104 | const hideNewCommentPopup = () => { 105 | $newComment.removeClass('popup-show'); 106 | 107 | // force focus to be lost, so virtual keyboard is hidden on mobile devices 108 | $newComment.find(':focus').blur(); 109 | 110 | // unmark selected text, as now there is no text being commented 111 | pad.plugins.ep_comments_page.preCommentMarker.unmarkSelectedText(); 112 | }; 113 | 114 | exports.localizenewCommentPopup = localizenewCommentPopup; 115 | exports.insertNewCommentPopupIfDontExist = insertNewCommentPopupIfDontExist; 116 | exports.showNewCommentPopup = showNewCommentPopup; 117 | exports.hideNewCommentPopup = hideNewCommentPopup; 118 | -------------------------------------------------------------------------------- /static/js/preCommentMark.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.MARK_CLASS = 'pre-selected-comment'; 4 | 5 | const PreCommentMarker = function (ace) { 6 | this.ace = ace; 7 | const self = this; 8 | 9 | // do nothing if this feature is not enabled 10 | if (!this.highlightSelectedText()) return; 11 | 12 | // remove any existing marks, as there is no comment being added on plugin initialization 13 | // (we need the timeout to let the plugin be fully initialized before starting to remove 14 | // marked texts) 15 | setTimeout(() => { 16 | self.unmarkSelectedText(); 17 | }, 0); 18 | }; 19 | 20 | // Indicates if Etherpad is configured to highlight text 21 | PreCommentMarker.prototype.highlightSelectedText = function () { 22 | return clientVars.highlightSelectedText; 23 | }; 24 | 25 | PreCommentMarker.prototype.markSelectedText = function () { 26 | // do nothing if this feature is not enabled 27 | if (!this.highlightSelectedText()) return; 28 | 29 | this.ace.callWithAce(doNothing, 'markPreSelectedTextToComment', true); 30 | }; 31 | 32 | PreCommentMarker.prototype.unmarkSelectedText = function () { 33 | // do nothing if this feature is not enabled 34 | if (!this.highlightSelectedText()) return; 35 | 36 | this.ace.callWithAce(doNothing, 'unmarkPreSelectedTextToComment', true); 37 | }; 38 | 39 | PreCommentMarker.prototype.performNonUnduableEvent = function (eventType, callstack, action) { 40 | callstack.startNewEvent('nonundoable'); 41 | action(); 42 | callstack.startNewEvent(eventType); 43 | }; 44 | 45 | PreCommentMarker.prototype.handleMarkText = function (context) { 46 | const editorInfo = context.editorInfo; 47 | const rep = context.rep; 48 | const callstack = context.callstack; 49 | 50 | // first we need to unmark any existing text, otherwise we'll have 2 text ranges marked 51 | this.removeMarks(editorInfo, rep, callstack); 52 | 53 | this.addMark(editorInfo, callstack); 54 | }; 55 | 56 | PreCommentMarker.prototype.handleUnmarkText = function (context) { 57 | const editorInfo = context.editorInfo; 58 | const rep = context.rep; 59 | const callstack = context.callstack; 60 | 61 | this.removeMarks(editorInfo, rep, callstack); 62 | }; 63 | 64 | PreCommentMarker.prototype.addMark = function (editorInfo, callstack) { 65 | const eventType = callstack.editEvent.eventType; 66 | 67 | // we don't want the text marking to be undoable 68 | this.performNonUnduableEvent(eventType, callstack, () => { 69 | editorInfo.ace_setAttributeOnSelection(exports.MARK_CLASS, clientVars.userId); 70 | }); 71 | }; 72 | 73 | PreCommentMarker.prototype.removeMarks = function (editorInfo, rep, callstack) { 74 | const eventType = callstack.editEvent.eventType; 75 | const originalSelStart = rep.selStart; 76 | const originalSelEnd = rep.selEnd; 77 | 78 | // we don't want the text marking to be undoable 79 | this.performNonUnduableEvent(eventType, callstack, () => { 80 | // remove marked text 81 | const padInner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); 82 | const selector = `.${exports.MARK_CLASS}`; 83 | const repArr = editorInfo.ace_getRepFromSelector(selector, padInner); 84 | // repArr is an array of reps 85 | $.each(repArr, (index, rep) => { 86 | editorInfo.ace_performSelectionChange(rep[0], rep[1], true); 87 | editorInfo.ace_setAttributeOnSelection(exports.MARK_CLASS, false); 88 | }); 89 | 90 | // make sure selected text is back to original value 91 | editorInfo.ace_performSelectionChange(originalSelStart, originalSelEnd, true); 92 | }); 93 | }; 94 | 95 | // we do nothing on callWithAce; actions will be handled on aceEditEvent 96 | const doNothing = () => {}; 97 | 98 | exports.init = (ace) => new PreCommentMarker(ace); 99 | -------------------------------------------------------------------------------- /static/js/shared.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; 4 | 5 | const collectContentPre = (hookName, context, cb) => { 6 | const comment = /(?:^| )(c-[A-Za-z0-9]*)/.exec(context.cls); 7 | const fakeComment = /(?:^| )(fakecomment-[A-Za-z0-9]*)/.exec(context.cls); 8 | 9 | if (comment && comment[1]) { 10 | context.cc.doAttrib(context.state, `comment::${comment[1]}`); 11 | } 12 | 13 | // a fake comment is a comment copied from this or another pad. To avoid conflicts 14 | // with existing comments, a fake commentId is used, so then we generate a new one 15 | // when the comment is saved 16 | if (fakeComment) { 17 | const mapFakeComments = pad.plugins.ep_comments_page.getMapfakeComments(); 18 | const fakeCommentId = fakeComment[1]; 19 | const commentId = mapFakeComments[fakeCommentId]; 20 | context.cc.doAttrib(context.state, `comment::${commentId}`); 21 | } 22 | return cb(); 23 | }; 24 | 25 | exports.collectContentPre = collectContentPre; 26 | 27 | 28 | exports.generateCommentId = () => { 29 | const commentId = `c-${randomString(16)}`; 30 | return commentId; 31 | }; 32 | -------------------------------------------------------------------------------- /static/tests/backend/specs/api/exportEtherpad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Import and Export tests for comments in .etherpad format 5 | */ 6 | 7 | const assert = require('assert').strict; 8 | const common = require('ep_etherpad-lite/tests/backend/common'); 9 | const superagent = require('superagent'); 10 | const fs = require('fs'); 11 | 12 | // test doc 13 | const etherpadDoc = fs.readFileSync(`${__dirname}/test.etherpad`); 14 | const apiVersion = 1; 15 | const apiKey = common.apiKey; 16 | const testPadId = makeid(); 17 | let api; 18 | 19 | describe(__filename, function () { 20 | before(async function () { api = await common.init(); }); 21 | 22 | describe('Imports and Exports', function () { 23 | it('creates a new Pad, imports content to it, checks that content', async function () { 24 | await api.get(`${endPoint('createPad')}&padID=${testPadId}`) 25 | .expect(200) 26 | .expect('Content-Type', /json/); 27 | }); 28 | 29 | it('imports .etherpad incuding a comment', async function () { 30 | await api.post(`/p/${testPadId}/import`) 31 | .attach('file', etherpadDoc, { 32 | filename: '/test.etherpad', 33 | contentType: 'application/etherpad', 34 | }) 35 | .expect(200) 36 | .expect('Content-Type', /json/) 37 | .expect((res) => assert.equal(res.body.code, 0)); 38 | }); 39 | 40 | it('exports .etherpad and checks it includes comments', async function () { 41 | await api.get(`/p/${testPadId}/export/etherpad`) 42 | .buffer(true).parse(superagent.parse.text) 43 | .expect(200) 44 | .expect(/comments:/); 45 | }); 46 | }); 47 | }); // End of tests. 48 | 49 | 50 | const endPoint = function (point, version) { 51 | version = version || apiVersion; 52 | return `/api/${version}/${point}?apikey=${apiKey}`; 53 | }; 54 | 55 | function makeid() { 56 | let text = ''; 57 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 58 | 59 | for (let i = 0; i < 5; i++) { 60 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 61 | } 62 | return text; 63 | } 64 | -------------------------------------------------------------------------------- /static/tests/backend/specs/api/test.etherpad: -------------------------------------------------------------------------------- 1 | {"pad:zGGskX7AKJDuSpvf6Tko":{"atext":{"text":"Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https://etherpad.org\n\nWarning: DirtyDB is used. This is fine for testing but not recommended for production. -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n\n","attribs":"*0+k|8+b0"},"pool":{"numToAttrib":{"0":["comment","c-Uq6NnJI5KbXYlFjm"],"1":["author","a.uINuFrApO6uEUGKH"]},"nextNum":2},"head":1,"chatHead":-1,"publicStatus":false,"savedRevisions":[]},"globalAuthor:a.uINuFrApO6uEUGKH":{"colorId":19,"name":null,"timestamp":1604493562559,"padIDs":"zGGskX7AKJDuSpvf6Tko"},"pad:zGGskX7AKJDuSpvf6Tko:revs:0":{"changeset":"Z:1>bj|7+bj$Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https://etherpad.org\n\nWarning: DirtyDB is used. This is fine for testing but not recommended for production. -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n","meta":{"author":"","timestamp":1604493546929,"pool":{"numToAttrib":{},"attribToNum":{},"nextNum":0},"atext":{"text":"Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https://etherpad.org\n\nWarning: DirtyDB is used. This is fine for testing but not recommended for production. -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n\n","attribs":"|8+bk"}}},"pad:zGGskX7AKJDuSpvf6Tko:revs:1":{"changeset":"Z:bk>0*0=k$","meta":{"author":"a.uINuFrApO6uEUGKH","timestamp":1604493558498}},"comments:zGGskX7AKJDuSpvf6Tko":{"c-Uq6NnJI5KbXYlFjm":{"author":"a.uINuFrApO6uEUGKH","name":"Anonymous","text":"Hello cruel world","timestamp":1604493551801}}} 2 | -------------------------------------------------------------------------------- /static/tests/backend/specs/padCopy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const common = require('ep_etherpad-lite/tests/backend/common'); 4 | const utils = require('../../utils'); 5 | const createPad = utils.createPad; 6 | const createComment = utils.createComment; 7 | const createCommentReply = utils.createCommentReply; 8 | const commentsEndPointFor = utils.commentsEndPointFor; 9 | const commentRepliesEndPointFor = utils.commentRepliesEndPointFor; 10 | 11 | let api; 12 | const apiKey = common.apiKey; 13 | 14 | describe(__filename, function () { 15 | let padID; 16 | 17 | before(async function () { api = await common.init(); }); 18 | 19 | beforeEach(function (done) { 20 | createPad((err, newPadID) => { 21 | padID = newPadID; 22 | done(err); 23 | }); 24 | }); 25 | 26 | it('creates copies of pad comments when pad is duplicated', function (done) { 27 | // create comment... 28 | createComment(padID, {}, (err, comment) => { 29 | if (err) throw err; 30 | // ... duplicate pad... 31 | const copiedPadID = `${padID}-copy`; 32 | copyPad(padID, copiedPadID, () => { 33 | // ... and finally check if comments are returned 34 | const getCommentsRoute = `${commentsEndPointFor(copiedPadID)}?apikey=${apiKey}`; 35 | api.get(getCommentsRoute) 36 | .expect((res) => { 37 | const commentsFound = Object.keys(res.body.data.comments); 38 | if (commentsFound.length !== 1) { 39 | throw new Error('Comments from pad should had been copied.'); 40 | } 41 | }) 42 | .end(done); 43 | }); 44 | }); 45 | }); 46 | 47 | it('creates copies of pad comment replies when pad is duplicated', function (done) { 48 | // create comment... 49 | createComment(padID, {}, (err, comment) => { 50 | if (err) throw err; 51 | 52 | // ... create reply... 53 | createCommentReply(padID, comment, {}, (err, reply) => { 54 | if (err) throw err; 55 | 56 | // ... duplicate pad... 57 | const copiedPadID = `${padID}-copy`; 58 | copyPad(padID, copiedPadID, () => { 59 | // ... and finally check if replies are returned 60 | const getRepliesRoute = `${commentRepliesEndPointFor(copiedPadID)}?apikey=${apiKey}`; 61 | api.get(getRepliesRoute) 62 | .expect((res) => { 63 | const repliesFound = Object.keys(res.body.data.replies); 64 | if (repliesFound.length !== 1) { 65 | throw new Error('Comment replies from pad should had been copied.'); 66 | } 67 | }) 68 | .end(done); 69 | }); 70 | }); 71 | }); 72 | }); 73 | }); 74 | 75 | const copyPad = function (originalPadID, copiedPadID, callback) { 76 | const copyPadRoute = 77 | `/api/1.2.9/copyPad?apikey=${apiKey}&sourceID=${originalPadID}&destinationID=${copiedPadID}`; 78 | api.get(copyPadRoute).end((err, res) => { 79 | if (err || res.body.code !== 0) { 80 | throw (err || res.body.message || `unknown error while calling API route ${copyPadRoute}`); 81 | } 82 | 83 | callback(); 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /static/tests/backend/specs/padRemove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const common = require('ep_etherpad-lite/tests/backend/common'); 4 | const utils = require('../../utils'); 5 | const createPad = utils.createPad; 6 | const createComment = utils.createComment; 7 | const createCommentReply = utils.createCommentReply; 8 | const commentsEndPointFor = utils.commentsEndPointFor; 9 | const commentRepliesEndPointFor = utils.commentRepliesEndPointFor; 10 | 11 | let api; 12 | const apiKey = common.apiKey; 13 | 14 | describe(__filename, function () { 15 | let padID; 16 | 17 | before(async function () { api = await common.init(); }); 18 | 19 | beforeEach(function (done) { 20 | createPad((err, newPadID) => { 21 | padID = newPadID; 22 | done(err); 23 | }); 24 | }); 25 | 26 | it('removes pad comments when pad is deleted', function (done) { 27 | // create comment... 28 | createComment(padID, {}, (err, comment) => { 29 | if (err) throw err; 30 | // ... remove pad... 31 | deletePad(padID, () => { 32 | // ... and finally check if comments are returned 33 | const getCommentsRoute = `${commentsEndPointFor(padID)}?apikey=${apiKey}`; 34 | api.get(getCommentsRoute) 35 | .expect((res) => { 36 | const commentsFound = Object.keys(res.body.data.comments); 37 | if (commentsFound.length !== 0) { 38 | throw new Error('Comments from pad should had been removed. ' + 39 | `Found ${commentsFound.length} comment(s)`); 40 | } 41 | }) 42 | .end(done); 43 | }); 44 | }); 45 | }); 46 | 47 | it('removes pad comments replies when pad is deleted', function (done) { 48 | // create comment... 49 | createComment(padID, {}, (err, comment) => { 50 | if (err) throw err; 51 | 52 | // ... create reply... 53 | createCommentReply(padID, comment, {}, (err, reply) => { 54 | if (err) throw err; 55 | 56 | // ... remove pad... 57 | deletePad(padID, () => { 58 | // ... and finally check if replies are returned 59 | const getRepliesRoute = `${commentRepliesEndPointFor(padID)}?apikey=${apiKey}`; 60 | api.get(getRepliesRoute) 61 | .expect((res) => { 62 | const repliesFound = Object.keys(res.body.data.replies); 63 | if (repliesFound.length !== 0) { 64 | throw new Error('Comment replies from pad should had been removed. ' + 65 | `Found ${repliesFound.length} reply(ies)`); 66 | } 67 | }) 68 | .end(done); 69 | }); 70 | }); 71 | }); 72 | }); 73 | }); 74 | 75 | const deletePad = function (padID, callback) { 76 | const deletePadRoute = `/api/1/deletePad?apikey=${apiKey}&padID=${padID}`; 77 | api.get(deletePadRoute).end((err, res) => { 78 | if (err || res.body.code !== 0) { 79 | throw (err || res.body.message || `unknown error while calling API route ${deletePadRoute}`); 80 | } 81 | 82 | callback(); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /static/tests/backend/specs/readOnlyPad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); 4 | const Changeset = require('ep_etherpad-lite/static/js/Changeset'); 5 | const assert = require('assert').strict; 6 | const common = require('ep_etherpad-lite/tests/backend/common'); 7 | const padManager = require('ep_etherpad-lite/node/db/PadManager'); 8 | const readOnlyManager = require('ep_etherpad-lite/node/db/ReadOnlyManager'); 9 | const shared = require('../../../js/shared.js'); 10 | 11 | describe(__filename, function () { 12 | let agent; 13 | let pad; 14 | let padId; 15 | let roPadId; 16 | let socket; 17 | 18 | const makeUserChanges = (opcode, attribs) => { 19 | const oldLen = pad.text().length; 20 | assert(oldLen > 0); 21 | const apool = new AttributePool(); 22 | const op = Changeset.newOp(opcode); 23 | op.chars = 1; 24 | op.attribs = Changeset.makeAttribsString(opcode, attribs, apool); 25 | const assem = Changeset.smartOpAssembler(); 26 | assem.append(op); 27 | const cs = assem.toString(); 28 | const newLen = oldLen + assem.getLengthChange(); 29 | const changeset = Changeset.pack(oldLen, newLen, cs, opcode === '+' ? 'x' : ''); 30 | return {baseRev: pad.head, changeset, apool: apool.toJsonable()}; 31 | }; 32 | 33 | before(async function () { 34 | agent = await common.init(); 35 | }); 36 | 37 | beforeEach(async function () { 38 | padId = `testpad${common.randomString()}`; 39 | assert(!await padManager.doesPadExist(padId)); 40 | pad = await padManager.getPad(padId, 'text'); 41 | assert(pad.text().startsWith('text')); 42 | roPadId = await readOnlyManager.getReadOnlyId(padId); 43 | const res = await agent.get(`/p/${roPadId}`).expect(200); 44 | socket = await common.connect(res); 45 | const {type, data: clientVars} = await common.handshake(socket, roPadId); 46 | assert.equal(type, 'CLIENT_VARS'); 47 | assert(clientVars.readonly); 48 | assert.equal(clientVars.readOnlyId, roPadId); 49 | }); 50 | 51 | afterEach(async function () { 52 | if (socket != null) socket.close(); 53 | socket = null; 54 | if (pad != null) await pad.remove(); 55 | pad = null; 56 | }); 57 | 58 | describe('comment-only changes are accepted', function () { 59 | it('add/change comment attribute', async function () { 60 | await Promise.all([ 61 | common.waitForAcceptCommit(socket, pad.head + 1), 62 | common.sendUserChanges( 63 | socket, makeUserChanges('=', [['comment', shared.generateCommentId()]])), 64 | ]); 65 | }); 66 | 67 | it('remove comment attribute', async function () { 68 | await Promise.all([ 69 | common.waitForAcceptCommit(socket, pad.head + 1), 70 | common.sendUserChanges( 71 | socket, makeUserChanges('=', [['comment', shared.generateCommentId()]])), 72 | ]); 73 | await Promise.all([ 74 | common.waitForAcceptCommit(socket, pad.head + 1), 75 | common.sendUserChanges(socket, makeUserChanges('=', [['comment', '']])), 76 | ]); 77 | }); 78 | }); 79 | 80 | describe('other changes are rejected', function () { 81 | const testCases = [ 82 | { 83 | desc: 'keep with non-comment attrib add/change', 84 | opcode: '=', 85 | attribs: [['bold', 'true']], 86 | }, 87 | { 88 | desc: 'keep with non-comment attrib removal', 89 | opcode: '=', 90 | attribs: [['bold', '']], 91 | }, 92 | { 93 | desc: 'keep with comment and non-comment attrib adds/changes', 94 | opcode: '=', 95 | attribs: [['comment', shared.generateCommentId()], ['bold', 'true']], 96 | }, 97 | { 98 | desc: 'insert with no attribs', 99 | opcode: '+', 100 | attribs: [], 101 | }, 102 | { 103 | desc: 'insert with comment attrib', 104 | opcode: '+', 105 | attribs: [['comment', shared.generateCommentId()]], 106 | }, 107 | { 108 | desc: 'insert with non-comment attrib', 109 | opcode: '+', 110 | attribs: [['bold', 'true']], 111 | }, 112 | { 113 | desc: 'insert with comment and non-comment attribs', 114 | opcode: '+', 115 | attribs: [['comment', shared.generateCommentId()], ['bold', 'true']], 116 | }, 117 | { 118 | desc: 'remove', 119 | opcode: '-', 120 | attribs: [], 121 | }, 122 | ]; 123 | 124 | for (const {desc, opcode, attribs} of testCases) { 125 | it(desc, async function () { 126 | const head = pad.head; 127 | await assert.rejects(common.sendUserChanges(socket, makeUserChanges(opcode, attribs))); 128 | // common.sendUserChanges() waits for message ack, so if the message was accepted then head 129 | // should have already incremented by the time we get here. 130 | assert.equal(pad.head, head); 131 | }); 132 | } 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /static/tests/frontend/specs/commentDelete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../utils'); 4 | 5 | let helperFunctions; 6 | const textOfComment = 'original comment'; 7 | const textOfReply = 'original reply'; 8 | const FIRST_LINE = 0; 9 | 10 | // create pad with a comment and a reply 11 | beforeEach(async function () { 12 | helperFunctions = commentDelete; 13 | await helperFunctions.createPad(this); 14 | await helperFunctions.addCommentAndReplyToLine(FIRST_LINE, textOfComment, textOfReply); 15 | }); 16 | 17 | context('when user presses the delete button on a comment', function () { 18 | it('should delete comment', function (done) { 19 | const outer$ = helper.padOuter$; 20 | const inner$ = helper.padInner$; 21 | outer$('.comment-delete').click(); 22 | helper.waitFor(() => inner$('.comment').length === 0).done(() => { 23 | if (inner$('.comment').length !== 0) throw new Error('Error deleting comment'); 24 | done(); 25 | }); 26 | }); 27 | }); 28 | 29 | context('when user presses the delete button on other users comment', function () { 30 | it('should not delete comment', async function () { 31 | let outer$ = helper.padOuter$; 32 | await new Promise((resolve) => setTimeout(resolve, 500)); 33 | await utils.aNewPad({id: helperFunctions.padId}); 34 | await helper.waitForPromise(() => { 35 | outer$ = helper.padOuter$; 36 | return !!outer$ && outer$('.comment-delete').length; 37 | }); 38 | outer$('.comment-delete').click(); 39 | await helper.waitForPromise(() => { 40 | const chrome$ = helper.padChrome$; 41 | return chrome$('#gritter-container').find('.error').length > 0; 42 | }); 43 | const inner$ = helper.padInner$; 44 | if (inner$('.comment').length === 0) throw new Error('Comment should not have been deleted'); 45 | }); 46 | }); 47 | 48 | const commentDelete = { 49 | padId: undefined, 50 | async createPad(test) { 51 | test.timeout(60000); 52 | this.padId = await utils.aNewPad(); 53 | this.enlargeScreen(); 54 | await this.createOrResetPadText(); 55 | }, 56 | async createOrResetPadText() { 57 | await this.cleanPad(); 58 | const inner$ = helper.padInner$; 59 | inner$('div').first().sendkeys('something\n anything'); 60 | await helper.waitForPromise(() => { 61 | const inner$ = helper.padInner$; 62 | const lineLength = inner$('div').length; 63 | return lineLength > 1; 64 | }); 65 | }, 66 | async cleanPad() { 67 | const inner$ = helper.padInner$; 68 | const $padContent = inner$('#innerdocbody'); 69 | $padContent.html(' '); 70 | 71 | // wait for Etherpad to re-create first line 72 | await helper.waitForPromise(() => { 73 | const lineNumber = inner$('div').length; 74 | return lineNumber === 1; 75 | }, 20000); 76 | }, 77 | enlargeScreen() { 78 | $('#iframe-container iframe').css('max-width', '3000px'); 79 | }, 80 | async addCommentAndReplyToLine(line, textOfComment, textOfReply) { 81 | await this.addCommentToLine(line, textOfComment); 82 | await this.addCommentReplyToLine(line, textOfReply); 83 | }, 84 | async addCommentToLine(line, textOfComment) { 85 | const chrome$ = helper.padChrome$; 86 | const $line = this.getLine(line); 87 | $line.sendkeys('{selectall}'); // needs to select content to add comment to 88 | const $commentButton = chrome$('.addComment'); 89 | $commentButton.click(); 90 | 91 | // fill the comment form and submit it 92 | const $commentField = chrome$('textarea.comment-content'); 93 | $commentField.val(textOfComment); 94 | const $submittButton = chrome$('.comment-buttons input[type=submit]'); 95 | $submittButton.click(); 96 | 97 | // wait until comment is created and comment id is set 98 | await this.createdCommentOnLine(line); 99 | }, 100 | async addCommentReplyToLine(line, textOfReply) { 101 | const outer$ = helper.padOuter$; 102 | const commentId = this.getCommentIdOfLine(line); 103 | const existingReplies = outer$('.sidebar-comment-reply').length; 104 | 105 | // if comment icons are enabled, make sure we display the comment box: 106 | if (this.commentIconsEnabled()) { 107 | // click on the icon 108 | const $commentIcon = outer$(`#commentIcons #icon-${commentId}`).first(); 109 | $commentIcon.click(); 110 | } 111 | 112 | // fill reply field 113 | const $replyField = outer$('.comment-content'); 114 | $replyField.val(textOfReply); 115 | 116 | // submit reply 117 | const $submitReplyButton = outer$("form.new-comment input[type='submit']").first(); 118 | $submitReplyButton.click(); 119 | 120 | // wait for the reply to be saved 121 | await helper.waitForPromise(() => { 122 | const hasSavedReply = outer$('.sidebar-comment-reply').length === existingReplies + 1; 123 | return hasSavedReply; 124 | }); 125 | }, 126 | getLine(lineNum) { 127 | const inner$ = helper.padInner$; 128 | const $line = inner$('div').slice(lineNum, lineNum + 1); 129 | return $line; 130 | }, 131 | async createdCommentOnLine(line) { 132 | await helper.waitForPromise(() => this.getCommentIdOfLine(line) != null); 133 | }, 134 | getCommentIdOfLine(line) { 135 | const $line = this.getLine(line); 136 | const comment = $line.find('.comment'); 137 | const cls = comment.attr('class'); 138 | const classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); 139 | const commentId = (classCommentId) ? classCommentId[1] : null; 140 | 141 | return commentId; 142 | }, 143 | commentIconsEnabled() { 144 | return helper.padOuter$('#commentIcons').length > 0; 145 | }, 146 | }; 147 | -------------------------------------------------------------------------------- /static/tests/frontend/specs/commentReply.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ep_comments_page - Comment Reply', function () { 4 | // create a new pad with comment before each test run 5 | beforeEach(function (cb) { 6 | helper.newPad(() => { 7 | chooseToShowComments(true, () => { 8 | createComment(() => { 9 | // make sure Etherpad has enough space to display comments 10 | $('#iframe-container iframe').css('max-width', '1000px'); 11 | cb(); 12 | }); 13 | }); 14 | }); 15 | this.timeout(60000); 16 | }); 17 | 18 | after(function (cb) { 19 | // undo what was done on before() 20 | $('#iframe-container iframe').css('max-width', ''); 21 | cb(); 22 | }); 23 | 24 | xit('Ensures a comment can be replied', function (done) { 25 | createReply(false, () => { 26 | done(); 27 | }); 28 | }); 29 | 30 | xit('Ensures a comment reply can have suggestion', function (done) { 31 | createReply(true, () => { 32 | const outer$ = helper.padOuter$; 33 | const $replySuggestion = outer$('.comment-changeTo-form'); 34 | expect($replySuggestion.is(':visible')).to.be(true); 35 | done(); 36 | }); 37 | }); 38 | 39 | xit('Clears the comment reply form after submitting a reply with suggestion', function (done) { 40 | createReply(true, () => { 41 | const outer$ = helper.padOuter$; 42 | const $replyForm = outer$('form.new-comment'); 43 | const $replyField = $replyForm.find('.comment-content'); 44 | const $replyWithSuggestionCheckbox = $replyForm.find('.suggestion-checkbox'); 45 | const $replySuggestionTextarea = $replyForm.find('.to-value'); 46 | expect($replyField.text()).to.be(''); 47 | expect($replyWithSuggestionCheckbox.is(':checked')).to.be(false); 48 | expect($replySuggestionTextarea.text()).to.be(''); 49 | done(); 50 | }); 51 | }); 52 | 53 | xit('Replaces the original text with reply suggestion', function (done) { 54 | createReply(true, () => { 55 | const inner$ = helper.padInner$; 56 | const outer$ = helper.padOuter$; 57 | 58 | // click to accept suggested change of the reply 59 | const $replyAcceptChangeButton = 60 | outer$(".sidebar-comment-reply .comment-changeTo-form input[type='submit']")[0]; 61 | $replyAcceptChangeButton.click(); 62 | 63 | // check the pad text 64 | const $firstTextElement = inner$('div').first(); 65 | // cake waitFor 66 | helper.waitFor(() => { 67 | console.log($firstTextElement.text()); 68 | return $firstTextElement.text() === 'My suggestion'; 69 | }); 70 | expect($firstTextElement.text()).to.be('My suggestion'); 71 | 72 | done(); 73 | }); 74 | }); 75 | 76 | xit('Replaces orig with reply sugg. after replacing orig with comment sugg.', function (done) { 77 | createReply(true, () => { 78 | const inner$ = helper.padInner$; 79 | const outer$ = helper.padOuter$; 80 | 81 | // click to accept suggested change of the original comment 82 | const $commentAcceptChangeButton = 83 | outer$(".sidebar-comment .comment-changeTo-form input[type='submit']").first(); 84 | $commentAcceptChangeButton.click(); 85 | 86 | // click to accept suggested change of the reply 87 | const $replyAcceptChangeButton = 88 | outer$(".sidebar-comment-reply .comment-changeTo-form input[type='submit']"); 89 | $replyAcceptChangeButton.click(); 90 | 91 | // check the pad text 92 | const $firstTextElement = inner$('div').first(); 93 | expect($firstTextElement.text()).to.be('My suggestion'); 94 | 95 | done(); 96 | }); 97 | }); 98 | 99 | const createComment = (callback) => { 100 | const inner$ = helper.padInner$; 101 | const outer$ = helper.padOuter$; 102 | const chrome$ = helper.padChrome$; 103 | 104 | // get the first text element out of the inner iframe 105 | const $firstTextElement = inner$('div').first(); 106 | 107 | // simulate key presses to delete content 108 | $firstTextElement.sendkeys('{selectall}'); // select all 109 | $firstTextElement.sendkeys('{del}'); // clear the first line 110 | $firstTextElement.sendkeys('This content will receive a comment'); // insert text 111 | 112 | // get the comment button and click it 113 | $firstTextElement.sendkeys('{selectall}'); // needs to select content to add comment to 114 | const $commentButton = chrome$('.addComment'); 115 | $commentButton.click(); 116 | 117 | // fill the comment form and submit it 118 | const $commentField = chrome$('textarea.comment-content'); 119 | $commentField.val('My comment'); 120 | const $hasSuggestion = outer$('.suggestion-checkbox'); 121 | $hasSuggestion.click(); 122 | const $suggestionField = outer$('textarea.to-value'); 123 | $suggestionField.val('Change to this suggestion'); 124 | const $submittButton = chrome$('.comment-buttons input[type=submit]'); 125 | $submittButton.click(); 126 | 127 | // wait until comment is created and comment id is set 128 | helper.waitFor(() => getCommentId() != null) 129 | .done(callback); 130 | }; 131 | 132 | const createReply = (withSuggestion, callback) => { 133 | const outer$ = helper.padOuter$; 134 | const commentId = getCommentId(); 135 | const existingReplies = outer$('.sidebar-comment-reply').length; 136 | 137 | // if comment icons are enabled, make sure we display the comment box: 138 | if (commentIconsEnabled()) { 139 | // click on the icon 140 | const $commentIcon = outer$(`#commentIcons #icon-${commentId}`).first(); 141 | $commentIcon.click(); 142 | } 143 | 144 | // fill reply field 145 | const $replyField = outer$('.comment-content'); 146 | $replyField.val('My reply'); 147 | 148 | // fill suggestion 149 | if (withSuggestion) { 150 | // show suggestion field 151 | const $replySuggestionCheckbox = outer$('.suggestion-checkbox'); 152 | $replySuggestionCheckbox.click(); 153 | 154 | // fill suggestion field 155 | const $suggestionField = outer$('textarea.to-value'); 156 | $suggestionField.val('My suggestion'); 157 | } 158 | 159 | // submit reply 160 | const $submitReplyButton = outer$("form.new-comment input[type='submit']").first(); 161 | $submitReplyButton.click(); 162 | 163 | // wait for the reply to be saved 164 | helper.waitFor(() => outer$('.sidebar-comment-reply').length === existingReplies + 1) 165 | .done(callback); 166 | }; 167 | 168 | const getCommentId = () => { 169 | helper.waitFor(() => { 170 | const inner$ = helper.padInner$; 171 | if (inner$) return true; 172 | }).done(() => { 173 | const inner$ = helper.padInner$; 174 | const comment = inner$('.comment').first(); 175 | const cls = comment.attr('class'); 176 | const classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); 177 | const commentId = (classCommentId) ? classCommentId[1] : null; 178 | return commentId; 179 | }); 180 | }; 181 | 182 | const chooseToShowComments = (shouldShowComments, callback) => { 183 | const chrome$ = helper.padChrome$; 184 | 185 | // click on the settings button to make settings visible 186 | const $settingsButton = chrome$('.buttonicon-settings'); 187 | $settingsButton.click(); 188 | 189 | // check "Show Comments" 190 | const $showComments = chrome$('#options-comments'); 191 | if ($showComments.is(':checked') !== shouldShowComments) $showComments.click(); 192 | 193 | // hide settings again 194 | $settingsButton.click(); 195 | 196 | callback(); 197 | }; 198 | 199 | const commentIconsEnabled = () => helper.padOuter$('#commentIcons').length > 0; 200 | }); 201 | -------------------------------------------------------------------------------- /static/tests/frontend/specs/commentSuggestion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../utils'); 4 | 5 | // create a new pad before each test run 6 | beforeEach(async function () { 7 | this.timeout(60000); 8 | await utils.aNewPad(); 9 | }); 10 | 11 | it('Fills suggestion Change From field when adding a comment with suggestion', async function () { 12 | const chrome$ = helper.padChrome$; 13 | 14 | // As in the function openCommentFormWithSuggestion we send all the text and call 'selectall', 15 | // we select the beginning of line as well. This situation does not happen in the browser, it's 16 | // not possible to select the beginning of first line of a selection. To fix this we add a first 17 | // text without line attribute, in this case a , to avoid select a '*' 18 | const targetText = 'A

  • text with
  • line attributes
'; 19 | 20 | await openCommentFormWithSuggestion(targetText); 21 | const $suggestionFrom = chrome$('.from-value'); 22 | expect($suggestionFrom.text()).to.be('A\n text with\n line attributes'); 23 | }); 24 | 25 | it('Cancel suggestion and try again fills suggestion Change From field', async function () { 26 | const outer$ = helper.padOuter$; 27 | const chrome$ = helper.padChrome$; 28 | 29 | await openCommentFormWithSuggestion('This content will receive a comment'); 30 | 31 | // cancel 32 | const $cancelButton = chrome$('#comment-reset'); 33 | $cancelButton.click(); 34 | 35 | // wait for comment form to close 36 | await helper.waitForPromise(() => outer$('#newComments.active').length === 0); 37 | await openCommentFormWithSuggestion('New target for comment'); 38 | 39 | const $suggestionFrom = chrome$('.from-value'); 40 | expect($suggestionFrom.text()).to.be('New target for comment'); 41 | }); 42 | 43 | it('Fills suggestion Change From field, adds sugestion', async function () { 44 | const outer$ = helper.padOuter$; 45 | const inner$ = helper.padInner$; 46 | const chrome$ = helper.padChrome$; 47 | const origText = 'This content will receive a comment'; 48 | const suggestedText = 'amp: & dq: " sq: \' lt: < gt: > bs: \\ end'; 49 | await openCommentFormWithSuggestion(origText); 50 | 51 | await helper.waitForPromise(() => chrome$('#newComment.popup-show').is(':visible')); 52 | chrome$('#newComment').find('textarea.comment-content').val('A new comment text'); 53 | chrome$('#newComment').find('suggestion-checkbox').click(); 54 | let newCommentSuggestion; 55 | await helper.waitForPromise(() => { 56 | newCommentSuggestion = chrome$('#newComment').find('textarea.to-value'); 57 | return newCommentSuggestion.length > 0 && newCommentSuggestion.is(':visible'); 58 | }); 59 | newCommentSuggestion.val(suggestedText); 60 | chrome$('#comment-create-btn').click(); 61 | 62 | let commentedText$; 63 | await helper.waitForPromise(() => { 64 | commentedText$ = inner$('div').first().find('.comment'); 65 | return commentedText$.length > 0; 66 | }); 67 | commentedText$.click(); 68 | let comment$; 69 | await helper.waitForPromise(() => { 70 | comment$ = outer$('.comment-container'); 71 | const fd$ = comment$.find('.full-display-content'); 72 | return comment$.length > 0 && fd$.length > 0 && fd$.is(':visible'); 73 | }); 74 | await helper.waitForPromise( 75 | () => comment$.find('.comment-title-wrapper .from-label').text().includes(suggestedText)); 76 | 77 | outer$('.approve-suggestion-btn:visible').click(); 78 | commentedText$ = inner$('div').first().find('.comment'); 79 | await helper.waitForPromise( 80 | () => inner$('div').first().find('.comment').text() === suggestedText); 81 | }); 82 | 83 | const openCommentFormWithSuggestion = async (targetText) => { 84 | const inner$ = helper.padInner$; 85 | const chrome$ = helper.padChrome$; 86 | 87 | // get the first text element out of the inner iframe 88 | const $firstTextElement = inner$('div').first(); 89 | 90 | // simulate key presses to delete content 91 | $firstTextElement.sendkeys('{selectall}'); // select all 92 | $firstTextElement.sendkeys('{del}'); // clear the first line 93 | // to simulate a selection with more than one line we have to send the sendkeys selectall 94 | // at the same line. The sendkeys will be run before the line break. 95 | $firstTextElement.html(targetText).sendkeys('{selectall}'); 96 | chrome$('.addComment').first().click(); 97 | await helper.waitForPromise( 98 | () => chrome$('#newComment.popup-show').find('.suggestion-checkbox').length); 99 | chrome$('#newComment.popup-show').find('.suggestion-checkbox').first().click(); 100 | }; 101 | -------------------------------------------------------------------------------- /static/tests/frontend/specs/comment_l10n.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../utils'); 4 | 5 | const commentedText = 'This content will receive a comment'; 6 | const suggestedText = 'Change to this suggestion'; 7 | 8 | // create a new pad with comment before each test run 9 | beforeEach(async function () { 10 | this.timeout(60000); 11 | await utils.aNewPad(); 12 | await createComment(); 13 | await changeEtherpadLanguageTo('en'); 14 | }); 15 | 16 | // ensure we go back to English to avoid breaking other tests: 17 | after(async function () { 18 | await changeEtherpadLanguageTo('en'); 19 | }); 20 | 21 | it('uses default values when language was not localized yet', async function () { 22 | await changeEtherpadLanguageTo('oc'); 23 | const outer$ = helper.padOuter$; 24 | 25 | // get the title of the comment 26 | const $changeToLabel = outer$('.comment-suggest').first(); 27 | expect($changeToLabel.text()).to.be( 28 | ' Include suggested change '); 29 | }); 30 | 31 | it('localizes comment when Etherpad language is changed', async function () { 32 | await changeEtherpadLanguageTo('pt-br'); 33 | const outer$ = helper.padOuter$; 34 | const commentId = getCommentId(); 35 | 36 | // get the 'Suggested Change' label 37 | const $changeToLabel = outer$(`#${commentId} .from-label`).first(); 38 | expect($changeToLabel.text()) 39 | .to.be(`Alteração sugerida de "${commentedText}" para "${suggestedText}"`); 40 | }); 41 | 42 | it("localizes 'new comment' form when Etherpad language is changed", async function () { 43 | // make sure form was created before changing the language 44 | const inner$ = helper.padInner$; 45 | const outer$ = helper.padOuter$; 46 | const chrome$ = helper.padChrome$; 47 | 48 | // get the first text element out of the inner iframe 49 | const $firstTextElement = inner$('div').first(); 50 | 51 | // get the comment button and click it 52 | $firstTextElement.sendkeys('{selectall}'); // needs to select content to add comment to 53 | const $commentButton = chrome$('.addComment'); 54 | $commentButton.click(); 55 | 56 | await changeEtherpadLanguageTo('pt-br'); 57 | // get the 'Include suggested change' label 58 | const $changeToLabel = outer$('.new-comment label.label-suggestion-checkbox').first(); 59 | expect($changeToLabel.text()).to.be('Incluir alteração sugerida'); 60 | }); 61 | 62 | /* ********** Helper functions ********** */ 63 | 64 | const createComment = async () => { 65 | const inner$ = helper.padInner$; 66 | const chrome$ = helper.padChrome$; 67 | 68 | // Returns the first line div. Must be a function because Etherpad might replace the div with a 69 | // new div if the content changes. 70 | const $firstTextElement = () => { 71 | const $div = inner$('div').first(); 72 | expect($div.length).to.be(1); 73 | return $div; 74 | }; 75 | 76 | // simulate key presses to delete content 77 | $firstTextElement().sendkeys('{selectall}'); // select all 78 | $firstTextElement().sendkeys('{del}'); // clear the first line 79 | $firstTextElement().sendkeys(commentedText); // insert text 80 | 81 | // get the comment button and click it 82 | $firstTextElement().sendkeys('{selectall}'); // needs to select content to add comment to 83 | const $commentButton = chrome$('.addComment'); 84 | expect($commentButton.length).to.be(1); 85 | $commentButton.click(); 86 | 87 | // fill the comment form and submit it 88 | const $commentField = chrome$('textarea.comment-content'); 89 | expect($commentField.length).to.be(1); 90 | $commentField.val('My comment'); 91 | const $hasSuggestion = chrome$('#newComment .suggestion-checkbox'); 92 | expect($hasSuggestion.length).to.be(1); 93 | $hasSuggestion.click(); 94 | const $suggestionField = chrome$('textarea.to-value'); 95 | expect($suggestionField.length).to.be(1); 96 | $suggestionField.val(suggestedText); 97 | const $submittButton = chrome$('.comment-buttons input[type=submit]'); 98 | expect($submittButton.length).to.be(1); 99 | $submittButton.click(); 100 | 101 | // wait until comment is created and comment id is set 102 | await helper.waitForPromise(() => getCommentId() != null); 103 | }; 104 | 105 | const changeEtherpadLanguageTo = async (lang) => { 106 | const boldTitles = { 107 | 'en': 'Bold (Ctrl+B)', 108 | 'pt-br': 'Negrito (Ctrl-B)', 109 | 'oc': 'Gras (Ctrl-B)', 110 | }; 111 | const chrome$ = helper.padChrome$; 112 | 113 | // click on the settings button to make settings visible 114 | const $settingsButton = chrome$('.buttonicon-settings'); 115 | $settingsButton.click(); 116 | 117 | // select the language 118 | const $language = chrome$('#languagemenu'); 119 | const $languageoption = $language.find(`[value=${lang}]`); 120 | $languageoption.attr('selected', 'selected'); 121 | $language.trigger('change'); 122 | 123 | // hide settings again 124 | $settingsButton.click(); 125 | 126 | await helper.waitForPromise( 127 | () => { 128 | console.log(chrome$('.buttonicon-bold').parent()[0].title); 129 | return chrome$('.buttonicon-bold').parent()[0].title === boldTitles[lang]; 130 | }); 131 | }; 132 | 133 | const getCommentId = () => { 134 | const inner$ = helper.padInner$; 135 | const comment = inner$('.comment'); 136 | if (comment.length === 0) return null; 137 | for (const cls of comment[0].classList) { 138 | if (cls.startsWith('c-')) return cls; 139 | } 140 | return null; 141 | }; 142 | -------------------------------------------------------------------------------- /static/tests/frontend/specs/comment_settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ep_comments_page - Comment settings', function () { 4 | describe("when user unchecks 'Show Comments'", function () { 5 | // create a new pad and check "Show Comments" checkbox 6 | before(function (cb) { 7 | helper.newPad(() => { 8 | helper.waitFor(() => helper.padInner$).done(() => { 9 | chooseToShowComments(false, cb); 10 | }); 11 | }); 12 | this.timeout(60000); 13 | }); 14 | 15 | xit('sidebar comments should not be visible when opening a new pad', function (done) { 16 | this.timeout(60000); 17 | // force to create a new pad, so validation would be on brand new pads 18 | helper.newPad(() => { 19 | const outer$ = helper.padOuter$; 20 | helper.waitFor(() => { 21 | const outer$ = helper.padOuter$; 22 | return outer$; 23 | }).done(() => { 24 | helper.waitFor(() => { 25 | const outer$ = helper.padOuter$; 26 | // hidden 27 | if (outer$('#comments').is(':visible') === false) { 28 | return true; 29 | } 30 | }).done(() => { 31 | expect(outer$('#comments').is(':visible')).to.be(false); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | }); 37 | 38 | xit('sidebar comments not visible when adding a new comment to a new pad', function (done) { 39 | this.timeout(60000); 40 | // force to create a new pad, so validation would be on brand new pads 41 | helper.newPad(() => { 42 | createComment(() => { 43 | const inner$ = helper.padInner$; 44 | const outer$ = helper.padOuter$; 45 | const chrome$ = helper.padChrome$; 46 | 47 | // get the first text element out of the inner iframe 48 | const $firstTextElement = inner$('div').first(); 49 | $firstTextElement.sendkeys('{selectall}'); // needs to select content to add comment to 50 | 51 | // get the comment button and click it 52 | const $commentButton = chrome$('.addComment'); 53 | $commentButton.click(); 54 | 55 | expect(outer$('#comments:visible').length).to.be(0); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | /* ********** Helper functions ********** */ 63 | 64 | const chooseToShowComments = (shouldShowComments, callback) => { 65 | const chrome$ = helper.padChrome$; 66 | 67 | // click on the settings button to make settings visible 68 | const $settingsButton = chrome$('.buttonicon-settings'); 69 | console.log($settingsButton); 70 | $settingsButton.click(); 71 | 72 | // check "Show Comments" 73 | const $showComments = chrome$('#options-comments'); 74 | console.log($showComments); 75 | if ($showComments.is(':checked') !== shouldShowComments) { 76 | $showComments.click(); 77 | console.log('clicking to disable'); 78 | } 79 | 80 | // hide settings again 81 | $settingsButton.click(); 82 | 83 | callback(); 84 | }; 85 | 86 | const createComment = (callback) => { 87 | const inner$ = helper.padInner$; 88 | const outer$ = helper.padOuter$; 89 | const chrome$ = helper.padChrome$; 90 | 91 | // get the first text element out of the inner iframe 92 | const $firstTextElement = inner$('div').first(); 93 | 94 | // simulate key presses to delete content 95 | $firstTextElement.sendkeys('{selectall}'); // select all 96 | $firstTextElement.sendkeys('{del}'); // clear the first line 97 | $firstTextElement.sendkeys('This content will receive a comment'); // insert text 98 | 99 | // get the comment button and click it 100 | $firstTextElement.sendkeys('{selectall}'); // needs to select content to add comment to 101 | const $commentButton = chrome$('.addComment'); 102 | $commentButton.click(); 103 | 104 | // fill the comment form and submit it 105 | const $commentField = chrome$('textarea.comment-content'); 106 | $commentField.val('My comment'); 107 | const $hasSuggestion = outer$('.suggestion-checkbox'); 108 | $hasSuggestion.click(); 109 | const $suggestionField = outer$('textarea.to-value'); 110 | $suggestionField.val('Change to this suggestion'); 111 | const $submittButton = chrome$('.comment-buttons input[type=submit]'); 112 | $submittButton.click(); 113 | 114 | // wait until comment is created and comment id is set 115 | helper.waitFor(() => getCommentId() != null) 116 | .done(callback); 117 | }; 118 | 119 | const getCommentId = () => { 120 | helper.waitFor(() => { 121 | const inner$ = helper.padInner$; 122 | if (inner$) return true; 123 | }).done(() => { 124 | const inner$ = helper.padInner$; 125 | const comment = inner$('.comment').first(); 126 | const cls = comment.attr('class'); 127 | const classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); 128 | const commentId = (classCommentId) ? classCommentId[1] : null; 129 | return commentId; 130 | }); 131 | }; 132 | }); 133 | -------------------------------------------------------------------------------- /static/tests/frontend/specs/newComment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../utils'); 4 | 5 | before(async function () { 6 | await utils.aNewPad(); 7 | helper.padInner$('div').first() 8 | .sendkeys('{selectall}') 9 | .sendkeys('{del}') 10 | .text('commented text') 11 | .sendkeys('{selectall}'); 12 | }); 13 | 14 | it('new comment button focuses on comment textarea', async function () { 15 | helper.padChrome$('.addComment').click(); 16 | expect(helper.padChrome$.document.activeElement) 17 | .to.be(helper.padChrome$('#newComment').find('.comment-content')[0]); 18 | }); 19 | -------------------------------------------------------------------------------- /static/tests/frontend/specs/preCommentMark.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ep_comments_page - Pre-comment text mark', function () { 4 | let padId; 5 | 6 | // create a new pad before each test run 7 | beforeEach(function (cb) { 8 | padId = helper.newPad(() => { 9 | createPadWithTwoLines(() => { 10 | selectLineAndOpenCommentForm(0, cb); 11 | }); 12 | }); 13 | this.timeout(60000); 14 | }); 15 | 16 | it('marks selected text when New Comment form is opened', function (done) { 17 | if (textHighlightIsDisabled()) { 18 | return done(); 19 | } 20 | const inner$ = helper.padInner$; 21 | 22 | // verify if text was marked with pre-comment class 23 | const $preCommentTextMarked = inner$('.pre-selected-comment'); 24 | expect($preCommentTextMarked.length).to.be(1); 25 | expect($preCommentTextMarked.text()).to.be('Line 1'); 26 | 27 | done(); 28 | }); 29 | 30 | context('when user reloads pad', function () { 31 | beforeEach(function (cb) { 32 | this.timeout(20000); 33 | 34 | // wait for changes to be saved as a revision before reloading the pad, otherwise 35 | // it won't have the text that we created on beforeEach after reload 36 | setTimeout(() => { 37 | helper.newPad(cb, padId); 38 | }, 5000); 39 | }); 40 | 41 | it('does not have any marked text after pad is fully loaded', function (done) { 42 | if (textHighlightIsDisabled()) { 43 | return done(); 44 | } 45 | const inner$ = helper.padInner$; 46 | 47 | // it takes some time for marks to be removed, so wait for it 48 | helper.waitFor(() => { 49 | const $preCommentTextMarked = inner$('.pre-selected-comment'); 50 | return $preCommentTextMarked.length === 0; 51 | }).done(done); 52 | }); 53 | }); 54 | 55 | context('when user performs UNDO operation', function () { 56 | beforeEach(function (cb) { 57 | this.timeout(20000); 58 | 59 | // wait for changes to be saved as a revision and reload pad, otherwise 60 | // UNDO will remove the text that we created on beforeEach 61 | setTimeout(() => { 62 | helper.newPad(cb, padId); 63 | }, 5000); 64 | }); 65 | 66 | it('keeps marked text', function (done) { 67 | if (textHighlightIsDisabled()) { 68 | return done(); 69 | } 70 | const chrome$ = helper.padChrome$; 71 | const inner$ = helper.padInner$; 72 | 73 | // marks text 74 | selectLineAndOpenCommentForm(0, () => { 75 | // perform UNDO 76 | const $undoButton = chrome$('.buttonicon-undo'); 77 | $undoButton.click(); 78 | 79 | // verify if text was marked with pre-comment class 80 | const $preCommentTextMarked = inner$('.pre-selected-comment'); 81 | expect($preCommentTextMarked.length).to.be(1); 82 | expect($preCommentTextMarked.text()).to.be('Line 1'); 83 | 84 | done(); 85 | }); 86 | }); 87 | }); 88 | 89 | context('when user changes selected text', function () { 90 | beforeEach(function (cb) { 91 | const inner$ = helper.padInner$; 92 | 93 | // select second line of text 94 | const $secondLine = inner$('div').first().next(); 95 | $secondLine.sendkeys('{selectall}'); 96 | 97 | cb(); 98 | }); 99 | 100 | it('keeps marked text', function (done) { 101 | if (textHighlightIsDisabled()) { 102 | return done(); 103 | } 104 | const inner$ = helper.padInner$; 105 | 106 | // verify if text was marked with pre-comment class 107 | const $preCommentTextMarked = inner$('.pre-selected-comment'); 108 | expect($preCommentTextMarked.length).to.be(1); 109 | expect($preCommentTextMarked.text()).to.be('Line 1'); 110 | 111 | done(); 112 | }); 113 | }); 114 | 115 | context('when user closes the New Comment form', function () { 116 | beforeEach(function (cb) { 117 | const outer$ = helper.padOuter$; 118 | 119 | const $cancelButton = outer$('#comment-reset'); 120 | $cancelButton.click(); 121 | 122 | cb(); 123 | }); 124 | 125 | it('unmarks text', function (done) { 126 | if (textHighlightIsDisabled()) { 127 | return done(); 128 | } 129 | const inner$ = helper.padInner$; 130 | 131 | // verify if there is no text marked with pre-comment class 132 | const $preCommentTextMarked = inner$('.pre-selected-comment'); 133 | expect($preCommentTextMarked.length).to.be(0); 134 | 135 | done(); 136 | }); 137 | }); 138 | 139 | context('when user submits the comment', function () { 140 | beforeEach(function (cb) { 141 | const outer$ = helper.padOuter$; 142 | const chrome$ = helper.padChrome$; 143 | 144 | // fill the comment form and submit it 145 | const $commentField = chrome$('textarea.comment-content'); 146 | $commentField.val('My comment'); 147 | const $hasSuggestion = outer$('.suggestion-checkbox'); 148 | $hasSuggestion.click(); 149 | const $suggestionField = outer$('textarea.to-value'); 150 | $suggestionField.val('Change to this suggestion'); 151 | const $submittButton = chrome$('.comment-buttons input[type=submit]'); 152 | $submittButton.click(); 153 | 154 | // wait until comment is created and comment id is set 155 | helper.waitFor(() => getCommentId() != null).done(cb); 156 | }); 157 | 158 | it('unmarks text', function (done) { 159 | if (textHighlightIsDisabled()) { 160 | return done(); 161 | } 162 | const inner$ = helper.padInner$; 163 | 164 | // verify if there is no text marked with pre-comment class 165 | const $preCommentTextMarked = inner$('.pre-selected-comment'); 166 | expect($preCommentTextMarked.length).to.be(0); 167 | 168 | done(); 169 | }); 170 | }); 171 | 172 | context('when user selects another text range and opens New Comment form for it', function () { 173 | beforeEach(function (cb) { 174 | selectLineAndOpenCommentForm(1, cb); 175 | }); 176 | 177 | it('changes the marked text', function (done) { 178 | if (textHighlightIsDisabled()) { 179 | return done(); 180 | } 181 | const inner$ = helper.padInner$; 182 | 183 | // verify if text was marked with pre-comment class 184 | const $preCommentTextMarked = inner$('.pre-selected-comment'); 185 | expect($preCommentTextMarked.length).to.be(1); 186 | expect($preCommentTextMarked.text()).to.be('Line 2'); 187 | 188 | done(); 189 | }); 190 | }); 191 | 192 | /* ********** Helper functions ********** */ 193 | const createPadWithTwoLines = (callback) => { 194 | const inner$ = helper.padInner$; 195 | 196 | // replace the first text element of pad with two lines 197 | const $firstLine = inner$('div').first(); 198 | $firstLine.html('Line 1
Line 2
'); 199 | 200 | // wait until the two lines are split into two divs 201 | helper.waitFor(() => { 202 | const $secondLine = inner$('div').first().next(); 203 | return $secondLine.text() === 'Line 2'; 204 | }).done(callback); 205 | }; 206 | 207 | const selectLineAndOpenCommentForm = (lineNumber, callback) => { 208 | const chrome$ = helper.padChrome$; 209 | 210 | // select first line to add comment to 211 | const $targetLine = getLine(lineNumber); 212 | $targetLine.sendkeys('{selectall}'); 213 | 214 | // get the comment button and click it 215 | const $commentButton = chrome$('.addComment'); 216 | $commentButton.click(); 217 | 218 | callback(); 219 | }; 220 | 221 | const getCommentId = () => { 222 | const inner$ = helper.padInner$; 223 | const comment = inner$('.comment').first(); 224 | const cls = comment.attr('class'); 225 | const classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); 226 | const commentId = (classCommentId) ? classCommentId[1] : null; 227 | 228 | return commentId; 229 | }; 230 | 231 | const getLine = (lineNumber) => { 232 | const inner$ = helper.padInner$; 233 | let line = inner$('div').first(); 234 | for (let i = lineNumber - 1; i >= 0; i--) { 235 | line = line.next(); 236 | } 237 | return line; 238 | }; 239 | 240 | const textHighlightIsDisabled = () => !helper.padChrome$.window.clientVars.highlightSelectedText; 241 | }); 242 | -------------------------------------------------------------------------------- /static/tests/frontend/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.aNewPad = async (...args) => { 4 | const padId = await helper.aNewPad(...args); 5 | // Most ep_comments_page initialization happens during postAceInit, which runs after 6 | // helper.aNewPad() returns. Wait for initialization to complete to avoid race conditions. 7 | await helper.waitForPromise(async () => { 8 | const {plugins: {ep_comments_page: {initDone} = {}} = {}} = helper.padChrome$.window.pad; 9 | if (!initDone) return false; 10 | await initDone; 11 | return true; 12 | }); 13 | return padId; 14 | }; 15 | -------------------------------------------------------------------------------- /static/tests/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const common = require('ep_etherpad-lite/tests/backend/common'); 4 | const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; 5 | 6 | const apiKey = common.apiKey; 7 | const apiVersion = 1; 8 | 9 | // Functions to validate API responses: 10 | const codeToBe = function (expectedCode, res) { 11 | if (res.body.code !== expectedCode) { 12 | throw new Error(`Code should be ${expectedCode}, was ${res.body.code}`); 13 | } 14 | }; 15 | 16 | const codeToBe0 = function (res) { codeToBe(0, res); }; 17 | const codeToBe1 = function (res) { codeToBe(1, res); }; 18 | const codeToBe4 = function (res) { codeToBe(4, res); }; 19 | 20 | // App end point to create a comment via API 21 | const commentsEndPointFor = function (pad) { 22 | return `/p/${pad}/comments`; 23 | }; 24 | 25 | // App end point to create a comment reply via API 26 | const commentRepliesEndPointFor = function (pad) { 27 | return `/p/${pad}/commentReplies`; 28 | }; 29 | 30 | // Creates a pad and returns the pad id. Calls the callback when finished. 31 | const createPad = function (done) { 32 | common.init().then((agent) => { 33 | const pad = randomString(5); 34 | agent.get(`/api/${apiVersion}/createPad?apikey=${apiKey}&padID=${pad}`) 35 | .end((err, res) => { 36 | if (err || (res.body.code !== 0)) return done(new Error('Unable to create new Pad')); 37 | done(null, pad); 38 | }); 39 | }); 40 | }; 41 | 42 | const readOnlyId = function (padID, done) { 43 | common.init().then((agent) => { 44 | agent.get(`/api/${apiVersion}/getReadOnlyID?apikey=${apiKey}&padID=${padID}`) 45 | .end((err, res) => { 46 | if (err || (res.body.code !== 0)) return done(new Error('Unable to get read only id')); 47 | done(null, res.body.data.readOnlyID); 48 | }); 49 | }); 50 | }; 51 | 52 | // Creates a comment and calls the callback when finished. 53 | const createComment = function (pad, commentData, done) { 54 | commentData = commentData || {}; 55 | commentData.name = commentData.name || 'John Doe'; 56 | commentData.text = commentData.text || 'This is a comment'; 57 | common.init().then((agent) => { 58 | agent.post(commentsEndPointFor(pad)) 59 | .send({ 60 | apikey: apiKey, 61 | data: JSON.stringify([commentData]), 62 | }) 63 | .expect(200) 64 | .expect('Content-Type', /json/) 65 | .expect(codeToBe0) 66 | .end((err, res) => { 67 | if (err) return done(err); 68 | done(null, res.body.commentIds[0]); 69 | }); 70 | }); 71 | }; 72 | 73 | // Creates a comment reply and calls the callback when finished. 74 | const createCommentReply = function (pad, comment, replyData, done) { 75 | replyData = replyData || {}; 76 | replyData.commentId = comment; 77 | replyData.name = replyData.name || 'John Doe'; 78 | replyData.text = replyData.text || 'This is a reply'; 79 | common.init().then((agent) => { 80 | agent.post(commentRepliesEndPointFor(pad)) 81 | .send({ 82 | apikey: apiKey, 83 | data: JSON.stringify([replyData]), 84 | }) 85 | .expect(200) 86 | .expect('Content-Type', /json/) 87 | .expect(codeToBe0) 88 | .end((err, res) => { 89 | if (err) return done(err); 90 | done(null, res.body.replyIds[0]); 91 | }); 92 | }); 93 | }; 94 | 95 | /* ********** Available functions/values: ********** */ 96 | exports.apiVersion = apiVersion; 97 | exports.createPad = createPad; 98 | exports.readOnlyId = readOnlyId; 99 | exports.createComment = createComment; 100 | exports.createCommentReply = createCommentReply; 101 | exports.codeToBe0 = codeToBe0; 102 | exports.codeToBe1 = codeToBe1; 103 | exports.codeToBe4 = codeToBe4; 104 | exports.commentsEndPointFor = commentsEndPointFor; 105 | exports.commentRepliesEndPointFor = commentRepliesEndPointFor; 106 | -------------------------------------------------------------------------------- /templates/commentBarButtons.ejs: -------------------------------------------------------------------------------- 1 |
  • 2 |
  • 3 | 4 | 5 | 6 |
  • 7 | -------------------------------------------------------------------------------- /templates/commentIcons.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /templates/comments.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 42 | 43 | 44 | 84 | 85 | 86 | 93 | 94 | 95 | 111 | 112 | 113 | 129 | 130 | 131 | 140 | -------------------------------------------------------------------------------- /templates/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Comments 5 | 6 | 7 | 8 | <%- body %> 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/menuButtons.ejs: -------------------------------------------------------------------------------- 1 |
  • 2 | Comment 3 |
  • 4 | -------------------------------------------------------------------------------- /templates/settings.ejs: -------------------------------------------------------------------------------- 1 |

    2 | 3 | 4 |

    5 | -------------------------------------------------------------------------------- /templates/styles.html: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------