1 Commits
Author SHA1 Message Date
Luka Romih 9867875ef2 First full release 2023-03-10 12:50:23 +01:00
285 changed files with 10980 additions and 4692 deletions
+1
View File
@@ -3,3 +3,4 @@ node_modules/
/.env
.idea
express/public/stylesheets
/draft/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Amebis, d.o.o.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+164 -3
View File
@@ -1,5 +1,166 @@
# Terminološki portal
# Terminology Portal
## Navodila za namesitev
## Quick Overview
Navodila so zaradi dinamičnih sprememb v integriranih zunanjih komponentah še v pripravi in bodo pripravljena, ko bo celota stabilnejša.
An opensource Terminology portal which anybody can set up (hopefully without much trouble).
The main instance is available at [https://terminoloski.slovenscina.eu](https://terminoloski.slovenscina.eu).
You can use your own instance completely independently or link it with other such instances, including the main one, and have them share dictionaries and their entries.
Likewise, the included consultancy module can use its built-in, fully featured tools for your consultants to process terminology questions (default)
or simply be linked with the one at [ISJFR, ZRC SAZU](https://isjfr.zrc-sazu.si) so that their professional consultants will answer the questions.
## Setting up your own instance of the portal
_The guide is written for Ubuntu 22.04 LTS. Adjust according to your OS._
### 1. Install external dependencies first
1. Install the [Docker Engine](https://docs.docker.com/engine/install/).
2. Install the [Term Candidate Extraction API](https://github.com/clarinsi/rsdo_luscilnik).
3. Make sure you have a SMTP server/service available, which the portal will require.
4. Install a reverse proxy (e.g. [Nginx](https://nginx.org/en/)).
### 2. Reconfigure the docker engine
By default, docker engine doesn't perform log rotation, which will eventually lead to disk exhaustion.
Search indexes will prevent all write operations at 90% full which will make many operations on the portal fail until
disk usage falls under 85% again.
Also, by default, image build process is not optimized.
To address both of the above, it is suggested to add the following to the _/etc/docker/daemon.json_ :
```
{
"log-driver": "local",
"features": {
"buildkit": true
}
}
```
Even better, set `"log-driver": "journald"` instead, so the logs get sent straight to your journald.
Alternatively, set your own preferred log aggregator as per [the official documentation](https://docs.docker.com/config/containers/logging/configure/).
**You need to restart the engine for the settings to take effect. Run:** `sudo systemctl restart docker.service`
### 3. Get and configure the portal
1. Using [git](https://git-scm.com/) transfer the portal code into desired directory with `git clone https://github.com/clarinsi/rsdo_term_portal.git <directory path>`
2. Navigate into that directory and copy the _.env_ file from the _dev_ folder one level up (into the root directory of the portal).
3. Open the copied _.env_ file in a text editor and adjust the parameters of your portal.
### 4. (Advanced/optional) Adjust data storage locations
By default, all data is stored and persisted inside Docker's [volumes](https://docs.docker.com/storage/volumes/)
on your host's root partition.
If you have a setup where for any reason you want to store data elsewhere,
you can reconfigure the docker-compose.prod.yml to use [bind mounts](https://docs.docker.com/storage/bind-mounts/),
just be mindful not to break shared volume dependencies.
_Using bind mounts has not been tested yet and might cause additional complications
regarding file system permissions._
### 5. Start the portal service cluster
1. Inside the portal directory, run the following command in CLI: `docker compose -f docker-compose.prod.yml --profile concordancer-manager up -d --build`
2. It's going to take some time for all the resources to get downloaded and built.
Wait for control to be returned to CLI before continuing.
### 6. First time concordancer setup
1. Open a bash session inside the _concordancer-manager_ container by running:
`docker exec -it $(docker ps --format "{{.Names}}" | grep concordancer-manager) bash`
2. In that session, run: `dotnet Rsdo.Concordancer.SystemManager.dll createMasterDb --connectionString="Host=postgres;Username=concordancer;Password=<POSTGRES_CONCORDANCER_PASSWORD>;Database=postgres"`.
Replace `<POSTGRES_CONCORDANCER_PASSWORD>` with the value you set for it in the _.env_ file.
3. After that, also in that session, run: `dotnet Rsdo.Concordancer.SystemManager.dll importSloleks --sourceFile=/sloleks/Sloleks2.0.LMF/sloleks_clarin_2.0.xml`.
This command takes a bit more time than the previous one to finish.
4. Close the session _(Ctrl + D)_
### 7. Reverse proxy setup
1. Forward the majority of trafic to the the main port,
set appropriate headers, needed for normal functioning of the webserver
and increase the maximum allowed request body size.
Example location block for Nginx config:
```
location / {
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 1100M;
proxy_pass http://127.0.0.1:<EXPRESS_LISTEN_PORT>;
}
```
Replace <EXPRESS_LISTEN_PORT> with whichever port you set as EXPRESS_LISTEN_PORT in the .env file,
or even change the IP address or protocol if you've done done some changes to docker-compose.prod.yml or other system wide configuration and generally know, what you're doing.
2. Forward concordancer client trafic to the _/korpus_ path
Example location block for Nginx config:
```
location /korpus/ {
proxy_pass http://127.0.0.1:<CONCORDANCER_LISTEN_PORT>/;
}
```
Same as above, except replace <CONCORDANCER_LISTEN_PORT> with the value of CONCORDANCER_LISTEN_PORT.
3. Don't forget setting up proper SSL termination, compression, ...
4. Also don't forget to make your changes take effect.
For Nginx, `sudo systemctl reload nginx.service` is usually the easiest way.
### 8. Done
Your portal should now be available at whichever address/domain your reverse proxy is set to listen to.
## Linking your instance with others
1. Using a web browser, navigate to your portal's main page.
2. In the upper right corner, change language to English.
(the instructions are written using english UI labels)
3. Log in as the portal admin user.
4. Navigate Menu -> Administration -> Links -> Add link.
5. Fill in all the fields:
- **Portal name** - your choice; it'll be displayed as the source of found entries, when the users use the portal's search tool.
- **Portal label** - a 2 letter code of your choice; same as portal name, except a shorter version.
- **URL (dictionaries sync)** - Format: <target_portal_origin>/api/v1/system/inter-instance-sync/dictionaries
Replace <target_portal_origin> with the origin of the portal, the dictionaries of which you wish to add to your portal.
To link the main instance of the portal, for example, you would enter:
https://terminoloski.slovenscina.eu/api/v1/system/inter-instance-sync/dictionaries
- **URL (dictionary entries sync)** - Format: <target_portal_origin>/api/v1/system/inter-instance-sync/dictionary/$SOURCE_ID/entries?lastSynced=$SINCE
<target_portal_origin> should be the same as for previous URL.
To link the main instance of the portal, for example, you would enter:
https://terminoloski.slovenscina.eu/api/v1/system/inter-instance-sync/dictionary/$SOURCE_ID/entries?lastSynced=$SINCE
6. Click _Add link_. You will be returned to the list of linked portals, where the newly created portal will now be listed.
7. Click _Dictionaries_ to see the list of all available dictionaries of the target portal. Enable the ones you wish to be added to your portal, click _Save_.
8. The selected dictionaries and all their entries will be automatically synchronized each night.
## Linking to ZRC SAZU Terminological counselling
If you wish professional counselors to answer your users' terminological questions,
you can do so by simply toggling a switch in the administration console.
1. As previously described, navigate to the Menu -> Administration -> Basic settings -> Consultancy.
2. Select _ZRC SAZU Terminological counselling_.
3. Click Save.
That's it.
Your questions will now be automatically forwarded to ZRC SAZU Terminological counselling
and their answers back to your portal.
+1 -1
View File
@@ -2,5 +2,5 @@ FROM alpine:3.16 AS builder
WORKDIR /sloleks
RUN wget -qO- "https://www.clarin.si/repository/xmlui/bitstream/handle/11356/1230/Sloleks2.0.LMF.zip?sequence=3&isAllowed=y" | unzip -
FROM ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.0
FROM ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.3
COPY --from=builder /sloleks /sloleks
+6
View File
@@ -0,0 +1,6 @@
FROM alpine:3.16 AS builder
WORKDIR /sloleks
RUN wget -qO- "https://www.clarin.si/repository/xmlui/bitstream/handle/11356/1230/Sloleks2.0.LMF.zip?sequence=3&isAllowed=y" | unzip -
FROM ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.3
COPY --from=builder /sloleks /sloleks
+45 -5
View File
@@ -1,26 +1,66 @@
# Initial values are default vaules.
# Change if needed.
# PORTAL PARAMETERS
# Initial values are default vaules.
# Changing at least all passwords and secrets is strongly advised.
# Preferably at least 16 chars longs and randomly generated.
# Initial values for the admin user (portal administrator role),
# that will be created on first startup.
PORTAL_ADMIN_INITIAL_EMAIL="admin@email.com"
PORTAL_ADMIN_INITIAL_PASSWORD="admin"
# The endpoint where you've made Term Candidate Extraction API available.
# It is required for the extraction module to work properly.
EXTRACTION_API_ORIGIN="http://rsdo.lhrs.feri.um.si:8080"
# The main port, which will be exposed on localhost of the of host running the Docker Engine.
# If you need to expose it on all of host's network interfaces,
# modify docker-compose.prod.yml in project's root directory where it is used.
EXPRESS_LISTEN_PORT=3000
# Set it to true for production and properly configure a reverse proxy in front of it.
EXPRESS_IS_BEHIND_PROXY=false
# It is used for cookie signing.
EXPRESS_SECRET="weak_secret"
# Used for administration.
POSTGRES_ADMIN_PASSWORD="weak_admin_password" # user: postgres
# Used by the express webserver service.
POSTGRES_EXPRESS_PASSWORD="weak_express_password" # user: express
# Used by the concordancer service.
POSTGRES_CONCORDANCER_PASSWORD="weak_concordancer_password" # user: concordancer
# Exposed on host's localhost for administration purposes.
POSTGRES_LISTEN_PORT=5432
# Configuration for webserver to communicate with your SMTP provider.
SMTP_HOST="maildev"
SMTP_PORT=1025
SMTP_TLS_REJECT_UNAUTHORIZED=true
SMTP_USER=""
SMTP_PASSWORD=""
SMTP_SECURE=false # Most often only "true" for port 465.
SMTP_REQUIRE_TLS=false # If "true" and SMTP_SECURE is "false", only send messages if server supports STARTTLS.
SMTP_TLS_ALLOW_INVALID_CERTS=false
SMTP_FROM="Sender name <sender@server.com>"
# Exposed on host's localhost for search index administration purposes.
# Used only if docker compose was ran with opensearch-dashboards profile.
OS_DASHBOARDS_LISTEN_PORT=3002
# The second port, which will be exposed on localhost of the of host running the Docker Engine.
# Used by the concordancer client.
CONCORDANCER_LISTEN_PORT=3003
# Development only settings
MAILDEV_WEB_GUI_PORT=3001
# Production only settings
# The origin, where the portal will be made available.
URL_ORIGIN="https://mywebportal.com"
# Development only settings (not used in production)
MAILDEV_WEB_GUI_PORT=3001
+28 -15
View File
@@ -21,16 +21,23 @@ services:
PGDATABASE: term_portal
SMTP_HOST: "${SMTP_HOST:?}"
SMTP_PORT: "${SMTP_PORT:?}"
SMTP_TLS_REJECT_UNAUTHORIZED: "${SMTP_TLS_REJECT_UNAUTHORIZED:?}"
SMTP_USER: "${SMTP_USER?}"
SMTP_PASSWORD: "${SMTP_PASSWORD?}"
SMTP_SECURE: "${SMTP_SECURE:?}"
SMTP_REQUIRE_TLS: "${SMTP_REQUIRE_TLS:?}"
SMTP_TLS_ALLOW_INVALID_CERTS: "${SMTP_TLS_ALLOW_INVALID_CERTS:?}"
SMTP_FROM: "${SMTP_FROM:?}"
ORIGIN: "${URL_ORIGIN:?}"
PORTAL_ADMIN_INITIAL_EMAIL: "${PORTAL_ADMIN_INITIAL_EMAIL:?}"
PORTAL_ADMIN_INITIAL_PASSWORD: "${PORTAL_ADMIN_INITIAL_PASSWORD:?}"
EXTRACTION_API_ORIGIN: "${EXTRACTION_API_ORIGIN:?}"
volumes:
- express-data:/usr/src/app/data_files
ports:
- "127.0.0.1:${EXPRESS_LISTEN_PORT:?}:3000"
postgres:
image: postgres:15-alpine
image: postgres:15.1-alpine3.17
restart: always
environment:
POSTGRES_PASSWORD: "${POSTGRES_ADMIN_PASSWORD:?}"
@@ -58,18 +65,18 @@ services:
environment:
- cluster.name=term-portal
- node.name=node-1
# - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
# - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
- bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
- "DISABLE_INSTALL_DEMO_CONFIG=true" # disables execution of install_demo_configuration.sh bundled with security plugin, which installs demo certificates and security configurations to OpenSearch
- "DISABLE_SECURITY_PLUGIN=true" # disables security plugin entirely in OpenSearch by setting plugins.security.disabled: true in opensearch.yml
- "discovery.type=single-node" # disables bootstrap checks that are enabled when network.host is set to a non-loopback address
# ulimits:
# memlock:
# soft: -1
# hard: -1
# nofile:
# soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
# hard: 65536
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
hard: 65536
volumes:
- opensearch-data:/usr/share/opensearch/data
@@ -88,8 +95,10 @@ services:
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
concordancer:
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.0
build: concordancer
# image: ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.3
build:
context: concordancer
dockerfile: Dockerfile.prod
depends_on:
- postgres
- opensearch
@@ -106,7 +115,7 @@ services:
# Disabled by default. Enable if needed.
concordancer-manager:
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.0
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.3
profiles:
- concordancer-manager
depends_on:
@@ -137,7 +146,11 @@ services:
PGDATABASE: term_portal
SMTP_HOST: "${SMTP_HOST:?}"
SMTP_PORT: "${SMTP_PORT:?}"
SMTP_TLS_REJECT_UNAUTHORIZED: "${SMTP_TLS_REJECT_UNAUTHORIZED:?}"
SMTP_USER: "${SMTP_USER?}"
SMTP_PASSWORD: "${SMTP_PASSWORD?}"
SMTP_SECURE: "${SMTP_SECURE:?}"
SMTP_REQUIRE_TLS: "${SMTP_REQUIRE_TLS:?}"
SMTP_TLS_ALLOW_INVALID_CERTS: "${SMTP_TLS_ALLOW_INVALID_CERTS:?}"
SMTP_FROM: "${SMTP_FROM:?}"
entrypoint: ["scheduled/entrypoint.sh"]
command: ["crond", "-f", "-l", "2"]
+16 -5
View File
@@ -20,9 +20,16 @@ services:
PGDATABASE: term_portal
SMTP_HOST: "${SMTP_HOST:?}"
SMTP_PORT: "${SMTP_PORT:?}"
SMTP_TLS_REJECT_UNAUTHORIZED: "${SMTP_TLS_REJECT_UNAUTHORIZED:?}"
SMTP_USER: "${SMTP_USER?}"
SMTP_PASSWORD: "${SMTP_PASSWORD?}"
SMTP_SECURE: "${SMTP_SECURE:?}"
SMTP_REQUIRE_TLS: "${SMTP_REQUIRE_TLS:?}"
SMTP_TLS_ALLOW_INVALID_CERTS: "${SMTP_TLS_ALLOW_INVALID_CERTS:?}"
SMTP_FROM: "${SMTP_FROM:?}"
ORIGIN: http://localhost:${EXPRESS_LISTEN_PORT:?}
PORTAL_ADMIN_INITIAL_EMAIL: "${PORTAL_ADMIN_INITIAL_EMAIL:?}"
PORTAL_ADMIN_INITIAL_PASSWORD: "${PORTAL_ADMIN_INITIAL_PASSWORD:?}"
EXTRACTION_API_ORIGIN: "${EXTRACTION_API_ORIGIN:?}"
volumes:
# When developing/debugging concordancer, disable express bind mount and node_modules volume and enable express-data.
# - express-data:/usr/src/app/data_files
@@ -35,7 +42,7 @@ services:
tty: true
postgres:
image: postgres:15-alpine
image: postgres:15.1-alpine3.17
# Use any of the two custom Dockerfiles below for debugging PL/pgSQL functions.
# Disable docker-entrypoint-initdb.d bind mount when doing so.
# build:
@@ -95,7 +102,7 @@ services:
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
concordancer:
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.0
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.3
build: concordancer
profiles:
- develop-concordancer
@@ -114,7 +121,7 @@ services:
- sloleks:/sloleks
concordancer-manager:
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.0
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.3
profiles:
- develop-concordancer
depends_on:
@@ -146,7 +153,11 @@ services:
# PGDATABASE: term_portal
# SMTP_HOST: "${SMTP_HOST:?}"
# SMTP_PORT: "${SMTP_PORT:?}"
# SMTP_TLS_REJECT_UNAUTHORIZED: "${SMTP_TLS_REJECT_UNAUTHORIZED:?}"
# SMTP_USER: "${SMTP_USER?}"
# SMTP_PASSWORD: "${SMTP_PASSWORD?}"
# SMTP_SECURE: "${SMTP_SECURE:?}"
# SMTP_REQUIRE_TLS: "${SMTP_REQUIRE_TLS:?}"
# SMTP_TLS_ALLOW_INVALID_CERTS: "${SMTP_TLS_ALLOW_INVALID_CERTS:?}"
# SMTP_FROM: "${SMTP_FROM:?}"
# volumes:
# - /usr/src/app/node_modules
+19 -5
View File
@@ -6,12 +6,15 @@ const helmet = require('helmet')
const favicon = require('serve-favicon')
const cookieParser = require('cookie-parser')
const createError = require('http-errors')
const debug = require('debug')('termPortal:app')
const i18next = require('i18next')
const i18nextMiddleware = require('i18next-http-middleware')
// const debug = require('debug')('termPortal:app')
// Import own modules.
const { isBehindProxy, secret } = require('./config/keys')
const helmetConfig = require('./config/helmet')
const session = require('./middleware/session')
const i18n = require('./middleware/i18n')
const passport = require('./middleware/auth')
const user = require('./middleware/user')
const settings = require('./middleware/settings')
@@ -37,6 +40,7 @@ app.locals.basedir = viewsPath
// Other settings.
const inDevEnv = app.get('env') === 'development'
if (isBehindProxy) app.set('trust proxy', 1) // Trust first proxy.
app.locals.inDevEnv = inDevEnv
// Mount middleware.
app.use(logger('dev'))
@@ -50,10 +54,19 @@ app.use(session)
app.use(passport.initialize())
app.use(passport.session())
app.use(passport.authenticate('remember-me'))
app.use(i18n.determineRequestLanguage)
app.use(i18nextMiddleware.handle(i18next))
app.use(user.enhance)
app.use(settings.prepareRequiredSettings)
app.use(enhanceLocals)
if (inDevEnv) {
app.post(
'/locales/add/:lng/:ns',
i18nextMiddleware.missingKeyHandler(i18next)
)
}
// Apparently express-debug can't be run inside another middleware and must be run in this file.
// To help you debug, temporarily uncomment the next line, but comment the helmet line due to strict CSP.
// if (app.get('env') === 'development') require('express-debug')(app)
@@ -75,7 +88,8 @@ app.use((req, res, next) => next(createError(404)))
// Error handler.
app.use((err, req, res, next) => {
if (err.status !== 404) debug(err)
// eslint-disable-next-line no-console
if (err.status !== 404) console.error(err)
// Set error info to be displayed to user depending on environment.
let message, error
@@ -86,8 +100,8 @@ app.use((err, req, res, next) => {
} else {
message =
err.status === 404
? 'Stran ne obstaja'
: 'Prišlo je do strežniške napake. Poskusite kasneje.'
? req.t('Stran ne obstaja')
: req.t('Prišlo je do strežniške napake. Poskusite kasneje.')
error = {}
}
@@ -99,7 +113,7 @@ app.use((err, req, res, next) => {
}
// Render the error page.
res.render('error', { title: 'Napaka', message, error })
res.render('error', { title: req.t('Napaka'), message, error })
})
module.exports = app
+9 -2
View File
@@ -4,12 +4,16 @@
* Module dependencies.
*/
// Temporary noop function to be used until i18n is fully in place.
global.__ = str => str
const db = require('../models/db')
const cache = require('../models/cache')
const searchEngine = require('../models/search-engine')
const email = require('../models/email')
const init = require('../config/init')
const { seedDummyData } = require('../models/comment')
const { initDemoData } = require('../models/demo-paginacija')
// const { initDemoData } = require('../models/demo-paginacija')
const app = require('../app')
const debug = require('debug')('termPortal:server')
const http = require('http')
@@ -37,7 +41,8 @@ const server = http.createServer(app)
db.waitForConnection(),
cache.waitForConnection(),
searchEngine.waitForConnection(),
email.waitForConnection()
email.waitForConnection(),
init()
])
await searchEngine.initEntryIndex()
await searchEngine.initConsultancyEntryIndex()
@@ -102,5 +107,7 @@ function onError(error) {
function onListening() {
const addr = server.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
// eslint-disable-next-line no-console
console.log('Started listening')
debug('Listening on ' + bind)
}
+6
View File
@@ -0,0 +1,6 @@
const { mkdir } = require('fs/promises')
const { TEMP_EXPORT_PATH } = require('./settings')
module.exports = async () => {
await mkdir(TEMP_EXPORT_PATH, { recursive: true })
}
+9 -3
View File
@@ -4,8 +4,14 @@ module.exports = {
cookiesSecure: process.env.COOKIES_SECURE === 'true',
smtpHost: process.env.SMTP_HOST,
smtpPort: process.env.SMTP_PORT,
smtpTlsRejectUnauthorized:
process.env.SMTP_TLS_REJECT_UNAUTHORIZED === 'true',
smtpUser: process.env.SMTP_USER,
smtpPassword: process.env.SMTP_PASSWORD,
smtpSecure: process.env.SMTP_SECURE === 'true',
smtpRequireTls: process.env.SMTP_REQUIRE_TLS === 'true',
smtpAllowInvalidCerts: process.env.SMTP_TLS_ALLOW_INVALID_CERTS === 'true',
smtpFrom: process.env.SMTP_FROM,
origin: process.env.ORIGIN
origin: process.env.ORIGIN,
portalAdminInitialEmail: process.env.PORTAL_ADMIN_INITIAL_EMAIL,
portalAdminInitialPassword: process.env.PORTAL_ADMIN_INITIAL_PASSWORD,
extractionApiOrigin: process.env.EXTRACTION_API_ORIGIN
}
+6 -2
View File
@@ -16,7 +16,11 @@ exports.DEFAULT_HITS_PER_PAGE = 10
exports.EDITOR_MAX_HITS = 10000
// If you change this one, don't forget to also update the volume mount in docker-compose.prod.yml.
exports.DATA_FILES_PATH = 'data_files'
// If you change this one, don't forget to also update the volume mount in docker-compose.prod.yml
// and nodemon ignore flag in package.json scripts.
const DATA_FILES_PATH = 'data_files'
exports.DATA_FILES_PATH = DATA_FILES_PATH
exports.TEMP_EXPORT_PATH = `${DATA_FILES_PATH}/export_temp`
exports.MAX_EXTRACTIONS_PER_USER = 5
+248 -12
View File
@@ -1,11 +1,18 @@
const ConsultancyEntry = require('../../../models/consultancy-entry')
const Domain = require('../../../models/domain')
const User = require('../../../models/user')
const { promisify } = require('util')
const i18next = require('i18next')
const {
deleteConsultancyEntryFromIndex
} = require('../../../models/search-engine')
const email = require('../../../models/email')
const helper = require('../../../models/helpers')
const { getInstanceSetting } = require('../../../models/helpers')
const { searchConsultancyEntryIndex } = require('../../../models/search-engine')
const { prepareConsultancyEntries } = require('../../../models/helpers/search')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const generateQuery = require('../../../models/helpers/search/generate-query')
const consultancy = {}
@@ -23,6 +30,43 @@ consultancy.listNewEntries = async (req, res) => {
res.send(data)
}
consultancy.sendPaginationData = async (req, res) => {
let requestType = req.query.type
const isAdminPage = req.query.isAdmin === 'true'
let page = +req.query.p || 1
if (page < 1) {
page = 1
}
if (isAdminPage) {
if (
req.user &&
(req.user.hasRole('portal admin') ||
req.user.hasRole('consultancy admin'))
) {
// Everything is allowed, nothing to do here
} else if (req.user && req.user.hasRole('consultant')) {
if (!(requestType === 'in progress' || requestType === 'published')) {
return res.status(400).send()
}
} else {
// for now, public can only see published entries
requestType = 'published'
}
}
return await consultancyRequestItems(
req,
res,
requestType || 'published',
'components/consultancy/api/consultancy-item-rendered',
isAdminPage,
!isAdminPage, // -> In current implementation, it is just the inverse of isAdminPage
page
)
}
consultancy.createQuestion = async (req, res) => {
const q = req.body
const consultancyEntry = {}
@@ -35,7 +79,7 @@ consultancy.createQuestion = async (req, res) => {
const { description } = q
if (!description) {
return res.status(400).send('description is a required parameter!')
return res.status(400).send('Description is a required parameter!')
}
consultancyEntry.description = description
@@ -52,24 +96,46 @@ consultancy.createQuestion = async (req, res) => {
consultancyEntry[key] = helper.removeHtmlTags(consultancyEntry[key]).trim()
})
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
const isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
// TODO SEND EMAIL
// TODOOOOOOOOO
let domainNameSl
if (consultancyEntry.domainPrimaryIdInitial) {
domainNameSl = (
await Domain.fetchById(consultancyEntry.domainPrimaryIdInitial)
).nameSl
} else {
domainNameSl = ''
}
const emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
let emails
let subjectText
if (isOwnConsultancyEnabled) {
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
subjectText = req.t('Ustvarjeno novo vprašanje v svetovalnici')
emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
} else {
subjectText = req.t('Novo vprašanje za Terminološko svetovalnico')
emails = await getInstanceSetting('zrc_email')
}
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-creation-notify', {
propertyToPassGoesHere: 'test1234'
nameAndSurname: `${res.locals.user.firstName} ${res.locals.user.lastName}`,
email: res.locals.user.email,
domain: domainNameSl,
institution: consultancyEntry.institution,
question: consultancyEntry.description,
existingSolutions: consultancyEntry.existingSolutions,
examplesOfUse: consultancyEntry.examplesOfUse
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Ustvarjeno novo vprašanje v svetovalnici',
subject: subjectText,
html: emailHtml
})
/// /////////////////
res.status(201).send()
}
@@ -179,9 +245,10 @@ consultancy.assign = async (req, res) => {
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-assigned')
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Novo terminološko vprašanje',
subject: req.t('Novo terminološko vprašanje'),
html: emailHtml
})
@@ -234,9 +301,10 @@ consultancy.sendToReview = async (req, res) => {
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-item-review')
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Potrditev objave',
subject: req.t('Potrditev objave'),
html: emailHtml
})
@@ -254,6 +322,19 @@ consultancy.publish = async (req, res) => {
if (!entry.title) {
return res.status(400).send('Answer not completed')
}
const author = await User.fetchUser(entry.authorId)
const portalName = await getInstanceSetting(`portal_name_${author.language}`)
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-publish-notify', {
portalName
})
await email.send({
to: author.email,
subject: i18next.t('Objava terminološkega vprašanja', {
lng: author.language
}),
html: emailHtml
})
await ConsultancyEntry.publish(questionId, answerAuthors)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
@@ -267,7 +348,9 @@ consultancy.updateQuestion = async (req, res) => {
if (!id) return res.status(400).send({})
if (questionTitle === '' || question === '' || answer === '') {
return res.status(422).send('Polja naslov, vprašanje in mnenje so obvezna!')
return res
.status(422)
.send(req.t('Polja naslov, vprašanje in mnenje so obvezna!'))
}
const entry = await ConsultancyEntry.fetchById(id)
@@ -318,4 +401,157 @@ consultancy.deleteQuestion = async (req, res) => {
res.send()
}
async function consultancyRequestItems(
req,
res,
type,
url,
isAdminPage = false,
privilegeToSeAll = true, // this method seperates consultancy main from admin, so all results get visible TO ALL REGISTERED USERS, not just admins
page
) {
const searchString = req.query.q?.trim() ?? ''
/// //////////////////////////////////////////
// let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
/// filter selected domains for the prompt ///
// Not duplicates of this code arise...
// const pdList = intoDbArray(req.query.pd, 'always')
// allPrimaryDomains = allPrimaryDomains.map(entry => {
// if (pdList.includes(`${entry.id}`)) {
// entry.selected = true
// }
// return entry
// })
/// //////////////////////////////////////////
let assignedConsultant
if (
!privilegeToSeAll &&
req.user &&
!(req.user.hasRole('portal admin') || req.user.hasRole('consultancy admin'))
) {
assignedConsultant = req.user.id
} else {
assignedConsultant = undefined
}
const filters = {
status: type,
assignedConsultant,
primaryDomain: req.query.pd
}
if (!isAdminPage) {
filters.assignedConsultant = undefined
filters.status = 'published' // guard
}
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
// const page = +req.query.p > 0 ? +req.query.p : 1
const hitsQuery = generateQuery.consultancy(
searchString,
filters,
hitsPerPage,
page
)
const hits = await searchConsultancyEntryIndex(hitsQuery)
const numberOfAllHits = hits.body.hits.total.value
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
let entries = prepareConsultancyEntries(hits)
// console.log({ entries, numberOfAllHits, numberOfAllPages })
entries = entries.map(entry => {
entry.primaryDomain = entry.primaryDomain
? entry.primaryDomain.nameSl
: req.t('nedefinirano')
if (entry.assignedConsultants) {
entry.firstName = entry.assignedConsultants[0]?.firstName
entry.lastName = entry.assignedConsultants[0]?.lastName
}
if (entry.assignedConsultants && entry.assignedConsultants.length > 1) {
entry.sharedAuthors = []
for (let i = 1; i < entry.assignedConsultants.length; i++) {
// skip first element (moderator)
entry.sharedAuthors.push(
`${entry.assignedConsultants[i].firstName} ${entry.assignedConsultants[i].lastName}`
)
}
}
if (entry.timeCreated) {
// TODO i18n date format
const date = new Date(entry.timeCreated)
entry.formattedTimeCreated = `${date.getDate()}. ${
date.getMonth() + 1
}. ${date.getFullYear()}`
}
return entry
})
// const entryList = await mapEntryList(inProgressEntryList)
const userList = await User.fetchConsultants()
if (!isAdminPage) {
entries.map(entry => {
const MAX_CHARACTER_LENGTH = 200
let appendAnswer = ''
let appendQuestion = ''
if (entry.answer && entry.answer.length > MAX_CHARACTER_LENGTH) {
appendAnswer = '...'
}
if (entry.question && entry.question.length > MAX_CHARACTER_LENGTH) {
appendQuestion = '...'
}
entry.answerSummary = `${entry.answer.slice(
0,
MAX_CHARACTER_LENGTH
)}${appendAnswer}`
entry.question = `${entry.question.slice(
0,
MAX_CHARACTER_LENGTH
)}${appendQuestion}`
return entry
})
}
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render(url, {
entries, // entryList,
userList,
numberOfAllPages,
isAdminPage,
section: consultancyAdminPageMapper(type),
queryCount: numberOfAllHits
})
}
// mapping required due to the inconsistent naming convention
function consultancyAdminPageMapper(type) {
if (type === 'in progress') {
return { inProgress: true }
}
if (type === 'review') {
return { prepared: true }
}
return { type: true }
}
module.exports = consultancy
+141 -8
View File
@@ -1,5 +1,7 @@
const { rm } = require('fs/promises')
const Dictionary = require('../../../models/dictionary')
const Entry = require('../../../models/entry')
const Extraction = require('../../../models/extraction')
const {
searchEntryIndex,
deleteEntryFromIndex,
@@ -9,6 +11,7 @@ const genEditorAllQuery = require('../../../models/helpers/search/generate-query
const { prepareEditorEntries } = require('../../../models/helpers/search')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { minEntriesRequirementCheckAndAct } = require('../../helpers/dictionary')
const { getExportFilesPath } = require('../../../models/helpers/dictionary')
const dictionary = {}
@@ -115,6 +118,8 @@ dictionary.delete = async (req, res) => {
const dictionaryId = +req.params.dictionaryId
await Dictionary.delete(dictionaryId)
await deleteDictionaryEntriesFromIndex(dictionaryId)
const exportFilesPath = getExportFilesPath(dictionaryId)
await rm(exportFilesPath, { recursive: true, force: true })
res.end()
}
@@ -167,6 +172,56 @@ dictionary.listDomainLabels = async (req, res) => {
res.send({ page, numberOfAllPages, results })
}
dictionary.listFilteredDomainLabels = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
let { q } = req.query
if (!q) {
q = ''
}
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchFilteredPaginationDomainLabels(
dictionaryId,
q,
resultsPerPage,
page
)
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render('utilities/response-pug-wrapper/domainLabelLister', {
dictionary: { id: dictionaryId },
numberOfAllPages,
results
})
}
dictionary.listSecondaryDomainData = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
let { q } = req.query
const page = +req.query.p > 0 ? +req.query.p : 1
if (!q) {
q = ''
}
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchFilteredSecondaryDomains(q, resultsPerPage, page)
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render('utilities/response-pug-wrapper/secondaryDomainLister', {
page,
numberOfAllPages,
results
})
}
dictionary.listSecondaryDomains = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
@@ -176,14 +231,92 @@ dictionary.listSecondaryDomains = async (req, res) => {
res.send({ page, numberOfAllPages, results })
}
dictionary.extractionImport = async (req, res) => {
// TODO Import logic (Luka's task)
// const { id: dictionaryId, extractionId } = req.params
// const { from, to } = req.query
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
// console.log({ dictionaryId, extractionId, fromIndex, toIndex })
res.send('IMPORTING')
dictionary.showImportFromFileForm = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllImports(dictionaryId, resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.showExportToFileForm = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchExports(dictionaryId, resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.importFromExtraction = async (req, res) => {
const { id: dictionaryId, extractionId } = req.params
const { from, to } = req.body
const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
const toIndex = Number.isInteger(+(to === '' ? undefined : to))
? Math.abs(to)
: undefined
// TODO Authentication, authorization, validation.
const termCandidatesToImport = await Extraction.fetchTermCandidatesSlice(
extractionId,
fromIndex,
toIndex
)
await Dictionary.importFromExtraction(
dictionaryId,
req.user.id,
termCandidatesToImport
)
await Dictionary.indexIntoSearchEngine(dictionaryId)
res.end()
}
dictionary.exportBegin = async (req, res) => {
const dictionaryId = req.params.id
const exportParams = {
isValidFilter:
req.body.isValidFilter === 'on' ? undefined : req.body.isValidFilter,
isPublishedFilter:
req.body.isPublishedFilter === 'on'
? undefined
: req.body.isPublishedFilter,
statusFilter:
req.body.statusFilter === 'complete'
? 'complete'
: req.body.statusFilter === 'inEdit'
? 'in_edit'
: undefined,
isTerminologyReviewedFilter:
req.body.isTerminologyReviewedFilter === 'on'
? undefined
: req.body.isTerminologyReviewedFilter,
isLanguageReviewedFilter:
req.body.isLanguageReviewedFilter === 'on'
? undefined
: req.body.isLanguageReviewedFilter,
exportFileFormat: req.body.exportFileFormat
}
const exportId = await Dictionary.beginExport(dictionaryId, exportParams)
res.end()
// Explicitcly catch any errors after the response has been sent
// since the the final error handler won't be able to send another.
try {
// TODO Consider delegating processing of export to a seperate process or at least a seperate thread (much like importing, extraction, ...).
await Dictionary.processExport(exportId)
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
}
module.exports = dictionary
+102 -52
View File
@@ -1,22 +1,27 @@
const { unlink, rm, mkdir } = require('fs/promises')
const { unlink, rm, mkdir, writeFile } = require('fs/promises')
const { promisify } = require('util')
const { URLSearchParams } = require('url')
const multer = require('multer')
const i18next = require('i18next')
const validator = require('validator')
const axios = require('axios')
const {
getExtractionFilesPath,
getDocumentsPath,
getStopTermsPath,
getConllusPath
getConllusPath,
getFileStats
} = require('../../../models/helpers/extraction')
const { checkIfcanBegin } = require('../../helpers/extraction')
const Extraction = require('../../../models/extraction')
const Domain = require('../../../models/domain')
const email = require('../../../models/email')
const { intoDbArray } = require('../../../models/helpers')
const { origin } = require('../../../config/keys')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { origin, extractionApiOrigin } = require('../../../config/keys')
const {
DEFAULT_HITS_PER_PAGE,
TEMP_EXPORT_PATH
} = require('../../../config/settings')
const MAX_FILE_NAME_LENGTH = 100
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
@@ -77,16 +82,19 @@ extraction.docsList = async (req, res) => {
extraction.docsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
const fileStats = await getFileStats(req.file.path)
res.send(fileStats)
} catch (error) {
if (
error instanceof multer.MulterError &&
error.code === 'LIMIT_FILE_SIZE'
) {
throw Error('File too large. Must not be over 1 GB.')
const customError = Error('File too large. Must not be over 1 GB.')
customError.displayInProd = true
throw customError
}
throw error
}
res.end()
}
extraction.docDelete = async (req, res) => {
@@ -108,16 +116,19 @@ extraction.stopTermsList = async (req, res) => {
extraction.stopTermsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
const fileStats = await getFileStats(req.file.path)
res.send(fileStats)
} catch (error) {
if (
error instanceof multer.MulterError &&
error.code === 'LIMIT_FILE_SIZE'
) {
throw Error('File too large. Must not be over 1 GB.')
const customError = Error('File too large. Must not be over 1 GB.')
customError.displayInProd = true
throw customError
}
throw error
}
res.end()
}
extraction.stopTermDelete = async (req, res) => {
@@ -141,7 +152,7 @@ extraction.ossSearch = [
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
...(ossParams.domainUdk && { udk: ossParams.domainUdk })
})
const searchApiUrl = `http://rsdo.lhrs.feri.um.si:8080/oss/steviloBesedilPoIskanju?${searchParams}`
const searchApiUrl = `${extractionApiOrigin}/oss/steviloBesedilPoIskanju?${searchParams}`
const { data: documentCount } = await axios.get(searchApiUrl)
const canSave = documentCount && documentCount <= MAX_OSS_DOCUMENT_COUNT
@@ -183,28 +194,39 @@ extraction.begin = async (req, res) => {
res.send(timeStarted)
if (ossParams) {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOss(extractionId, ossParams.params)
} else {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOwn(extractionId, extractionName)
}
// Explicitcly catch any errors after the response has been sent
// since the the final error handler won't be able to send another.
try {
if (ossParams) {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOss(extractionId, ossParams.params)
} else {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOwn(extractionId, extractionName)
}
const extractionLink = new URL('/luscenje', origin)
const renderAsync = promisify(req.app.render.bind(req.app))
const authorEmail = await Extraction.fetchAuthorEmail(extractionId)
const emailHtml = await renderAsync('email/extraction-done', {
extractionName,
extractionLink
})
await email.send({
to: authorEmail,
subject: 'Luščenje končano',
html: emailHtml
})
const extractionLink = new URL('/luscenje', origin)
const renderAsync = promisify(req.app.render.bind(req.app))
const { email: authorEmail, language: authorLanguage } =
await Extraction.fetchAuthorData(extractionId)
const emailHtml = await renderAsync(
`email/extraction-done_${authorLanguage}`,
{
extractionName,
extractionLink
}
)
await email.send({
to: authorEmail,
subject: i18next.t('Luščenje končano', { lng: authorLanguage }),
html: emailHtml
})
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
}
extraction.duplicate = async (req, res) => {
@@ -214,13 +236,36 @@ extraction.duplicate = async (req, res) => {
}
extraction.termCandidatesExport = async (req, res) => {
// TODO CSV logic (Luka's task)
// const extractionId = req.params.id
// const { from, to } = req.query
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
// console.log({ extractionId, fromIndex, toIndex })
res.download('public/images/help-amebis-logo-pug-demo.png')
// TODO More formats (CSV, TSV, TXT, ...)
const extractionId = req.params.id
const { from, to } = req.query
const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
const toIndex = Number.isInteger(+(to === '' ? undefined : to))
? Math.abs(to)
: undefined
// TODO Authentication, authorization, validation.
const termCandidatesToExport = await Extraction.fetchTermCandidatesSlice(
extractionId,
fromIndex,
toIndex
)
const exportFileName = `term_candidates_${extractionId}`
const exportFilePath = `${TEMP_EXPORT_PATH}/${exportFileName}`
await writeFile(
exportFilePath,
JSON.stringify({ terminoloski_kandidati: termCandidatesToExport })
)
const downloadAsync = promisify(res.download.bind(res))
try {
await downloadAsync(exportFilePath, 'term_candidates.json')
} finally {
await unlink(exportFilePath)
}
}
extraction.listFinishedForUser = async (req, res) => {
@@ -251,8 +296,11 @@ function extractionFileFilter(req, file, cb) {
fileType = 'stopTerms'
break
default:
return cb(Error('Invalid API endpoint'))
default: {
const customError = Error('Invalid API endpoint')
customError.displayInProd = true
return cb(customError)
}
}
const filenamePartsArray = file.originalname.split('.')
@@ -264,30 +312,32 @@ function extractionFileFilter(req, file, cb) {
(fileType === 'stopTerms' &&
fileExtension !== VALID_STOP_TERMS_FILE_EXTENSION)
) {
return cb(Error('Invalid file type'))
const customError = Error('Invalid file type')
customError.displayInProd = true
return cb(customError)
}
const fileName = filenamePartsArray.join('.')
if (!fileName || fileName.length > MAX_FILE_NAME_LENGTH) {
return cb(
Error(
`Filename must be between 1 and ${MAX_FILE_NAME_LENGTH} characters long.`
)
const customError = Error(
`Filename must be between 1 and ${MAX_FILE_NAME_LENGTH} characters long.`
)
customError.displayInProd = true
return cb(customError)
}
if (!validator.isAlphanumeric(fileName[0], 'sl-SI', { ignore: '_' })) {
return cb(
Error(
'Filename must begin with an alphanumeric character or an underscore.'
)
const customError = Error(
'Filename must begin with an alphanumeric character or an underscore.'
)
customError.displayInProd = true
return cb(customError)
}
if (!validator.isAlphanumeric(fileName, 'sl-SI', { ignore: ' _-.' })) {
return cb(
Error(
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
)
const customError = Error(
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
)
customError.displayInProd = true
return cb(customError)
}
req.fileType = fileType
+21
View File
@@ -66,6 +66,27 @@ exports.listMainEntries = async (req, res) => {
{}
)
// duplicated code below for filtering foreignent, optimize later
const categoriesLabels = Object.keys(entriesByCategory)
for (
let categoryIndex = 0;
categoryIndex < categoriesLabels.length;
categoryIndex++
) {
entriesByCategory[categoriesLabels[categoryIndex]] = entriesByCategory[
categoriesLabels[categoryIndex]
].map(entry => {
entry.foreignEntries = entry.foreignEntries?.filter(foreignEntry => {
if (filters.targetLanguages.length > 0) {
return filters.targetLanguages.includes(`${foreignEntry.lang.id}`)
} else {
return true
}
})
return entry
})
}
// res.send({ page, numberOfAllPages, entries })
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
+62 -18
View File
@@ -8,6 +8,7 @@ const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
const generateQuery = require('../models/helpers/search/generate-query')
const { searchConsultancyEntryIndex } = require('../models/search-engine')
const { prepareConsultancyEntries } = require('../models/helpers/search')
const { getInstanceSetting, intoDbArray } = require('../models/helpers')
// const { minEntriesRequirementCheckAndAct } = require('./helpers/dictionary')
const consultancy = {}
@@ -16,6 +17,9 @@ const consultancyAdmin = {}
consultancy.index = async (req, res) => {
req.indexHitPageAmount = '5'
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
return await consultancyRequest(
req,
res,
@@ -25,17 +29,24 @@ consultancy.index = async (req, res) => {
}
consultancy.search = async (req, res) => {
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
res.locals.queryKey = req.query.q
return await consultancyRequest(
req,
res,
'published',
'pages/consultancy/search'
'pages/consultancy/search',
req.t('Odgovori')
)
}
consultancy.specificQuestion = async (req, res) => {
const { id } = req.params
// TODO i18n TIME FORMAT
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
// const author = await User.fetchUser(entry.authorId)
@@ -44,12 +55,13 @@ consultancy.specificQuestion = async (req, res) => {
entry.answerAuthors = entry.answerAuthors.filter(author => author !== '')
let authorString
// TODO I18n
if (entry.answerAuthors.length === 1) {
authorString = 'Avtor'
authorString = req.t('Avtor')
} else if (entry.answerAuthors.length === 2) {
authorString = 'Avtorja'
authorString = req.t('Avtorja')
} else {
authorString = 'Avtorji'
authorString = req.t('Avtorji')
}
entry.domain = allPrimaryDomains.filter(
@@ -62,18 +74,26 @@ consultancy.specificQuestion = async (req, res) => {
entry.domain = false
}
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
res.render('pages/consultancy/item-details', {
allPrimaryDomains,
authorString,
entry
entry,
title: req.t('Odgovor')
})
}
consultancy.new = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
res.render('pages/consultancy/ask', {
allPrimaryDomains
allPrimaryDomains,
title: req.t('Novo vprašanje')
})
}
@@ -83,6 +103,7 @@ consultancyAdmin.new = async (req, res) => {
res,
'new',
'pages/consultancy/admin/index',
req.t('Novo'),
true,
false
)
@@ -95,7 +116,8 @@ consultancyAdmin.users = async (req, res) => {
res.render('pages/consultancy/admin/users', {
allPrimaryDomains,
users
users,
title: req.t('Svetovalci')
})
}
@@ -105,6 +127,7 @@ consultancyAdmin.rejected = async (req, res) => {
res,
'rejected',
'pages/consultancy/admin/rejected',
req.t('Zavrnjeno'),
true,
false
)
@@ -116,8 +139,10 @@ consultancyAdmin.published = async (req, res) => {
res,
'published',
'pages/consultancy/admin/published',
req.t('Objavljeno'),
true,
false
false,
'published'
)
}
@@ -127,6 +152,7 @@ consultancyAdmin.prepared = async (req, res) => {
res,
'review',
'pages/consultancy/admin/prepared',
req.t('Pripravljeno'),
true,
false
)
@@ -142,6 +168,7 @@ consultancyAdmin.inProgress = async (req, res) => {
res,
'in progress',
'pages/consultancy/admin/in-progress',
req.t('V delu'),
true,
false
)
@@ -171,6 +198,7 @@ consultancyAdmin.edit = async (req, res) => {
} else if (editors.filter(editors => editors.id === req.user.id) < 1) {
return res.send('You do not have permsisions to edit this answer')
}
// TODO i18n TIME FORMAT
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
const author = await User.fetchUser(entry.authorId)
@@ -186,7 +214,8 @@ consultancyAdmin.edit = async (req, res) => {
author,
isPublished,
// TODO Luka: I suspect this will not work as intended on staging or production environments. Test.
urlPrefix: req.protocol + '://' + req.get('host')
urlPrefix: req.protocol + '://' + req.get('host'),
title: req.t('Urejanje')
})
}
@@ -198,14 +227,14 @@ function dateMap(obj) {
return obj
}
async function mapDomainIdToDomainNameSlovene(obj) {
async function mapDomainIdToDomainNameSlovene(obj, t) {
try {
const area = await Domain.fetchById(
obj.domainPrimaryId ? obj.domainPrimaryId : obj.domainPrimaryIdInitial
)
obj.area = area.nameSl
} catch {
obj.area = 'Ni področja'
obj.area = t('Ni področja')
}
return obj
@@ -227,14 +256,14 @@ function mapInitialValuesAsEmpty(obj) {
return obj
}
async function mapEntryList(list) {
async function mapEntryList(list, t) {
return await Promise.all(
list.map(entry => {
let entity = utils.compose(dateMap, mapInitialValuesAsEmpty)(entry)
// TODO Each mapDomainIdToDomainNameSlovene call leads to one DB query.
// TODO Test if and what scenarios can lead to too many calls and how it can be avoided.
entity = utils.composeAsync(mapDomainIdToDomainNameSlovene)(entry)
entity = utils.composeAsync(mapDomainIdToDomainNameSlovene)(entry, t)
return entity
})
@@ -283,11 +312,26 @@ async function consultancyRequest(
res,
type,
url,
title = req.t('Svetovanje'),
isAdminPage = false,
privilegeToSeAll = true // this method seperates consultancy main from admin, so all results get visible TO ALL REGISTERED USERS, not just admins
) {
const searchString = req.query.q?.trim() ?? ''
let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
/// filter selected domains for the prompt ///
// Not duplicates of this code arise...
const pdList = intoDbArray(req.query.pd, 'always')
allPrimaryDomains = allPrimaryDomains.map(entry => {
if (pdList.includes(`${entry.id}`)) {
entry.selected = true
}
return entry
})
/// //////////////////////////////////////////
let assignedConsultant
if (
@@ -333,11 +377,11 @@ async function consultancyRequest(
let entries = prepareConsultancyEntries(hits)
// console.log({ entries, numberOfAllHits, numberOfAllPages })
// TODO I18n - nameSl
entries = entries.map(entry => {
entry.primaryDomain = entry.primaryDomain
? entry.primaryDomain.nameSl
: 'nedefinirano'
: req.t('nedefinirano')
if (entry.assignedConsultants) {
entry.firstName = entry.assignedConsultants[0]?.firstName
@@ -365,8 +409,6 @@ async function consultancyRequest(
return entry
})
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
// const entryList = await mapEntryList(inProgressEntryList)
const userList = await User.fetchConsultants()
@@ -401,7 +443,9 @@ async function consultancyRequest(
entries, // entryList,
userList,
numberOfAllPages,
queryCount: numberOfAllHits
queryCount: numberOfAllHits,
consultancyPageType: type,
title
})
}
+88 -37
View File
@@ -9,7 +9,8 @@ const Comment = require('../models/comment')
const genEditorAllQuery = require('../models/helpers/search/generate-query/editor/all')
const { searchEntryIndex } = require('../models/search-engine')
const { prepareEditorEntries } = require('../models/helpers/search')
const { getInstanceSetting } = require('../models/helpers')
const { getInstanceSetting, intoDbArray } = require('../models/helpers')
const { getExportFilesPath } = require('../models/helpers/dictionary')
const { DEFAULT_HITS_PER_PAGE, DATA_FILES_PATH } = require('../config/settings')
const {
statusChangeCheckAndAct,
@@ -17,6 +18,7 @@ const {
} = require('./helpers/dictionary')
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
const Extraction = require('../models/extraction')
const { isGeneratorFunction } = require('util/types')
const importFileBodyParser = multer({
dest: `${DATA_FILES_PATH}/dict_import_temp`,
@@ -39,7 +41,7 @@ dictionary.list = async (req, res) => {
dictionaries = await Dictionary.fetchAllByUser(req.user.id)
}
res.render('pages/dictionaries/list', {
title: 'Seznam slovarjev',
title: req.t('Seznam slovarjev'),
dictionaries
})
}
@@ -51,11 +53,11 @@ dictionary.new = async (req, res) => {
await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchAllLanguages(language)
Dictionary.fetchAllLanguages(language, true)
])
res.render('pages/dictionaries/new', {
title: 'Nov slovar',
title: req.t('Nov slovar'),
allPrimaryDomains,
allSecondaryDomains,
allLanguages
@@ -82,7 +84,7 @@ dictionary.editDescription = async (req, res) => {
])
res.render('pages/dictionaries/description', {
title: 'Ime in opis',
title: req.t('Osnovni podatki'),
allPrimaryDomains,
allSecondaryDomains,
dictionary,
@@ -125,7 +127,7 @@ dictionary.editUsers = async (req, res) => {
}
res.render(viewPath, {
title: 'Uporabniki',
title: req.t('Uporabniki'),
dictionary,
userRights,
entriesCount,
@@ -142,6 +144,7 @@ dictionary.updateUsers = async (req, res) => {
const newDictStatus = await determineNewStatus(isPublished)
// TODO I18n - nameSl
const { nameSl, status: oldDictStatus } = await Dictionary.fetchEditUsers(
dictionaryId
)
@@ -172,12 +175,13 @@ dictionary.updateUsers = async (req, res) => {
dictionary.editStructure = async (req, res) => {
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
// TODO I18n - nameSl
const language = 'name_sl'
const { dictionaryId } = req.params
const [dictionary, associatedLanguages, allLanguages] = await Promise.all([
Dictionary.fetchEditStructure(dictionaryId),
Dictionary.fetchLanguages(dictionaryId),
Dictionary.fetchAllLanguages(language)
Dictionary.fetchAllLanguages(language, true)
])
let viewPath
@@ -190,7 +194,7 @@ dictionary.editStructure = async (req, res) => {
}
res.render(viewPath, {
title: 'Struktura slovarskega sestavka',
title: req.t('Struktura slovarskega sestavka'),
dictionary,
associatedLanguages,
allLanguages
@@ -223,7 +227,7 @@ dictionary.editAdvanced = async (req, res) => {
}
res.render(viewPath, {
title: 'Napredno',
title: req.t('Napredno'),
dictionary: { id: req.params.dictionaryId },
dictionaryName
})
@@ -251,7 +255,7 @@ dictionary.comments = async (req, res) => {
}
res.render(viewPath, {
title: 'Komentarji',
title: req.t('Komentarji'),
numberOfAllPages,
dictionary: { id: req.params.dictionaryId },
comments,
@@ -261,10 +265,12 @@ dictionary.comments = async (req, res) => {
dictionary.showImportFromFileForm = async (req, res) => {
const { dictionaryId } = req.params
const [imports, dictionaryName] = await Promise.all([
Dictionary.fetchAllImports(dictionaryId),
Dictionary.fetchName(dictionaryId)
])
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
await Promise.all([
Dictionary.fetchAllImports(dictionaryId, resultsPerPage, 1),
Dictionary.fetchName(dictionaryId)
])
let viewPath
switch (req.baseUrl) {
case '/slovarji':
@@ -275,9 +281,10 @@ dictionary.showImportFromFileForm = async (req, res) => {
}
res.render(viewPath, {
title: 'Uvoz iz datoteke',
title: req.t('Uvoz iz datoteke'),
dictionary: { id: dictionaryId },
imports,
numberOfAllPages,
results,
dictionaryName
})
}
@@ -289,7 +296,7 @@ dictionary.listAdminDictionaries = async (req, res) => {
await Dictionary.fetchAllAdminDictionaries(resultsPerPage, 1)
res.render('pages/admin/dictionaries-list', {
title: 'Struktura slovarjev',
title: req.t('Seznam slovarjev'),
numberOfAllPages,
results
})
@@ -312,7 +319,7 @@ dictionary.adminEditDescription = async (req, res) => {
])
res.render('pages/admin/dictionary-description', {
title: 'Podatki',
title: req.t('Osnovni podatki'),
allPrimaryDomains,
allSecondaryDomains,
dictionary,
@@ -364,11 +371,11 @@ dictionary.showImportFromExtractionForm = async (req, res) => {
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/extraction-import'
title = 'Uvoz'
title = req.t('Uvoz iz luščilnika')
break
case '/admin':
viewPath = 'pages/admin/dictionary-extraction-import'
title = 'Uvoz luščenje'
title = req.t('Uvoz iz luščilnika')
}
res.render(viewPath, {
@@ -381,7 +388,12 @@ dictionary.showImportFromExtractionForm = async (req, res) => {
dictionary.showExportToFileForm = async (req, res) => {
const { dictionaryId } = req.params
const dictionaryName = await Dictionary.fetchName(dictionaryId)
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const [dictionaryName, { pages_total: numberOfAllPages, results }] =
await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchExports(dictionaryId, resultsPerPage, 1)
])
let viewPath
switch (req.baseUrl) {
case '/slovarji':
@@ -390,11 +402,12 @@ dictionary.showExportToFileForm = async (req, res) => {
case '/admin':
viewPath = 'pages/admin/dictionary-export'
}
res.render(viewPath, {
title: 'Izvoz',
dictionary: { id: req.params.dictionaryId },
dictionaryName
title: req.t('Izvoz'),
dictionary: { id: dictionaryId },
dictionaryName,
numberOfAllPages,
results
})
}
@@ -418,7 +431,7 @@ dictionary.editDomainLabels = async (req, res) => {
}
res.render(viewPath, {
title: 'Področne oznake',
title: req.t('Področne oznake'),
dictionary: { id: dictionaryId },
numberOfAllPages,
results,
@@ -448,7 +461,7 @@ dictionary.showContent = async (req, res) => {
const terms = prepareEditorEntries(hits)
res.render('pages/dictionaries/content', {
title: 'Vsebina slovarja',
title: req.t('Vsebina slovarja'),
terms,
canPublishEntriesInEdit,
dictionaryName,
@@ -466,7 +479,7 @@ dictionary.showSecondaryDomains = async (req, res) => {
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, 1)
res.render('pages/admin/areas', {
title: 'Podpodročja',
title: req.t('Področne oznake'),
numberOfAllPages,
results
})
@@ -501,12 +514,27 @@ dictionary.dictionaryList = async (req, res) => {
)
const numberOfAllHits = parseInt(
(await Dictionary.fetchAllDictionariesCount()).count
(await Dictionary.fetchAllDictionariesPublishedCount()).count
)
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
/*
/// filter selected domains for the prompt ///
// Not duplicates of this code arise...
const pdList = intoDbArray(req.query.pd, 'always')
allPrimaryDomains = allPrimaryDomains.map(entry => {
if (pdList.includes(`${entry.id}`)) {
entry.selected = true
}
return entry
})
/// //////////////////////////////////////////
*/
dictionaries = dictionaries.map(e => {
if (!e.portalcode) {
e.portalcode = defaultportalcode
@@ -518,7 +546,7 @@ dictionary.dictionaryList = async (req, res) => {
const isDictionaryListPage = true
res.render('pages/dictionaries/dictlist', {
title: 'Seznam slovarjev',
title: req.t('Seznam slovarjev'),
dictionaries,
allPrimaryDomains,
numberOfAllPages,
@@ -532,6 +560,7 @@ dictionary.dictionaryList = async (req, res) => {
dictionary.dictionaryDetails = async (req, res) => {
const { absolutePrevPath, sentFromEntryId } = req.query
const dictId = req.params.dictionaryId
const title = req.t('O slovarju')
const {
allPrimaryDomains,
sourceLanguages,
@@ -555,7 +584,7 @@ dictionary.dictionaryDetails = async (req, res) => {
// check if it is a local dictionary
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
dictionaryData[0].portalname = await getInstanceSetting('portal_name_sl')
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
}
@@ -579,7 +608,7 @@ dictionary.dictionaryDetails = async (req, res) => {
)
const structData = {
prevWindowTitle: 'Nazaj',
prevWindowTitle: req.t('Nazaj'),
dictName: dictionaryData[0].dictionarysl,
portalCode: dictionaryData[0].portalcode,
portalName: dictionaryData[0].portalname,
@@ -589,13 +618,14 @@ dictionary.dictionaryDetails = async (req, res) => {
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
}
// TODO I18n
if (reducedData.author) {
if (reducedData.author.length > 2) {
structData.authorLabel = 'Avtorji'
structData.authorLabel = req.t('Avtorji')
} else if (reducedData.author.length === 2) {
structData.authorLabel = 'Avtorja'
structData.authorLabel = req.t('Avtorja')
} else if (reducedData.author.length === 1) {
structData.authorLabel = 'Avtor'
structData.authorLabel = req.t('Avtor')
}
}
@@ -623,7 +653,8 @@ dictionary.dictionaryDetails = async (req, res) => {
finalData,
numberOfAllPages,
comments,
commentCount
commentCount,
title
}) // todo
}
@@ -680,8 +711,28 @@ dictionary.importFromFile = async (req, res) => {
}
}
dictionary.exportDownload = async (req, res) => {
// TODO Add authentication and authorization.
const { exportId } = req.params
const { exportStatus, dictionaryId, nameString, timeString, fileFormat } =
await Dictionary.fetchExportDownloadMetadata(exportId)
if (exportStatus !== 'finished') {
throw Error("Can't request file for unfinished export")
}
const exportFilesPath = getExportFilesPath(dictionaryId)
const exportFilePath = `${exportFilesPath}/${exportId}`
const exportFileName = `${nameString}_${timeString}.${fileFormat}`
res.download(exportFilePath, exportFileName)
}
function importFileFilter(req, file, cb) {
if (file.mimetype !== 'text/xml') return cb(Error('Invalid file type'))
if (file.mimetype !== 'text/xml') {
const customError = Error('Invalid file type')
customError.displayInProd = true
return cb(customError)
}
cb(null, true)
}
+10 -4
View File
@@ -29,7 +29,10 @@ extraction.list = async (req, res) => {
)
}
res.render('pages/extraction/list', { title: 'Luščenje seznam', extractions })
res.render('pages/extraction/list', {
title: req.t('Seznam luščenj'),
extractions
})
}
extraction.create = async (req, res) => {
@@ -39,7 +42,7 @@ extraction.create = async (req, res) => {
return res.redirect(303, 'back')
}
const extractionName = `Luščenje ${extractionCount + 1}`
const extractionName = req.t('Luščenje') + `${extractionCount + 1}`
const { extractionType } = req.body
let extractionId
@@ -81,7 +84,7 @@ extraction.edit = async (req, res) => {
extraction.keywords = intoDbArray(params.keywords, 'always')
res.render('pages/extraction/edit-oss', {
title: 'KAS + dokumenti',
title: req.t('Besedila'),
id: extractionId,
extraction,
allPrimaryDomains,
@@ -93,7 +96,7 @@ extraction.edit = async (req, res) => {
Extraction.fetchAllStopTermsFilesStats(extractionId)
])
res.render('pages/extraction/edit-own', {
title: 'Besedila',
title: req.t('Besedila'),
id: extractionId,
extraction,
extractionDocuments,
@@ -117,6 +120,7 @@ extraction.docsEdit = async (req, res) => {
)
res.render('pages/extraction/docs-edit', {
title: req.t('Besedila'),
id: extractionId,
extractionDocuments
})
@@ -129,6 +133,7 @@ extraction.stopTermsEdit = async (req, res) => {
)
res.render('pages/extraction/stop-terms-edit', {
title: req.t('Stop termini'),
id: extractionId,
stopTermsFiles
})
@@ -145,6 +150,7 @@ extraction.listTermCandidates = async (req, res) => {
const firstPageOfTermCandidates = termCandidates.slice(0, hitsPerPage)
res.render('pages/extraction/term-candidates', {
title: req.t('Terminološki kandidati'),
extractionId,
termCandidatesJson,
firstPageOfTermCandidates,
+13 -3
View File
@@ -1,3 +1,5 @@
/* global __ */
const { getInstanceSetting } = require('../../models/helpers')
const Dictionary = require('../../models/dictionary')
const cache = require('../../models/cache')
@@ -56,6 +58,7 @@ exports.minEntriesRequirementCheckAndAct = {
if (!isBelowMinEntriesThreshold || wasEmailAlreadySent) return
// Prepare and send notification emails.
// TODO I18n - nameSl
const [nameSl, adminEmails, dictionariesAdminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchAdminEmails(dictionaryId),
@@ -68,14 +71,16 @@ exports.minEntriesRequirementCheckAndAct = {
const type = 'delete'
const renderAsync = promisify(appRef.render.bind(appRef))
// TODO I18n - nameSl
const emailHtml = await renderAsync('email/dictionary-status-change', {
type,
nameSl
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: allEmails,
subject: 'Obvestilo o številu gesel',
subject: __('Obvestilo o številu gesel'),
html: emailHtml
})
@@ -118,6 +123,7 @@ exports.determineNewStatus = async isPublished => {
// Exports actions related to checking and acting on dictionary status changes.
exports.statusChangeCheckAndAct = {
// Notify dictionaries admins by email on dictionary status changes.
// TODO I18n - nameSl
async updateUsers(
dictionaryId,
isPublishedNew,
@@ -133,7 +139,7 @@ exports.statusChangeCheckAndAct = {
const dictionariesAdminEmails =
await Dictionary.fetchDictionariesAdminEmails()
const type = 'unpublish'
// TODO I18n - nameSl
await renderAndSendStatusChangeEmails(
appRef,
type,
@@ -151,6 +157,7 @@ exports.statusChangeCheckAndAct = {
const type =
isApprovalRequired === 'T' ? 'publish-approval' : 'publish-no-approval'
// TODO I18n - nameSl
await renderAndSendStatusChangeEmails(
appRef,
type,
@@ -171,6 +178,7 @@ exports.statusChangeCheckAndAct = {
user
) {
if (statusOld === 'reviewed' && statusNew !== 'reviewed') {
// TODO I18n - nameSl
const [nameSl, adminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchAdminEmails(dictionaryId)
@@ -189,6 +197,7 @@ exports.statusChangeCheckAndAct = {
}
// Helper function used by statusChangeCheckAndAct methods.
// TODO I18n - nameSl
async function renderAndSendStatusChangeEmails(
appRef,
type,
@@ -203,9 +212,10 @@ async function renderAndSendStatusChangeEmails(
nameSl
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: targetEmails,
subject: 'Sprememba stanja slovarja',
subject: __('Sprememba stanja slovarja'),
html: emailHtml
})
}
@@ -8,12 +8,15 @@ helper.initialize = async () => {
const initializers = {}
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
// TODO i18n name_sl
const language = 'name_sl'
// TODO Consider parallelizing following queries. Single vs pooled clients?
initializers.allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
initializers.sourceLanguages = await Dictionary.fetchAllLanguages(language)
initializers.targetLanguages = initializers.sourceLanguages.filter(
initializers.targetLanguages = (
await Dictionary.fetchAllLanguages(language)
).filter(
// drop slovene language
l => l.id !== 32
)
@@ -22,7 +25,7 @@ helper.initialize = async () => {
initializers.portals = []
initializers.portals.push({
name: await getInstanceSetting('portal_name'),
name: await getInstanceSetting('portal_name_sl'),
code: await getInstanceSetting('portal_code')
})
+211 -17
View File
@@ -2,12 +2,16 @@ const Entry = require('../models/entry')
const { getInstanceSetting } = require('../models/helpers')
const Dictionary = require('../models/dictionary')
const Comment = require('../models/comment')
const { searchEntryIndex } = require('../models/search-engine')
const {
searchEntryIndex,
searchConsultancyEntryIndex
} = require('../models/search-engine')
const { intoDbArray } = require('../models/helpers')
const {
prepareEntries,
prepareAggregation,
prepareSeachFilterData
prepareSeachFilterData,
prepareConsultancyEntries
} = require('../models/helpers/search')
const generateQuery = require('../models/helpers/search/generate-query')
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
@@ -16,6 +20,7 @@ const User = require('../models/user')
// TODO Luka (note to self): Measure performance, consider caching.
exports.index = async (req, res) => {
const { language } = req
const {
allPrimaryDomains,
sourceLanguages,
@@ -30,8 +35,10 @@ exports.index = async (req, res) => {
englishLanguageEnabled
)
const portalName = await getInstanceSetting('portal_name')
const portalDescription = await getInstanceSetting('portal_description')
const portalName = await getInstanceSetting(`portal_name_${language}`)
const portalDescription = await getInstanceSetting(
`portal_description_${language}`
)
const isRoot = true
@@ -53,7 +60,9 @@ exports.search = async (req, res) => {
if (!searchString) return res.redirect('/')
const {
const title = req.t('Iskanje')
let {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
@@ -81,6 +90,32 @@ exports.search = async (req, res) => {
true
)
// Consultancy
const hitsQueryConsultancy = generateQuery.consultancy(
searchString,
{
status: 'published',
primaryDomain: filters.primaryDomains[0]
},
hitsPerPage,
page
)
const hitsConsultancy = await searchConsultancyEntryIndex(
hitsQueryConsultancy
)
const consultancyHits = hitsConsultancy.body.hits.total.value
const consultancyURL = `/svetovanje/iskanje?q=${searchString}${
filters.primaryDomains.length > 0 ? '&pd=' + filters.primaryDomains[0] : '' // consultancy filtering only supports one domain
}`
// console.log(entries)
//
const [hits, aggregationRaw] = await Promise.all([
searchEntryIndex(hitsQuery),
searchEntryIndex(aggregateQuery)
@@ -143,6 +178,30 @@ exports.search = async (req, res) => {
sources: false
}
// Keep selected items on refresh
sourceLanguages = addSelectedIdentifierToFilters(
sourceLanguages,
filters.sourceLanguages
)
targetLanguages = addSelectedIdentifierToFilters(
targetLanguages,
filters.targetLanguages
)
allPrimaryDomains = addSelectedIdentifierToFilters(
allPrimaryDomains,
filters.primaryDomains
)
allDictionaryNames = addSelectedIdentifierToFilters(
allDictionaryNames,
filters.dictionaries
)
/// //////////////
portals = addSelectedIdentifierToPortals(portals, filters.sources)
if (count < 1) {
return res.render('pages/search/no-results', {
allPrimaryDomains,
@@ -154,6 +213,8 @@ exports.search = async (req, res) => {
entriesByCategory,
searchFilterData,
disabledSideMenuFilters,
consultancyHits,
consultancyURL,
// allAggregation,
numberOfAllHits,
numberOfAllPages,
@@ -161,6 +222,74 @@ exports.search = async (req, res) => {
})
}
// duplicated code below for filtering foreignentries, optimize later
// Below code filters target languages based on source and target filters for each category
const categoriesLabels = Object.keys(entriesByCategory)
for (
let categoryIndex = 0;
categoryIndex < categoriesLabels.length;
categoryIndex++
) {
entriesByCategory[categoriesLabels[categoryIndex]] = entriesByCategory[
categoriesLabels[categoryIndex]
].map(entry => {
entry.foreignEntries = entry.foreignEntries?.filter(foreignEntry => {
if (filters.targetLanguages.length > 0) {
if (aggregation.sourceLanguages.length === 1) {
filters.sourceLanguages = [aggregation.sourceLanguages[0].id]
}
return (
filters.targetLanguages.includes(`${foreignEntry.lang.id}`) ||
filters.sourceLanguages?.includes(`${foreignEntry.lang.id}`)
)
} else {
return true
}
})
return entry
})
}
// Disable target languages logic
if (
filters.sourceLanguages.length > 1 || // more or equal than 2 source languages
(filters.sourceLanguages.length < 1 && // No filters, but more than 1 source language filters returned
aggregation.sourceLanguages.length > 1) ||
(aggregation.targetLanguages.length === 1 &&
aggregation.sourceLanguages.length === 1 &&
aggregation.targetLanguages[0].id === aggregation.sourceLanguages[0].id) //
) {
// predicate 1 Disable if 2 or more filters in Source languages are selected
// predicate 2 If none are selected, then check if displayed filters are more than 2
aggregation.targetLanguages = []
filters.targetLanguages = []
targetLanguages = []
searchFilterData.targetLanguages = []
disabledSideMenuFilters.targetLanguages = true
}
// remove source language from target language
filters.sourceLanguages.forEach(entry => {
if (
aggregation.targetLanguages.filter(toFilter => toFilter.id === entry)
.length > 0
) {
aggregation.targetLanguages = aggregation.targetLanguages.filter(
toFilter => toFilter.id !== entry
)
searchFilterData.targetLanguages =
searchFilterData.targetLanguages.filter(
toFilter => toFilter.id !== entry
)
}
})
if (aggregation.targetLanguages.length < 0) {
searchFilterData.targetLanguages = []
disabledSideMenuFilters.targetLanguages = true
}
// TODO Add a page title?
res.render('pages/search/results', {
allPrimaryDomains,
@@ -172,15 +301,19 @@ exports.search = async (req, res) => {
entriesByCategory,
searchFilterData,
disabledSideMenuFilters,
consultancyHits,
consultancyURL,
// allAggregation,
numberOfAllHits,
numberOfAllPages,
page
page,
title
})
}
exports.entryDetails = async (req, res) => {
const termId = req.params.entryId
const title = req.t('Termin')
/* const [entry, domainLabels] = await Promise.all([
Entry.fetchFullWithOrderedForeignLanguages(termId),
@@ -188,12 +321,16 @@ exports.entryDetails = async (req, res) => {
]) */
const entry = await Entry.fetchFullWithOrderedForeignLanguages(termId)
/* const entryData = {
entry
// allDomainLabelsJoined: domainLabels.map(e => e.name).join(', ')
} */
// If external url, just redirect
if (entry.external_url) {
return res.redirect(entry.external_url)
}
// unnecessary legacy assigment, refactor when time is available
const entryData = entry
@@ -228,8 +365,8 @@ exports.entryDetails = async (req, res) => {
///
// check if it is a local dictionary
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
if (!dictionaryData.portalnamesl && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name_sl')
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
}
@@ -258,7 +395,7 @@ exports.entryDetails = async (req, res) => {
// struct data contains important data and re-maps for unification (maybe refactor later)
const structData = {
termId: termId,
prevWindowTitle: 'Iskanje',
prevWindowTitle: req.t('Iskanje'),
prevHref: '/iskanje',
portalCode: dictionaryData[0].portalcode,
portalName: dictionaryData[0].portalname,
@@ -270,13 +407,14 @@ exports.entryDetails = async (req, res) => {
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
}
// TODO I18n
if (reducedData.author) {
if (reducedData.author.length > 2) {
structData.authorLabel = 'Avtorji'
structData.authorLabel = req.t('Avtorji')
} else if (reducedData.author.length === 2) {
structData.authorLabel = 'Avtorja'
structData.authorLabel = req.t('Avtorja')
} else if (reducedData.author.length === 1) {
structData.authorLabel = 'Avtor'
structData.authorLabel = req.t('Avtor')
}
}
@@ -321,27 +459,63 @@ exports.entryDetails = async (req, res) => {
selectedDomainLabelsForEntryString,
numberOfAllPages,
comments,
commentCount
commentCount,
title
})
}
exports.myProfile = async (req, res) => {
res.render('pages/profile/my-profile', { title: 'Moj račun' })
res.render('pages/profile/my-profile', { title: req.t('Osnovni podatki') })
}
exports.deleteProfile = async (req, res) => {
res.render('pages/profile/delete-profile', { title: req.t('Izbriši račun') })
}
exports.changePassword = async (req, res) => {
res.render('pages/profile/change-password', { title: 'Spremeni geslo' })
res.render('pages/profile/change-password', {
title: req.t('Spremeni geslo')
})
}
exports.resetPassword = async (req, res) => {
const { token } = req.query
// console.log(token)
res.render('pages/reset-password/reset-password', {
// title: 'Pozabljeno geslo'
isValidToken: token === '123',
token
})
}
exports.userSettings = async (req, res) => {
const hitsPerPageArr = await User.fetchAllowedHitsPerPage()
res.render('pages/profile/change-profile-settings', {
title: 'Nastavitve računa',
title: req.t('Nastavitve računa'),
hitsPerPageArr,
hitsForUser: req.user?.hitsPerPage
})
}
exports.changeUserLanguage = async (req, res) => {
const { languageCode } = req.params
const validCodes = ['sl', 'en']
if (!validCodes.includes(languageCode)) {
throw Error(`Invalid language: ${languageCode}`)
}
const { user } = req
if (user) {
await User.updateLanguage(user.id, languageCode)
} else {
req.session.language = languageCode
}
res.redirect('/')
}
function mergeDomains(
domainList,
aggregationFn = (acc, n) => {
@@ -384,3 +558,23 @@ function filterResults(entries) {
return [termLst, ftermLst, otherLst]
}
function addSelectedIdentifierToFilters(source, selectedIDs) {
return source.map(entry => {
if (selectedIDs.includes(`${entry.id}`)) {
entry.selected = true
}
return entry
})
}
function addSelectedIdentifierToPortals(source, selectedIDs) {
return source.map(entry => {
if (selectedIDs.includes(`${entry.code}`)) {
entry.selected = true
}
return entry
})
}
+12 -9
View File
@@ -8,7 +8,7 @@ portal.instanceSettings = async (req, res) => {
const portal = await Portal.fetchInstanceSettings()
res.render('pages/admin/portal', {
title: 'Nastavitve portala',
title: req.t('Nastavitve portala'),
portal
})
}
@@ -24,7 +24,7 @@ portal.instanceDictSettings = async (req, res) => {
const dictionary = await Portal.fetchInstanceDictSettings()
res.render('pages/admin/settings-dictionaries', {
title: 'Nastavitve slovarjev',
title: req.t('Nastavitve slovarjev'),
dictionary
})
}
@@ -39,7 +39,7 @@ portal.updateInstanceDictSettings = async (req, res) => {
portal.instanceConsultancySettings = async (req, res) => {
const consultancy = await Portal.fetchInstanceConsultancySettings()
res.render('pages/admin/portal-consultancy-settings', {
title: 'Nastavitve svetovalnice',
title: req.t('Nastavitve svetovalnice'),
consultancy
})
}
@@ -53,7 +53,7 @@ portal.updateInstanceConusltacySettings = async (req, res) => {
portal.new = async (req, res) => {
res.render('pages/admin/new-connection', {
title: 'Nova povezava'
title: req.t('Nova povezava')
})
}
@@ -61,7 +61,7 @@ portal.list = async (req, res) => {
const allLinkedPortals = await Portal.fetchAll()
res.render('pages/admin/connections-list', {
title: 'Seznam povezav',
title: req.t('Seznam povezav'),
allLinkedPortals
})
}
@@ -70,7 +70,10 @@ portal.fetchPortal = async (req, res) => {
const portalId = req.params.portalId
const portal = await Portal.fetchPortal(portalId)
res.render('pages/admin/portal-edit', { title: 'Uredi povezavo', portal })
res.render('pages/admin/portal-edit', {
title: req.t('Uredi povezavo'),
portal
})
}
portal.updatePortal = async (req, res) => {
@@ -88,7 +91,7 @@ portal.fetchSelectedLinkedDictionaries = async (req, res) => {
await Portal.fetchSelectedLinkedDictionaries(linkedId, resultsPerPage, 1)
res.render('pages/admin/portal-list-dict', {
title: 'Slovarji portala',
title: req.t('Slovarji portala'),
linkedId,
numberOfAllPages,
results
@@ -108,7 +111,7 @@ portal.fetchAllLinkedDictionaries = async (req, res) => {
await Portal.fetchAllLinkedDictionaries(resultsPerPage, 1)
res.render('pages/admin/portals-all-linked-dictionaries', {
title: 'Povezani',
title: req.t('Povezani slovarji'),
numberOfAllPages,
results
})
@@ -130,7 +133,7 @@ portal.comments = async (req, res) => {
1
)
res.render('pages/admin/comments', {
title: 'Komentarji',
title: req.t('Komentarji'),
numberOfAllPages,
dictionary: { id: req.params.dictionaryId },
comments
+29 -9
View File
@@ -12,7 +12,10 @@ const user = {}
user.register = async (req, res) => {
// TODO Add validation.
const userId = await User.create(req.body)
const userId = await User.create({
...req.body,
language: req.session.language
})
const activationToken = (await RandomBytesAsync(32)).toString('hex')
await User.saveActivationToken(userId, activationToken)
const { email: userEmail, username } = req.body
@@ -20,16 +23,16 @@ user.register = async (req, res) => {
activationLink.searchParams.set('token', activationToken)
activationLink = activationLink.href
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/user-activation', {
const emailHtml = await renderAsync(`email/user-activation_${req.language}`, {
username,
activationLink
})
await email.send({
to: userEmail,
subject: 'Aktivacija računa',
subject: req.t('Aktivacija računa'),
html: emailHtml
})
res.send('Registracija uspešna')
res.send(req.t('Registracija uspešna'))
}
user.activateAccount = async (req, res) => {
@@ -39,6 +42,12 @@ user.activateAccount = async (req, res) => {
await User.activateAccount(user)
const loginAsync = promisify(req.login.bind(req))
await loginAsync(user)
if (req.session.language) {
await User.updateLanguage(user.id, req.session.language)
delete req.session.language
}
res.redirect('/')
}
@@ -46,8 +55,9 @@ user.login = async (req, res, next) => {
passport.authenticate(
'local',
{
badRequestMessage:
badRequestMessage: req.t(
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
)
},
async (err, user, info) => {
if (err) return next(err)
@@ -69,7 +79,12 @@ user.login = async (req, res, next) => {
res.cookie('remember_me', rememberMeToken, rememberMeCookieSettings)
}
res.send('Prijava uspešna')
if (req.session.language) {
await User.updateLanguage(user.id, req.session.language)
delete req.session.language
}
res.send(req.t('Prijava uspešna'))
}
)(req, res, next)
}
@@ -82,6 +97,8 @@ user.logout = async (req, res) => {
await User.clearRememberMeToken(rememberMeToken)
}
req.session.language = req.user.language
req.logout()
// Manually clear session.passport due to bug in current passport version.
delete req.session.passport.user
@@ -96,7 +113,7 @@ user.list = async (req, res) => {
1
)
res.render('pages/admin/user-list', {
title: 'Seznam slovarjeva',
title: req.t('Seznam uporabnikov'),
numberOfAllPages,
results
})
@@ -104,7 +121,10 @@ user.list = async (req, res) => {
user.listAllWithPortalRoles = async (req, res) => {
const users = await User.fetchAllWithPortalRoles()
res.render('pages/admin/user-portals', { title: 'Seznam slovarjev', users })
res.render('pages/admin/user-portals', {
title: req.t('Skrbniki portala'),
users
})
}
user.findByUsernameOrEmail = async (req, res) => {
@@ -125,7 +145,7 @@ user.adminEdit = async (req, res) => {
User.fetchUserRoles(userId)
])
res.render('pages/admin/user-edit', {
title: 'Urejanje uporabnikov',
title: req.t('Uporabnik'),
userData,
userRoles
})
+10 -5
View File
@@ -29,8 +29,8 @@ passport.deserializeUser(async (id, done) => {
passport.use(
new LocalStrategy(
{ usernameField: 'usernameOrEmail' },
async (usernameOrEmail, password, done) => {
{ passReqToCallback: true, usernameField: 'usernameOrEmail' },
async (req, usernameOrEmail, password, done) => {
try {
const { rows } = await db.query(
'SELECT id, status, bcrypt_hash FROM "user" WHERE username = $1 OR email = $1',
@@ -40,14 +40,17 @@ passport.use(
if (!user) {
return done(null, false, {
message: 'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
message: req.t(
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
)
})
}
if (user.status !== 'active') {
return done(null, false, {
message:
message: req.t(
'Uporabniški račun še ni aktiviran. Kliknite aktivacijsko povezavo, katero smo vam poslali po elektronski pošti.'
)
})
}
@@ -57,7 +60,9 @@ passport.use(
)
if (!isCorrectPassword) {
return done(null, false, {
message: 'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
message: req.t(
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
)
})
}
+47
View File
@@ -0,0 +1,47 @@
const i18next = require('i18next')
const i18nextMiddleware = require('i18next-http-middleware')
const i18nextBackend = require('i18next-fs-backend')
// If you change this one, don't forget to also update the nodemon ignore flag in package.json scripts.
const LOCALES_PATH = 'public/locales'
const DEFAULT_LANGUAGE = 'sl'
exports.determineRequestLanguage = (req, res, next) => {
const language =
req.user?.language || req.session.language || DEFAULT_LANGUAGE
req.determinedLanguage = language
res.locals.determinedLanguage = language
next()
}
const customLanguageDetector = {
name: 'ownDetector',
lookup(req) {
return req.determinedLanguage
}
}
const languageDetector = new i18nextMiddleware.LanguageDetector()
languageDetector.addDetector(customLanguageDetector)
const inDevEnv = process.env.NODE_ENV === 'development'
i18next
.use(languageDetector)
.use(i18nextBackend)
.init({
// debug: true,
supportedLngs: ['sl', 'en', 'dev'],
preload: ['sl', 'en'],
ns: ['core', 'extended'],
defaultNS: 'core',
// TODO Reenable separators once keys are refactored (also in frontend init in scripts.js)
nsSeparator: false,
keySeparator: false,
...(inDevEnv && { saveMissing: true }),
...(!inDevEnv && { fallbackLng: false }),
detection: { order: ['ownDetector'] },
backend: {
loadPath: `${LOCALES_PATH}/{{lng}}/{{ns}}.json`,
addPath: `${LOCALES_PATH}/{{lng}}/{{ns}}.json`
}
})
+1
View File
@@ -2,5 +2,6 @@ const { getInstanceSetting } = require('../models/helpers')
exports.enhanceLocals = async (req, res, next) => {
res.locals.portalCode = await getInstanceSetting('portal_code')
next()
}
+20 -2
View File
@@ -14,6 +14,13 @@ user.enhance = (req, res, next) => {
next()
}
user.isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) return next()
if (req.isAjax) return res.status(400).end()
res.redirect(req.baseUrl || '/')
}
user.isDictionaryAdmin = (req, res, next) => {
const { dictionaryId } = req.params
const isAdmin = req.user.hasDictionaryRole(dictionaryId, 'administration')
@@ -21,7 +28,7 @@ user.isDictionaryAdmin = (req, res, next) => {
if (isAdmin) return next()
if (req.isAjax) return res.status(400).end()
res.redirect(req.baseUrl)
res.redirect(req.baseUrl || '/')
}
user.isDictionaryEditor = (req, res, next) => {
@@ -31,7 +38,18 @@ user.isDictionaryEditor = (req, res, next) => {
if (isEditor) return next()
if (req.isAjax) return res.status(400).end()
res.redirect(req.baseUrl)
res.redirect(req.baseUrl || '/')
}
user.canContentEdit = (req, res, next) => {
const { dictionaryId } = req.params
const isEditor = req.user.hasAnyDictionaryRole(dictionaryId)
const isPortalAdmin = req.user.hasRole('portal admin')
const isDictionariesAdmin = req.user.hasRole('dictionaries admin')
if (isEditor || isPortalAdmin || isDictionariesAdmin) return next()
if (req.isAjax) return res.status(400).end()
res.redirect(req.baseUrl || '/')
}
/**
+13 -12
View File
@@ -1,6 +1,10 @@
const db = require('./db')
const debug = require('debug')('termPortal:models/comment')
const User = require('../models/user')
const {
portalAdminInitialEmail,
portalAdminInitialPassword
} = require('../config/keys')
class Comment {
// Deserialize flat data into an organized comment object.
@@ -311,24 +315,21 @@ async function seedMockUsersInDb() {
}
async function seedPortalAdmin() {
const MOCK_ADMIN_BASE = 'admin'
const {
rows: [mockAdmin]
} = await db.query('SELECT id FROM "user" WHERE username = $1', [
MOCK_ADMIN_BASE
])
rows: [{ exists }]
} = await db.query(
"SELECT EXISTS (SELECT 1 FROM user_role WHERE role_name = 'portal admin')"
)
if (mockAdmin) {
return `Portal admin already exists (username: ${MOCK_ADMIN_BASE}, password: ${MOCK_ADMIN_BASE})`
}
if (exists) return 'Skipping creation of portal admin (already exists)'
const MOCK_ADMIN_BASE = 'admin'
const adminUser = {
username: MOCK_ADMIN_BASE,
firstName: MOCK_ADMIN_BASE,
lastName: MOCK_ADMIN_BASE,
password: MOCK_ADMIN_BASE,
email: `${MOCK_ADMIN_BASE}@rsdo.com`
password: portalAdminInitialPassword,
email: portalAdminInitialEmail
}
const userId = await User.create(adminUser)
const assignAdminRole = db.query(
@@ -345,7 +346,7 @@ async function seedPortalAdmin() {
[adminUser.username]
)
await Promise.all([assignAdminRole, activateAdminUser])
return `Successfully seeded portal admin (username: ${MOCK_ADMIN_BASE}, password: ${MOCK_ADMIN_BASE})`
return `Successfully created portal admin (username: ${adminUser.username}, password: ${adminUser.password})`
}
async function seedConsultants() {
+2
View File
@@ -70,6 +70,7 @@ class ConsultancyEntry {
// Fetch consultancy entry by ID
// to_char(time_created,'HH24:MI:SS DD/MM/YYYY')
// TODO i18n date format
static async fetchByIdWithFormattedTime(id) {
const { rows: fetchedConsEntry } = await db.query(
`
@@ -679,6 +680,7 @@ class ConsultancyEntry {
}
// (Re)index specific consultancy entry into consultancy search index.
// TODO i18n name_sl
static async indexIntoSearchEngine(entryId, shouldWait) {
const values = [entryId]
const text = `
+675 -40
View File
@@ -1,8 +1,15 @@
const { mkdir, open, unlink } = require('fs/promises')
const Cursor = require('pg-cursor')
const db = require('./db')
const { intoDbArray, getInstanceSetting } = require('./helpers')
const { deserialize, bulkIndex } = require('./helpers/dictionary')
const {
deserialize,
bulkIndex,
getExportFilesPath
} = require('./helpers/dictionary')
const { readFileIntoDb } = require('./helpers/dictionary/import-file')
const { transformAndAppend } = require('./helpers/dictionary/export-file')
const { origin } = require('../config/keys')
// const debug = require('debug')('termPortal:models/dictionary')
class Dictionary {
@@ -25,6 +32,7 @@ class Dictionary {
}
// Fetch all dictionaries from DB.
// TODO i18n name_sl
static async fetchAll() {
// TODO Implement SQL stored procedures or functions.
const { rows: fetchedDictionaries } = await db.query(`
@@ -65,6 +73,7 @@ class Dictionary {
}
*/
// TODO i18n name_sl
// Fetch all dictionaries from DB for which the user has at least one dictionary role.
static async fetchAllByUser(userId) {
// TODO Implement SQL stored procedures or functions.
@@ -75,7 +84,12 @@ class Dictionary {
time_modified,
status,
count_entries,
count_comments
count_comments,
(
SELECT administration
FROM user_role
WHERE user_id = $1 and dictionary_id = id
) as is_admin
FROM dictionary
WHERE id IN (
SELECT dictionary_id
@@ -86,8 +100,8 @@ class Dictionary {
`
const values = [userId]
const { rows: fetchedDictionaries } = await db.query(text, values)
const deserializedDictionaries = fetchedDictionaries.map(
dictionary => new this(dictionary)
const deserializedDictionaries = fetchedDictionaries.map(dictionary =>
deserialize.dictionary(dictionary)
)
return deserializedDictionaries
}
@@ -138,13 +152,13 @@ class Dictionary {
}
// Fetch all languages from DB.
static async fetchAllLanguages(lang) {
static async fetchAllLanguages(lang, excludeSlovene) {
// TODO Implement SQL stored procedures or functions.
const { rows: fetchedLanguages } = await db.query(`
SELECT
id,
${lang}
FROM language
FROM language${excludeSlovene ? "\nWHERE code <> 'sl'" : ''}
ORDER BY ${lang}`)
const deserializedLanguages = fetchedLanguages.map(language =>
deserialize.language(language)
@@ -413,15 +427,15 @@ class Dictionary {
description,
l.name_sl languageSl,
ds.name_sl domainSecondarySl,
lp.name portalname,
lp.name portalnamesl,
lp.code portalcode
FROM
dictionary d
INNER JOIN dictionary_language dl ON dl.dictionary_id = d.id
INNER JOIN language l ON dl.language_id = l.id
LEFT JOIN dictionary_language dl ON dl.dictionary_id = d.id
LEFT JOIN language l ON dl.language_id = l.id
LEFT JOIN dictionary_domain_secondary dds on dds.dictionary_id = d.id
LEFT JOIN domain_secondary ds ON dds.domain_secondary_id = ds.id
INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id
LEFT JOIN domain_primary dp ON d.domain_primary_id = dp.id
LEFT JOIN linked_dictionary ld ON ld.target_dictionary_id = d.id
LEFT JOIN linked_portal lp ON ld.linked_portal_id = lp.id
WHERE d.id = $1`
@@ -447,7 +461,7 @@ class Dictionary {
d.author,
dp.name_sl domain_primary,
description,
lp.name portalname,
lp.name portalnamesl,
lp.code portalcode
FROM
dictionary d
@@ -509,7 +523,7 @@ class Dictionary {
d.author,
dp.name_sl domain_primary,
description,
lp.name portalname,
lp.name portalnamesl,
lp.code portalcode
FROM
dictionary d
@@ -577,12 +591,13 @@ class Dictionary {
distinct (d.id),
d.${lang} dictionarysl,
d.count_entries,
d.count_comments,
to_char(d.time_modified,'YYYY-MM-DD') time_modified,
d.issn,
d.author,
dp.name_sl domain_primary,
description,
lp.name portalname,
lp.name portalnamesl,
lp.code portalcode
FROM
dictionary d
@@ -590,7 +605,7 @@ class Dictionary {
INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id
LEFT JOIN linked_dictionary ld ON ld.target_dictionary_id = d.id
LEFT JOIN linked_portal lp ON ld.linked_portal_id = lp.id
WHERE d.name_sl LIKE '%' || $1 || '%' ${queryAppend}
WHERE d.status = 'published' AND LOWER(d.name_sl) LIKE '%' || LOWER($1) || '%' ${queryAppend}
ORDER BY ${orderBy}
LIMIT $2
OFFSET $3`
@@ -643,7 +658,7 @@ class Dictionary {
INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id
LEFT JOIN linked_dictionary ld ON ld.target_dictionary_id = d.id
LEFT JOIN linked_portal lp ON ld.linked_portal_id = lp.id
WHERE d.name_sl LIKE '%' || $1 || '%' ${queryAppend}`
WHERE d.status = 'published' AND d.name_sl LIKE '%' || $1 || '%' ${queryAppend}`
const { rows } = await db.query(text, [searchQuery])
@@ -656,8 +671,8 @@ class Dictionary {
count(d.id)
FROM
dictionary d
INNER JOIN dictionary_language dl ON dl.dictionary_id = d.id
INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id
LEFT JOIN dictionary_language dl ON dl.dictionary_id = d.id
LEFT JOIN domain_primary dp ON d.domain_primary_id = dp.id
LEFT JOIN linked_dictionary ld ON ld.target_dictionary_id = d.id
LEFT JOIN linked_portal lp ON ld.linked_portal_id = lp.id`
@@ -666,6 +681,23 @@ class Dictionary {
return rows[0]
}
static async fetchAllDictionariesPublishedCount() {
const text = `
SELECT
count(d.id)
FROM
dictionary d
LEFT JOIN dictionary_language dl ON dl.dictionary_id = d.id
LEFT JOIN domain_primary dp ON d.domain_primary_id = dp.id
LEFT JOIN linked_dictionary ld ON ld.target_dictionary_id = d.id
LEFT JOIN linked_portal lp ON ld.linked_portal_id = lp.id
WHERE d.status = 'published'`
const { rows } = await db.query(text)
return rows[0]
}
// Fetch single dictionary data for editing structure from DB.
static async fetchDictionaryWithEditStructure(dictionaryId) {
const text = `
@@ -734,10 +766,11 @@ class Dictionary {
SELECT
d.id,
${lang} dictionarysl,
dp.name_sl domain_primary
dp.name_sl domain_primary,
d.count_comments
FROM dictionary d
INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id
where d.time_published is not NULL
where d.time_published is not NULL and d.status = 'published'
ORDER BY d.time_published desc
limit 3`
@@ -984,15 +1017,62 @@ class Dictionary {
}
}
// Import entries from extraction into DB.
static async importFromExtraction(dictionaryId, userId, termCandidates) {
await db.transaction(async dbClient => {
// TODO Depending on performance, consider batching requests.
for (const {
kanonicnaoblika: term,
definicija: other
} of termCandidates) {
const values = [
dictionaryId,
false,
'suggestion',
term || null,
userId,
null,
null,
null,
null,
[],
null,
null,
null,
[],
other || null,
[],
null,
null,
null
]
const text = `SELECT entry_new (${db.genParamStr(values)})`
await dbClient.query(text, values)
}
await this.updateMetadataAfterModifyingEntries(dictionaryId, dbClient)
})
}
// Index entries of specific dictionary from DB into search engine.
static async indexIntoSearchEngine(dictionaryId) {
// TODO Luka: I expect "source" needing a rework once linked portals and dictionaries start working.
const source = {
code: await getInstanceSetting('portal_code'),
name: await getInstanceSetting('portal_name')
}
const dbClient = await db.getClient()
try {
const {
rows: [{ code: linkedPortalCode, name: linkedPortalName } = {}]
} = await dbClient.query(
'SELECT p.code, p.name FROM linked_dictionary d LEFT JOIN linked_portal p ON p.id = d.linked_portal_id WHERE d.id = $1',
[dictionaryId]
)
// TODO i18n Luka: index portal name for both languages?
const source = {
code: linkedPortalCode ?? (await getInstanceSetting('portal_code')),
name: linkedPortalName ?? (await getInstanceSetting('portal_name_sl'))
}
const queryValues = [dictionaryId]
const dictionaryQueryText = `
@@ -1060,7 +1140,7 @@ class Dictionary {
)
)
FROM entry_foreign ef
LEFT JOIN LANGUAGE l ON l.id = ef.language_id
LEFT JOIN language l ON l.id = ef.language_id
WHERE entry_id = e.id
) foreign_entries
FROM entry e
@@ -1154,6 +1234,41 @@ class Dictionary {
return result
}
static async fetchFilteredPaginationDomainLabels(
dictionaryId,
query,
resultsPerPage,
page
) {
const {
rows: [{ result }]
} = await db.query(
`
SELECT jsonb_build_object(
'pages_total', (
SELECT CEIL(COUNT(*) / $3::float)
FROM domain_label
WHERE dictionary_id = $1 and name LIKE '%' || $2 || '%'
),
'results', ARRAY(
SELECT jsonb_build_object(
'id', id,
'name', name,
'isVisible', is_visible
)
FROM domain_label
WHERE dictionary_id = $1 and LOWER(name) LIKE '%' || LOWER($2) || '%'
ORDER BY name
LIMIT $3
OFFSET $4
)
) result`,
[dictionaryId, query, resultsPerPage, resultsPerPage * (page - 1)]
)
return result
}
// Fetch all secondary domains from DB.
static async fetchAllSecondaryDomains(resultsPerPage, page) {
const {
@@ -1184,6 +1299,38 @@ class Dictionary {
return result
}
// Fetch filtered secondary domains from DB.
static async fetchFilteredSecondaryDomains(query, resultsPerPage, page) {
const {
rows: [{ result }]
} = await db.query(
`
SELECT jsonb_build_object(
'pages_total', (
SELECT CEIL(COUNT(*) / $2::float)
FROM domain_secondary
WHERE name_sl LIKE '%' || $1 || '%'
),
'results', ARRAY(
SELECT jsonb_build_object(
'id', id,
'isApproved', approved,
'nameSl', name_sl,
'nameEn', name_en
)
FROM domain_secondary
WHERE LOWER(name_sl) LIKE '%' || LOWER($1) || '%'
ORDER BY name_sl
LIMIT $2
OFFSET $3
)
) result`,
[query, resultsPerPage, resultsPerPage * (page - 1)]
)
return result
}
static async updateDomainLabel(dictionaryId, data) {
const dbClient = await db.getClient()
try {
@@ -1343,24 +1490,54 @@ class Dictionary {
}
}
static async fetchAllImports(dictionaryId) {
const text = `
SELECT
time_started,
status,
delete_existing_entries,
file_format,
count_valid_entries
FROM import_file_job
WHERE dictionary_id = $1`
// static async fetchAllImports(dictionaryId) {
// const text = `
// SELECT
// time_started,
// status,
// delete_existing_entries,
// file_format,
// count_valid_entries
// FROM import_file_job
// WHERE dictionary_id = $1`
const value = [dictionaryId]
const { rows: fetchedImports } = await db.query(text, value)
// const value = [dictionaryId]
// const { rows: fetchedImports } = await db.query(text, value)
const deserializedImports = fetchedImports.map(oneImport =>
deserialize.imports(oneImport)
// const deserializedImports = fetchedImports.map(oneImport =>
// deserialize.imports(oneImport)
// )
// return deserializedImports
// }
static async fetchAllImports(dictionaryId, resultsPerPage, page) {
const {
rows: [{ result }]
} = await db.query(
`
SELECT jsonb_build_object(
'pages_total', (
SELECT CEIL(COUNT(*) / $2::float)
FROM import_file_job
WHERE dictionary_id = $1
),
'results', ARRAY(
SELECT jsonb_build_object(
'time_started', time_started,
'status', status,
'delete_existing_entries', delete_existing_entries,
'file_format', file_format,
'count_valid_entries', count_valid_entries
)
FROM import_file_job
WHERE dictionary_id = $1
LIMIT $2
OFFSET $3
)
) result`,
[dictionaryId, resultsPerPage, resultsPerPage * (page - 1)]
)
return deserializedImports
return result
}
static async delete(dictionaryId) {
@@ -1368,6 +1545,464 @@ class Dictionary {
const value = [dictionaryId]
await db.query(text, value)
}
// static async fetchExports(dictionaryId) {
// const text = `
// SELECT
// id,
// status,
// to_char(time_created, 'FMDD. FMMM. YYYY') date_created,
// entry_count,
// is_valid_filter,
// is_published_filter,
// is_terminology_reviewed_filter,
// is_language_reviewed_filter,
// status_filter,
// export_file_format
// FROM dictionary_export
// WHERE dictionary_id = $1
// ORDER BY id DESC`
// const value = [dictionaryId]
// const { rows: fetchedExports } = await db.query(text, value)
// const deserializedExports = fetchedExports.map(eachExport =>
// deserialize.exports(eachExport)
// )
// return deserializedExports
// }
static async fetchExports(dictionaryId, resultsPerPage, page) {
const {
rows: [{ result }]
} = await db.query(
`
SELECT jsonb_build_object(
'pages_total', (
SELECT CEIL(COUNT(*) / $2::float)
FROM dictionary_export
WHERE dictionary_id = $1
),
'results', ARRAY(
SELECT jsonb_build_object(
'id', id,
'status', status,
'time_created', time_created,
'entry_count', entry_count,
'is_valid_filter', is_valid_filter,
'is_published_filter', is_published_filter,
'is_terminology_reviewed_filter', is_terminology_reviewed_filter,
'is_language_reviewed_filter', is_language_reviewed_filter,
'status_filter', status_filter,
'export_file_format', export_file_format
)
FROM dictionary_export
WHERE dictionary_id = $1
LIMIT $2
OFFSET $3
)
) result`,
[dictionaryId, resultsPerPage, resultsPerPage * (page - 1)]
)
return result
}
static async beginExport(dictionaryId, exportParams) {
const text = `
INSERT INTO dictionary_export (
dictionary_id,
is_valid_filter,
is_published_filter,
is_terminology_reviewed_filter,
is_language_reviewed_filter,
status_filter,
export_file_format
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
`
const values = [
dictionaryId,
exportParams.isValidFilter,
exportParams.isPublishedFilter,
exportParams.isTerminologyReviewedFilter,
exportParams.isLanguageReviewedFilter,
exportParams.statusFilter,
exportParams.exportFileFormat
]
const {
rows: [{ id }]
} = await db.query(text, values)
return id
}
static async processExport(exportId) {
const dbClient = await db.getClient()
let exportFilePath
let exportFile
let cursor
try {
const exportQueryText = `
UPDATE dictionary_export
SET status = 'in progress', time_started = NOW()
WHERE id = $1
RETURNING
dictionary_id,
is_valid_filter,
is_published_filter,
is_terminology_reviewed_filter,
is_language_reviewed_filter,
status_filter,
export_file_format
`
const exportQueryParams = [exportId]
const {
rows: [
{
dictionary_id: dictionaryId,
is_valid_filter: isValidFilter,
is_published_filter: isPublishedFilter,
is_terminology_reviewed_filter: isTerminologyReviewedFilter,
is_language_reviewed_filter: isLanguageReviewedFilter,
status_filter: statusFilter,
export_file_format: exportFileFormat
}
]
} = await dbClient.query(exportQueryText, exportQueryParams)
const dictionaryQueryText = `
SELECT
entries_have_domain_labels,
entries_have_label,
entries_have_definition,
entries_have_synonyms,
entries_have_links,
entries_have_other,
entries_have_foreign_languages,
entries_have_foreign_definitions,
entries_have_foreign_synonyms,
entries_have_images,
entries_have_audio,
entries_have_videos
FROM dictionary
WHERE id = $1
`
const dictionaryQueryParams = [dictionaryId]
const {
rows: [dictionaryStructure]
} = await dbClient.query(dictionaryQueryText, dictionaryQueryParams)
const exportFields = {
domainLabels: dictionaryStructure.entries_have_domain_labels,
label: dictionaryStructure.entries_have_label,
definition: dictionaryStructure.entries_have_definition,
synonyms: dictionaryStructure.entries_have_synonyms,
links: dictionaryStructure.entries_have_links,
other: dictionaryStructure.entries_have_other,
foreignTerms: dictionaryStructure.entries_have_foreign_languages,
foreignDefinitions:
dictionaryStructure.entries_have_foreign_definitions,
foreignSynonyms: dictionaryStructure.entries_have_foreign_synonyms,
images: dictionaryStructure.entries_have_images,
audio: dictionaryStructure.entries_have_audio,
videos: dictionaryStructure.entries_have_videos
}
let entryQueryText = `
SELECT
term${
exportFields.label
? `,
label`
: ''
}${
exportFields.definition
? `,
definition`
: ''
}${
exportFields.synonyms
? `,
synonym synonyms`
: ''
}${
exportFields.other
? `,
other`
: ''
}${
exportFields.images
? `,
image`
: ''
}${
exportFields.audio
? `,
audio`
: ''
}${
exportFields.videos
? `,
video`
: ''
}${
exportFields.domainLabels
? `,
ARRAY(
SELECT name
FROM entry_domain_label edl
LEFT JOIN domain_label dl ON dl.id = edl.domain_label_id
WHERE entry_id = e.id
) domain_labels`
: ''
}${
exportFields.links
? `,
ARRAY(
SELECT jsonb_build_object(
'link', link,
'type', type)
FROM entry_link
WHERE entry_id = e.id
) links`
: ''
}${
exportFields.foreignTerms
? `,
ARRAY(
SELECT jsonb_build_object(
'lang_code', l.code,
'terms', ef.term${
exportFields.foreignDefinitions
? `,
'definition', ef.definition`
: ''
}${
exportFields.foreignSynonyms
? `,
'synonyms', ef.synonym`
: ''
}
)
FROM dictionary_language dl
LEFT JOIN language l ON l.id = dl.language_id
LEFT JOIN entry_foreign ef ON ef.language_id = l.id
WHERE dl.dictionary_id = $1 AND ef.entry_id = e.id
ORDER BY dl.selection_order
) foreign_entries`
: ''
}
FROM entry e
WHERE
e.dictionary_id = $1`
const entryQueryParams = [dictionaryId]
if (isValidFilter !== null) {
entryQueryParams.push(isValidFilter)
entryQueryText += ` AND is_valid = $${entryQueryParams.length}`
}
if (isPublishedFilter !== null) {
entryQueryParams.push(isPublishedFilter)
entryQueryText += ` AND is_published = $${entryQueryParams.length}`
}
if (isTerminologyReviewedFilter !== null) {
entryQueryParams.push(isTerminologyReviewedFilter)
entryQueryText += ` AND is_terminology_reviewed = $${entryQueryParams.length}`
}
if (isLanguageReviewedFilter !== null) {
entryQueryParams.push(isLanguageReviewedFilter)
entryQueryText += ` AND is_language_reviewed = $${entryQueryParams.length}`
}
if (statusFilter !== null) {
entryQueryParams.push(statusFilter)
entryQueryText += ` AND status = $${entryQueryParams.length}`
}
entryQueryText += '\nORDER BY term'
const exportFilesPath = getExportFilesPath(dictionaryId)
await mkdir(exportFilesPath, { recursive: true })
exportFilePath = `${exportFilesPath}/${exportId}`
exportFile = await open(exportFilePath, 'ax')
const isXmlFileFormat = exportFileFormat === 'xml'
const isDsvFileFormat = ['csv', 'tsv'].includes(exportFileFormat)
const isTbxFileFormat = exportFileFormat === 'tbx'
let dsvConfig
if (isXmlFileFormat) {
const openingMarkup =
'<?xml version="1.0" encoding="utf-8"?>\n<dictionary>\n'
await exportFile.write(openingMarkup)
} else if (isDsvFileFormat) {
if (exportFileFormat === 'csv') dsvConfig = { delimiter: ';' }
else if (exportFileFormat === 'tsv') dsvConfig = { delimiter: '\t' }
if (exportFields.foreignTerms) {
const { rows: languages } = await dbClient.query(
`
SELECT l.code
FROM dictionary d
LEFT JOIN dictionary_language dl ON dl.dictionary_id = d.id
LEFT JOIN language l ON l.id = dl.language_id
WHERE d.id = $1
ORDER BY dl.selection_order
`,
[dictionaryId]
)
dsvConfig.languageCodes = languages.map(language => language.code)
}
const fieldNamesArr = ['term']
if (exportFields.domainLabels) fieldNamesArr.push('domainLabels')
if (exportFields.label) fieldNamesArr.push('label')
if (exportFields.definition) fieldNamesArr.push('def')
if (exportFields.synonyms) fieldNamesArr.push('syns')
if (exportFields.links) fieldNamesArr.push('links')
if (exportFields.other) fieldNamesArr.push('other')
dsvConfig.languageCodes?.forEach(languageCode => {
fieldNamesArr.push(`[${languageCode}]fTerms`)
if (exportFields.foreignDefinitions) {
fieldNamesArr.push(`[${languageCode}]fDef`)
}
if (exportFields.foreignSynonyms) {
fieldNamesArr.push(`[${languageCode}]fSyns`)
}
})
if (exportFields.images) fieldNamesArr.push('images')
if (exportFields.audio) fieldNamesArr.push('audios')
if (exportFields.videos) fieldNamesArr.push('videos')
const headerLine = `${fieldNamesArr.join(dsvConfig.delimiter)}\n`
await exportFile.write(headerLine)
} else if (isTbxFileFormat) {
const {
rows: [tbxMetadata]
} = await dbClient.query(
`
SELECT
name_sl,
name_en,
author,
to_char(time_modified, 'YYYY-MM-DD') modified_date_string,
status,
(
SELECT to_char(time_created, 'YYYY-MM-DD')
FROM dictionary_export de
WHERE de.id = $1
) export_date_string
FROM dictionary
WHERE id = $2
`,
[exportId, dictionaryId]
)
const portalName = await getInstanceSetting('portal_name_sl')
const authorsString = tbxMetadata.author?.join(', ')
const urlPublished =
tbxMetadata.status === 'published'
? new URL(`/slovarji/${dictionaryId}/o-slovarju`, origin).href
: null
let openingMarkup = '<?xml version="1.0" encoding="utf-8"?>\n'
openingMarkup += '<!DOCTYPE martif SYSTEM "TBXcoreStructV02.dtd">\n'
openingMarkup += '<martif type="TBX" xml:lang="sl">\n<martifHeader>\n'
openingMarkup += '<fileDesc>\n<titleStmt>\n'
openingMarkup += `<title>${tbxMetadata.name_sl}</title>\n`
openingMarkup += `<note xml:lang="en">${tbxMetadata.name_en}</note>\n`
openingMarkup += '</titleStmt>\n<publicationStmt>\n'
openingMarkup += `<p>Datum objave: ${tbxMetadata.export_date_string}</p>\n`
openingMarkup +=
'<p>Avtorske pravice: Delo je dostopno pod pogoji licence CC BY 4.0.</p>\n'
openingMarkup += '</publicationStmt>\n<sourceDesc>\n'
openingMarkup += `<p>Vir: ${portalName}</p>\n`
if (authorsString) openingMarkup += `<p>Avtorji: ${authorsString}</p>\n`
openingMarkup += `<p>Datum objave: ${tbxMetadata.modified_date_string}</p>\n`
if (urlPublished) {
openingMarkup += `<p>Mesto objave: ${urlPublished}</p>\n`
}
openingMarkup += '</sourceDesc>\n</fileDesc>\n'
openingMarkup += '</martifHeader>\n<text>\n<body>\n'
await exportFile.write(openingMarkup)
}
cursor = dbClient.query(new Cursor(entryQueryText, entryQueryParams))
let entries = []
let batchEntriesCount
let entriesWritten = 0
do {
// Keep getting and writing entries to file in batches of 100.
entries = await cursor.read(100)
batchEntriesCount = entries.length
if (batchEntriesCount) {
await transformAndAppend(
entries,
exportFields,
exportFile,
exportFileFormat,
dsvConfig
)
entriesWritten += batchEntriesCount
}
} while (batchEntriesCount === 100)
if (isXmlFileFormat) {
const closingMarkup = '</dictionary>\n'
await exportFile.write(closingMarkup)
} else if (isTbxFileFormat) {
const closingMarkup = '</body>\n</text>\n</martif>\n'
await exportFile.write(closingMarkup)
}
await dbClient.query(
"UPDATE dictionary_export SET status = 'finished', time_finished = NOW(), entry_count = $1 WHERE id = $2",
[entriesWritten, exportId]
)
} catch (error) {
await cursor?.close()
await dbClient.query(
"UPDATE dictionary_export SET status = 'failed', time_finished = NOW() WHERE id = $1",
[exportId]
)
await unlink(exportFilePath)
throw error
} finally {
dbClient.release()
await exportFile?.close()
}
}
static async fetchExportDownloadMetadata(exportId) {
const {
rows: [fetchedMetadata]
} = await db.query(
`
SELECT
e.status,
e.dictionary_id,
d.name_sl_short name_string,
to_char(e.time_created, 'YYYYMMDDHH24MISS') time_string,
e.export_file_format
FROM dictionary_export e
LEFT JOIN dictionary d ON d.id = e.dictionary_id
WHERE e.id = $1`,
[exportId]
)
const deserializedMetadata =
deserialize.exportDownloadMetadata(fetchedMetadata)
return deserializedMetadata
}
}
module.exports = Dictionary
+15 -2
View File
@@ -3,15 +3,28 @@ const htmlToText = require('nodemailer-html-to-text').htmlToText()
const {
smtpHost,
smtpPort,
smtpTlsRejectUnauthorized,
smtpUser,
smtpPassword,
smtpSecure,
smtpRequireTls,
smtpAllowInvalidCerts,
smtpFrom
} = require('../config/keys')
const options = {
host: smtpHost,
port: smtpPort,
tls: { rejectUnauthorized: smtpTlsRejectUnauthorized }
secure: smtpSecure,
requireTLS: smtpRequireTls,
tls: { rejectUnauthorized: !smtpAllowInvalidCerts }
}
if (smtpUser || smtpPassword) {
options.auth = {
user: smtpUser,
pass: smtpPassword
}
}
const defaults = { from: smtpFrom }
const transporter = nodemailer.createTransport(options, defaults)
+20 -7
View File
@@ -184,7 +184,12 @@ Entry.fetchFull = async entryId => {
SELECT jsonb_strip_nulls(
jsonb_build_object(
'version', version,
'version_time', version_time
'version_time', version_time,
'version_author', (
SELECT username
FROM "user"
WHERE id = (version_snapshot['version_author'])::int
)
)
)
FROM entry_version_history
@@ -232,6 +237,7 @@ Entry.fetchFullWithOrderedForeignLanguages = async entryId => {
'audio', e.audio,
'video', e.video,
'time_modified', e.time_modified,
'external_url', e.external_url,
'domain_labels', ARRAY(
SELECT name
FROM entry_domain_label edl
@@ -421,6 +427,8 @@ Entry.deleteAllLinks = async dictionaryId => {
// (Re)index specific entry into entry search index.
Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
// TODO If this method is ever used for linked portals/dictionaries,
// TODO rework the source object below (already done in Dictionary.indexIntoSearchEngine).
const values = [entryId]
const text = `
SELECT
@@ -465,7 +473,7 @@ Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
)
)
FROM entry_foreign ef
LEFT JOIN LANGUAGE l ON l.id = ef.language_id
LEFT JOIN language l ON l.id = ef.language_id
WHERE entry_id = e.id
)
)
@@ -495,10 +503,11 @@ Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
const { dictionary, primary_domain: primaryDomain } = dataToIndex
let { entry } = dataToIndex
// TODO Luka: I expect "source" needing a rework once linked portals and dictionaries start working.
// TODO i18n Luka: index portal name for both languages?
const source = {
code: await getInstanceSetting('portal_code'),
name: await getInstanceSetting('portal_name')
name: await getInstanceSetting('portal_name_sl')
}
entry = prepareEntryForIndexing(entry)
@@ -618,13 +627,17 @@ Entry.update = async (userId, entry) => {
// Fetch a single version snapshot of a single entry from DB.
Entry.fetchVersionSnapshot = async (entryId, version) => {
const {
rows: [{ version_snapshot: historySnapshot }]
rows: [{ version_snapshot: historySnapshot, author }]
} = await db.query(
'SELECT version_snapshot FROM entry_version_history WHERE entry_id = $1 and version = $2',
`SELECT v.version_snapshot, (
SELECT u.username
FROM "user" u
WHERE u.id = (v.version_snapshot['version_author'])::int) as author
FROM entry_version_history v WHERE v.entry_id = $1 and v.version = $2`,
[entryId, version]
)
return historySnapshot
return { data: historySnapshot, author }
}
/* Fetch by language and entry Id. Note that this version includes the language name */
+170 -50
View File
@@ -12,13 +12,14 @@ const {
getFileNamesInFolder,
getFileStatsInFolder
} = require('./helpers/extraction')
const { extractionApiOrigin } = require('../config/keys')
const Extraction = {}
// Fetch all extractions for a specific user.
Extraction.fetchAllForUser = async userId => {
const { rows: fetchedExtractions } = await db.query(
'SELECT id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE user_id = $1 ORDER BY id',
'SELECT id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE user_id = $1 ORDER BY status ASC, id DESC, time_finished DESC',
[userId]
)
@@ -74,19 +75,19 @@ Extraction.fetch = async id => {
return deserialize.extraction(fetchedExtraction)
}
// Fetch author email of a specific extraction entry from DB.
Extraction.fetchAuthorEmail = async id => {
// Fetch data of the author of a specific extraction entry from DB.
Extraction.fetchAuthorData = async id => {
const {
rows: [{ email }]
rows: [authorData]
} = await db.query(
`SELECT u.email
`SELECT u.email, u.language
FROM extraction e
LEFT JOIN "user" u ON u.id = e.user_id
WHERE e.id = $1`,
[id]
)
return email
return authorData
}
// Update extraction entry in DB.
@@ -160,6 +161,18 @@ Extraction.fetchTermCandidatesCount = async function (extractionId) {
return termCandidates.length
}
// Fetch term candidates slice for a specific extraction.
Extraction.fetchTermCandidatesSlice = async function (
extractionId,
fromIndex,
toIndex
) {
const termCandidatesJson = await this.fetchTermCandidatesJson(extractionId)
const termCandidates = JSON.parse(termCandidatesJson).terminoloski_kandidati
const termCandidatesSlice = termCandidates.slice(fromIndex, toIndex)
return termCandidatesSlice
}
// Mark extraction from own documents as began.
Extraction.beginOwn = async (extractionId, documentsNames) => {
let timeStarted
@@ -195,20 +208,20 @@ Extraction.beginOwn = async (extractionId, documentsNames) => {
Extraction.beginOss = async extractionId => {
let timeStarted
await db.transaction(async dbClient => {
const insertExtractionJob = dbClient.query(
'INSERT INTO extraction_job (extraction_id, job_type, filename) VALUES ($1, $2, $3)',
[extractionId, 'oss term candidates', '']
)
const updateExtraction = dbClient.query(
"UPDATE extraction SET status = 'in progress', time_started = NOW() WHERE id = $1 RETURNING time_started",
[extractionId]
)
;[
{
rows: [{ time_started: timeStarted }]
}
] = await Promise.all([updateExtraction, insertExtractionJob])
] = await Promise.all([
dbClient.query(
"UPDATE extraction SET status = 'in progress', time_started = NOW() WHERE id = $1 RETURNING time_started",
[extractionId]
),
dbClient.query(
'INSERT INTO extraction_job (extraction_id, job_type, filename) VALUES ($1, $2, $3)',
[extractionId, 'oss term candidates', '']
)
])
})
return timeStarted
@@ -235,6 +248,7 @@ Extraction.processOwn = async function (extractionId, extractionName) {
const documentNames = await this.fetchAllDocumentsNames(extractionId)
const conllusPath = getConllusPath(extractionId)
const conllusPaths = []
const MAX_BODY_LENGTH = 10 ** 9 // 1 GB
// Using remote API, transform each document into conllu format.
for (const documentName of documentNames) {
const filePath = `${documentsPath}/${documentName}`
@@ -242,17 +256,18 @@ Extraction.processOwn = async function (extractionId, extractionName) {
form.append('file', createReadStream(filePath), documentName)
try {
const { data: data1 } = await axios.post(
'http://rsdo.lhrs.feri.um.si:8080/datotekaVConlluAsync',
`${extractionApiOrigin}/datotekaVConlluAsync`,
form,
{
headers: {
...form.getHeaders()
}
},
maxBodyLength: MAX_BODY_LENGTH
}
)
const remotejobId = +data1.check_job_url.split('/').at(-1)
await db.query(
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
[remotejobId, extractionId, 'doc to conllu', documentName]
)
@@ -262,10 +277,15 @@ Extraction.processOwn = async function (extractionId, extractionName) {
while (true) {
await sleep(5)
const { data: data2 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
`${extractionApiOrigin}/job/${remotejobId}`
)
if (data2.finished_on) {
if (data2.job_status !== 'finished processing (OK)') throw Error()
if (data2.job_status !== 'finished processing (OK)') {
throw Error(
`Remote job with id ${remotejobId} failed with result:\n${data2.job_result}`
)
}
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
const fileSavePath = `${conllusPath}/${documentName}.conllu`
await writeFile(fileSavePath, data2.job_result)
@@ -277,7 +297,8 @@ Extraction.processOwn = async function (extractionId, extractionName) {
break
}
}
} catch {
} catch (error) {
logExtractionError(error, extractionId, 'doc to conllu', documentName)
await failTheJob(extractionId, 'doc to conllu', documentName)
}
}
@@ -309,15 +330,18 @@ Extraction.processOwn = async function (extractionId, extractionName) {
try {
const { data: data3 } = await axios.post(
'http://rsdo.lhrs.feri.um.si:8080/izlusciAsync',
`${extractionApiOrigin}/izlusciAsync`,
{
conllus: conllusArr,
prepovedaneBesede: Array.from(stopTermsSet)
}
prepovedaneBesede: Array.from(stopTermsSet),
// TODO Enabled for all cases. Add a switch for users later.
definicije: true
},
{ maxBodyLength: MAX_BODY_LENGTH }
)
const remotejobId = +data3.check_job_url.split('/').at(-1)
await db.query(
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
[remotejobId, extractionId, 'conllus to term candidates', '']
)
@@ -325,14 +349,21 @@ Extraction.processOwn = async function (extractionId, extractionName) {
while (true) {
await sleep(5)
const { data: data4 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
`${extractionApiOrigin}/job/${remotejobId}`
)
if (data4.finished_on) {
if (data4.job_status !== 'finished processing (OK)') throw Error()
const { job_result: jobResult } = data4
if (
data4.job_status !== 'finished processing (OK)' ||
!jobResult.terminoloski_kandidati
) {
throw Error(
`Remote job with id ${remotejobId} failed with result:\n${jobResult}`
)
}
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
await writeFile(termCandidatesPath, JSON.stringify(data4.job_result))
// TODO Once returned JSON is properly formed, use the bottom line instead.
// await writeFile(termCandidatesPath, data4.job_result.terminoloski_kandidati)
await writeFile(termCandidatesPath, JSON.stringify(jobResult))
await db.query(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'conllus to term candidates', '']
@@ -340,8 +371,10 @@ Extraction.processOwn = async function (extractionId, extractionName) {
break
}
}
} catch {
} catch (error) {
logExtractionError(error, extractionId, 'conllus to term candidates')
await failTheJob(extractionId, 'conllus to term candidates', '')
await skipConcordancerJob(extractionId)
await failExtraction(extractionId)
return
}
@@ -350,7 +383,7 @@ Extraction.processOwn = async function (extractionId, extractionName) {
// Start concondancer corpus processing.
try {
await db.query(
"UPDATE extraction_job SET status = 'in progress' WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
"UPDATE extraction_job SET status = 'in progress', time_started = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'concordancer', '']
)
console.log('CREATING CORPUS')
@@ -361,32 +394,90 @@ Extraction.processOwn = async function (extractionId, extractionName) {
} = await axios.post('http://concordancer:5000/dashboard/corpus', {
title: extractionName
})
// Wait for creation of corpus.
while (true) {
console.log('SLEEP FOR 5 SECS')
await sleep(5)
const {
data: { status }
} = await axios.get(
`http://concordancer:5000/dashboard/corpus/${corpusId}`
)
if (status === 'Creating') continue
if (status === 'Active') break
throw Error('Error creating concorcander corpus')
}
console.log('CORPUS CREATED')
console.log('SLEEP FOR 10 SECS')
await sleep(10)
const inProgressStatusList = [
'Waiting',
'Importing',
'ImportingCompleted',
'Indexing',
'IndexingCompleted'
]
for (const conlluPath of conllusPaths) {
const textPathParts = conlluPath.split('/')
textPathParts[0] = '/data'
const textPath = textPathParts.join('/')
console.log('ADDING TEXT')
await axios.post(
const {
data: {
entityInfo: { id: textId }
}
} = await axios.post(
`http://concordancer:5000/dashboard/corpus/${corpusId}/text`,
{ sourceFile: textPath }
)
// Wait for text ingestion.
while (true) {
console.log('SLEEP FOR 5 SECS')
await sleep(5)
const {
data: { status }
} = await axios.get(
`http://concordancer:5000/dashboard/corpus/${corpusId}/text/${textId}`
)
if (inProgressStatusList.includes(status)) continue
if (status === 'Active') break
throw Error('Error importing concorcander text')
}
console.log('TEXT ADDED')
console.log('SLEEP FOR 10 SECS')
await sleep(10)
}
const termListPathParts = termCandidatesPath.split('/')
termListPathParts[0] = '/data'
const termListPath = termListPathParts.join('/')
console.log('ADDING TERMS')
await axios.post(
const {
data: {
entityInfo: { id: termListId }
}
} = await axios.post(
`http://concordancer:5000/dashboard/corpus/${corpusId}/termList`,
{ sourceFile: termListPath }
)
// Wait for term list ingestion.
while (true) {
console.log('SLEEP FOR 5 SECS')
await sleep(5)
const {
data: { status }
} = await axios.get(
`http://concordancer:5000/dashboard/corpus/${corpusId}/termList/${termListId}`
)
if (inProgressStatusList.includes(status)) continue
if (status === 'Active') break
throw Error('Error importing concorcander text')
}
console.log('TERMS ADDED')
await db.query(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'concordancer', '']
@@ -397,9 +488,8 @@ Extraction.processOwn = async function (extractionId, extractionName) {
[corpusId, extractionId]
)
console.log('EXTRACTION SUCCESSFUL')
} catch (e) {
console.log('EXTRACTION ERROR')
console.log(e)
} catch (error) {
logExtractionError(error, extractionId, 'concordancer')
await failTheJob(extractionId, 'concordancer', '')
await failExtraction(extractionId)
}
@@ -435,15 +525,17 @@ Extraction.processOss = async function (extractionId, ossParams) {
...(ossParams.documentType && { vrste: ossParams.documentType }),
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
...(ossParams.domainUdk && { udk: ossParams.domainUdk }),
...(stopTerms.length && { prepovedaneBesede: stopTerms })
...(stopTerms.length && { prepovedaneBesede: stopTerms }),
// TODO Enabled for all cases. Add a switch for users later.
definicije: true
})
const extractApiUrl = `http://rsdo.lhrs.feri.um.si:8080/oss/izlusciPoIskanjuAsync?${searchParams}`
const extractApiUrl = `${extractionApiOrigin}/oss/izlusciPoIskanjuAsync?${searchParams}`
try {
const { data: data1 } = await axios.get(extractApiUrl)
const remotejobId = +data1.check_job_url.split('/').at(-1)
await db.query(
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
[remotejobId, extractionId, 'oss term candidates', '']
)
@@ -451,15 +543,21 @@ Extraction.processOss = async function (extractionId, ossParams) {
while (true) {
await sleep(5)
const { data: data2 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
`${extractionApiOrigin}/job/${remotejobId}`
)
if (data2.finished_on) {
if (data2.job_status !== 'finished processing (OK)') throw Error()
if (
data2.job_status !== 'finished processing (OK)' ||
!Array.isArray(data2.job_result?.terminoloski_kandidati)
) {
throw Error(
`Remote job with id ${remotejobId} failed with result:\n${data2.job_result}`
)
}
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
const termCandidatesPath = getTermCandidatesPath(extractionId)
await writeFile(termCandidatesPath, JSON.stringify(data2.job_result))
// TODO Once returned JSON is properly formed, use the bottom line instead.
// await writeFile(termCandidatesPath, data4.job_result.terminoloski_kandidati)
await db.query(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'oss term candidates', '']
@@ -474,7 +572,8 @@ Extraction.processOss = async function (extractionId, ossParams) {
"UPDATE extraction SET status = 'finished', time_finished = NOW() WHERE id = $1",
[extractionId]
)
} catch {
} catch (error) {
logExtractionError(error, extractionId, 'oss term candidates')
await failTheJob(extractionId, 'oss term candidates', '')
await failExtraction(extractionId)
}
@@ -487,6 +586,13 @@ async function failTheJob(extractionId, jobType, documentName) {
)
}
async function skipConcordancerJob(extractionId) {
await db.query(
"UPDATE extraction_job SET status = 'skipped' WHERE extraction_id = $1 AND job_type = $2",
[extractionId, 'concordancer']
)
}
async function failExtraction(extractionId) {
await db.query(
"UPDATE extraction SET status = 'failed', time_finished = NOW() WHERE id = $1",
@@ -498,4 +604,18 @@ function sleep(seconds) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
}
function logExtractionError(error, extractionId, jobType, filename) {
// eslint-disable-next-line no-console
console.error(
Error(`Failed extraction job:
extractionId: ${extractionId},
jobType: ${jobType},
filename: ${filename}`)
)
if (error.isAxiosError) error = Error(`Axios message: ${error.message}`)
// eslint-disable-next-line no-console
console.error(error)
}
module.exports = Extraction
@@ -0,0 +1,486 @@
const xmlFlow = require('xml-flow')
const xss = require('xss')
exports.transformAndAppend = async (
entries,
exportFields,
exportFile,
exportFileFormat,
dsvConfig
) => {
let transformEntry
if (exportFileFormat === 'xml') transformEntry = intoXml
else if (dsvConfig) transformEntry = intoDsv(dsvConfig)
else if (exportFileFormat === 'tbx') transformEntry = intoTbx
else throw Error('Specified export format not supported yet')
for (const entry of entries) {
const transformedEntry = transformEntry(entry, exportFields)
await exportFile.write(`${transformedEntry}\n`)
}
}
function intoXml(entry, exportFields) {
const entryObj = { $name: 'entry', $markup: [] }
if (entry.term) {
entryObj.$markup.push({ term: entry.term })
}
if (exportFields.domainLabels && entry.domain_labels.length) {
const domainLabelsObj = {
$name: 'domainLabels',
$markup: entry.domain_labels.map(domainLabel => {
return { domainLabel }
})
}
entryObj.$markup.push(domainLabelsObj)
}
if (exportFields.label && entry.label) {
entryObj.$markup.push({ label: entry.label })
}
if (exportFields.definition && entry.definition) {
entryObj.$markup.push({ def: entry.definition })
}
if (exportFields.synonyms && entry.synonyms?.length) {
const SynonymsObj = {
$name: 'syns',
$markup: entry.synonyms.map(synonym => {
return { syn: synonym }
})
}
entryObj.$markup.push(SynonymsObj)
}
if (exportFields.links && entry.links.length) {
const LinksObj = {
$name: 'links',
$markup: entry.links.map(linkObj => {
return {
$name: 'link',
$attrs: { type: linkObj.type },
$text: linkObj.link
}
})
}
entryObj.$markup.push(LinksObj)
}
if (exportFields.other && entry.other) {
entryObj.$markup.push({ other: entry.other })
}
if (exportFields.foreignTerms && entry.foreign_entries.length) {
const fLangsObj = {
$name: 'fLangs',
$markup: entry.foreign_entries.map(fEntryObj => {
const fLangObj = {
$name: 'fLang',
$attrs: { lang: fEntryObj.lang_code },
$markup: []
}
if (fEntryObj.terms) {
const fTermsObj = {
$name: 'fTerms',
$markup: fEntryObj.terms.map(term => {
return { fTerm: term }
})
}
fLangObj.$markup.push(fTermsObj)
}
if (exportFields.foreignDefinitions && fEntryObj.definition) {
fLangObj.$markup.push({ fDef: fEntryObj.definition })
}
if (exportFields.foreignSynonyms && fEntryObj.synonyms?.length) {
const fSynsObj = {
$name: 'fSyns',
$markup: fEntryObj.synonyms.map(synonym => {
return { fSyn: synonym }
})
}
fLangObj.$markup.push(fSynsObj)
}
return fLangObj
})
}
entryObj.$markup.push(fLangsObj)
}
const shouldExportImages = exportFields.images && entry.image?.length
const shouldExportAudio = exportFields.audio && entry.audio?.length
const shouldExportVideos = exportFields.videos && entry.video?.length
const shouldCreateMm =
shouldExportImages || shouldExportAudio || shouldExportVideos
if (shouldCreateMm) {
const MmObj = {
$name: 'mm',
$markup: []
}
if (shouldExportImages) {
entry.image.forEach(el => {
MmObj.$markup.push({ image: el })
})
}
if (shouldExportAudio) {
entry.audio.forEach(el => {
MmObj.$markup.push({ audio: el })
})
}
if (shouldExportVideos) {
entry.video.forEach(el => {
MmObj.$markup.push({ video: el })
})
}
entryObj.$markup.push(MmObj)
}
return xmlFlow.toXml(entryObj, { escape: str => str })
}
function intoDsv({ delimiter, languageCodes }) {
return function (entry, exportFields) {
const fieldsArr = [intoDsvField(entry.term)]
if (exportFields.domainLabels) {
fieldsArr.push(
intoDsvField(
entry.domain_labels.reduce((agg, domainLabel, index) => {
agg += `${index ? '\n' : ''}${domainLabel}`
return agg
}, '')
)
)
}
if (exportFields.label) fieldsArr.push(intoDsvField(entry.label))
if (exportFields.definition) fieldsArr.push(intoDsvField(entry.definition))
if (exportFields.synonyms) {
fieldsArr.push(
intoDsvField(
entry.synonyms?.reduce((agg, synonym, index) => {
agg += `${index ? '\n' : ''}${synonym}`
return agg
}, '')
)
)
}
if (exportFields.links) {
fieldsArr.push(
intoDsvField(
entry.links.reduce((agg, linkObj, index) => {
agg += `${index ? '\n' : ''}[${linkObj.type[0]}t]${linkObj.link}`
return agg
}, '')
)
)
}
if (exportFields.other) fieldsArr.push(intoDsvField(entry.other))
languageCodes?.forEach(languageCode => {
const fEntryObj = entry.foreign_entries.find(
entryObj => entryObj.lang_code === languageCode
)
fieldsArr.push(
intoDsvField(
fEntryObj?.terms?.reduce((agg, term, index) => {
agg += `${index ? '\n' : ''}${term}`
return agg
}, '')
)
)
if (exportFields.foreignDefinitions) {
fieldsArr.push(intoDsvField(fEntryObj?.definition))
}
if (exportFields.foreignSynonyms) {
fieldsArr.push(
intoDsvField(
fEntryObj?.synonyms?.reduce((agg, synonym, index) => {
agg += `${index ? '\n' : ''}${synonym}`
return agg
}, '')
)
)
}
})
if (exportFields.images) {
fieldsArr.push(
intoDsvField(
entry.image?.reduce((agg, el, index) => {
agg += `${index ? '\n' : ''}${el}`
return agg
}, '')
)
)
}
if (exportFields.audio) {
fieldsArr.push(
intoDsvField(
entry.audio?.reduce((agg, el, index) => {
agg += `${index ? '\n' : ''}${el}`
return agg
}, '')
)
)
}
if (exportFields.videos) {
fieldsArr.push(
intoDsvField(
entry.video?.reduce((agg, el, index) => {
agg += `${index ? '\n' : ''}${el}`
return agg
}, '')
)
)
}
return fieldsArr.join(delimiter)
}
}
function intoDsvField(fieldString) {
if (!fieldString) return ''
return `"${fieldString.replaceAll('"', '""')}"`
}
const tbxLinkTypeMap = {
related: 'relatedConcept',
broader: 'relatedConceptBroader',
narrow: 'relatedConceptNarrower'
}
const brTagPattern = /<br[^>]*>/
function intoTbx(entry, exportFields) {
const entryObj = { $name: 'termEntry', $markup: [] }
if (exportFields.images) {
entry.image?.forEach(el => {
entryObj.$markup.push({
$name: 'xref',
$attrs: { type: 'xGraphic' },
$text: el
})
})
}
if (exportFields.audio) {
entry.audio?.forEach(el => {
entryObj.$markup.push({
$name: 'xref',
$attrs: { type: 'xAudio' },
$text: el
})
})
}
if (exportFields.videos) {
entry.video?.forEach(el => {
entryObj.$markup.push({
$name: 'xref',
$attrs: { type: 'xVideo' },
$text: el
})
})
}
const slLangObj = {
$name: 'langSet',
$attrs: { 'xml:lang': 'sl' },
$markup: []
}
if (exportFields.label && entry.label) {
slLangObj.$markup.push({
$name: 'descrip',
$attrs: { type: 'explanation' },
$text: intoTbxMixed(entry.label)
})
}
if (exportFields.definition && entry.definition) {
slLangObj.$markup.push({
$name: 'descrip',
$attrs: { type: 'definition' },
$text: intoTbxMixed(entry.definition)
})
}
if (exportFields.links) {
entry.links.forEach(linkObj => {
slLangObj.$markup.push({
$name: 'descrip',
$attrs: { type: tbxLinkTypeMap[linkObj.type] },
$text: intoTbxMixed(linkObj.link)
})
})
}
if (exportFields.other && entry.other) {
const otherLines = entry.other.split(brTagPattern)
otherLines.forEach(line => {
slLangObj.$markup.push({
$name: 'descrip',
$attrs: { type: 'other' },
$text: intoTbxMixed(line)
})
})
}
const shouldExportTerm = entry.term
const shouldExportDomainLabels =
exportFields.domainLabels && entry.domain_labels.length
const shouldCreateTermNtig = shouldExportTerm || shouldExportDomainLabels
if (shouldCreateTermNtig) {
const termNtigObj = {
$name: 'ntig',
$markup: [{ $name: 'termGrp', $markup: [] }]
}
const termGrpMarkup = termNtigObj.$markup[0].$markup
if (shouldExportTerm) {
termGrpMarkup.push({ term: intoTbxMixed(entry.term) })
termGrpMarkup.push({
$name: 'termNote',
$attrs: { type: 'termType' },
$text: 'entryTerm'
})
}
if (shouldExportDomainLabels) {
entry.domain_labels.forEach(domainLabel => {
termGrpMarkup.push({
$name: 'termNote',
$attrs: { type: 'domain' },
$text: domainLabel
})
})
}
slLangObj.$markup.push(termNtigObj)
}
if (exportFields.synonyms) {
entry.synonyms?.forEach(synonym => {
slLangObj.$markup.push({
$name: 'ntig',
$markup: [
{
$name: 'termGrp',
$markup: [
{ term: intoTbxMixed(synonym) },
{
$name: 'termNote',
$attrs: { type: 'termType' },
$text: 'synonym'
}
]
}
]
})
})
}
entryObj.$markup.push(slLangObj)
if (exportFields.foreignTerms) {
entry.foreign_entries.forEach(fEntryObj => {
const langSetObj = {
$name: 'langSet',
$attrs: { 'xml:lang': fEntryObj.lang_code },
$markup: []
}
if (exportFields.foreignDefinitions && fEntryObj.definition) {
langSetObj.$markup.push({
$name: 'descrip',
$attrs: { type: 'definition' },
$text: intoTbxMixed(fEntryObj.definition)
})
}
fEntryObj.terms?.forEach(term => {
langSetObj.$markup.push({
$name: 'ntig',
$markup: [
{
$name: 'termGrp',
$markup: [{ term: intoTbxMixed(term) }]
}
]
})
})
if (exportFields.foreignSynonyms) {
fEntryObj.synonyms?.forEach(synonym => {
langSetObj.$markup.push({
$name: 'ntig',
$markup: [
{
$name: 'termGrp',
$markup: [
{ term: intoTbxMixed(synonym) },
{
$name: 'termNote',
$attrs: { type: 'termType' },
$text: 'synonym'
}
]
}
]
})
})
}
entryObj.$markup.push(langSetObj)
})
}
return xmlFlow.toXml(entryObj, { escape: str => str })
}
const tbxRichFilter = new xss.FilterXSS({
whiteList: {
sup: [],
sub: [],
b: [],
i: []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
})
const tagPattern = /<\s*(\/)?\s*([^\s/>]+)\s*>/g
function intoTbxMixed(mixedContentStr) {
return tbxRichFilter
.process(mixedContentStr)
.replace(tagPattern, tbxMixedReplacer)
}
const tbxMixedTypeMap = {
sup: 'superscript',
sub: 'subscript',
b: 'bold',
i: 'italics'
}
function tbxMixedReplacer(match, closingSlash, tagName) {
if (closingSlash) return '</hi>'
return `<hi type="${tbxMixedTypeMap[tagName]}">`
}
@@ -250,7 +250,7 @@ function customTagHandler(tag, html, { isWhite, isClosing }) {
const matchUrl = html.match(/href="?(?<url>https?:\/\/.*?)"?[\s>]/)
const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined
return `<a href${url ? `="${url}" target="_blank"` : ''}>`
return `<a href="${url || ''}" target="_blank">`
}
function toText(markupObj) {
+89 -8
View File
@@ -1,7 +1,22 @@
const { removeHtmlTags } = require('../../helpers')
const { searchEngineClient, ENTRY_INDEX } = require('../../search-engine')
const { DATA_FILES_PATH } = require('../../../config/settings')
exports.deserialize = {
dictionary(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
timeModified: dictionary.time_modified,
status: dictionary.status,
countEntries: dictionary.count_entries,
countComments: dictionary.count_comments,
isAdmin: dictionary.is_admin
}
return deserializedDictionary
},
primaryDomain(domain) {
const deserializedDomain = {
id: domain.id,
@@ -102,16 +117,78 @@ exports.deserialize = {
return deserializedDomainLabel
},
imports(oneImport) {
const deserializedImports = {
timeStarted: oneImport.time_started,
status: oneImport.status,
deleteExisting: oneImport.delete_existing_entries,
fileFormat: oneImport.file_format,
countValidEntries: oneImport.count_valid_entries
// imports(oneImport) {
// const deserializedImports = {
// timeStarted: oneImport.time_started,
// status: oneImport.status,
// deleteExisting: oneImport.delete_existing_entries,
// fileFormat: oneImport.file_format,
// countValidEntries: oneImport.count_valid_entries
// }
// return deserializedImports
// },
exports(oneExport) {
const deserializedExports = {
id: oneExport.id,
status: oneExport.status,
dateCreated: oneExport.date_created,
entryCount: oneExport.entry_count ?? '',
typeString: `${
oneExport.is_valid_filter === true
? 2
: oneExport.is_valid_filter === false
? 3
: 1
}${
oneExport.is_published_filter === true
? 2
: oneExport.is_published_filter === false
? 3
: 1
}${
oneExport.status_filter === 'complete'
? 2
: oneExport.status_filter === 'in_edit'
? 3
: 1
}${
oneExport.is_terminology_reviewed_filter === true
? 2
: oneExport.is_terminology_reviewed_filter === false
? 3
: 1
}${
oneExport.is_language_reviewed_filter === true
? 2
: oneExport.is_language_reviewed_filter === false
? 3
: 1
}-${
oneExport.export_file_format === 'xml'
? 1
: oneExport.export_file_format === 'csv'
? 2
: oneExport.export_file_format === 'tsv'
? 3
: 0
}`
}
return deserializedImports
return deserializedExports
},
exportDownloadMetadata(metadata) {
const deserializedMetadata = {
exportStatus: metadata.status,
dictionaryId: metadata.dictionary_id,
nameString: metadata.name_string,
timeString: metadata.time_string,
fileFormat: metadata.export_file_format
}
return deserializedMetadata
}
}
@@ -194,3 +271,7 @@ function prepareEntryForIndexing(entry) {
}
exports.prepareEntryForIndexing = prepareEntryForIndexing
exports.getExportFilesPath = dictId => {
return `${DATA_FILES_PATH}/dict_export/${dictId}`
}
+14 -9
View File
@@ -1,4 +1,5 @@
const { readdir, stat } = require('fs/promises')
const path = require('path')
const { partial } = require('filesize')
const { DATA_FILES_PATH } = require('../../config/settings')
@@ -43,17 +44,21 @@ exports.getFileNamesInFolder = async folderPath => {
return filenames
}
exports.getFileStats = getFileStats
exports.getFileStatsInFolder = async folderPath => {
const filenames = await readdir(folderPath)
const fileStats = await Promise.all(
filenames.map(async filename => {
const filePath = `${folderPath}/${filename}`
const { mtimeMs: timeModified, size } = await stat(filePath)
const sizeHumanReadable = formatFileSize(size)
const fileStats = { filename, size: sizeHumanReadable, timeModified }
return fileStats
})
)
const filePaths = filenames.map(filename => `${folderPath}/${filename}`)
const fileStats = await Promise.all(filePaths.map(getFileStats))
fileStats.sort((a, b) => a.timeModified - b.timeModified)
return fileStats
}
async function getFileStats(filePath) {
const filename = path.basename(filePath)
const { mtimeMs: timeModified, size } = await stat(filePath)
const sizeHumanReadable = formatFileSize(size)
const fileStats = { filename, size: sizeHumanReadable, timeModified }
return fileStats
}
+4 -2
View File
@@ -9,8 +9,10 @@ exports.aggregateSettings = settings => {
exports.deserialize = {
settings(settings) {
const deserializedSettings = {
name: settings.portal_name,
description: settings.portal_description,
nameSl: settings.portal_name_sl,
nameEn: settings.portal_name_en,
descriptionSl: settings.portal_description_sl,
descriptionEn: settings.portal_description_en,
code: settings.portal_code,
isExtractionEnabled: settings.is_extraction_enabled,
isDictionariesEnabled: settings.is_dictionaries_enabled,
@@ -9,7 +9,7 @@ module.exports = function (filters, hitsPerPage, page) {
filter: []
}
},
sort: ['_score', 'timeCreated']
sort: ['_score', { timeCreated: 'desc' }]
}
if (filters.status) {
@@ -45,7 +45,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
filter: []
}
},
sort: ['_score', 'timeCreated']
sort: ['_score', { timeCreated: 'desc' }]
}
if (filters.status) {
@@ -39,7 +39,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
filter: []
}
},
sort: ['_score', 'timeCreated']
sort: ['_score', { timeCreated: 'desc' }]
}
if (filters.status) {
@@ -54,7 +54,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
filter: []
}
},
sort: ['_score', 'timeCreated']
sort: ['_score', { timeCreated: 'desc' }]
}
if (filters.status) {
@@ -2,7 +2,7 @@ const { EDITOR_MAX_HITS } = require('../../../../../config/settings')
module.exports = function (dictionaryId, filters, searchFieldFilters) {
const queryDsl = {
_source: ['id', 'isValid', 'isPublished', 'term'],
_source: ['id', 'isValid', 'isPublished', 'term', 'homonymSort'],
fields: ['foreignEntries.terms'],
script_fields: {
commentActivityIndicator: {
+3 -1
View File
@@ -7,6 +7,7 @@ exports.deserialize = {
lastName: user.last_name,
email: user.email,
hitsPerPage: user.hits_per_page,
language: user.language,
userRoles: user.user_roles,
assignedConsultancyEntries: user.assigned_consultancy_entries
}
@@ -21,7 +22,8 @@ exports.deserialize = {
firstName: userData.first_name,
lastName: userData.last_name,
email: userData.email,
password: userData.password
status: userData.status,
language: userData.language
}
return deserializedData
+21
View File
@@ -73,6 +73,9 @@ class InterInstanceSync {
'SELECT l.code AS language_code, ef.entry_id, ef.term, ef.definition, ef.synonym FROM entry_foreign AS ef' +
' INNER JOIN language AS l ON l.id = ef.language_id' +
' WHERE entry_id = ANY($1::int[]) ORDER BY ef.entry_id, l.code'
const sqlLinks =
'SELECT entry_id, link, type FROM entry_link WHERE entry_id = ANY($1::int[]) ORDER BY entry_id, type, link'
const { rows: entryList } = await db.query(sqlEntries, [
dictionaryId,
since
@@ -83,6 +86,7 @@ class InterInstanceSync {
const { rows: translationList } = await db.query(sqlTranslations, [
entryIds
])
const { rows: linkList } = await db.query(sqlLinks, [entryIds])
let currentEntryId = 0
let lastEntryId = 0
translationList.forEach(t => {
@@ -102,6 +106,23 @@ class InterInstanceSync {
}
entry.translations.push(t)
})
linkList.forEach(l => {
currentEntryId = l.entry_id
const entry = entryList.find(e => {
return e.id === currentEntryId
})
if (!entry) return
if (lastEntryId === 0) {
entry.links = []
lastEntryId = l.entry_id
} else if (currentEntryId !== lastEntryId) {
// new entry translations : save previous
entry.links = []
lastEntryId = currentEntryId
currentEntryId = l.entry_id
}
entry.links.push({ type: l.type, link: l.link })
})
return entryList
}
}
+18 -10
View File
@@ -12,9 +12,11 @@ Portal.fetchInstanceSettings = async () => {
WHERE
name
IN (
'portal_name',
'portal_name_sl',
'portal_name_en',
'portal_code',
'portal_description',
'portal_description_sl',
'portal_description_en',
'is_consultancy_enabled',
'is_dictionaries_enabled',
'is_extraction_enabled')`
@@ -32,9 +34,11 @@ Portal.updateInstaceSettings = async payload => {
const isConsultancyEnabled = payload.isConsultancyEnabled ? 'T' : 'F'
const values = [
payload.portalName,
payload.portalNameSl,
payload.portalNameEn,
payload.portalCode,
payload.portalDescription,
payload.portalDescriptionSl,
payload.portalDescriptionEn,
isExtractionEnabled,
isDictionariesEnabled,
isConsultancyEnabled
@@ -47,17 +51,21 @@ Portal.updateInstaceSettings = async payload => {
value
= CASE name
WHEN
'portal_name' THEN $1
'portal_name_sl' THEN $1
WHEN
'portal_code' THEN $2
'portal_name_en' THEN $2
WHEN
'portal_description' THEN $3
'portal_code' THEN $3
WHEN
'is_extraction_enabled' THEN $4
'portal_description_sl' THEN $4
WHEN
'is_dictionaries_enabled' THEN $5
'portal_description_en' THEN $5
WHEN
'is_consultancy_enabled' THEN $6
'is_extraction_enabled' THEN $6
WHEN
'is_dictionaries_enabled' THEN $7
WHEN
'is_consultancy_enabled' THEN $8
ELSE value
END`
+1 -1
View File
@@ -47,7 +47,7 @@ Eurotermbank.push = async () => {
'definition', ef.definition,
'synonyms', ef.synonym)
FROM entry_foreign ef
LEFT JOIN LANGUAGE l ON l.id = ef.language_id
LEFT JOIN language l ON l.id = ef.language_id
WHERE entry_id = e.id
) foreign_entries
FROM entry e
+37 -7
View File
@@ -17,6 +17,7 @@ User.create = async user => {
user.email || null,
bcryptHash || null
]
if (user.language) values.push(user.language)
const text = `INSERT INTO "user" (
username,
@@ -24,6 +25,7 @@ User.create = async user => {
last_name,
email,
bcrypt_hash
${user.language ? ', language' : ''}
)
VALUES (${db.genParamStr(values)})
RETURNING id`
@@ -67,7 +69,10 @@ User.fetchByActivationToken = async activationToken => {
// Activate user account.
User.activateAccount = async user => {
await db.query(`UPDATE "user" SET status = 'active' WHERE id = $1`, [user.id])
await db.query(
`UPDATE "user" SET status = 'active', time_activated = NOW() WHERE id = $1`,
[user.id]
)
}
// Generate a user remember me token.
@@ -101,6 +106,7 @@ User.fetchDeserializedDataById = async userId => {
u.last_name,
u.email,
u.hits_per_page,
u.language,
ARRAY(
SELECT jsonb_build_object(
'roleName', r.role_name,
@@ -258,9 +264,9 @@ User.updatePortalRoles = async rolesPerUser => {
User.fetchUser = async userId => {
const text = `
SELECT id, username, first_name, last_name, email
SELECT id, username, first_name, last_name, email, status, language
FROM "user"
WHERE id=$1`
WHERE id = $1`
const value = [userId]
const { rows } = await db.query(text, value)
@@ -271,17 +277,33 @@ User.fetchUser = async userId => {
}
User.updateUser = async (userId, payload) => {
const text = `
const previousStatusText = 'SELECT status FROM "user" WHERE id = $1'
const { rows } = await db.query(previousStatusText, [userId])
const previousStatus = rows[0].status
let statusValue
if (previousStatus === 'registered') {
statusValue = !payload.status ? 'registered' : 'active'
} else statusValue = !payload.status ? 'inactive' : 'active'
const updateText = `
UPDATE "user"
SET
username = $2,
first_name = $3,
last_name = $4
last_name = $4,
status = $5
WHERE id = $1`
const values = [userId, payload.username, payload.firstName, payload.lastName]
const values = [
userId,
payload.username,
payload.firstName,
payload.lastName,
statusValue
]
await db.query(text, values)
await db.query(updateText, values)
}
User.fetchUserRoles = async userId => {
@@ -467,4 +489,12 @@ User.updateHitsPerPage = async (username, hitsPerPageAmount) => {
)
}
// Update user's language.
User.updateLanguage = async (userId, languageCode) => {
await db.query('UPDATE "user" SET language = $1 WHERE id = $2', [
languageCode,
userId
])
}
module.exports = User
+82
View File
@@ -22,6 +22,9 @@
"form-data": "^4.0.0",
"helmet": "^5.0.2",
"http-errors": "^2.0.0",
"i18next": "^22.4.10",
"i18next-fs-backend": "^2.1.1",
"i18next-http-middleware": "^3.2.2",
"ioredis": "^4.27.8",
"morgan": "^1.10.0",
"multer": "^1.4.3",
@@ -64,6 +67,17 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
@@ -1678,6 +1692,38 @@
"node": ">= 6"
}
},
"node_modules/i18next": {
"version": "22.4.10",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.10.tgz",
"integrity": "sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.20.6"
}
},
"node_modules/i18next-fs-backend": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz",
"integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA=="
},
"node_modules/i18next-http-middleware": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/i18next-http-middleware/-/i18next-http-middleware-3.2.2.tgz",
"integrity": "sha512-OW2sWnbns+PuLi77T+/ni4Mi+TNJ6Q6XNGdZicMv9FD+QfZrFrynBVqryHB/bfflszx2Zswg/kxtCildVLdhSA=="
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -3039,6 +3085,11 @@
"node": ">=4"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/registry-auth-token": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
@@ -3789,6 +3840,14 @@
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
"integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ=="
},
"@babel/runtime": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
},
"@babel/types": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
@@ -5032,6 +5091,24 @@
"debug": "4"
}
},
"i18next": {
"version": "22.4.10",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.10.tgz",
"integrity": "sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==",
"requires": {
"@babel/runtime": "^7.20.6"
}
},
"i18next-fs-backend": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz",
"integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA=="
},
"i18next-http-middleware": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/i18next-http-middleware/-/i18next-http-middleware-3.2.2.tgz",
"integrity": "sha512-OW2sWnbns+PuLi77T+/ni4Mi+TNJ6Q6XNGdZicMv9FD+QfZrFrynBVqryHB/bfflszx2Zswg/kxtCildVLdhSA=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -6100,6 +6177,11 @@
"redis-errors": "^1.0.0"
}
},
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"registry-auth-token": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
+5 -2
View File
@@ -4,8 +4,8 @@
"private": true,
"scripts": {
"start": "node ./bin/www",
"devstart": "nodemon --ignore data_files/ --inspect=0.0.0.0:9229 ./bin/www",
"devstart-wait": "nodemon --ignore data_files/ --inspect-brk=0.0.0.0:9229 ./bin/www"
"devstart": "nodemon --ignore data_files/ --ignore public/locales/dev --inspect=0.0.0.0:9229 ./bin/www",
"devstart-wait": "nodemon --ignore data_files/ --ignore public/locales/dev --inspect-brk=0.0.0.0:9229 ./bin/www"
},
"dependencies": {
"@opensearch-project/opensearch": "^2.1.0",
@@ -22,6 +22,9 @@
"form-data": "^4.0.0",
"helmet": "^5.0.2",
"http-errors": "^2.0.0",
"i18next": "^22.4.10",
"i18next-fs-backend": "^2.1.1",
"i18next-http-middleware": "^3.2.2",
"ioredis": "^4.27.8",
"morgan": "^1.10.0",
"multer": "^1.4.3",
Binary file not shown.
@@ -0,0 +1,285 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="dictionary">
<xs:annotation>
<xs:documentation>Dictionary (root element)</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="entry" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Entry</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="term" type="mixedBasic">
<xs:annotation>
<xs:documentation>Slovenian Term</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="hwGrp" minOccurs="0">
<xs:annotation>
<xs:documentation>Headword Group</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attribute name="wfs" type="headwordStatus" default="no content">
<xs:annotation>
<xs:documentation>Wordforms</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="acc" type="headwordStatus" default="no content">
<xs:annotation>
<xs:documentation>Accent</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="pron" type="headwordStatus" default="no content">
<xs:annotation>
<xs:documentation>Pronunciation</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="domainLabels" minOccurs="0">
<xs:annotation>
<xs:documentation>Domain Labels</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="domainLabel" type="xs:token" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Domain Label</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="label" type="mixedExtended" minOccurs="0">
<xs:annotation>
<xs:documentation>Label</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="def" type="mixedExtended" minOccurs="0">
<xs:annotation>
<xs:documentation>Definition</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="syns" minOccurs="0">
<xs:annotation>
<xs:documentation>Synonyms</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="syn" type="mixedBasic" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Synonym</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="links" minOccurs="0">
<xs:annotation>
<xs:documentation>Links</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="link" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Link</xs:documentation>
</xs:annotation>
<xs:complexType mixed="true">
<xs:complexContent mixed="true">
<xs:extension base="mixedBasic">
<xs:attribute name="type" type="linkType" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="other" type="mixedOther" minOccurs="0">
<xs:annotation>
<xs:documentation>Other</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="fLangs" minOccurs="0">
<xs:annotation>
<xs:documentation>Foreign Languages</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="fLang" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Foreign Language</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="fTerms">
<xs:annotation>
<xs:documentation>Foreign Terms</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="fTerm" type="mixedBasic" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Foreign Term</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="fDef" type="mixedExtended" minOccurs="0">
<xs:annotation>
<xs:documentation>Foreign Definition</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="fSyns" minOccurs="0">
<xs:annotation>
<xs:documentation>Foreign Synonims</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="fSyn" type="mixedBasic" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Foreign Synonim</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="lang" type="xs:language" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="mm" minOccurs="0">
<xs:annotation>
<xs:documentation>Multimedia</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element name="image" type="xs:anyURI">
<xs:annotation>
<xs:documentation>Image</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="audio" type="xs:anyURI">
<xs:annotation>
<xs:documentation>Audio</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="video" type="xs:anyURI">
<xs:annotation>
<xs:documentation>Video</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="sup" type="xs:string">
<xs:annotation>
<xs:documentation>Superscript</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="sub" type="xs:string">
<xs:annotation>
<xs:documentation>Subscript</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="b">
<xs:annotation>
<xs:documentation>Bold</xs:documentation>
</xs:annotation>
<xs:complexType mixed="true">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="sup"/>
<xs:element ref="sub"/>
<xs:element ref="i"/>
<xs:element ref="a"/>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:element name="i">
<xs:annotation>
<xs:documentation>Italic</xs:documentation>
</xs:annotation>
<xs:complexType mixed="true">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="sup"/>
<xs:element ref="sub"/>
<xs:element ref="b"/>
<xs:element ref="a"/>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:element name="a">
<xs:annotation>
<xs:documentation>Link</xs:documentation>
</xs:annotation>
<xs:complexType mixed="true">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="sup"/>
<xs:element ref="sub"/>
<xs:element ref="b"/>
<xs:element ref="i"/>
</xs:choice>
<xs:attribute name="href" type="xs:anyURI" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="br">
<xs:annotation>
<xs:documentation>New Line</xs:documentation>
</xs:annotation>
<xs:complexType/>
</xs:element>
<xs:simpleType name="headwordStatus">
<xs:restriction base="xs:string">
<xs:enumeration value="no content"/>
<xs:enumeration value="unverified"/>
<xs:enumeration value="unconfirmed"/>
<xs:enumeration value="confirmed"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="linkType">
<xs:restriction base="xs:string">
<xs:enumeration value="related"/>
<xs:enumeration value="broader"/>
<xs:enumeration value="narrow"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="mixedBasic" mixed="true">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="sup"/>
<xs:element ref="sub"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="mixedExtended" mixed="true">
<xs:complexContent mixed="true">
<xs:extension base="mixedBasic">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="b"/>
<xs:element ref="i"/>
<xs:element ref="a"/>
</xs:choice>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="mixedOther" mixed="true">
<xs:complexContent mixed="true">
<xs:extension base="mixedExtended">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="br"/>
</xs:choice>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:schema>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.49999 22C2.99999 22 2.49999 21.9 1.99999 21.6C0.599987 20.8 0.0999871 18.9 0.899987 17.5L9.39999 3.30002C9.69999 2.90002 9.99999 2.50002 10.4 2.30002C11.1 1.90002 11.9 1.80002 12.7 2.00002C13.5 2.20002 14.1 2.70002 14.6 3.40002L23 17.5C23.3 18 23.4 18.5 23.4 19C23.4 19.8 23.1 20.6 22.5 21.1C22 21.7 21.3 22 20.5 22H3.49999ZM11.1 4.40002L2.69999 18.5C2.39999 19 2.59999 19.6 3.09999 19.9C3.19999 20 3.39999 20 3.49999 20H20.4C20.7 20 20.9 19.9 21.1 19.7C21.3 19.5 21.4 19.3 21.4 19C21.4 18.8 21.4 18.7 21.3 18.5L12.9 4.40002C12.6 3.90002 12 3.80002 11.5 4.00002C11.3 4.10002 11.2 4.20002 11.1 4.40002Z" fill="white"/>
<path d="M12 14C11.4 14 11 13.6 11 13V9.00002C11 8.40002 11.4 8.00002 12 8.00002C12.6 8.00002 13 8.40002 13 9.00002V13C13 13.6 12.6 14 12 14Z" fill="white"/>
<path d="M12 18C11.7 18 11.5 17.9 11.3 17.7C11.1 17.5 11 17.3 11 17C11 16.9 11 16.7 11.1 16.6C11.2 16.5 11.2 16.4 11.3 16.3C11.4 16.2 11.5 16.1 11.6 16.1C11.8 16 12 16 12.2 16C12.3 16 12.3 16 12.4 16.1C12.5 16.1 12.5 16.1 12.6 16.2C12.6 16.2 12.7 16.3 12.8 16.3C12.9 16.4 13 16.5 13 16.6C13 16.7 13.1 16.9 13.1 17C13.1 17.3 13 17.5 12.8 17.7C12.5 17.9 12.3 18 12 18Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.46447 15.4645C2.40215 14.5268 3.67392 14 5 14H12C13.3261 14 14.5979 14.5268 15.5355 15.4645C16.4732 16.4021 17 17.6739 17 19V21C17 21.5523 16.5523 22 16 22C15.4477 22 15 21.5523 15 21V19C15 18.2044 14.6839 17.4413 14.1213 16.8787C13.5587 16.3161 12.7956 16 12 16H5C4.20435 16 3.44129 16.3161 2.87868 16.8787C2.31607 17.4413 2 18.2044 2 19V21C2 21.5523 1.55228 22 1 22C0.447715 22 0 21.5523 0 21V19C0 17.6739 0.526784 16.4021 1.46447 15.4645Z" fill="#46535B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 4C6.84315 4 5.5 5.34315 5.5 7C5.5 8.65685 6.84315 10 8.5 10C10.1569 10 11.5 8.65685 11.5 7C11.5 5.34315 10.1569 4 8.5 4ZM3.5 7C3.5 4.23858 5.73858 2 8.5 2C11.2614 2 13.5 4.23858 13.5 7C13.5 9.76142 11.2614 12 8.5 12C5.73858 12 3.5 9.76142 3.5 7Z" fill="#46535B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.2929 7.29289C17.6834 6.90237 18.3166 6.90237 18.7071 7.29289L23.7071 12.2929C24.0976 12.6834 24.0976 13.3166 23.7071 13.7071C23.3166 14.0976 22.6834 14.0976 22.2929 13.7071L17.2929 8.70711C16.9024 8.31658 16.9024 7.68342 17.2929 7.29289Z" fill="#46535B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.7071 7.29289C24.0976 7.68342 24.0976 8.31658 23.7071 8.70711L18.7071 13.7071C18.3166 14.0976 17.6834 14.0976 17.2929 13.7071C16.9024 13.3166 16.9024 12.6834 17.2929 12.2929L22.2929 7.29289C22.6834 6.90237 23.3166 6.90237 23.7071 7.29289Z" fill="#46535B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+87 -161
View File
@@ -1,7 +1,9 @@
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData */
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData, replaceContainer, i18next */
// const currentPagePath = location.pathname
let queryBattery = ''
window.addEventListener('load', () => {
initAdmin()
})
@@ -62,11 +64,11 @@ function initAdmin() {
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -130,11 +132,11 @@ function initAdmin() {
ifUserExists(data, type)
event.target.reset()
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
console.log(message)
}
@@ -155,11 +157,11 @@ function initAdmin() {
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -189,9 +191,9 @@ function initAdmin() {
aEl.type = 'link'
aEl.href = `/admin/uporabniki/${result.id}/urejanje`
imgEl.src = '/images/u_edit-alt.svg'
imgEl.alt = 'Uredi'
imgEl.alt = i18next.t('Uredi')
spanEl.className = 'normal-gray ms-1'
spanEl.textContent = 'Uredi'
spanEl.textContent = i18next.t('Uredi')
td4.append(aEl)
aEl.append(imgEl, spanEl)
rowEl.append(td1, td2, td3, td4)
@@ -214,11 +216,11 @@ function initAdmin() {
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -267,9 +269,9 @@ function initAdmin() {
aEl.type = 'link'
aEl.href = `/admin/slovarji/${result.id}/podatki`
imgEl.src = '/images/u_edit-alt.svg'
imgEl.alt = 'Uredi'
imgEl.alt = i18next.t('Uredi')
spanEl.className = 'normal-gray ms-1'
spanEl.textContent = 'Uredi'
spanEl.textContent = i18next.t('Uredi')
td6.append(aEl)
aEl.append(imgEl, spanEl)
rowEl.append(td1, td2, td3, td4, td5, td6)
@@ -343,18 +345,20 @@ function initAdmin() {
async function onPageChange(newPage) {
try {
const { page, numberOfAllPages, results } = await getDataForPage(
newPage
)
const results = await getDataForPage(newPage)
const numberOfAllPages = +results.headers['number-of-all-pages']
const page = +results.headers.page
removeAllChildNodes(resultsListEl)
renderResults(results)
renderResults(results.data)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -362,56 +366,46 @@ function initAdmin() {
}
async function getDataForPage(page) {
const url = `/api/v1/dictionaries/listSecondaryDomains?p=${page}`
const { data } = await axios.get(url)
return data
const url = `/api/v1/dictionaries/secondaryDomains?q=${queryBattery}&p=${page}`
return await axios.get(url)
}
function renderResults(results) {
results.forEach(result => {
const rowEl = document.createElement('tr')
const input = document.createElement('input')
const th = document.createElement('th')
const input2 = document.createElement('input')
const td1 = document.createElement('td')
const tdTrans = document.createElement('td')
const td2 = document.createElement('td')
const div = document.createElement('div')
const editBtn = document.createElement('button')
const deleteBtn = document.createElement('button')
const imgEditEl = document.createElement('img')
const imgDelEl = document.createElement('img')
input.value = result.id
input.type = 'hidden'
input.name = 'domainLabelId'
th.scope = 'row'
input2.className = 'form-check checkbox-table'
input2.type = 'checkbox'
input2.name = 'isVisible'
input2.disabled = true
input2.checked = !!result.isApproved
th.append(input2)
td1.className = 'tdata-area'
td1.textContent = result.nameSl
tdTrans.className = 'tdata-translation'
tdTrans.textContent = result.nameEn
td2.classList.add('buttons-group')
div.classList.add('table-buttons')
editBtn.className = 'p-0 table-button-grp me-3 edit-row-btn'
editBtn.type = 'button'
imgEditEl.src = '/images/u_edit-alt.svg'
deleteBtn.className = 'p-0 table-button-grp delete-row-btn'
deleteBtn.dataset.bsTarget = '#alert-modal'
deleteBtn.dataset.bsToggle = 'modal'
deleteBtn.type = 'button'
imgDelEl.src = '/images/red-trash-icon.svg'
td2.append(div)
div.append(editBtn, deleteBtn)
editBtn.append(imgEditEl)
deleteBtn.append(imgDelEl)
replaceContainer('page-results', results)
}
rowEl.append(input, th, td1, tdTrans, td2)
resultsListEl.appendChild(rowEl)
const updatePaginationOnFilter = axiosResult => {
const numberOfAllPages = +axiosResult.headers['number-of-all-pages']
const page = +axiosResult.headers.page
// removeAllChildNodes(resultsListEl)
// renderResults(results.data)
// console.log(numberOfAllPages)
// console.log(page)
// console.log(updatePager)
updatePager(page, numberOfAllPages)
}
/// / Due to unsual design, the function was moved inside
function searchController() {
// if (window.location.pathname.includes('podrocne-oznake')) { // No if required since already checkek above
queryBattery = document.getElementById('input-search').value
// console.log(queryBattery)
axios
.get(`/api/v1/dictionaries/secondaryDomains?q=${queryBattery}`)
.then(result => {
updatePaginationOnFilter(result)
replaceContainer('page-results', result.data)
})
// }
}
const inlineSearchButton = document.getElementById('inline-search-btn')
if (inlineSearchButton) {
inlineSearchButton.addEventListener('click', searchController)
$('#input-search').on('keyup', function (e) {
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
searchController()
}
})
}
}
@@ -450,7 +444,7 @@ function initAdmin() {
ifUserExists(data, type)
event.target.reset()
} catch (error) {
const message = 'Prišlo je do napake.'
const message = i18next.t('Prišlo je do napake.')
}
}
}
@@ -460,17 +454,6 @@ function initAdmin() {
formEditUser.addEventListener('input', enableButton)
}
if (/\/slovarji\/\d+\/struktura/.test(currentPagePath)) {
changePreview()
const switchForm = document.querySelector('.switch-forms')
switchForm.addEventListener('click', () => changePreview())
const wholeForm = document.getElementById('form-dictionary-structure')
wholeForm.addEventListener('input', enableButton)
const dictSideMenu = document.querySelector('.admin-nav-content')
unsavedData(wholeForm, dictSideMenu)
$('#languages-input').on('change', enableButton)
}
if (currentPagePath === '/admin/nastavitve/portal') {
const formAdminPortal = document.getElementById('admin-portal-settings')
formAdminPortal.addEventListener('input', enableButton)
@@ -537,7 +520,7 @@ function createNewAuthorInput(pageForm) {
divAuthor.className = 'author mt-sm-4 added-field'
divSubjectName.className = 'subject-name'
spanName.className = 'smaller-gray-uppercase'
spanName.textContent = 'AVTOR'
spanName.textContent = i18next.t('AVTOR')
divRow.className = 'row align-items-center'
divColSm5.className = 'col-sm-6'
inputGroup.className = 'input-group'
@@ -553,7 +536,7 @@ function createNewAuthorInput(pageForm) {
divColSm.className = 'col-sm'
spanNameInfoTxt.className =
'd-md-inline d-block name-info-txt ms-xxl-3 ms-md-3 mt-3 mt-ms-0'
spanNameInfoTxt.textContent = 'Dodaten avtor.'
spanNameInfoTxt.textContent = i18next.t('Dodaten avtor.')
divAuthor.appendChild(divSubjectName)
divSubjectName.appendChild(spanName)
@@ -594,9 +577,9 @@ function createNewAreaInput(pageForm) {
divSmallNameArea.className = 'author mt-4 added-field'
divSubjectName.className = 'subject-name'
spanInputNameTxtSlo.className = 'smaller-gray-uppercase'
spanInputNameTxtSlo.textContent = 'NOVO PODPODROČJE (slovensko)'
spanInputNameTxtSlo.textContent = i18next.t('NOVO PODPODROČJE (slovensko)')
spanInputNameTxtEng.className = 'smaller-gray-uppercase mt-4'
spanInputNameTxtEng.textContent = 'NOVO PODPODROČJE (angleško)'
spanInputNameTxtEng.textContent = i18next.t('NOVO PODPODROČJE (angleško)')
divRow.className = 'row align-items-center'
divRow2.className = 'row align-items-center'
divEnglishInput.className = 'mt-4'
@@ -617,11 +600,12 @@ function createNewAreaInput(pageForm) {
divColSm.className = 'col-sm mt-3 mt-sm-0'
divColSm2.className = 'col-sm mt-3 mt-sm-0'
spanNameInfoTxt.className = 'd-sm-inline name-info-txt ms-xxl-3 ms-md-3'
spanNameInfoTxt.textContent =
spanNameInfoTxt.textContent = i18next.t(
'Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.'
)
spanNameInfoTxtEng.className =
'd-sm-inline name-info-txt ms-xxl-3 ms-md-3 mt-4'
spanNameInfoTxtEng.textContent = 'Novo podpodročje (angleško).'
spanNameInfoTxtEng.textContent = i18next.t('Novo podpodročje (angleško).')
divSmallNameArea.appendChild(divSubjectName)
divSubjectName.appendChild(spanInputNameTxtSlo)
@@ -782,7 +766,7 @@ function handleAreasClick({ target }) {
const newButtonGroup = document.createElement('div')
const cancelButton = document.createElement('button')
const saveButton = document.createElement('button')
cancelButton.textContent = 'Prekliči'
cancelButton.textContent = i18next.t('Prekliči')
cancelButton.type = 'button'
cancelButton.className = 'btn btn-secondary me-2'
cancelButton.style.height = '33px'
@@ -790,7 +774,7 @@ function handleAreasClick({ target }) {
cancelButton.addEventListener('click', () =>
abortEditing(newButtonGroup, tableButtons, tDataArea, tDataTranslation)
)
saveButton.textContent = 'POTRDI'
saveButton.textContent = i18next.t('POTRDI')
saveButton.type = 'button'
saveButton.className = 'btn btn-primary'
saveButton.style.height = '33px'
@@ -986,76 +970,12 @@ function mobileMoveContent() {
currentPagePath.includes('slovarji') &&
!currentPagePath.includes('admin')
)
navTitle.textContent = 'Urejanje'
else navTitle.textContent = 'Administrator'
navTitle.textContent = i18next.t('Urejanje')
else navTitle.textContent = i18next.t('Administracija')
siteHeading.style.display = 'block'
}
}
function changePreview() {
const switchDomainLabel = document.getElementById('domain-labels')
const switchLabel = document.getElementById('label-checkbox')
const switchDefinition = document.getElementById('definition-check-box')
const switchSynonym = document.getElementById('synonyms')
const switchLink = document.getElementById('links')
const switchForeignLang = document.getElementById('language-group')
const switchForeignTerm = document.getElementById('termin-language-subgroup')
const switchForeignDef = document.getElementById(
'definition-language-subgroup'
)
const switchForeignSyn = document.getElementById('synonym-language-subgroup')
const switchImage = document.getElementById('images')
const switchAudio = document.getElementById('audio')
const switchVideo = document.getElementById('video')
const previewDomainSecondary = document.querySelector(
'.preview-domain-secondary'
)
const selectedLabel = document.querySelector('.preview-label')
const selectedDef = document.querySelector('.preview-definition')
const selectedSynonyms = document.querySelector('.preview-synonym')
const linkedTerms = document.getElementById('linked-terms')
const foreignLanguages = document.querySelector(
'.preview-languages-container'
)
const listForeignTerm = document.querySelector('.preview-f-term')
const listForeignDefinitions = document.querySelector(
'.preview-foreign-definition'
)
const foreignSynonyms = document.querySelector('.preview-foreign-synonyms')
const selectedImages = document.querySelector('.preview-images')
const selectedAudio = document.querySelector('.preview-audio')
const selectedVideo = document.querySelector('.preview-video')
if (!switchDomainLabel.checked)
previewDomainSecondary.classList.add('hide-preview')
else previewDomainSecondary.classList.remove('hide-preview')
if (!switchLabel.checked) selectedLabel.classList.add('hide-preview')
else selectedLabel.classList.remove('hide-preview')
if (!switchDefinition.checked) selectedDef.classList.add('hide-preview')
else selectedDef.classList.remove('hide-preview')
if (!switchSynonym.checked) selectedSynonyms.classList.add('hide-preview')
else selectedSynonyms.classList.remove('hide-preview')
if (!switchLink.checked) linkedTerms.classList.add('hide-preview')
else linkedTerms.classList.remove('hide-preview')
if (!switchForeignLang.checked) foreignLanguages.classList.add('hide-preview')
else foreignLanguages.classList.remove('hide-preview')
if (!switchForeignSyn.checked) foreignSynonyms.classList.add('hide-preview')
else foreignSynonyms.classList.remove('hide-preview')
if (!switchForeignTerm.checked) listForeignTerm.classList.add('hide-preview')
else listForeignTerm.classList.remove('hide-preview')
if (!switchForeignDef.checked)
listForeignDefinitions.classList.add('hide-preview')
else listForeignDefinitions.classList.remove('hide-preview')
if (!switchImage.checked) selectedImages.classList.add('hide-preview')
else selectedImages.classList.remove('hide-preview')
if (!switchAudio.checked) selectedAudio.classList.add('hide-preview')
else selectedAudio.classList.remove('hide-preview')
if (!switchVideo.checked) selectedVideo.classList.add('hide-preview')
else selectedVideo.classList.remove('hide-preview')
}
function checkUserRightsCb(el) {
const { terminologyReviewCb, languageReviewCb } = window.adminElements
const termRevCbs = document.querySelectorAll('.terminology-review-cb')
@@ -1109,12 +1029,18 @@ function ifUserExists(data, type) {
}
}
if (bool === true && data[0] !== undefined) {
modalText.textContent = `Uporabnik ${data[0].username} je že v tabeli.`
// modalText.textContent = `Uporabnik ${data[0].username} je že v tabeli.`
modalText.textContent =
i18next.t('Uporabnik') +
`${data[0].username}` +
i18next.t('je že v tabeli.')
modalEl.toggle()
} else createNewUserArea(data, type)
} else createNewUserArea(data, type)
} else {
modalText.textContent = `Preverite, če ste pravilno vpisali uporabniško ime. Bodite pozorni na velike in male črke.`
modalText.textContent = i18next.t(
'Preverite, če ste pravilno vpisali uporabniško ime. Bodite pozorni na velike in male črke.'
)
modalEl.toggle()
}
}
@@ -1172,24 +1098,24 @@ function createNewUserArea(data, type) {
inputConsultAdmin.type = 'checkbox'
if (type === 'portal') {
tdPortAdminOrAdmin.dataset.label = 'Skrbnik portala'
tdPortAdminOrAdmin.dataset.label = i18next.t('Skrbnik portala')
inputPortAdminOrAdmin.name = `rolesPerUser['${data[0].id}'][isPortalAdmin]`
tdDictAdmin.dataset.label = 'Skrbnik slovarjev'
tdDictAdmin.dataset.label = i18next.t('Skrbnik slovarjev')
inputDictAdmin.name = `rolesPerUser['${data[0].id}'][isDictionariesAdmin]`
tdConsultAdmin.dataset.label = 'Skrbnik svetovalnice'
tdConsultAdmin.dataset.label = i18next.t('Skrbnik svetovalnice')
inputConsultAdmin.name = `rolesPerUser['${data[0].id}'][isConsultancyAdmin]`
} else {
tdPortAdminOrAdmin.dataset.label = 'Administrator'
tdPortAdminOrAdmin.dataset.label = i18next.t('Administrator')
inputPortAdminOrAdmin.classList.add('administration-cb')
inputPortAdminOrAdmin.name = `rightsPerUser['${data[0].id}'][isAdministration]`
tdDictAdmin.dataset.label = 'Urejanje'
tdDictAdmin.dataset.label = i18next.t('Urejanje')
inputDictAdmin.name = `rightsPerUser['${data[0].id}'][isEditing]`
inputDictAdmin.classList.add('edit-cb')
tdConsultAdmin.dataset.label = 'Strokovni pregled'
tdConsultAdmin.dataset.label = i18next.t('Strokovni pregled')
inputConsultAdmin.classList.add('terminology-review-cb')
inputConsultAdmin.name = `rightsPerUser['${data[0].id}'][isTerminologyReview]`
tdLanguageRev.className = 'pt-1 pb-1'
tdLanguageRev.dataset.label = 'Jezikovni pregled'
tdLanguageRev.dataset.label = i18next.t('Jezikovni pregled')
divLanguageRev.className =
'form-check d-flex justify-content-left justify-content-xl-center'
inputLanguageRev.className = 'language-review-cb form-check-input'
@@ -1235,7 +1161,7 @@ function createNewUserArea(data, type) {
// Summernote
$('.summernote').summernote({
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
height: 300,
minheight: 150,
toolbar: [
@@ -4,21 +4,28 @@
const commentsContainer = document.querySelector('#comments-container')
const dropImage = document.querySelector('#dropImage')
const commentCount = document.querySelector('#comments-count')
const collapsableHR = document.querySelector('.collapable-hr')
const hide = () => {
pager.className += ' d-none'
pager.className += ' invisible'
commentsContainer.className += ' d-none'
commentCount.className += ' d-none'
commentCount.className += ' invisible'
collapsableHR.className += ' d-none'
}
const show = () => {
pager.className = 'pager d-flex'
commentsContainer.className = 'comments-container pe-1'
dropImage.src = '/images/arrow_drop_up.svg'
commentCount.className = 'comment-count d-flex ms-auto me-4 text-p875rem'
collapsableHR.className += 'comments-top-hr mt-3'
}
document.getElementById('collapseComments').addEventListener('click', e => {
isVisible = !isVisible
if (isVisible) {
pager.className = 'pager'
commentsContainer.className = 'comments-container pe-1'
dropImage.src = '/images/arrow_drop_up.svg'
commentCount.className = 'comment-count d-flex ms-auto me-4 text-p875rem'
show()
} else {
dropImage.src = '/images/arrow_drop_down.svg'
hide()
+10 -9
View File
@@ -1,6 +1,6 @@
// TODO Remove no-console ignore rule once things are out of rapid dev phase.
/* eslint no-console: 0 */
/* global axios, initPagination */
/* global axios, initPagination, i18next */
const pageURL = location.pathname
window.addEventListener('load', () => {
@@ -127,20 +127,21 @@ function renderCommentCount(commentCount) {
let displayText = `${commentCount} `
// TODO Let a i18n library handle the following logic.
// TODO I18n
switch (commentCount % 100) {
case 1:
displayText += 'komentar'
displayText += i18next.t('komentar')
break
case 2:
displayText += 'komentarja'
displayText += i18next.t('komentarja')
break
case 3:
case 4:
displayText += 'komentarji'
displayText += i18next.t('komentarji')
break
default:
displayText += 'komentarjev'
displayText += i18next.t('komentarjev')
break
}
@@ -309,7 +310,7 @@ function submitComment() {
let ctxId = null
if (ctxData.ctxId !== undefined) ctxId = ctxData.ctxId
if (!message) {
alert('Vaš komentar je brez vsebine.')
alert(i18next.t('Vaš komentar je brez vsebine.'))
} else {
const payload = { message, ctxType, ctxId, quoteId: null }
createComment(payload)
@@ -326,7 +327,7 @@ function submitCommentReply() {
let ctxId = null
if (ctxData.ctxId !== undefined) ctxId = ctxData.ctxId
if (!message) {
alert('Vaš komentar je brez vsebine.')
alert(i18next.t('Vaš komentar je brez vsebine.'))
} else {
const payload = { message, ctxType, ctxId, quoteId }
createComment(payload)
@@ -577,11 +578,11 @@ async function onPageChange(newPage) {
const { page, numberOfAllPages } = await displayComments(newPage)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -11,6 +11,7 @@ function routeConsultancy(searchString) {
} else {
url = new URL(location)
url.searchParams.delete('p')
const sq = document.getElementById('search-query')
if (sq) {
url.searchParams.set('q', sq.value)
@@ -65,3 +66,14 @@ if (inputMainSearch) {
propagateFunctionalityToASearchButton(searchButton)
propagateFunctionalityToASearchButton(advancedSearchButton)
$(document).ready(() => {
const focused = $('#description')
if (focused) {
focused.focus()
}
if (window.location.pathname === '/svetovanje/iskanje') {
// todo
}
})
+273 -8
View File
@@ -1,4 +1,5 @@
/* global $, handleDomainsClickForConsultancy, inputFieldsChecker, currentPagePath, axios, nthParent, tooltipListWOL, tooltipTriggerList */
/* global $, bootstrap, handleDomainsClickForConsultancy, initPagination, inputFieldsChecker, currentPagePath,
axios, nthParent, tooltipListWOL, tooltipTriggerList, removeAllChildNodes, i18next */
/*
author: Miha Stele, 2022
@@ -9,6 +10,10 @@ let selectedID = -1
// consultancy FORM
let offsetMain
let OFFSET_PADDING_MASK = 16
if (window.location.pathname === '/svetovanje/vprasanje/admin/svetovalci') {
OFFSET_PADDING_MASK = 0
}
function adjustOffsetBy() {
offsetMain = document.querySelector('#offset-main')
const fixedTopSection = document.querySelector('#fixed-top-section')
@@ -22,15 +27,21 @@ function adjustOffsetBy() {
if (offsetHeaderPadding !== null) offsetMain.style.paddingTop = `0px`
} else {
for (let i = 0; i < offsetHeader.length; i++) {
offsetHeader[i].style.paddingTop = `${referenceHeight}px`
offsetHeader[i].style.paddingTop = `${
referenceHeight - OFFSET_PADDING_MASK
}px`
}
if (headerPadding !== null) {
const headerPaddingHeight = headerPadding.offsetHeight
offsetMain.style.paddingTop = `${headerPaddingHeight}px`
offsetMain.style.paddingTop = `${
headerPaddingHeight - OFFSET_PADDING_MASK
}px`
}
if (offsetHeaderPadding !== null) {
const offsetHeaderHeight = offsetHeaderPadding.offsetHeight
offsetMain.style.paddingTop = `${referenceHeight + offsetHeaderHeight}px`
offsetMain.style.paddingTop = `${
referenceHeight + offsetHeaderHeight - OFFSET_PADDING_MASK
}px`
}
}
}
@@ -46,7 +57,7 @@ window.addEventListener('load', () => {
window.adminElements = ce
/* copy source admin.js in case of refactoring */
if (currentPagePath === '/svetovanje/vprasanje/admin/uporabniki') {
if (currentPagePath === '/svetovanje/vprasanje/admin/svetovalci') {
offsetMain.addEventListener('click', handleDomainsClickForConsultancy)
ce.name = document.getElementById('name-input')
if (ce.name) {
@@ -108,7 +119,7 @@ try {
const summernote = $('.summernote')
if (summernote) {
summernote.summernote({
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
height: 300,
minheight: 150,
toolbar: [
@@ -383,7 +394,17 @@ if (createAnswerForm) {
// console.log(data)
if (res.status === 201) {
window.location.href = '/svetovanje'
const infoModal = document.getElementById('begin-response')
if (infoModal) {
const responseModal = new bootstrap.Modal(infoModal)
responseModal.toggle()
}
document
.getElementById('understand-btn')
.addEventListener('click', () => {
window.location.href = '/svetovanje'
})
}
}
@@ -407,7 +428,7 @@ if (insertConsultantForm) {
})
.then(result => {
console.log(result)
window.location.href = '/svetovanje/vprasanje/admin/uporabniki'
window.location.href = '/svetovanje/vprasanje/admin/svetovalci'
})
})
}
@@ -421,6 +442,243 @@ try {
console.log(e)
}
/// Frontend Pagination (as in other pages in this project)
function onResultClick(e) {
const entryEl = e.target.closest(`#${this.id} a.rl`)
if (!entryEl) return
const anchorId = entryEl.querySelector('.anchor').id
const anchoredUrl = new URL(location)
anchoredUrl.hash = anchorId
history.replaceState(null, '', anchoredUrl)
}
const resultsListEl = document.getElementById('results-data')
if (resultsListEl) {
resultsListEl.addEventListener('click', onResultClick)
}
const initialPage = +new URL(location).searchParams.get('p') || 1
let updatePager
try {
updatePager = initPagination('pagination', onPageChange, initialPage)
} catch (e) {}
async function onPageChange(newPage) {
const receivedPageNumber = await changePage(newPage)
updateUrlAndHistory(receivedPageNumber)
}
async function changePage(newPage) {
try {
const res = await getDataForPage(newPage)
const page = +res.headers.page
const numberOfAllPages = +res.headers['number-of-all-pages']
const resultsMarkup = res.data
removeAllChildNodes(resultsListEl)
renderResults(resultsMarkup)
updatePager(page, numberOfAllPages)
return page
} catch (error) {
// console.log(error)
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
return // return to bypass error caused (assumed) by popstate on iOS/MacOS
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
}
}
async function getDataForPage(page) {
const qParams = new URL(location).searchParams
qParams.set('p', page)
// const url = `/api/v1/search/main?${qParams}`
// let url = `/api/v1/consultancy/entry-pagination?${qParams}`
// URL PATTERN PARSING
const PATH = window.location.pathname
if (PATH.includes('/admin/')) {
// todo ADMIN URL handling
qParams.set('isAdmin', 'true')
// url = `/api/v1/consultancy/entry-pagination?${qParams}` // required due to updated queryParams
if (PATH.includes('/novo')) {
qParams.set('type', 'new')
} else if (PATH.includes('/pripravljeno')) {
qParams.set('type', 'review')
} else if (PATH.includes('/zavrnjeno')) {
qParams.set('type', 'rejected')
} else if (PATH.includes('/v-delu')) {
qParams.set('type', 'in progress')
} else if (PATH.includes('/objavljeno')) {
qParams.set('type', 'published')
} else {
// Invalid case
console.log('INVALID CASE, CHECK URL')
return
}
}
// const url = `/api/v1/search/main?${qParams}`
const url = `/api/v1/consultancy/entry-pagination?${qParams}`
console.log(url)
return await axios.get(url)
}
function renderResults(resultsMarkup) {
const wrapper = document.createElement('div')
wrapper.innerHTML = resultsMarkup
resultsListEl.appendChild(wrapper)
}
function updateUrlAndHistory(page) {
const newUrl = new URL(location)
newUrl.searchParams.set('p', page)
history.pushState(null, '', newUrl)
}
window.addEventListener('popstate', () => {
const page = +new URL(location).searchParams.get('p') || 1
changePage(page)
})
/// // PAGINATION (backend only) /////
/* const updatePager = initPaginationBE('pagination', onPageChangeBE)
function onPageChangeBE(newPage) {
const newUrl = new URL(location)
newUrl.searchParams.set('p', newPage)
history.pushState(null, '', newUrl)
window.location.href = newUrl
}
function initPaginationBE(paginationRootElId, onPageChange, currentPage = 1) {
const rootEl = document.getElementById(paginationRootElId)
const btnFirstPage = rootEl.querySelector('.first-page')
const btnPreviousPage = rootEl.querySelector('.previous-page')
const btnNextPage = rootEl.querySelector('.next-page')
const btnLastPage = rootEl.querySelector('.last-page')
const formEl = rootEl.querySelector('form')
const pageInputEl = formEl.querySelector('input')
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
let reqLock = false
rootEl.addEventListener('click', handleButtonClick)
formEl.addEventListener('submit', handleFormSubmit)
function handleButtonClick({ target }) {
if (reqLock) return
const buttonEl = target.closest(`#${paginationRootElId} button`)
if (!buttonEl) return
const numOfAllPages = +pagesCountDisplayEl.textContent
if (buttonEl.classList.contains('first-page')) {
if (currentPage === 1) return
enableLock()
onPageChange(1)
} else if (buttonEl.classList.contains('previous-page')) {
if (currentPage === 1) return
enableLock()
onPageChange(currentPage - 1)
} else if (buttonEl.classList.contains('next-page')) {
if (currentPage === numOfAllPages) return
enableLock()
onPageChange(currentPage + 1)
} else if (buttonEl.classList.contains('last-page')) {
if (currentPage === numOfAllPages) return
enableLock()
onPageChange(numOfAllPages)
}
}
function handleFormSubmit(e) {
e.preventDefault()
if (reqLock) return
const inputValue = +pageInputEl.value
if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) {
alert('Nepravilna vrednost strani')
pageInputEl.value = currentPage
return
}
enableLock()
onPageChange(inputValue)
}
function enableLock() {
reqLock = true
btnFirstPage.disabled = true
btnPreviousPage.disabled = true
btnNextPage.disabled = true
btnLastPage.disabled = true
pageInputEl.disabled = true
}
function disableLock() {
reqLock = false
btnFirstPage.disabled = false
btnPreviousPage.disabled = false
btnNextPage.disabled = false
btnLastPage.disabled = false
pageInputEl.disabled = false
}
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
disableLock()
if (!newCurrentPage) return
currentPage = newCurrentPage
pageInputEl.value = newCurrentPage
pagesCountDisplayEl.textContent = newNumOfAllPages
if (newCurrentPage === 1) {
btnFirstPage.disabled = true
btnPreviousPage.disabled = true
} else {
btnFirstPage.disabled = false
btnPreviousPage.disabled = false
}
if (newCurrentPage === newNumOfAllPages) {
btnNextPage.disabled = true
btnLastPage.disabled = true
} else {
btnNextPage.disabled = false
btnLastPage.disabled = false
}
}
updatePagerUi(
+new URL(location).searchParams.get('p') || 1,
+document.querySelector('.pages-total').innerHTML
)
return updatePagerUi
}
window.addEventListener('popstate', () => {
const page = +new URL(location).searchParams.get('p') || 1
onPageChangeBE(page)
})
*/
/// ///////////////////
/*
if (tooltipList) {
console.log(tooltipList)
@@ -438,3 +696,10 @@ $(document).ready(() => {
focused.focus()
}
})
const askBackButton = document.querySelector('#cancel-cons-btn')
if (askBackButton) {
askBackButton.addEventListener('click', () => {
history.back()
})
}
+53 -37
View File
@@ -7,9 +7,15 @@
// Paginacijo inicializiraš s klicem funkcije initPagination:
// 1. parameter: id pager elementa. V demo-paginacija.pug je to #pagination.
// UPDATE: Sedaj sprejme tudi array idjev, če je pager kontrolerjev več (npr. en zgoraj (#pagination-top), en spodaj (#pagination-bottom)).
// 2. parameter: callback funkcijo, ki jo pager pokliče vsakič, ko uporabnik zahteva novo stran. Poliče jo s številko zahtevane strani.
// vrnjena vrednost: funkcija, ki jo (kasneje) kličeš za posodobitev pagerja. Tu jo poimenujem updateDemoPager.
const updateDemoPager = initPagination('pagination', onPageChange)
const updateDemoPager = initPagination(
['pagination-top', 'pagination-bottom'],
onPageChange
)
// Za samo en kontroler je bilo:
// const updateDemoPager = initPagination('pagination', onPageChange)
// Fukncija, prejme številko nove strani in naj:
// 1. Pridobi podatke nove strani.
@@ -62,27 +68,49 @@
* Na vsaki strani, kjer bo paginacija, jo inicializiraj in uporabljaj po zgledu zgornje kode. *
\** ****************************************************************************************************************************************** **/
function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
const rootEl = document.getElementById(paginationRootElId)
const btnFirstPage = rootEl.querySelector('.first-page')
const btnPreviousPage = rootEl.querySelector('.previous-page')
const btnNextPage = rootEl.querySelector('.next-page')
const btnLastPage = rootEl.querySelector('.last-page')
const formEl = rootEl.querySelector('form')
const pageInputEl = formEl.querySelector('input')
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
function initPagination(paginationRootElIds, onPageChange, currentPage = 1) {
let reqLock = false
let numOfAllPages
const backwardBtnEls = []
const forwardBtnEls = []
const pageInputEls = []
const pagesCountDisplayEls = []
rootEl.addEventListener('click', handleButtonClick)
formEl.addEventListener('submit', handleFormSubmit)
if (Array.isArray(paginationRootElIds)) {
paginationRootElIds.forEach(id => initControls(id))
} else {
initControls(paginationRootElIds)
}
const allControlEls = [...backwardBtnEls, ...forwardBtnEls, ...pageInputEls]
function handleButtonClick({ target }) {
function initControls(paginationRootElId) {
const rootEl = document.getElementById(paginationRootElId)
const btnFirstPage = rootEl.querySelector('.first-page')
const btnPreviousPage = rootEl.querySelector('.previous-page')
const btnNextPage = rootEl.querySelector('.next-page')
const btnLastPage = rootEl.querySelector('.last-page')
const formEl = rootEl.querySelector('form')
const pageInputEl = formEl.querySelector('input')
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
numOfAllPages = +pagesCountDisplayEl.textContent
backwardBtnEls.push(btnFirstPage, btnPreviousPage)
forwardBtnEls.push(btnNextPage, btnLastPage)
pageInputEls.push(pageInputEl)
pagesCountDisplayEls.push(pagesCountDisplayEl)
rootEl.addEventListener('click', e => {
handleButtonClick(e, paginationRootElId)
})
formEl.addEventListener('submit', e => handleFormSubmit(e, pageInputEl))
}
function handleButtonClick({ target }, paginationRootElId) {
if (reqLock) return
const buttonEl = target.closest(`#${paginationRootElId} button`)
if (!buttonEl) return
const numOfAllPages = +pagesCountDisplayEl.textContent
if (buttonEl.classList.contains('first-page')) {
if (currentPage === 1) return
@@ -103,12 +131,12 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
}
}
function handleFormSubmit(e) {
function handleFormSubmit(e, pageInputEl) {
e.preventDefault()
if (reqLock) return
const inputValue = +pageInputEl.value
if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) {
if (!(inputValue > 0 && inputValue <= numOfAllPages)) {
alert('Nepravilna vrednost strani')
pageInputEl.value = currentPage
return
@@ -120,20 +148,12 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
function enableLock() {
reqLock = true
btnFirstPage.disabled = true
btnPreviousPage.disabled = true
btnNextPage.disabled = true
btnLastPage.disabled = true
pageInputEl.disabled = true
allControlEls.forEach(el => (el.disabled = true))
}
function disableLock() {
reqLock = false
btnFirstPage.disabled = false
btnPreviousPage.disabled = false
btnNextPage.disabled = false
btnLastPage.disabled = false
pageInputEl.disabled = false
allControlEls.forEach(el => (el.disabled = false))
}
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
@@ -141,23 +161,19 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
if (!newCurrentPage) return
currentPage = newCurrentPage
pageInputEl.value = newCurrentPage
pagesCountDisplayEl.textContent = newNumOfAllPages
pageInputEls.forEach(el => (el.value = newCurrentPage))
pagesCountDisplayEls.forEach(el => (el.textContent = newNumOfAllPages))
if (newCurrentPage === 1) {
btnFirstPage.disabled = true
btnPreviousPage.disabled = true
backwardBtnEls.forEach(el => (el.disabled = true))
} else {
btnFirstPage.disabled = false
btnPreviousPage.disabled = false
backwardBtnEls.forEach(el => (el.disabled = false))
}
if (newCurrentPage === newNumOfAllPages) {
btnNextPage.disabled = true
btnLastPage.disabled = true
forwardBtnEls.forEach(el => (el.disabled = true))
} else {
btnNextPage.disabled = false
btnLastPage.disabled = false
forwardBtnEls.forEach(el => (el.disabled = false))
}
}
+377 -147
View File
@@ -1,4 +1,4 @@
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData */
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData, replaceContainer, i18next */
// Temporary workaround (use of currentPagePath).
@@ -6,6 +6,8 @@ window.addEventListener('load', () => {
initDictionaries()
})
let queryBattery = ''
function initDictionaries() {
const ce = {}
window.dictionaryElements = ce
@@ -166,7 +168,7 @@ function initDictionaries() {
if (ce.fileUploadInput.files.item(0) !== null)
chosenFile.textContent = ce.fileUploadInput.files.item(0).name
else {
chosenFile.textContent = 'Izberi datoteko'
chosenFile.textContent = i18next.t('Izberi datoteko')
}
}
})
@@ -185,12 +187,140 @@ function initDictionaries() {
await axios.post(event.target.action, payload)
alert('SLOVAR UVOŽEN')
alert(i18next.t('SLOVAR UVOŽEN'))
} catch (error) {
alert('NAPAKA V UVOZU')
alert(i18next.t('NAPAKA V UVOZU'))
}
}
{
const resultsListEl = document.getElementById('page-results')
const dictionaryId = document.getElementById('dictionary-id').value
const updatePager = initPagination('pagination', onPageChange)
async function onPageChange(newPage) {
try {
const { page, numberOfAllPages, results } = await getDataForPage(
newPage
)
removeAllChildNodes(resultsListEl)
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
}
}
async function getDataForPage(page) {
const url = `/api/v1/dictionaries/${dictionaryId}/showImportFromFileForm?p=${page}`
const { data } = await axios.get(url)
return data
}
function renderResults(results) {
const localeOptions = {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}
results.forEach(result => {
const rowEl = document.createElement('tr')
const td1 = document.createElement('td')
const td2 = document.createElement('td')
const td3 = document.createElement('td')
const td4 = document.createElement('td')
const date = new Date(result.time_started).toLocaleDateString(
'sl-SL',
localeOptions
)
td1.textContent = date
td2.textContent = result.file_format
td3.textContent = result.count_valid_entries
td4.textContent = result.status
rowEl.append(td1, td2, td3, td4)
resultsListEl.appendChild(rowEl)
})
}
}
}
if (/\/slovarji\/\d+\/izvoz/.test(currentPagePath)) {
const resultsListEl = document.getElementById('page-results')
const dictionaryId = document.getElementById('dictionary-id').value
const updatePager = initPagination('pagination', onPageChange)
async function onPageChange(newPage) {
try {
const { page, numberOfAllPages, results } = await getDataForPage(
newPage
)
removeAllChildNodes(resultsListEl)
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
}
}
async function getDataForPage(page) {
const url = `/api/v1/dictionaries/${dictionaryId}/showExportToFileForm?p=${page}`
const { data } = await axios.get(url)
return data
}
function renderResults(results) {
const localeOptions = {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}
results.forEach(result => {
const rowEl = document.createElement('tr')
const td1 = document.createElement('td')
const td2 = document.createElement('td')
const td3 = document.createElement('td')
const td4 = document.createElement('td')
const date = new Date(result.time_created).toLocaleDateString(
'sl-SL',
localeOptions
)
td1.textContent = date
td2.textContent = result.export_file_format
td3.textContent = result.entry_count
if (result.status === 'finished') {
const aEl = document.createElement('a')
aEl.href = `/slovarji/export-download/${result.id}`
aEl.textContent = i18next.t('Shrani')
aEl.className = 'btn btn-primary'
td4.append(aEl)
} else td4.textContent = ''
rowEl.append(td1, td2, td3, td4)
resultsListEl.appendChild(rowEl)
})
}
}
if (/\/slovarji\/\d+\/vsebina/.test(currentPagePath)) {
ce.formEditContent = document.getElementById('form-edit-content')
ce.formEditContent.addEventListener('submit', updateEntry)
@@ -233,6 +363,7 @@ function initDictionaries() {
ce.clearSearchInput = document.querySelector('.clear-search-input')
ce.collapsablePart = document.querySelectorAll('.collapsable-entry-part')
ce.scrollEl = document.querySelector('.content-nav-content')
$('.multiple').on('select2:select', enableButton)
ce.collapsablePart.forEach(el => {
el.addEventListener('hide.bs.collapse', () =>
changePageContent('collapse-hidden')
@@ -257,7 +388,7 @@ function initDictionaries() {
})
ce.statusEditEl = document.getElementById('status-edit')
// ce.deleteEntryBtn.addEventListener('click', () => ajaxDeleteEntry())
ce.newEntryBtn.addEventListener('click', () => {
ce.newEntryBtn.addEventListener('click', e => {
ce.formEditContent.reset()
changePageContent('new-entry-btn')
})
@@ -307,7 +438,8 @@ function initDictionaries() {
if (mcBtnsGrp.length) {
mcBtnsGrp.forEach(e => e.addEventListener('click', enableButton))
}
ce.newHeadwordGroupBtn.addEventListener('click', createHeadword)
if (ce.newHeadwordGroupBtn)
ce.newHeadwordGroupBtn.addEventListener('click', createHeadword)
if (ce.newConnectionBtn)
ce.newConnectionBtn.addEventListener('click', () =>
addConnectionField(ce.formEditContent, ce.newConnectionBtn)
@@ -408,7 +540,7 @@ function initDictionaries() {
if (termListMenu.childElementCount > 1) {
const termListLabels = termListMenu.querySelectorAll('label')
termListLabels[0].click()
if (termListLabels.length) termListLabels[0].click()
}
function handleClick({ target }) {
@@ -423,10 +555,11 @@ function initDictionaries() {
const okBtnText = modalUseBtn.textContent
const cnclBtnTxt = modalCnclBtn.textContent
const modalMainTxt = modalMain.textContent
modalUseBtn.textContent = 'Shrani'
modalCnclBtn.textContent = 'Ne shrani'
modalMain.textContent =
modalUseBtn.textContent = i18next.t('Shrani')
modalCnclBtn.textContent = i18next.t('Ne shrani')
modalMain.textContent = i18next.t(
'Imate neshranjene spremembe. Ali jih želite shraniti?'
)
const alertModal = new bootstrap.Modal(
document.getElementById('alert-modal')
)
@@ -478,8 +611,8 @@ function initDictionaries() {
if (deleteEntryEl) {
const modalUseBtn = document.getElementById('modal-del-btn')
const modalCnclBtn = document.getElementById('cancel-btn')
modalUseBtn.textContent = 'Izbriši'
modalCnclBtn.textContent = 'Ne izbriši'
modalUseBtn.textContent = i18next.t('Izbriši')
modalCnclBtn.textContent = i18next.t('Ne izbriši')
const alertModal = new bootstrap.Modal(
document.getElementById('delete-modal')
)
@@ -497,8 +630,8 @@ function initDictionaries() {
const { data } = await axios.get(
`/api/v1/entries/${entryId}/version-snapshots/${versionId}`
)
const payload = { entry: data }
insertTermData(entryId, payload)
const payload = { entry: data.data }
insertTermData(entryId, payload, data.author)
const isPublishedEl = payload.entry.is_published
const info = new FormData(formEditContent)
info.append('isPublishedEl', isPublishedEl)
@@ -531,6 +664,7 @@ function initDictionaries() {
info.append('isPublishedEl', isPublishedEl)
loadPreview(info)
changeVersionList(data.entry.versions)
setLatestVersion(data.entry)
ce.formEditContent.addEventListener('input', () => (unsaved = true))
formEditContent.dataset.entryId = entryId
formEditContent.action = '/api/v1/entries/update'
@@ -553,7 +687,7 @@ function initDictionaries() {
}
}
function insertTermData(entryId, data) {
function insertTermData(entryId, data, vAuthor) {
const { newConnectionBtn } = window.dictionaryElements
const localeOptions = {
day: '2-digit',
@@ -609,12 +743,20 @@ function initDictionaries() {
}
else
for (let i = 0; i < termIdText.length; i++) {
termIdText[i].textContent = `ID: Ni idja`
termIdText[i].textContent = i18next.t('ID: Ni idja')
}
ce.authorEl.textContent = data.entry.version_author
ce.versionEl.textContent = `Verzija ${data.entry.version}`
ce.previewVersion.textContent = `Verzija ${data.entry.version}`
ce.previewAuthor.textContent = data.entry.version_author
if (vAuthor) {
ce.authorEl.textContent = vAuthor
ce.previewAuthor.textContent = vAuthor
} else {
ce.authorEl.textContent = data.entry.version_author
ce.previewAuthor.textContent = data.entry.version_author
}
// ce.versionEl.textContent = `Verzija ${data.entry.version}`
ce.versionEl.textContent = i18next.t('Verzija') + `${data.entry.version}`
// ce.previewVersion.textContent = `Verzija ${data.entry.version}`
ce.previewVersion.textContent =
i18next.t('Verzija') + `${data.entry.version}`
termName.value = data.entry.term
? data.entry.term.replace(/&quot;/g, '"')
: ''
@@ -757,18 +899,19 @@ function initDictionaries() {
const payload = new URLSearchParams(new FormData(ce.formEditContent))
payload.set('entryId', entryId)
try {
saveBtn.textContent = i18next.t('Shranjujem ...')
await axios.post(ce.formEditContent.action, payload)
spinnerEl.classList.remove('d-none')
saveBtn.classList.add('saved-entry-btn')
saveBtn.textContent = 'Shranjeno'
saveBtn.textContent = i18next.t('Shranjeno')
if (id != null) entryId = id
ajaxSideMenu(entryId)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
messageContainer.textContent = message
}
@@ -776,19 +919,20 @@ function initDictionaries() {
event.preventDefault()
const payload = new URLSearchParams(new FormData(ce.formEditContent))
try {
saveBtn.textContent = i18next.t('Shranjujem ...')
const res = await axios.post(ce.formEditContent.action, payload)
spinnerEl.classList.remove('d-none')
saveBtn.classList.add('saved-entry-btn')
saveBtn.textContent = 'Shranjeno'
saveBtn.textContent = i18next.t('Shranjeno')
let entryId = res.data.entryId
if (id != null) entryId = id
ajaxSideMenu(entryId)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
messageContainer.textContent = message
} finally {
@@ -806,11 +950,11 @@ function initDictionaries() {
})
renderTerms(data, entryId)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
messageContainer.textContent = message
}
@@ -829,7 +973,7 @@ function initDictionaries() {
selectNextEntry(selectedIndex)
} catch (error) {
console.log(error)
const message = 'Prišlo je do napake.'
const message = i18next.t('Prišlo je do napake.')
messageContainer.textContent = message
}
}
@@ -870,21 +1014,27 @@ function initDictionaries() {
/&quot;/g,
'"'
)}`
} else labelElement.textContent = '[ni termina]'
} else labelElement.textContent = '[ni termina]'
} else labelElement.textContent = i18next.t('[ni termina]')
} else labelElement.textContent = i18next.t('[ni termina]')
} else {
if (el.isValid) {
if (el.isPublished) {
labelElement.textContent = sloTerm
labelElement.textContent = `${sloTerm} ${
el.homonymSort ? '(' + el.homonymSort + ')' : ''
}`
labelElement.className =
'terms-label term-good-btn btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
} else {
labelElement.className =
'terms-label btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
labelElement.textContent = sloTerm
labelElement.textContent = `${sloTerm} ${
el.homonymSort ? '(' + el.homonymSort + ')' : ''
}`
}
} else {
labelElement.textContent = sloTerm
labelElement.textContent = `${sloTerm} ${
el.homonymSort ? '(' + el.homonymSort + ')' : ''
}`
labelElement.className =
'terms-label not-valid-not-published btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
}
@@ -949,84 +1099,83 @@ function initDictionaries() {
}
}
{
const resultsListEl = document.getElementById('page-results')
const dictionaryId = document.getElementById('subareas-dict-id').value
const resultsListEl = document.getElementById('page-results')
const dictionaryId = document.getElementById('subareas-dict-id').value
const updatePager = initPagination('pagination', onPageChange)
const updatePager = initPagination('pagination', onPageChange)
async function onPageChange(newPage) {
try {
const { page, numberOfAllPages, results } = await getDataForPage(
newPage
)
removeAllChildNodes(resultsListEl)
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
}
alert(message)
updatePager()
async function onPageChange(newPage) {
try {
const results = await getDataForPage(newPage)
const numberOfAllPages = +results.headers['number-of-all-pages']
const page = +results.headers.page
removeAllChildNodes(resultsListEl)
renderResults(results.data)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
}
async function getDataForPage(page) {
const url = `/api/v1/dictionaries/${dictionaryId}/listDomainLabels?p=${page}`
const { data } = await axios.get(url)
return data
}
function renderResults(results) {
results.forEach(result => {
const rowEl = document.createElement('tr')
const input = document.createElement('input')
const th = document.createElement('th')
const input2 = document.createElement('input')
const td1 = document.createElement('td')
const td2 = document.createElement('td')
const div = document.createElement('div')
const editBtn = document.createElement('button')
const deleteBtn = document.createElement('button')
const imgEditEl = document.createElement('img')
const imgDelEl = document.createElement('img')
input.value = result.id
input.type = 'hidden'
input.name = 'domainLabelId'
th.scope = 'row'
input2.className = 'form-check checkbox-table'
input2.type = 'checkbox'
input2.name = 'isVisible'
input2.disabled = true
input2.checked = !!result.isVisible
th.append(input2)
td1.className = 'tdata-area'
td1.textContent = result.name
td2.classList.add('buttons-group')
div.classList.add('table-buttons')
editBtn.className = 'p-0 table-button-grp me-3 edit-row-btn'
editBtn.type = 'button'
imgEditEl.src = '/images/u_edit-alt.svg'
deleteBtn.className = 'p-0 table-button-grp delete-row-btn'
deleteBtn.dataset.bsTarget = '#alert-modal'
deleteBtn.dataset.bsToggle = 'modal'
deleteBtn.type = 'button'
imgDelEl.src = '/images/red-trash-icon.svg'
td2.append(div)
div.append(editBtn, deleteBtn)
editBtn.append(imgEditEl)
deleteBtn.append(imgDelEl)
rowEl.append(input, th, td1, td2)
resultsListEl.appendChild(rowEl)
})
alert(message)
updatePager()
}
}
async function getDataForPage(page) {
// const url = `/api/v1/dictionaries/${dictionaryId}/listDomainLabels?p=${page}`
const url = `/api/v1/dictionaries/${dictionaryId}/domainLabels?q=${queryBattery}&p=${page}`
// const { data } = await axios.get(url)
return await axios.get(url)
}
function renderResults(results) {
replaceContainer('page-results', results)
}
const updatePaginationOnFilter = axiosResult => {
const numberOfAllPages = +axiosResult.headers['number-of-all-pages']
const page = +axiosResult.headers.page
// removeAllChildNodes(resultsListEl)
// renderResults(results.data)
// console.log(numberOfAllPages)
// console.log(page)
// console.log(updatePager)
updatePager(page, numberOfAllPages)
}
/// / Due to unsual design, the function was moved inside
function searchController() {
if (window.location.pathname.includes('podrocne-oznake')) {
queryBattery = document.getElementById('input-search').value
// console.log(queryBattery)
axios
.get(
`/api/v1/dictionaries/${
window.location.pathname.split('/')[2]
}/domainLabels?q=${queryBattery}&p=${1}`
)
.then(result => {
updatePaginationOnFilter(result)
replaceContainer('page-results', result.data)
})
}
}
const inlineSearchButton = document.getElementById('inline-search-btn')
if (inlineSearchButton) {
inlineSearchButton.addEventListener('click', searchController)
$('#input-search').on('keyup', function (e) {
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
searchController()
}
})
}
/// //
}
if (/\/slovarji\/\d+\/napredno/.test(currentPagePath)) {
@@ -1217,8 +1366,8 @@ function mobileMoveContent() {
currentPagePath.includes('slovarji') &&
!currentPagePath.includes('admin')
)
navTitle.textContent = 'Urejanje'
else navTitle.textContent = 'Administrator'
navTitle.textContent = i18next.t('Urejanje')
else navTitle.textContent = i18next.t('Administracija')
siteHeading.style.display = 'block'
}
}
@@ -1310,9 +1459,9 @@ function createNewAreaInput(pageForm) {
divSmallNameArea.className = 'author mt-4 added-field'
divSubjectName.className = 'subject-name'
spanInputNameTxtSlo.className = 'input-name-txt'
spanInputNameTxtSlo.textContent = 'NOVO PODPODROČJE (slovensko)'
spanInputNameTxtSlo.textContent = i18next.t('NOVO PODPODROČJE (slovensko)')
spanInputNameTxtEng.className = 'input-name-txt mt-4'
spanInputNameTxtEng.textContent = 'NOVO PODPODROČJE (angleško)'
spanInputNameTxtEng.textContent = i18next.t('NOVO PODPODROČJE (angleško)')
divRow.className = 'row align-items-center'
divRow2.className = 'row align-items-center'
divEnglishInput.className = 'mt-4'
@@ -1333,11 +1482,12 @@ function createNewAreaInput(pageForm) {
divColSm.className = 'col-sm mt-3 mt-sm-0'
divColSm2.className = 'col-sm mt-3 mt-sm-0'
spanNameInfoTxt.className = 'd-sm-inline name-info-txt ms-xxl-3 ms-md-3'
spanNameInfoTxt.textContent =
spanNameInfoTxt.textContent = i18next.t(
'Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.'
)
spanNameInfoTxtEng.className =
'd-sm-inline name-info-txt ms-xxl-3 ms-md-3 mt-4'
spanNameInfoTxtEng.textContent = 'Novo podpodročje (angleško).'
spanNameInfoTxtEng.textContent = i18next.t('Novo podpodročje (angleško).')
divSmallNameArea.appendChild(divSubjectName)
divSubjectName.appendChild(spanInputNameTxtSlo)
@@ -1468,6 +1618,10 @@ function changePageContent(el) {
const collapsableBtn = document.querySelectorAll('.collapsable-entry-btn')
const addedFields = document.querySelectorAll('.added-field')
const collapsibleData = document.querySelectorAll('.collapsible-data')
const responseModal = new bootstrap.Modal(
document.getElementById('duplicate-modal'),
{ keyboard: false }
)
switch (el) {
case 'content-preview':
loadPreview(info)
@@ -1530,6 +1684,7 @@ function changePageContent(el) {
break
case 'new-entry-btn': {
// formEditContent.reset()
formEditContent.removeAttribute('data-entry-id')
formEditContent.action = '/api/v1/entries/create'
if (editSection.classList.contains('d-none'))
@@ -1546,6 +1701,16 @@ function changePageContent(el) {
previewBtnEl.disabled = true
delete newEntryBtn.dataset.term
}
// editBtnEl.click()
editBtnEl.classList.add('active-site-link')
previewBtnEl.classList.remove('active-site-link')
commentsBtnEl.classList.remove('active-site-link')
previewButtonsGroup.classList.remove('d-none')
commentsButtonsGroup.classList.add('d-none')
previewSection.classList.add('d-none')
editSection.classList.remove('d-none')
commentsSection.classList.add('d-none')
resizeFields.forEach(el => autoResize(el))
authorEl.textContent = ''
versionEl.textContent = 'Verzija 1'
previewAuthor.textContent = ''
@@ -1558,15 +1723,19 @@ function changePageContent(el) {
deleteEntry.disabled = true
changeCollapsedContent()
if (addedFields.length) addedFields.forEach(el => el.remove())
editBtnEl.click()
termInputField.focus()
const messageContainer = document.querySelectorAll('.message-container')
messageContainer.forEach(el => el.classList.add('d-none'))
window.scrollTo({ top: 0, behavior: 'smooth' })
selectedEntry.classList.remove('selected-term-btn')
$('.multiple').val(null).trigger('change')
$('.without-dropdown').val(null).trigger('change')
editBtnEl.click()
// editBtnEl.click()
// termInputField.addEventListener('focus', () =>
// showMCButtons(termInputField, 'link')
// )
termInputField.focus()
termInputField.click()
break
}
case 'first':
@@ -1610,7 +1779,6 @@ function changePageContent(el) {
commentsBtnEl.disabled = true
commentsBtnEl.classList.add('disabled')
showDates.disabled = true
duplicateEntry.disabled = true
deleteEntry.disabled = true
changeCollapsedContent()
editBtnEl.click()
@@ -1618,6 +1786,8 @@ function changePageContent(el) {
window.scrollTo({ top: 0, behavior: 'smooth' })
selectedEntry.classList.remove('selected-term-btn')
editBtnEl.click()
responseModal.toggle()
duplicateEntry.disabled = true
break
case 'true':
spanFilterText.classList.remove('normal-gray-label')
@@ -1798,8 +1968,9 @@ function loadPreview(info) {
: linksArr[i][key] === 'broader'
? 'BT:'
: ''
linkedTerms.innerHTML += keyTxt + ' ' + key + ' '
linkedTerms.innerHTML += keyTxt + ' ' + key
if (parseInt(i) !== linksArr.length - 1) linkedTerms.innerHTML += ', '
else linkedTerms.innerHTML += ' '
}
}
} else changeClasses(previewLinkedTerms, 'hide')
@@ -1924,6 +2095,7 @@ function changeVersionList(versions) {
dateLabel.dataset.bsCustomClass = 'dark-gray-tooltip'
dateLabel.dataset.bsPlacement = 'bottom'
dateLabel.dataset.bsToggle = 'tooltip'
dateLabel.dataset.bsHtml = 'true'
const date = new Date(el.version_time).toLocaleDateString(
'sl-SL',
localeOptions
@@ -1932,9 +2104,16 @@ function changeVersionList(versions) {
dateRadio.id = `date${el.version}`
dateRadio.value = `${el.version}`
dateLabel.htmlFor = `date${el.version}`
// new bootstrap.Tooltip(dateLabel, {
// title: `Verzija ${el.version} <br> Avtor: ${el.version_author}`
// })
// eslint-disable-next-line
new bootstrap.Tooltip(dateLabel, {
title: `Verzija ${el.version}`
title:
i18next.t('Verzija') +
`${el.version} <br>` +
i18next.t('Avtor:') +
`${el.version_author}`
})
allDatesEl.append(dateRadio)
allDatesEl.appendChild(dateLabel)
@@ -1942,6 +2121,30 @@ function changeVersionList(versions) {
}
}
function setLatestVersion(data) {
const latestVersionLabel = document.getElementById('latest-version-label')
// const tooltip = new bootstrap.Tooltip(latestVersionLabel, {
// title: `Verzija: ${data.version} <br> Avtor: ${data.version_author}`,
// customClass: 'dark-gray-tooltip',
// html: true,
// placement: 'bottom'
// })
// eslint-disable-next-line
const tooltip = new bootstrap.Tooltip(latestVersionLabel, {
title:
i18next.t('Verzija:') +
`${data.version} <br>` +
i18next.t('Avtor:') +
`${data.version_author}`,
customClass: 'dark-gray-tooltip',
html: true,
placement: 'bottom'
})
// Can't think of any better solution for the not disapearing label bug
const dateArea = document.querySelector('.date-scroller')
dateArea.addEventListener('click', () => tooltip.hide())
}
function removeOldMedia(images, audio, video) {
if (images !== null || audio !== null || video !== null) {
const media = [images, audio, video]
@@ -1998,7 +2201,7 @@ function showSelectedDateData(date) {
// classicOverview.classList.add('d-none')
// selectedOverview.classList.remove('d-none')
btnSaveIcon.firstChild.src = '/images/u_redo.svg'
btnSaveIcon.children[1].textContent = 'Obnovi'
btnSaveIcon.children[1].textContent = i18next.t('Obnovi')
btnSaveIcon.id = 'redo-action'
}
@@ -2038,7 +2241,7 @@ function changeCollapsedContent() {
function createHeadword() {
const { termInputField, headerwordTable } = window.dictionaryElements
if (!termInputField.value.length) alert('Vnesti morate termin')
if (!termInputField.value.length) alert(i18next.t('Vnesti morate termin'))
else {
const createHeadwordGroup = document.getElementById('create-headword-group')
const createdHeadwordGroup = document.getElementById(
@@ -2181,21 +2384,21 @@ function addField(form, element, content) {
if (formEditContent) {
switch (element) {
case newImageBtn:
spanName.textContent = 'SLIKA'
spanName.textContent = i18next.t('SLIKA')
inputField.name = 'image'
spanNameInfoTxt.textContent = 'Nova slika.'
spanNameInfoTxt.textContent = i18next.t('Nova slika.')
if (content) inputField.value = content
break
case newAudioBtn:
spanName.textContent = 'ZVOK'
spanName.textContent = i18next.t('ZVOK')
inputField.name = 'audio'
spanNameInfoTxt.textContent = 'Nov zvok.'
spanNameInfoTxt.textContent = i18next.t('Nov zvok.')
if (content) inputField.value = content
break
case newVideoBtn:
spanName.textContent = 'VIDEO'
spanName.textContent = i18next.t('VIDEO')
inputField.name = 'video'
spanNameInfoTxt.textContent = 'Nov video.'
spanNameInfoTxt.textContent = i18next.t('Nov video.')
if (content) inputField.value = content
break
}
@@ -2204,10 +2407,11 @@ function addField(form, element, content) {
element.parentElement.parentElement.parentElement
)
} else {
spanName.textContent = 'AVTOR'
spanName.textContent = i18next.t('AVTOR')
inputField.name = 'author'
spanNameInfoTxt.textContent =
'Dodajte ime in priimek naslednjega avtorja slovarja..'
spanNameInfoTxt.textContent = i18next.t(
'Dodajte ime in priimek naslednjega avtorja slovarja.'
)
form.insertBefore(
divMarginTop,
element.parentElement.parentElement.parentElement
@@ -2224,7 +2428,6 @@ function addConnectionField(form, element, data) {
const divRow = document.createElement('div')
const divColLg2 = document.createElement('div')
const divColLg4 = document.createElement('div')
const divColLg = document.createElement('div')
const colLg2Input = document.createElement('select')
const inputGroup = document.createElement('div')
const inputField = document.createElement('input')
@@ -2243,7 +2446,6 @@ function addConnectionField(form, element, data) {
divColLg2.appendChild(colLg2Input)
colLg2Input.appendChild(opt)
divRow.appendChild(divColLg4)
divRow.appendChild(divColLg)
divColLg4.appendChild(inputGroup)
inputGroup.appendChild(inputField)
inputGroup.appendChild(spanInputGroup)
@@ -2252,32 +2454,34 @@ function addConnectionField(form, element, data) {
divColSm.appendChild(spanNameInfoTxt)
divMarginTop.className = 'mt-4 added-field'
divSubjectName.className = 'subject-name'
divSubjectName.className =
'subject-name col-sm-6 d-flex justify-content-between'
spanName.className = 'input-name-txt'
divRow.className = 'row align-items-center'
divColLg2.className = 'col-lg-2 col-6'
divColLg4.className = 'col-lg-4 mt-2 mt-lg-0'
divColLg.className = 'col-lg align-items-center'
divColLg2.className = 'col-xl-2 col-4'
divColLg4.className = 'col-8 col-xl-6 col-xxl-4'
colLg2Input.className = 'name-input form-select d-inline'
colLg2Input.name = 'type'
inputGroup.className = 'input-group'
inputField.className = 'name-input d-inline form-control icon-trash mc-field'
inputField.className =
'name-input d-inline form-control icon-trash mc-field dispatch-tab'
spanInputGroup.className = 'input-group-text delete-author-btn'
spanInputGroup.id = 'trash-icon-btn'
spanInputGroup.addEventListener('click', deleteField)
imgTrashIcon.className = 'delete-author p-0'
imgTrashIcon.src = '/images/red-trash-icon.svg'
imgTrashIcon.alt = 'Delete'
divColSm.className = 'col-sm'
spanNameInfoTxt.className = 'name-info-txt mt-3'
spanName.textContent = 'POVEZAVA'
divColSm.className = 'col d-none d-xl-flex align-items-center'
spanNameInfoTxt.className = 'd-md-inline d-block name-info-txt mt-3 mt-sm-0'
spanNameInfoTxt.textContent = i18next.t('Nov povezan termin.')
spanName.textContent = i18next.t('POVEZANI TERMIN')
inputField.name = 'links'
opt.value = 'broader'
opt.text = 'Širši'
opt.text = i18next.t('Širši')
opt2.value = 'related'
opt3.value = 'narrow'
opt2.text = 'Sorodni'
opt3.text = 'Ožji'
opt2.text = i18next.t('Sorodni')
opt3.text = i18next.t('Ožji')
colLg2Input.add(opt2)
colLg2Input.add(opt)
colLg2Input.add(opt3)
@@ -2286,6 +2490,32 @@ function addConnectionField(form, element, data) {
divMarginTop,
element.parentElement.parentElement.parentElement
)
// Create mixed content buttons above input field
const mcDiv = document.createElement('div')
const supscriptBtn = document.createElement('button')
const subscriptBtn = document.createElement('button')
const supImg = document.createElement('img')
const subImg = document.createElement('img')
mcDiv.appendChild(supscriptBtn)
mcDiv.appendChild(subscriptBtn)
supscriptBtn.appendChild(supImg)
subscriptBtn.appendChild(subImg)
mcDiv.className = 'mc-buttons-group d-none'
supscriptBtn.className = 'mc-button mc-supscript'
supscriptBtn.type = 'button'
subscriptBtn.className = 'mc-button mc-subscript'
subscriptBtn.type = 'button'
supImg.src = '/images/superscript.svg'
supImg.className = 'mc-center-img'
subImg.className = 'mc-center-img'
subImg.src = '/images/subscript.svg'
divSubjectName.appendChild(mcDiv)
// eslint-disable-next-line
inputField.addEventListener('focus', () => showMCButtons(inputField, 'link'))
if (data) {
inputField.value = data.link
colLg2Input.value = data.type
@@ -2336,14 +2566,14 @@ function handleAreasClick({ target }) {
const saveButton = document.createElement('button')
saveButton.type = 'button'
cancelButton.type = 'button'
cancelButton.textContent = 'Prekliči'
cancelButton.textContent = i18next.t('Prekliči')
cancelButton.className = 'btn btn-secondary me-2'
cancelButton.style.height = '33px'
cancelButton.style.width = '105px'
cancelButton.addEventListener('click', () =>
abortEditing(newButtonGroup, tableButtons, tDataArea)
)
saveButton.textContent = 'POTRDI'
saveButton.textContent = i18next.t('POTRDI')
saveButton.type = 'button'
saveButton.className = 'btn btn-primary'
saveButton.style.height = '33px'
@@ -2666,7 +2896,7 @@ function checkLanguages() {
}
$('.summernote').summernote({
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
height: 300,
minheight: 150,
toolbar: [
@@ -0,0 +1,24 @@
/* globals axios, i18next */
const exportForm = document.forms['dictionary-export']
exportForm.addEventListener('submit', async event => {
event.preventDefault()
const payload = Object.fromEntries(new FormData(event.target))
try {
await axios.post(event.target.action, payload)
// TODO This (page refresh) is a temporary hack. Replace with proper UI updating logic.
location.reload()
} catch (error) {
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
}
})
@@ -1,11 +1,16 @@
/* global axios, initPagination, removeAllChildNodes */
/* global axios, initPagination, removeAllChildNodes, i18next, dictionaryId */
{
const selectorEl = document.getElementById('select-extraction-name')
selectorEl.addEventListener('change', () => loadCandidates(selectorEl.value))
const resultsListEl = document.getElementById('page-results')
const importFormEl = document.getElementById('import-form')
const importButtonEl = document.getElementById('import-btn')
let termCandidates
let hitsPerPage
let numberOfAllPages
importFormEl.addEventListener('submit', submitForm)
async function loadCandidates(id) {
try {
const { data } = await axios.get(
@@ -18,6 +23,8 @@
const results = getDataForFirstPage(data)
renderResults(results)
updateDemoPager(1, numberOfAllPages)
importFormEl.action = `/api/v1/dictionaries/${dictionaryId}/import-extraction/${id}`
importButtonEl.disabled = false
} catch (error) {
console.log(error)
}
@@ -64,9 +71,27 @@
tdId.textContent = sequentialCount
tdName.textContent = candidate.kanonicnaoblika
tdSize.textContent = candidate.ranking
tdDate.textContent = candidate.pogostostpojavljanja
tdDate.textContent = candidate.pogostostpojavljanja[0]
rowEl.append(tdId, tdName, tdSize, tdDate)
resultsListEl.appendChild(rowEl)
})
}
async function submitForm(event) {
event.preventDefault()
const payload = Object.fromEntries(new FormData(event.target))
try {
await axios.post(event.target.action, payload)
alert(i18next.t('SLOVAR UVOŽEN'))
} catch (error) {
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
}
}
}
@@ -0,0 +1,7 @@
const nameEl = document.getElementById('name')
nameEl.addEventListener('input', enableButton)
function enableButton() {
const disabledBtn = document.getElementById('mpbtn')
disabledBtn.classList.remove('disabled')
}
@@ -1,16 +1,20 @@
/* global axios, removeAllChildNodes, bootstrap */
/* global axios, removeAllChildNodes, bootstrap, i18next */
const fileUploadContainerEl = document.getElementById('upload-files-container')
const fileUploadForm = document.forms['upload-files']
const fileInputEl = fileUploadForm.querySelector('input[type="file"]')
const filesListEl = document.getElementById('files-list')
const dropArea = document.querySelector('.drag-area')
const dragButton = dropArea.querySelector('button')
const dragInput = dropArea.querySelector('input')
const modalSpinner = new bootstrap.Modal(
document.getElementById('modal-spinner')
)
const modalAlert = new bootstrap.Modal(document.getElementById('alert-modal'))
let file
let fileListElCount = filesListEl.childElementCount
let isUploadInProgress = false
let isDeletionInProgress = false
const existingFileNames = [...filesListEl.children].map(
fileListEl => fileListEl.querySelector('.file-name').textContent
)
dragButton.onclick = () => {
dragInput.click()
@@ -68,96 +72,216 @@ fileInputEl.addEventListener('change', submitFiles)
filesListEl.addEventListener('click', handleFileClick)
async function submitFiles() {
// TODO Lock additional submits for the duration of this function execution?
if (isUploadInProgress) return
isUploadInProgress = true
fileUploadContainerEl.hidden = true
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
const filesToUpload = []
const failedUploads = []
modalSpinner.toggle()
const succeededFileListEls = []
let didRemoveAnyFileListEls = false
for (const file of fileInputEl.files) {
const filename = file.name
const indexOfSameNamed = existingFileNames.indexOf(filename)
if (indexOfSameNamed !== -1) {
filesListEl.children[indexOfSameNamed].remove()
existingFileNames.splice(indexOfSameNamed, 1)
didRemoveAnyFileListEls = true
}
const fileListEl = createFileListEl(filename)
filesListEl.appendChild(fileListEl)
existingFileNames.push(filename)
if (file.size > MAX_FILE_SIZE) {
const failedUpload = {
filename: file.name,
filename,
message: 'File too large. Must not be over 1 GB.'
}
failedUploads.push(failedUpload)
updateFileListEl(fileListEl, { status: 'failed' })
continue
}
const payload = new FormData()
payload.set(fileInputEl.name, file)
filesToUpload.push([file, fileListEl])
}
if (didRemoveAnyFileListEls) reindexFileListEls()
for (const [file, fileListEl] of filesToUpload) {
try {
await axios.put(apiEndpointBase, payload)
const payload = new FormData()
payload.set(fileInputEl.name, file)
const { data: fileStats } = await axios.put(apiEndpointBase, payload, {
onUploadProgress: displayUploadProgress(fileListEl)
})
succeededFileListEls.push(fileListEl)
updateFileListEl(fileListEl, { status: 'success', fileStats })
} catch (error) {
const failedUpload = {
filename: file.name,
message: error.response.data
}
failedUploads.push(failedUpload)
updateFileListEl(fileListEl, { status: 'failed' })
}
}
try {
const { data: files } = await axios.get(apiEndpointBase)
updateFilesList(files)
} catch {
alert('Pri posodobljanju seznama naloženih datotek je prišlo do napake.')
}
fileInputEl.value = ''
displayFailedUploads(failedUploads)
modalSpinner.toggle()
succeededFileListEls.forEach(el => updateFileListEl(el, { status: 'done' }))
if (failedUploads.length) displayFailedUploads(failedUploads)
isUploadInProgress = false
fileUploadContainerEl.hidden = false
}
function updateFilesList(files) {
removeAllChildNodes(filesListEl)
files.forEach(({ filename, size, timeModified }, index) => {
const rowEl = document.createElement('tr')
const tdId = document.createElement('td')
const tdName = document.createElement('td')
const tdSize = document.createElement('td')
const tdDate = document.createElement('td')
const tdDelete = document.createElement('td')
tdId.textContent = index + 1
tdName.textContent = filename
tdName.className = 'filename'
tdSize.textContent = size
const formattedDate = new Date(timeModified).toLocaleDateString('sl-SL')
tdDate.textContent = formattedDate
const delBtn = document.createElement('button')
delBtn.className = 'p-0 delete-file delete-btn-table'
delBtn.type = 'button'
const deleteImg = document.createElement('img')
deleteImg.src = '/images/red-trash-icon.svg'
deleteImg.alt = 'Izbriši'
const delSpan = document.createElement('span')
delSpan.className = 'ms-2'
delSpan.textContent = 'Briši'
delBtn.appendChild(deleteImg)
delBtn.appendChild(delSpan)
tdDelete.appendChild(delBtn)
rowEl.append(tdId, tdName, tdSize, tdDate, tdDelete)
filesListEl.appendChild(rowEl)
})
function createFileListEl(filename) {
const indexEl = document.createElement('td')
indexEl.className = 'file-index'
indexEl.textContent = ++fileListElCount
const nameEl = document.createElement('td')
nameEl.className = 'file-name'
nameEl.textContent = filename
const sizeEl = document.createElement('td')
sizeEl.className = 'file-size'
const dateEl = document.createElement('td')
dateEl.className = 'file-date-modified'
const progressBarId = `progress-bar-${fileListElCount}`
const progressLabelEl = document.createElement('label')
progressLabelEl.className = 'me-1'
progressLabelEl.for = progressBarId
progressLabelEl.textContent = '0%'
const progressBarEl = document.createElement('progress')
progressBarEl.id = progressBarId
progressBarEl.className = 'upload-progress'
progressBarEl.value = 0
progressBarEl.max = 100
const lastEl = document.createElement('td')
lastEl.className = 'file-last-cell'
lastEl.append(progressLabelEl, progressBarEl)
const fileListEl = document.createElement('tr')
fileListEl.append(indexEl, nameEl, sizeEl, dateEl, lastEl)
return fileListEl
}
function updateFileListEl(fileListEl, { status, fileStats, index }) {
if (status === 'success') {
const nameEl = fileListEl.querySelector('.file-name')
nameEl.textContent = fileStats.filename
const sizeEl = fileListEl.querySelector('.file-size')
sizeEl.textContent = fileStats.size
const dateEl = fileListEl.querySelector('.file-date-modified')
const formattedDate = new Date(fileStats.timeModified).toLocaleDateString(
'sl-SL'
)
dateEl.textContent = formattedDate
const successImgEl = document.createElement('img')
successImgEl.src = '/images/valid.svg'
successImgEl.alt = i18next.t('Datoteka uspešno naložena')
const lastEl = fileListEl.querySelector('.file-last-cell')
removeAllChildNodes(lastEl)
lastEl.append(successImgEl)
} else if (status === 'done') {
const deleteImgEl = document.createElement('img')
deleteImgEl.src = '/images/red-trash-icon.svg'
deleteImgEl.alt = ''
const deleteSpanEl = document.createElement('span')
deleteSpanEl.className = 'ms-2'
deleteSpanEl.textContent = 'Briši'
const deleteButtonEl = document.createElement('button')
deleteButtonEl.className = 'p-0 delete-file delete-btn-table'
deleteButtonEl.append(deleteImgEl, deleteSpanEl)
const lastEl = fileListEl.querySelector('.file-last-cell')
removeAllChildNodes(lastEl)
lastEl.append(deleteButtonEl)
} else if (status === 'reindex') {
const indexEl = fileListEl.querySelector('.file-index')
indexEl.textContent = index + 1
} else if (status === 'failed') {
const failImgEl = document.createElement('img')
failImgEl.src = '/images/x_red.svg'
failImgEl.alt = i18next.t('Napaka pri nalaganju datoteke')
const lastEl = fileListEl.querySelector('.file-last-cell')
removeAllChildNodes(lastEl)
lastEl.append(failImgEl)
}
}
function displayFailedUploads(failedUploads) {
const modalContentEl = modalAlert._element.querySelector(
'.modal-alert-content'
)
modalContentEl.classList.remove('d-flex')
const descriptionEl = modalAlert._element.querySelector('#alert-text')
descriptionEl.textContent = i18next.t(
'Naslednje datoteke niso bile naložene zaradi navedenih razlogov:'
)
modalContentEl.querySelector('ul')?.remove()
const listEl = document.createElement('ul')
modalContentEl.append(listEl)
failedUploads.forEach(({ filename, message }) => {
const alertText = modalAlert.querySelector('#alert-text')
alertText.textContent = `NAPAKA - Ime datoteke: ${filename}, razlog: ${message}`
modalAlert.toggle()
const bulletEl = document.createElement('li')
bulletEl.textContent = `${filename} - ${message}`
listEl.appendChild(bulletEl)
})
modalAlert.toggle()
}
async function handleFileClick(e) {
if (e.target.closest('.delete-file')) {
if (isUploadInProgress) {
alert(
i18next.t('Brisanje je onemogočeno, doker se nalagajo nove datoteke.')
)
return
}
if (isDeletionInProgress) {
alert(i18next.t('Brisanje je onemogočeno, saj je eno še v procesu.'))
return
}
isDeletionInProgress = true
const fileEl = e.target.closest('tr')
const filename = fileEl.querySelector('.filename').textContent
const filename = fileEl.querySelector('.file-name').textContent
try {
await axios.delete(`${apiEndpointBase}/${filename}`)
fileEl.remove()
existingFileNames.splice(existingFileNames.indexOf(filename), 1)
reindexFileListEls()
isDeletionInProgress = false
} catch {
alert('Pri brisanju datoteke je prišlo do napake.')
alert(i18next.t('Pri brisanju datoteke je prišlo do napake.'))
}
}
}
function displayUploadProgress(fileListEl) {
const progressEl = fileListEl.querySelector('.upload-progress')
return function ({ loaded, total }) {
const percentCompleted = Math.round((loaded / total) * 100)
progressEl.setAttribute('value', percentCompleted)
progressEl.previousElementSibling.textContent = `${percentCompleted}%`
}
}
function reindexFileListEls() {
fileListElCount = filesListEl.childElementCount
for (let i = 0; i < fileListElCount; i++) {
const fileListEl = filesListEl.children[i]
updateFileListEl(fileListEl, { status: 'reindex', index: i })
}
}
@@ -16,15 +16,20 @@ async function onListClick({ target }) {
modalUseBtn.addEventListener('click', async () => {
await axios.delete(`/api/v1/extraction/${extractionId}`)
extractionEl.remove()
// TODO This (page refresh) is an unneeded workaround for an otherwise simple user friendlier solution.
location.reload()
})
} else if (target.classList.contains('btn-begin')) {
} else if (target.closest('.btn-begin')) {
const extractionEl = target.closest('.task')
const extractionId = extractionEl.dataset.id
await axios.put(`/api/v1/extraction/${extractionId}/begin`)
const responseModal = new bootstrap.Modal(
document.getElementById('begin-response')
)
const beginExtractionModalEl = document.getElementById('begin-response')
const responseModal = new bootstrap.Modal(beginExtractionModalEl)
responseModal.toggle()
// TODO This (page refresh) is a temporary hack. Replace with proper UI updating logic.
beginExtractionModalEl.addEventListener('hide.bs.modal', () =>
location.reload()
)
} else if (target.classList.contains('btn-duplicate')) {
const extractionEl = target.closest('.task')
const extractionId = extractionEl.dataset.id
@@ -1,4 +1,4 @@
/* global $, axios, bootstrap */
/* global $, axios, bootstrap, i18next */
$('.pick-multiple').select2()
$('.enter-multiple').select2({
@@ -41,8 +41,8 @@ async function handleSearch() {
}
function handleSearchError() {
const alertText = modalAlert.querySelector('#alert-text')
alertText.textContent = `NAPAKA pri iskanju`
const alertText = modalAlert._element.querySelector('#alert-text')
alertText.textContent = i18next.t('NAPAKA pri iskanju')
modalAlert.toggle()
}
@@ -32,7 +32,7 @@
tdId.textContent = sequentialCount
tdName.textContent = candidate.kanonicnaoblika
tdSize.textContent = candidate.ranking
tdDate.textContent = candidate.pogostostpojavljanja
tdDate.textContent = candidate.pogostostpojavljanja[0]
rowEl.append(tdId, tdName, tdSize, tdDate)
resultsListEl.appendChild(rowEl)
})
+6 -6
View File
@@ -1,4 +1,4 @@
/* global currentPagePath */
/* global currentPagePath, i18next */
// const currentPagePath = location.pathname
@@ -103,22 +103,22 @@ function initExtraction() {
btnStart.type = 'button'
btnImg.src = '/images/fi_arrow-right-circle.svg'
btnSpan.className = 'ms-1'
btnSpan.textContent = 'Začni'
btnSpan.textContent = i18next.t('Začni')
hr.className = 'mt-2 mb-3'
divSmFlex2.className = 'd-flex justify-content-between'
divSmFlexACMb.className = 'd-sm-flex align-items-center mb-0'
imgAlertCircle.src = '/images/alert-circle.svg'
spanNew.className = 'normal-gray ms-1'
spanNew.textContent = 'Nov'
spanNew.textContent = i18next.t('Nov')
divSmFlexACMbMe.className = 'd-sm-flex align-content-center mb-0 me-3'
divEditTask.className = 'align-items-center me-3 edit-task'
imgEditAlt.src = '/images/u_edit-alt.svg'
spanEdit.className = 'ms-1 normal-gray'
spanEdit.textContent = 'Uredi'
spanEdit.textContent = i18next.t('Uredi')
divDeleteTask.className = 'ms-3 align-items-center delete-task'
imgDeleteAlt.src = '/images/red-trash-icon.svg'
spanDelete.className = 'ms-1 normal-gray'
spanDelete.textContent = 'Briši'
spanDelete.textContent = i18next.t('Briši')
taskNewContainer.appendChild(divSmFlex)
divSmFlex.appendChild(divSmGrid)
@@ -233,7 +233,7 @@ function mobileMoveContent() {
primaryButton.style.whiteSpace = ''
}
navTitle.textContent = 'Urejanje'
navTitle.textContent = i18next.t('Urejanje')
siteHeading.style.display = 'block'
}
}
@@ -1,4 +1,4 @@
/* global axios, validator, removeJumpLogic */
/* global axios, validator, $, i18next */
/*
@@ -27,6 +27,7 @@ const listenRegisterBtn = event => {
loginHeader.className = loginHeader.className + ' d-none'
regDescription.className = regDescription.className.replace('d-none', '')
loginDescription.className = loginDescription.className + ' d-none'
clearErrorView()
}
const listenLoginBtn = event => {
@@ -38,6 +39,7 @@ const listenLoginBtn = event => {
regHeader.className = regHeader.className + ' d-none'
loginDescription.className = loginDescription.className.replace('d-none', '')
regDescription.className = regDescription.className + ' d-none'
clearErrorView()
}
regBtn.addEventListener('click', listenRegisterBtn)
@@ -106,6 +108,23 @@ function updateLoginRegisterErrorView(
}
}
function clearErrorView() {
Object.keys(registerErrorIndicatorpairs).forEach(key => {
registerErrorIndicatorpairs[key][0].style.visibility = 'hidden'
registerErrorIndicatorpairs[key][1].style.visibility = 'hidden'
})
document.querySelectorAll('.error-login-icon').forEach(e => {
e.style.visibility = 'hidden'
})
document.querySelector('.error-text').style.visibility = 'hidden'
document.getElementById('login-remember').checked = false
document.getElementById('terms-of-use').checked = false
document.getElementById('privacy-policy').checked = false
}
/*
Show error message from the label that represents the error returned from the server
Includes error X icons for login
@@ -152,14 +171,23 @@ function updateRegisterWindowOnSuccess(
footer.appendChild(document.createElement('button'))
const btn = footer.firstElementChild
btn.classList = 'btn btn-secondary text-secondary'
btn.textContent = 'ZAPRI'
btn.textContent = i18next.t('ZAPRI')
btn.ariaLabel = 'Close'
btn.dataset.bsDismiss = 'modal'
const successDescriptionPararaph = body.children[1]
successDescriptionPararaph.textContent = `Pozdravljeni ${name} ${surname},
na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu.
Povezava za potrditev je veljavna ${days} dni.`
// successDescriptionPararaph.textContent = `Pozdravljeni ${name} ${surname},
// na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu.
// Povezava za potrditev je veljavna ${days} dni.`
successDescriptionPararaph.textContent =
i18next.t('Pozdravljeni ') +
`${name} ${surname}, ` +
i18next.t(
'na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu. Povezava za potrditev je veljavna '
) +
`${days} ` +
i18next.t('dni.')
}
// login and register
@@ -199,7 +227,7 @@ function updateRegisterWindowOnSuccess(
updateLoginRegisterErrorView(
registerErrorLabels[iterator],
true,
'Prazno obvezno polje'
i18next.t('Prazno obvezno polje')
)
failFrontendValidation = true
} else {
@@ -215,7 +243,7 @@ function updateRegisterWindowOnSuccess(
updateLoginRegisterErrorView(
'error-email',
true,
'Neveljavna e-pošta'
i18next.t('Neveljavna e-pošta')
)
} else {
updateLoginRegisterErrorView('error-email', false)
@@ -226,7 +254,7 @@ function updateRegisterWindowOnSuccess(
updateLoginRegisterErrorView(
'error-password',
true,
'Geslo je prekratko'
i18next.t('Geslo je prekratko')
)
} else {
updateLoginRegisterErrorView('error-password', false)
@@ -237,7 +265,7 @@ function updateRegisterWindowOnSuccess(
updateLoginRegisterErrorView(
'error-password-repeat',
true,
'Geslo se ne ujema'
i18next.t('Geslo se ne ujema')
)
}
}
@@ -263,11 +291,11 @@ function updateRegisterWindowOnSuccess(
// event.target.messageBind.textContent = data
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
showErrorReturnedFromServer(
@@ -279,3 +307,28 @@ function updateRegisterWindowOnSuccess(
}
}
}
function registerConditions() {
// Requirement 1: Terms of Use Checkbox checked
const requirement1 = document.getElementById('terms-of-use')
// Requirement 2: Privacy Policy checked
const requirement2 = document.getElementById('privacy-policy')
const registerButton = document.getElementById('regbtnmain')
if (requirement1.checked && requirement2.checked) {
registerButton.disabled = false
} else {
registerButton.disabled = true
}
}
document
.getElementById('terms-of-use')
.addEventListener('change', registerConditions)
document
.getElementById('privacy-policy')
.addEventListener('change', registerConditions)
$('#staticBackdrop').on('hidden.bs.modal', function () {
clearErrorView()
})
@@ -154,6 +154,21 @@ function searchQuery(searchString, searchFilterDOM = {}) {
document.location.href = url
}
function searchQueryWithExistingFilters(searchString) {
const stringBuilder = `/iskanje?q=${searchString}`
const thisUrl = new URL(location)
const url = new URL(stringBuilder, location.protocol + '//' + location.host)
thisUrl.searchParams.forEach((value, key, parent) => {
// console.log(`VALUE: ${value}`)
if (!(key === 'q' || key === 'p')) {
url.searchParams.append(key, value)
}
})
document.location.href = url
}
// end search implementation functions
// search filter functions
@@ -173,7 +188,11 @@ function sbmFn(sbm) {
inputString = '*'
}
searchQuery(inputString, searchFilterDOM)
if (sbm === document.querySelector('.search-btn-a')) {
searchQuery(inputString, searchFilterDOM)
} else {
searchQueryWithExistingFilters(inputString)
}
})
}
}
+57 -19
View File
@@ -1,21 +1,42 @@
const allMixedContentFields = document.querySelectorAll('.mc-field')
allMixedContentFields.forEach(el =>
el.addEventListener('focusin', () => showMCButtons(el))
el.addEventListener('focus', () => showMCButtons(el))
)
document.addEventListener('keypress', e => {
if (e.key === 'Enter' && e.target.classList.contains('dispatch-tab')) {
const form = event.target.form
const index = Array.prototype.indexOf.call(form, e.target)
form.elements[index + 1]?.focus()
e.preventDefault()
const currentNode = e.target
const allElements = document.querySelectorAll('input, select, textarea')
const currentIndex = [...allElements].findIndex(el =>
currentNode.isEqualNode(el)
)
const targetIndex = (currentIndex + 1) % allElements.length
allElements[targetIndex].focus()
}
})
function showMCButtons(element) {
const parent = element.parentElement.parentElement.parentElement
function showMCButtons(element, linkTerm) {
let parent
if (linkTerm) {
parent = element.parentElement.parentElement.parentElement.parentElement
} else {
parent = element.parentElement.parentElement.parentElement
}
const mainContent = document.getElementById('offset-main')
const sideMenu = document.querySelector('.content-nav-content')
const btnGrp = parent.querySelector('.mc-buttons-group')
// Warning, do not touch *_*
document.addEventListener('click', function (event) {
if (mainContent.contains(event.target) || sideMenu.contains(event.target)) {
if (element !== event.target && document.activeElement !== element) {
if (!parent.contains(event.target)) {
btnGrp.classList.add('d-none')
}
}
}
})
btnGrp.classList.remove('d-none')
const btnGrpChildren = btnGrp.children
const childrenBtns = Array.from(btnGrpChildren)
@@ -29,10 +50,10 @@ function showMCButtons(element) {
{ once: true }
)
)
document.addEventListener('click', function (event) {
if (parent !== event.target && !parent.contains(event.target)) {
btnGrp.classList.add('d-none')
}
document.addEventListener('keyup', function (e) {
const selectedEl = document.activeElement
if (!parent.contains(selectedEl)) btnGrp.classList.add('d-none')
})
}
@@ -50,8 +71,9 @@ function addMixedContentInput(el, selectedEl) {
inputText.substring(0, selectedStart) +
'<b>' +
inputText.substring(selectedStart, selectedEnd) +
'</b> ' +
'</b>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 7)
}
if (el.classList.contains('mc-italic') && selectedText.length) {
@@ -59,8 +81,9 @@ function addMixedContentInput(el, selectedEl) {
inputText.substring(0, selectedStart) +
'<i>' +
inputText.substring(selectedStart, selectedEnd) +
'</i> ' +
'</i>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 7)
}
if (el.classList.contains('mc-supscript') && selectedText.length) {
@@ -68,8 +91,9 @@ function addMixedContentInput(el, selectedEl) {
inputText.substring(0, selectedStart) +
'<sup>' +
inputText.substring(selectedStart, selectedEnd) +
'</sup> ' +
'</sup>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 11)
}
if (el.classList.contains('mc-subscript') && selectedText.length) {
@@ -77,27 +101,41 @@ function addMixedContentInput(el, selectedEl) {
inputText.substring(0, selectedStart) +
'<sub>' +
inputText.substring(selectedStart, selectedEnd) +
'</sub> ' +
'</sub>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 11)
}
if (el.classList.contains('mc-hyperlink') && selectedText.length) {
selectedEl.value =
inputText.substring(0, selectedStart) +
'<link url="">' +
'<a href="">' +
inputText.substring(selectedStart, selectedEnd) +
'</link> ' +
'</a>' +
inputText.substring(selectedEnd)
}
if (el.classList.contains('mc-line-break')) {
selectedEl.value =
inputText.substring(0, selectedStart) +
'<br />' +
'<br/>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 5)
el.removeEventListener('click', () => addMixedContentInput, false)
}
}
// selectedEl.focus()
// selectedEl.setSelectionRange(selectedStart, selectedEnd)
function placeCaret(elem, caretPos) {
if (elem != null) {
if (elem.createTextRange) {
const range = elem.createTextRange()
range.move('character', caretPos)
range.select()
} else {
if (elem.selectionStart) {
elem.focus()
elem.setSelectionRange(caretPos, caretPos)
} else elem.focus()
}
}
}
@@ -27,6 +27,8 @@
return await axios.get(url)
} */
// TODO MARK FOR REVIEW WHETHER THIS METHODS ARE STILL NEEDED
function prepareQueryArrayForArrayWithIDs(page, searchQuery, filters = {}) {
const qParams = new URL(location).searchParams
qParams.set('p', page)
@@ -0,0 +1,11 @@
/* global isI18nReady */
isI18nReady.then(t => {
const alertText = document.querySelector('#alert-text')
alertText.textContent = t(
'Ali želite zbrisati ta profil. S tem bodo izbrisani vsi podatki, ki ste jih ustvarili'
)
})
const redConfirm = document.querySelector('#modal-use-btn')
redConfirm.style.backgroundColor = '#AC7171'
@@ -0,0 +1,26 @@
// Note to myself: please optimize and remove the redundacy of the code written in multiple classes to achieve the same result as this
const focusOnCompletePromtBase = queryInput => {
queryInput.focus()
queryInput.select()
}
// Tradeoff: Declare a variable for the element to reduce duplication?
const inputQuery = document.querySelector('#search-query')
if (inputQuery) {
focusOnCompletePromtBase(inputQuery)
if (window.location.pathname === '/slovarji') {
document
.querySelector('#dicts-search-btn')
.addEventListener('click', () => {
focusOnCompletePromtBase(inputQuery)
})
document
.querySelector('#search-in-filter-modal')
.addEventListener('click', () => {
focusOnCompletePromtBase(inputQuery)
})
}
}
@@ -0,0 +1,59 @@
/* global $, axios, i18next */
// Function to verify password and repeat password
function verifyPassword() {
const password = document.getElementById('reset-password').value
const repeatPassword = document.getElementById('reset-password-repeat').value
if (password !== repeatPassword) {
alert(i18next.t('Gesli se ne ujemata')) // 'Passwords do not match')
return false
}
return true
}
// Handle submit event
document
.querySelector('#reset-and-redirect')
.addEventListener('submit', event => {
event.preventDefault()
if (verifyPassword()) {
const password = document.getElementById('reset-password').value
const repeatPassword = document.getElementById(
'reset-password-repeat'
).value
const token = document.getElementById('token').value
// const email = document.getElementById('email').value
// const data = { password, repeatPassword, token, email }
axios
.post('/api/v1/users/reset-passwordPLACEHOLDER_URL', {
password,
repeatPassword,
token
})
.then(response => {
if (response.data.success) {
$('#reset-pass-info').modal('show')
// window.location = '/'
} else {
alert(response.data.message)
}
})
.catch(error => {
// Very unlikely, but can happen
alert(i18next.t('Napaka na strežniku'))
console.log(error)
// DEBUG SUCCESS DUE TO NO ENDPOINT $('#reset-pass-info').modal('show') <<- REMOVE
})
}
})
// Handle cancel event
document.querySelector('#cancel-btn').addEventListener('click', event => {
event.preventDefault()
window.location = '/'
})
// Handle redirect on success
document.querySelector('#modal-fp-info-close').addEventListener('click', () => {
window.location = '/'
})
+203 -45
View File
@@ -1,4 +1,14 @@
/* global $, bootstrap */
/* global $, bootstrap, axios, i18next, i18nextHttpBackend */
// eslint-disable-next-line no-unused-vars
const isI18nReady = i18next.use(i18nextHttpBackend).init({
lng: document.documentElement.lang ?? 'sl',
ns: 'core',
nsSeparator: false,
keySeparator: false,
...(window.inDevEnv && { saveMissing: true }),
...(!window.inDevEnv && { fallbackLng: false })
})
const windows = ['#suggestions-root', '.advanced-search-root', '.kbd-root']
@@ -302,7 +312,7 @@ function renderSuggestions(
ajustElementSettings(preElement, options)
preElement.textContent = 'Za to besedo še ni podatkov.'
preElement.textContent = i18next.t('Za to besedo še ni podatkov.')
preElement.disabled = true
preElement.classList = ['w-100']
preElement.classList += ` ${type}-sugg`
@@ -646,31 +656,53 @@ document.querySelectorAll('.loginregisterlabel').forEach(label => {
/**
* Enables pagination logic for the specified pager UI.
*
* @param {string} paginationRootElId - Pager element id.
* @param {(string|string[])} paginationRootElIds - Pager element id.
* @param {function} onPageChange - Gets called with the number of the page the user requested.
* @returns {function} - Call it with (newCurrentPage, newNumOfAllPages) to update the ui. Or without arguments, after handling a potential error.
*/
function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
const rootEl = document.getElementById(paginationRootElId)
const btnFirstPage = rootEl.querySelector('.first-page')
const btnPreviousPage = rootEl.querySelector('.previous-page')
const btnNextPage = rootEl.querySelector('.next-page')
const btnLastPage = rootEl.querySelector('.last-page')
const formEl = rootEl.querySelector('form')
const pageInputEl = formEl.querySelector('input')
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
function initPagination(paginationRootElIds, onPageChange, currentPage = 1) {
let reqLock = false
let numOfAllPages
const backwardBtnEls = []
const forwardBtnEls = []
const pageInputEls = []
const pagesCountDisplayEls = []
rootEl.addEventListener('click', handleButtonClick)
formEl.addEventListener('submit', handleFormSubmit)
if (Array.isArray(paginationRootElIds)) {
paginationRootElIds.forEach(id => initControls(id))
} else {
initControls(paginationRootElIds)
}
const allControlEls = [...backwardBtnEls, ...forwardBtnEls, ...pageInputEls]
function handleButtonClick({ target }) {
function initControls(paginationRootElId) {
const rootEl = document.getElementById(paginationRootElId)
const btnFirstPage = rootEl.querySelector('.first-page')
const btnPreviousPage = rootEl.querySelector('.previous-page')
const btnNextPage = rootEl.querySelector('.next-page')
const btnLastPage = rootEl.querySelector('.last-page')
const formEl = rootEl.querySelector('form')
const pageInputEl = formEl.querySelector('input')
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
numOfAllPages = +pagesCountDisplayEl.textContent
backwardBtnEls.push(btnFirstPage, btnPreviousPage)
forwardBtnEls.push(btnNextPage, btnLastPage)
pageInputEls.push(pageInputEl)
pagesCountDisplayEls.push(pagesCountDisplayEl)
rootEl.addEventListener('click', e => {
handleButtonClick(e, paginationRootElId)
})
formEl.addEventListener('submit', e => handleFormSubmit(e, pageInputEl))
}
function handleButtonClick({ target }, paginationRootElId) {
if (reqLock) return
const buttonEl = target.closest(`#${paginationRootElId} button`)
if (!buttonEl) return
const numOfAllPages = +pagesCountDisplayEl.textContent
if (buttonEl.classList.contains('first-page')) {
if (currentPage === 1) return
@@ -691,13 +723,13 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
}
}
function handleFormSubmit(e) {
function handleFormSubmit(e, pageInputEl) {
e.preventDefault()
if (reqLock) return
const inputValue = +pageInputEl.value
if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) {
alert('Nepravilna vrednost strani')
if (!(inputValue > 0 && inputValue <= numOfAllPages)) {
alert(i18next.t('Nepravilna vrednost strani'))
pageInputEl.value = currentPage
return
}
@@ -708,20 +740,12 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
function enableLock() {
reqLock = true
btnFirstPage.disabled = true
btnPreviousPage.disabled = true
btnNextPage.disabled = true
btnLastPage.disabled = true
pageInputEl.disabled = true
allControlEls.forEach(el => (el.disabled = true))
}
function disableLock() {
reqLock = false
btnFirstPage.disabled = false
btnPreviousPage.disabled = false
btnNextPage.disabled = false
btnLastPage.disabled = false
pageInputEl.disabled = false
allControlEls.forEach(el => (el.disabled = false))
}
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
@@ -729,23 +753,20 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
if (!newCurrentPage) return
currentPage = newCurrentPage
pageInputEl.value = newCurrentPage
pagesCountDisplayEl.textContent = newNumOfAllPages
numOfAllPages = newNumOfAllPages
pageInputEls.forEach(el => (el.value = newCurrentPage))
pagesCountDisplayEls.forEach(el => (el.textContent = newNumOfAllPages))
if (newCurrentPage === 1) {
btnFirstPage.disabled = true
btnPreviousPage.disabled = true
backwardBtnEls.forEach(el => (el.disabled = true))
} else {
btnFirstPage.disabled = false
btnPreviousPage.disabled = false
backwardBtnEls.forEach(el => (el.disabled = false))
}
if (newCurrentPage === newNumOfAllPages) {
btnNextPage.disabled = true
btnLastPage.disabled = true
forwardBtnEls.forEach(el => (el.disabled = true))
} else {
btnNextPage.disabled = false
btnLastPage.disabled = false
forwardBtnEls.forEach(el => (el.disabled = false))
}
}
@@ -923,11 +944,11 @@ if (registerSwithcButton) {
function initSelect2(querySelector, placeholder) {
$(querySelector).select2({
allowClear: true,
// allowClear: true,
placeholder: placeholder
})
$(querySelector).val(null).trigger('change')
// $(querySelector).val(null).trigger('change')
}
$(document).ready(function () {
@@ -935,7 +956,7 @@ if (registerSwithcButton) {
$('.select-search-field').select2({})
initSelect2('.select-domain-field', 'Področje')
initSelect2('.select-src-lang-field', 'Izvorni jezik')
initSelect2('.select-src-lang-field', 'Jezik iskanja')
initSelect2('.select-dest-lang-field', 'Ciljni jezik')
initSelect2('.select-dict-field', 'Slovar')
initSelect2('.select-source-field', 'Vir')
@@ -978,7 +999,7 @@ if (registerSwithcButton) {
$('.select-search-field').select2({})
initSelect2('.select-domain-field', 'Področje')
initSelect2('.select-src-lang-field', 'Izvorni jezik')
initSelect2('.select-src-lang-field', 'Jezik iskanja')
initSelect2('.select-dest-lang-field', 'Ciljni jezik')
initSelect2('.select-dict-field', 'Slovar')
initSelect2('.select-source-field', 'Vir')
@@ -1097,7 +1118,7 @@ function transferText(sideMenuText, removeOptionalBreak = false) {
const optionalBreak = document.getElementById('disposable-break')
if (document.body.clientWidth <= 1200) {
if (window.innerWidth < 1200) {
navTitle.textContent = siteHeadingTextContent
siteHeading.style.display = 'none'
if (removeOptionalBreak) {
@@ -1111,3 +1132,140 @@ function transferText(sideMenuText, removeOptionalBreak = false) {
}
}
}
function replaceContainer(id, HTMLContent) {
const el = document.getElementById(id)
if (el) {
el.innerHTML = HTMLContent
}
}
// STATE MANAGER FOR WINDOWS IN FORGOT PASSWORD
class StateManager {
constructor() {
this.state = {}
}
setState(newState) {
this.state = { ...this.state, ...newState }
}
getState() {
return this.state
}
}
class StateManagerInvoker {
constructor(stateManager, invokeFunction) {
this.stateManager = stateManager
this.invokeFn = invokeFunction
}
setState(newState) {
this.stateManager.setState(newState)
this.invoke()
}
invoke() {
this.invokeFn()
}
}
const stateManager = new StateManager()
const forgottenPasswordInvoker = new StateManagerInvoker(stateManager, () => {
function closeAllFPRelatedModals() {
$('#staticBackdrop').modal('hide')
$('#reset-pass-modal').modal('hide')
$('#reset-pass-info').modal('hide')
}
function showLoginModal() {
$('#staticBackdrop').modal('show')
}
function showResetPasswordModal() {
$('#reset-pass-modal').modal('show')
}
function showResetPasswordModalSuccessOrFail() {
$('#reset-pass-info').modal('show')
}
function apllyDescriptionAccordingToState() {
$('#alert-text').text(stateManager.getState().fpassWindowDescription)
}
closeAllFPRelatedModals()
switch (stateManager.getState().fpassWindowState) {
case ForgotPasswordState.LOGIN_SIGNUP:
showLoginModal()
break
case ForgotPasswordState.FORGOT_PASSWORD_MODAL:
showResetPasswordModal()
break
case ForgotPasswordState.FORGOT_PASSWORD_ERROR:
case ForgotPasswordState.FORGOT_PASSWORD_SUCCESS:
showResetPasswordModalSuccessOrFail()
apllyDescriptionAccordingToState()
break
default:
console.log('Unknown state')
}
})
const ForgotPasswordState = {
LOGIN_SIGNUP: 1,
FORGOT_PASSWORD_MODAL: 2,
FORGOT_PASSWORD_ERROR: 3,
FORGOT_PASSWORD_SUCCESS: 4
}
function onForgotPassword() {
forgottenPasswordInvoker.setState({
fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_MODAL
})
}
function onCancelForgotPassword() {
forgottenPasswordInvoker.setState({
fpassWindowState: ForgotPasswordState.LOGIN_SIGNUP
})
}
function onSendForgottenEmailRequest() {
axios
.post('/REPLACETHISDUMMYURL', {
usernameOrEmail: document.getElementById('forgot-pass-input').value
})
.then(res => {
// const EMAIL = 'DUMMY_EMAIL'
// State manager because we don't want to invoker twice
// it is required to update description first before updating state of the invoker
stateManager.setState({
fpassWindowDescription: i18next.t(
'Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.'
)
})
forgottenPasswordInvoker.setState({
fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_SUCCESS
})
})
.catch(err => {
stateManager.setState({
fpassWindowDescription: i18next.t(
'Prišlo je do napake pri pošiljanju sporočila na vaš elektronski naslov. Poskusite ponovno.'
)
})
forgottenPasswordInvoker.setState({
fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_ERROR
})
console.log(err)
})
}
$('#modal-fp-cancel-btn').on('click', onCancelForgotPassword)
$('#modal-fp-use-btn').on('click', onSendForgottenEmailRequest)
$('#modal-fp-info-close').on('click', () => $('#reset-pass-info').modal('hide'))
$('#forgotten-password').on('click', onForgotPassword)
@@ -1,4 +1,5 @@
/* global $, axios, initPagination, removeAllChildNodes, closeUtilityContainers, prepareQueryArrayForArrayWithIDs, getDataFromURLSearchParams, prepareQueryParams, focusOnCompletePrompt */
/* global $, axios, initPagination, removeAllChildNodes, closeUtilityContainers, prepareQueryArrayForArrayWithIDs,
getDataFromURLSearchParams, prepareQueryParams, focusOnCompletePrompt, i18next */
{
/*
@@ -117,11 +118,11 @@
return page
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
+45 -301
View File
@@ -1,10 +1,15 @@
/* global $, axios, currentPagePath, initPagination, removeAllChildNodes, transferText, tooltipTriggerList, tooltipList, reinitalizeDefaultTooltipSet, createTooltip */
/* global $, axios, currentPagePath, initPagination,
removeAllChildNodes, transferText, tooltipTriggerList,
tooltipList, createTooltip, resetTooltipTriggerList,
transferTextExtended, i18next */
// position correction functions
function adjustOffsetBy() {
// const { offsetMain, fixedTopSection } = window.dictionaryElements
const BROWSER_UNUSUAL_OFFSET = 17 // computer from chrome
const offsetMain = document.querySelector('#offset-main')
const fixedTopSection = document.querySelector('#fixed-top-section')
@@ -14,7 +19,7 @@ function adjustOffsetBy() {
// const adminNavMobile = document.getElementsByClassName('admin-nav')
const headerPadding = document.getElementById('header-padding')
if (document.body.clientWidth < 1200) {
if (document.body.clientWidth + BROWSER_UNUSUAL_OFFSET < 1200) {
if (offsetHeaderPadding !== null) offsetMain.style.paddingTop = `0px`
} else {
for (let i = 0; i < offsetHeader.length; i++) {
@@ -80,8 +85,14 @@ function mobileMoveContent() {
'.header-container-divider-right'
)
// let actualInnerWidth = $("body").prop("clientWidth"); // El. width minus scrollbar width
// let actualInnerWidth = $("body").prop("scrollWidth"); // El. width minus scrollbar width
// const BROWSER_UNUSUAL_OFFSET = 17 // computer from chrome
// console.log($('body').innerWidth() + BROWSER_UNUSUAL_OFFSET)
try {
if (document.body.clientWidth <= 1200) {
if (window.innerWidth < 1200) {
if (secondaryButton) {
mobileRightHolder.appendChild(secondaryButton)
// secondaryButton.style.height = '28px'
@@ -98,9 +109,11 @@ function mobileMoveContent() {
primaryButton.style.whiteSpace = 'nowrap'
}
navTitle.textContent = siteHeadingTextContent
siteHeading.style.display = 'none'
// siteHeading.style.display = 'none'
siteHeading.style.display = 'inline' // I know this is hacky, but sadly due to design this is mandatory in order to fix the bug
}
if (document.body.clientWidth > 1200) {
if (window.innerWidth >= 1200) {
if (secondaryButton) {
headerContainerRight.appendChild(secondaryButton)
secondaryButton.style.height = ''
@@ -117,7 +130,7 @@ function mobileMoveContent() {
primaryButton.style.whiteSpace = ''
}
navTitle.textContent = 'Urejanje'
navTitle.textContent = i18next.t('Urejanje')
siteHeading.style.display = 'block'
}
} catch (e) {}
@@ -128,7 +141,11 @@ if (resultsListEl) {
resultsListEl.addEventListener('click', onResultClick)
}
const initialPage = +new URL(location).searchParams.get('p') || 1
const updatePager = initPagination('pagination', onPageChange, initialPage)
const updatePager = initPagination(
['pagination-top', 'pagination-bottom'],
onPageChange,
initialPage
)
async function onPageChange(newPage) {
const receivedPageNumber = await changePage(newPage)
@@ -146,7 +163,16 @@ async function changePage(newPage) {
removeAllChildNodes(resultsListEl)
renderResults(resultsMarkup)
updatePager(page, numberOfAllPages)
reinitalizeDefaultTooltipSet()
// reinitalizeDefaultTooltipSet()
// console.log($('[data-toggle="tooltip"]'))
// $('[data-toggle="tooltip"]').tooltip()
// toltip reinitialization after moving a page
resetTooltipTriggerList()
if (tooltipTriggerList.length > 0) {
// initialize tooltips
tooltipList(null, 'dark-gray-tooltip') // ADD for tooltip debug in the end -> [0].show()
}
/*
try {
@@ -160,12 +186,12 @@ async function changePage(newPage) {
return page
} catch (error) {
// console.log(error)
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
return // return to bypass error caused (assumed) by popstate on iOS/MacOS
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -210,119 +236,10 @@ function onResultClick(e) {
try {
if (tooltipTriggerList.length > 0) {
// initialize tooltips
tooltipList(null, 'gray-tooltip') // ADD for tooltip debug in the end -> [0].show()
tooltipList(null, 'dark-gray-tooltip') // ADD for tooltip debug in the end -> [0].show()
}
} catch (e) {}
/*
// refresh: bool
function largeStringsOnSmallScreen(alwaysRefresh) {
let headwordTexts
let translationWords
let synonymWords
function init() {
headwordTexts = [...$('.rihw')]
translationWords = [...$('.term-h')].map(e => {
return e.children[0]
})
synonymWords = [...$('.syn-h'), ...$('.risy')]
}
function setupToolTips() {
setDynamicTooltipList([])
if (headwordTexts) {
headwordTexts.map(ttElement =>
createTooltip(ttElement, ttElement.innerHTML, '')
)
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
}
if (translationWords) {
translationWords.map(ttElement =>
createTooltip(ttElement, ttElement.innerHTML, '')
)
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
}
if (synonymWords) {
synonymWords.map(ttElement =>
createTooltip(ttElement, ttElement.innerHTML, '')
)
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
}
// console.log(dynamicTooltipList)
}
if (!headwordTexts || !translationWords || !synonymWords || alwaysRefresh) {
init()
}
function initTooltips() {
try {
// if (dynamicTooltipList.length || alwaysRefresh) {
setupToolTips()
// }
} catch (e) {}
}
function removeTooltips() {
try {
$('.rihw').tooltip('dispose')
$('.term-h').children().tooltip('dispose')
$('.syn-h').tooltip('dispose')
$('.risy').tooltip('dispose')
} catch (e) {}
}
function manageResize() {
const tmpTerms = [...$('.term-h')].map(e => {
return e.children[0]
})
const elements = [...tmpTerms, ...$('.syn-h')]
// console.log(document.documentElement.clientWidth)
const dcw = document.documentElement.clientWidth
const checkBorders = (dcw > 991 && dcw < 1600) || dcw < 370
if (checkBorders) {
const TEXT_LIMIT = 17
elements.forEach(el => {
el.dataset.textStore = el.innerHTML.includes('...')
? el.dataset.textStore
: el.innerHTML
el.innerHTML = `${el.innerHTML.slice(0, TEXT_LIMIT)}${
el.innerHTML.length > TEXT_LIMIT ? '...' : ''
}`
})
initTooltips()
} else {
elements.forEach(el => {
if (el.dataset.textStore) {
el.innerHTML = el.dataset.textStore
}
})
removeTooltips()
}
}
return {
init: function () {
init()
},
refresh: function () {
init()
try {
setupToolTips()
} catch (e) {}
},
manageResize: manageResize
}
}
const textsScreenManager = largeStringsOnSmallScreen(true)
textsScreenManager.manageResize()
window.onresize = textsScreenManager.manageResize
*/
// refresh: bool
function largeStringsOnSmallScreen(alwaysRefresh) {
let headwordTexts
@@ -336,30 +253,6 @@ function largeStringsOnSmallScreen(alwaysRefresh) {
synonymWords = [...$('.syn-h'), ...$('.risy')]
}
/*
function setupToolTips() {
setDynamicTooltipList([])
if (headwordTexts) {
headwordTexts.map(ttElement =>
createTooltip(ttElement, ttElement.innerHTML, '')
)
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
}
if (translationWords) {
translationWords.map(ttElement =>
createTooltip(ttElement, ttElement.innerHTML, '')
)
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
}
if (synonymWords) {
synonymWords.map(ttElement =>
createTooltip(ttElement, ttElement.innerHTML, '')
)
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
}
// console.log(dynamicTooltipList)
} */
if (!headwordTexts || !translationWords || !synonymWords || alwaysRefresh) {
init()
}
@@ -379,169 +272,20 @@ function largeStringsOnSmallScreen(alwaysRefresh) {
ttElement.tooltip('dispose')
} catch (e) {}
}
/*
function initTooltips() {
try {
// if (dynamicTooltipList.length || alwaysRefresh) {
setupToolTips()
// }
} catch (e) {}
}
function removeTooltips() {
try {
$('.rihw').tooltip('dispose')
$('.term-h').children().tooltip('dispose')
$('.syn-h').tooltip('dispose')
$('.risy').tooltip('dispose')
} catch (e) {}
}
*/
/* let OLD_TEXT_LIMIT = 5000 // Guard to not prevent overdisposing
function manageResize() {
const tmpTerms = [...$('.term-h')].map(e => {
return e.children[0]
})
const elements = [...tmpTerms, ...$('.syn-h')]
// console.log(document.documentElement.clientWidth) */
/*
$('.word-constraint').each((i, element) => {
// WRONG const wordBr = $(`.word-constraint:nth-child(${i}) > .word-breakable`)
const wordList = $(element).find('.word-breakable')
wordList.each((i, wordElement) => {
// console.log(wordElement.innerText)
if (
wordElement.clientWidth > element.clientWidth &&
wordElement.innerText.split(' ').length <= 1
) {
console.log('Lion')
createTooltip($(wordElement))
wordElement.classList.push('text-ellipsis')
} else {
removeTooltip($(wordElement))
// console.log(wordElement.classList)
wordElement.className = Array.from(wordElement.classList)
.filter(e => e !== 'text-ellipsis')
.join(' ')
// console.log(wordElement.classList)
}
})
// console.log(wordBr)
})
*/
/* const dcw = document.documentElement.clientWidth
// const checkBorders = (dcw > 420 && dcw < 750) || dcw < 370
let borderIndex
// check _common.scc long-text-strip and make sure the widths are aligned
if (dcw < 420) {
borderIndex = 0 // width = 8 chars (<420)
} else if (dcw < 750 || (dcw > 990 && dcw < 1750)) {
borderIndex = 1 // width = 12 chars (420 - 750, 990-1750)
} else if (dcw > 1750) {
borderIndex = 3 // width = 100% (>1759)
} else {
borderIndex = 2 // width = 25 chars (750-990)
}
let TEXT_LIMIT
if (borderIndex === 0) {
TEXT_LIMIT = 8
} else if (borderIndex === 1) {
TEXT_LIMIT = 12
} else if (borderIndex === 2) {
TEXT_LIMIT = 25
} else {
TEXT_LIMIT = null
}
if (OLD_TEXT_LIMIT !== TEXT_LIMIT) {
elements.forEach(el => {
// console.log(el.innerHTML)
if (TEXT_LIMIT && el.innerHTML.length > TEXT_LIMIT) {
console.log(`DEBUG TEXT LIMIT: ${TEXT_LIMIT}`)
console.log(`DEBUG EL LEN: ${el.innerHTML.length}`)
console.log(`DEBUG EL TEXT: ${el.innerHTML}`)
console.log('TOOLTIP CREATED')
createTooltipEl(el, 'gray-tooltip')
} else {
removeTooltip($(el))
}
})
}
OLD_TEXT_LIMIT = TEXT_LIMIT */
/*
if (checkBorders) {
console.log('HEYOO')
const TEXT_LIMIT = 17
elements.forEach(el => {
el.dataset.textStore = el.innerHTML.includes('...')
? el.dataset.textStore
: el.innerHTML
el.innerHTML = `${el.innerHTML.slice(0, TEXT_LIMIT)}${
el.innerHTML.length > TEXT_LIMIT ? '...' : ''
}`
})
initTooltips()
} else {
elements.forEach(el => {
if (el.dataset.textStore) {
el.innerHTML = el.dataset.textStore
}
})
removeTooltips()
}
*/
/* }
return {
init: function () {
init()
},
refresh: function () {
init()
},
manageResize: manageResize
} */
}
// const textsScreenManager = largeStringsOnSmallScreen(true)
// textsScreenManager.manageResize()
// window.onresize = textsScreenManager.manageResize
/*
function transferText(sideMenuText) {
const siteHeading = document.getElementById('site-heading')
const siteHeadingTextContent = siteHeading.textContent
const navTitle = document.getElementById('nav-title')
if (document.body.clientWidth <= 1200) {
navTitle.textContent = siteHeadingTextContent
siteHeading.style.display = 'none'
} else {
navTitle.textContent = sideMenuText
}
}
*/
window.addEventListener('load', () => {
$('[data-toggle="tooltip"]').tooltip()
})
function handleProperTextDisplay() {
// const BROWSER_UNUSUAL_OFFSET = 17
if (/\/iskanje/.test(currentPagePath)) {
transferText('Iskanje po slovarjih', true)
transferText('Iskanje po slovarjih', true, 'site-heading') //,
// BROWSER_UNUSUAL_OFFSET
// )
} else if (/\/termin/.test(currentPagePath)) {
transferText('', true)
transferText('', true, 'site-heading') // , BROWSER_UNUSUAL_OFFSET)
}
}
+6 -6
View File
@@ -1,4 +1,4 @@
/* global $, currentPagePath */
/* global $, currentPagePath, i18next */
// const currentPagePath = location.pathname
@@ -60,13 +60,13 @@ function initSelect() {
const userPassword2 = ce.userConfirmationPassEl.value
if (userPassword1 === '') {
alert('Prosim, vnesite geslo')
alert(i18next.t('Prosim, vnesite geslo'))
} else if (userPassword2 === '') {
alert('Prosim, ponovno vnesite geslo')
alert(i18next.t('Prosim, ponovno vnesite geslo'))
} else if (userPassword1 !== userPassword2) {
alert('Gesli se ne ujemata')
alert(i18next.t('Gesli se ne ujemata'))
} else {
alert('Gesli se ujemata!')
alert(i18next.t('Gesli se ujemata!'))
}
}
}
@@ -115,7 +115,7 @@ function deleteField(ele) {
// Summernote
$('.summernote').summernote({
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
height: 300,
minheight: 150,
toolbar: [
@@ -1,4 +1,4 @@
/* global axios */
/* global axios, i18next */
/*
TODO
@@ -8,6 +8,8 @@ not just language filter, but filter for many properties...
/** BEGIN VARIABLE CONSTRUCTION AREA */
let allAggregationDataForSF = null
// TODO insert all sections of ids
const ids = [
'src-lang-side',
@@ -176,6 +178,10 @@ function cleanOldList() {
modalListRoot.innerHTML = ''
}
function setTitleOnModal(title) {
document.getElementById('modal-filter-label').innerHTML = title
}
async function selectMoreListener(event) {
const idstring = `${event.currentTarget.id
.replace('select-', '')
@@ -183,12 +189,17 @@ async function selectMoreListener(event) {
.concat('-side')}`
// console.log(extractMap(idstring))
const titleText =
event.currentTarget.parentElement.parentElement.children[0].textContent
setTitleOnModal(titleText)
// mp is the array of the currently displayed filters (required to get the checked information)
// const mp = initializedValues[idstring]
// all filters for search
// const allAggregationData = { id: idstring }
const allAggregationData = { id: idstring }
allAggregationDataForSF = { id: idstring }
const currentURL = new URL(window.location.href)
const searchParams = currentURL.searchParams
@@ -200,10 +211,10 @@ async function selectMoreListener(event) {
// const parsed = JSON.parse(event.currentTarget.dataset.aggregationInfo ?? '[]')
allAggregationData.items = data[idToSearchFiltersMapper(idstring)]
allAggregationDataForSF.items = data[idToSearchFiltersMapper(idstring)]
cleanOldList()
initModalList(allAggregationData.id, allAggregationData.items)
initModalList(allAggregationDataForSF.id, allAggregationDataForSF.items)
}
function idToSearchFiltersMapper(inpt) {
@@ -297,6 +308,18 @@ function finishSelectingFromModal(e) {
window.location.href = url
}
function finishSelectingFromModal2(e) {
const url = new URL(window.location.href)
const searchParams = url.searchParams
searchParams.delete(queryOrderedArr[sectionIDBattery])
selectedIds[sectionIDBattery].forEach(e => {
searchParams.append(queryOrderedArr[sectionIDBattery], e)
})
window.location.href = url
}
function reRenderSpecific(event) {
const idstring = event.currentTarget.id
const id = idstring.replace('show-all-', '')
@@ -349,7 +372,7 @@ function rePlotAndCheck(sectionId, selectedIdsArray, plotInverse = false) {
const img = document.createElement('img')
img.setAttribute('src', '/images/chevron-left-blue.svg')
const span = document.createElement('span')
span.innerHTML = 'PRIKAŽI VSE'
span.innerHTML = i18next.t('PRIKAŽI VSE')
anchr.appendChild(img)
anchr.appendChild(span)
@@ -378,6 +401,7 @@ function clearModalFilters() {
e.children[0].children[0].checked = false
})
selectedIds[sectionIDBattery] = []
clearSnapshots()
}
function clearModalFiltersWID(id) {
@@ -402,7 +426,7 @@ function initializeClickables() {
.addEventListener('click', selectFromModal)
document
.querySelector('#sf-modal-button')
.addEventListener('click', finishSelectingFromModal)
.addEventListener('click', finishSelectingFromModal2)
document.querySelector('#ccf').addEventListener('click', clearModalFilters)
@@ -428,6 +452,40 @@ function initializeClickables() {
// initModalList('id1', langsMapModal)
function filterModalEntries(event) {
cleanOldList()
// console.log(allAggregationDataForSF.items)
const prompt = document.getElementById('filterSfmText')
let filtered
if (prompt) {
filtered = allAggregationDataForSF.items.filter(item =>
item.name.toLowerCase().includes(prompt.value.toLowerCase())
)
} else {
return
}
initModalList(allAggregationDataForSF.id, filtered)
}
const sfmFilterButton = document.getElementById('filter-sfm')
const sfmOnEnterListener = document.querySelector('.onEnterListener')
if (sfmFilterButton) {
sfmFilterButton.addEventListener('click', filterModalEntries)
}
if (sfmOnEnterListener) {
sfmOnEnterListener.addEventListener('keyup', event => {
if (event.key === 'Enter') {
filterModalEntries(event)
}
})
}
/// global run
initializeClickables()
+14 -11
View File
@@ -70,20 +70,23 @@ function generateTooltipWithStyles(
) {
let nonEmptyContent
if (!content) {
nonEmptyContent = el => {
el.getAttribute('data-tooltip-content')
}
return generateTooltipBase(list, function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, {
html: true,
title: tooltipTriggerEl.getAttribute('data-tooltip-content') || '',
customClass: styles
})
})
} else {
nonEmptyContent = content
}
return generateTooltipBase(list, function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, {
html: true,
title: nonEmptyContent,
customClass: styles
return generateTooltipBase(list, function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, {
html: true,
title: nonEmptyContent || '',
customClass: styles
})
})
})
}
}
function generateTooltipBase(list, fn) {
+3 -3
View File
@@ -1,4 +1,4 @@
/* global axios */
/* global axios, i18next */
function autoGrow(element) {
element.style.height = '5px'
@@ -156,14 +156,14 @@ function editRowBodyForConsultancy(fieldInfo) {
fieldInfo.newButtonGroup = newButtonGroup
fieldInfo.cancelButton = cancelButton
fieldInfo.saveButton = saveButton
cancelButton.textContent = 'Prekliči'
cancelButton.textContent = i18next.t('Prekliči')
cancelButton.className = 'btn btn-secondary me-2'
cancelButton.style.height = '33px'
cancelButton.style.width = '105px'
cancelButton.addEventListener('click', () =>
abortConsultancyUserEditing(fieldInfo)
)
saveButton.textContent = 'POTRDI'
saveButton.textContent = i18next.t('POTRDI')
saveButton.className = 'btn btn-primary'
saveButton.style.height = '33px'
saveButton.style.width = '105px'
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+710
View File
@@ -0,0 +1,710 @@
{
" Terminološke odgovore pripravljajo sodelavci Terminološke sekcije Inštituta za slovenski jezik Frana Ramovša ZRC SAZU (in so objavljeni tudi na spletišču <a href='https://isjfr.zrc-sazu.si/sl/terminologisce/svetovanje'>Terminologišče</a>), ki pri delu upoštevajo osnovna terminološka načela.": " Answers to terminology questions are prepared by the employees of the Fran Ramovš Institute of the Slovenian Language at the Research Centre of the Slovenian Academy of Sciences and Arts (the answers are also published on their <a href='https://isjfr.zrc-sazu.si/sl/terminologisce/svetovanje'>Terminology</a> website). When addressing the questions, the consultants take into account the basic terminology principles.",
"(STOP) Termini": "(STOP) Terms",
"[ni termina]": "[no term]",
"#": "#",
"<b>Odgovor:</b>": "<b>Answer:</b>",
"Administracija": "Administration",
"Administrator": "Administrator",
"Administratorska konzola": "Admin console",
"Administratorski del portala je namenjen skupini oseb, ki vsebinsko in tehnično ureja portal, odpira slovarske vire, daje pooblastila posameznim uporabnikom in skrbi za vsebinsko in tehnično urejenost portala. Izberite module, ki jih boste ponudili na terminološkem portalu in na kratko opišite, kaj ponuja vaš portal.": "The administrative functions of the portal is intended for the group of people who edit the content and the technical aspects of the portal, publish dictionaries, give authorizations to other individual users and are responsible for the content and the technical aspects of the portal.",
"Aktivacija računa": "Account activation",
"Ali res želite izbrisati vnos? Dejanja ni mogoče razveljaviti.": "Are you sure you want to delete the entry? This action is cannot be undone.",
"Ali želite indeksirati?": "Do you want to index?",
"Ali želite izbrisati slovarski sestavek?": "Do you want to delete the dictionary entry?",
"Ali želite objaviti vsa gesla?": "Do you want to publish all entries?",
"Ali želite zbrisati ta profil. S tem bodo izbrisani vsi podatki, ki ste jih ustvarili": "Do you want to delete this profile? This action will also delete all data you created.",
"Ali želite zbrisati ta vnos?": "Do you want to delete this entry?",
"angleščina": "English",
"ANGLEŠKI NASLOV SLOVARJA *": "DICTIONARY TITLE IN ENGLISH *",
"ANGLEŠKI OPIS PORTALA": "PORTAL DESCRIPTION IN ENGLISH",
"ANGLEŠKO IME PORTALA": "PORTAL NAME IN ENGLISH",
"Angleško ime terminološkega portala.": "English name of the terminology portal.",
"Avtor": "Author",
"AVTOR": "AUTHOR",
"AVTOR SLOVARJA": "DICTIONARY AUTHOR",
"Avtor:": "Author:",
"Avtorja": "Authors",
"Avtorji": "Authors",
"Avtorji mnenja:": "Opinion authors:",
"Basic": "Basic",
"Besedila": "Texts",
"Besedilo gre tukaj": "The text goes here",
"Brisanje je onemogočeno, doker se nalagajo nove datoteke.": "Deletion is disabled while new files are being uploaded.",
"Brisanje je onemogočeno, saj je eno še v procesu.": "Deletion is disabled because previous deletion has not yet been completed.",
"Brisanje slovarja": "Delete the dictionary",
"Brisanje slovarskih sestavkov": "Delete dictionary entries",
"Briši": "Delete",
"BRIŠI": "DELETE",
"Briši uporabnika": "Delete user",
"Če imate pripravljen slovar v enem od formatov, ki so navedeni spodaj, lahko svoje podatke uvozite.": "If you have a dictionary in one of the formats listed below, you can import your data.",
"Če izberete to možnost, se bodo vsi doslejšnji slovarski sestavki ob uvozu nove datoteke izbrisali.": "If you select this option, all previous dictionary entries will be deleted when the new file is imported.",
"Če želite dodati novo luščenje, morate najprej pobrisati vsaj eno od obstoječih, saj je na posameznega uporabnika dovoljenih največ 5 luščenj.": "If you want to add a new extraction, you must first delete at least one of your current extraction, as a maximum of 5 extractions are allowed per user.",
"Celotni naslov slovarja v angleščini.": "Full title of the dictionary in English.",
"Celotni naslov slovarja, ki bo zabeležen v bibliografskih podatkih.": "The full dictionary title to be recorded in the bibliographic information.",
"Ciljni jeziki": "Target languages",
"Čim bolj natančno opišite terminološki problem, zlasti opišite vsebino pojma.": "Describe the terminological problem as precisely as you can, especially the concept the term should represent.",
"DATOTEKA": "FILE",
"Datoteka uspešno naložena": "File uploaded successfully",
"DATUM": "DATE",
"Datum objave/spremembe": "Date of publication/modification",
"Datum vprašanja:": "Question date:",
"Definicija": "Definition",
"DEFINICIJA": "DEFINITION",
"DEFINICIJA:": "DEFINITION:",
"Delete profile": "Delete profile",
"Deli": "Share",
"DELI": "SHARE",
"dni.": "days.",
"do številke": "and",
"Dodaj": "Add",
"DODAJ": "ADD",
"Dodaj besedilo za luščenje terminoloških kandidatov iz lastnega specializiranega korpusa. Ko boste dodali vsa besedila, ki ste jih izbrali, morate izbiro shraniti.": "Add text to extract term candidates from your own specialized corpus. After adding all selected texts, save your selection.",
"Dodaj datoteko": "Add file",
"Dodaj komentar...": "Add a comment...",
"Dodaj odgovor...": "Add a reply...",
"Dodaj opravilo": "Add a task",
"Dodaj povezani termin.": "Add a related term.",
"Dodaj povezavo": "Add link",
"Dodaj slovar": "Add a dictionary",
"Dodajte ime in priimek naslednjega avtorja slovarja.": "Add first and last name of the next dictionary author.",
"Dodajte ime in priimek naslednjega avtorja slovarja..": "Add first and last name of the next dictionary author..",
"Dodajte leto izida besedil, ki jih želite luščiti. Lahko dodate več posameznih let. Če boste polje pustili prazno, bodo vključena vsa leta. Če bo besedil preveč, boste morali omejiti izbiro.": "Add the year of publication of the texts to be used for extraction. You can add several years. If the field is left empty, all years will be included. If this results in too many results, you will need to limit your selection.",
"Dodajte podatek o zunajjezikovnih okoliščinah, ki niso povezane s pojmom, npr. letnico.": "Add information about extra-linguistic circumstances unrelated to the concept, e.g. ",
"Dodajte povezavo do slike.": "Link an image.",
"Dodajte povezavo do videa.": "Link a video.",
"Dodajte povezavo do zvočnega posnetka.": "Link an audio file.",
"Dodajte termin, ki je sicer definiran v samostojnem slovarskem sestavku, vendar je povezan s terminom, ki ga opisujete v tem slovarskem sestavku.": "Add a term that is defined in a separate dictionary entry but is related to the term you are describing in this dictionary entry.",
"Dodajte terminološki odgovor in ga utemeljite.": "Add a terminological answer and justify it.",
"Dodajte tujejezične ustreznike, ki se za opisani pojem tudi uporabljajo v tujem jeziku.": "Add foreign language equivalents that are also used in a foreign language for the described concept.",
"Dodajte tujejezični ustreznik.": "Add a foreign language equivalent.",
"Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\">Več...</a>": "You can add a list of words that you do not want on the list of term candidates. The list should be saved in a .txt format. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\">Read more...</a>",
"Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. Na koncu morate spremembe shraniti. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\" target=\"_blank\">Več ...</a>": "You can add a list of words that you do not want on the list of term candidates. The list should be saved in a .txt format. Save all changes after you are done. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\" target=\"_blank\">Read more...</a>",
"Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. Vse spremembe morate shraniti. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\">Več...</a>": "You can add a list of words that you do not want on the list of term candidates. The list should be saved in a .txt format. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\">Read more...</a>",
"Dodaten avtor.": "Additional author.",
"DODATNI POVEZANI TERMIN": "ADDITIONAL RELATED TERM",
"Dodeli": "Assign",
"DODELI": "ASSIGN",
"Določite lastnosti terminoloških virov na portalu, zlasti minimalno število sestavkov, ki so pogoj za objavo, možnosti potrjevanja objave novih slovarjev, število različic slovarja, ki jih lahko hrani posamezni uporabnik. Te nastavitve veljajo za vse terminološke vire na portalu.": "Define the characteristics of the terminology resources on the portal, specifically the minimal number of entries required to publish a dictionary, the options for approving the publication of new terminology dictionaries, as well as the number of dictionary versions and exports that can be saved by individual users. These settings apply to all terminology resources on the portal.",
"Domači slovarji": "Local dictionaries",
"Drugo": "Other",
"DRUGO": "OTHER",
"DRUGO:": "OTHER:",
"Dvočrkovna oznaka terminološkega portala.": "Two-letter label of the terminology portal.",
"E - naslov": "E-mail address",
"E-naslov": "E-mail address",
"E-NASLOV": "E-MAIL ADDRESS",
"E-pošta": "E-mail",
"ELEKTRONSKI NASLOV": "E-MAIL ADDRESS",
"Elektronski naslov trenutno prijavljenega uporabnika.": "E-mail address of the currently logged-in user.",
"Elektronski naslov že obstaja": "The e-mail address already exists",
"EVROPSKI SKLAD ZA REGIONALNI RAZVOJ": "EUROPEAN REGIONAL DEVELOPMENT FUND",
"FAZA UREJANJA": "EDITING PHASE",
"Faze urejanja": "Editing phases",
"Filtri": "Filters",
"Filtriranje": "Filter",
"FORMAT ZAPISA": "FILE FORMAT",
"Gesli se ne ujemata": "Passwords do not match",
"Gesli se ujemata!": "Passwords match!",
"Geslo": "Password",
"GESLO": "PASSWORD",
"Geslo je prekratko": "The password is too short",
"Geslo je prekratko.": "The password is too short.",
"Geslo se ne ujema": "Incorrect password",
"Geslo se ne ujema.": "Incorrect password.",
"Hrvaščina": "Croatian",
"Hvala za poslano vprašanje, ki je bilo posredovano svetovalcem terminološkega portala. Odgovor boste prejeli na e-naslov, ki ste ga navedli ob registraciji.": "Thank you for submitting the question, which was forwarded to the terminology portal consultants. The answer will be sent to the e-mail address you used for registration.",
"Hvala za poslano vprašanje, ki je bilo posredovano v Terminološko svetovalnico ZRC SAZU. Odgovor boste prejeli na e-naslov, ki ste ga navedli ob registraciji.": "Thank you for submitting the question, which was forwarded to the Terminological Counselling of the ZRC SAZU. The answer will be sent to the e-mail address you used for registration.",
"ID Slovarja": "Dictionary ID",
"ID: Ni idja": "ID: No ID",
"IDEKSIRAJ": "INDEX",
"Imate neshranjene spremebe. Ali jih želite shraniti?": "Your changes have not been saved. Do you want to save them?",
"Imate neshranjene spremembe. Ali jih želite shraniti?": "Your changes have not been saved. Do you want to save them?",
"Ime": "First name",
"IME": "FIRST NAME",
"IME DATOTEKE": "FILE NAME",
"Ime in opis": "Name and description",
"IME IN PRIIMEK": "FIST AND LAST NAME",
"Ime in priimek trenutno prijavljenega uporabnika.": "First and last name of the currently logged-in user.",
"IME LUŠČENJA": "EXTRACTION NAME",
"IME PORTALA": "PORTAL NAME",
"IME PORTALA *": "PORTAL NAME *",
"Ime Priimek": "First Name Last Name",
"Ime registriranega uporabnika.": "Name of rhe registered user.",
"Ime slovarja": "Dictionary name",
"Ime, s katerim se uporabnik predstavlja na terminološkem portalu.": "The name with which the user presents himself on the terminology portal.",
"Indeksiranje slovarja": "Index dictionary",
"Institucija": "Institution",
"INSTITUCIJA": "INSTITUTION",
"Institucija, kjer trenutno prijavljeni uporabnik deluje.": "The institution where the currently logged-in user works.",
"Interni": "Internal",
"Išči": "Search",
"Išči po slovarju": "Search the dictionary",
"ISKANI NIZ JE BIL NAJDEN": "THE SEARCH STRING WAS FOUND",
"Iskanje": "Search",
"Iskanje po slovarjih": "Search the dictionaries",
"Iskanje po vseh": "Search all",
"ISSN": "ISSN",
"ISSN OZNAKA": "ISSN CODE",
"ISSN oznaka.": "ISSN code.",
"Italijanščina": "Italian",
"IZBERI": "SELECT",
"Izberi datoteko": "Select a file",
"IZBERI VEČ": "SELECT MORE",
"Izberite enega od rezultatov luščenja s seznama.": "Select one of the extraction results from the list.",
"Izberite format datoteke, v kateri je slovar shranjen na vašem računalniku.": "Select the file format of the dictionary saved on your computer.",
"Izberite format izpisa terminološkega slovarja.": "Select the file format to export the terminology dictionary.",
"Izberite luščenje": "Select extraction",
"Izberite področje": "Select an domain",
"Izberite področje iz seznama področij, da zmanjšate obseg besedil, iz katerih bo potekalo luščenje.": "Select a domain from the the list of domains to limit the scope of texts used for extraction.",
"Izberite področje svojega terminološkega slovarja na seznamu področij.": "Select the domain of your dictionary from the list of domains.",
"Izberite slovarske podatke, ki ste jih shranili na svojem računalniku.": "Select the dictionary data you saved on your computer.",
"Izberite takšno ime, ki vam bo pomagalo slediti rezultatom, če boste luščenje besedil opravili večkrat.": "Choose a name that will help you keep track of the results if you decide to run several extractions.",
"Izberite vrsto dokumenta iz seznama, npr. članek, diplomsko delo.": "Select the document type from the list, e.g. article, graduate thesis.",
"Izberite vse tuje jezike, ki jih bo terminološki slovar vseboval.": "Select all foreign languages you want to include into the terminology dictionary.",
"Izberite, ali želite izpisati samo jezikovno pregledane slovarske sestavke.": "Choose whether you want to list only linguistically reviewed dictionary entries.",
"Izberite, ali želite izpisati samo objavljene ali tudi neobjavljene slovarske sestavke.": "Choose whether you want to list only published or unpublished dictionary entries.",
"Izberite, ali želite izpisati samo strokovno pregledane slovarske sestavke.": "Choose whether you want to list only terminologically reviewed dictionary entries.",
"Izberite, ali želite izpisati vse slovarske sestavke ali samo tiste, ki so v določeni fazi urejanja.": "Choose whether you want to list all dictionary entries or only those in a certain editing phase.",
"Izberite, katere slovarske sestavke želite izpisati glede na njihovo veljavnost.": "Choose which dictionary entries you want to list based on their validity.",
"Izbrišete lahko celoten slovar z vsemi metapodatki. Dejanja ni mogoče preklicati.": "You can delete the entire dictionary with all metadata. The action is irreversible.",
"Izbrišete lahko vse doslej obdelane slovarske sestavke in ohranite vse metapodatke o slovarju. Dejanja ni mogoče preklicati.": "You can delete all dictionary entries you have edited until now and keep all dictionary metadata. The action is irreversible.",
"Izbriši": "Delete",
"IZBRIŠI": "DELETE",
"Izbriši obstoječe slovarske sestavke.": "Delete existing dictionary entries.",
"Izbriši polje": "Remove the field",
"Izbriši račun": "Delete account",
"Izgovor": "Pronunciation",
"Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.": "Fill in the fields and briefly describe the contents of the terminology dictionary.",
"Izvoz": "Export",
"Izvozi": "Export",
"IZVOZI": "EXPORT",
"Izvozi termine od številke": "Export entries between the numbers",
"je že v tabeli.": "is already in the table.",
"Jeziki": "Languages",
"Jeziki iskanja": "Search languages",
"Jezikovni pregled": "Language review",
"Jezikovno pregledano": "Linguistically reviewed",
"KANONIČNA OBLIKA": "CANONICAL FORM",
"KLJUČNE BESEDE": "KEY WORDS",
"Ko bo luščenje uspešno zaključeno, boste na svoj elektronski naslov, ki ste ga navedli ob registraciji, prejeli obvestilo.": "After the extraction is successfully completed, you will receive a notification to the email address that you provided during registration.",
"Ko boste končali z urejanjem svojega terminološkega slovarja, lahko objavite vse slovarske sestavke, ki bodo postali vidni vsem uporabnikom. Vaše dejanje mora potrditi še administrator portala.": "After you finish editing your terminology dictionary, you can publish all the dictionary entries, which will become visible to all users. Your action must be approved by the portal administrator.",
"komentar": "comment",
"komentarja": "comments",
"komentarjev": "comments",
"komentarji": "comments",
"Komentarji": "Comments",
"Končan": "Finished",
"Konec": "Finish",
"Kontrola objave": "Publication control",
"KONTROLA OBJAVE": "PUBLICATION CONTROL",
"Korpus OSS": "OSS Corpus",
"Lastna svetovalnica": "Your own consultancy",
"Lastni dokumenti": "Your own documents",
"Lastnosti": "Properties",
"LETO": "YEAR",
"Luščenje": "Extraction",
"Luščenje | dodaj opravilo": "Extraction | add a task",
"Luščenje iz korpusa besedil OSS": "Extraction from the texts of the OSS corpus",
"Luščenje iz lastnih besedil": "Extraction from your own texts",
"Luščenje iz lastnih besedil | dodaj opravilo ": "Extraction from your own texts | add a task",
"Luščenje je bilo dano v fazo obdelave.": "The extraction was sent to the processing phase.",
"Luščenje končano": "Extraction completed",
"Luščenje terminoloških kandidatov iz besedil, ki jih ima uporabnik shranjena pri sebi. Za boljšo učinkovitost svetujemo format .txt.": "Extracting term candidates from texts that the user has stored on his own devices. We recommend using .txt file format for better results.",
"Luščenje terminoloških kandidatov iz besedil, ki jih uporabnik izbere med vsemi besedili, vključenimi v <a class=\"gray-info\" href=\"https://openscience.si/\" target=\"_blank\">Nacionalni portal odprte znanosti</a>.": "Extracting term candidates from texts selected by the user among all texts included in the <a class=\"gray-info\" href=\"https://openscience.si/\" target=\"_blank\">National Open Science Portal</a>.",
"Luščenje terminoloških kandidatov iz besedil, ki jih uporabnik namensko izbere, je najučinkovitejše. Primerno izbrano ime opravila, vam omogoča, da lahko sledite, katera luščenja ste že opravili. Ko vnesete podatke, morate vse spremembe shraniti. Zaradi omejenega prostora za shranjevanje lahko shranite največ pet zadnjih luščenj. ": "Extracting term candidates from texts purposefully selected by the user is more effective. Using a well-chosen name for the task helps you track which extractions have already been performed. After entering the data, make sure to save all changes. Due to limited storage space, only the last five extractions will be saved.",
"Luščenje terminoloških kandidatov iz besedil, ki so že predhodno oblikoslovno označena, nudi dobre rezultate, vendar je treba nabor besedil omejiti. Svetujemo vam, da zoožite področje in dodatno omejite izbiro s tipi besedil in časovnim razponom, v katerih so nastala. Ko vnesete podatke, morate vse spremembe shraniti. Po vnosu podatkov, s katerimi boste omejili nabor izbranih besedil, morate pritisniti gumb Najdi. Izbiro boste shranili lahko le v primeru, da ne bo število najdenih dokumentov preveliko.": "Extracting term candidates from texts annotated with linguistic metadate provides good results, but the selection of texts must be limited. We recommend narrowing down the domain and selecting the text types and year of publication to further limit the scope. Make sure to save all changes after entering the data. After filling in the data to limit the text selection, click the Search button. You will only be able to save the selection if the number of found texts does not exceed the limit.",
"MAKSIMALNO ŠTEVILO KOPIJ SLOVARSKIH SESTAVKOV": "MAXIMUM NUMBER OF DICTIONARY ENTRY VERSIONS",
"Meni": "Menu",
"MENI": "MENU",
"MINIMALNO ŠTEVILO SLOVARSKIH SESTAVKOV": "MINIMUM NUMBER OF DICTIONARY ENTRIES",
"Modul za luščenje terminoloških kandidatov iz besedil.": "Module for extracting term candidates from texts.",
"Modul za svetovanje pri terminoloških zagatah.": "Terminology consulting module.",
"Modul za urejanje terminoloških slovarjev.": "Module for editing terminology dictionaries.",
"MODULI": "MODULES",
"Moj Profil": "My Profile",
"Moji slovarji": "My dictionaries",
"Morebitne že obstoječe poimenovalne rešitve, če obstajajo.": "Any already existing naming solutions.",
"Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.": "Any examples of term use in texts or links to text where the term is used.",
"MOŽNOST OBJAVE SLOVARSKEGA SESTAVKA MED UREJANJEM": "OPTION OF PUBLISHING THE DICTIONARY ENTRY WHILE EDITING",
"MULTIMEDIJA": "MULTIMEDIA",
"Na kratko opišite zasnovo in namen slovarja.": "Briefly describe the composition and purpose of the dictionary.",
"Na seznamu uporabnikov lahko določite posameznemu uporabniku dodatne vloge ali urejate njihove podatke.": "You can assign additional roles or edit user data from all users on the list.",
"Na tem mestu lahko dodajate svetovalce, ki so registrirani uporabniki terminološkega portala. Če boste pripisali področje, boste lahko svetovalcu dodeljevali samo vprašanja, ki sodijo na izbrano področje, vsem drugim pa vsa.": "Here you can add consultants who are registered users of the terminology portal. If you assign them a domain, you will only be able to assign them questions related to this domain, otherwise you can assign them all questions.",
"Na tem mestu lahko določite področne oznake, če želite posamezne termine v svojem terminološkem slovarju razvrstiti še podrobneje.": "Here you can define domain labels, if you want to classify individual terms in your terminology dictionary in more detail.",
"Na tem mestu so zbrani vsi komentarji, povezani s terminološkim portalom.": "All comments related to the terminology portal can be found here.",
"Na tem mestu so zbrani vsi komentarji, povezani z izbranim terminološkim slovarjem.": "All comments related to the selected terminology dictionary can be found here.",
"na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu. Povezava za potrditev je veljavna ": "we a message with a link to verify your Termonology Portal user account to your e-mail address. The verification link is valid.",
"Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.": "We sent a password reset link to your e-mail address. Please, check your inbox.",
"Nabor vseh luščenj, s katerimi lahko uporabnik iz izbranih besedil pridobi sezname terminoloških kandidatov. Posamezne terminološke kandidate lahko preveri tudi v konkordančniku.": "All extractions the users can use to obtain a list of term candidates from the selected texts. The user can also check individual term candidates using the concordance tool.",
"Nabor vseh luščenj, s pomočjo katerih uporabnik iz vhodnih besedil pridobi sezname terminoloških kandidatov in dostop do konkordančnika po teh besedilih.": "All extractions the users can use to obtain a list of term candidates from the selected texts and access to the concordance tools for these texts.",
"Nabor vseh slovarjev, ki jih uporabnik lahko ureja kot glavni urednik ali pa ima dodeljeno pravico urejanja, pregledovanja, popravljanja ipd.": "All dictionaries where the user is the main editor or has the editing, reviewing, correcting, etc. rights.",
"NAČIN UVOZA": "IMPORT METHOD",
"Naglas": "Accent",
"Najdi": "Search",
"Naložbo sofinancirata Republika Slovenija in Evropska unija iz Evropskega sklada za regionalni razvoj": "The investment is co-financed by the Republic of Slovenia and the EU via the European Regional Development Fund",
"Napačno geslo.": "Wrong password.",
"Napaka": "Error",
"Napaka na strežniku": "Server error",
"NAPAKA pri iskanju": "Search ERROR",
"Napaka pri nalaganju datoteke": "Error loading file",
"NAPAKA V UVOZU": "IMPORT ERROR",
"Napredno": "Advanced",
"Napredno urejanje omogoča spreminjanje večjega števila podatkov v terminološkem slovarju.": "Advanced editing allows the user to change more data in the terminology dictionary.",
"Naprej": "Next",
"Naslednje datoteke niso bile naložene zaradi navedenih razlogov:": "The following files were not uploaded due to the following reasons:",
"Naslov": "Address",
"NASLOV": "ADDRESS",
"NASLOV SLOVARJA *": "DICTIONARY TITLE *",
"Nastavitve": "Settings",
"Nastavitve portala": "Portal settings",
"Nastavitve računa": "Account settings",
"Nastavitve slovarjev": "Dictionaries settings",
"Nastavitve svetovalnice": "Consultancy settings",
"Nazaj": "Back",
"Nazaj na prijavo": "Back to login",
"Nazaj na zadetke": "Back to results",
"Ne": "No",
"Ne izbriši": "Do not delete",
"Ne morete imeti več kot 5 luščenj!": "You cannot have more than 5 extractions!",
"Ne shrani": "Do not save",
"nedefinirano": "undefined",
"Nemško-slovenski slovar tehnike in naravoslovja": "German-Slovenian dictionary of technology and natural sciences",
"Neobjavljeni": "Unpublished",
"Nepravilna vrednost strani": "Invalid page value",
"Nepravilno uporabniško ime, elektronski naslov ali geslo.": "Incorrect username, e-mail address or password.",
"Nepregledani": "Not reviewed",
"Neveljavna e-pošta": "Invalid e-mail",
"Neveljavni": "Invalid",
"Neveljavni elektronski naslov.": "Invalid e-mail address",
"Ni področja": "No domain",
"Ni še dodanih uvozov.": "No imports have been added.",
"Ni še povezanih portalov": "No portals have been connected",
"Ni slovarskih sestavkov za urejanje.": "There are no dictionary entries to edit.",
"Ni vprašanj.": "No questions.",
"Ni zadetkov": "No results",
"Nimate izbranega področja CERIF.": "No CERIF domain selected.",
"Nimate luščenj za urejanje.": "No extractions to edit.",
"Nimate povezav za urejanje.": "No linked portals to edit.",
"Nimate slovarjev za urejanje.": "No dictionaries to edit.",
"Nimate ustreznih pravic": "You do not have the required rights",
"Niste izbrali glavnega področja.": "Primary domain missing.",
"Niste vnesli področnih oznak.": "Domain labels missing.",
"Niste vpisali angleškega naslova slovarja.": "Dictionary title in English missing.",
"Niste vpisali imena": "First name missing.",
"Niste vpisali imena slovarja.": "Dictionary name missing.",
"Niste vpisali naslova slovarja.": "Dictionary title missing.",
"Niste vpisali opisa terminološkega problema.": "Terminology problem description missing.",
"Niste vpisali priimka.": "Last name missing.",
"Niste vpisali tujih jezikov.": "Foreign languages missing.",
"Nov": "New",
"Nov avtor": "New author",
"NOV AVTOR SLOVARJA": "NEW DICTIONARY AUTHOR",
"Nov povezan termin.": "New related term.",
"Nov slovar": "New dictionary",
"NOV UPORABNIK": "NEW USER",
"Nov video": "New video",
"NOV VIDEO": "NEW VIDEO",
"Nov video.": "New video.",
"Nov zvok": "New audio file",
"NOV ZVOK": "NEW AUDIO FILE",
"Nov zvok.": "New audio file.",
"Nova povezava": "New link",
"Nova slika": "New image",
"NOVA SLIKA": "NEW IMAGE",
"Nova slika.": "New image.",
"Novo": "New",
"NOVO GESLO": "NEW PASSWORD",
"Novo podpodročje": "New secondary domain",
"NOVO PODPODROČJE": "NEW SECONDARY DOMAIN",
"NOVO PODPODROČJE (angleško)": "NEW SECONDARY DOMAIN (English)",
"Novo podpodročje (angleško).": "New secondary domain (English).",
"NOVO PODPODROČJE (slovensko)": "NEW SECONDARY DOMAIN (Slovenian)",
"Novo terminološko vprašanje": "New terminology question",
"Novo vprašanje": "New question",
"Novo vprašanje za Terminološko svetovalnico": "New question for the Terminological Consulting",
"O slovarju": "About the dictionary",
"O terminu": "About the term",
"Objava med urejanjem": "Publish while editing",
"Objava terminološkega vprašanja": "Publish a terminology question",
"Objava vseh slovarskih sestavkov": "Publish all dictionary entries",
"Objavi": "Publish",
"OBJAVI": "PUBLISH",
"Objavljeni": "Published",
"Objavljeno": "Published",
"Oblike": "Forms",
"Oblike, naglasi, izgovor": "Forms, accents, pronunciation",
"Obljavljeno": "Published",
"Obnovi": "Restore",
"Obstoječe poimenovalne rešitve": "Existing naming solutions",
"OBSTOJEČE POIMENOVALNE REŠITVE": "EXISTING NAMING SOLUTIONS",
"Obvestilo": "Notification",
"Obvestilo o številu gesel": "Notification about the number of entries",
"Odgovor": "Answer",
"Odgovori": "Answer",
"Odgovori na vprašanja:": "Answers to the questions:",
"Odjava": "Log out",
"odprt": "published",
"Odprt": "Published",
"Odstrani": "Remove",
"Omogočeno": "Enabled",
"OPIS PORTALA": "PORTAL DESCRIPTION",
"OPIS SLOVARJA": "DICTIONARY DESCRIPTION",
"Opis terminološkega problema": "Description of the terminology problem",
"OPIS TERMINOLOŠKEGA PROBLEMA": "DESCRIPTION OF THE TERMINOLOGY PROBLEM",
"Opis terminološkega problema:": "Description of the terminology problem:",
"Opis terminološkega problema: ": "Description of the terminology problem: ",
"Osnovne nastavitve": "Basic settings",
"Osnovni podatki": "Basic data",
"Ožji": "Narrow",
"Oznaka portala": "Portal label",
"OZNAKA PORTALA *": "PORTAL LABEL *",
"Po koncu urejanja vseh slovarskih sestavkov lahko slovar objavite in ga tako prikažete na javnem delu terminološkega portala.": "After all dictionary entires are edited, you can publish the dictionary to make it available on the public part of the terminology portal.",
"Počisti filtre": "Remove filters",
"POČISTI FILTRE": "REMOVE FILTERS",
"Podnaslov": "Subtitle",
"Podpodročja": "Secondary domains",
"Podpodročja so namenjena podrobnejšemu razvrščanju terminoloških slovarjev. Administrator mora vsa novo predlagana podpodročja potrditi, preden jih lahko na seznamu vidijo tudi drugi uporabniki portala.": "Secondary domains are used for more detailed classification of terminology dictionaries. All new suggested secondary domains must be approved by the Portal Administrator before other portal users can see them on the list.",
"Podpodročje": "Secondary domain",
"PODPODROČJE": "SECONDARY DOMAIN",
"Podpodročje.": "Secondary domain.",
"Področja": "Domains",
"Področje": "Domain",
"PODROČJE": "DOMAIN",
"PODROČJE *": "DOMAIN *",
"Področje, v katero sodi opisani terminološki problem.": "Domain of the described terminology problem.",
"Področje.": "Domain.",
"Področna oznaka": "Domain label",
"PODROČNA OZNAKA": "DOMAIN LABEL",
"PODROČNA OZNAKA:": "DOMAIN LABEL:",
"Področne oznake": "Domain labels",
"PODROČNE OZNAKE": "DOMAIN LABELS",
"Podvoji": "Duplicate",
"PODVOJI": "DUPLICATE",
"Poiščite naslove terminoloških slovarjev, s katerimi želite dopolniti iskalne zadetke na svojem portalu. Lahko izberete vse slovarje ali le nekatere. Svoje izbire morate shraniti. Vse vaše izbire mora potrditi administrator povezanega portala.": "Find the titles of terminology dictionaries you want to add to the search results on your portal. You can select all the dictionaries or only a few. You have to save your selection. Your selection must be approved by the administrator of the linked portal.",
"Pojasnilo": "Qualifier",
"POJASNILO": "QUALIFIER",
"POJASNILO:": "QUALIFIER:",
"POJAVITVE": "APPEARANCES",
"Poleg objave slovarskega sestavka v fazi \"Urejeno\" je omogočena tudi objava v fazi \"V urejanju\".": "In addition to the publication of dictionary entries in the \"Edited\" phase, they can also be published while \"In editing\" phase.",
"Polja naslov, vprašanje in mnenje so obvezna!": "The Title, Question and Opinion fields are mandatory!",
"Polno ime terminološkega portala.": "Full name of the terminology portal.",
"Pomoč": "Help",
"Ponastavi geslo": "Reset password",
"Ponovi Geslo": "Repeat password",
"PONOVI GESLO": "REPEAT PASSWORD",
"PONOVI NOVO GESLO": "REPEAT NEW PASSWORD",
"Portal": "Portal",
"Portal lahko povežete s Terminološko svetovalnico ZRC SAZU in tako prikazujete terminološke odgovore na svojem portalu, lahko pa vklopite lastno svetovalnico. V tem primeru morate med registriranimi uporabniki izbrati svetovalce in urednika svetovalnice, ki bodo odgovarjali na terminološka vprašanja uporabnikov.": "You can connect your portal to the ZRC SAZU Terminological counselling and show their terminology answers on your portal, or you can activate your own consultancy service. In the latter case, you have to select the consultants and the consultancy service editor who will answer terminology questions of your portal users. The consultants and consultancy service editor must be registered users.",
"Portali": "Portals",
"Pošlji": "Send",
"Potrdi": "Confirm",
"POTRDI": "CONFIRM",
"Potrditev objave": "Confirm publication",
"Potrjen": "Confirmed",
"Povezani slovarji": "Linked dictionaries",
"Povezani termin": "Related term",
"POVEZANI TERMIN": "RELATED TERM",
"POVEZANI TERMIN:": "RELATED TERM:",
"POVEZAVA": "LINK",
"Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.": "The link is (no longer) valid. Please, request new password reset link.",
"Povezave": "Links",
"Povezave s portali": "Links to other portals",
"Pozabljeno geslo": "Forgoten password",
"Pozdravljeni ": "Hello ",
"Pozor": "Attention",
"Prazno obvezno polje": "Mandatory field is empty",
"Preden naložite besedila, jih shranite v besedilni obliki (končnica .txt).": "Before uploading texts, save them in text format (.txt file).",
"Predlog": "Suggestion",
"Predogled": "Preview",
"Pregledani": "Reviewed",
"Prekinjen": "Interrupted",
"Prekliči": "Cancel",
"Preverite, če ste pravilno vpisali uporabniško ime. Bodite pozorni na velike in male črke.": "Check if you entered the username correctly. Pay attention to capital letters.",
"Prevod": "Translation",
"Pri brisanju datoteke je prišlo do napake.": "An error occurred during file deletion.",
"Priimek": "Last name",
"PRIIMEK": "LAST NAME",
"Priimek registriranega uporabnika.": "Last name of the registered user.",
"Prijava": "Login",
"PRIJAVA": "LOGIN",
"Prijava uspešna": "Login successful",
"Prikaži datume": "Show dates",
"Prikaži filtre": "Show filters",
"PRIKAŽI VSE": "SHOW ALL",
"Primeri rabe": "Examples of use",
"PRIMERI RABE V BESEDILIH": "EXAMPLES OF USE IN TEXTS",
"Pripravljeno": "Ready",
"Prišlo je do napake pri pošiljanju sporočila na vaš elektronski naslov. Poskusite ponovno.": "An error occurred while sending the message to your e-mail address. Please, try again.",
"Prišlo je do napake.": "An error has occurred.",
"Prišlo je do strežniške napake. Poskusite kasneje.": "A server error has occurred. Please, try again later.",
"Prosim, ponovno vnesite geslo": "Please re-enter your password",
"Prosim, vnesite geslo": "Please enter your password",
"RAČUN": "ACCOUNT",
"Razumem": "I understand",
"registracija": "registration",
"Registracija": "Registration",
"Registracija uporabnika je potrjena.": "User registration confirmed.",
"Registracija uspešna": "Registration successful",
"Registrirani uporabniki lahko dobijo različne vloge na portalu. Kot administrator jim lahko dodelite tudi vlogo skrbnika slovarjev in/ali skrbnika svetovalnice.": "Registered users on the portal can have different roles. As Portal Administrator you can assign them the role of Dictionary Administrator and/or Consultancy Administrator.",
"REPUBLIKA SLOVENIJA MINISTRSTVO ZA KULTURO": "REPUBLIC OF SLOVENIA, MINISTRY OF CULTURE",
"rezultat": "result",
"Rezultat luščenja so terminološki kandidati. Z uvozom rezultatov posameznega luščenja lahko dopolnite geslovnik terminološkega slovarja, ki ga urejate.": "Extraction results in a list of term candidates. You can import the results of individual extractions to add the term candidates to the list of terms in the dictionary you are editing.",
"rezultata": "results",
"rezultati": "results",
"Rezultati: ": "Results: ",
"rezultatov": "results",
"S ključnimi besedami, ki so vključene v večino znanstvenih in strokovnih, lahko bolj natančno izberete besedila, ki jih želite uporabiti za luščenje terminoloških kandidatov.": "Most of the scientific and specialized texts have keywords you can use for a more precise selection of texts from which to extract term candidates.",
"S prijavo pridobite možnost uporabe vseh funkcij terminološkega portala.": "By registering, you get access to all functions of the terminology portal.",
"S tem dejanjem boste izbrisali vse slovarske sestavke v slovarju in vse metapodatke. Dejanja ni mogoče razveljaviti.": "This action will delete all dictionary entries in the dictionary and all metadata. The action cannot be undone.",
"S tem dejanjem boste izbrisali vse slovarske sestavke v slovarju. Metapodatki bodo ostali. Dejanja ni mogoče razveljaviti.": "This action will delete all dictionary entries in the dictionary. The metadata will not be deleted. The action cannot be undone.",
"Še niste registriran uporabnik, registrirajte se.": "You are not a registered user. Please, register.",
"Settings": "Settings",
"Seznam": "List",
"Seznam luščenj": "List of exctractions",
"Seznam neželenih terminov": "List of unwanted terms",
"Seznam objavljenih vprašanj.": "List of published questions.",
"Seznam povezav": "List of linked portals",
"Seznam slovarjev": "List of dictionaries",
"Seznam slovarjev s povezanega portala.": "List of dictionaries from the linked portal.",
"Seznam svetovalcev": "List of consultants",
"Seznam terminoloških kandidatov": "List of term candidates",
"Seznam terminoloških kandidatov bo natančnejši, če boste dodali tudi seznam neželenih besed, ki naj jih luščilnik izloči iz seznama. Seznam lahko vsebuje splošne termine, npr. tabela, kazalnik, ali pogoste slovnične besede, zlasti veznike, pomožne glagole.": "The list of term candidates will be more accurate if you also add a list of unwanted words that the extractor should exclude from the list of term candidates. This list of unwanted words should include general terms, such as table or indicator, and commonly used grammatical words, such as conjunctions and auxiliary vers.",
"Seznam terminoloških kandidatov, ki so rezultat izbranega luščenja.": "List of term candidates resulting from the selected extraction.",
"Seznam uporabnikov": "List of users",
"Seznam urejenih vprašanj, ki čakajo na potrditev dokončne objave.": "List of edited questions waiting for publication approval.",
"Seznam vprašanj, ki so jih moderatorji zavrnili in jih je treba dodeliti nekomu drugemu.": "List of questions rejected by moderators that should be assigned to someone else.",
"Seznam vprašanj, ki so jih poslali uporabniki, in še niso bila dodeljena moderatorjem.": "List of questions sent by the users waiting to be assigned to moderators.",
"Seznam vprašanj, ki so še v urejanju.": "List of questions that are still being edited.",
"Seznam vseh slovarjev, ki jih uredniki, sicer registrirani uporabniki, urejajo na tem portalu.": "List of all dictionaries being edited on this portal by editors, who are also registered users.",
"Seznam vseh slovarjev, ki so na tem portalu na voljo uporabnikom.": "List of all dictionaries available to users on this portal.",
"Seznam zadetkov": "List of results",
"Shemo za format xml si prenesete <a href=\"/slovarji/xml-schema\" target=\"_blank\">tukaj</a>.": "You can download the xml format scheme <a href=\"/slovarji/xml-schema\" target=\"_blank\">here</a>.",
"Shrani": "Save",
"Shranjeno": "Saved",
"Shranjujem ...": "Saving...",
"Sinhroniziraj": "Synchronize",
"Sinonim": "Synonym",
"SINONIM:": "SYNONYM:",
"Sinonimi": "Synonyms",
"SINONIMI": "SYNONYMS",
"Širši": "Broad",
"SKRAJŠANI NASLOV SLOVARJA": "SHORT DICTIONARY TITLE",
"Skrbnik portala": "Portal administrator",
"Skrbnik slovarjev": "Dictionary administrator",
"Skrbnik svetovalnice": "Consultancy administrator",
"Skrbniki portala": "Portal administrators",
"slika": "image",
"Slika": "Image",
"SLIKA": "IMAGE",
"SLIKA:": "IMAGE:",
"Slovar": "Dictionary",
"Slovar je bil izbrisan.": "The dictionary has been deleted.",
"Slovar je odprt.": "The dictionary is published.",
"Slovar je pregledal jezikoslovec.": "The dictionary was reviewed by a linguist.",
"Slovar je pregledal področni strokovnjak.": "The dictionary has been reviewed by a terminology expert.",
"Slovar je v odpiranju - čaka na potrditev skrbnika slovarjev": "The dictionary is being published - waiting for approval from the dictionary administrator",
"Slovar odprt": "Dictionary is published",
"SLOVAR UVOŽEN": "DICTIONARY IMPORTED",
"Slovarji": "Dictionaries",
"Slovarji portala": "Dictionaries on the portal",
"Slovarski sestavek je neveljaven": "The dictionary entry is invalid",
"Slovarski sestavek je pregledan in dokončan.": "The dictionary entry has been reviewed and completed.",
"Slovarski sestavek je v fazi, ko se dodajajo in oblikujejo elementi, ki ste jih izbrali v strukturi slovarskega sestavka.": "The dictionary entry is in the editing phase, in which the elements of the dictionary entry structure are being added and edited.",
"Slovarski sestavki so bili izbrisani.": "Dictionary entries have been deleted.",
"Slovarski sestavki so bili objavljeni.": "Dictionary entries have been published.",
"SLOVARSKIH SESTAVKOV": "DICTIONARY ENTRIES",
"Slovenščina": "Slovenian",
"Soglašam s <a target=\"_blank\" href=\"/politika-zasebnosti\">politiko zasebnosti</a>": "I agree with the <a target=\"_blank\" href=\"/politika-zasebnosti\">Privacy policy</a>",
"Sorodni": "Similar",
"Sprememba stanja slovarja": "Dictionary status change",
"Spremeni geslo": "Change password",
"SPREMENJEN": "CHANGED",
"ŠT. GESEL": "NO. OF ENTRIES",
"STARO GESLO": "OLD PASSWORD",
"Statistika": "Statistics",
"Status": "Status",
"STATUS": "STATUS",
"Ste pozabili geslo? Napišite svoje uporabniško ime ali elektronski naslov, ki ste ga uporabili ob registraciji. Na ta naslov vam bomo poslali sporočilo, s pomočjo katerega boste lahko vnesli novo geslo.": "Forgot your password? Enter you username or the e-mail address you used for registration. We send you a message with instructions for password reset to this e-mail address.",
"Število dokumentov": "Number of documents",
"Število slovarskih sestavkov je manjše od zahtevanega števila - povečajte število slovarskih sestavkov, sicer ga skrbnik slovarjev lahko zapre": "The dictionary has less entries than the required minimum - add more entries to the dictionary, otherwise it may be unpublished by the Dictionary administrator",
"Število slovarskih sestavkov, ki jih mora vsebovati slovar, da je omogočena objava slovarja na portalu.": "Minimal number of dictionary entries required for a dictionary to be published on the portal.",
"Število terminov": "Number of terms",
"Število vprašanj:": "Number of questions:",
"ŠTEVILO ZADETKOV NA STRANI": "NUMBER OF RESULTS ON THE PAGE",
"Število zadnjih kopij spremenjenih slovarskih sestavkov, ki se hranijo. Če vrednost ni določena, omejitve ni.": "The largest allowed number of saved of recent dictionary entry versions. If no value is entered, there is not limit.",
"Stop termini": "Stop terms",
"Stran ne obstaja": "Page does not exist",
"Strežnik ni dosegljiv. Poskusite kasneje.": "Unable to connect to server. Try again later.",
"Strinjam se s <a target=\"_blank\" href=\"/pogoji-uporabe\">pogoji uporabe</a>": "I agree to the <a target=\"_blank\" href=\"/pogoji-uporabe\">Terms of use</a>",
"Strokovni pregled": "Terminology review",
"Strokovno pregledano": "Terminologically reviewed",
"Struktura": "Structure",
"STRUKTURA - ELEMENTI SLOVARSKEGA SESTAVKA:": "STRUCTURE - ELEMENTS OF THA DICTIONARY ENTRY:",
"Struktura slovarskega sestavka": "The structure of the dictionary entry",
"Svetovalci": "Consultants",
"Svetovalec": "Consultant",
"Svetovalnica": "Consultancy",
"Svetovanje": "Consulting",
"Svoj terminološki portal lahko povežete še z drugimi terminološkimi portali in med iskalnimi prikazujete tudi njihove zadetke.": "You can link your terminology portal to other terminology portals and show their terminology resources in your results.",
"Ta slovar nima opisa": "This dictionary has no description",
"Termin": "Term",
"TERMIN": "TERM",
"TERMIN:": "TERM:",
"TERMINI": "TERMS",
"Terminološka svetovalnica je namenjena širši strokovni javnosti, ki se sooča s konkretnimi poimenovalnimi problemi, pa naj gre za popolnoma nove pojme, ki jih je v slovenščini šele treba poimenovati, ali že znane pojme, za katere obstaja več poimenovanj.": "The Terminological consulting is aimed at wider expert public when faced with the problem of naming specific concepts, whether they are completely new concept that do not yet have a term, or already known concepts for which several different terms are used.",
"Terminološka svetovalnica ZRC SAZU": "ZRC SAZU Terminological counselling",
"Terminološke slovarje lahko v celoti ali po izbranih kriterijih izvozite v različnih formatih in shranite na svojem računalniku.": "Terminological dictionaries in their entirety or according to selected criteria can be exported formats in various and saved to your computer.",
"Terminološkemu slovarju določite status. Izbirate lahko med zaprt, v urejanju in odprt.": "Set the status of the terminology dictionary. Possible statuses are unpublished, in editing, and published.",
"Terminološki kandidati": "Term candidates",
"Terminološko svetovanje": "Terminology counseling",
"Tip napake 1 (ni slovenskega termina)": "Error type 1 (no Slovenian term)",
"Tip napake 2 (ni definicije ali tujejezičnega termina)": "Error type 2 (no definition or term in foreign language)",
"Tu so zbrani vsi komentarji, povezani z vašim slovarjem.": "All comments related to the your dictionary can be found here.",
"Tuj sinonim": "Synonym in a foreign language",
"Tuj termin": "Term in a foreign language",
"Tuja definicija": "Definition in a foreign language",
"Tuji jezik": "Foreign language",
"TUJI JEZIKI": "FOREIGN LANGUAGES",
"Tuji slovarji": "Dictionaries in a foreign language",
"TUJI TERMIN": "TERMS IN A FOREIGN LANGUAGE",
"Tukaj lahko izbrišete svoj račun.": "Here you can delete your account.",
"Tukaj lahko ponovno ideksirate slovar.": "Here you can re-index the dictionary.",
"Tukaj lahko spremenite geslo za prijavo.": "Here you can change your login password.",
"Tukaj lahko spremenite ime, priimek in elektronski naslov uporabnika.": "Here you can change the user's first name, last name and e-mail address.",
"Tukaj lahko spremenite nekatere nastavitve, vezane na posameznega uporabnika.": "Here you can change certain settings related to individual users.",
"Unikatna oznaka povezanega terminološkega portala za prikaz na vašem portalu, sestavljena iz dveh črk ali črke in številke.": "A unique identifier of the linked terminology portal to be display on your portal, consisting of two letters or a letter and a number.",
"Uporabi": "Use",
"Uporabnik": "User",
"UPORABNIK": "USER",
"Uporabnik na tem mestu izdela specializirani korpus iz besedil, ki jih je zbral in shranil sam. Besedila naj bodo izbrana po načelih tvorjenja specializiranih korpusov. Za uspešno luščenje priporočamo najmanj 10 besedil. Vsa besedila naj bodo shranjena v besedilnem formatu (.txt). Luščenje podpira tudi formate .docx in .pdf, vendar so rezultati slabši.": "Here the user can create a specialized corpus from texts selected and saved by the user. The texts should be selected in accordance with the principles for creation of specialized corpora. For successful extraction, at least 10 texts should be used. All texts should be saved in a .txt format. While the extractor also supports the .docx and .pdf formats, the results will not be as good.",
"Uporabniki": "Users",
"Uporabnikovi dokumenti": "User documents",
"Uporabniške pravice/vloge": "User rights/roles",
"Uporabniške vloge": "User roles",
"Uporabniški korpus": "User's corpus",
"Uporabniški račun še ni aktiviran. Kliknite aktivacijsko povezavo, katero smo vam poslali po elektronski pošti.": "The user account has not been activated. Click the link sent to your e-mail to activate your account.",
"Uporabniško ime": "Username",
"UPORABNIŠKO IME": "USERNAME",
"Uporabniško ime ali elektronski naslov": "Username or e-mail address",
"Uporabniško ime že obstaja": "Username already exists",
"Uredi": "Edit",
"UREDI": "EDIT",
"Uredi lastnosti": "Edit properties",
"Uredi povezavo": "Edit the link",
"Uredi vsebino": "Edit content",
"Urednik slovarja": "Dictionary editor",
"Urejanje": "Edit",
"Urejanje terminološkega odgovora": "Edit terminological answer",
"Urejeni": "Edited",
"Urejeno": "Edited",
"URL naslov terminološkega portala, s katerim boste sinhronizirali podatke iz terminoloških virov.": "URL address of the terminology portal hosting terminology resources with data you want to show on your portal.",
"URL naslov terminološkega portala, s katerim želimo povezati svoj portal.": "URL address of the terminology portal you want to link to your portal.",
"URL naslov terminološkega portala, s katerim želimo povezati svoj portal. (API klic)": "URL address of the terminology portal with which we want to connect our portal. (API call)",
"URL naslov terminološkega portala, s katerim želite povezati svoj portal. (API klic)": "URL address of the terminology portal you want to link to your portal. (API call)",
"URL povezava do mnenja:": "URL link to opinion:",
"URL za sinhronizacijo slovarjev *": "URL for synchronization of dictionaries *",
"URL za sinhronizacijo slovarskih sestavkov *": "URL for synchronization of dictionary entries *",
"Uspešno ste ponastavili svoje geslo.": "You have successfully reset your password.",
"Ustvari": "Create",
"USTVARJEN": "CREATED",
"Ustvarjeno novo vprašanje v svetovalnici": "A new question has been created in the Consultancy service",
"UTEŽ": "WEIGHT",
"Uvoz": "Import",
"Uvoz iz datoteke": "Import from file",
"Uvoz iz luščilnika": "Import from the extractor",
"UVOZI": "IMPORTS",
"Uvozi termine od številke": "Import appointments between the numbers",
"V delu": "In progress",
"V DRUGI VSEBINI": "IN OTHER PARTS OF THE ENTRY",
"V IZTOČNICAH": "IN THE TERMS",
"V obdelavi": "In progress",
"V odpiranju": "In the process of publication",
"v predogledu": "in preview",
"V slovarju so samo terminološki kandidati. Slovarja ne morete objaviti.": "The dictionary contains only term candidates. The dictionary cannot be published.",
"V tem razdelku lahko določite elemente slovarskega sestavka. Z izbiro elementov se vam prikazuje podoba slovarskega sestavka. Izbiro lahko tudi med urejanjem vsebine kadarkoli spremenite.": "In this section, you can select the elements of the dictionary entry. When you select the elements, you can see what the entry will look like. You can change your selection any time during the editing process.",
"V tem razdelku lahko določite glavne administratorske pravice na terminološkem portalu.": "In this section, you can assign the main administrative rights on the terminology portal.",
"V tem razdelku lahko določite uporabniške vloge posameznega uporabnika in urejate njegove podatke.": "In this section, you can define the user roles of individual users and edit their data.",
"V TUJEJEZIČNIH USTREZNIKIH": "IN FOREIGN LANGUAGE EQUIVALENTS",
"V urejanju": "In editing",
"Vaš komentar je brez vsebine.": "Your comment is empty.",
"Vaše iskanje ni bilo uspešno. Vpišite novo iskalno poizvedbo in poizkusite znova.": "Your search was not successful. Enter a new search string and try again.",
"Več ...": "More...",
"Več …": "More...",
"Več...": "More...",
"VELIKOST": "SIZE",
"Veljavni": "Valid",
"Veljavni elektronski naslov uporabnika, na katerega uporabnik prejema sporočila, povezana s portalom.": "A valid e-mail address of the user to which the user receives messages related to the portal.",
"Veljavno": "Valid",
"Veljavnost": "Validity",
"Verzija": "Version",
"Verzija 1": "Version 1",
"Verzija:": "Version:",
"video": "video",
"Video": "Video",
"VIDEO": "VIDEO",
"VIDEO:": "VIDEO:",
"Vidno": "Visible",
"Vir": "Source",
"Viri": "Sources",
"Vnesite e-naslov, ki ga za obveščanje o novih terminoloških vprašanjih uporabljajo terminološki svetovalci.": "Enter the email address used to notify Terminology Consultants about new terminology questions.",
"Vnesite naslov spletnega mesta, kjer so zbrani vsi odgovori terminološke svetovalnice.": "Enter the URL of the website where all the answers of your consultancy service are collected.",
"Vnesite novo geslo. Ko ga potrdite, se boste v vaš uporabniški račun lahko spet prijavili z novim geslom.": "Enter a new password. After confirming it, you will be able to log into your account with the new password.",
"Vnesite podatke terminološkega portala, s katerim želite povezati svoj portal.": "Enter the information of the terminology portal with which you want to link your portal.",
"Vnesite podatke, ki niso sistemsko vključeni v druga polja, npr. vir, zgled rabe.": "Enter data that is not systemically included in other fields, e.g. examples of use.",
"Vnesite polno ime terminološkega portala, s katerim se povezujete.": "Enter the full name of the terminology portal you are linking with.",
"Vnesite termin.": "Enter the term.",
"Vnesti morate termin": "You must enter the term",
"Vpišite avtorja slovarja, če ste glavni avtor, na prvo mesto zapišite svoje ime.": "Enter the author of the dictionary. If you are the main author, write your name first.",
"Vpišite definicijo v tujem jeziku.": "Enter the definition in a foreign language.",
"Vpišite iskalni niz": "Enter a search string",
"Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.": "Enter a new secondary domain. It will be visible on the list of secondary domain immediately after Portal Administrator's approval.",
"Vpišite podpodročje glavnega področja, ki ste ga izbrali. Če podpodročja ni med naborom, izberite polje Novo podpodročje.": "Enter the secondary domain of the domain you selected. If you cannot find the secondary domain on the list, select New secondary domain.",
"Vprašanje poslano:": "Question sent:",
"VRSTA": "TYPE",
"VRSTA DOKUMENTA": "TYPE OF DOCUMENT",
"Vsebina": "Content",
"Vsebina slovarja": "Contents of the dictionary",
"Vsebina slovarskega sestavka je bila duplicirana.": "The content of the dictionary entry has been duplicated.",
"Vsi povezani slovarji": "All linked dictionaries",
"Vsi slovarji": "All dictionaries",
"Vsi slovarski sestavki": "All dictionary entries",
"Vstavite definicijo pojma.": "Enter the definition of the term.",
"Vstavite termine, ki se za definirani pojem tudi uporabljajo.": "Enter terms which are also used for the defined concept.",
"Vstavite ustrezno področno oznako, ki jo želite določiti za posamezni termin.": "Enter the domain label that you want to assign to the individual term.",
"Z izbiro te možnosti boste pobrisali samo slovarske sestavke, ki so v določeni fazi urejanja.": "By selecting this option, you will only delete dictionary entries that are in a certain editing phase.",
"Z registracijo pridobite možnost uporabe vseh funkcij terminološkega portala.": "By registering, you get access to all functions of the terminology portal.",
"Za luščenje terminoloških kandidatov morate biti prijavljeni.": "You must be logged in to extract term candidates.",
"Za prvo objavo slovarja je potrebno dovoljenje skrbnika slovarjev.": "Dictionary administrator's approval is required before the first publication of the dictionary.",
"Za to besedo še ni podatkov.": "There is no data for this word.",
"Za urejanje slovarjev morate biti prijavljeni.": "You must be logged in to edit dictionaries.",
"Za zastavljanje terminoloških vprašanj morate biti prijavljeni.": "You must be logged in to ask terminology questions.",
"Začetek": "Start",
"Začni": "Start",
"ZAČNI": "START",
"Zadnja sprememba": "Last change",
"Zadnji izvozi": "Recent exports",
"Zadnji objavljeni slovarji": "Recent published dictionaries",
"Zadnji odgovori na vprašanja": "Recent answers to questions",
"Zadnji uvozi": "Recent imports",
"Zapomni si prijavo": "Remember my login",
"Zaporedje:": "Sequence:",
"Zapri": "Close",
"ZAPRI": "CLOSE",
"zaprt": "unpublished",
"Zaprt": "Unpublished",
"Zaradi organizacije podatkov na portalu za skrajšani naslov slovarja predlagamo poenobesede\tnje, ki se bo izpisovalo ob slovarju, npr. Davčni terminološki slovar &rarr; Davki.": "Due to the organization of the data on the portal, we suggest using only one word for the short dictionary title, which will show next to the dictionary, e.g. Tax Terminology Dictionary&rarr; Taxes",
"Zaradi organizacije podatkov na portalu za skrajšani naslov slovarja predlagamo poenobesedenje, ki se bo izpisovalo ob slovarju, npr. Davčni terminološki slovar &rarr; Davki": "Due to the organization of the data on the portal, we suggest using only one word for the short dictionary title, which will show next to the dictionary, e.g. Tax Terminology Dictionary&rarr; Taxes",
"Zastavi novo vprašanje": "Ask a new question",
"Zastavi terminološko vprašanje": "Ask a terminology question",
"ZAVRJEN": "REJECTED",
"ZAVRNI": "REJECT",
"Zavrnjeno": "Rejected",
"Zunanji": "External",
"zvok": "audio",
"Zvok": "Audio",
"ZVOK": "AUDIO",
"ZVOK:": "AUDIO:",
"titleTermsOfUse": "Terms of Use",
"titlePrivacyPolicy": "Privacy Policy"
}
+50
View File
@@ -0,0 +1,50 @@
{
"helpPageTitleHelp": "Help",
"helpPageTitleAbout": "General information about the Portal",
"helpPageTitleRegistration": "Registration",
"helpPageTitleSearchIndex": "Search",
"helpPageTitleSearchBasic": "Basic search",
"helpPageTitleSearchAdvanced": "Advanced search",
"helpPageTitleExtractionIndex": "Extraction",
"helpPageTitleExtractionSpecializedCorpora": "Specialized corpora",
"helpPageTitleExtractionPersonalCorpus": "Users personal corpus",
"helpPageTitleExtractionOssCorpus": "OSS Corpus",
"helpPageTitleExtractionStopTerms": "Lists of unwanted words or \"Stop terms\"",
"helpPageTitleExtractionDomains": "Domains",
"helpPageTitleExtractionTermCandidates": "Term candidates",
"helpPageTitleEditingIndex": "Editing",
"helpPageTitleEditingNewDictionary": "New dictionary",
"helpPageTitleEditingDictionaryProperties": "Editing dictionary properties",
"helpPageTitleEditingUsers": "Users",
"helpPageTitleEditingStructure": "Structure of dictionary entries",
"helpPageTitleEditingComments": "Comments",
"helpPageTitleEditingDictionaryContent": "The content of the dictionary",
"helpPageTitleEditingImportingData": "Importing data",
"helpPageTitleEditingEntriesIndex": "Adding new dictionary entries",
"helpPageTitleEditingEntriesDomainLabels": "Domain labels",
"helpPageTitleEditingEntriesLabel": "Qualifiers",
"helpPageTitleEditingEntriesDefinition": "Definition",
"helpPageTitleEditingEntriesSynonyms": "Synonyms",
"helpPageTitleEditingEntriesRelatedTerms": "Related terms",
"helpPageTitleEditingEntriesOtherLanguages": "Other languages",
"helpPageTitleEditingEntriesOther": "Other",
"helpPageTitleEditingEntriesEditingHistory": "Editing history",
"helpPageTitleEditingEntriesComments": "Comments",
"helpPageTitleEditingEntriesDataExport": "Data export",
"helpPageTitleConsultancyIndex": "Consultancy",
"helpPageTitleConsultancyQuestions": "How to ask a question",
"helpPageTitleConsultancyTerminologyPrinciples": "Terminology principles",
"helpPageTitleConsultancyLink": "Link",
"helpPageTitleConsultancyAnswers": "Published answers",
"helpPageTitleAdministrationIndex": "Administration",
"helpPageTitleAdministrationBasicSettingsIndex": "Basic settings",
"helpPageTitleAdministrationBasicSettingsPortal": "Portal",
"helpPageTitleAdministrationBasicSettingsDictionaries": "Dictionaries",
"helpPageTitleAdministrationBasicSettingsConsultancy": "Consultancy",
"helpPageTitleAdministrationLinksToOtherPortalsIndex": "Links to other portals",
"helpPageTitleAdministrationLinksToOtherPortalsList": "List",
"helpPageTitleAdministrationLinksToOtherPortalsDictionaries": "Dictionaries",
"helpPageTitleAdministrationDictionaries": "Dictionaries",
"helpPageTitleAdministrationUsers": "Users",
"helpPageTitleAdministrationSecondaryDomains": "Secondary domains"
}
+710
View File
@@ -0,0 +1,710 @@
{
" Terminološke odgovore pripravljajo sodelavci Terminološke sekcije Inštituta za slovenski jezik Frana Ramovša ZRC SAZU (in so objavljeni tudi na spletišču <a href='https://isjfr.zrc-sazu.si/sl/terminologisce/svetovanje'>Terminologišče</a>), ki pri delu upoštevajo osnovna terminološka načela.": " Terminološke odgovore pripravljajo sodelavci Terminološke sekcije Inštituta za slovenski jezik Frana Ramovša ZRC SAZU (in so objavljeni tudi na spletišču <a href='https://isjfr.zrc-sazu.si/sl/terminologisce/svetovanje'>Terminologišče</a>), ki pri delu upoštevajo osnovna terminološka načela.",
"(STOP) Termini": "(STOP) Termini",
"[ni termina]": "[ni termina]",
"#": "#",
"<b>Odgovor:</b>": "<b>Odgovor:</b>",
"Administracija": "Administracija",
"Administrator": "Administrator",
"Administratorska konzola": "Administratorska konzola",
"Administratorski del portala je namenjen skupini oseb, ki vsebinsko in tehnično ureja portal, odpira slovarske vire, daje pooblastila posameznim uporabnikom in skrbi za vsebinsko in tehnično urejenost portala. Izberite module, ki jih boste ponudili na terminološkem portalu in na kratko opišite, kaj ponuja vaš portal.": "Administratorski del portala je namenjen skupini oseb, ki vsebinsko in tehnično ureja portal, odpira slovarske vire, daje pooblastila posameznim uporabnikom in skrbi za vsebinsko in tehnično urejenost portala. Izberite module, ki jih boste ponudili na terminološkem portalu in na kratko opišite, kaj ponuja vaš portal.",
"Aktivacija računa": "Aktivacija računa",
"Ali res želite izbrisati vnos? Dejanja ni mogoče razveljaviti.": "Ali res želite izbrisati vnos? Dejanja ni mogoče razveljaviti.",
"Ali želite indeksirati?": "Ali želite indeksirati?",
"Ali želite izbrisati slovarski sestavek?": "Ali želite izbrisati slovarski sestavek?",
"Ali želite objaviti vsa gesla?": "Ali želite objaviti vsa gesla?",
"Ali želite zbrisati ta profil. S tem bodo izbrisani vsi podatki, ki ste jih ustvarili": "Ali želite zbrisati ta profil. S tem bodo izbrisani vsi podatki, ki ste jih ustvarili",
"Ali želite zbrisati ta vnos?": "Ali želite zbrisati ta vnos?",
"angleščina": "angleščina",
"ANGLEŠKI NASLOV SLOVARJA *": "ANGLEŠKI NASLOV SLOVARJA *",
"ANGLEŠKI OPIS PORTALA": "ANGLEŠKI OPIS PORTALA",
"ANGLEŠKO IME PORTALA": "ANGLEŠKO IME PORTALA",
"Angleško ime terminološkega portala.": "Angleško ime terminološkega portala.",
"Avtor": "Avtor",
"AVTOR": "AVTOR",
"AVTOR SLOVARJA": "AVTOR SLOVARJA",
"Avtor:": "Avtor:",
"Avtorja": "Avtorja",
"Avtorji": "Avtorji",
"Avtorji mnenja:": "Avtorji mnenja:",
"Basic": "Basic",
"Besedila": "Besedila",
"Besedilo gre tukaj": "Besedilo gre tukaj",
"Brisanje je onemogočeno, doker se nalagajo nove datoteke.": "Brisanje je onemogočeno, doker se nalagajo nove datoteke.",
"Brisanje je onemogočeno, saj je eno še v procesu.": "Brisanje je onemogočeno, saj je eno še v procesu.",
"Brisanje slovarja": "Brisanje slovarja",
"Brisanje slovarskih sestavkov": "Brisanje slovarskih sestavkov",
"Briši": "Briši",
"BRIŠI": "BRIŠI",
"Briši uporabnika": "Briši uporabnika",
"Če imate pripravljen slovar v enem od formatov, ki so navedeni spodaj, lahko svoje podatke uvozite.": "Če imate pripravljen slovar v enem od formatov, ki so navedeni spodaj, lahko svoje podatke uvozite.",
"Če izberete to možnost, se bodo vsi doslejšnji slovarski sestavki ob uvozu nove datoteke izbrisali.": "Če izberete to možnost, se bodo vsi doslejšnji slovarski sestavki ob uvozu nove datoteke izbrisali.",
"Če želite dodati novo luščenje, morate najprej pobrisati vsaj eno od obstoječih, saj je na posameznega uporabnika dovoljenih največ 5 luščenj.": "Če želite dodati novo luščenje, morate najprej pobrisati vsaj eno od obstoječih, saj je na posameznega uporabnika dovoljenih največ 5 luščenj.",
"Celotni naslov slovarja v angleščini.": "Celotni naslov slovarja v angleščini.",
"Celotni naslov slovarja, ki bo zabeležen v bibliografskih podatkih.": "Celotni naslov slovarja, ki bo zabeležen v bibliografskih podatkih.",
"Ciljni jeziki": "Ciljni jeziki",
"Čim bolj natančno opišite terminološki problem, zlasti opišite vsebino pojma.": "Čim bolj natančno opišite terminološki problem, zlasti opišite vsebino pojma.",
"DATOTEKA": "DATOTEKA",
"Datoteka uspešno naložena": "Datoteka uspešno naložena",
"DATUM": "DATUM",
"Datum objave/spremembe": "Datum objave/spremembe",
"Datum vprašanja:": "Datum vprašanja:",
"Definicija": "Definicija",
"DEFINICIJA": "DEFINICIJA",
"DEFINICIJA:": "DEFINICIJA:",
"Delete profile": "Delete profile",
"Deli": "Deli",
"DELI": "DELI",
"dni.": "dni.",
"do številke": "do številke",
"Dodaj": "Dodaj",
"DODAJ": "DODAJ",
"Dodaj besedilo za luščenje terminoloških kandidatov iz lastnega specializiranega korpusa. Ko boste dodali vsa besedila, ki ste jih izbrali, morate izbiro shraniti.": "Dodaj besedilo za luščenje terminoloških kandidatov iz lastnega specializiranega korpusa. Ko boste dodali vsa besedila, ki ste jih izbrali, morate izbiro shraniti.",
"Dodaj datoteko": "Dodaj datoteko",
"Dodaj komentar...": "Dodaj komentar...",
"Dodaj odgovor...": "Dodaj odgovor...",
"Dodaj opravilo": "Dodaj opravilo",
"Dodaj povezani termin.": "Dodaj povezani termin.",
"Dodaj povezavo": "Dodaj povezavo",
"Dodaj slovar": "Dodaj slovar",
"Dodajte ime in priimek naslednjega avtorja slovarja.": "Dodajte ime in priimek naslednjega avtorja slovarja.",
"Dodajte ime in priimek naslednjega avtorja slovarja..": "Dodajte ime in priimek naslednjega avtorja slovarja..",
"Dodajte leto izida besedil, ki jih želite luščiti. Lahko dodate več posameznih let. Če boste polje pustili prazno, bodo vključena vsa leta. Če bo besedil preveč, boste morali omejiti izbiro.": "Dodajte leto izida besedil, ki jih želite luščiti. Lahko dodate več posameznih let. Če boste polje pustili prazno, bodo vključena vsa leta. Če bo besedil preveč, boste morali omejiti izbiro.",
"Dodajte podatek o zunajjezikovnih okoliščinah, ki niso povezane s pojmom, npr. letnico.": "Dodajte podatek o zunajjezikovnih okoliščinah, ki niso povezane s pojmom, npr. letnico.",
"Dodajte povezavo do slike.": "Dodajte povezavo do slike.",
"Dodajte povezavo do videa.": "Dodajte povezavo do videa.",
"Dodajte povezavo do zvočnega posnetka.": "Dodajte povezavo do zvočnega posnetka.",
"Dodajte termin, ki je sicer definiran v samostojnem slovarskem sestavku, vendar je povezan s terminom, ki ga opisujete v tem slovarskem sestavku.": "Dodajte termin, ki je sicer definiran v samostojnem slovarskem sestavku, vendar je povezan s terminom, ki ga opisujete v tem slovarskem sestavku.",
"Dodajte terminološki odgovor in ga utemeljite.": "Dodajte terminološki odgovor in ga utemeljite.",
"Dodajte tujejezične ustreznike, ki se za opisani pojem tudi uporabljajo v tujem jeziku.": "Dodajte tujejezične ustreznike, ki se za opisani pojem tudi uporabljajo v tujem jeziku.",
"Dodajte tujejezični ustreznik.": "Dodajte tujejezični ustreznik.",
"Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\">Več...</a>": "Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\">Več...</a>",
"Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. Na koncu morate spremembe shraniti. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\" target=\"_blank\">Več ...</a>": "Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. Na koncu morate spremembe shraniti. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\" target=\"_blank\">Več ...</a>",
"Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. Vse spremembe morate shraniti. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\">Več...</a>": "Dodate lahko seznam besed, ki jih v seznam terminoloških kandidatov ne želite vključiti. Seznam naj bo shranjen v formatu .txt. Vse spremembe morate shraniti. <a class=\"smaller-gray-info\" href=\"/pomoc#help-stop-lists\">Več...</a>",
"Dodaten avtor.": "Dodaten avtor.",
"DODATNI POVEZANI TERMIN": "DODATNI POVEZANI TERMIN",
"Dodeli": "Dodeli",
"DODELI": "DODELI",
"Določite lastnosti terminoloških virov na portalu, zlasti minimalno število sestavkov, ki so pogoj za objavo, možnosti potrjevanja objave novih slovarjev, število različic slovarja, ki jih lahko hrani posamezni uporabnik. Te nastavitve veljajo za vse terminološke vire na portalu.": "Določite lastnosti terminoloških virov na portalu, zlasti minimalno število sestavkov, ki so pogoj za objavo, možnosti potrjevanja objave novih slovarjev, število različic slovarja, ki jih lahko hrani posamezni uporabnik. Te nastavitve veljajo za vse terminološke vire na portalu.",
"Domači slovarji": "Domači slovarji",
"Drugo": "Drugo",
"DRUGO": "DRUGO",
"DRUGO:": "DRUGO:",
"Dvočrkovna oznaka terminološkega portala.": "Dvočrkovna oznaka terminološkega portala.",
"E - naslov": "E - naslov",
"E-naslov": "E-naslov",
"E-NASLOV": "E-NASLOV",
"E-pošta": "E-pošta",
"ELEKTRONSKI NASLOV": "ELEKTRONSKI NASLOV",
"Elektronski naslov trenutno prijavljenega uporabnika.": "Elektronski naslov trenutno prijavljenega uporabnika.",
"Elektronski naslov že obstaja": "Elektronski naslov že obstaja",
"EVROPSKI SKLAD ZA REGIONALNI RAZVOJ": "EVROPSKI SKLAD ZA REGIONALNI RAZVOJ",
"FAZA UREJANJA": "FAZA UREJANJA",
"Faze urejanja": "Faze urejanja",
"Filtri": "Filtri",
"Filtriranje": "Filtriranje",
"FORMAT ZAPISA": "FORMAT ZAPISA",
"Gesli se ne ujemata": "Gesli se ne ujemata",
"Gesli se ujemata!": "Gesli se ujemata!",
"Geslo": "Geslo",
"GESLO": "GESLO",
"Geslo je prekratko": "Geslo je prekratko",
"Geslo je prekratko.": "Geslo je prekratko.",
"Geslo se ne ujema": "Geslo se ne ujema",
"Geslo se ne ujema.": "Geslo se ne ujema.",
"Hrvaščina": "Hrvaščina",
"Hvala za poslano vprašanje, ki je bilo posredovano svetovalcem terminološkega portala. Odgovor boste prejeli na e-naslov, ki ste ga navedli ob registraciji.": "Hvala za poslano vprašanje, ki je bilo posredovano svetovalcem terminološkega portala. Odgovor boste prejeli na e-naslov, ki ste ga navedli ob registraciji.",
"Hvala za poslano vprašanje, ki je bilo posredovano v Terminološko svetovalnico ZRC SAZU. Odgovor boste prejeli na e-naslov, ki ste ga navedli ob registraciji.": "Hvala za poslano vprašanje, ki je bilo posredovano v Terminološko svetovalnico ZRC SAZU. Odgovor boste prejeli na e-naslov, ki ste ga navedli ob registraciji.",
"ID Slovarja": "ID Slovarja",
"ID: Ni idja": "ID: Ni idja",
"IDEKSIRAJ": "IDEKSIRAJ",
"Imate neshranjene spremebe. Ali jih želite shraniti?": "Imate neshranjene spremebe. Ali jih želite shraniti?",
"Imate neshranjene spremembe. Ali jih želite shraniti?": "Imate neshranjene spremembe. Ali jih želite shraniti?",
"Ime": "Ime",
"IME": "IME",
"IME DATOTEKE": "IME DATOTEKE",
"Ime in opis": "Ime in opis",
"IME IN PRIIMEK": "IME IN PRIIMEK",
"Ime in priimek trenutno prijavljenega uporabnika.": "Ime in priimek trenutno prijavljenega uporabnika.",
"IME LUŠČENJA": "IME LUŠČENJA",
"IME PORTALA": "IME PORTALA",
"IME PORTALA *": "IME PORTALA *",
"Ime Priimek": "Ime Priimek",
"Ime registriranega uporabnika.": "Ime registriranega uporabnika.",
"Ime slovarja": "Ime slovarja",
"Ime, s katerim se uporabnik predstavlja na terminološkem portalu.": "Ime, s katerim se uporabnik predstavlja na terminološkem portalu.",
"Indeksiranje slovarja": "Indeksiranje slovarja",
"Institucija": "Institucija",
"INSTITUCIJA": "INSTITUCIJA",
"Institucija, kjer trenutno prijavljeni uporabnik deluje.": "Institucija, kjer trenutno prijavljeni uporabnik deluje.",
"Interni": "Interni",
"Išči": "Išči",
"Išči po slovarju": "Išči po slovarju",
"ISKANI NIZ JE BIL NAJDEN": "ISKANI NIZ JE BIL NAJDEN",
"Iskanje": "Iskanje",
"Iskanje po slovarjih": "Iskanje po slovarjih",
"Iskanje po vseh": "Iskanje po vseh",
"ISSN": "ISSN",
"ISSN OZNAKA": "ISSN OZNAKA",
"ISSN oznaka.": "ISSN oznaka.",
"Italijanščina": "Italijanščina",
"IZBERI": "IZBERI",
"Izberi datoteko": "Izberi datoteko",
"IZBERI VEČ": "IZBERI VEČ",
"Izberite enega od rezultatov luščenja s seznama.": "Izberite enega od rezultatov luščenja s seznama.",
"Izberite format datoteke, v kateri je slovar shranjen na vašem računalniku.": "Izberite format datoteke, v kateri je slovar shranjen na vašem računalniku.",
"Izberite format izpisa terminološkega slovarja.": "Izberite format izpisa terminološkega slovarja.",
"Izberite luščenje": "Izberite luščenje",
"Izberite področje": "Izberite področje",
"Izberite področje iz seznama področij, da zmanjšate obseg besedil, iz katerih bo potekalo luščenje.": "Izberite področje iz seznama področij, da zmanjšate obseg besedil, iz katerih bo potekalo luščenje.",
"Izberite področje svojega terminološkega slovarja na seznamu področij.": "Izberite področje svojega terminološkega slovarja na seznamu področij.",
"Izberite slovarske podatke, ki ste jih shranili na svojem računalniku.": "Izberite slovarske podatke, ki ste jih shranili na svojem računalniku.",
"Izberite takšno ime, ki vam bo pomagalo slediti rezultatom, če boste luščenje besedil opravili večkrat.": "Izberite takšno ime, ki vam bo pomagalo slediti rezultatom, če boste luščenje besedil opravili večkrat.",
"Izberite vrsto dokumenta iz seznama, npr. članek, diplomsko delo.": "Izberite vrsto dokumenta iz seznama, npr. članek, diplomsko delo.",
"Izberite vse tuje jezike, ki jih bo terminološki slovar vseboval.": "Izberite vse tuje jezike, ki jih bo terminološki slovar vseboval.",
"Izberite, ali želite izpisati samo jezikovno pregledane slovarske sestavke.": "Izberite, ali želite izpisati samo jezikovno pregledane slovarske sestavke.",
"Izberite, ali želite izpisati samo objavljene ali tudi neobjavljene slovarske sestavke.": "Izberite, ali želite izpisati samo objavljene ali tudi neobjavljene slovarske sestavke.",
"Izberite, ali želite izpisati samo strokovno pregledane slovarske sestavke.": "Izberite, ali želite izpisati samo strokovno pregledane slovarske sestavke.",
"Izberite, ali želite izpisati vse slovarske sestavke ali samo tiste, ki so v določeni fazi urejanja.": "Izberite, ali želite izpisati vse slovarske sestavke ali samo tiste, ki so v določeni fazi urejanja.",
"Izberite, katere slovarske sestavke želite izpisati glede na njihovo veljavnost.": "Izberite, katere slovarske sestavke želite izpisati glede na njihovo veljavnost.",
"Izbrišete lahko celoten slovar z vsemi metapodatki. Dejanja ni mogoče preklicati.": "Izbrišete lahko celoten slovar z vsemi metapodatki. Dejanja ni mogoče preklicati.",
"Izbrišete lahko vse doslej obdelane slovarske sestavke in ohranite vse metapodatke o slovarju. Dejanja ni mogoče preklicati.": "Izbrišete lahko vse doslej obdelane slovarske sestavke in ohranite vse metapodatke o slovarju. Dejanja ni mogoče preklicati.",
"Izbriši": "Izbriši",
"IZBRIŠI": "IZBRIŠI",
"Izbriši obstoječe slovarske sestavke.": "Izbriši obstoječe slovarske sestavke.",
"Izbriši polje": "Izbriši polje",
"Izbriši račun": "Izbriši račun",
"Izgovor": "Izgovor",
"Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.": "Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.",
"Izvoz": "Izvoz",
"Izvozi": "Izvozi",
"IZVOZI": "IZVOZI",
"Izvozi termine od številke": "Izvozi termine od številke",
"je že v tabeli.": "je že v tabeli.",
"Jeziki": "Jeziki",
"Jeziki iskanja": "Jeziki iskanja",
"Jezikovni pregled": "Jezikovni pregled",
"Jezikovno pregledano": "Jezikovno pregledano",
"KANONIČNA OBLIKA": "KANONIČNA OBLIKA",
"KLJUČNE BESEDE": "KLJUČNE BESEDE",
"Ko bo luščenje uspešno zaključeno, boste na svoj elektronski naslov, ki ste ga navedli ob registraciji, prejeli obvestilo.": "Ko bo luščenje uspešno zaključeno, boste na svoj elektronski naslov, ki ste ga navedli ob registraciji, prejeli obvestilo.",
"Ko boste končali z urejanjem svojega terminološkega slovarja, lahko objavite vse slovarske sestavke, ki bodo postali vidni vsem uporabnikom. Vaše dejanje mora potrditi še administrator portala.": "Ko boste končali z urejanjem svojega terminološkega slovarja, lahko objavite vse slovarske sestavke, ki bodo postali vidni vsem uporabnikom. Vaše dejanje mora potrditi še administrator portala.",
"komentar": "komentar",
"komentarja": "komentarja",
"komentarjev": "komentarjev",
"komentarji": "komentarji",
"Komentarji": "Komentarji",
"Končan": "Končan",
"Konec": "Konec",
"Kontrola objave": "Kontrola objave",
"KONTROLA OBJAVE": "KONTROLA OBJAVE",
"Korpus OSS": "Korpus OSS",
"Lastna svetovalnica": "Lastna svetovalnica",
"Lastni dokumenti": "Lastni dokumenti",
"Lastnosti": "Lastnosti",
"LETO": "LETO",
"Luščenje": "Luščenje",
"Luščenje | dodaj opravilo": "Luščenje | dodaj opravilo",
"Luščenje iz korpusa besedil OSS": "Luščenje iz korpusa besedil OSS",
"Luščenje iz lastnih besedil": "Luščenje iz lastnih besedil",
"Luščenje iz lastnih besedil | dodaj opravilo ": "Luščenje iz lastnih besedil | dodaj opravilo ",
"Luščenje je bilo dano v fazo obdelave.": "Luščenje je bilo dano v fazo obdelave.",
"Luščenje končano": "Luščenje končano",
"Luščenje terminoloških kandidatov iz besedil, ki jih ima uporabnik shranjena pri sebi. Za boljšo učinkovitost svetujemo format .txt.": "Luščenje terminoloških kandidatov iz besedil, ki jih ima uporabnik shranjena pri sebi. Za boljšo učinkovitost svetujemo format .txt.",
"Luščenje terminoloških kandidatov iz besedil, ki jih uporabnik izbere med vsemi besedili, vključenimi v <a class=\"gray-info\" href=\"https://openscience.si/\" target=\"_blank\">Nacionalni portal odprte znanosti</a>.": "Luščenje terminoloških kandidatov iz besedil, ki jih uporabnik izbere med vsemi besedili, vključenimi v <a class=\"gray-info\" href=\"https://openscience.si/\" target=\"_blank\">Nacionalni portal odprte znanosti</a>.",
"Luščenje terminoloških kandidatov iz besedil, ki jih uporabnik namensko izbere, je najučinkovitejše. Primerno izbrano ime opravila, vam omogoča, da lahko sledite, katera luščenja ste že opravili. Ko vnesete podatke, morate vse spremembe shraniti. Zaradi omejenega prostora za shranjevanje lahko shranite največ pet zadnjih luščenj. ": "Luščenje terminoloških kandidatov iz besedil, ki jih uporabnik namensko izbere, je najučinkovitejše. Primerno izbrano ime opravila, vam omogoča, da lahko sledite, katera luščenja ste že opravili. Ko vnesete podatke, morate vse spremembe shraniti. Zaradi omejenega prostora za shranjevanje lahko shranite največ pet zadnjih luščenj. ",
"Luščenje terminoloških kandidatov iz besedil, ki so že predhodno oblikoslovno označena, nudi dobre rezultate, vendar je treba nabor besedil omejiti. Svetujemo vam, da zoožite področje in dodatno omejite izbiro s tipi besedil in časovnim razponom, v katerih so nastala. Ko vnesete podatke, morate vse spremembe shraniti. Po vnosu podatkov, s katerimi boste omejili nabor izbranih besedil, morate pritisniti gumb Najdi. Izbiro boste shranili lahko le v primeru, da ne bo število najdenih dokumentov preveliko.": "Luščenje terminoloških kandidatov iz besedil, ki so že predhodno oblikoslovno označena, nudi dobre rezultate, vendar je treba nabor besedil omejiti. Svetujemo vam, da zoožite področje in dodatno omejite izbiro s tipi besedil in časovnim razponom, v katerih so nastala. Ko vnesete podatke, morate vse spremembe shraniti. Po vnosu podatkov, s katerimi boste omejili nabor izbranih besedil, morate pritisniti gumb Najdi. Izbiro boste shranili lahko le v primeru, da ne bo število najdenih dokumentov preveliko.",
"MAKSIMALNO ŠTEVILO KOPIJ SLOVARSKIH SESTAVKOV": "MAKSIMALNO ŠTEVILO KOPIJ SLOVARSKIH SESTAVKOV",
"Meni": "Meni",
"MENI": "MENI",
"MINIMALNO ŠTEVILO SLOVARSKIH SESTAVKOV": "MINIMALNO ŠTEVILO SLOVARSKIH SESTAVKOV",
"Modul za luščenje terminoloških kandidatov iz besedil.": "Modul za luščenje terminoloških kandidatov iz besedil.",
"Modul za svetovanje pri terminoloških zagatah.": "Modul za svetovanje pri terminoloških zagatah.",
"Modul za urejanje terminoloških slovarjev.": "Modul za urejanje terminoloških slovarjev.",
"MODULI": "MODULI",
"Moj Profil": "Moj Profil",
"Moji slovarji": "Moji slovarji",
"Morebitne že obstoječe poimenovalne rešitve, če obstajajo.": "Morebitne že obstoječe poimenovalne rešitve, če obstajajo.",
"Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.": "Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.",
"MOŽNOST OBJAVE SLOVARSKEGA SESTAVKA MED UREJANJEM": "MOŽNOST OBJAVE SLOVARSKEGA SESTAVKA MED UREJANJEM",
"MULTIMEDIJA": "MULTIMEDIJA",
"Na kratko opišite zasnovo in namen slovarja.": "Na kratko opišite zasnovo in namen slovarja.",
"Na seznamu uporabnikov lahko določite posameznemu uporabniku dodatne vloge ali urejate njihove podatke.": "Na seznamu uporabnikov lahko določite posameznemu uporabniku dodatne vloge ali urejate njihove podatke.",
"Na tem mestu lahko dodajate svetovalce, ki so registrirani uporabniki terminološkega portala. Če boste pripisali področje, boste lahko svetovalcu dodeljevali samo vprašanja, ki sodijo na izbrano področje, vsem drugim pa vsa.": "Na tem mestu lahko dodajate svetovalce, ki so registrirani uporabniki terminološkega portala. Če boste pripisali področje, boste lahko svetovalcu dodeljevali samo vprašanja, ki sodijo na izbrano področje, vsem drugim pa vsa.",
"Na tem mestu lahko določite področne oznake, če želite posamezne termine v svojem terminološkem slovarju razvrstiti še podrobneje.": "Na tem mestu lahko določite področne oznake, če želite posamezne termine v svojem terminološkem slovarju razvrstiti še podrobneje.",
"Na tem mestu so zbrani vsi komentarji, povezani s terminološkim portalom.": "Na tem mestu so zbrani vsi komentarji, povezani s terminološkim portalom.",
"Na tem mestu so zbrani vsi komentarji, povezani z izbranim terminološkim slovarjem.": "Na tem mestu so zbrani vsi komentarji, povezani z izbranim terminološkim slovarjem.",
"na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu. Povezava za potrditev je veljavna ": "na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu. Povezava za potrditev je veljavna ",
"Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.": "Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.",
"Nabor vseh luščenj, s katerimi lahko uporabnik iz izbranih besedil pridobi sezname terminoloških kandidatov. Posamezne terminološke kandidate lahko preveri tudi v konkordančniku.": "Nabor vseh luščenj, s katerimi lahko uporabnik iz izbranih besedil pridobi sezname terminoloških kandidatov. Posamezne terminološke kandidate lahko preveri tudi v konkordančniku.",
"Nabor vseh luščenj, s pomočjo katerih uporabnik iz vhodnih besedil pridobi sezname terminoloških kandidatov in dostop do konkordančnika po teh besedilih.": "Nabor vseh luščenj, s pomočjo katerih uporabnik iz vhodnih besedil pridobi sezname terminoloških kandidatov in dostop do konkordančnika po teh besedilih.",
"Nabor vseh slovarjev, ki jih uporabnik lahko ureja kot glavni urednik ali pa ima dodeljeno pravico urejanja, pregledovanja, popravljanja ipd.": "Nabor vseh slovarjev, ki jih uporabnik lahko ureja kot glavni urednik ali pa ima dodeljeno pravico urejanja, pregledovanja, popravljanja ipd.",
"NAČIN UVOZA": "NAČIN UVOZA",
"Naglas": "Naglas",
"Najdi": "Najdi",
"Naložbo sofinancirata Republika Slovenija in Evropska unija iz Evropskega sklada za regionalni razvoj": "Naložbo sofinancirata Republika Slovenija in Evropska unija iz Evropskega sklada za regionalni razvoj",
"Napačno geslo.": "Napačno geslo.",
"Napaka": "Napaka",
"Napaka na strežniku": "Napaka na strežniku",
"NAPAKA pri iskanju": "NAPAKA pri iskanju",
"Napaka pri nalaganju datoteke": "Napaka pri nalaganju datoteke",
"NAPAKA V UVOZU": "NAPAKA V UVOZU",
"Napredno": "Napredno",
"Napredno urejanje omogoča spreminjanje večjega števila podatkov v terminološkem slovarju.": "Napredno urejanje omogoča spreminjanje večjega števila podatkov v terminološkem slovarju.",
"Naprej": "Naprej",
"Naslednje datoteke niso bile naložene zaradi navedenih razlogov:": "Naslednje datoteke niso bile naložene zaradi navedenih razlogov:",
"Naslov": "Naslov",
"NASLOV": "NASLOV",
"NASLOV SLOVARJA *": "NASLOV SLOVARJA *",
"Nastavitve": "Nastavitve",
"Nastavitve portala": "Nastavitve portala",
"Nastavitve računa": "Nastavitve računa",
"Nastavitve slovarjev": "Nastavitve slovarjev",
"Nastavitve svetovalnice": "Nastavitve svetovalnice",
"Nazaj": "Nazaj",
"Nazaj na prijavo": "Nazaj na prijavo",
"Nazaj na zadetke": "Nazaj na zadetke",
"Ne": "Ne",
"Ne izbriši": "Ne izbriši",
"Ne morete imeti več kot 5 luščenj!": "Ne morete imeti več kot 5 luščenj!",
"Ne shrani": "Ne shrani",
"nedefinirano": "nedefinirano",
"Nemško-slovenski slovar tehnike in naravoslovja": "Nemško-slovenski slovar tehnike in naravoslovja",
"Neobjavljeni": "Neobjavljeni",
"Nepravilna vrednost strani": "Nepravilna vrednost strani",
"Nepravilno uporabniško ime, elektronski naslov ali geslo.": "Nepravilno uporabniško ime, elektronski naslov ali geslo.",
"Nepregledani": "Nepregledani",
"Neveljavna e-pošta": "Neveljavna e-pošta",
"Neveljavni": "Neveljavni",
"Neveljavni elektronski naslov.": "Neveljavni elektronski naslov.",
"Ni področja": "Ni področja",
"Ni še dodanih uvozov.": "Ni še dodanih uvozov.",
"Ni še povezanih portalov": "Ni še povezanih portalov",
"Ni slovarskih sestavkov za urejanje.": "Ni slovarskih sestavkov za urejanje.",
"Ni vprašanj.": "Ni vprašanj.",
"Ni zadetkov": "Ni zadetkov",
"Nimate izbranega področja CERIF.": "Nimate izbranega področja CERIF.",
"Nimate luščenj za urejanje.": "Nimate luščenj za urejanje.",
"Nimate povezav za urejanje.": "Nimate povezav za urejanje.",
"Nimate slovarjev za urejanje.": "Nimate slovarjev za urejanje.",
"Nimate ustreznih pravic": "Nimate ustreznih pravic",
"Niste izbrali glavnega področja.": "Niste izbrali glavnega področja.",
"Niste vnesli področnih oznak.": "Niste vnesli področnih oznak.",
"Niste vpisali angleškega naslova slovarja.": "Niste vpisali angleškega naslova slovarja.",
"Niste vpisali imena": "Niste vpisali imena",
"Niste vpisali imena slovarja.": "Niste vpisali imena slovarja.",
"Niste vpisali naslova slovarja.": "Niste vpisali naslova slovarja.",
"Niste vpisali opisa terminološkega problema.": "Niste vpisali opisa terminološkega problema.",
"Niste vpisali priimka.": "Niste vpisali priimka.",
"Niste vpisali tujih jezikov.": "Niste vpisali tujih jezikov.",
"Nov": "Nov",
"Nov avtor": "Nov avtor",
"NOV AVTOR SLOVARJA": "NOV AVTOR SLOVARJA",
"Nov povezan termin.": "Nov povezan termin.",
"Nov slovar": "Nov slovar",
"NOV UPORABNIK": "NOV UPORABNIK",
"Nov video": "Nov video",
"NOV VIDEO": "NOV VIDEO",
"Nov video.": "Nov video.",
"Nov zvok": "Nov zvok",
"NOV ZVOK": "NOV ZVOK",
"Nov zvok.": "Nov zvok.",
"Nova povezava": "Nova povezava",
"Nova slika": "Nova slika",
"NOVA SLIKA": "NOVA SLIKA",
"Nova slika.": "Nova slika.",
"Novo": "Novo",
"NOVO GESLO": "NOVO GESLO",
"Novo podpodročje": "Novo podpodročje",
"NOVO PODPODROČJE": "NOVO PODPODROČJE",
"NOVO PODPODROČJE (angleško)": "NOVO PODPODROČJE (angleško)",
"Novo podpodročje (angleško).": "Novo podpodročje (angleško).",
"NOVO PODPODROČJE (slovensko)": "NOVO PODPODROČJE (slovensko)",
"Novo terminološko vprašanje": "Novo terminološko vprašanje",
"Novo vprašanje": "Novo vprašanje",
"Novo vprašanje za Terminološko svetovalnico": "Novo vprašanje za Terminološko svetovalnico",
"O slovarju": "O slovarju",
"O terminu": "O terminu",
"Objava med urejanjem": "Objava med urejanjem",
"Objava terminološkega vprašanja": "Objava terminološkega vprašanja",
"Objava vseh slovarskih sestavkov": "Objava vseh slovarskih sestavkov",
"Objavi": "Objavi",
"OBJAVI": "OBJAVI",
"Objavljeni": "Objavljeni",
"Objavljeno": "Objavljeno",
"Oblike": "Oblike",
"Oblike, naglasi, izgovor": "Oblike, naglasi, izgovor",
"Obljavljeno": "Obljavljeno",
"Obnovi": "Obnovi",
"Obstoječe poimenovalne rešitve": "Obstoječe poimenovalne rešitve",
"OBSTOJEČE POIMENOVALNE REŠITVE": "OBSTOJEČE POIMENOVALNE REŠITVE",
"Obvestilo": "Obvestilo",
"Obvestilo o številu gesel": "Obvestilo o številu gesel",
"Odgovor": "Odgovor",
"Odgovori": "Odgovori",
"Odgovori na vprašanja:": "Odgovori na vprašanja:",
"Odjava": "Odjava",
"odprt": "odprt",
"Odprt": "Odprt",
"Odstrani": "Odstrani",
"Omogočeno": "Omogočeno",
"OPIS PORTALA": "OPIS PORTALA",
"OPIS SLOVARJA": "OPIS SLOVARJA",
"Opis terminološkega problema": "Opis terminološkega problema",
"OPIS TERMINOLOŠKEGA PROBLEMA": "OPIS TERMINOLOŠKEGA PROBLEMA",
"Opis terminološkega problema:": "Opis terminološkega problema:",
"Opis terminološkega problema: ": "Opis terminološkega problema: ",
"Osnovne nastavitve": "Osnovne nastavitve",
"Osnovni podatki": "Osnovni podatki",
"Ožji": "Ožji",
"Oznaka portala": "Oznaka portala",
"OZNAKA PORTALA *": "OZNAKA PORTALA *",
"Po koncu urejanja vseh slovarskih sestavkov lahko slovar objavite in ga tako prikažete na javnem delu terminološkega portala.": "Po koncu urejanja vseh slovarskih sestavkov lahko slovar objavite in ga tako prikažete na javnem delu terminološkega portala.",
"Počisti filtre": "Počisti filtre",
"POČISTI FILTRE": "POČISTI FILTRE",
"Podnaslov": "Podnaslov",
"Podpodročja": "Podpodročja",
"Podpodročja so namenjena podrobnejšemu razvrščanju terminoloških slovarjev. Administrator mora vsa novo predlagana podpodročja potrditi, preden jih lahko na seznamu vidijo tudi drugi uporabniki portala.": "Podpodročja so namenjena podrobnejšemu razvrščanju terminoloških slovarjev. Administrator mora vsa novo predlagana podpodročja potrditi, preden jih lahko na seznamu vidijo tudi drugi uporabniki portala.",
"Podpodročje": "Podpodročje",
"PODPODROČJE": "PODPODROČJE",
"Podpodročje.": "Podpodročje.",
"Področja": "Področja",
"Področje": "Področje",
"PODROČJE": "PODROČJE",
"PODROČJE *": "PODROČJE *",
"Področje, v katero sodi opisani terminološki problem.": "Področje, v katero sodi opisani terminološki problem.",
"Področje.": "Področje.",
"Področna oznaka": "Področna oznaka",
"PODROČNA OZNAKA": "PODROČNA OZNAKA",
"PODROČNA OZNAKA:": "PODROČNA OZNAKA:",
"Področne oznake": "Področne oznake",
"PODROČNE OZNAKE": "PODROČNE OZNAKE",
"Podvoji": "Podvoji",
"PODVOJI": "PODVOJI",
"Poiščite naslove terminoloških slovarjev, s katerimi želite dopolniti iskalne zadetke na svojem portalu. Lahko izberete vse slovarje ali le nekatere. Svoje izbire morate shraniti. Vse vaše izbire mora potrditi administrator povezanega portala.": "Poiščite naslove terminoloških slovarjev, s katerimi želite dopolniti iskalne zadetke na svojem portalu. Lahko izberete vse slovarje ali le nekatere. Svoje izbire morate shraniti. Vse vaše izbire mora potrditi administrator povezanega portala.",
"Pojasnilo": "Pojasnilo",
"POJASNILO": "POJASNILO",
"POJASNILO:": "POJASNILO:",
"POJAVITVE": "POJAVITVE",
"Poleg objave slovarskega sestavka v fazi \"Urejeno\" je omogočena tudi objava v fazi \"V urejanju\".": "Poleg objave slovarskega sestavka v fazi \"Urejeno\" je omogočena tudi objava v fazi \"V urejanju\".",
"Polja naslov, vprašanje in mnenje so obvezna!": "Polja naslov, vprašanje in mnenje so obvezna!",
"Polno ime terminološkega portala.": "Polno ime terminološkega portala.",
"Pomoč": "Pomoč",
"Ponastavi geslo": "Ponastavi geslo",
"Ponovi Geslo": "Ponovi Geslo",
"PONOVI GESLO": "PONOVI GESLO",
"PONOVI NOVO GESLO": "PONOVI NOVO GESLO",
"Portal": "Portal",
"Portal lahko povežete s Terminološko svetovalnico ZRC SAZU in tako prikazujete terminološke odgovore na svojem portalu, lahko pa vklopite lastno svetovalnico. V tem primeru morate med registriranimi uporabniki izbrati svetovalce in urednika svetovalnice, ki bodo odgovarjali na terminološka vprašanja uporabnikov.": "Portal lahko povežete s Terminološko svetovalnico ZRC SAZU in tako prikazujete terminološke odgovore na svojem portalu, lahko pa vklopite lastno svetovalnico. V tem primeru morate med registriranimi uporabniki izbrati svetovalce in urednika svetovalnice, ki bodo odgovarjali na terminološka vprašanja uporabnikov.",
"Portali": "Portali",
"Pošlji": "Pošlji",
"Potrdi": "Potrdi",
"POTRDI": "POTRDI",
"Potrditev objave": "Potrditev objave",
"Potrjen": "Potrjen",
"Povezani slovarji": "Povezani slovarji",
"Povezani termin": "Povezani termin",
"POVEZANI TERMIN": "POVEZANI TERMIN",
"POVEZANI TERMIN:": "POVEZANI TERMIN:",
"POVEZAVA": "POVEZAVA",
"Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.": "Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.",
"Povezave": "Povezave",
"Povezave s portali": "Povezave s portali",
"Pozabljeno geslo": "Pozabljeno geslo",
"Pozdravljeni ": "Pozdravljeni ",
"Pozor": "Pozor",
"Prazno obvezno polje": "Prazno obvezno polje",
"Preden naložite besedila, jih shranite v besedilni obliki (končnica .txt).": "Preden naložite besedila, jih shranite v besedilni obliki (končnica .txt).",
"Predlog": "Predlog",
"Predogled": "Predogled",
"Pregledani": "Pregledani",
"Prekinjen": "Prekinjen",
"Prekliči": "Prekliči",
"Preverite, če ste pravilno vpisali uporabniško ime. Bodite pozorni na velike in male črke.": "Preverite, če ste pravilno vpisali uporabniško ime. Bodite pozorni na velike in male črke.",
"Prevod": "Prevod",
"Pri brisanju datoteke je prišlo do napake.": "Pri brisanju datoteke je prišlo do napake.",
"Priimek": "Priimek",
"PRIIMEK": "PRIIMEK",
"Priimek registriranega uporabnika.": "Priimek registriranega uporabnika.",
"Prijava": "Prijava",
"PRIJAVA": "PRIJAVA",
"Prijava uspešna": "Prijava uspešna",
"Prikaži datume": "Prikaži datume",
"Prikaži filtre": "Prikaži filtre",
"PRIKAŽI VSE": "PRIKAŽI VSE",
"Primeri rabe": "Primeri rabe",
"PRIMERI RABE V BESEDILIH": "PRIMERI RABE V BESEDILIH",
"Pripravljeno": "Pripravljeno",
"Prišlo je do napake pri pošiljanju sporočila na vaš elektronski naslov. Poskusite ponovno.": "Prišlo je do napake pri pošiljanju sporočila na vaš elektronski naslov. Poskusite ponovno.",
"Prišlo je do napake.": "Prišlo je do napake.",
"Prišlo je do strežniške napake. Poskusite kasneje.": "Prišlo je do strežniške napake. Poskusite kasneje.",
"Prosim, ponovno vnesite geslo": "Prosim, ponovno vnesite geslo",
"Prosim, vnesite geslo": "Prosim, vnesite geslo",
"RAČUN": "RAČUN",
"Razumem": "Razumem",
"registracija": "registracija",
"Registracija": "Registracija",
"Registracija uporabnika je potrjena.": "Registracija uporabnika je potrjena.",
"Registracija uspešna": "Registracija uspešna",
"Registrirani uporabniki lahko dobijo različne vloge na portalu. Kot administrator jim lahko dodelite tudi vlogo skrbnika slovarjev in/ali skrbnika svetovalnice.": "Registrirani uporabniki lahko dobijo različne vloge na portalu. Kot administrator jim lahko dodelite tudi vlogo skrbnika slovarjev in/ali skrbnika svetovalnice.",
"REPUBLIKA SLOVENIJA MINISTRSTVO ZA KULTURO": "REPUBLIKA SLOVENIJA MINISTRSTVO ZA KULTURO",
"rezultat": "rezultat",
"Rezultat luščenja so terminološki kandidati. Z uvozom rezultatov posameznega luščenja lahko dopolnite geslovnik terminološkega slovarja, ki ga urejate.": "Rezultat luščenja so terminološki kandidati. Z uvozom rezultatov posameznega luščenja lahko dopolnite geslovnik terminološkega slovarja, ki ga urejate.",
"rezultata": "rezultata",
"rezultati": "rezultati",
"Rezultati: ": "Rezultati: ",
"rezultatov": "rezultatov",
"S ključnimi besedami, ki so vključene v večino znanstvenih in strokovnih, lahko bolj natančno izberete besedila, ki jih želite uporabiti za luščenje terminoloških kandidatov.": "S ključnimi besedami, ki so vključene v večino znanstvenih in strokovnih, lahko bolj natančno izberete besedila, ki jih želite uporabiti za luščenje terminoloških kandidatov.",
"S prijavo pridobite možnost uporabe vseh funkcij terminološkega portala.": "S prijavo pridobite možnost uporabe vseh funkcij terminološkega portala.",
"S tem dejanjem boste izbrisali vse slovarske sestavke v slovarju in vse metapodatke. Dejanja ni mogoče razveljaviti.": "S tem dejanjem boste izbrisali vse slovarske sestavke v slovarju in vse metapodatke. Dejanja ni mogoče razveljaviti.",
"S tem dejanjem boste izbrisali vse slovarske sestavke v slovarju. Metapodatki bodo ostali. Dejanja ni mogoče razveljaviti.": "S tem dejanjem boste izbrisali vse slovarske sestavke v slovarju. Metapodatki bodo ostali. Dejanja ni mogoče razveljaviti.",
"Še niste registriran uporabnik, registrirajte se.": "Še niste registriran uporabnik, registrirajte se.",
"Settings": "Settings",
"Seznam": "Seznam",
"Seznam luščenj": "Seznam luščenj",
"Seznam neželenih terminov": "Seznam neželenih terminov",
"Seznam objavljenih vprašanj.": "Seznam objavljenih vprašanj.",
"Seznam povezav": "Seznam povezav",
"Seznam slovarjev": "Seznam slovarjev",
"Seznam slovarjev s povezanega portala.": "Seznam slovarjev s povezanega portala.",
"Seznam svetovalcev": "Seznam svetovalcev",
"Seznam terminoloških kandidatov": "Seznam terminoloških kandidatov",
"Seznam terminoloških kandidatov bo natančnejši, če boste dodali tudi seznam neželenih besed, ki naj jih luščilnik izloči iz seznama. Seznam lahko vsebuje splošne termine, npr. tabela, kazalnik, ali pogoste slovnične besede, zlasti veznike, pomožne glagole.": "Seznam terminoloških kandidatov bo natančnejši, če boste dodali tudi seznam neželenih besed, ki naj jih luščilnik izloči iz seznama. Seznam lahko vsebuje splošne termine, npr. tabela, kazalnik, ali pogoste slovnične besede, zlasti veznike, pomožne glagole.",
"Seznam terminoloških kandidatov, ki so rezultat izbranega luščenja.": "Seznam terminoloških kandidatov, ki so rezultat izbranega luščenja.",
"Seznam uporabnikov": "Seznam uporabnikov",
"Seznam urejenih vprašanj, ki čakajo na potrditev dokončne objave.": "Seznam urejenih vprašanj, ki čakajo na potrditev dokončne objave.",
"Seznam vprašanj, ki so jih moderatorji zavrnili in jih je treba dodeliti nekomu drugemu.": "Seznam vprašanj, ki so jih moderatorji zavrnili in jih je treba dodeliti nekomu drugemu.",
"Seznam vprašanj, ki so jih poslali uporabniki, in še niso bila dodeljena moderatorjem.": "Seznam vprašanj, ki so jih poslali uporabniki, in še niso bila dodeljena moderatorjem.",
"Seznam vprašanj, ki so še v urejanju.": "Seznam vprašanj, ki so še v urejanju.",
"Seznam vseh slovarjev, ki jih uredniki, sicer registrirani uporabniki, urejajo na tem portalu.": "Seznam vseh slovarjev, ki jih uredniki, sicer registrirani uporabniki, urejajo na tem portalu.",
"Seznam vseh slovarjev, ki so na tem portalu na voljo uporabnikom.": "Seznam vseh slovarjev, ki so na tem portalu na voljo uporabnikom.",
"Seznam zadetkov": "Seznam zadetkov",
"Shemo za format xml si prenesete <a href=\"/slovarji/xml-schema\" target=\"_blank\">tukaj</a>.": "Shemo za format xml si prenesete <a href=\"/slovarji/xml-schema\" target=\"_blank\">tukaj</a>.",
"Shrani": "Shrani",
"Shranjeno": "Shranjeno",
"Shranjujem ...": "Shranjujem ...",
"Sinhroniziraj": "Sinhroniziraj",
"Sinonim": "Sinonim",
"SINONIM:": "SINONIM:",
"Sinonimi": "Sinonimi",
"SINONIMI": "SINONIMI",
"Širši": "Širši",
"SKRAJŠANI NASLOV SLOVARJA": "SKRAJŠANI NASLOV SLOVARJA",
"Skrbnik portala": "Skrbnik portala",
"Skrbnik slovarjev": "Skrbnik slovarjev",
"Skrbnik svetovalnice": "Skrbnik svetovalnice",
"Skrbniki portala": "Skrbniki portala",
"slika": "slika",
"Slika": "Slika",
"SLIKA": "SLIKA",
"SLIKA:": "SLIKA:",
"Slovar": "Slovar",
"Slovar je bil izbrisan.": "Slovar je bil izbrisan.",
"Slovar je odprt.": "Slovar je odprt.",
"Slovar je pregledal jezikoslovec.": "Slovar je pregledal jezikoslovec.",
"Slovar je pregledal področni strokovnjak.": "Slovar je pregledal področni strokovnjak.",
"Slovar je v odpiranju - čaka na potrditev skrbnika slovarjev": "Slovar je v odpiranju - čaka na potrditev skrbnika slovarjev",
"Slovar odprt": "Slovar odprt",
"SLOVAR UVOŽEN": "SLOVAR UVOŽEN",
"Slovarji": "Slovarji",
"Slovarji portala": "Slovarji portala",
"Slovarski sestavek je neveljaven": "Slovarski sestavek je neveljaven",
"Slovarski sestavek je pregledan in dokončan.": "Slovarski sestavek je pregledan in dokončan.",
"Slovarski sestavek je v fazi, ko se dodajajo in oblikujejo elementi, ki ste jih izbrali v strukturi slovarskega sestavka.": "Slovarski sestavek je v fazi, ko se dodajajo in oblikujejo elementi, ki ste jih izbrali v strukturi slovarskega sestavka.",
"Slovarski sestavki so bili izbrisani.": "Slovarski sestavki so bili izbrisani.",
"Slovarski sestavki so bili objavljeni.": "Slovarski sestavki so bili objavljeni.",
"SLOVARSKIH SESTAVKOV": "SLOVARSKIH SESTAVKOV",
"Slovenščina": "Slovenščina",
"Soglašam s <a target=\"_blank\" href=\"/politika-zasebnosti\">politiko zasebnosti</a>": "Soglašam s <a target=\"_blank\" href=\"/politika-zasebnosti\">politiko zasebnosti</a>",
"Sorodni": "Sorodni",
"Sprememba stanja slovarja": "Sprememba stanja slovarja",
"Spremeni geslo": "Spremeni geslo",
"SPREMENJEN": "SPREMENJEN",
"ŠT. GESEL": "ŠT. GESEL",
"STARO GESLO": "STARO GESLO",
"Statistika": "Statistika",
"Status": "Status",
"STATUS": "STATUS",
"Ste pozabili geslo? Napišite svoje uporabniško ime ali elektronski naslov, ki ste ga uporabili ob registraciji. Na ta naslov vam bomo poslali sporočilo, s pomočjo katerega boste lahko vnesli novo geslo.": "Ste pozabili geslo? Napišite svoje uporabniško ime ali elektronski naslov, ki ste ga uporabili ob registraciji. Na ta naslov vam bomo poslali sporočilo, s pomočjo katerega boste lahko vnesli novo geslo.",
"Število dokumentov": "Število dokumentov",
"Število slovarskih sestavkov je manjše od zahtevanega števila - povečajte število slovarskih sestavkov, sicer ga skrbnik slovarjev lahko zapre": "Število slovarskih sestavkov je manjše od zahtevanega števila - povečajte število slovarskih sestavkov, sicer ga skrbnik slovarjev lahko zapre",
"Število slovarskih sestavkov, ki jih mora vsebovati slovar, da je omogočena objava slovarja na portalu.": "Število slovarskih sestavkov, ki jih mora vsebovati slovar, da je omogočena objava slovarja na portalu.",
"Število terminov": "Število terminov",
"Število vprašanj:": "Število vprašanj:",
"ŠTEVILO ZADETKOV NA STRANI": "ŠTEVILO ZADETKOV NA STRANI",
"Število zadnjih kopij spremenjenih slovarskih sestavkov, ki se hranijo. Če vrednost ni določena, omejitve ni.": "Število zadnjih kopij spremenjenih slovarskih sestavkov, ki se hranijo. Če vrednost ni določena, omejitve ni.",
"Stop termini": "Stop termini",
"Stran ne obstaja": "Stran ne obstaja",
"Strežnik ni dosegljiv. Poskusite kasneje.": "Strežnik ni dosegljiv. Poskusite kasneje.",
"Strinjam se s <a target=\"_blank\" href=\"/pogoji-uporabe\">pogoji uporabe</a>": "Strinjam se s <a target=\"_blank\" href=\"/pogoji-uporabe\">pogoji uporabe</a>",
"Strokovni pregled": "Strokovni pregled",
"Strokovno pregledano": "Strokovno pregledano",
"Struktura": "Struktura",
"STRUKTURA - ELEMENTI SLOVARSKEGA SESTAVKA:": "STRUKTURA - ELEMENTI SLOVARSKEGA SESTAVKA:",
"Struktura slovarskega sestavka": "Struktura slovarskega sestavka",
"Svetovalci": "Svetovalci",
"Svetovalec": "Svetovalec",
"Svetovalnica": "Svetovalnica",
"Svetovanje": "Svetovanje",
"Svoj terminološki portal lahko povežete še z drugimi terminološkimi portali in med iskalnimi prikazujete tudi njihove zadetke.": "Svoj terminološki portal lahko povežete še z drugimi terminološkimi portali in med iskalnimi prikazujete tudi njihove zadetke.",
"Ta slovar nima opisa": "Ta slovar nima opisa",
"Termin": "Termin",
"TERMIN": "TERMIN",
"TERMIN:": "TERMIN:",
"TERMINI": "TERMINI",
"Terminološka svetovalnica je namenjena širši strokovni javnosti, ki se sooča s konkretnimi poimenovalnimi problemi, pa naj gre za popolnoma nove pojme, ki jih je v slovenščini šele treba poimenovati, ali že znane pojme, za katere obstaja več poimenovanj.": "Terminološka svetovalnica je namenjena širši strokovni javnosti, ki se sooča s konkretnimi poimenovalnimi problemi, pa naj gre za popolnoma nove pojme, ki jih je v slovenščini šele treba poimenovati, ali že znane pojme, za katere obstaja več poimenovanj.",
"Terminološka svetovalnica ZRC SAZU": "Terminološka svetovalnica ZRC SAZU",
"Terminološke slovarje lahko v celoti ali po izbranih kriterijih izvozite v različnih formatih in shranite na svojem računalniku.": "Terminološke slovarje lahko v celoti ali po izbranih kriterijih izvozite v različnih formatih in shranite na svojem računalniku.",
"Terminološkemu slovarju določite status. Izbirate lahko med zaprt, v urejanju in odprt.": "Terminološkemu slovarju določite status. Izbirate lahko med zaprt, v urejanju in odprt.",
"Terminološki kandidati": "Terminološki kandidati",
"Terminološko svetovanje": "Terminološko svetovanje",
"Tip napake 1 (ni slovenskega termina)": "Tip napake 1 (ni slovenskega termina)",
"Tip napake 2 (ni definicije ali tujejezičnega termina)": "Tip napake 2 (ni definicije ali tujejezičnega termina)",
"Tu so zbrani vsi komentarji, povezani z vašim slovarjem.": "Tu so zbrani vsi komentarji, povezani z vašim slovarjem.",
"Tuj sinonim": "Tuj sinonim",
"Tuj termin": "Tuj termin",
"Tuja definicija": "Tuja definicija",
"Tuji jezik": "Tuji jezik",
"TUJI JEZIKI": "TUJI JEZIKI",
"Tuji slovarji": "Tuji slovarji",
"TUJI TERMIN": "TUJI TERMIN",
"Tukaj lahko izbrišete svoj račun.": "Tukaj lahko izbrišete svoj račun.",
"Tukaj lahko ponovno ideksirate slovar.": "Tukaj lahko ponovno ideksirate slovar.",
"Tukaj lahko spremenite geslo za prijavo.": "Tukaj lahko spremenite geslo za prijavo.",
"Tukaj lahko spremenite ime, priimek in elektronski naslov uporabnika.": "Tukaj lahko spremenite ime, priimek in elektronski naslov uporabnika.",
"Tukaj lahko spremenite nekatere nastavitve, vezane na posameznega uporabnika.": "Tukaj lahko spremenite nekatere nastavitve, vezane na posameznega uporabnika.",
"Unikatna oznaka povezanega terminološkega portala za prikaz na vašem portalu, sestavljena iz dveh črk ali črke in številke.": "Unikatna oznaka povezanega terminološkega portala za prikaz na vašem portalu, sestavljena iz dveh črk ali črke in številke.",
"Uporabi": "Uporabi",
"Uporabnik": "Uporabnik",
"UPORABNIK": "UPORABNIK",
"Uporabnik na tem mestu izdela specializirani korpus iz besedil, ki jih je zbral in shranil sam. Besedila naj bodo izbrana po načelih tvorjenja specializiranih korpusov. Za uspešno luščenje priporočamo najmanj 10 besedil. Vsa besedila naj bodo shranjena v besedilnem formatu (.txt). Luščenje podpira tudi formate .docx in .pdf, vendar so rezultati slabši.": "Uporabnik na tem mestu izdela specializirani korpus iz besedil, ki jih je zbral in shranil sam. Besedila naj bodo izbrana po načelih tvorjenja specializiranih korpusov. Za uspešno luščenje priporočamo najmanj 10 besedil. Vsa besedila naj bodo shranjena v besedilnem formatu (.txt). Luščenje podpira tudi formate .docx in .pdf, vendar so rezultati slabši.",
"Uporabniki": "Uporabniki",
"Uporabnikovi dokumenti": "Uporabnikovi dokumenti",
"Uporabniške pravice/vloge": "Uporabniške pravice/vloge",
"Uporabniške vloge": "Uporabniške vloge",
"Uporabniški korpus": "Uporabniški korpus",
"Uporabniški račun še ni aktiviran. Kliknite aktivacijsko povezavo, katero smo vam poslali po elektronski pošti.": "Uporabniški račun še ni aktiviran. Kliknite aktivacijsko povezavo, katero smo vam poslali po elektronski pošti.",
"Uporabniško ime": "Uporabniško ime",
"UPORABNIŠKO IME": "UPORABNIŠKO IME",
"Uporabniško ime ali elektronski naslov": "Uporabniško ime ali elektronski naslov",
"Uporabniško ime že obstaja": "Uporabniško ime že obstaja",
"Uredi": "Uredi",
"UREDI": "UREDI",
"Uredi lastnosti": "Uredi lastnosti",
"Uredi povezavo": "Uredi povezavo",
"Uredi vsebino": "Uredi vsebino",
"Urednik slovarja": "Urednik slovarja",
"Urejanje": "Urejanje",
"Urejanje terminološkega odgovora": "Urejanje terminološkega odgovora",
"Urejeni": "Urejeni",
"Urejeno": "Urejeno",
"URL naslov terminološkega portala, s katerim boste sinhronizirali podatke iz terminoloških virov.": "URL naslov terminološkega portala, s katerim boste sinhronizirali podatke iz terminoloških virov.",
"URL naslov terminološkega portala, s katerim želimo povezati svoj portal.": "URL naslov terminološkega portala, s katerim želimo povezati svoj portal.",
"URL naslov terminološkega portala, s katerim želimo povezati svoj portal. (API klic)": "URL naslov terminološkega portala, s katerim želimo povezati svoj portal. (API klic)",
"URL naslov terminološkega portala, s katerim želite povezati svoj portal. (API klic)": "URL naslov terminološkega portala, s katerim želite povezati svoj portal. (API klic)",
"URL povezava do mnenja:": "URL povezava do mnenja:",
"URL za sinhronizacijo slovarjev *": "URL za sinhronizacijo slovarjev *",
"URL za sinhronizacijo slovarskih sestavkov *": "URL za sinhronizacijo slovarskih sestavkov *",
"Uspešno ste ponastavili svoje geslo.": "Uspešno ste ponastavili svoje geslo.",
"Ustvari": "Ustvari",
"USTVARJEN": "USTVARJEN",
"Ustvarjeno novo vprašanje v svetovalnici": "Ustvarjeno novo vprašanje v svetovalnici",
"UTEŽ": "UTEŽ",
"Uvoz": "Uvoz",
"Uvoz iz datoteke": "Uvoz iz datoteke",
"Uvoz iz luščilnika": "Uvoz iz luščilnika",
"UVOZI": "UVOZI",
"Uvozi termine od številke": "Uvozi termine od številke",
"V delu": "V delu",
"V DRUGI VSEBINI": "V DRUGI VSEBINI",
"V IZTOČNICAH": "V IZTOČNICAH",
"V obdelavi": "V obdelavi",
"V odpiranju": "V odpiranju",
"v predogledu": "v predogledu",
"V slovarju so samo terminološki kandidati. Slovarja ne morete objaviti.": "V slovarju so samo terminološki kandidati. Slovarja ne morete objaviti.",
"V tem razdelku lahko določite elemente slovarskega sestavka. Z izbiro elementov se vam prikazuje podoba slovarskega sestavka. Izbiro lahko tudi med urejanjem vsebine kadarkoli spremenite.": "V tem razdelku lahko določite elemente slovarskega sestavka. Z izbiro elementov se vam prikazuje podoba slovarskega sestavka. Izbiro lahko tudi med urejanjem vsebine kadarkoli spremenite.",
"V tem razdelku lahko določite glavne administratorske pravice na terminološkem portalu.": "V tem razdelku lahko določite glavne administratorske pravice na terminološkem portalu.",
"V tem razdelku lahko določite uporabniške vloge posameznega uporabnika in urejate njegove podatke.": "V tem razdelku lahko določite uporabniške vloge posameznega uporabnika in urejate njegove podatke.",
"V TUJEJEZIČNIH USTREZNIKIH": "V TUJEJEZIČNIH USTREZNIKIH",
"V urejanju": "V urejanju",
"Vaš komentar je brez vsebine.": "Vaš komentar je brez vsebine.",
"Vaše iskanje ni bilo uspešno. Vpišite novo iskalno poizvedbo in poizkusite znova.": "Vaše iskanje ni bilo uspešno. Vpišite novo iskalno poizvedbo in poizkusite znova.",
"Več ...": "Več ...",
"Več …": "Več …",
"Več...": "Več...",
"VELIKOST": "VELIKOST",
"Veljavni": "Veljavni",
"Veljavni elektronski naslov uporabnika, na katerega uporabnik prejema sporočila, povezana s portalom.": "Veljavni elektronski naslov uporabnika, na katerega uporabnik prejema sporočila, povezana s portalom.",
"Veljavno": "Veljavno",
"Veljavnost": "Veljavnost",
"Verzija": "Verzija",
"Verzija 1": "Verzija 1",
"Verzija:": "Verzija:",
"video": "video",
"Video": "Video",
"VIDEO": "VIDEO",
"VIDEO:": "VIDEO:",
"Vidno": "Vidno",
"Vir": "Vir",
"Viri": "Viri",
"Vnesite e-naslov, ki ga za obveščanje o novih terminoloških vprašanjih uporabljajo terminološki svetovalci.": "Vnesite e-naslov, ki ga za obveščanje o novih terminoloških vprašanjih uporabljajo terminološki svetovalci.",
"Vnesite naslov spletnega mesta, kjer so zbrani vsi odgovori terminološke svetovalnice.": "Vnesite naslov spletnega mesta, kjer so zbrani vsi odgovori terminološke svetovalnice.",
"Vnesite novo geslo. Ko ga potrdite, se boste v vaš uporabniški račun lahko spet prijavili z novim geslom.": "Vnesite novo geslo. Ko ga potrdite, se boste v vaš uporabniški račun lahko spet prijavili z novim geslom.",
"Vnesite podatke terminološkega portala, s katerim želite povezati svoj portal.": "Vnesite podatke terminološkega portala, s katerim želite povezati svoj portal.",
"Vnesite podatke, ki niso sistemsko vključeni v druga polja, npr. vir, zgled rabe.": "Vnesite podatke, ki niso sistemsko vključeni v druga polja, npr. vir, zgled rabe.",
"Vnesite polno ime terminološkega portala, s katerim se povezujete.": "Vnesite polno ime terminološkega portala, s katerim se povezujete.",
"Vnesite termin.": "Vnesite termin.",
"Vnesti morate termin": "Vnesti morate termin",
"Vpišite avtorja slovarja, če ste glavni avtor, na prvo mesto zapišite svoje ime.": "Vpišite avtorja slovarja, če ste glavni avtor, na prvo mesto zapišite svoje ime.",
"Vpišite definicijo v tujem jeziku.": "Vpišite definicijo v tujem jeziku.",
"Vpišite iskalni niz": "Vpišite iskalni niz",
"Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.": "Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.",
"Vpišite podpodročje glavnega področja, ki ste ga izbrali. Če podpodročja ni med naborom, izberite polje Novo podpodročje.": "Vpišite podpodročje glavnega področja, ki ste ga izbrali. Če podpodročja ni med naborom, izberite polje Novo podpodročje.",
"Vprašanje poslano:": "Vprašanje poslano:",
"VRSTA": "VRSTA",
"VRSTA DOKUMENTA": "VRSTA DOKUMENTA",
"Vsebina": "Vsebina",
"Vsebina slovarja": "Vsebina slovarja",
"Vsebina slovarskega sestavka je bila duplicirana.": "Vsebina slovarskega sestavka je bila duplicirana.",
"Vsi povezani slovarji": "Vsi povezani slovarji",
"Vsi slovarji": "Vsi slovarji",
"Vsi slovarski sestavki": "Vsi slovarski sestavki",
"Vstavite definicijo pojma.": "Vstavite definicijo pojma.",
"Vstavite termine, ki se za definirani pojem tudi uporabljajo.": "Vstavite termine, ki se za definirani pojem tudi uporabljajo.",
"Vstavite ustrezno področno oznako, ki jo želite določiti za posamezni termin.": "Vstavite ustrezno področno oznako, ki jo želite določiti za posamezni termin.",
"Z izbiro te možnosti boste pobrisali samo slovarske sestavke, ki so v določeni fazi urejanja.": "Z izbiro te možnosti boste pobrisali samo slovarske sestavke, ki so v določeni fazi urejanja.",
"Z registracijo pridobite možnost uporabe vseh funkcij terminološkega portala.": "Z registracijo pridobite možnost uporabe vseh funkcij terminološkega portala.",
"Za luščenje terminoloških kandidatov morate biti prijavljeni.": "Za luščenje terminoloških kandidatov morate biti prijavljeni.",
"Za prvo objavo slovarja je potrebno dovoljenje skrbnika slovarjev.": "Za prvo objavo slovarja je potrebno dovoljenje skrbnika slovarjev.",
"Za to besedo še ni podatkov.": "Za to besedo še ni podatkov.",
"Za urejanje slovarjev morate biti prijavljeni.": "Za urejanje slovarjev morate biti prijavljeni.",
"Za zastavljanje terminoloških vprašanj morate biti prijavljeni.": "Za zastavljanje terminoloških vprašanj morate biti prijavljeni.",
"Začetek": "Začetek",
"Začni": "Začni",
"ZAČNI": "ZAČNI",
"Zadnja sprememba": "Zadnja sprememba",
"Zadnji izvozi": "Zadnji izvozi",
"Zadnji objavljeni slovarji": "Zadnji objavljeni slovarji",
"Zadnji odgovori na vprašanja": "Zadnji odgovori na vprašanja",
"Zadnji uvozi": "Zadnji uvozi",
"Zapomni si prijavo": "Zapomni si prijavo",
"Zaporedje:": "Zaporedje:",
"Zapri": "Zapri",
"ZAPRI": "ZAPRI",
"zaprt": "zaprt",
"Zaprt": "Zaprt",
"Zaradi organizacije podatkov na portalu za skrajšani naslov slovarja predlagamo poenobesede\tnje, ki se bo izpisovalo ob slovarju, npr. Davčni terminološki slovar &rarr; Davki.": "Zaradi organizacije podatkov na portalu za skrajšani naslov slovarja predlagamo poenobesede\tnje, ki se bo izpisovalo ob slovarju, npr. Davčni terminološki slovar &rarr; Davki.",
"Zaradi organizacije podatkov na portalu za skrajšani naslov slovarja predlagamo poenobesedenje, ki se bo izpisovalo ob slovarju, npr. Davčni terminološki slovar &rarr; Davki": "Zaradi organizacije podatkov na portalu za skrajšani naslov slovarja predlagamo poenobesedenje, ki se bo izpisovalo ob slovarju, npr. Davčni terminološki slovar &rarr; Davki",
"Zastavi novo vprašanje": "Zastavi novo vprašanje",
"Zastavi terminološko vprašanje": "Zastavi terminološko vprašanje",
"ZAVRJEN": "ZAVRJEN",
"ZAVRNI": "ZAVRNI",
"Zavrnjeno": "Zavrnjeno",
"Zunanji": "Zunanji",
"zvok": "zvok",
"Zvok": "Zvok",
"ZVOK": "ZVOK",
"ZVOK:": "ZVOK:",
"titleTermsOfUse": "Pogoji uporabe",
"titlePrivacyPolicy": "Politika zasebnosti"
}
+50
View File
@@ -0,0 +1,50 @@
{
"helpPageTitleHelp": "Pomoč",
"helpPageTitleAbout": "Splošno o portalu",
"helpPageTitleRegistration": "Registracija",
"helpPageTitleSearchIndex": "Iskanje",
"helpPageTitleSearchBasic": "Osnovno iskanje",
"helpPageTitleSearchAdvanced": "Napredno iskanje",
"helpPageTitleExtractionIndex": "Luščenje",
"helpPageTitleExtractionSpecializedCorpora": "Specializirani korpusi",
"helpPageTitleExtractionPersonalCorpus": "Lastni uporabniški korpus",
"helpPageTitleExtractionOssCorpus": "Korpus OSS",
"helpPageTitleExtractionStopTerms": "Seznami neželenih besed ali \"Stop termini\"",
"helpPageTitleExtractionDomains": "Področja",
"helpPageTitleExtractionTermCandidates": "Terminološki kandidati",
"helpPageTitleEditingIndex": "Urejanje",
"helpPageTitleEditingNewDictionary": "Nov slovar",
"helpPageTitleEditingDictionaryProperties": "Urejanje lastnosti slovarja",
"helpPageTitleEditingUsers": "Uporabniki",
"helpPageTitleEditingStructure": "Struktura slovarskega sestavka",
"helpPageTitleEditingComments": "Komentiranje",
"helpPageTitleEditingDictionaryContent": "Vsebina slovarja",
"helpPageTitleEditingImportingData": "Uvoz podatkov",
"helpPageTitleEditingEntriesIndex": "Dodajanje novih slovarskih sestavkov",
"helpPageTitleEditingEntriesDomainLabels": "Področne oznake",
"helpPageTitleEditingEntriesLabel": "Pojasnilo",
"helpPageTitleEditingEntriesDefinition": "Definicja",
"helpPageTitleEditingEntriesSynonyms": "Sinonimi",
"helpPageTitleEditingEntriesRelatedTerms": "Povezani termini",
"helpPageTitleEditingEntriesOtherLanguages": "Tuji jeziki",
"helpPageTitleEditingEntriesOther": "Drugo",
"helpPageTitleEditingEntriesEditingHistory": "Zgodovina urejanja",
"helpPageTitleEditingEntriesComments": "Komentarji",
"helpPageTitleEditingEntriesDataExport": "Izvoz podatkov",
"helpPageTitleConsultancyIndex": "Svetovanje",
"helpPageTitleConsultancyQuestions": "Pošiljanje vprašanj",
"helpPageTitleConsultancyTerminologyPrinciples": "Terminološka načela",
"helpPageTitleConsultancyLink": "Povezava",
"helpPageTitleConsultancyAnswers": "Objava odgovorov",
"helpPageTitleAdministrationIndex": "Administracija",
"helpPageTitleAdministrationBasicSettingsIndex": "Osnovne nastavitve",
"helpPageTitleAdministrationBasicSettingsPortal": "Portal",
"helpPageTitleAdministrationBasicSettingsDictionaries": "Slovarji",
"helpPageTitleAdministrationBasicSettingsConsultancy": "Svetovalnica",
"helpPageTitleAdministrationLinksToOtherPortalsIndex": "Povezave s portali",
"helpPageTitleAdministrationLinksToOtherPortalsList": "Seznam",
"helpPageTitleAdministrationLinksToOtherPortalsDictionaries": "Slovarji",
"helpPageTitleAdministrationDictionaries": "Slovarji",
"helpPageTitleAdministrationUsers": "Uporabniki",
"helpPageTitleAdministrationSecondaryDomains": "Podpodročja"
}
+127 -2
View File
@@ -6,6 +6,67 @@
padding: 0;
}
body {
color: v.$tp-black;
}
// .check {
// -webkit-appearance: none; /*hides the default checkbox*/
// height: 20px;
// width: 20px;
// position: relative;
// top: 20px;
// left: 20px;
// transition: 0.10s;
// background-color: #FE0006;
// text-align: center;
// font-weight: 600;
// color: white;
// border-radius: 3px;
// outline: none;
// }
/*
input[type="checkbox"]:checked {
appearance: none;
height: 14px;
width: 14px;
font-size: 10px !important;
transition: 0.1s;
background-color: v.$navigation-maincolor;
text-align: center;
// font-weight: 600;
color: white;
border-radius: 3px;
outline: none;
&:before {
content: "";
}
}*/
/*
.check:checked {
background-color: #0E9700;
}
.check:before {
content: "";
}
.check:checked:before {
content: "";
}
.check:hover {
cursor: pointer;
opacity: 0.8;
}
*/
.dropdown-item:active {
background-color: v.$navigation-maincolor;
}
// input:focus-visible,
// button:focus-visible,
// button:focus {
@@ -17,6 +78,15 @@
cursor: pointer;
}
.outline-enabled {
outline: auto;
}
.hover-opacity-blue:hover {
color: v.$navigation-maincolor;
opacity: 0.8;
}
.index-font-size {
font-size: 1rem;
}
@@ -76,10 +146,32 @@
margin-top: 0.25rem;
}
.h-41 {
.rpm-corr {
margin-top: -1.75rem;
}
.h-40px {
height: 40px !important;
}
// Todo check if this is needed
.h-41px {
display: block;
height: 41px !important;
}
// end check
.w320px {
width: 320px !important;
}
.rm-outline:focus {
outline: none;
}
.max-512px {
max-width: 512px !important;
}
.navigation-text-color {
color: v.$navigation-maincolor;
@@ -123,6 +215,11 @@
width: 32px !important;
}
.i40x40 {
height: 40px !important;
width: 40px !important;
}
.height60 {
height: 60px !important;
}
@@ -147,6 +244,10 @@
}
}
.clipx {
overflow-x: hidden;
}
.strength500 {
font-weight: 500;
}
@@ -278,6 +379,14 @@
border: 1px solid v.$gray-2;
}
.mt-minus-1 {
margin-top: -0.25rem;
}
.mt-minus-p3 {
margin-top: -0.3rem;
}
.ps-2rem {
padding-left: 2rem !important;
}
@@ -328,6 +437,7 @@
.pager {
white-space: nowrap;
// min-width: 13.4rem;
* {
display: inline-block;
@@ -337,6 +447,14 @@
opacity: 25%;
}
button {
opacity: 75%;
}
button:not([disabled]):hover {
opacity: 100%;
}
.pager-input {
width: 54px;
height: 38px;
@@ -428,6 +546,9 @@
.termin-offset-up-wide {
top: 85px !important;
max-height: calc(
100vh - 130px + 10px
) !important; // correction due to bigger panel size
}
}
@@ -436,6 +557,10 @@
margin-right: 2rem !important;
}
.me-2rem {
margin-right: 2rem !important;
}
.mx-max-md-2rem {
@media (max-width: v.$layout-breakpoint-medium) {
margin-left: 2rem !important;
@@ -475,7 +600,7 @@
.txt {
font-size: 0.75rem !important;
}
$padding-wide: 3rem;
$padding-wide: 2rem;
img.rsmzk {
height: 30px;
padding-left: $padding-wide;
@@ -241,13 +241,21 @@ html {
border: none;
background: none;
@media screen and (min-width: v.$layout-breakpoint-medium) {
font-size: 1.3rem;
}
&.pdf {
font-size: 1rem;
}
}
.search-button-keyboard {
display: flex;
flex-shrink: 0;
margin: 0 20px;
width: 30px;
// width: 30px;
height: 100%;
line-height: 10px;
align-items: center;
@@ -312,6 +320,7 @@ html {
}
.menutxt {
margin-left: 0.5rem;
display: flex !important;
}
@@ -421,7 +430,7 @@ html {
.search-button-keyboard {
margin: 0 20px;
width: 25px;
// width: 25px;
line-height: 10px;
}
}
@@ -429,7 +438,7 @@ html {
.descriptionArea {
width: 48rem;
padding: 5rem 2rem;
padding: 5rem 2rem 3.5rem; // add specific bottom padding to align with footer padding
margin: 0 auto;
}
@@ -73,7 +73,7 @@
/*responsive*/
@media (max-width: 1192px) {
@media (max-width: v.$layout-breakpoint-x-large) {
.table-users thead {
display: none;
}
@@ -109,6 +109,23 @@
}
}
#page-results {
td {
min-width: 32px;
&.tdata-area {
width: 40%;
}
&.tdata-translation {
width: 40%;
}
&.buttons-group {
min-width: 48px;
}
}
}
#add-area {
height: 41px;
padding: 10px 40px;
@@ -41,7 +41,15 @@ body {
font-weight: 400;
color: #46535b;
text-decoration: none;
display: block;
// display: block;
display: flex;
align-items: center;
// gap: 0.5rem;
> * {
padding: 0;
margin: 0;
}
}
a:visited {
@@ -6,6 +6,11 @@
align-items: center;
}
.pager-container {
display: flex;
width: 130px;
}
.comments-container {
list-style: none;
margin: 0;
@@ -173,7 +178,7 @@ form.comment-form::after {
.comments-top-hr {
color: #b6bec4;
opacity: 0.7;
opacity: 0.8;
margin-top: 0.063rem;
margin-bottom: 0rem;
height: 10%;
@@ -354,7 +359,7 @@ form.comment-reply-form button {
.comments-top-hr {
color: #b6bec4;
opacity: 0.7;
opacity: 0.8;
margin-top: 0.63rem;
margin-bottom: 0rem;
height: 10%;
@@ -450,7 +455,7 @@ form.comment-reply-form button {
.comments-holder {
padding-left: 16px;
}
@media only screen and (min-width: 1200px) {
@media only screen and (min-width: v.$layout-breakpoint-x-large) {
.comments-holder {
padding-left: 36px;
}

Some files were not shown because too many files have changed in this diff Show More