diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fe1d7d9d5a..b6b791dc59 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,5 +3,6 @@ /docs/*.md @benhaynes /packages/shared @nickrum -/packages/extension-sdk @nickrum +/packages/extensions-sdk @nickrum +/packages/create-directus-extension @nickrum /app/vite.config.js @nickrum diff --git a/.github/workflows/sync-dockerhub-readme.yml b/.github/workflows/sync-dockerhub-readme.yml new file mode 100644 index 0000000000..1e8fc57c5e --- /dev/null +++ b/.github/workflows/sync-dockerhub-readme.yml @@ -0,0 +1,24 @@ +name: Sync Readme to Docker Hub + +on: + push: + branches: + - main + paths: # ensures this workflow only runs when the readme.md or this file changes. + - 'readme.md' + - '.github/workflows/sync-dockerhub-readme.yml' + workflow_dispatch: + +jobs: + sync-dockerhub-readme: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Sync Readme to Docker Hub + uses: peter-evans/dockerhub-description@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + readme-filepath: ./readme.md diff --git a/.gitignore b/.gitignore index 2bfd6d11f3..b25c5868be 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ dist app/public/img/docs/* *.tsbuildinfo .e2e-containers.json +coverage diff --git a/Dockerfile b/Dockerfile index 4e488976fc..870de37f6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,5 +34,5 @@ RUN npm install WORKDIR /directus/api -CMD ["sh", "-c", "node ./dist/cli/index.js bootstrap; node ./dist/start.js;"] +CMD ["sh", "-c", "node ./cli.js bootstrap; node ./dist/start.js;"] EXPOSE 8055/tcp diff --git a/api/.gitignore b/api/.gitignore index c813612f69..16f3a346cc 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -10,4 +10,4 @@ test dist tmp keys.json - +coverage diff --git a/api/cli.js b/api/cli.js index d7ce758fbc..6c1cc9f5c0 100755 --- a/api/cli.js +++ b/api/cli.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('./dist/cli/index.js'); +require('./dist/cli/run.js'); diff --git a/api/example.env b/api/example.env index 9ca6f4b637..25ca618190 100644 --- a/api/example.env +++ b/api/example.env @@ -9,18 +9,58 @@ LOG_STYLE="pretty" #################################################################################################### # Database -## PostgreSQL Example +## These match the databases defined in the docker-compose file in the root of this repo + +## Postgres DB_CLIENT="pg" DB_HOST="localhost" -DB_PORT=5432 +DB_PORT=5100 DB_DATABASE="directus" DB_USER="postgres" -DB_PASSWORD="psql1234" +DB_PASSWORD="secret" + +## MySQL 8 +# DB_CLIENT="mysql" +# DB_HOST="localhost" +# DB_PORT=5101 +# DB_DATABASE="directus" +# DB_USER="root" +# DB_PASSWORD="secret" + +## MariaDB +# DB_CLIENT="mysql" +# DB_HOST="localhost" +# DB_PORT=5102 +# DB_DATABASE="directus" +# DB_USER="root" +# DB_PASSWORD="secret" + +## MS SQL +# DB_CLIENT="mssql" +# DB_HOST="localhost" +# DB_PORT=5103 +# DB_DATABASE="directus" +# DB_USER="sa" +# DB_PASSWORD="Test@123" + +## OracleDB +# DB_CLIENT="oracle" +# DB_CONNECT_STRING="localhost:5104/XE" +# DB_USER="secretsysuser" +# DB_PASSWORD="secretpassword" ## SQLite Example # DB_CLIENT="sqlite3" # DB_FILENAME="./data.db" +## MySQL 5.7 +# DB_CLIENT="mysql" +# DB_HOST="localhost" +# DB_PORT=5102 +# DB_DATABASE="directus" +# DB_USER="root" +# DB_PASSWORD="secret" + #################################################################################################### # Rate Limiting @@ -32,45 +72,25 @@ RATE_LIMITER_DURATION=1 RATE_LIMITER_STORE=memory # memory | redis | memcache -## Redis (see https://github.com/animir/node-rate-limiter-flexible/wiki/Redis and -## https://www.npmjs.com/package/ioredis#connect-to-redis) -# RATE_LIMITER_EXEC_EVENLY=false -# RATE_LIMITER_BLOCK_DURATION=0 -# RATE_LIMITER_KEY_PREFIX=rlflx - -# RATE_LIMITER_REDIS="redis://:authpassword@127.0.0.1:6380/4" -# --OR-- -# RATE_LIMITER_REDIS_HOST="127.0.0.1" -# RATE_LIMITER_REDIS_PORT="127.0.0.1" -# RATE_LIMITER_REDIS_PASSWORD="127.0.0.1" -# RATE_LIMITER_REDIS_DB="127.0.0.1" - -## Memcache (see https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache and -## https://www.npmjs.com/package/memcached) -# RATE_LIMITER_MEMCACHE='localhost:11211' +# RATE_LIMITER_REDIS="redis://@127.0.0.1:5105" +# RATE_LIMITER_MEMCACHE="localhost:5109" #################################################################################################### # Caching CACHE_ENABLED=true CACHE_TTL="30m" CACHE_NAMESPACE="directus-cache" -CACHE_STORE=memory -# memory | redis | memcache CACHE_AUTO_PURGE=true +# memory | redis | memcache +CACHE_STORE=memory + ASSETS_CACHE_TTL="30m" -# CACHE_REDIS="redis://:authpassword@127.0.0.1:6380/4" -# --OR-- -# CACHE_REDIS_HOST="127.0.0.1" -# CACHE_REDIS_PORT="127.0.0.1" -# CACHE_REDIS_PASSWORD="127.0.0.1" -# CACHE_REDIS_DB="127.0.0.1" +# CACHE_REDIS="redis://@127.0.0.1:5105" -## Memcache (see https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache and -## https://www.npmjs.com/package/memcached) -# CACHE_MEMCACHE='localhost:11211' +# CACHE_MEMCACHE="localhost:5109" #################################################################################################### # File Storage @@ -113,6 +133,16 @@ CORS_EXPOSED_HEADERS=Content-Range CORS_CREDENTIALS="true" CORS_MAX_AGE=18000 +#################################################################################################### +# Argon2 + +# HASH_MEMORY_COST=81920 +# HASH_HASH_LENGTH=32 +# HASH_TIME_COST=10 +# HASH_PARALLELISM=2 +# HASH_TYPE=2 +# HASH_ASSOCIATED_DATA=foo + #################################################################################################### # SSO (OAuth) Providers diff --git a/api/jest.config.js b/api/jest.config.js new file mode 100644 index 0000000000..86e9e44003 --- /dev/null +++ b/api/jest.config.js @@ -0,0 +1,12 @@ +const base = require('../jest.config.js'); + +require('dotenv').config(); + +module.exports = { + ...base, + roots: ['/src'], + verbose: true, + setupFiles: ['dotenv/config'], + testURL: process.env.TEST_URL || 'http://localhost', + collectCoverageFrom: ['src/**/*.ts'], +}; diff --git a/api/package.json b/api/package.json index 678995363a..3ceffbb4d4 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "directus", - "version": "9.0.0-rc.90", + "version": "9.0.0-rc.92", "license": "GPL-3.0-only", "homepage": "https://github.com/directus/directus#readme", "description": "Directus is a real-time API and App dashboard for managing SQL database content.", @@ -56,7 +56,9 @@ "build": "tsc --build && copyfiles \"src/**/*.*\" -e \"src/**/*.ts\" -u 1 dist", "cleanup": "rimraf dist", "dev": "cross-env NODE_ENV=development SERVE_APP=false ts-node-dev --files --transpile-only --respawn --watch \".env\" --inspect --exit-child -- src/start.ts", - "cli": "cross-env NODE_ENV=development SERVE_APP=false ts-node --script-mode --transpile-only src/cli/index.ts" + "cli": "cross-env NODE_ENV=development SERVE_APP=false ts-node --script-mode --transpile-only src/cli/run.ts", + "test": "jest --coverage", + "test:watch": "jest --watchAll" }, "engines": { "node": ">=12.20.0" @@ -68,19 +70,20 @@ "example.env" ], "dependencies": { - "@directus/app": "9.0.0-rc.90", - "@directus/drive": "9.0.0-rc.90", - "@directus/drive-azure": "9.0.0-rc.90", - "@directus/drive-gcs": "9.0.0-rc.90", - "@directus/drive-s3": "9.0.0-rc.90", - "@directus/format-title": "9.0.0-rc.90", - "@directus/schema": "9.0.0-rc.90", - "@directus/shared": "9.0.0-rc.90", - "@directus/specs": "9.0.0-rc.90", + "@directus/app": "9.0.0-rc.92", + "@directus/drive": "9.0.0-rc.92", + "@directus/drive-azure": "9.0.0-rc.92", + "@directus/drive-gcs": "9.0.0-rc.92", + "@directus/drive-s3": "9.0.0-rc.92", + "@directus/extensions-sdk": "9.0.0-rc.92", + "@directus/format-title": "9.0.0-rc.92", + "@directus/schema": "9.0.0-rc.92", + "@directus/shared": "9.0.0-rc.92", + "@directus/specs": "9.0.0-rc.92", "@godaddy/terminus": "^4.9.0", "@rollup/plugin-alias": "^3.1.2", "@rollup/plugin-virtual": "^2.0.3", - "argon2": "^0.28.1", + "argon2": "^0.28.2", "async": "^3.2.0", "async-mutex": "^0.3.1", "atob": "^2.1.2", @@ -114,7 +117,7 @@ "jsonwebtoken": "^8.5.1", "keyv": "^4.0.3", "knex": "^0.95.6", - "knex-schema-inspector": "1.5.13", + "knex-schema-inspector": "1.6.0", "liquidjs": "^9.25.0", "lodash": "^4.17.21", "macos-release": "^2.4.1", @@ -128,16 +131,17 @@ "openapi3-ts": "^2.0.0", "ora": "^5.4.0", "otplib": "^12.0.1", - "pino": "6.13.0", + "pino": "6.13.2", "pino-colada": "^2.1.0", - "pino-http": "5.6.0", + "pino-http": "5.7.0", "prettier": "^2.3.1", "qs": "^6.9.4", "rate-limiter-flexible": "^2.2.2", "resolve-cwd": "^3.0.0", "rollup": "^2.52.1", - "sharp": "^0.28.3", + "sharp": "^0.29.0", "stream-json": "^1.7.1", + "supertest": "^6.1.6", "update-check": "^1.5.4", "uuid": "^8.3.2", "uuid-validate": "0.0.3", @@ -155,7 +159,7 @@ "nodemailer-mailgun-transport": "^2.1.3", "pg": "^8.6.0", "sqlite3": "^5.0.2", - "tedious": "^11.0.8" + "tedious": "^12.0.0" }, "gitHead": "24621f3934dc77eb23441331040ed13c676ceffd", "devDependencies": { @@ -171,27 +175,31 @@ "@types/express-session": "1.17.4", "@types/flat": "^5.0.2", "@types/fs-extra": "9.0.12", - "@types/inquirer": "7.3.3", - "@types/js-yaml": "4.0.2", + "@types/inquirer": "8.1.1", + "@types/jest": "27.0.1", + "@types/js-yaml": "4.0.3", "@types/json2csv": "5.0.3", - "@types/jsonwebtoken": "8.5.4", - "@types/keyv": "3.1.2", + "@types/jsonwebtoken": "8.5.5", + "@types/keyv": "3.1.3", "@types/lodash": "4.14.172", - "@types/mime-types": "2.1.0", + "@types/mime-types": "2.1.1", "@types/ms": "0.7.31", "@types/node": "15.12.2", "@types/node-cron": "2.0.4", "@types/nodemailer": "6.4.4", - "@types/object-hash": "2.1.1", + "@types/object-hash": "2.2.0", "@types/qs": "6.9.7", - "@types/sharp": "0.28.5", + "@types/sharp": "0.29.1", "@types/stream-json": "1.7.1", + "@types/supertest": "2.0.11", "@types/uuid": "8.3.1", "@types/uuid-validate": "0.0.1", "@types/wellknown": "0.5.1", "copyfiles": "2.4.1", "cross-env": "7.0.3", + "jest": "27.2.0", + "ts-jest": "27.0.5", "ts-node-dev": "1.1.8", - "typescript": "4.3.5" + "typescript": "4.4.3" } } diff --git a/api/src/__mocks__/cache.ts b/api/src/__mocks__/cache.ts new file mode 100644 index 0000000000..ff7440d3b1 --- /dev/null +++ b/api/src/__mocks__/cache.ts @@ -0,0 +1,6 @@ +export const cache = { + get: jest.fn().mockResolvedValue(undefined), + set: jest.fn().mockResolvedValue(true), +}; + +export const getCache = jest.fn().mockReturnValue({ cache }); diff --git a/api/src/app.ts b/api/src/app.ts index b902c987ae..6dcd70c186 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -24,7 +24,7 @@ import settingsRouter from './controllers/settings'; import usersRouter from './controllers/users'; import utilsRouter from './controllers/utils'; import webhooksRouter from './controllers/webhooks'; -import { isInstalled, validateDBConnection, validateMigrations } from './database'; +import { isInstalled, validateDatabaseConnection, validateDatabaseExtensions, validateMigrations } from './database'; import { emitAsyncSafe } from './emitter'; import env from './env'; import { InvalidPayloadException } from './exceptions'; @@ -45,20 +45,19 @@ import { validateStorage } from './utils/validate-storage'; import { register as registerWebhooks } from './webhooks'; import { session } from './middleware/session'; import { flushCaches } from './cache'; -import { URL } from 'url'; +import { Url } from './utils/url'; export default async function createApp(): Promise { validateEnv(['KEY', 'SECRET']); - try { - new URL(env.PUBLIC_URL); - } catch { - logger.warn('PUBLIC_URL is not a valid URL'); + if (!new Url(env.PUBLIC_URL).isAbsolute()) { + logger.warn('PUBLIC_URL should be a full URL'); } await validateStorage(); - await validateDBConnection(); + await validateDatabaseConnection(); + await validateDatabaseExtensions(); if ((await isInstalled()) === false) { logger.error(`Database doesn't have Directus tables installed.`); @@ -126,11 +125,14 @@ export default async function createApp(): Promise { if (env.SERVE_APP) { const adminPath = require.resolve('@directus/app/dist/index.html'); - const publicUrl = env.PUBLIC_URL.endsWith('/') ? env.PUBLIC_URL : env.PUBLIC_URL + '/'; + const adminUrl = new Url(env.PUBLIC_URL).addPath('admin'); // Set the App's base path according to the APIs public URL let html = fse.readFileSync(adminPath, 'utf-8'); - html = html.replace(//, `\n\t\t`); + html = html.replace( + //, + `\n\t\t` + ); app.get('/admin', (req, res) => res.send(html)); app.use('/admin', express.static(path.join(adminPath, '..'))); @@ -183,7 +185,8 @@ export default async function createApp(): Promise { app.use('/users', usersRouter); app.use('/utils', utilsRouter); app.use('/webhooks', webhooksRouter); - app.use('/custom', customRouter); + + app.use(customRouter); // Register custom hooks / endpoints await emitAsyncSafe('routes.custom.init.before', { app }); diff --git a/api/src/cache.ts b/api/src/cache.ts index 813fc9a7da..682bd5cf94 100644 --- a/api/src/cache.ts +++ b/api/src/cache.ts @@ -52,7 +52,6 @@ function getConfig(store: 'memory' | 'redis' | 'memcache' = 'memory', ttl: numbe config.store = new KeyvRedis(env.CACHE_REDIS || getConfigFromEnv('CACHE_REDIS_'), { commandTimeout: 500, - retryStrategy: false, }); } diff --git a/api/src/cli/commands/bootstrap/index.ts b/api/src/cli/commands/bootstrap/index.ts index 75ebd55a55..8d30bd1be8 100644 --- a/api/src/cli/commands/bootstrap/index.ts +++ b/api/src/cli/commands/bootstrap/index.ts @@ -6,7 +6,7 @@ import env from '../../../env'; import logger from '../../../logger'; import { getSchema } from '../../../utils/get-schema'; import { RolesService, UsersService, SettingsService } from '../../../services'; -import getDatabase, { isInstalled, validateDBConnection, hasDatabaseConnection } from '../../../database'; +import getDatabase, { isInstalled, validateDatabaseConnection, hasDatabaseConnection } from '../../../database'; import { SchemaOverview } from '../../../types'; export default async function bootstrap({ skipAdminInit }: { skipAdminInit?: boolean }): Promise { @@ -59,7 +59,7 @@ async function waitForDatabase(database: Knex) { } // This will throw and exit the process if the database is not available - await validateDBConnection(database); + await validateDatabaseConnection(database); } async function createDefaultAdmin(schema: SchemaOverview) { diff --git a/api/src/cli/commands/count/index.ts b/api/src/cli/commands/count/index.ts index 62a7021eef..3ee4fed229 100644 --- a/api/src/cli/commands/count/index.ts +++ b/api/src/cli/commands/count/index.ts @@ -1,12 +1,11 @@ -/* eslint-disable no-console */ - import getDatabase from '../../../database'; +import logger from '../../../logger'; export default async function count(collection: string): Promise { const database = getDatabase(); if (!collection) { - console.error('Collection is required'); + logger.error('Collection is required'); process.exit(1); } @@ -14,11 +13,11 @@ export default async function count(collection: string): Promise { const records = await database(collection).count('*', { as: 'count' }); const count = Number(records[0].count); - console.log(count); + process.stdout.write(`${count}\n`); database.destroy(); process.exit(0); - } catch (err) { - console.error(err); + } catch (err: any) { + logger.error(err); database.destroy(); process.exit(1); } diff --git a/api/src/cli/commands/database/install.ts b/api/src/cli/commands/database/install.ts index 48fbc1c105..ea4d367c41 100644 --- a/api/src/cli/commands/database/install.ts +++ b/api/src/cli/commands/database/install.ts @@ -1,8 +1,7 @@ -/* eslint-disable no-console */ - import runMigrations from '../../../database/migrations/run'; import installSeeds from '../../../database/seeds/run'; import getDatabase from '../../../database'; +import logger from '../../../logger'; export default async function start(): Promise { const database = getDatabase(); @@ -12,8 +11,8 @@ export default async function start(): Promise { await runMigrations(database, 'latest'); database.destroy(); process.exit(0); - } catch (err) { - console.log(err); + } catch (err: any) { + logger.error(err); database.destroy(); process.exit(1); } diff --git a/api/src/cli/commands/database/migrate.ts b/api/src/cli/commands/database/migrate.ts index 8c5b32a17f..caae05408d 100644 --- a/api/src/cli/commands/database/migrate.ts +++ b/api/src/cli/commands/database/migrate.ts @@ -1,25 +1,24 @@ -/* eslint-disable no-console */ - import run from '../../../database/migrations/run'; import getDatabase from '../../../database'; +import logger from '../../../logger'; export default async function migrate(direction: 'latest' | 'up' | 'down'): Promise { const database = getDatabase(); try { - console.log('✨ Running migrations...'); + logger.info('Running migrations...'); await run(database, direction); if (direction === 'down') { - console.log('✨ Downgrade successful'); + logger.info('Downgrade successful'); } else { - console.log('✨ Database up to date'); + logger.info('Database up to date'); } database.destroy(); process.exit(); - } catch (err) { - console.log(err); + } catch (err: any) { + logger.error(err); database.destroy(); process.exit(1); } diff --git a/api/src/cli/commands/init/index.ts b/api/src/cli/commands/init/index.ts index ed5aca4dad..a20fdc82e4 100644 --- a/api/src/cli/commands/init/index.ts +++ b/api/src/cli/commands/init/index.ts @@ -1,6 +1,3 @@ -/* eslint-disable no-console */ - -import argon2 from 'argon2'; import chalk from 'chalk'; import execa from 'execa'; import inquirer from 'inquirer'; @@ -13,6 +10,7 @@ import createDBConnection, { Credentials } from '../../utils/create-db-connectio import createEnv from '../../utils/create-env'; import { drivers, getDriverForClient } from '../../utils/drivers'; import { databaseQuestions } from './questions'; +import { generateHash } from '../../../utils/generate-hash'; export default async function init(): Promise { const rootPath = process.cwd(); @@ -48,20 +46,17 @@ export default async function init(): Promise { try { await runSeed(db); await runMigrations(db, 'latest'); - } catch (err) { - console.log(); - console.log('Something went wrong while seeding the database:'); - console.log(); - console.log(`${chalk.red(`[${err.code || 'Error'}]`)} ${err.message}`); - console.log(); - console.log('Please try again'); - console.log(); + } catch (err: any) { + process.stdout.write('\nSomething went wrong while seeding the database:\n'); + process.stdout.write(`\n${chalk.red(`[${err.code || 'Error'}]`)} ${err.message}\n`); + process.stdout.write('\nPlease try again\n\n'); + attemptsRemaining--; if (attemptsRemaining > 0) { return await trySeed(); } else { - console.log(`Couldn't seed the database. Exiting.`); + process.stdout.write("Couldn't seed the database. Exiting.\n"); process.exit(1); } } @@ -71,10 +66,7 @@ export default async function init(): Promise { await createEnv(dbClient, credentials!, rootPath); - console.log(); - console.log(); - - console.log(`Create your first admin user:`); + process.stdout.write('\nCreate your first admin user:\n\n'); const firstUser = await inquirer.prompt([ { @@ -95,7 +87,7 @@ export default async function init(): Promise { }, ]); - firstUser.password = await argon2.hash(firstUser.password); + firstUser.password = await generateHash(firstUser.password); const userID = uuidV4(); const roleID = uuidV4(); @@ -120,15 +112,11 @@ export default async function init(): Promise { await db.destroy(); - console.log(` -Your project has been created at ${chalk.green(rootPath)}. - -The configuration can be found in ${chalk.green(rootPath + '/.env')} - -Start Directus by running: - ${chalk.blue('cd')} ${rootPath} - ${chalk.blue('npx directus')} start -`); + process.stdout.write(`\nYour project has been created at ${chalk.green(rootPath)}.\n`); + process.stdout.write(`\nThe configuration can be found in ${chalk.green(rootPath + '/.env')}\n`); + process.stdout.write(`\nStart Directus by running:\n`); + process.stdout.write(` ${chalk.blue('cd')} ${rootPath}\n`); + process.stdout.write(` ${chalk.blue('npx directus')} start\n`); process.exit(0); } diff --git a/api/src/cli/commands/roles/create.ts b/api/src/cli/commands/roles/create.ts index ab60884feb..acf6e0ddff 100644 --- a/api/src/cli/commands/roles/create.ts +++ b/api/src/cli/commands/roles/create.ts @@ -1,14 +1,13 @@ -/* eslint-disable no-console */ - import { getSchema } from '../../../utils/get-schema'; import { RolesService } from '../../../services'; import getDatabase from '../../../database'; +import logger from '../../../logger'; export default async function rolesCreate({ role: name, admin }: { role: string; admin: boolean }): Promise { const database = getDatabase(); if (!name) { - console.error('Name is required'); + logger.error('Name is required'); process.exit(1); } @@ -17,11 +16,11 @@ export default async function rolesCreate({ role: name, admin }: { role: string; const service = new RolesService({ schema: schema, knex: database }); const id = await service.createOne({ name, admin_access: admin }); - console.log(id); + process.stdout.write(`${String(id)}\n`); database.destroy(); process.exit(0); - } catch (err) { - console.error(err); + } catch (err: any) { + logger.error(err); process.exit(1); } } diff --git a/api/src/cli/commands/users/create.ts b/api/src/cli/commands/users/create.ts index 348eca565e..b07d4080c6 100644 --- a/api/src/cli/commands/users/create.ts +++ b/api/src/cli/commands/users/create.ts @@ -1,8 +1,7 @@ -/* eslint-disable no-console */ - import { getSchema } from '../../../utils/get-schema'; import { UsersService } from '../../../services'; import getDatabase from '../../../database'; +import logger from '../../../logger'; export default async function usersCreate({ email, @@ -16,7 +15,7 @@ export default async function usersCreate({ const database = getDatabase(); if (!email || !password || !role) { - console.error('Email, password, role are required'); + logger.error('Email, password, role are required'); process.exit(1); } @@ -25,11 +24,11 @@ export default async function usersCreate({ const service = new UsersService({ schema, knex: database }); const id = await service.createOne({ email, password, role, status: 'active' }); - console.log(id); + process.stdout.write(`${String(id)}\n`); database.destroy(); process.exit(0); - } catch (err) { - console.error(err); + } catch (err: any) { + logger.error(err); process.exit(1); } } diff --git a/api/src/cli/commands/users/passwd.ts b/api/src/cli/commands/users/passwd.ts index ca40cbfba5..25867d55a9 100644 --- a/api/src/cli/commands/users/passwd.ts +++ b/api/src/cli/commands/users/passwd.ts @@ -1,35 +1,34 @@ -/* eslint-disable no-console */ - -import argon2 from 'argon2'; import { getSchema } from '../../../utils/get-schema'; +import { generateHash } from '../../../utils/generate-hash'; import { UsersService } from '../../../services'; import getDatabase from '../../../database'; +import logger from '../../../logger'; export default async function usersPasswd({ email, password }: { email?: string; password?: string }): Promise { const database = getDatabase(); if (!email || !password) { - console.error('Email and password are required'); + logger.error('Email and password are required'); process.exit(1); } try { - const passwordHashed = await argon2.hash(password); + const passwordHashed = await generateHash(password); const schema = await getSchema(); const service = new UsersService({ schema, knex: database }); const user = await service.knex.select('id').from('directus_users').where({ email }).first(); if (user) { await service.knex('directus_users').update({ password: passwordHashed }).where({ id: user.id }); - console.log(`Password is updated for user ${user.id}`); + logger.info(`Password is updated for user ${user.id}`); } else { - console.log('No such user by this email'); + logger.error('No such user by this email'); } await database.destroy(); process.exit(user ? 0 : 1); - } catch (err) { - console.error(err); + } catch (err: any) { + logger.error(err); process.exit(1); } } diff --git a/api/src/cli/index.test.ts b/api/src/cli/index.test.ts new file mode 100644 index 0000000000..d1af25ed8a --- /dev/null +++ b/api/src/cli/index.test.ts @@ -0,0 +1,62 @@ +import { Command } from 'commander'; +import { Extension } from '@directus/shared/types'; +import { createCli } from '.'; + +jest.mock('../env', () => ({ + ...jest.requireActual('../env').default, + LOG_LEVEL: 'silent', + EXTENSIONS_PATH: '', + SERVE_APP: false, +})); + +jest.mock('@directus/shared/utils/node/get-extensions', () => ({ + getPackageExtensions: jest.fn(() => Promise.resolve([])), + getLocalExtensions: jest.fn(() => Promise.resolve([customCliExtension])), +})); + +jest.mock(`/hooks/custom-cli/index.js`, () => () => customCliHook, { virtual: true }); + +const customCliExtension: Extension = { + path: `/hooks/custom-cli`, + name: 'custom-cli', + type: 'hook', + entrypoint: 'index.js', + local: true, + root: true, +}; + +const beforeHook = jest.fn(); +const afterAction = jest.fn(); +const afterHook = jest.fn(({ program }: { program: Command }) => program.command('custom').action(afterAction)); +const customCliHook = { 'cli.init.before': beforeHook, 'cli.init.after': afterHook }; + +const writeOut = jest.fn(); +const writeErr = jest.fn(); + +const setup = async () => { + const program = await createCli(); + program.exitOverride(); + program.configureOutput({ writeOut, writeErr }); + return program; +}; + +beforeEach(jest.clearAllMocks); + +describe('cli hooks', () => { + test('should call hooks before and after creating the cli', async () => { + const program = await setup(); + + expect(beforeHook).toHaveBeenCalledTimes(1); + expect(beforeHook).toHaveBeenCalledWith({ program }); + + expect(afterHook).toHaveBeenCalledTimes(1); + expect(afterHook).toHaveBeenCalledWith({ program }); + }); + + test('should be able to add a custom cli command', async () => { + const program = await setup(); + program.parseAsync(['custom'], { from: 'user' }); + + expect(afterAction).toHaveBeenCalledTimes(1); + }); +}); diff --git a/api/src/cli/index.ts b/api/src/cli/index.ts index 6e657020ef..b09e1ec57a 100644 --- a/api/src/cli/index.ts +++ b/api/src/cli/index.ts @@ -1,9 +1,7 @@ -#!/usr/bin/env node - -/* eslint-disable no-console */ - -import { program } from 'commander'; +import { Command } from 'commander'; import start from '../start'; +import { emitAsyncSafe } from '../emitter'; +import { initializeExtensions, registerExtensionHooks } from '../extensions'; import bootstrap from './commands/bootstrap'; import count from './commands/count'; import dbInstall from './commands/database/install'; @@ -15,61 +13,69 @@ import usersPasswd from './commands/users/passwd'; const pkg = require('../../package.json'); -program.name('directus').usage('[command] [options]'); -program.version(pkg.version, '-v, --version'); +export async function createCli(): Promise { + const program = new Command(); -program.command('start').description('Start the Directus API').action(start); -program.command('init').description('Create a new Directus Project').action(init); + await initializeExtensions(); + registerExtensionHooks(); -const dbCommand = program.command('database'); -dbCommand.command('install').description('Install the database').action(dbInstall); -dbCommand - .command('migrate:latest') - .description('Upgrade the database') - .action(() => dbMigrate('latest')); -dbCommand - .command('migrate:up') - .description('Upgrade the database') - .action(() => dbMigrate('up')); -dbCommand - .command('migrate:down') - .description('Downgrade the database') - .action(() => dbMigrate('down')); + await emitAsyncSafe('cli.init.before', { program }); -const usersCommand = program.command('users'); + program.name('directus').usage('[command] [options]'); + program.version(pkg.version, '-v, --version'); -usersCommand - .command('create') - .description('Create a new user') - .option('--email ', `user's email`) - .option('--password ', `user's password`) - .option('--role ', `user's role`) - .action(usersCreate); + program.command('start').description('Start the Directus API').action(start); + program.command('init').description('Create a new Directus Project').action(init); -usersCommand - .command('passwd') - .description('Set user password') - .option('--email ', `user's email`) - .option('--password ', `user's new password`) - .action(usersPasswd); + const dbCommand = program.command('database'); + dbCommand.command('install').description('Install the database').action(dbInstall); + dbCommand + .command('migrate:latest') + .description('Upgrade the database') + .action(() => dbMigrate('latest')); + dbCommand + .command('migrate:up') + .description('Upgrade the database') + .action(() => dbMigrate('up')); + dbCommand + .command('migrate:down') + .description('Downgrade the database') + .action(() => dbMigrate('down')); -const rolesCommand = program.command('roles'); -rolesCommand - .command('create') - .description('Create a new role') - .option('--role ', `name for the role`) - .option('--admin', `whether or not the role has admin access`) - .action(rolesCreate); + const usersCommand = program.command('users'); -program.command('count ').description('Count the amount of items in a given collection').action(count); + usersCommand + .command('create') + .description('Create a new user') + .option('--email ', `user's email`) + .option('--password ', `user's password`) + .option('--role ', `user's role`) + .action(usersCreate); -program - .command('bootstrap') - .description('Initialize or update the database') - .option('--skipAdminInit', 'Skips the creation of the default Admin Role and User') - .action(bootstrap); + usersCommand + .command('passwd') + .description('Set user password') + .option('--email ', `user's email`) + .option('--password ', `user's new password`) + .action(usersPasswd); -program.parseAsync(process.argv).catch((err) => { - console.error(err); - process.exit(1); -}); + const rolesCommand = program.command('roles'); + rolesCommand + .command('create') + .description('Create a new role') + .option('--role ', `name for the role`) + .option('--admin', `whether or not the role has admin access`) + .action(rolesCreate); + + program.command('count ').description('Count the amount of items in a given collection').action(count); + + program + .command('bootstrap') + .description('Initialize or update the database') + .option('--skipAdminInit', 'Skips the creation of the default Admin Role and User') + .action(bootstrap); + + await emitAsyncSafe('cli.init.after', { program }); + + return program; +} diff --git a/api/src/cli/run.ts b/api/src/cli/run.ts new file mode 100644 index 0000000000..5bbf0fd1ea --- /dev/null +++ b/api/src/cli/run.ts @@ -0,0 +1,9 @@ +import { createCli } from './index'; + +createCli() + .then((program) => program.parseAsync(process.argv)) + .catch((err) => { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); + }); diff --git a/api/src/controllers/activity.ts b/api/src/controllers/activity.ts index e5fe4d1f30..398e883889 100644 --- a/api/src/controllers/activity.ts +++ b/api/src/controllers/activity.ts @@ -99,7 +99,7 @@ router.post( res.locals.payload = { data: record || null, }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -138,7 +138,7 @@ router.patch( res.locals.payload = { data: record || null, }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index 4c37c1654d..ab41d3729a 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -177,7 +177,7 @@ router.post( try { await service.requestPasswordReset(req.body.email, req.body.reset_url || null); return next(); - } catch (err) { + } catch (err: any) { if (err instanceof InvalidPayloadException) { throw err; } else { @@ -320,7 +320,7 @@ router.get( authResponse = await authenticationService.authenticate({ email, }); - } catch (error) { + } catch (error: any) { emitStatus('fail'); logger.warn(error); diff --git a/api/src/controllers/collections.ts b/api/src/controllers/collections.ts index 03d4731556..1868f4e56f 100644 --- a/api/src/controllers/collections.ts +++ b/api/src/controllers/collections.ts @@ -88,7 +88,7 @@ router.patch( try { const collection = await collectionsService.readOne(req.params.collection); res.locals.payload = { data: collection || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/fields.ts b/api/src/controllers/fields.ts index 732e6a589c..8409a1c71c 100644 --- a/api/src/controllers/fields.ts +++ b/api/src/controllers/fields.ts @@ -101,7 +101,7 @@ router.post( try { const createdField = await service.readOne(req.params.collection, field.field); res.locals.payload = { data: createdField || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -138,7 +138,7 @@ router.patch( results.push(updatedField); res.locals.payload = { data: results || null }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -193,7 +193,7 @@ router.patch( try { const updatedField = await service.readOne(req.params.collection, req.params.field); res.locals.payload = { data: updatedField || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/files.ts b/api/src/controllers/files.ts index 78ae4077dc..9fe6753ab1 100644 --- a/api/src/controllers/files.ts +++ b/api/src/controllers/files.ts @@ -55,10 +55,6 @@ const multipartHandler = asyncHandler(async (req, res, next) => { payload.title = formatTitle(path.parse(filename).name); } - if (req.accountability?.user) { - payload.uploaded_by = req.accountability.user; - } - const payloadWithRequiredFields: Partial & { filename_download: string; type: string; @@ -77,7 +73,7 @@ const multipartHandler = asyncHandler(async (req, res, next) => { const primaryKey = await service.uploadOne(fileStream, payloadWithRequiredFields, existingPrimaryKey); savedFiles.push(primaryKey); tryDone(); - } catch (error) { + } catch (error: any) { busboy.emit('error', error); } }); @@ -131,7 +127,7 @@ router.post( data: record, }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -168,7 +164,7 @@ router.post( try { const record = await service.readOne(primaryKey, req.sanitizedQuery); res.locals.payload = { data: record || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -246,7 +242,7 @@ router.patch( try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals.payload = { data: result || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -273,7 +269,7 @@ router.patch( try { const record = await service.readOne(req.params.pk, req.sanitizedQuery); res.locals.payload = { data: record || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/folders.ts b/api/src/controllers/folders.ts index 46c44cf878..049e8afae0 100644 --- a/api/src/controllers/folders.ts +++ b/api/src/controllers/folders.ts @@ -37,7 +37,7 @@ router.post( const record = await service.readOne(savedKeys[0], req.sanitizedQuery); res.locals.payload = { data: record }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -114,7 +114,7 @@ router.patch( try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals.payload = { data: result || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -140,7 +140,7 @@ router.patch( try { const record = await service.readOne(primaryKey, req.sanitizedQuery); res.locals.payload = { data: record || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/items.ts b/api/src/controllers/items.ts index 36121abd34..c0e1fe8ad4 100644 --- a/api/src/controllers/items.ts +++ b/api/src/controllers/items.ts @@ -42,7 +42,7 @@ router.post( const result = await service.readOne(savedKeys[0], req.sanitizedQuery); res.locals.payload = { data: result || null }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -97,10 +97,6 @@ router.get( asyncHandler(async (req, res, next) => { if (req.params.collection.startsWith('directus_')) throw new ForbiddenException(); - if (req.singleton) { - throw new RouteNotFoundException(req.path); - } - const service = new ItemsService(req.collection, { accountability: req.accountability, schema: req.schema, @@ -111,6 +107,7 @@ router.get( res.locals.payload = { data: result || null, }; + return next(); }), respond @@ -147,7 +144,7 @@ router.patch( try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals.payload = { data: result }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -180,7 +177,7 @@ router.patch( try { const result = await service.readOne(updatedPrimaryKey, req.sanitizedQuery); res.locals.payload = { data: result || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/not-found.ts b/api/src/controllers/not-found.ts index 09d94d62a4..9d1d30fd74 100644 --- a/api/src/controllers/not-found.ts +++ b/api/src/controllers/not-found.ts @@ -20,7 +20,7 @@ const notFound: RequestHandler = async (req, res, next) => { return next(); } next(new RouteNotFoundException(req.path)); - } catch (err) { + } catch (err: any) { next(err); } }; diff --git a/api/src/controllers/permissions.ts b/api/src/controllers/permissions.ts index cd4137c4a0..b26b40f389 100644 --- a/api/src/controllers/permissions.ts +++ b/api/src/controllers/permissions.ts @@ -37,7 +37,7 @@ router.post( const item = await service.readOne(savedKeys[0], req.sanitizedQuery); res.locals.payload = { data: item }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -116,7 +116,7 @@ router.patch( try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals.payload = { data: result }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -142,7 +142,7 @@ router.patch( try { const item = await service.readOne(primaryKey, req.sanitizedQuery); res.locals.payload = { data: item || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/presets.ts b/api/src/controllers/presets.ts index f9cd9a248e..b1cff328a4 100644 --- a/api/src/controllers/presets.ts +++ b/api/src/controllers/presets.ts @@ -37,7 +37,7 @@ router.post( const record = await service.readOne(savedKeys[0], req.sanitizedQuery); res.locals.payload = { data: record }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -115,7 +115,7 @@ router.patch( try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals.payload = { data: result }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -141,7 +141,7 @@ router.patch( try { const record = await service.readOne(primaryKey, req.sanitizedQuery); res.locals.payload = { data: record }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/relations.ts b/api/src/controllers/relations.ts index 878d422985..a4a2b91efd 100644 --- a/api/src/controllers/relations.ts +++ b/api/src/controllers/relations.ts @@ -90,7 +90,7 @@ router.post( try { const createdRelation = await service.readOne(req.body.collection, req.body.field); res.locals.payload = { data: createdRelation || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -135,7 +135,7 @@ router.patch( try { const updatedField = await service.readOne(req.params.collection, req.params.field); res.locals.payload = { data: updatedField || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/roles.ts b/api/src/controllers/roles.ts index f852abefa8..8b47ecf05b 100644 --- a/api/src/controllers/roles.ts +++ b/api/src/controllers/roles.ts @@ -37,7 +37,7 @@ router.post( const item = await service.readOne(savedKeys[0], req.sanitizedQuery); res.locals.payload = { data: item }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -106,7 +106,7 @@ router.patch( try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals.payload = { data: result }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -132,7 +132,7 @@ router.patch( try { const item = await service.readOne(primaryKey, req.sanitizedQuery); res.locals.payload = { data: item || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/settings.ts b/api/src/controllers/settings.ts index 98115b6041..5a22694249 100644 --- a/api/src/controllers/settings.ts +++ b/api/src/controllers/settings.ts @@ -35,7 +35,7 @@ router.patch( try { const record = await service.readSingleton(req.sanitizedQuery); res.locals.payload = { data: record || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/users.ts b/api/src/controllers/users.ts index b148471f72..241c2135d5 100644 --- a/api/src/controllers/users.ts +++ b/api/src/controllers/users.ts @@ -38,7 +38,7 @@ router.post( const item = await service.readOne(savedKeys[0], req.sanitizedQuery); res.locals.payload = { data: item }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -86,7 +86,7 @@ router.get( try { const item = await service.readOne(req.accountability.user, req.sanitizedQuery); res.locals.payload = { data: item || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { res.locals.payload = { data: { id: req.accountability.user } }; return next(); @@ -177,7 +177,7 @@ router.patch( try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals.payload = { data: result }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -203,7 +203,7 @@ router.patch( try { const item = await service.readOne(primaryKey, req.sanitizedQuery); res.locals.payload = { data: item || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/controllers/utils.ts b/api/src/controllers/utils.ts index ec64b628fd..dbd9d098d4 100644 --- a/api/src/controllers/utils.ts +++ b/api/src/controllers/utils.ts @@ -9,6 +9,7 @@ import { RevisionsService, UtilsService, ImportService } from '../services'; import asyncHandler from '../utils/async-handler'; import Busboy from 'busboy'; import { flushCaches } from '../cache'; +import { generateHash } from '../utils/generate-hash'; const router = Router(); @@ -31,7 +32,7 @@ router.post( throw new InvalidPayloadException(`"string" is required`); } - const hash = await argon2.hash(req.body.string); + const hash = await generateHash(req.body.string); return res.json({ data: hash }); }) @@ -103,7 +104,7 @@ router.post( busboy.on('file', async (fieldname, fileStream, filename, encoding, mimetype) => { try { await service.import(req.params.collection, mimetype, fileStream); - } catch (err) { + } catch (err: any) { return next(err); } diff --git a/api/src/controllers/webhooks.ts b/api/src/controllers/webhooks.ts index edca81dfeb..9eef455ed7 100644 --- a/api/src/controllers/webhooks.ts +++ b/api/src/controllers/webhooks.ts @@ -37,7 +37,7 @@ router.post( const item = await service.readOne(savedKeys[0], req.sanitizedQuery); res.locals.payload = { data: item }; } - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -106,7 +106,7 @@ router.patch( try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals.payload = { data: result }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } @@ -132,7 +132,7 @@ router.patch( try { const item = await service.readOne(primaryKey, req.sanitizedQuery); res.locals.payload = { data: item || null }; - } catch (error) { + } catch (error: any) { if (error instanceof ForbiddenException) { return next(); } diff --git a/api/src/database/helpers/geometry.ts b/api/src/database/helpers/geometry.ts index c3dcc3e84a..489fd3166c 100644 --- a/api/src/database/helpers/geometry.ts +++ b/api/src/database/helpers/geometry.ts @@ -14,6 +14,7 @@ export function getGeometryHelper(): KnexSpatial { mariadb: KnexSpatial_MySQL, sqlite3: KnexSpatial, pg: KnexSpatial_PG, + postgres: KnexSpatial_PG, redshift: KnexSpatial_Redshift, mssql: KnexSpatial_MSSQL, oracledb: KnexSpatial_Oracle, diff --git a/api/src/database/index.ts b/api/src/database/index.ts index 9a3b2ed434..9e952dfb91 100644 --- a/api/src/database/index.ts +++ b/api/src/database/index.ts @@ -8,6 +8,7 @@ import { validateEnv } from '../utils/validate-env'; import fse from 'fs-extra'; import path from 'path'; import { merge } from 'lodash'; +import { promisify } from 'util'; let database: Knex | null = null; let inspector: ReturnType | null = null; @@ -22,6 +23,7 @@ export default function getDatabase(): Knex { 'DB_SEARCH_PATH', 'DB_CONNECTION_STRING', 'DB_POOL', + 'DB_EXCLUDE_TABLES', ]); const poolConfig = getConfigFromEnv('DB_POOL'); @@ -54,7 +56,12 @@ export default function getDatabase(): Knex { connection: env.DB_CONNECTION_STRING || connectionConfig, log: { warn: (msg) => { + // Ignore warnings about returning not being supported in some DBs if (msg.startsWith('.returning()')) return; + + // Ignore warning about MySQL not supporting TRX for DDL + if (msg.startsWith('Transaction was implicitly committed, do not mix transactions and DDL with MySQL')) return; + return logger.warn(msg); }, error: (msg) => logger.error(msg), @@ -66,8 +73,14 @@ export default function getDatabase(): Knex { if (env.DB_CLIENT === 'sqlite3') { knexConfig.useNullAsDefault = true; - poolConfig.afterCreate = (conn: any, cb: any) => { - conn.run('PRAGMA foreign_keys = ON', cb); + + poolConfig.afterCreate = async (conn: any, callback: any) => { + logger.trace('Enabling SQLite Foreign Keys support...'); + + const run = promisify(conn.run.bind(conn)); + await run('PRAGMA foreign_keys = ON'); + + callback(null, conn); }; } @@ -111,7 +124,7 @@ export async function hasDatabaseConnection(database?: Knex): Promise { database = database ?? getDatabase(); try { - if (env.DB_CLIENT === 'oracledb') { + if (getDatabaseClient(database) === 'oracle') { await database.raw('select 1 from DUAL'); } else { await database.raw('SELECT 1'); @@ -123,28 +136,48 @@ export async function hasDatabaseConnection(database?: Knex): Promise { } } -export async function validateDBConnection(database?: Knex): Promise { +export async function validateDatabaseConnection(database?: Knex): Promise { database = database ?? getDatabase(); try { - if (env.DB_CLIENT === 'oracledb') { + if (getDatabaseClient(database) === 'oracle') { await database.raw('select 1 from DUAL'); } else { await database.raw('SELECT 1'); } - } catch (error) { + } catch (error: any) { logger.error(`Can't connect to the database.`); logger.error(error); process.exit(1); } } +export function getDatabaseClient(database?: Knex): 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'mssql' { + database = database ?? getDatabase(); + + switch (database.client.constructor.name) { + case 'Client_MySQL': + return 'mysql'; + case 'Client_PG': + return 'postgres'; + case 'Client_SQLite3': + return 'sqlite'; + case 'Client_Oracledb': + case 'Client_Oracle': + return 'oracle'; + case 'Client_MSSQL': + return 'mssql'; + } + + throw new Error(`Couldn't extract database client`); +} + export async function isInstalled(): Promise { const inspector = getSchemaInspector(); // The existence of a directus_collections table alone isn't a "proper" check to see if everything // is installed correctly of course, but it's safe enough to assume that this collection only - // exists when using the installer CLI. + // exists when Directus is properly installed. return await inspector.hasTable('directus_collections'); } @@ -173,9 +206,45 @@ export async function validateMigrations(): Promise { ); return requiredVersions.every((version) => completedVersions.includes(version)); - } catch (error) { + } catch (error: any) { logger.error(`Database migrations cannot be found`); logger.error(error); throw process.exit(1); } } + +/** + * These database extensions should be optional, so we don't throw or return any problem states when they don't + */ +export async function validateDatabaseExtensions(): Promise { + const database = getDatabase(); + const databaseClient = getDatabaseClient(database); + + if (databaseClient === 'postgres') { + let available = false; + let installed = false; + + const exists = await database.raw(`SELECT name FROM pg_available_extensions WHERE name = 'postgis';`); + + if (exists.rows.length > 0) { + available = true; + } + + if (available) { + try { + await database.raw(`SELECT PostGIS_version();`); + installed = true; + } catch { + installed = false; + } + } + + if (available === false) { + logger.warn(`PostGIS isn't installed. Geometry type support will be limited.`); + } else if (available === true && installed === false) { + logger.warn( + `PostGIS is installed, but hasn't been activated on this database. Geometry type support will be limited.` + ); + } + } +} diff --git a/api/src/database/migrations/20210518A-add-foreign-key-constraints.ts b/api/src/database/migrations/20210518A-add-foreign-key-constraints.ts index d27efecc2e..60e24386c1 100644 --- a/api/src/database/migrations/20210518A-add-foreign-key-constraints.ts +++ b/api/src/database/migrations/20210518A-add-foreign-key-constraints.ts @@ -68,7 +68,7 @@ export async function up(knex: Knex): Promise { await knex(constraint.many_collection) .update({ [constraint.many_field]: null }) .whereIn(currentPrimaryKeyField, ids); - } catch (err) { + } catch (err: any) { logger.error( `${constraint.many_collection}.${constraint.many_field} contains illegal foreign keys which couldn't be set to NULL. Please fix these references and rerun this migration to complete the upgrade.` ); @@ -111,7 +111,7 @@ export async function up(knex: Knex): Promise { builder.onDelete('SET NULL'); } }); - } catch (err) { + } catch (err: any) { logger.warn( `Couldn't add foreign key constraint for ${constraint.many_collection}.${constraint.many_field}<->${constraint.one_collection}` ); @@ -140,7 +140,7 @@ export async function down(knex: Knex): Promise { await knex.schema.alterTable(relation.many_collection, (table) => { table.dropForeign([relation.many_field]); }); - } catch (err) { + } catch (err: any) { logger.warn( `Couldn't drop foreign key constraint for ${relation.many_collection}.${relation.many_field}<->${relation.one_collection}` ); diff --git a/api/src/database/migrations/20210519A-add-system-fk-triggers.ts b/api/src/database/migrations/20210519A-add-system-fk-triggers.ts index 2cf58f6a0c..e682eb023e 100644 --- a/api/src/database/migrations/20210519A-add-system-fk-triggers.ts +++ b/api/src/database/migrations/20210519A-add-system-fk-triggers.ts @@ -99,7 +99,7 @@ export async function up(knex: Knex): Promise { await knex.schema.alterTable(update.table, (table) => { table.dropForeign([constraint.column], existingForeignKey?.constraint_name || undefined); }); - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`); logger.warn(err); } @@ -114,7 +114,7 @@ export async function up(knex: Knex): Promise { // Knex uses a default convention for index names: `table_column_type` table.dropIndex([constraint.column], `${update.table}_${constraint.column}_foreign`); }); - } catch (err) { + } catch (err: any) { logger.warn( `Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}` ); @@ -126,7 +126,7 @@ export async function up(knex: Knex): Promise { await knex.schema.alterTable(update.table, (table) => { table.foreign(constraint.column).references(constraint.references).onDelete(constraint.on_delete); }); - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't add foreign key to ${update.table}.${constraint.column}->${constraint.references}`); logger.warn(err); } @@ -141,7 +141,7 @@ export async function down(knex: Knex): Promise { await knex.schema.alterTable(update.table, (table) => { table.dropForeign([constraint.column]); }); - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`); logger.warn(err); } @@ -156,7 +156,7 @@ export async function down(knex: Knex): Promise { // Knex uses a default convention for index names: `table_column_type` table.dropIndex([constraint.column], `${update.table}_${constraint.column}_foreign`); }); - } catch (err) { + } catch (err: any) { logger.warn( `Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}` ); @@ -168,7 +168,7 @@ export async function down(knex: Knex): Promise { await knex.schema.alterTable(update.table, (table) => { table.foreign(constraint.column).references(constraint.references); }); - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't add foreign key to ${update.table}.${constraint.column}->${constraint.references}`); logger.warn(err); } diff --git a/api/src/database/migrations/20210802A-replace-groups.ts b/api/src/database/migrations/20210802A-replace-groups.ts index 7ae9e05373..8cd924752a 100644 --- a/api/src/database/migrations/20210802A-replace-groups.ts +++ b/api/src/database/migrations/20210802A-replace-groups.ts @@ -14,7 +14,7 @@ export async function up(knex: Knex): Promise { if (options.icon) newOptions.headerIcon = options.icon; if (options.color) newOptions.headerColor = options.color; - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't convert previous options from field ${dividerGroup.collection}.${dividerGroup.field}`); logger.warn(err); } @@ -27,7 +27,7 @@ export async function up(knex: Knex): Promise { options: JSON.stringify(newOptions), }) .where('id', '=', dividerGroup.id); - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't update ${dividerGroup.collection}.${dividerGroup.field} to new group interface`); logger.warn(err); } diff --git a/api/src/database/migrations/20210831A-remove-limit-column.ts b/api/src/database/migrations/20210831A-remove-limit-column.ts new file mode 100644 index 0000000000..3a9c1c84db --- /dev/null +++ b/api/src/database/migrations/20210831A-remove-limit-column.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('directus_permissions', (table) => { + table.dropColumn('limit'); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('directus_permissions', (table) => { + table.integer('limit').unsigned(); + }); +} diff --git a/api/src/database/migrations/20210907A-webhooks-collections-not-null.ts b/api/src/database/migrations/20210907A-webhooks-collections-not-null.ts new file mode 100644 index 0000000000..208b07e752 --- /dev/null +++ b/api/src/database/migrations/20210907A-webhooks-collections-not-null.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('directus_webhooks', (table) => { + table.text('collections').notNullable().alter(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('directus_webhooks', (table) => { + table.text('collections').alter(); + }); +} diff --git a/api/src/database/migrations/run.ts b/api/src/database/migrations/run.ts index fb9c20bc6a..f7c9eb77b0 100644 --- a/api/src/database/migrations/run.ts +++ b/api/src/database/migrations/run.ts @@ -1,10 +1,9 @@ -/* eslint-disable no-console */ - import formatTitle from '@directus/format-title'; import fse from 'fs-extra'; import { Knex } from 'knex'; import path from 'path'; import env from '../../env'; +import logger from '../../logger'; import { Migration } from '../../types'; export default async function run(database: Knex, direction: 'up' | 'down' | 'latest'): Promise { @@ -62,7 +61,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la const { up } = require(nextVersion.file); - console.log(`✨ Applying ${nextVersion.name}...`); + logger.info(`Applying ${nextVersion.name}...`); await up(database); await database.insert({ version: nextVersion.version, name: nextVersion.name }).into('directus_migrations'); @@ -83,7 +82,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la const { down } = require(migration.file); - console.log(`✨ Undoing ${migration.name}...`); + logger.info(`Undoing ${migration.name}...`); await down(database); await database('directus_migrations').delete().where({ version: migration.version }); @@ -94,7 +93,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la if (migration.completed === false) { const { up } = require(migration.file); - console.log(`✨ Applying ${migration.name}...`); + logger.info(`Applying ${migration.name}...`); await up(database); await database.insert({ version: migration.version, name: migration.name }).into('directus_migrations'); diff --git a/api/src/database/system-data/app-access-permissions/index.ts b/api/src/database/system-data/app-access-permissions/index.ts index a41dc5d30a..498aa0ea3d 100644 --- a/api/src/database/system-data/app-access-permissions/index.ts +++ b/api/src/database/system-data/app-access-permissions/index.ts @@ -8,7 +8,6 @@ const defaults: Partial = { validation: null, presets: null, fields: ['*'], - limit: null, system: true, }; diff --git a/api/src/database/system-data/collections/collections.yaml b/api/src/database/system-data/collections/collections.yaml index 7cc7270f5f..6ea3ddae42 100644 --- a/api/src/database/system-data/collections/collections.yaml +++ b/api/src/database/system-data/collections/collections.yaml @@ -12,48 +12,48 @@ defaults: data: - collection: directus_activity - note: Accountability logs for all events + note: $t:directus_collection.directus_activity - collection: directus_collections icon: list_alt - note: Additional collection configuration and metadata + note: $t:directus_collection.directus_collections - collection: directus_fields icon: input - note: Additional field configuration and metadata + note: $t:directus_collection.directus_fields - collection: directus_files icon: folder - note: Metadata for all managed file assets + note: $t:directus_collection.directus_files display_template: '{{ $thumbnail }} {{ title }}' - collection: directus_folders - note: Provides virtual directories for files + note: $t:directus_collection.directus_folders display_template: '{{ name }}' - collection: directus_migrations - note: What version of the database you're using + note: $t:directus_collection.directus_migrations - collection: directus_permissions icon: admin_panel_settings - note: Access permissions for each role + note: $t:directus_collection.directus_permissions - collection: directus_presets icon: bookmark_border - note: Presets for collection defaults and bookmarks + note: $t:directus_collection.directus_presets accountability: null - collection: directus_relations icon: merge_type - note: Relationship configuration and metadata + note: $t:directus_collection.directus_relations - collection: directus_revisions - note: Data snapshots for all activity + note: $t:directus_collection.directus_revisions - collection: directus_roles icon: supervised_user_circle - note: Permission groups for system users + note: $t:directus_collection.directus_roles - collection: directus_sessions - note: User session information + note: $t:directus_collection.directus_sessions - collection: directus_settings singleton: true - note: Project configuration options + note: $t:directus_collection.directus_settings - collection: directus_users archive_field: status archive_value: archived unarchive_value: draft icon: people_alt - note: System users for the platform + note: $t:directus_collection.directus_users display_template: '{{ first_name }} {{ last_name }}' - collection: directus_webhooks - note: Configuration for event-based HTTP requests + note: $t:directus_collection.directus_webhooks diff --git a/api/src/database/system-data/fields/activity.yaml b/api/src/database/system-data/fields/activity.yaml index 3e759a6ac5..482122e7b8 100644 --- a/api/src/database/system-data/fields/activity.yaml +++ b/api/src/database/system-data/fields/activity.yaml @@ -13,19 +13,19 @@ fields: defaultForeground: 'var(--foreground-normal)' defaultBackground: 'var(--background-normal-alt)' choices: - - text: Create + - text: $t:field_options.directus_activity.create value: create foreground: 'var(--primary)' background: 'var(--primary-25)' - - text: Update + - text: $t:field_options.directus_activity.update value: update foreground: 'var(--blue)' background: 'var(--blue-25)' - - text: Delete + - text: $t:field_options.directus_activity.delete value: delete foreground: 'var(--danger)' background: 'var(--danger-25)' - - text: Login + - text: $t:field_options.directus_activity.login value: authenticate foreground: 'var(--purple)' background: 'var(--purple-25)' diff --git a/api/src/database/system-data/fields/collections.yaml b/api/src/database/system-data/fields/collections.yaml index 4fbf09a4c8..efdd176dc0 100644 --- a/api/src/database/system-data/fields/collections.yaml +++ b/api/src/database/system-data/fields/collections.yaml @@ -8,7 +8,7 @@ fields: interface: presentation-divider options: icon: box - title: Collection Setup + title: $t:field_options.directus_collections.collection_setup width: full - field: collection @@ -32,7 +32,7 @@ fields: - field: color interface: select-color options: - placeholder: Choose a color... + placeholder: $t:field_options.directus_collections.note_placeholder width: half - field: display_template @@ -45,7 +45,7 @@ fields: special: boolean interface: boolean options: - label: Hide within the App + label: $t:field_options.directus_collections.hidden_label width: half - field: singleton @@ -102,7 +102,7 @@ fields: interface: presentation-divider options: icon: archive - title: Archive + title: $t:field_options.directus_collections.archive_divider width: full - field: archive_field @@ -110,14 +110,14 @@ fields: options: collectionField: collection allowNone: true - placeholder: Choose a field... + placeholder: $t:field_options.directus_collections.archive_field width: half - field: archive_app_filter interface: boolean special: boolean options: - label: Enable App Archive Filter + label: $t:field_options.directus_collections.archive_app_filter width: half - field: archive_value @@ -125,7 +125,7 @@ fields: options: font: monospace iconRight: archive - placeholder: Value set when archiving... + placeholder: $t:field_options.directus_collections.archive_value width: half - field: unarchive_value @@ -133,7 +133,7 @@ fields: options: font: monospace iconRight: unarchive - placeholder: Value set when unarchiving... + placeholder: $t:field_options.directus_collections.unarchive_value width: half - field: sort_divider @@ -143,14 +143,14 @@ fields: interface: presentation-divider options: icon: sort - title: Sort + title: $t:field_options.directus_collections.divider width: full - field: sort_field interface: system-field options: collectionField: collection - placeholder: Choose a field... + placeholder: $t:field_options.directus_collections.sort_field typeAllowList: - float - decimal @@ -165,7 +165,7 @@ fields: interface: presentation-divider options: icon: admin_panel_settings - title: Accountability + title: $t:field_options.directus_collections.accountability_divider width: full - field: accountability diff --git a/api/src/database/system-data/fields/files.yaml b/api/src/database/system-data/fields/files.yaml index b0733daaea..8eeb56281c 100644 --- a/api/src/database/system-data/fields/files.yaml +++ b/api/src/database/system-data/fields/files.yaml @@ -10,14 +10,14 @@ fields: interface: input options: iconRight: title - placeholder: A unique title... + placeholder: $t:field_options.directus_files.title width: full - field: description interface: input-multiline width: full options: - placeholder: An optional description... + placeholder: $t:field_options.directus_files.description - field: tags interface: tags @@ -35,7 +35,7 @@ fields: interface: input options: iconRight: place - placeholder: An optional location... + placeholder: $t:field_options.directus_files.location width: half - field: storage @@ -49,7 +49,7 @@ fields: interface: presentation-divider options: icon: insert_drive_file - title: File Naming + title: $t:field_options.directus_files.storage_divider special: - alias - no-data @@ -59,7 +59,7 @@ fields: interface: input options: iconRight: publish - placeholder: Name on disk storage... + placeholder: $t:field_options.directus_files.filename_disk readonly: true width: half @@ -67,7 +67,7 @@ fields: interface: input options: iconRight: get_app - placeholder: Name when downloading... + placeholder: $t:field_options.directus_files.filename_download width: half - field: metadata @@ -106,6 +106,7 @@ fields: display: user width: half hidden: true + special: user-created - field: uploaded_on display: datetime diff --git a/api/src/database/system-data/fields/permissions.yaml b/api/src/database/system-data/fields/permissions.yaml index 2b564453e4..d9842bb2f7 100644 --- a/api/src/database/system-data/fields/permissions.yaml +++ b/api/src/database/system-data/fields/permissions.yaml @@ -15,9 +15,6 @@ fields: - field: role width: half - - field: limit - width: half - - field: collection width: half diff --git a/api/src/database/system-data/fields/roles.yaml b/api/src/database/system-data/fields/roles.yaml index 664204a226..85880b0956 100644 --- a/api/src/database/system-data/fields/roles.yaml +++ b/api/src/database/system-data/fields/roles.yaml @@ -9,7 +9,7 @@ fields: - field: name interface: input options: - placeholder: The unique name for this role... + placeholder: $t:field_options.directus_roles.name width: half - field: icon @@ -20,7 +20,7 @@ fields: - field: description interface: input options: - placeholder: A description of this role... + placeholder: $t:field_options.directus_roles.description width: full - field: app_access @@ -36,7 +36,7 @@ fields: - field: ip_access interface: tags options: - placeholder: Add allowed IP addresses, leave empty to allow all... + placeholder: $t:field_options.directus_roles.ip_access special: csv width: full @@ -60,13 +60,13 @@ fields: template: '{{ name }}' addLabel: Add New Module... fields: - - name: Icon + - name: $t:field_options.directus_roles.fields.icon_name field: icon type: string meta: interface: select-icon width: half - - name: Name + - name: $t:field_options.directus_roles.fields.name_name field: name type: string meta: @@ -74,8 +74,8 @@ fields: width: half options: iconRight: title - placeholder: Enter a title... - - name: Link + placeholder: + - name: $t:field_options.directus_roles.fields.link_name field: link type: string meta: @@ -83,7 +83,7 @@ fields: width: full options: iconRight: link - placeholder: Relative or absolute URL... + placeholder: $t:field_options.directus_roles.fields.link_placeholder special: json width: full @@ -91,9 +91,9 @@ fields: interface: list options: template: '{{ group_name }}' - addLabel: Add New Group... + addLabel: $t:field_options.directus_roles.collection_list.group_name_addLabel fields: - - name: Group Name + - name: $t:field_options.directus_roles.collection_list.fields.group_name field: group_name type: string meta: @@ -101,10 +101,10 @@ fields: interface: input options: iconRight: title - placeholder: Label this group... + placeholder: $t:field_options.directus_roles.collection_list.fields.group_placeholder schema: is_nullable: false - - name: Type + - name: $t:field_options.directus_roles.collection_list.fields.type_name field: accordion type: string schema: @@ -115,21 +115,21 @@ fields: options: choices: - value: always_open - text: Always Open + text: $t:field_options.directus_roles.collection_list.fields.choices_always - value: start_open - text: Start Open + text: $t:field_options.directus_roles.collection_list.fields.choices_start_open - value: start_collapsed - text: Start Collapsed - - name: Collections + text: $t:field_options.directus_roles.collection_list.fields.choices_start_collapsed + - name: $t:field_options.directus_roles.collections_name field: collections type: JSON meta: interface: list options: - addLabel: Add New Collection... + addLabel: $t:field_options.directus_roles.collections_addLabel template: '{{ collection }}' fields: - - name: Collection + - name: $t:field_options.directus_roles.collections_name field: collection type: string meta: diff --git a/api/src/database/system-data/fields/settings.yaml b/api/src/database/system-data/fields/settings.yaml index f135037b38..442cf0df05 100644 --- a/api/src/database/system-data/fields/settings.yaml +++ b/api/src/database/system-data/fields/settings.yaml @@ -8,7 +8,7 @@ fields: interface: input options: iconRight: title - placeholder: My project... + placeholder: $t:field_options.directus_settings.project_name_placeholder translations: language: en-US translations: Name @@ -26,7 +26,7 @@ fields: - field: project_color interface: select-color - note: Login & Logo Background + note: $t:field_options.directus_settings.project_logo_note translations: language: en-US translations: Brand Color @@ -67,7 +67,7 @@ fields: - field: public_note interface: input-multiline options: - placeholder: A short, public message that supports markdown formatting... + placeholder: $t:field_options.directus_settings.public_note_placeholder width: full - field: security_divider @@ -85,11 +85,11 @@ fields: options: choices: - value: null - text: None – Not Recommended + text: $t:field_options.directus_settings.auth_password_policy.none_text - value: '/^.{8,}$/' - text: Weak – Minimum 8 Characters + text: $t:field_options.directus_settings.auth_password_policy.weak_text - value: "/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/" - text: Strong – Upper / Lowercase / Numbers / Special + text: $t:field_options.directus_settings.auth_password_policy.strong_text allowOther: true width: half @@ -135,13 +135,13 @@ fields: options: choices: - value: contain - text: Contain (preserve aspect ratio) + text: $t:field_options.directus_settings.storage_asset_presets.fit.contain_text - value: cover - text: Cover (forces exact size) + text: $t:field_options.directus_settings.storage_asset_presets.fit.cover_text - value: inside - text: Fit inside + text: $t:field_options.directus_settings.storage_asset_presets.fit.fit_text - value: outside - text: Fit outside + text: $t:field_options.directus_settings.storage_asset_presets.fit.outside_text width: half - field: width name: $t:width @@ -181,7 +181,7 @@ fields: interface: boolean width: half options: - label: Don't upscale images + label: $t:no_upscale - field: format name: Format type: string @@ -203,15 +203,14 @@ fields: text: Tiff width: half - field: transforms - name: Additional Transformations + name: $t:field_options.directus_settings.additional_transforms type: json schema: is_nullable: false default_value: [] meta: - note: - The Sharp method name and its arguments. See https://sharp.pixelplumbing.com/api-constructor for more - information. + note: $t:field_options.directus_settings.transforms_note + interface: json options: template: > @@ -279,8 +278,8 @@ fields: interface: input options: icon: key - title: Mapbox Access Token - placeholder: pk.eyJ1Ijo..... + title: $t:field_options.directus_settings.mapbox_key + placeholder: $t:field_options.directus_settings.mapbox_placeholder iconLeft: vpn_key font: monospace width: half @@ -306,11 +305,11 @@ fields: options: choices: - value: raster - text: Raster + text: $t:field_options.directus_settings.basemaps_raster - value: tile - text: Raster TileJSON + text: $t:field_options.directus_settings.basemaps_tile - value: style - text: Mapbox Style + text: $t:field_options.directus_settings.basemaps_style - field: url name: $t:url schema: @@ -319,3 +318,17 @@ fields: interface: text-input options: placeholder: http://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png + - field: tileSize + name: $t:tile_size + schema: + is_nullable: true + meta: + interface: input + options: + placeholder: '512' + conditions: + - name: typeNeqRaster + rule: + type: + _neq: 'raster' + hidden: true diff --git a/api/src/emitter.ts b/api/src/emitter.ts index 8329626ffc..79397dee46 100644 --- a/api/src/emitter.ts +++ b/api/src/emitter.ts @@ -18,7 +18,7 @@ const emitter = new EventEmitter2({ export async function emitAsyncSafe(name: string, ...args: any[]): Promise { try { return await emitter.emitAsync(name, ...args); - } catch (err) { + } catch (err: any) { logger.warn(`An error was thrown while executing hook "${name}"`); logger.warn(err); } diff --git a/api/src/env.ts b/api/src/env.ts index 2dff4b2de1..c6822fe823 100644 --- a/api/src/env.ts +++ b/api/src/env.ts @@ -19,6 +19,8 @@ const defaults: Record = { PUBLIC_URL: '/', MAX_PAYLOAD_SIZE: '100kb', + DB_EXCLUDE_TABLES: 'spatial_ref_sys', + STORAGE_LOCATIONS: 'local', STORAGE_LOCAL_DRIVER: 'local', STORAGE_LOCAL_ROOT: './uploads', diff --git a/api/src/exceptions/database/translate.ts b/api/src/exceptions/database/translate.ts index 99f38367bd..06ae4fcfb9 100644 --- a/api/src/exceptions/database/translate.ts +++ b/api/src/exceptions/database/translate.ts @@ -1,4 +1,6 @@ -import getDatabase from '../../database'; +import { compact, last } from 'lodash'; +import { getDatabaseClient } from '../../database'; +import emitter from '../../emitter'; import { extractError as mssql } from './dialects/mssql'; import { extractError as mysql } from './dialects/mysql'; import { extractError as oracle } from './dialects/oracle'; @@ -16,22 +18,29 @@ import { SQLError } from './dialects/types'; * - Value Too Long */ export async function translateDatabaseError(error: SQLError): Promise { - const database = getDatabase(); + const client = getDatabaseClient(); + let defaultError: any; - switch (database.client.constructor.name) { - case 'Client_MySQL': - return mysql(error); - case 'Client_PG': - return postgres(error); - case 'Client_SQLite3': - return sqlite(error); - case 'Client_Oracledb': - case 'Client_Oracle': - return oracle(error); - case 'Client_MSSQL': - return await mssql(error); - - default: - return error; + switch (client) { + case 'mysql': + defaultError = mysql(error); + break; + case 'postgres': + defaultError = postgres(error); + break; + case 'sqlite': + defaultError = sqlite(error); + break; + case 'oracle': + defaultError = oracle(error); + break; + case 'mssql': + defaultError = await mssql(error); + break; } + + const hookResult = await emitter.emitAsync('database.error', defaultError, { client }); + const hookError = Array.isArray(hookResult) ? last(compact(hookResult)) : hookResult; + + return hookError || defaultError; } diff --git a/api/src/extensions.ts b/api/src/extensions.ts index 1856574860..e9893605c4 100644 --- a/api/src/extensions.ts +++ b/api/src/extensions.ts @@ -21,7 +21,7 @@ import emitter from './emitter'; import env from './env'; import * as exceptions from './exceptions'; import logger from './logger'; -import { HookRegisterFunction, EndpointRegisterFunction } from './types'; +import { HookConfig, EndpointConfig } from './types'; import fse from 'fs-extra'; import { getSchema } from './utils/get-schema'; @@ -33,15 +33,18 @@ import { rollup } from 'rollup'; // @ts-expect-error import virtual from '@rollup/plugin-virtual'; import alias from '@rollup/plugin-alias'; +import { Url } from './utils/url'; +import getModuleDefault from './utils/get-module-default'; let extensions: Extension[] = []; let extensionBundles: Partial> = {}; +const registeredHooks: string[] = []; export async function initializeExtensions(): Promise { try { await ensureExtensionDirs(env.EXTENSIONS_PATH, env.SERVE_APP ? EXTENSION_TYPES : API_EXTENSION_TYPES); extensions = await getExtensions(); - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't load extensions`); logger.warn(err); } @@ -121,14 +124,15 @@ async function generateExtensionBundles() { async function getSharedDepsMapping(deps: string[]) { const appDir = await fse.readdir(path.join(resolvePackage('@directus/app'), 'dist')); - const adminUrl = env.PUBLIC_URL.endsWith('/') ? env.PUBLIC_URL + 'admin' : env.PUBLIC_URL + '/admin'; const depsMapping: Record = {}; for (const dep of deps) { const depName = appDir.find((file) => dep.replace(/\//g, '_') === file.substring(0, file.indexOf('.'))); if (depName) { - depsMapping[dep] = `${adminUrl}/${depName}`; + const depUrl = new Url(env.PUBLIC_URL).addPath('admin', depName); + + depsMapping[dep] = depUrl.toString({ rootRelative: true }); } else { logger.warn(`Couldn't find shared extension dependency "${dep}"`); } @@ -141,7 +145,7 @@ function registerHooks(hooks: Extension[]) { for (const hook of hooks) { try { registerHook(hook); - } catch (error) { + } catch (error: any) { logger.warn(`Couldn't register hook "${hook.name}"`); logger.warn(error); } @@ -149,16 +153,18 @@ function registerHooks(hooks: Extension[]) { function registerHook(hook: Extension) { const hookPath = path.resolve(hook.path, hook.entrypoint || ''); - const hookInstance: HookRegisterFunction | { default?: HookRegisterFunction } = require(hookPath); + const hookInstance: HookConfig | { default: HookConfig } = require(hookPath); - let register: HookRegisterFunction = hookInstance as HookRegisterFunction; - if (typeof hookInstance !== 'function') { - if (hookInstance.default) { - register = hookInstance.default; - } + // Make sure hooks are only registered once + if (registeredHooks.includes(hookPath)) { + return; + } else { + registeredHooks.push(hookPath); } - const events = register({ services, exceptions, env, database: getDatabase(), getSchema }); + const register = getModuleDefault(hookInstance); + + const events = register({ services, exceptions, env, database: getDatabase(), logger, getSchema }); for (const [event, handler] of Object.entries(events)) { if (event.startsWith('cron(')) { @@ -180,7 +186,7 @@ function registerEndpoints(endpoints: Extension[], router: Router) { for (const endpoint of endpoints) { try { registerEndpoint(endpoint); - } catch (error) { + } catch (error: any) { logger.warn(`Couldn't register endpoint "${endpoint.name}"`); logger.warn(error); } @@ -188,18 +194,16 @@ function registerEndpoints(endpoints: Extension[], router: Router) { function registerEndpoint(endpoint: Extension) { const endpointPath = path.resolve(endpoint.path, endpoint.entrypoint || ''); - const endpointInstance: EndpointRegisterFunction | { default?: EndpointRegisterFunction } = require(endpointPath); + const endpointInstance: EndpointConfig | { default: EndpointConfig } = require(endpointPath); - let register: EndpointRegisterFunction = endpointInstance as EndpointRegisterFunction; - if (typeof endpointInstance !== 'function') { - if (endpointInstance.default) { - register = endpointInstance.default; - } - } + const mod = getModuleDefault(endpointInstance); + + const register = typeof mod === 'function' ? mod : mod.handler; + const pathName = typeof mod === 'function' ? endpoint.name : mod.id; const scopedRouter = express.Router(); - router.use(`/${endpoint.name}/`, scopedRouter); + router.use(`/${pathName}`, scopedRouter); - register(scopedRouter, { services, exceptions, env, database: getDatabase(), getSchema }); + register(scopedRouter, { services, exceptions, env, database: getDatabase(), logger, getSchema }); } } diff --git a/api/src/mailer.ts b/api/src/mailer.ts index ab32b8cd7d..6b8bb3b7b4 100644 --- a/api/src/mailer.ts +++ b/api/src/mailer.ts @@ -43,7 +43,7 @@ export default function getMailer(): Transporter { api_key: env.EMAIL_MAILGUN_API_KEY, domain: env.EMAIL_MAILGUN_DOMAIN, }, - host: env.EMAIL_MAILGUN_HOST || 'https://api.mailgun.net', + host: env.EMAIL_MAILGUN_HOST || 'api.mailgun.net', }) as any ); } else { diff --git a/api/src/middleware/authenticate.ts b/api/src/middleware/authenticate.ts index 8e6abe3b52..e156eb07cb 100644 --- a/api/src/middleware/authenticate.ts +++ b/api/src/middleware/authenticate.ts @@ -4,7 +4,7 @@ import getDatabase from '../database'; import env from '../env'; import { InvalidCredentialsException } from '../exceptions'; import asyncHandler from '../utils/async-handler'; -import isJWT from '../utils/is-jwt'; +import isDirectusJWT from '../utils/is-directus-jwt'; /** * Verify the passed JWT and assign the user ID and role to `req` @@ -23,12 +23,12 @@ const authenticate: RequestHandler = asyncHandler(async (req, res, next) => { const database = getDatabase(); - if (isJWT(req.token)) { + if (isDirectusJWT(req.token)) { let payload: { id: string }; try { - payload = jwt.verify(req.token, env.SECRET as string) as { id: string }; - } catch (err) { + payload = jwt.verify(req.token, env.SECRET as string, { issuer: 'directus' }) as { id: string }; + } catch (err: any) { if (err instanceof TokenExpiredError) { throw new InvalidCredentialsException('Token expired.'); } else if (err instanceof JsonWebTokenError) { diff --git a/api/src/middleware/cache.test.ts b/api/src/middleware/cache.test.ts new file mode 100644 index 0000000000..08450d0876 --- /dev/null +++ b/api/src/middleware/cache.test.ts @@ -0,0 +1,76 @@ +import express from 'express'; +import request from 'supertest'; +import checkCacheMiddleware from './cache'; + +jest.mock('../cache'); +jest.mock('../env', () => ({ + CACHE_ENABLED: true, + CACHE_NAMESPACE: 'test', + CACHE_STORE: 'memory', + CACHE_TTL: '5s', + CACHE_CONTROL_S_MAXAGE: true, +})); + +const { cache } = jest.requireMock('../cache'); +const env = jest.requireMock('../env'); + +const handler = jest.fn((req, res) => res.json({ data: 'Uncached value' })); +const setup = () => express().use(checkCacheMiddleware).all('/items/test', handler); + +beforeEach(jest.clearAllMocks); + +describe('cache middleware', () => { + test('should return the cached response for a request', async () => { + cache.get.mockResolvedValueOnce({ data: 'Cached value' }); + cache.get.mockResolvedValueOnce(new Date().getTime() + 1000 * 60); + + const res = await request(setup()).get('/items/test').send(); + + expect(res.body.data).toBe('Cached value'); + expect(res.headers['vary']).toBe('Origin, Cache-Control'); + expect(res.headers['cache-control']).toMatch(/public, max-age=\d+, s-maxage=\d+/); + expect(handler).not.toHaveBeenCalled(); + }); + + test('should call the handler when there is no cached value', async () => { + cache.get.mockResolvedValueOnce(undefined); + + const res = await request(setup()).get('/items/test').send(); + + expect(res.body.data).toBe('Uncached value'); + expect(cache.get).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledTimes(1); + }); + + test('should not cache requests then the cache is disabled', async () => { + env.CACHE_ENABLED = false; + + const res = await request(setup()).get('/items/test').send(); + + expect(res.body.data).toBe('Uncached value'); + expect(cache.get).not.toHaveBeenCalled(); + expect(handler).toHaveBeenCalledTimes(1); + + env.CACHE_ENABLED = true; + }); + + test('should not use cache when the "Cache-Control" header is set to "no-store"', async () => { + const res = await request(setup()).get('/items/test').set('Cache-Control', 'no-store').send(); + + expect(res.body.data).toBe('Uncached value'); + expect(cache.get).not.toHaveBeenCalled(); + expect(handler).toHaveBeenCalledTimes(1); + }); + + test('should only cache get requests', async () => { + const app = setup(); + + await request(app).post('/items/test').send(); + await request(app).put('/items/test').send(); + await request(app).patch('/items/test').send(); + await request(app).delete('/items/test').send(); + + expect(cache.get).not.toHaveBeenCalled(); + expect(handler).toHaveBeenCalledTimes(4); + }); +}); diff --git a/api/src/middleware/cache.ts b/api/src/middleware/cache.ts index 747db06885..52fe344c1b 100644 --- a/api/src/middleware/cache.ts +++ b/api/src/middleware/cache.ts @@ -23,7 +23,7 @@ const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next) try { cachedData = await cache.get(key); - } catch (err) { + } catch (err: any) { logger.warn(err, `[cache] Couldn't read key ${key}. ${err.message}`); return next(); } @@ -33,7 +33,7 @@ const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next) try { cacheExpiryDate = (await cache.get(`${key}__expires_at`)) as number | null; - } catch (err) { + } catch (err: any) { logger.warn(err, `[cache] Couldn't read key ${`${key}__expires_at`}. ${err.message}`); return next(); } diff --git a/api/src/middleware/graphql.ts b/api/src/middleware/graphql.ts index 3374e61d5c..c2b6b411b5 100644 --- a/api/src/middleware/graphql.ts +++ b/api/src/middleware/graphql.ts @@ -17,7 +17,7 @@ export const parseGraphQL: RequestHandler = asyncHandler(async (req, res, next) if (req.method === 'GET') { query = (req.query.query as string | undefined) || null; - if (req.params.variables) { + if (req.query.variables) { try { variables = JSON.parse(req.query.variables as string); } catch { @@ -40,7 +40,7 @@ export const parseGraphQL: RequestHandler = asyncHandler(async (req, res, next) try { document = parse(new Source(query)); - } catch (err) { + } catch (err: any) { throw new InvalidPayloadException(`GraphQL schema validation error.`, { graphqlErrors: [err], }); diff --git a/api/src/middleware/rate-limiter.ts b/api/src/middleware/rate-limiter.ts index 0c59b6b01f..6e715e2f3d 100644 --- a/api/src/middleware/rate-limiter.ts +++ b/api/src/middleware/rate-limiter.ts @@ -18,7 +18,7 @@ if (env.RATE_LIMITER_ENABLED === true) { checkRateLimit = asyncHandler(async (req, res, next) => { try { await rateLimiter.consume(req.ip, 1); - } catch (rateLimiterRes) { + } catch (rateLimiterRes: any) { if (rateLimiterRes instanceof Error) throw rateLimiterRes; res.set('Retry-After', String(rateLimiterRes.msBeforeNext / 1000)); diff --git a/api/src/middleware/respond.ts b/api/src/middleware/respond.ts index 072f3db411..385943bba3 100644 --- a/api/src/middleware/respond.ts +++ b/api/src/middleware/respond.ts @@ -25,7 +25,7 @@ export const respond: RequestHandler = asyncHandler(async (req, res) => { try { await cache.set(key, res.locals.payload, ms(env.CACHE_TTL as string)); await cache.set(`${key}__expires_at`, Date.now() + ms(env.CACHE_TTL as string)); - } catch (err) { + } catch (err: any) { logger.warn(err, `[cache] Couldn't set key ${key}. ${err}`); } diff --git a/api/src/services/authentication.ts b/api/src/services/authentication.ts index 82bb2772e3..5f50c56951 100644 --- a/api/src/services/authentication.ts +++ b/api/src/services/authentication.ts @@ -65,13 +65,13 @@ export class AuthenticationService { const { email, password, ip, userAgent, otp } = options; - let user = await this.knex + const user = await this.knex .select('id', 'password', 'role', 'tfa_secret', 'status') .from('directus_users') .whereRaw('LOWER(??) = ?', ['email', email.toLowerCase()]) .first(); - const updatedUser = await emitter.emitAsync('auth.login.before', options, { + const updatedOptions = await emitter.emitAsync('auth.login.before', options, { event: 'auth.login.before', action: 'login', schema: this.schema, @@ -82,8 +82,8 @@ export class AuthenticationService { database: this.knex, }); - if (updatedUser) { - user = updatedUser.length > 0 ? updatedUser.reduce((val, acc) => merge(acc, val)) : user; + if (updatedOptions) { + options = updatedOptions.length > 0 ? updatedOptions.reduce((acc, val) => merge(acc, val), {}) : options; } const emitStatus = (status: 'fail' | 'success') => { @@ -121,7 +121,7 @@ export class AuthenticationService { try { await loginAttemptsLimiter.consume(user.id); - } catch (err) { + } catch { await this.knex('directus_users').update({ status: 'suspended' }).where({ id: user.id }); user.status = 'suspended'; @@ -171,6 +171,7 @@ export class AuthenticationService { */ const accessToken = jwt.sign(payload, env.SECRET as string, { expiresIn: env.ACCESS_TOKEN_TTL, + issuer: 'directus', }); const refreshToken = nanoid(64); @@ -237,6 +238,7 @@ export class AuthenticationService { const accessToken = jwt.sign({ id: record.id }, env.SECRET as string, { expiresIn: env.ACCESS_TOKEN_TTL, + issuer: 'directus', }); const newRefreshToken = nanoid(64); diff --git a/api/src/services/authorization.ts b/api/src/services/authorization.ts index e36f49d0a8..c5c749d95f 100644 --- a/api/src/services/authorization.ts +++ b/api/src/services/authorization.ts @@ -186,15 +186,6 @@ export class AuthorizationService { } if (query.filter._and.length === 0) delete query.filter._and; - - if (permissions.limit && query.limit && query.limit > permissions.limit) { - throw new ForbiddenException(); - } - - // Default to the permissions limit if limit hasn't been set - if (permissions.limit && !query.limit) { - query.limit = permissions.limit; - } } } } @@ -215,7 +206,6 @@ export class AuthorizationService { action, permissions: {}, validation: {}, - limit: null, fields: ['*'], presets: {}, }; @@ -316,7 +306,7 @@ export class AuthorizationService { }; if (Array.isArray(pk)) { - const result = await itemsService.readMany(pk, query, { permissionsAction: action }); + const result = await itemsService.readMany(pk, { ...query, limit: pk.length }, { permissionsAction: action }); if (!result) throw new ForbiddenException(); if (result.length !== pk.length) throw new ForbiddenException(); } else { diff --git a/api/src/services/collections.ts b/api/src/services/collections.ts index df6a55d5bc..7bf1995a84 100644 --- a/api/src/services/collections.ts +++ b/api/src/services/collections.ts @@ -414,15 +414,6 @@ export class CollectionsService { if (relation.related_collection === collectionKey) { await fieldsService.deleteField(relation.collection, relation.field); } - - const isM2O = relation.collection === collectionKey; - - // Delete any fields that have a relationship to/from the current collection - if (isM2O && relation.related_collection && relation.meta?.one_field) { - await fieldsService.deleteField(relation.related_collection!, relation.meta.one_field); - } else { - await fieldsService.deleteField(relation.collection, relation.field); - } } const m2aRelationsThatIncludeThisCollection = this.schema.relations.filter((relation) => { diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index 6aabdc8482..18b2f643c1 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -277,7 +277,7 @@ export class FieldsService { if (!field.schema) return; this.addColumnToTable(table, field, existingColumn); }); - } catch (err) { + } catch (err: any) { throw await translateDatabaseError(err); } } diff --git a/api/src/services/files.ts b/api/src/services/files.ts index 80b03f0b0d..10a8080fe1 100644 --- a/api/src/services/files.ts +++ b/api/src/services/files.ts @@ -63,7 +63,7 @@ export class FilesService extends ItemsService { try { await storage.disk(data.storage).put(payload.filename_disk, stream, payload.type); - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't save file ${payload.filename_disk}`); logger.warn(err); throw new ServiceUnavailableException(`Couldn't save file ${payload.filename_disk}`, { service: 'files' }); @@ -88,7 +88,7 @@ export class FilesService extends ItemsService { try { payload.metadata = await exifr.parse(buffer.content, { - icc: true, + icc: false, iptc: true, ifd1: true, interop: true, @@ -105,7 +105,7 @@ export class FilesService extends ItemsService { if (payload.metadata?.iptc?.Keywords) { payload.tags = payload.metadata.iptc.Keywords; } - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't extract metadata from file`); logger.warn(err); } @@ -156,7 +156,7 @@ export class FilesService extends ItemsService { fileResponse = await axios.get(importURL, { responseType: 'stream', }); - } catch (err) { + } catch (err: any) { logger.warn(`Couldn't fetch file from url "${importURL}"`); logger.warn(err); throw new ServiceUnavailableException(`Couldn't fetch file from url "${importURL}"`, { diff --git a/api/src/services/graphql.ts b/api/src/services/graphql.ts index e80d30c7d9..77623e4822 100644 --- a/api/src/services/graphql.ts +++ b/api/src/services/graphql.ts @@ -74,6 +74,7 @@ import { SpecificationService } from './specifications'; import { UsersService } from './users'; import { UtilsService } from './utils'; import { WebhooksService } from './webhooks'; +import { generateHash } from '../utils/generate-hash'; const GraphQLVoid = new GraphQLScalarType({ name: 'Void', @@ -157,7 +158,7 @@ export class GraphQLService { variableValues: variables, operationName, }); - } catch (err) { + } catch (err: any) { throw new InvalidPayloadException('GraphQL execution error.', { graphqlErrors: [err.message] }); } @@ -1169,8 +1170,8 @@ export class GraphQLService { return { ids: keys }; } } - } catch (err) { - this.formatError(err); + } catch (err: any) { + return this.formatError(err); } } @@ -1206,7 +1207,7 @@ export class GraphQLService { } return true; - } catch (err) { + } catch (err: any) { throw this.formatError(err); } } @@ -1766,7 +1767,7 @@ export class GraphQLService { try { await service.requestPasswordReset(args.email, args.reset_url || null); - } catch (err) { + } catch (err: any) { if (err instanceof InvalidPayloadException) { throw err; } @@ -1864,7 +1865,7 @@ export class GraphQLService { string: GraphQLNonNull(GraphQLString), }, resolve: async (_, args) => { - return await argon2.hash(args.string); + return await generateHash(args.string); }, }, utils_hash_verify: { diff --git a/api/src/services/import.ts b/api/src/services/import.ts index b82ca3b6a2..37c8f674cd 100644 --- a/api/src/services/import.ts +++ b/api/src/services/import.ts @@ -104,8 +104,16 @@ export class ImportService { .pipe(csv()) .on('data', (value: Record) => { const obj = transform(value, (result: Record, value, key) => { - if (value.length === 0) delete result[key]; - else set(result, key, value); + if (value.length === 0) { + delete result[key]; + } else { + try { + const parsedJson = JSON.parse(value); + set(result, key, parsedJson); + } catch { + set(result, key, value); + } + } }); saveQueue.push(obj); diff --git a/api/src/services/items.ts b/api/src/services/items.ts index ce0b95324f..979fb448fc 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -135,7 +135,7 @@ export class ItemsService implements AbstractSer try { const result = await trx.insert(payloadWithoutAliases).into(this.collection).returning(primaryKeyField); primaryKey = primaryKey ?? result[0]; - } catch (err) { + } catch (err: any) { throw await translateDatabaseError(err); } @@ -442,7 +442,7 @@ export class ItemsService implements AbstractSer if (Object.keys(payloadWithTypeCasting).length > 0) { try { await trx(this.collection).update(payloadWithTypeCasting).whereIn(primaryKeyField, keys); - } catch (err) { + } catch (err: any) { throw await translateDatabaseError(err); } } diff --git a/api/src/services/mail/index.ts b/api/src/services/mail/index.ts index b02a4c90ce..8a990961fc 100644 --- a/api/src/services/mail/index.ts +++ b/api/src/services/mail/index.ts @@ -11,6 +11,7 @@ import { Accountability } from '@directus/shared/types'; import getMailer from '../../mailer'; import { Transporter, SendMailOptions } from 'nodemailer'; import prettier from 'prettier'; +import { Url } from '../../utils/url'; const liquidEngine = new Liquid({ root: [path.resolve(env.EXTENSIONS_PATH, 'templates'), path.resolve(__dirname, 'templates')], @@ -100,16 +101,15 @@ export class MailService { }; function getProjectLogoURL(logoID?: string) { - let projectLogoURL = env.PUBLIC_URL; - if (projectLogoURL.endsWith('/') === false) { - projectLogoURL += '/'; - } + const projectLogoUrl = new Url(env.PUBLIC_URL); + if (logoID) { - projectLogoURL += `assets/${logoID}`; + projectLogoUrl.addPath('assets', logoID); } else { - projectLogoURL += `admin/img/directus-white.png`; + projectLogoUrl.addPath('admin', 'img', 'directus-white.png'); } - return projectLogoURL; + + return projectLogoUrl.toString(); } } } diff --git a/api/src/services/payload.ts b/api/src/services/payload.ts index 1250805ed1..ae0e59b4be 100644 --- a/api/src/services/payload.ts +++ b/api/src/services/payload.ts @@ -1,4 +1,3 @@ -import argon2 from 'argon2'; import { format, parseISO } from 'date-fns'; import Joi from 'joi'; import { Knex } from 'knex'; @@ -14,6 +13,7 @@ import { unflatten } from 'flat'; import { isNativeGeometry } from '../utils/geometry'; import { getGeometryHelper } from '../database/helpers/geometry'; import { parse as wktToGeoJSON } from 'wellknown'; +import { generateHash } from '../utils/generate-hash'; type Action = 'create' | 'read' | 'update'; @@ -49,9 +49,8 @@ export class PayloadService { public transformers: Transformers = { async hash({ action, value }) { if (!value) return; - if (action === 'create' || action === 'update') { - return await argon2.hash(String(value)); + return await generateHash(String(value)); } return value; diff --git a/api/src/services/server.ts b/api/src/services/server.ts index 827e6c7db4..c371ddf447 100644 --- a/api/src/services/server.ts +++ b/api/src/services/server.ts @@ -209,7 +209,7 @@ export class ServerService { try { await cache!.set(`health-${checkID}`, true, 5); await cache!.delete(`health-${checkID}`); - } catch (err) { + } catch (err: any) { checks['cache:responseTime'][0].status = 'error'; checks['cache:responseTime'][0].output = err; } finally { @@ -249,7 +249,7 @@ export class ServerService { try { await rateLimiter.consume(`health-${checkID}`, 1); await rateLimiter.delete(`health-${checkID}`); - } catch (err) { + } catch (err: any) { checks['rateLimiter:responseTime'][0].status = 'error'; checks['rateLimiter:responseTime'][0].output = err; } finally { @@ -289,7 +289,7 @@ export class ServerService { await disk.put(`health-${checkID}`, 'check'); await disk.get(`health-${checkID}`); await disk.delete(`health-${checkID}`); - } catch (err) { + } catch (err: any) { checks[`storage:${location}:responseTime`][0].status = 'error'; checks[`storage:${location}:responseTime`][0].output = err; } finally { @@ -323,7 +323,7 @@ export class ServerService { try { await mailer.verify(); - } catch (err) { + } catch (err: any) { checks['email:connection'][0].status = 'error'; checks['email:connection'][0].output = err; } diff --git a/api/src/services/users.ts b/api/src/services/users.ts index 0a90249ca9..ffbd864af4 100644 --- a/api/src/services/users.ts +++ b/api/src/services/users.ts @@ -1,4 +1,3 @@ -import argon2 from 'argon2'; import jwt from 'jsonwebtoken'; import { Knex } from 'knex'; import { clone, cloneDeep } from 'lodash'; @@ -17,7 +16,9 @@ import { AbstractServiceOptions, Item, PrimaryKey, Query, SchemaOverview } from import { Accountability } from '@directus/shared/types'; import isUrlAllowed from '../utils/is-url-allowed'; import { toArray } from '@directus/shared/utils'; +import { Url } from '../utils/url'; import { AuthenticationService } from './authentication'; +import { generateHash } from '../utils/generate-hash'; import { ItemsService, MutationOptions } from './items'; import { MailService } from './mail'; import { SettingsService } from './settings'; @@ -304,10 +305,10 @@ export class UsersService extends ItemsService { await service.createOne({ email, role, status: 'invited' }); const payload = { email, scope: 'invite' }; - const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '7d' }); - const inviteURL = url ?? env.PUBLIC_URL + '/admin/accept-invite'; - const acceptURL = inviteURL + '?token=' + token; - const subjectLine = subject ? subject : "You've been invited"; + const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '7d', issuer: 'directus' }); + const subjectLine = subject ?? "You've been invited"; + const inviteURL = url ? new Url(url) : new Url(env.PUBLIC_URL).addPath('admin', 'accept-invite'); + inviteURL.setQuery('token', token); await mailService.send({ to: email, @@ -315,7 +316,7 @@ export class UsersService extends ItemsService { template: { name: 'user-invitation', data: { - url: acceptURL, + url: inviteURL.toString(), email, }, }, @@ -325,7 +326,7 @@ export class UsersService extends ItemsService { } async acceptInvite(token: string, password: string): Promise { - const { email, scope } = jwt.verify(token, env.SECRET as string) as { + const { email, scope } = jwt.verify(token, env.SECRET as string, { issuer: 'directus' }) as { email: string; scope: string; }; @@ -338,7 +339,7 @@ export class UsersService extends ItemsService { throw new InvalidPayloadException(`Email address ${email} hasn't been invited.`); } - const passwordHashed = await argon2.hash(password); + const passwordHashed = generateHash(password); await this.knex('directus_users').update({ password: passwordHashed, status: 'active' }).where({ id: user.id }); @@ -364,7 +365,7 @@ export class UsersService extends ItemsService { }); const payload = { email, scope: 'password-reset' }; - const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '1d' }); + const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '1d', issuer: 'directus' }); if (url && isUrlAllowed(url, env.PASSWORD_RESET_URL_ALLOW_LIST) === false) { throw new InvalidPayloadException(`Url "${url}" can't be used to reset passwords.`); @@ -389,7 +390,7 @@ export class UsersService extends ItemsService { } async resetPassword(token: string, password: string): Promise { - const { email, scope } = jwt.verify(token, env.SECRET as string) as { + const { email, scope } = jwt.verify(token, env.SECRET as string, { issuer: 'directus' }) as { email: string; scope: string; }; @@ -402,7 +403,7 @@ export class UsersService extends ItemsService { throw new ForbiddenException(); } - const passwordHashed = await argon2.hash(password); + const passwordHashed = await generateHash(password); await this.knex('directus_users').update({ password: passwordHashed, status: 'active' }).where({ id: user.id }); diff --git a/api/src/start.ts b/api/src/start.ts index 8228c8e4ee..9079617f2d 100644 --- a/api/src/start.ts +++ b/api/src/start.ts @@ -30,7 +30,7 @@ export default async function start(): Promise { // No need to log/warn here. The update message is only an informative nice-to-have }); - logger.info(`Server started at port ${port}`); + logger.info(`Server started at http://localhost:${port}`); emitAsyncSafe('server.start'); }) .once('error', (err: any) => { diff --git a/api/src/types/extensions.ts b/api/src/types/extensions.ts index 2250bd89d5..4b5f525e82 100644 --- a/api/src/types/extensions.ts +++ b/api/src/types/extensions.ts @@ -1,6 +1,7 @@ import { ListenerFn } from 'eventemitter2'; import { Router } from 'express'; import { Knex } from 'knex'; +import { Logger } from 'pino'; import env from '../env'; import * as exceptions from '../exceptions'; import * as services from '../services'; @@ -11,8 +12,18 @@ export type ExtensionContext = { exceptions: typeof exceptions; database: Knex; env: typeof env; + logger: Logger; getSchema: typeof getSchema; }; -export type HookRegisterFunction = (context: ExtensionContext) => Record; -export type EndpointRegisterFunction = (router: Router, context: ExtensionContext) => void; +type HookHandlerFunction = (context: ExtensionContext) => Record; + +export type HookConfig = HookHandlerFunction; + +type EndpointHandlerFunction = (router: Router, context: ExtensionContext) => void; +interface EndpointAdvancedConfig { + id: string; + handler: EndpointHandlerFunction; +} + +export type EndpointConfig = EndpointHandlerFunction | EndpointAdvancedConfig; diff --git a/api/src/types/permissions.ts b/api/src/types/permissions.ts index 17d97fa1aa..76dddb6f41 100644 --- a/api/src/types/permissions.ts +++ b/api/src/types/permissions.ts @@ -9,7 +9,6 @@ export type Permission = { action: PermissionsAction; permissions: Record; validation: Filter | null; - limit: number | null; presets: Record | null; fields: string[] | null; system?: true; diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index 15436bf94e..d9c4e9c0e3 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -322,15 +322,13 @@ export function applyFilter( if (operator === '_empty' || (operator === '_nempty' && compareValue === false)) { dbQuery[logical].andWhere((query) => { - query.whereNull(selectionRaw); - query.orWhere(selectionRaw, '=', ''); + query.where(key, '=', ''); }); } if (operator === '_nempty' || (operator === '_empty' && compareValue === false)) { dbQuery[logical].andWhere((query) => { - query.whereNotNull(selectionRaw); - query.orWhere(selectionRaw, '!=', ''); + query.where(key, '!=', ''); }); } @@ -344,8 +342,6 @@ export function applyFilter( // reported as [undefined]. // We need to remove any undefined values, as they are useless compareValue = compareValue.filter((val) => val !== undefined); - // And ignore the result filter if there are no values in it - if (compareValue.length === 0) return; } if (operator === '_eq') { diff --git a/api/src/utils/generate-hash.ts b/api/src/utils/generate-hash.ts new file mode 100644 index 0000000000..d474cd170f --- /dev/null +++ b/api/src/utils/generate-hash.ts @@ -0,0 +1,10 @@ +import argon2 from 'argon2'; +import { getConfigFromEnv } from './get-config-from-env'; + +export function generateHash(stringToHash: string): Promise { + const argon2HashConfigOptions = getConfigFromEnv('HASH_', 'HASH_RAW'); // Disallow the HASH_RAW option, see https://github.com/directus/directus/discussions/7670#discussioncomment-1255805 + // associatedData, if specified, must be passed as a Buffer to argon2.hash, see https://github.com/ranisalt/node-argon2/wiki/Options#associateddata + 'associatedData' in argon2HashConfigOptions && + (argon2HashConfigOptions.associatedData = Buffer.from(argon2HashConfigOptions.associatedData)); + return argon2.hash(stringToHash, argon2HashConfigOptions); +} diff --git a/api/src/utils/get-cache-key.test.ts b/api/src/utils/get-cache-key.test.ts new file mode 100644 index 0000000000..82f528a20f --- /dev/null +++ b/api/src/utils/get-cache-key.test.ts @@ -0,0 +1,60 @@ +import { Request } from 'express'; +import { getCacheKey } from './get-cache-key'; + +const restUrl = 'http://localhost/items/example'; +const graphQlUrl = 'http://localhost/graphql'; +const accountability = { user: '00000000-0000-0000-0000-000000000000' }; + +const requests = [ + { + name: 'as unauthenticated request', + params: { originalUrl: restUrl }, + key: '17da8272c9a0ec6eea38a37d6d78bddeb7c79045', + }, + { + name: 'as authenticated request', + params: { originalUrl: restUrl, accountability }, + key: '99a6394222a3d7d149ac1662fc2fff506932db58', + }, + { + name: 'a request with a fields query', + params: { originalUrl: restUrl, sanitizedQuery: { fields: ['id', 'name'] } }, + key: 'aa6e2d8a78de4dfb4af6eaa230d1cd9b7d31ed19', + }, + { + name: 'a request with a filter query', + params: { originalUrl: restUrl, sanitizedQuery: { filter: { name: { _eq: 'test' } } } }, + key: 'd7eb8970f0429e1cf85e12eb5bb8669f618b09d3', + }, + { + name: 'a GraphQL query request', + params: { originalUrl: graphQlUrl, query: { query: 'query { test { id } }' } }, + key: '201731b75c627c60554512d819b6935b54c73814', + }, +]; + +const cases = requests.map(({ name, params, key }) => [name, params, key]); + +describe('get cache key', () => { + test.each(cases)('should create a cache key for %s', (_, params, key) => { + expect(getCacheKey(params as unknown as Request)).toEqual(key); + }); + + test('should create a unique key for each request', () => { + const keys = requests.map((r) => r.key); + const hasDuplicate = keys.some((key) => keys.indexOf(key) !== keys.lastIndexOf(key)); + + expect(hasDuplicate).toBeFalsy(); + }); + + test('should create a unique key for GraphQL requests with different variables', () => { + const query = 'query Test ($name: String) { test (filter: { name: { _eq: $name } }) { id } }'; + const operationName = 'test'; + const variables1 = JSON.stringify({ name: 'test 1' }); + const variables2 = JSON.stringify({ name: 'test 2' }); + const req1: any = { originalUrl: graphQlUrl, query: { query, operationName, variables: variables1 } }; + const req2: any = { originalUrl: graphQlUrl, query: { query, operationName, variables: variables2 } }; + + expect(getCacheKey(req1)).not.toEqual(getCacheKey(req2)); + }); +}); diff --git a/api/src/utils/get-cache-key.ts b/api/src/utils/get-cache-key.ts index 99f83e54f3..99087518bd 100644 --- a/api/src/utils/get-cache-key.ts +++ b/api/src/utils/get-cache-key.ts @@ -1,14 +1,16 @@ import { Request } from 'express'; import url from 'url'; import hash from 'object-hash'; +import { pick } from 'lodash'; export function getCacheKey(req: Request): string { const path = url.parse(req.originalUrl).pathname; + const isGraphQl = path?.includes('/graphql'); const info = { user: req.accountability?.user || null, path, - query: path?.includes('/graphql') ? req.query.query : req.sanitizedQuery, + query: isGraphQl ? pick(req.query, ['query', 'variables']) : req.sanitizedQuery, }; const key = hash(info); diff --git a/api/src/utils/get-local-type.ts b/api/src/utils/get-local-type.ts index 3e70fd3d25..6c4932f191 100644 --- a/api/src/utils/get-local-type.ts +++ b/api/src/utils/get-local-type.ts @@ -145,8 +145,12 @@ export default function getLocalType( return { type: 'text' }; } - /** Handle Boolean as TINYINT*/ - if (column.data_type.toLowerCase() === 'tinyint(1)' || column.data_type.toLowerCase() === 'tinyint(0)') { + /** Handle Boolean as TINYINT and edgecase MySQL where it still is just tinyint */ + if ( + (database.client.constructor.name === 'Client_MySQL' && column.data_type.toLowerCase() === 'tinyint') || + column.data_type.toLowerCase() === 'tinyint(1)' || + column.data_type.toLowerCase() === 'tinyint(0)' + ) { return { type: 'boolean' }; } diff --git a/api/src/utils/get-module-default.ts b/api/src/utils/get-module-default.ts new file mode 100644 index 0000000000..3727e4e5ea --- /dev/null +++ b/api/src/utils/get-module-default.ts @@ -0,0 +1,6 @@ +export default function getModuleDefault(mod: T | { default: T }): T { + if ('default' in mod) { + return mod.default; + } + return mod; +} diff --git a/api/src/utils/get-schema.ts b/api/src/utils/get-schema.ts index 9f6759cf78..f3dcd0484b 100644 --- a/api/src/utils/get-schema.ts +++ b/api/src/utils/get-schema.ts @@ -32,7 +32,7 @@ export async function getSchema(options?: { try { cachedSchema = (await schemaCache.get('schema')) as SchemaOverview; - } catch (err) { + } catch (err: any) { logger.warn(err, `[schema-cache] Couldn't retrieve cache. ${err}`); } @@ -47,7 +47,7 @@ export async function getSchema(options?: { result, typeof env.CACHE_SCHEMA === 'string' ? ms(env.CACHE_SCHEMA) : undefined ); - } catch (err) { + } catch (err: any) { logger.warn(err, `[schema-cache] Couldn't save cache. ${err}`); } } @@ -116,6 +116,11 @@ async function getDatabaseSchema( ]; for (const [collection, info] of Object.entries(schemaOverview)) { + if (toArray(env.DB_EXCLUDE_TABLES).includes(collection)) { + logger.trace(`Collection "${collection}" is configured to be excluded and will be ignored`); + continue; + } + if (!info.primary) { logger.warn(`Collection "${collection}" doesn't have a primary key column and will be ignored`); continue; diff --git a/api/src/utils/is-jwt.ts b/api/src/utils/is-directus-jwt.ts similarity index 62% rename from api/src/utils/is-jwt.ts rename to api/src/utils/is-directus-jwt.ts index 819d7405be..4dc7ceae52 100644 --- a/api/src/utils/is-jwt.ts +++ b/api/src/utils/is-directus-jwt.ts @@ -2,9 +2,10 @@ import atob from 'atob'; import logger from '../logger'; /** - * Check if a given string conforms to the structure of a JWT. + * Check if a given string conforms to the structure of a JWT + * and whether it is issued by Directus. */ -export default function isJWT(string: string): boolean { +export default function isDirectusJWT(string: string): boolean { const parts = string.split('.'); // JWTs have the structure header.payload.signature @@ -15,7 +16,7 @@ export default function isJWT(string: string): boolean { atob(parts[0]); atob(parts[1]); atob(parts[2]); - } catch (err) { + } catch (err: any) { logger.error(err); return false; } @@ -23,7 +24,8 @@ export default function isJWT(string: string): boolean { // Check if the header and payload are valid JSON try { JSON.parse(atob(parts[0])); - JSON.parse(atob(parts[1])); + const payload = JSON.parse(atob(parts[1])); + if (payload.iss !== 'directus') return false; } catch { return false; } diff --git a/api/src/utils/merge-permissions.ts b/api/src/utils/merge-permissions.ts index 1af0ba1b8e..5b5f029898 100644 --- a/api/src/utils/merge-permissions.ts +++ b/api/src/utils/merge-permissions.ts @@ -25,7 +25,6 @@ function mergePerm(currentPerm: Permission, newPerm: Permission) { let validation = currentPerm.validation; let fields = currentPerm.fields; let presets = currentPerm.presets; - let limit = currentPerm.limit; if (newPerm.permissions) { if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === '_or') { @@ -73,16 +72,11 @@ function mergePerm(currentPerm: Permission, newPerm: Permission) { presets = merge({}, presets, newPerm.presets); } - if (newPerm.limit && newPerm.limit > (currentPerm.limit || 0)) { - limit = newPerm.limit; - } - return { ...currentPerm, permissions, validation, fields, presets, - limit, }; } diff --git a/api/src/utils/track.ts b/api/src/utils/track.ts index 2838db2a63..c0bccc57c0 100644 --- a/api/src/utils/track.ts +++ b/api/src/utils/track.ts @@ -13,7 +13,7 @@ export async function track(event: string): Promise { try { await axios.post('https://telemetry.directus.io/', info); - } catch (err) { + } catch (err: any) { if (env.NODE_ENV === 'development') { logger.error(err); } diff --git a/api/src/utils/transformations.ts b/api/src/utils/transformations.ts index 7d8eac17a1..75426e5cc6 100644 --- a/api/src/utils/transformations.ts +++ b/api/src/utils/transformations.ts @@ -16,14 +16,22 @@ export function resolvePreset(input: TransformationParams | TransformationPreset ); } -function extractOptions>(keys: (keyof T)[], numberKeys: (keyof T)[] = []) { +function extractOptions>( + keys: (keyof T)[], + numberKeys: (keyof T)[] = [], + booleanKeys: (keyof T)[] = [] +) { return function (input: TransformationParams | TransformationPreset): T { return Object.entries(input).reduce( (config, [key, value]) => keys.includes(key as any) && isNil(value) === false ? { ...config, - [key]: numberKeys.includes(key as any) ? +value : value, + [key]: numberKeys.includes(key as any) + ? +value + : booleanKeys.includes(key as any) + ? Boolean(value) + : value, } : config, {} as T @@ -53,7 +61,8 @@ function extractResize(input: TransformationParams | TransformationPreset): Tran 'resize', extractOptions( ['width', 'height', 'fit', 'withoutEnlargement'], - ['width', 'height'] + ['width', 'height'], + ['withoutEnlargement'] )(input), ]; } diff --git a/api/src/utils/url.ts b/api/src/utils/url.ts new file mode 100644 index 0000000000..a084a26757 --- /dev/null +++ b/api/src/utils/url.ts @@ -0,0 +1,78 @@ +import { URL } from 'url'; + +export class Url { + protocol: string | null; + host: string | null; + port: string | null; + path: string[]; + query: Record; + hash: string | null; + + constructor(url: string) { + const parsedUrl = new URL(url, 'http://localhost'); + + const isProtocolRelative = /^\/\//.test(url); + const isRootRelative = /^\/$|^\/[^/]/.test(url); + const isPathRelative = /^\./.test(url); + + this.protocol = + !isProtocolRelative && !isRootRelative && !isPathRelative + ? parsedUrl.protocol.substring(0, parsedUrl.protocol.length - 1) + : null; + this.host = !isRootRelative && !isPathRelative ? parsedUrl.host : null; + this.port = parsedUrl.port !== '' ? parsedUrl.port : null; + this.path = parsedUrl.pathname.split('/').filter((p) => p !== ''); + this.query = Object.fromEntries(parsedUrl.searchParams.entries()); + this.hash = parsedUrl.hash !== '' ? parsedUrl.hash.substring(1) : null; + } + + public isAbsolute(): boolean { + return this.protocol !== null && this.host !== null; + } + + public isProtocolRelative(): boolean { + return this.protocol === null && this.host !== null; + } + + public isRootRelative(): boolean { + return this.protocol === null && this.host === null; + } + + public addPath(...paths: string[]): Url { + const pathToAdd = paths.flatMap((p) => p.split('/')).filter((p) => p !== ''); + + for (const pathSegment of pathToAdd) { + if (pathSegment === '..') { + this.path.pop(); + } else if (pathSegment !== '.') { + this.path.push(pathSegment); + } + } + + return this; + } + + public setQuery(key: string, value: string): Url { + this.query[key] = value; + + return this; + } + + public toString({ rootRelative } = { rootRelative: false }): string { + const protocol = this.protocol !== null ? `${this.protocol}:` : ''; + const host = this.host ?? ''; + const port = this.port !== null ? `:${this.port}` : ''; + const origin = `${this.host !== null ? `${protocol}//` : ''}${host}${port}`; + + const path = `/${this.path.join('/')}`; + const query = + Object.keys(this.query).length !== 0 + ? `?${Object.entries(this.query) + .map(([k, v]) => `${k}=${v}`) + .join('&')}` + : ''; + const hash = this.hash !== null ? `#${this.hash}` : ''; + + return `${!rootRelative ? origin : ''}${path}${query}${hash}`; + } +} diff --git a/api/src/webhooks.ts b/api/src/webhooks.ts index d39d42be80..12f33f0a55 100644 --- a/api/src/webhooks.ts +++ b/api/src/webhooks.ts @@ -61,7 +61,7 @@ function createHandler(webhook: Webhook): ListenerFn { method: webhook.method, data: webhook.data ? webhookPayload : null, }); - } catch (error) { + } catch (error: any) { logger.warn(`Webhook "${webhook.name}" (id: ${webhook.id}) failed`); logger.warn(error); } diff --git a/app/package.json b/app/package.json index f0474552a6..dc34152bbd 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "@directus/app", - "version": "9.0.0-rc.90", + "version": "9.0.0-rc.92", "private": false, "description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases", "author": "Rijk van Zanten ", @@ -27,10 +27,10 @@ }, "gitHead": "24621f3934dc77eb23441331040ed13c676ceffd", "devDependencies": { - "@directus/docs": "9.0.0-rc.90", - "@directus/extension-sdk": "9.0.0-rc.90", - "@directus/format-title": "9.0.0-rc.90", - "@directus/shared": "9.0.0-rc.90", + "@directus/docs": "9.0.0-rc.92", + "@directus/extensions-sdk": "9.0.0-rc.92", + "@directus/format-title": "9.0.0-rc.92", + "@directus/shared": "9.0.0-rc.92", "@fullcalendar/core": "5.9.0", "@fullcalendar/daygrid": "5.9.0", "@fullcalendar/interaction": "5.9.0", @@ -38,7 +38,7 @@ "@fullcalendar/timegrid": "5.9.0", "@mapbox/mapbox-gl-draw": "1.3.0", "@mapbox/mapbox-gl-draw-static-mode": "1.0.1", - "@mapbox/mapbox-gl-geocoder": "4.7.2", + "@mapbox/mapbox-gl-geocoder": "4.7.3", "@popperjs/core": "2.9.3", "@rollup/plugin-yaml": "3.1.0", "@sindresorhus/slugify": "2.1.0", @@ -48,56 +48,58 @@ "@types/bytes": "3.1.1", "@types/codemirror": "5.60.2", "@types/color": "3.0.2", + "@types/diacritics": "1.3.1", "@types/diff": "5.0.1", "@types/dompurify": "2.2.3", "@types/geojson": "7946.0.8", "@types/lodash": "4.14.172", "@types/mapbox__mapbox-gl-draw": "1.2.3", "@types/mapbox__mapbox-gl-geocoder": "4.7.1", - "@types/markdown-it": "12.2.0", - "@types/marked": "2.0.4", - "@types/mime-types": "2.1.0", + "@types/markdown-it": "12.2.1", + "@types/marked": "2.0.5", + "@types/mime-types": "2.1.1", "@types/ms": "0.7.31", "@types/qrcode": "1.4.1", "@types/wellknown": "0.5.1", - "@vitejs/plugin-vue": "1.4.0", + "@vitejs/plugin-vue": "1.6.2", "@vue/cli-plugin-babel": "4.5.13", "@vue/cli-plugin-router": "4.5.13", "@vue/cli-plugin-typescript": "4.5.13", "@vue/cli-plugin-vuex": "4.5.13", "@vue/cli-service": "4.5.13", - "@vue/compiler-sfc": "3.2.2", - "axios": "0.21.1", + "@vue/compiler-sfc": "3.2.11", + "axios": "0.21.4", "base-64": "1.0.0", - "codemirror": "5.62.2", + "codemirror": "5.62.3", "copyfiles": "2.4.1", "cropperjs": "1.5.12", "date-fns": "2.23.0", - "dompurify": "2.3.0", + "diacritics": "1.3.0", + "dompurify": "2.3.1", "escape-string-regexp": "5.0.0", "front-matter": "4.0.2", "html-entities": "2.3.2", "jsonlint-mod": "1.7.6", "maplibre-gl": "1.15.2", - "marked": "2.1.3", + "marked": "3.0.0", "micromustache": "8.0.3", "mime": "2.5.2", "mitt": "3.0.0", "nanoid": "3.1.25", "p-queue": "7.1.0", - "pinia": "2.0.0-rc.4", - "prettier": "2.3.2", + "pinia": "2.0.0-rc.9", + "prettier": "2.4.0", "pretty-ms": "7.0.1", "qrcode": "1.4.4", "rimraf": "3.0.2", - "sass": "1.37.5", - "tinymce": "5.8.2", - "typescript": "4.3.5", - "vite": "2.4.4", - "vue": "3.2.2", + "sass": "1.39.2", + "tinymce": "5.9.2", + "typescript": "4.4.3", + "vite": "2.5.7", + "vue": "3.2.11", "vue-i18n": "9.1.7", "vue-router": "4.0.11", - "vuedraggable": "4.0.3", + "vuedraggable": "4.1.0", "wellknown": "0.5.0" } } diff --git a/app/src/api.ts b/app/src/api.ts index 3a7e2f957e..5c380b1a01 100644 --- a/app/src/api.ts +++ b/app/src/api.ts @@ -95,13 +95,13 @@ api.interceptors.response.use(onResponse, onError); export default api; -function getToken() { +export function getToken(): string | null { return api.defaults.headers?.['Authorization']?.split(' ')[1] || null; } export function addTokenToURL(url: string, token?: string): string { - token = token || getToken(); - if (!token) return url; + const accessToken = token || getToken(); + if (!accessToken) return url; - return addQueryToPath(url, { access_token: token }); + return addQueryToPath(url, { access_token: accessToken }); } diff --git a/app/src/app.vue b/app/src/app.vue index 18e2b723d3..93a5ec8f56 100644 --- a/app/src/app.vue +++ b/app/src/app.vue @@ -22,26 +22,22 @@ + + diff --git a/app/src/components/v-info/v-info.vue b/app/src/components/v-info/v-info.vue index cd269a0fa5..f4ab85a386 100644 --- a/app/src/components/v-info/v-info.vue +++ b/app/src/components/v-info/v-info.vue @@ -79,6 +79,7 @@ export default defineComponent({ .content { max-width: 300px; color: var(--foreground-subdued); + line-height: 22px; &:not(:last-child) { margin-bottom: 24px; diff --git a/app/src/components/v-input/v-input.vue b/app/src/components/v-input/v-input.vue index 1485b9fe32..8fdffa9f54 100644 --- a/app/src/components/v-input/v-input.vue +++ b/app/src/components/v-input/v-input.vue @@ -20,7 +20,7 @@ :max="max" :step="step" :disabled="disabled" - :value="modelValue" + :value="modelValue === null ? '' : String(modelValue)" v-on="listeners" /> @@ -152,7 +152,7 @@ export default defineComponent({ keydown: processValue, blur: (e: Event) => { trimIfEnabled(); - attrs?.onBlur?.(e); + if (typeof attrs.onBlur === 'function') attrs.onBlur(e); }, focus: (e: PointerEvent) => emit('focus', e), })); diff --git a/app/src/components/v-list/v-list-item-content.vue b/app/src/components/v-list/v-list-item-content.vue index 9fdc682ce2..effe853cef 100644 --- a/app/src/components/v-list/v-list-item-content.vue +++ b/app/src/components/v-list/v-list-item-content.vue @@ -1,6 +1,6 @@ diff --git a/app/src/components/v-list/v-list-item-icon.vue b/app/src/components/v-list/v-list-item-icon.vue index c6297ae7ec..c834194e97 100644 --- a/app/src/components/v-list/v-list-item-icon.vue +++ b/app/src/components/v-list/v-list-item-icon.vue @@ -1,6 +1,6 @@ diff --git a/app/src/components/v-list/v-list-item.vue b/app/src/components/v-list/v-list-item.vue index fdac527bc4..1e1ef3aad0 100644 --- a/app/src/components/v-list/v-list-item.vue +++ b/app/src/components/v-list/v-list-item.vue @@ -2,7 +2,7 @@ @@ -43,7 +43,7 @@ export default defineComponent({ }, href: { type: String, - default: null, + default: undefined, }, disabled: { type: Boolean, @@ -71,7 +71,7 @@ export default defineComponent({ }, download: { type: String, - default: null, + default: undefined, }, value: { type: [String, Number], @@ -88,7 +88,7 @@ export default defineComponent({ const { route: linkRoute, isActive, isExactActive } = useLink(props); - const component = computed(() => { + const component = computed(() => { if (props.to) return 'router-link'; if (props.href) return 'a'; return 'li'; @@ -130,11 +130,11 @@ export default defineComponent({ body { --v-list-item-padding-large: 0 8px; --v-list-item-padding: 0 8px 0 calc(8px + var(--v-list-item-indent, 0px)); - --v-list-item-margin-large: 4px 0; + --v-list-item-margin-large: 2px 0; --v-list-item-margin: 2px 0; --v-list-item-min-width: none; --v-list-item-max-width: none; - --v-list-item-min-height-large: 40px; + --v-list-item-min-height-large: 36px; --v-list-item-min-height: 32px; --v-list-item-max-height: auto; --v-list-item-border-radius: var(--border-radius); diff --git a/app/src/components/v-list/v-list.vue b/app/src/components/v-list/v-list.vue index fa840624a6..8a69b2d946 100644 --- a/app/src/components/v-list/v-list.vue +++ b/app/src/components/v-list/v-list.vue @@ -73,6 +73,7 @@ export default defineComponent({ overflow: auto; color: var(--v-list-color); line-height: 22px; + list-style: none; border-radius: var(--border-radius); } diff --git a/app/src/components/v-menu/v-menu.vue b/app/src/components/v-menu/v-menu.vue index 8a526e8909..21499158ec 100644 --- a/app/src/components/v-menu/v-menu.vue +++ b/app/src/components/v-menu/v-menu.vue @@ -4,8 +4,8 @@ ref="activator" class="v-menu-activator" :class="{ attached }" - @pointerenter="onPointerEnter" - @pointerleave="onPointerLeave" + @pointerenter.stop="onPointerEnter" + @pointerleave.stop="onPointerLeave" >
-
+
- +
diff --git a/app/src/components/v-select/select-list-item.vue b/app/src/components/v-select/select-list-item.vue index 3bfa3081e1..aea95a07e7 100644 --- a/app/src/components/v-select/select-list-item.vue +++ b/app/src/components/v-select/select-list-item.vue @@ -12,7 +12,7 @@ - {{ item.text }} + {{ item.text }} item.value === value)?.['text']; + return findValue(internalItems.value); + + function findValue(choices: Option[]): string | undefined { + let textValue: string | undefined = choices.find((item) => item.value === value)?.['text']; + + for (const choice of choices) { + if (!textValue) { + if (choice.children) { + textValue = findValue(choice.children); + } + } + } + + return textValue; + } } } }, diff --git a/app/src/components/v-tabs/v-tab-item/v-tab-item.vue b/app/src/components/v-tabs/v-tab-item/v-tab-item.vue index 2ebb146126..bab1f82666 100644 --- a/app/src/components/v-tabs/v-tab-item/v-tab-item.vue +++ b/app/src/components/v-tabs/v-tab-item/v-tab-item.vue @@ -16,7 +16,7 @@ export default defineComponent({ }, }, setup(props) { - const { active, toggle } = useGroupable({ value: props.value }); + const { active, toggle } = useGroupable({ value: props.value, group: 'v-tabs-items' }); return { active, toggle }; }, }); diff --git a/app/src/components/v-tabs/v-tabs-items/v-tabs-items.vue b/app/src/components/v-tabs/v-tabs-items/v-tabs-items.vue index 41a8d35e08..fcfc2a2ff4 100644 --- a/app/src/components/v-tabs/v-tabs-items/v-tabs-items.vue +++ b/app/src/components/v-tabs/v-tabs-items/v-tabs-items.vue @@ -1,5 +1,5 @@ diff --git a/app/src/components/v-upload/v-upload.vue b/app/src/components/v-upload/v-upload.vue index cf48d2ccc7..ec799397e5 100644 --- a/app/src/components/v-upload/v-upload.vue +++ b/app/src/components/v-upload/v-upload.vue @@ -92,6 +92,7 @@ import uploadFiles from '@/utils/upload-files'; import uploadFile from '@/utils/upload-file'; import DrawerCollection from '@/views/private/components/drawer-collection'; import api from '@/api'; +import emitter, { Events } from '@/events'; import { unexpectedError } from '@/utils/unexpected-error'; export default defineComponent({ @@ -200,7 +201,7 @@ export default defineComponent({ uploadedFile && emit('input', uploadedFile); } - } catch (err) { + } catch (err: any) { unexpectedError(err); } finally { uploading.value = false; @@ -293,6 +294,8 @@ export default defineComponent({ }, }); + emitter.emit(Events.upload); + if (props.multiple) { emit('input', [response.data.data]); } else { @@ -301,7 +304,7 @@ export default defineComponent({ activeDialog.value = null; url.value = ''; - } catch (err) { + } catch (err: any) { unexpectedError(err); } finally { loading.value = false; diff --git a/app/src/composables/groupable/groupable.ts b/app/src/composables/groupable/groupable.ts index b63e0b3739..78190b4d78 100644 --- a/app/src/composables/groupable/groupable.ts +++ b/app/src/composables/groupable/groupable.ts @@ -92,7 +92,7 @@ export function useGroupable(options?: GroupableOptions): UsableGroupable { } type GroupableParentState = { - selection?: Ref<(string | number)[]> | Ref; + selection?: Ref<(string | number)[] | undefined> | Ref; onSelectionChange?: (newSelectionValues: readonly (string | number)[]) => void; }; diff --git a/app/src/composables/use-field-tree/use-field-tree.ts b/app/src/composables/use-field-tree/use-field-tree.ts index 6743ff1a83..e076929856 100644 --- a/app/src/composables/use-field-tree/use-field-tree.ts +++ b/app/src/composables/use-field-tree/use-field-tree.ts @@ -3,7 +3,7 @@ import { Relation } from '@/types'; import { Field } from '@directus/shared/types'; import { getRelationType } from '@/utils/get-relation-type'; import { cloneDeep, orderBy } from 'lodash'; -import { computed, Ref, ComputedRef } from 'vue'; +import { Ref, ref, watch } from 'vue'; type FieldOption = { name: string; field: string; key: string; children?: FieldOption[]; group?: string }; @@ -14,22 +14,31 @@ export default function useFieldTree( inject?: Ref<{ fields: Field[]; relations: Relation[] } | null>, filter: (field: Field) => boolean = () => true, depth = 3 -): { tree: ComputedRef } { +): { tree: Ref } { const fieldsStore = useFieldsStore(); const collectionsStore = useCollectionsStore(); const relationsStore = useRelationsStore(); - const tree = computed(() => { - if (!collection.value) return []; - return parseLevel(collection.value, null); - }); + const tree = ref([]); + + watch( + collection, + (newCollection) => { + if (!newCollection) { + tree.value = []; + return; + } + tree.value = parseLevel(newCollection, null); + }, + { immediate: true } + ); return { tree }; function parseLevel(collection: string, parentPath: string | null, level = 0) { const fieldsInLevel = orderBy( [ - ...cloneDeep(fieldsStore.getFieldsForCollectionAlphabetical(collection)), + ...cloneDeep(fieldsStore.getFieldsForCollection(collection)), ...(inject?.value?.fields.filter((field) => field.collection === collection) || []), ] .filter((field: Field) => { diff --git a/app/src/composables/use-folders.ts b/app/src/composables/use-folders.ts index 1cd82c97ba..e52fb88fc1 100644 --- a/app/src/composables/use-folders.ts +++ b/app/src/composables/use-folders.ts @@ -61,7 +61,7 @@ export default function useFolders(): UsableFolders { folders.value = response.data.data; nestedFolders.value = nestFolders(response.data.data); - } catch (err) { + } catch (err: any) { error.value = err; } finally { loading.value = false; diff --git a/app/src/composables/use-item/use-item.ts b/app/src/composables/use-item/use-item.ts index cbfca15db3..549213d2c8 100644 --- a/app/src/composables/use-item/use-item.ts +++ b/app/src/composables/use-item/use-item.ts @@ -99,7 +99,7 @@ export function useItem(collection: Ref, primaryKey: Ref, primaryKey: Ref VALIDATION_TYPES.includes(err?.extensions?.code)) @@ -203,7 +203,7 @@ export function useItem(collection: Ref, primaryKey: Ref err?.extensions?.code === 'FAILED_VALIDATION') @@ -253,7 +253,7 @@ export function useItem(collection: Ref, primaryKey: Ref, primaryKey: Ref, query: Query, fetchOnIn } getItemCount(); - } catch (err) { + } catch (err: any) { error.value = err; } finally { clearTimeout(loadingTimeout); diff --git a/app/src/composables/use-layout.ts b/app/src/composables/use-layout.ts index d8338dc34b..eb1ca36c0b 100644 --- a/app/src/composables/use-layout.ts +++ b/app/src/composables/use-layout.ts @@ -1,26 +1,95 @@ import { getLayouts } from '@/layouts'; -import { computed, reactive, provide, Ref, UnwrapRef } from 'vue'; -import { LayoutProps, LayoutState } from '@directus/shared/types'; -import { LAYOUT_SYMBOL } from '@directus/shared/constants'; +import { computed, reactive, toRefs, defineComponent, Ref, PropType, Component, ComputedRef } from 'vue'; +import { AppFilter, Item, LayoutConfig } from '@directus/shared/types'; + +const NAME_SUFFIX = 'wrapper'; +const WRITABLE_PROPS = ['selection', 'layoutOptions', 'layoutQuery', 'filters', 'searchQuery'] as const; + +type WritableProp = typeof WRITABLE_PROPS[number]; + +function isWritableProp(prop: string): prop is WritableProp { + return (WRITABLE_PROPS as readonly string[]).includes(prop); +} + +function createLayoutWrapper(layout: LayoutConfig): Component { + return defineComponent({ + name: `${layout.id}-${NAME_SUFFIX}`, + props: { + collection: { + type: String, + required: true, + }, + selection: { + type: Array as PropType, + default: () => [], + }, + layoutOptions: { + type: Object as PropType, + default: () => ({}), + }, + layoutQuery: { + type: Object as PropType, + default: () => ({}), + }, + filters: { + type: Array as PropType, + default: () => [], + }, + searchQuery: { + type: String as PropType, + default: null, + }, + selectMode: { + type: Boolean, + default: false, + }, + readonly: { + type: Boolean, + default: false, + }, + resetPreset: { + type: Function as PropType<() => Promise>, + default: null, + }, + }, + emits: WRITABLE_PROPS.map((prop) => `update:${prop}` as const), + setup(props, { emit }) { + const state: Record = reactive({ ...layout.setup(props, { emit }), ...toRefs(props) }); + + for (const key in state) { + state[`onUpdate:${key}`] = (value: unknown) => { + if (isWritableProp(key)) { + emit(`update:${key}`, value); + } else if (!Object.keys(props).includes(key)) { + state[key] = value; + } + }; + } + + return { state }; + }, + render(ctx: any) { + return ctx.$slots.default !== undefined ? ctx.$slots.default({ layoutState: ctx.state }) : null; + }, + }); +} export function useLayout( - layoutName: Ref, - props: LayoutProps -): Ref, Options, Query>>> { + layoutId: Ref +): { layoutWrapper: ComputedRef } { const { layouts } = getLayouts(); - const setupLayouts: Record> = layouts.value.reduce( - (acc, { id, setup }) => ({ ...acc, [id]: setup(props) }), - {} - ); + const layoutWrappers = computed(() => layouts.value.map((layout) => createLayoutWrapper(layout))); - const layoutState = computed(() => { - const setupResult = setupLayouts[layoutName.value]; + const layoutWrapper = computed(() => { + const layout = layoutWrappers.value.find((layout) => layout.name === `${layoutId.value}-${NAME_SUFFIX}`); - return reactive, Options, Query>>({ ...setupResult, props }); + if (layout === undefined) { + return layoutWrappers.value.find((layout) => layout.name === `tabular-${NAME_SUFFIX}`)!; + } + + return layout; }); - provide(LAYOUT_SYMBOL, layoutState); - - return layoutState; + return { layoutWrapper }; } diff --git a/app/src/composables/use-system.ts b/app/src/composables/use-system.ts new file mode 100644 index 0000000000..6d3632938b --- /dev/null +++ b/app/src/composables/use-system.ts @@ -0,0 +1,9 @@ +import { provide } from 'vue'; +import api from '@/api'; +import * as stores from '@/stores'; +import { API_INJECT, STORES_INJECT } from '@directus/shared/constants'; + +export default function useSystem(): void { + provide(STORES_INJECT, stores); + provide(API_INJECT, api); +} diff --git a/app/src/composables/use-template-data.ts b/app/src/composables/use-template-data.ts index 417e0a80bc..3c4b1b439c 100644 --- a/app/src/composables/use-template-data.ts +++ b/app/src/composables/use-template-data.ts @@ -43,7 +43,7 @@ export default function useTemplateData( }); templateData.value = result.data.data; - } catch (err) { + } catch (err: any) { error.value = err; } finally { loading.value = false; diff --git a/app/src/displays/formatted-json-value/formatted-json-value.vue b/app/src/displays/formatted-json-value/formatted-json-value.vue index 1ba7695785..63e557468d 100644 --- a/app/src/displays/formatted-json-value/formatted-json-value.vue +++ b/app/src/displays/formatted-json-value/formatted-json-value.vue @@ -25,13 +25,13 @@ @@ -121,4 +161,9 @@ export default defineComponent({ .header-icon { margin-right: 12px !important; } + +.warning { + margin-left: 8px; + color: var(--danger); +} diff --git a/app/src/interfaces/group-raw/group-raw.vue b/app/src/interfaces/group-raw/group-raw.vue index ad779f545b..6899c02b8c 100644 --- a/app/src/interfaces/group-raw/group-raw.vue +++ b/app/src/interfaces/group-raw/group-raw.vue @@ -14,9 +14,8 @@ diff --git a/app/src/interfaces/register.ts b/app/src/interfaces/register.ts index d5a6d46ae0..1fa7bc9106 100644 --- a/app/src/interfaces/register.ts +++ b/app/src/interfaces/register.ts @@ -16,7 +16,7 @@ export async function registerInterfaces(app: App): Promise { : await import(/* @vite-ignore */ `${getRootPath()}extensions/interfaces/index.js`); interfaces.push(...customInterfaces.default); - } catch (err) { + } catch (err: any) { // eslint-disable-next-line no-console console.warn(`Couldn't load custom interfaces`); // eslint-disable-next-line no-console diff --git a/app/src/interfaces/select-dropdown-m2o/options.vue b/app/src/interfaces/select-dropdown-m2o/options.vue index 0f62292f6f..7208023f52 100644 --- a/app/src/interfaces/select-dropdown-m2o/options.vue +++ b/app/src/interfaces/select-dropdown-m2o/options.vue @@ -31,7 +31,7 @@ export default defineComponent({ default: () => [], }, value: { - type: Object as PropType, + type: Object as PropType>, default: null, }, }, diff --git a/app/src/interfaces/select-dropdown-m2o/select-dropdown-m2o.vue b/app/src/interfaces/select-dropdown-m2o/select-dropdown-m2o.vue index 87a022b310..e6107f0a0e 100644 --- a/app/src/interfaces/select-dropdown-m2o/select-dropdown-m2o.vue +++ b/app/src/interfaces/select-dropdown-m2o/select-dropdown-m2o.vue @@ -211,13 +211,14 @@ export default defineComponent({ const currentPrimaryKey = computed(() => { if (!currentItem.value) return '+'; if (!props.value) return '+'; + if (!relatedPrimaryKeyField.value) return '+'; if (typeof props.value === 'number' || typeof props.value === 'string') { - return props.value; + return props.value!; } - if (typeof props.value === 'object' && relatedPrimaryKeyField.value.field in props.value) { - return props.value[relatedPrimaryKeyField.value.field]; + if (typeof props.value === 'object' && relatedPrimaryKeyField.value.field in (props.value ?? {})) { + return props.value?.[relatedPrimaryKeyField.value.field] ?? '+'; } return '+'; @@ -226,11 +227,14 @@ export default defineComponent({ return { setCurrent, currentItem, loading, currentPrimaryKey }; function setCurrent(item: Record) { + if (!relatedPrimaryKeyField.value) return; currentItem.value = item; emit('input', item[relatedPrimaryKeyField.value.field]); } async function fetchCurrent() { + if (!relatedPrimaryKeyField.value || !relatedCollection.value) return; + loading.value = true; const fields = requiredFields.value || []; @@ -242,7 +246,7 @@ export default defineComponent({ try { const endpoint = relatedCollection.value.collection.startsWith('directus_') ? `/${relatedCollection.value.collection.substring(9)}/${props.value}` - : `/items/${relatedCollection.value.collection}/${encodeURIComponent(props.value)}`; + : `/items/${relatedCollection.value.collection}/${encodeURIComponent(props.value!)}`; const response = await api.get(endpoint, { params: { @@ -251,7 +255,7 @@ export default defineComponent({ }); currentItem.value = response.data.data; - } catch (err) { + } catch (err: any) { unexpectedError(err); } finally { loading.value = false; @@ -274,6 +278,7 @@ export default defineComponent({ async function fetchItems() { if (items.value !== null) return; + if (!relatedCollection.value || !relatedPrimaryKeyField.value) return; loading.value = true; @@ -296,7 +301,7 @@ export default defineComponent({ }); items.value = response.data.data; - } catch (err) { + } catch (err: any) { unexpectedError(err); } finally { loading.value = false; @@ -304,6 +309,8 @@ export default defineComponent({ } async function fetchTotalCount() { + if (!relatedCollection.value) return; + const endpoint = relatedCollection.value.collection.startsWith('directus_') ? `/${relatedCollection.value.collection.substring(9)}` : `/items/${relatedCollection.value.collection}`; @@ -325,10 +332,15 @@ export default defineComponent({ }); const relatedCollection = computed(() => { - return collectionsStore.getCollection(relation.value.related_collection!)!; + if (!relation.value?.related_collection) return null; + return collectionsStore.getCollection(relation.value.related_collection)!; }); - const { primaryKeyField: relatedPrimaryKeyField } = useCollection(relatedCollection.value.collection); + const relatedPrimaryKeyField = computed(() => { + if (!relatedCollection.value?.collection) return null; + const { primaryKeyField } = useCollection(relatedCollection.value?.collection); + return primaryKeyField.value; + }); return { relation, relatedCollection, relatedPrimaryKeyField }; } @@ -350,11 +362,12 @@ export default defineComponent({ function usePreview() { const displayTemplate = computed(() => { if (props.template !== null) return props.template; - return collectionInfo.value?.meta?.display_template || `{{ ${relatedPrimaryKeyField.value.field} }}`; + return collectionInfo.value?.meta?.display_template || `{{ ${relatedPrimaryKeyField?.value?.field || ''} }}`; }); const requiredFields = computed(() => { - if (!displayTemplate.value) return null; + if (!displayTemplate.value || !relatedCollection.value) return null; + return adjustFieldsForDisplays( getFieldsFromTemplate(displayTemplate.value), relatedCollection.value.collection @@ -381,13 +394,14 @@ export default defineComponent({ const selection = computed<(number | string)[]>(() => { if (!props.value) return []; + if (!relatedPrimaryKeyField.value) return []; - if (typeof props.value === 'object' && relatedPrimaryKeyField.value.field in props.value) { - return [props.value[relatedPrimaryKeyField.value.field]]; + if (typeof props.value === 'object' && relatedPrimaryKeyField.value.field in (props.value ?? {})) { + return [props.value![relatedPrimaryKeyField.value.field]]; } if (typeof props.value === 'string' || typeof props.value === 'number') { - return [props.value]; + return [props.value!]; } return []; @@ -418,6 +432,8 @@ export default defineComponent({ return { edits, stageEdits }; function stageEdits(newEdits: Record) { + if (!relatedPrimaryKeyField.value) return; + // Make sure we stage the primary key if it exists. This is needed to have the API // update the existing item instead of create a new one if (currentPrimaryKey.value && currentPrimaryKey.value !== '+') { diff --git a/app/src/interfaces/select-multiple-checkbox-tree/select-multiple-checkbox-tree.vue b/app/src/interfaces/select-multiple-checkbox-tree/select-multiple-checkbox-tree.vue index 5dea6d826e..2b2860341b 100644 --- a/app/src/interfaces/select-multiple-checkbox-tree/select-multiple-checkbox-tree.vue +++ b/app/src/interfaces/select-multiple-checkbox-tree/select-multiple-checkbox-tree.vue @@ -88,6 +88,7 @@ export default defineComponent({ .select-multiple-checkbox-tree { max-height: var(--input-height-max); overflow: auto; + background-color: var(--background-page); border: var(--border-width) solid var(--border-normal); border-radius: var(--border-radius); } diff --git a/app/src/interfaces/translations/options.vue b/app/src/interfaces/translations/options.vue index f2baa9f1cf..34fd34aa38 100644 --- a/app/src/interfaces/translations/options.vue +++ b/app/src/interfaces/translations/options.vue @@ -53,7 +53,7 @@ export default defineComponent({ default: () => [], }, value: { - type: Object as PropType, + type: Object as PropType>, default: null, }, }, diff --git a/app/src/interfaces/translations/translations.vue b/app/src/interfaces/translations/translations.vue index ae919dccc1..1019e33e37 100644 --- a/app/src/interfaces/translations/translations.vue +++ b/app/src/interfaces/translations/translations.vue @@ -136,7 +136,7 @@ export default defineComponent({ return translationsRelation.value.collection; }); - const translationsPrimaryKeyField = computed(() => { + const translationsPrimaryKeyField = computed(() => { if (!translationsRelation.value) return null; return fieldsStore.getPrimaryKeyFieldForCollection(translationsRelation.value.collection).field; }); @@ -151,8 +151,8 @@ export default defineComponent({ return languagesRelation.value.related_collection; }); - const languagesPrimaryKeyField = computed(() => { - if (!languagesRelation.value) return null; + const languagesPrimaryKeyField = computed(() => { + if (!languagesRelation.value || !languagesRelation.value.related_collection) return null; return fieldsStore.getPrimaryKeyFieldForCollection(languagesRelation.value.related_collection).field; }); @@ -195,7 +195,7 @@ export default defineComponent({ return { languages, loading, error, template }; async function fetchLanguages() { - if (!languagesCollection.value) return; + if (!languagesCollection.value || !languagesPrimaryKeyField.value) return; const fields = getFieldsFromTemplate(template.value); @@ -208,7 +208,7 @@ export default defineComponent({ try { const response = await api.get(`/items/${languagesCollection.value}`, { params: { fields, limit: -1 } }); languages.value = response.data.data; - } catch (err) { + } catch (err: any) { unexpectedError(err); } finally { loading.value = false; @@ -226,10 +226,12 @@ export default defineComponent({ const edits = ref>(); const existingPrimaryKeys = computed(() => { + const pkField = translationsPrimaryKeyField.value; + if (!pkField) return []; return (props.value || []) .map((value) => { if (typeof value === 'string' || typeof value === 'number') return value; - return value[translationsPrimaryKeyField.value]; + return value[pkField]; }) .filter((key) => key); }); @@ -239,7 +241,7 @@ export default defineComponent({ return { startEditing, editing, edits, stageEdits, cancelEdit }; function startEditing(language: string | number) { - if (!translationsLanguageField.value) return; + if (!translationsLanguageField.value || !translationsPrimaryKeyField.value) return; edits.value = { [translationsLanguageField.value]: language, @@ -275,12 +277,15 @@ export default defineComponent({ async function fetchKeyMap() { if (!props.value) return; if (keyMap.value) return; + if (!existingPrimaryKeys.value?.length) return; + const pkField = translationsPrimaryKeyField.value; + if (!pkField) return; const collection = translationsRelation.value?.collection; if (!collection) return; - const fields = [translationsPrimaryKeyField.value, translationsLanguageField.value]; + const fields = [pkField, translationsLanguageField.value]; loading.value = true; @@ -289,15 +294,16 @@ export default defineComponent({ params: { fields, filter: { - [translationsPrimaryKeyField.value]: { + [pkField]: { _in: existingPrimaryKeys.value, }, }, + limit: -1, }, }); keyMap.value = response.data.data; - } catch (err) { + } catch (err: any) { error.value = err; } finally { loading.value = false; @@ -306,6 +312,8 @@ export default defineComponent({ function stageEdits(edits: any) { if (!translationsLanguageField.value) return; + const pkField = translationsPrimaryKeyField.value; + if (!pkField) return; const editedLanguage = edits[translationsLanguageField.value]; @@ -337,7 +345,7 @@ export default defineComponent({ if (typeof val === 'string' || typeof val === 'number') { if (val === editing.value) return edits; } else { - if (val[translationsPrimaryKeyField.value] === editing.value) return edits; + if (val[pkField] === editing.value) return edits; } return val; @@ -399,29 +407,31 @@ export default defineComponent({ _eq: props.primaryKey, }, }, + limit: -1, }, }); previewItems.value = languages.value.map((language) => { + const pkField = languagesPrimaryKeyField.value; + if (!pkField) return; + const existingEdit = props.value && Array.isArray(props.value) ? (props.value.find( (edit) => isPlainObject(edit) && - (edit as Record)[languagesRelation.value!.field] === - language[languagesPrimaryKeyField.value] + (edit as Record)[languagesRelation.value!.field] === language[pkField] ) as Record) : {}; return { ...(existing.data.data?.find( - (item: Record) => - item[languagesRelation.value!.field] === language[languagesPrimaryKeyField.value] + (item: Record) => item[languagesRelation.value!.field] === language[pkField] ) ?? {}), ...existingEdit, }; }); - } catch (err) { + } catch (err: any) { error.value = err; previewItems.value = []; } finally { diff --git a/app/src/lang/set-language.ts b/app/src/lang/set-language.ts index ca40d6ccf5..a5f1e6c3da 100644 --- a/app/src/lang/set-language.ts +++ b/app/src/lang/set-language.ts @@ -17,24 +17,25 @@ export async function setLanguage(lang: Language): Promise { const fieldsStore = useFieldsStore(); if (Object.keys(availableLanguages).includes(lang) === false) { - return false; - } - - if (loadedLanguages.includes(lang) === false) { - try { - const translations = await import(`./translations/${lang}.yaml`); - i18n.global.mergeLocaleMessage(lang, translations); - loadedLanguages.push(lang); - } catch (err) { - // eslint-disable-next-line no-console - console.warn(err); + // eslint-disable-next-line no-console + console.warn(`"${lang}" is not an available language in the Directus app.`); + } else { + if (loadedLanguages.includes(lang) === false) { + try { + const translations = await import(`./translations/${lang}.yaml`); + i18n.global.mergeLocaleMessage(lang, translations); + loadedLanguages.push(lang); + } catch (err: any) { + // eslint-disable-next-line no-console + console.warn(err); + } } + + i18n.global.locale.value = lang; + + (document.querySelector('html') as HTMLElement).setAttribute('lang', lang); } - i18n.global.locale.value = lang; - - (document.querySelector('html') as HTMLElement).setAttribute('lang', lang); - modules.value = translate(modulesRaw.value); layouts.value = translate(layoutsRaw.value); interfaces.value = translate(interfacesRaw.value); diff --git a/app/src/lang/translations/af-ZA.yaml b/app/src/lang/translations/af-ZA.yaml index 7ad8716729..42a00bb67b 100644 --- a/app/src/lang/translations/af-ZA.yaml +++ b/app/src/lang/translations/af-ZA.yaml @@ -82,6 +82,13 @@ fields: description: Beskrywing directus_webhooks: status: Status +field_options: + directus_activity: + create: Skep + directus_collections: + language: Taal + directus_webhooks: + actions_create: Skep comment: Kommentaar description: Beskrywing email: E-pos diff --git a/app/src/lang/translations/ar-SA.yaml b/app/src/lang/translations/ar-SA.yaml index 717991791b..56619753f4 100644 --- a/app/src/lang/translations/ar-SA.yaml +++ b/app/src/lang/translations/ar-SA.yaml @@ -20,18 +20,25 @@ #'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', #'Proxy', 'Intl' edit_field: تعديل الحقل +conditions: الشروط +maps: الخرائط item_revision: التعديلات على العنصر duplicate_field: حقل مكرر half_width: نصف العرض full_width: العرض الكامل +limit: الحد group: المجموعة +and: و +or: أو fill_width: العرض الكامل field_name_translations: ترجمة اسم الحقل enter_password_to_enable_tfa: أدخل كلمة المرور الخاصة بك لتمكين المصادقة الثنائية add_field: إضافة حقل role_name: إسم الدور branch: فرع +leaf: ورقة الشجر indeterminate: غير محدد +edit_collection: تعديل المجموعة exclusive: حصري children: اطفال db_only_click_to_configure: 'فقط قاعدة البيانات: انقر لتعديل الإعدادات ' @@ -45,6 +52,7 @@ create_role: إنشاء دور أو صلاحية create_user: إضافة مستخدم create_webhook: إنشاء Webhook invite_users: دعوة مستخدمين +email_examples: "admin{'@'}example.com, user{'@'}example.com..." invite: دعوة email_already_invited: البريد الإلكتروني {email} تم دعوته مسبقاً emails: البريد الإلكتروني @@ -66,6 +74,7 @@ delete_bookmark_copy: >- logoutReason: SIGN_OUT: تسجيل الخروج SESSION_EXPIRED: انتهت الجلسة +public_label: عام public_description: التحكم بتوفر البيانات على واجهة الربط البرمجي بدون تسجيل دخول. not_allowed: غير مسموح directus_version: إصدار Directus @@ -111,6 +120,8 @@ no_access: ممنوع الدخول use_custom: تخصيص nullable: لا يمكن allow_null_value: السماح بقيمة الالتفاف +allow_multiple: السماح بالاختيار المتعدد +allow_multiple_to_be_open: السماح بالفتح المتعدد enter_value_to_replace_nulls: الرجاء إدخال قيمة جديدة لاستبدال أي ULLL حاليا داخل هذا الحقل. field_standard: قياسي field_presentation: Presentation & Aliases @@ -121,6 +132,7 @@ field_m2a: العلاقة field_o2m: العلاقة field_m2m: العلاقة field_translations: الترجمة +field_group: مجموعة الحقول item_permissions: أذونات العنصر field_permissions: أذونات العنصر field_validation: مجال المصادقة @@ -173,6 +185,7 @@ time: الوقت timestamp: الطابع الزمني uuid: UUID hash: الهاش +geometry: الأشكال الهندسية not_available_for_type: غير متوفّر لنوع الملفات هذا create_translations: إنشاء ترجمات auto_refresh: تحديث تلقائي @@ -296,8 +309,10 @@ months: november: نوفمبر december: ديسمبر drag_mode: وضع السحب +cancel_crop: إلغاء القص original: أصلي url: الرابط +import_label: إستيراد file_details: تفاصيل الملف dimensions: الأبعاد size: حجم @@ -311,6 +326,7 @@ zoom: تكبير/تصغير download: تنزيل open: فتح open_in_new_window: فتح في نافذة جديدة +foreground_color: لون الواجهة background_color: لون الخلفية upload_from_device: استبدال الملف من الجهاز choose_from_library: اختر ملف من المكتبة @@ -364,6 +380,7 @@ no_users_copy: لا يوجد مستخدمون في هذه التجزئة. webhooks_count: 'لاتوجد روابط ويب | رابط ويب | {count} روابط ويب' no_webhooks_copy: لا يوجد أسئلة بعد. all_items: كافة العناصر +any: أي csv: CSV no_collections: مجموعات create_collection: إنشاء مجموعة @@ -374,6 +391,7 @@ display_template_not_setup: خيار قالب العرض خاطئ التكوين collection_field_not_setup: خيار حقل المجموعة خاطئ التكوين select_a_collection: حدد مجموعة active: مفعل +inactive: غير نشط users: المستخدمين activity: الأنشطة webhooks: روابط الويب (Webhooks) @@ -386,6 +404,7 @@ documentation: التوثيق sidebar: الشريط الجانبي duration: المدة charset: اصطفاف تعليق افتراضي +second: ثانية file_moved: الملفات المنقولة collection_created: تم تحديث المجموعة modified_on: التعديل في @@ -417,12 +436,17 @@ errors: INVALID_OTP: كلمة مرور لمرة واحدة خاطئة INVALID_PAYLOAD: Invalid payload INVALID_QUERY: طلب غير صحيح + ITEM_LIMIT_REACHED: تم الوصول إلى الحد الأقصى للعناصر ITEM_NOT_FOUND: لم يتم العثور على العنصر ROUTE_NOT_FOUND: غير موجود + RECORD_NOT_UNIQUE: تم اكتشاف تكرار القيمة USER_SUSPENDED: المستخدم موقوف + CONTAINS_NULL_VALUES: الحقل يحتوي على قيم فارغة UNKNOWN: خطأ غير متوقع INTERNAL_SERVER_ERROR: خطأ غير متوقع NOT_NULL_VIOLATION: لا يمكن أن تكون القيمة فارغة +security: الأمان +value_hashed: تم تجزئة القيمة بشكل آمن bookmark_name: اسم الإشارة المرجعية... create_bookmark: إنشاء إشارة مرجعية edit_bookmark: تحرير الإشارة المرجعية @@ -431,6 +455,7 @@ presets: الإعدادات المسبقة unexpected_error: خطأ غير متوقع unexpected_error_copy: حدث خطأ غير متوقع. الرجاء المحاولة مرة أخرى لاحقاً. copy_details: نسخ التفاصيل +no_app_access_copy: هذا المستخدم غير مسموح له باستخدام تطبيق المدير. password_reset_sent: لقد أرسلنا لك رابط آمن لإعادة تعيين كلمة المرور الخاصة بك password_reset_successful: تم إعادة تعيين كلمة المرور بنجاح back: رجوع @@ -456,12 +481,14 @@ visible_collections: مجموعات مرئية hidden_collections: مجموعات مخفية show_hidden_collections: أظهر المجموعات المخفية hide_hidden_collections: اخفي المجموعات المخفية +unmanaged_collections: مجموعات غير مهيأة system_collections: مجموعات النظام placeholder: مكان محجوز icon_left: أيقونة اليسار icon_right: أيقونة اليمين count_other_revisions: '{count} مراجعات أخرى' font: الخط +sans_serif: Sans Serif serif: خط سيريف Serif monospace: Monospace divider: الفاصل @@ -469,6 +496,7 @@ color: لون circle: دائرة empty_item: عنصر فارغ log_in_with: 'تسجيل الدخول باستخدام {provider}' +advanced_settings: إعدادات متقدمة advanced_filter: تصفية متقدمة delete_advanced_filter: حذف التصفية change_advanced_filter_operator: تغيير المشغل @@ -481,16 +509,22 @@ operators: gte: أكبر من أو يساوي in: واحد من nin: ليس من + null: فارغ nnull: باطل contains: يحتوي على ncontains: لا يحتوي starts_with: إبدا بـ + nstarts_with: لا يبدأ ب + ends_with: ينتهي ب + nends_with: لا ينتهي ب between: بين nbetween: بين empty: فارغ nempty: ليس فارغا all: تتضمن هذه الوسوم has: تتضمن هذه الوسوم + intersects: تداخلات + nintersects: لا يتقاطع loading: جار التحميل... drop_to_upload: إسقاط إلى التحميل item: عنصر @@ -528,10 +562,12 @@ no_results_copy: ضبط أو مسح تصفية البحث لرؤية النتا clear_filters: مسح التصفية saves_automatically: يحفظ تلقائياً role: الدور +rule: قاعدة user: مستخدم no_presets: لا إعدادات مسبقة no_presets_copy: لم يتم حفظ أي إعدادات مسبقة أو إشارات مرجعية حتى الآن. no_presets_cta: إضافة إعداد مسبق +presets_only: الإعدادات المسبقة فقط create: انشاء on_create: عند الإنشاء on_update: عند التحديث @@ -539,7 +575,11 @@ read: قراءة update: تحديث select_fields: حدد حقول format_text: تنسيق النص +icon_on: تشغيل الأيقونة +icon_off: إيقاف الأيقونة +label: تسمية image_url: رابط الصورة +alt_text: النص البديل media: وسائط الإعلام width: العرض height: الارتفاع @@ -551,6 +591,7 @@ open_link_in: افتح الرابط في wysiwyg_options: alignleft: محاذاة إلى اليسار alignright: محاذاة إلى اليمين + forecolor: لون الواجهة backcolor: لون الخلفية italic: خط مائل underline: مسطر @@ -725,10 +766,38 @@ fields: name: الاسم status: حالة field_options: + directus_settings: + security_divider_title: الأمان + directus_activity: + login: تسجيل الدخول + create: انشاء + update: تحديث + delete: حذف directus_collections: track_activity_revisions: تتبع النشاط والتنقيحات only_track_activity: تتبع النشاط فقط do_not_track_anything: عدم تتبع أي شيء + collection_setup: لا توجد مجموعات الإعداد + singleton: معاملته ككائن واحد + language: لغة + archive_divider: أرشفة + divider: رتب + directus_roles: + fields: + icon_name: أيقونة + name_name: الاسم + name_placeholder: أدخل عنوانًا... + collection_list: + fields: + type_name: النوع + collections_name: مجموعات + directus_users: + status_dropdown_active: مفعل + directus_webhooks: + status_options_active: مفعل + status_options_inactive: غير نشط + actions_create: انشاء + actions_update: تحديث no_fields_in_collection: 'لا توجد حقول في "{collection}" حتى الآن' do_nothing: لا تفعل شيئا save_current_user_role: احفظ دور المستخدم الحالي @@ -926,6 +995,8 @@ displays: boolean: boolean: نعم/لا description: عرض حالة التشغيل وإيقاف التشغيل + icon_on: تشغيل الأيقونة + icon_off: إيقاف الأيقونة color_on: تشغيل اللون color_off: إيقاف اللون collection: @@ -1001,3 +1072,5 @@ layouts: calendar: تقويم start_date_field: حقل تاريخ البداية end_date_field: حقل تاريخ الإنتهاء + map: + field: الأشكال الهندسية diff --git a/app/src/lang/translations/bg-BG.yaml b/app/src/lang/translations/bg-BG.yaml index bffc7f417c..cfdc0fcd3d 100644 --- a/app/src/lang/translations/bg-BG.yaml +++ b/app/src/lang/translations/bg-BG.yaml @@ -21,10 +21,12 @@ #'Proxy', 'Intl' edit_field: Редактиране на поле conditions: Условия +maps: Географски карти item_revision: Ревизии на запис duplicate_field: Дублиране на поле half_width: Половин ширина full_width: Пълна ширина +limit: Лимит group: Групиране and: И or: Или @@ -36,6 +38,7 @@ role_name: Име на роля branch: Клон leaf: Листо indeterminate: Неопределен +edit_collection: Редактиране на колекция exclusive: Изключително children: Дъщерен db_only_click_to_configure: 'Само в базата от данни: Кликнете за конфигурация ' @@ -182,6 +185,7 @@ time: Час timestamp: Времеви печат uuid: UUID hash: Хеш +geometry: Геометрия not_available_for_type: Не е налично за типа create_translations: Създаване на преводи auto_refresh: Автоматично опресняване @@ -380,6 +384,7 @@ no_users_copy: Все още няма потребители в тази рол webhooks_count: "Няма уеб-куки | 1 уеб-кука | {count} уеб-куки\n" no_webhooks_copy: Все още няма уеб-куки. all_items: Всички записи +any: Който и да е csv: CSV no_collections: Няма колекции create_collection: Създаване на колекция @@ -497,6 +502,7 @@ color: Цвят circle: Кръг empty_item: Празен запис log_in_with: 'Вход чрез {provider}' +advanced_settings: Разширени настройки advanced_filter: Разширен филтър delete_advanced_filter: Изтриване на филтър change_advanced_filter_operator: Промяна на оператор @@ -523,6 +529,10 @@ operators: nempty: Не е празно all: Съдържа следните ключове has: Съдържа някои от + intersects: Пресичане + nintersects: Няма пресичане + intersects_bbox: Прилежащи към границите + nintersects_bbox: Не са прилежащи към границите loading: Зареждане... drop_to_upload: Пускане за качване item: Запис @@ -787,7 +797,7 @@ fields: admin_options: Административни настройки status: Статус status_draft: Чернова - status_invited: Неактивен + status_invited: Поканен status_active: Активен status_suspended: Замразен status_archived: Архивиран @@ -797,6 +807,15 @@ fields: last_page: Последна страница last_access: Последно посещение directus_settings: + jpg: JPEG + png: PNG + webP: WebP + tiff: Tiff + basemaps_raster: Растер + basemaps_tile: Растерен TileJSON + basemaps_style: Стил на географската карта + transforms_note: Име на метода и параметри за Sharp. Повече информация на https://sharp.pixelplumbing.com/api-constructor for more information. + additional_transforms: Допълнителни трансформации project_name: Име на проекта project_url: URL на проекта project_color: Цвят на проекта @@ -841,10 +860,96 @@ fields: triggers: Тригери actions: Действия field_options: + directus_settings: + project_name_placeholder: Моят проект... + project_logo_note: Фонт за логото и вход-а + public_note_placeholder: Кратко публично съобщение, markdown форматирането е позволено... + security_divider_title: Сигурност + auth_password_policy: + none_text: Без - не се препоръчва + weak_text: Слаба - минимум от 8 символа + strong_text: Силна - главни, малки, числа и специални символи + storage_asset_presets: + fit: + contain_text: Напасване (запазване на съотношението) + cover_text: Обложка (точни размери) + fit_text: Напасване отвътре + outside_text: Напасване отвън + additional_transforms: Допълнителни трансформации + transforms_note: Име на метода и параметри за Sharp. Повече информация на https://sharp.pixelplumbing.com/api-constructor for more information. + basemaps_raster: Растер + basemaps_tile: Растерен TileJSON + basemaps_style: Стил на географската карта + files_divider_title: Файлове и картинки + overrides_divider_title: Отмяна + directus_activity: + login: Вход + create: Създаване + update: Обновяване + delete: Изтриване directus_collections: track_activity_revisions: Проследяване на активността и ревизиите only_track_activity: Проследяване само на активността do_not_track_anything: Без следене + collection_setup: Настройки на колекция + note_placeholder: Описание на колекцията... + hidden_label: Скриване в приложението + singleton: Използване като единствен запис + language: Език + translation: Въвеждане на превод... + archive_divider: Архивиране + archive_field: Избор на поле... + archive_value: Задаване на стойност при архивиране... + unarchive_value: Задаване на стойност при разархивиране... + divider: Сортиране + sort_field: Избор на поле... + directus_files: + title: Уникално заглавие... + description: Незадължително описание... + location: Незадължително местоположение... + storage_divider: Име на файл + filename_disk: Име при запис... + filename_download: Име при изтегляне... + directus_roles: + name: Уникално име за ролята... + description: Описание на ролята... + ip_access: Позволяване на определни IP адреси, оставяне на празно за всички... + fields: + icon_name: Икона + name_name: Име + name_placeholder: Въвеждане на заглавие... + link_name: Връзка + link_placeholder: Релативен или абсолютен URL... + collection_list: + group_name_addLabel: Добавяне на група... + fields: + group_name: Име на група + group_placeholder: Именуване на групата... + type_name: Тип + choices_always: Винаги отворена + choices_start_open: Отворена в началото + collections_name: Колекции + directus_users: + preferences_divider: Потребителски настройки + dropdown_auto: Автоматично (според системата) + dropdown_light: Светъл режим + dropdown_dark: Тъмен режим + admin_divider: Административни настройки + status_dropdown_draft: Чернова + status_dropdown_invited: Поканен + status_dropdown_active: Активен + status_dropdown_suspended: Замразен + status_dropdown_archived: Архивиран + token: Въвеждане на сигурен токен... + directus_webhooks: + status_options_active: Активен + status_options_inactive: Неактивен + data_label: Изпращане на данните + triggers_divider: Тригери + actions_create: Създаване + actions_update: Обновяване + actions_delete: Изтриване + actions_login: Вход no_fields_in_collection: 'Все още няма полета в {collection}' do_nothing: Без действие generate_and_save_uuid: Генериране и запазване на UUID @@ -1052,7 +1157,20 @@ interfaces: imageToken: Токън за изображенията imageToken_label: Какъв (статичен) токън да се добави, към адресите на изображенията map: + map: Географска карта + description: Избор на местоположение по картата zoom: Мащабиране + geometry_type: Тип + geometry_format: Формат + default_view: Изглед по подразбиране + invalid_options: Невалидни опции + invalid_format: Невалиден формат ({format}) + unexpected_geometry: Очаква се {expected}, но има {got}. + fit_bounds: Напасване на изгледа към данните + geojson: GeoJSON + lnglat: Географски дължина/ширина + wkt: WKT + wkb: WKB presentation-notice: notice: Пояснение description: Показване на кратко пояснение @@ -1261,3 +1379,17 @@ layouts: calendar: Календар start_date_field: Начална дата end_date_field: Крайна дата + map: + map: Географска карта + basemap: Основна карта + layers: Слоеве + edit_custom_layers: Редактиране на слоевете + cluster_options: Настройки за групиране + cluster: Активиране на групирането + cluster_radius: Радиус за групиране + cluster_minpoints: Минимален размер при групиране + cluster_maxzoom: Максимално увеличение при групиране + field: Геометрия + invalid_geometry: Невалидна геометрия + auto_location_filter: Филтриране на данните спрямо границите + search_this_area: Търсене в тази зона diff --git a/app/src/lang/translations/br-FR.yaml b/app/src/lang/translations/br-FR.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/app/src/lang/translations/br-FR.yaml @@ -0,0 +1 @@ +--- diff --git a/app/src/lang/translations/ca-ES.yaml b/app/src/lang/translations/ca-ES.yaml index 23d37537e9..64554efc5d 100644 --- a/app/src/lang/translations/ca-ES.yaml +++ b/app/src/lang/translations/ca-ES.yaml @@ -1,29 +1,69 @@ --- +## Be aware: +#Due to the way this is imported, JavaScript reserved words, including "delete", "private", +#"void", etc are stripped out. See +#https://github.com/rollup/plugins/blob/8748b8cd3bbab3c5ac6190556930219f19060e63/packages/pluginutils/src/makeLegalIdentifier.ts#L4 +#and +#https://github.com/rollup/plugins/blob/8748b8cd3bbab3c5ac6190556930219f19060e63/packages/yaml/src/index.js#L45 +#Illegal words: +#'break', 'case', 'class', 'catch', 'const', 'continue', 'debugger', 'default', 'delete', 'do', +#'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', +#'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', +#'while', 'with', 'yield', 'enum', 'await', 'implements', 'package', 'protected', 'static', +#'interface', 'private', 'public', 'arguments', 'Infinity', 'NaN', 'undefined', 'null', 'true', +#'false', 'eval', 'uneval', 'isFinite', 'isNaN', 'parseFloat', 'parseInt', 'decodeURI', +#'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'escape', 'unescape', 'Object', +#'Function', 'Boolean', 'Symbol', 'Error', 'EvalError', 'InternalError', 'RangeError', +#'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'Number', 'Math', 'Date', 'String', +#'RegExp', 'Array', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', +#'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array', 'Map', 'Set', 'WeakMap', 'WeakSet', +#'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', +#'Proxy', 'Intl' +edit_field: Edita el camp +conditions: Condicions +maps: Mapes +item_revision: Revisió de l'element duplicate_field: Camp duplicat half_width: Mitja amplada full_width: Amplada Completa +limit: Límit +group: Grup +and: I +or: O fill_width: Amplada de l'emplenat field_name_translations: Traducció de noms de camps enter_password_to_enable_tfa: Introdueix la contrasenya per activar l'autenticació de dos factors add_field: Afegeix camp role_name: Nom del Rol +branch: Branca +leaf: Fulla +indeterminate: Indeterminat +exclusive: Exclusiu +children: Fills db_only_click_to_configure: 'Base de dades només: Feu clic per configurar ' show_archived_items: Mostra els elements arxivats +edited: Valor editat required: Requerit +required_for_app_access: Requerit per l'accés a l'aplicació requires_value: Requereix valor -create_preset: Crear un Predefinit +create_preset: Crear un predefinit create_role: Crea un rol create_user: Crear usuari create_webhook: Crear Webhook invite_users: Convidar usuaris +email_examples: "admin{'@'}example.com, user{'@'}example.com..." invite: Convida +email_already_invited: El correu "{email}" ja s'ha convidat emails: Correus electrònics connection_excellent: Connexió excel·lent connection_good: Connexió bona connection_fair: Connexió justa connection_poor: Connexió pobra +primary: Primari rename_folder: Renombra carpeta delete_folder: Suprimeix carpeta +prefix: Prefix +suffix: Sufix reset_bookmark: Reinicia marcador rename_bookmark: Renombra marcador update_bookmark: Actualitza marcador @@ -33,6 +73,7 @@ delete_bookmark_copy: >- logoutReason: SIGN_OUT: S'ha desconnectat SESSION_EXPIRED: Sessió caducada +public_label: Públic public_description: Controla quines dades API estan disponibles sense autenticar. not_allowed: No permès directus_version: Versió de Directus @@ -46,69 +87,647 @@ archive: Arxiu archive_confirm: Estàs segur de que vols arxivar aquest element? archive_confirm_count: >- No hi ha elements seleccionats | Segur que voleu arxivar aquest element? | Segur que voleu arxivar aquests {count} elements? -unarchive: Desarxivar -unarchive_confirm: Segur que voleu desarxivar aquest element? +reset_system_permissions_to: 'Restableix els permisos del sistema a:' +reset_system_permissions_copy: Aquesta acció rescriurà qualsevol permís personalitzat que hagis aplicat a les col·leccions del sistema. Estàs sgur? +the_following_are_minimum_permissions: Els següents són els mínims permisos requerits quan l'"Accés a l'aplicació" està habilitat. Pots estendre els permisos a partir d'aquí, però no els de més abaix. +app_access_minimum: Accés a l'aplicació mínim +recommended_defaults: Predeterminats recomanats +unarchive: Desarxiva +unarchive_confirm: Segur que vols desarxivar aquest element? +nested_files_folders_will_be_moved: Els fitxers i carpetes anidats es mouran un nivell amunt. +unknown_validation_errors: 'Hi ha errors de validació pels següents camps amagats:' +validationError: + eq: El valor ha de ser {valid} + neq: El valor no pot ser {invalid} + in: El valor ha de ser un dels {valid} + nin: El valor no pot ser un de {invalid} + contains: El valor ha de contenir {substring} + ncontains: El valor no pot contenir {substring} + gt: El valor ha de ser major que {valid} + gte: El valor ha de ser major o igual a {valid} + lt: El valor ha de ser menor que {valid} + lte: El valor ha de ser menor o igual a {valid} + empty: El valor ha de ser buit + nempty: El valor no pot ser buit + null: El valor ha de ser null + nnull: El valor no pot ser null + required: El valor es requerit + unique: El valor ha de ser únic + regex: El valor no té el format correcte +all_access: Accés complet +no_access: Sense accés +use_custom: Personalitzat +nullable: Nullable +allow_null_value: Permet valors nuls +allow_multiple: Permet múltiples +allow_multiple_to_be_open: Permet que s'obrin múltiples +enter_value_to_replace_nulls: Si us plau, introdueix un nou valor per reemplaçar qualsevol NULL que hi hagi en aquest camp. +field_standard: Estàndard +field_presentation: Presentació i àlies +field_file: Un fitxer +field_files: Multiples fitxers +field_m2o: Relació M2O +field_m2a: Relació M2A +field_o2m: Relació O2M +field_m2m: Relació M2M +field_translations: Traduccions +field_group: Agrupació de camps +item_permissions: Permisos de l'element +field_permissions: Permisos del camp +field_validation: Validació del camp +field_presets: Predefinits del camp +permissions_for_role: 'Elements {role} poden {action}.' +fields_for_role: 'Camps del {role} poden {action}.' +validation_for_role: 'El camp {action} determina que el rol {role} ha de obeir.' +presets_for_role: 'Valor de camp predeterminat per al rol {role}.' +presentation_and_aliases: Presentació i àlies +revision_post_update: Així és com queda l'element després d'actualitzar... +changes_made: Aquests són els canvis específics que s'han fet... +no_relational_data: Tingues present que això no inclou dades relacionals. +hide_field_on_detail: Amaga el camp en el detall +show_field_on_detail: Mostra el camp en el detall +delete_field: Elimina el camp +fields_and_layout: Camps i disposició +field_create_success: 'Camp creat: "{field}"' +field_update_success: 'Camp actualitzat: "{field}"' +duplicate_where_to: A on t'agradaria duplicar aquest camp? language: Idioma +global: Global +admins_have_all_permissions: Els administradors tenen tots els permisos +camera: Càmera +exposure: Exposició +shutter: Obturador +iso: ISO +focal_length: Distancia focal +schema_setup_key: Aquest nom de columna del camp i clau de l'API create_field: Crea un camp +creating_new_field: 'Nou camp ({collection})' +field_in_collection: '{field} ({collection})' +reset_page_preferences: Restableix la pàgina de preferències +hidden_field: Camp ocult +hidden_on_detail: Amagat al detall +disabled_editing_value: Deshabilita l'edició del valor +key: Clau +alias: Àlies +bigInteger: Big Integer +boolean: Boolean date: Data +datetime: DateTime decimal: Decimal +float: Float +integer: Integer +json: JSON +xml: XML +string: String text: Text +time: Time +timestamp: Timestamp +uuid: UUID +hash: Hash +geometry: Geometria +not_available_for_type: No disponible per aquest Tipus +create_translations: Crea traduccions +auto_refresh: Refrescament automàtic +refresh_interval: Interval de refresc +no_refresh: No refresquis +refresh_interval_seconds: Refresca immediatament | Cada Segon | Cada {seconds} segons +refresh_interval_minutes: Cada minut | Cada {minutes} minuts auto_generate: Generar automàticament +this_will_auto_setup_fields_relations: Això establirà automàticament tots els camps requerits i les relacions. +click_here: Fes clic aquí +to_manually_setup_translations: per establir les traduccions. +click_to_manage_translated_fields: >- + Encara no s'han traduït els camps. Fes clic aquí per crear-los. | Hi ha un camp traduït. Fes clic per gestionar-lo. | Hi ha {count} camps traduïts. Fes clic aquí per gestionar-los. +fields_group: Agrupació de camps +no_collections_found: No s'han trobat col·leccions. +new_data_alert: 'Es crearà el següent en el teu Model de dades:' +search_collection: Cerca la col·lecció... +new_field: 'Nou camp' +new_collection: 'Nova col·lecció' +add_m2o_to_collection: 'Afegeix Molts-a-Un a "{collection}"' +add_o2m_to_collection: 'Afegeix Un-a-Molts a "{collection}"' +add_m2m_to_collection: 'Afegeix Molts-a-Molts a "{collection}"' +choose_a_type: Tria un tipus... +determined_by_relationship: Determinat per la relació add_note: Afegiu una nota útil per als usuaris... default_value: Valor per defecte +standard_field: Camp estàndard +single_file: Un fitxer +multiple_files: Multiples fitxers +m2o_relationship: Relació Molts a Un +o2m_relationship: Relació Un a molts +m2m_relationship: Relació Molts a molts +m2a_relationship: Relació Molts a qualsevol +invalid_item: Element invàlid +next: Següent +field_name: Nom del camp +translations: Traduccions +note: Nota +enter_a_value: Introdueix un valor... +enter_a_placeholder: Introdueix un placeholder... +length: Mida +precision_scale: Precisió i escala +readonly: Només lectura unique: Únic +updated_on: Actualitzat el +updated_by: Actualitzat per +primary_key: Clau primària +foreign_key: Clau forana +finish_setup: Finalitza la configuració +dismiss: Descarta +raw_value: Valor en brut +edit_raw_value: Edita valor en brut +enter_raw_value: Introdueix valor en brut... clear_value: Esborra el valor +reset_to_default: Restableix valors predeterminats undo_changes: Desfer els canvis +notifications: Notificacions +show_all_activity: Mostra tota l'activitat +page_not_found: Pàgina no trobada +page_not_found_body: No hem trobat la pàgina que cerques. +confirm_revert: Confirma Revertir +confirm_revert_body: Això revertirà l'element a l'estat seleccionat. display: Mostra +settings_update_success: Ajustaments actualitzats title: Títol +revision_delta_created: Creat +revision_delta_created_externally: Creat externament +revision_delta_updated: 'Actualitzat 1 camp | Actualitzats {count} camps' +revision_delta_deleted: Eliminat +revision_delta_reverted: Revertit +revision_delta_other: Revisió +revision_delta_by: '{date} per {user}' +private_user: Usuari privat +revision_preview: Vista preliminar de la revisió +updates_made: Actualitzacions fetes +leave_comment: Deixa un comentari... +post_comment_success: Comentari publicat +item_create_success: Element creat | Elements creats +item_update_success: Element actualitzat | Elements actualitzats +item_delete_success: Element eliminat | Elements eliminats this_collection: Aquesta col·lecció +related_collection: Col·lecció relacionada +related_collections: Col·leccions relacionades +translations_collection: Col·lecció de traduccions +languages_collection: Col·lecció de idiomes +export_data: Exporta les dades format: Format +use_current_filters_settings: Utilitza els filtres i ajustaments actuals +export_collection: 'Exporta {collection}' +last_page: Última pàgina +last_access: Últim accés +fill_template: Emplena amb el valor de plantilla +a_unique_table_name: Un nom de taula únic... +a_unique_column_name: Un nom de columna únic... +enable_custom_values: Habilita valors personalitzats +submit: Envia +move_to_folder: Mou a la carpeta +move: Mou +system: Sistema +add_field_related: Afegeix camps a la col·lecció relacionada +interface_label: Interfície +today: Avui +yesterday: Ahir +delete_comment: Elimina comentari +date-fns_date: PPP +date-fns_time: 'h:mm:ss a' +date-fns_time_no_seconds: 'h.mm a' +date-fns_date_short: 'MMM d, u' +date-fns_time_short: 'h:mma' +date-fns_date_short_no_year: MMM d +month: Mes +year: Any +select_all: Marca totes +months: + january: Gener + february: Febrer + march: Març + april: Abril + may: Maig + june: Juny + july: Juliol + august: Agost + september: Setembre + october: Octubre + november: Novembre + december: Desembre +drag_mode: Mode d'arrossegament +cancel_crop: Cancel·la el retall +original: Original +url: URL +import_label: Importa +file_details: Detalls del fitxer +dimensions: Dimensions size: Mida +created: Creat +modified: Modificat checksum: Checksum owner: Propietari +edited_by: Editat per +folder: Carpeta +zoom: Zoom +download: Descarrega +open: Obre +open_in_new_window: Obre en una nova finestra +foreground_color: Color de primer pla +background_color: Color de fons +upload_from_device: Carrega el fitxer des del dispositiu +choose_from_library: Tria de la biblioteca +import_from_url: Importa el fitxer des de la URL +replace_from_device: Reemplaça el fitxer des del dispositiu +replace_from_library: Reemplaça el fitxer des de la biblioteca +replace_from_url: Reemplaça el fitxer des d'una URL +no_file_selected: Cap fitxer seleccionat +download_file: Descarrega el fitxer +collection_key: Clau de la col·lecció +name: Nom +primary_key_field: Camp de clau primària +type: Tipus +creating_new_collection: Creant una nova col·lecció created_by: Creat per created_on: Creat el +creating_collection_info: Fica nom a la col·lecció i estableix la seu camp "clau" únic... +creating_collection_system: Habilita i reanomena qualsevol d'aquests camps opcionals. +auto_increment_integer: Sencer auto-incremental +generated_uuid: UUID Generat +manual_string: String entrada manualment +save_and_create_new: Desa i crea un nou +save_and_stay: Desa i queda't +save_as_copy: Desa com a còpia +add_existing: Afegeix existent +creating_items: Creant elements +enable_create_button: Habilita el botó de creació +selecting_items: Seleccionant elements +enable_select_button: Habilita el botó de selecció comments: Comentaris +no_comments: No hi ha comentaris, encara +click_to_expand: Fes clic per ampliar +select_item: Selecciona element +no_items: No hi ha elements +search_items: Cerca elements... +disabled: Deshabilitat +information: Informació +report_bug: Informa d'un error +request_feature: Sol·licita una millora +interface_not_found: 'Interficie "{interface}" no trobada.' +reset_interface: Restableix la interfície +display_not_found: 'Disposició "{display}" no trobada.' +reset_display: Restableix disposició +list-m2a: Constructor (M2A) +item_count: 'No hi ha elements | Un element | {count} elements' +no_items_copy: Encara no hi ha elements en aquesta col·lecció. +file_count: 'No hi ha fitxers | Un fitxer | {count} fitxers' +no_files_copy: No hi ha fitxers aquí. +user_count: 'No hi ha usuaris | Un usuari | {count} usuaris' +no_users_copy: Encara no hi ha usuaris en aquest rol. +webhooks_count: 'No hi ha webhooks | Un webhook | {count} webhooks' +no_webhooks_copy: Encara no hi ha webhooks. +all_items: Tots els elements +any: Qualsevol +csv: CSV +no_collections: No hi ha col·leccions create_collection: Crea una col·lecció +no_collections_copy_admin: Encara no hi ha cap col·lecció. Fes clic al botó següent per començar. +no_collections_copy: Encara no hi ha cap col·lecció. Posa't en contacte amb l'administrador del sistema. relationship_not_setup: La relació no s'ha configurat correctament +display_template_not_setup: L'opció de plantilla de disposió no està configurada +collection_field_not_setup: L'opció de camp de col·lecció no està configurada +select_a_collection: Selecciona una col·lecció +active: Activa +inactive: Inactiva users: Usuaris activity: Activitat +webhooks: Webhooks +field_width: Amplada del camp +add_filter: Afegeix un filtre +upper_limit: Límit superior... +lower_limit: Límit inferior... user_directory: Directori de l'usuari +documentation: Documentació +sidebar: Barra lateral duration: Durada +charset: Conjunt de caràcters +second: segon +file_moved: Fitxer mogut +collection_created: Col·lecció creada +modified_on: Modificat el +card_size: Mida de la targeta sort_field: Camp d'ordenació +add_sort_field: Afegeix el camp d'ordenació +sort: Ordre status: Estat +remove: Elimina +toggle_manual_sorting: Activa l'ordenació manual +bookmark_doesnt_exist: No existeix el marcador +bookmark_doesnt_exist_copy: El marcador que estàs intentant obrir no pot ser trobat. +bookmark_doesnt_exist_cta: Torna a la col·lecció +select_an_item: Selecciona un element... edit: Edita +enabled: Habilitat +disable_tfa: Desactiva 2FA +tfa_scan_code: Escaneja el codi a l'aplicació d'autenticació per acabar d'establir el 2Fa +enter_otp_to_disable_tfa: Introdueix el OTP per deshabilitar el 2FA +create_account: Crea un compte +account_created_successfully: Compte creat amb èxit +auto_fill: Omple automàticament +corresponding_field: Camp corresponent +errors: + COLLECTION_NOT_FOUND: "La col·lecció no existeix" + FIELD_NOT_FOUND: Camp no trobat + FORBIDDEN: Prohibit + INVALID_CREDENTIALS: Identificador o contrasenya incorrectes + INVALID_OTP: Contrasenya d'un sol ús errònia + INVALID_PAYLOAD: Payload invàlid + INVALID_QUERY: Consulta invàlida + ITEM_LIMIT_REACHED: Límit d'elements assolit + ITEM_NOT_FOUND: Element no trobat + ROUTE_NOT_FOUND: No trobat + RECORD_NOT_UNIQUE: Valor duplicat detectat + USER_SUSPENDED: Usuari suspès + CONTAINS_NULL_VALUES: El camp conté valors nuls + UNKNOWN: Error inesperat + UNPROCESSABLE_ENTITY: Entitat no processable + INTERNAL_SERVER_ERROR: Error inesperat + NOT_NULL_VIOLATION: El valor no pot ser null +security: Seguretat +value_hashed: Aplicat hash segur al valor +bookmark_name: Nom del marcador... +create_bookmark: Crea un marcador +edit_bookmark: Edita el marcador bookmarks: Favorits +presets: Predefinits +unexpected_error: Error inesperat +unexpected_error_copy: Hi ha hagut un error inesperat. Si us plau, prova més tard. +copy_details: Copia detalls +no_app_access: Sense accés a l'aplicació +no_app_access_copy: Aquest usuari no pot administrar l'aplicació. +password_reset_sent: T'hem enviat un enllaç segur per restablir la teva contrasenya +password_reset_successful: Contrasenya restablerta correctament back: Enrere +editing_image: Editant imatge +square: Quadrat +free: Lliure +flip_horizontal: Capgira horitzontalment +flip_vertical: Capgira verticalment +aspect_ratio: Relació d'aspecte +rotate: Gira all_users: Tots els usuaris +delete_collection: Elimina la col·lecció +update_collection_success: Actualitza la col·lecció +delete_collection_success: Col·lecció eliminada +start_end_of_count_items: '{start}-{end} de {count} elements' +start_end_of_count_filtered_items: '{start}-{end} de {count} elements filtrats' +one_item: '1 element' +one_filtered_item: '1 element filtrat' +delete_collection_are_you_sure: >- + Estàs segur d'eliminar aquesta col·lecció? Això també eliminarà tots els elements que conté. Aquesta acció és permanent. +collections_shown: Col·leccions mostrades +visible_collections: Col·leccions visibles +hidden_collections: Col·leccions amagades +show_hidden_collections: Mosta les col·leccions amagades +hide_hidden_collections: Amaga les col·leccions amagades +unmanaged_collections: Col·leccions no configurades +system_collections: Col·leccions del sistema placeholder: Referència icon_left: Icona a l'esquerra icon_right: Icona a la dreta +count_other_revisions: '{count} revisions més' +font: Font +sans_serif: Sans Serif +serif: Serif monospace: Espai únic divider: Separador color: Color +circle: Cercle +empty_item: Element buit +log_in_with: 'Inicia sessió amb {provider}' +advanced_settings: Ajustaments avançats +advanced_filter: Filtre avançat +delete_advanced_filter: Elimina filtre +change_advanced_filter_operator: Operador Canvia operators: + eq: És igual + neq: No és igual + lt: Menor que + gt: Major que + lte: Menor o igual que + gte: Major o igual que + in: És un de + nin: No és un de + null: És nul + nnull: No és null contains: Conté + ncontains: No conté + starts_with: Comença amb + nstarts_with: No comença amb + ends_with: Acaba amb + nends_with: No acaba amb + between: És entre + nbetween: No és entre + empty: Es buida + nempty: No es buida + all: Conté aquestes claus + has: Conté alguna d'aquestes claus + intersects: Intersecta + nintersects: No intersecta + intersects_bbox: Intersecta la caixa delimitadora + nintersects_bbox: No intersecta la caixa delimitadora +loading: Carregant... +drop_to_upload: Deixa anar per carregar +item: Element +items: Elements +upload_file: Carrega un fitxer +upload_file_indeterminate: Carregant el fitxer... +upload_file_success: Fitxer carregat +upload_files_indeterminate: 'Carregant fitxers {done}/{total}' +upload_files_success: '{count} fitxers carregats' +upload_pending: Càrrega pendent +drag_file_here: Arrossega i deixa anar un fitxer aquí +click_to_browse: Fes clic per a explorar +interface_options: Opcions d'Interfície +layout_options: Opcions de presentació +rows: Files +columns: Columnes +collection_setup: Configuració de la col·lecció +optional_system_fields: Camps del sistema opcionals +value_unique: El valor ha de ser únic +all_activity: Tota l'activitat +create_item: Crea un element display_template: Mostra la plantilla +language_display_template: Plantilla de visualització d'idioma +translations_display_template: Plantilla de visualització de traduccions +n_items_selected: 'No hi han elements seleccionats | Un element seleccionat | {n} Elements seleccionats' +per_page: Per pàgina +all_files: Tots els fitxers +my_files: Els meus fitxers +recent_files: Fitxers recents +create_folder: Crea una carpeta +folder_name: Nom de la carpeta... +add_file: Afegeix un fitxer +replace_file: Reemplaça el fitxer +no_results: Sense resultats +no_results_copy: Ajusta o neteja els filtres de cerca per veure els resultats. +clear_filters: Neteja els filtres +saves_automatically: Desa automàticament role: Rol +rule: Regla +user: Usuari +no_presets: Sense predefinits +no_presets_copy: Encara no s'han desat predefinits o marcadors. +no_presets_cta: Afegeix predefinit +presets_only: Només predefinit create: Crear +on_create: En la creació +on_update: En l'actualització +read: Llegir +update: Actualitzar +select_fields: Selecciona els camps +format_text: Formata el text +bold: Negreta +toggle: Canvia +icon_on: Icona activada +icon_off: Icona desactivada +label: Etiqueta +image_url: URL de la imatge +alt_text: Text alternatiu +media: Mèdia +quality: Qualitat width: Amplada height: Alçada +source: Origen +url_placeholder: Introdueix una URL... +display_text: Text a mostrar +display_text_placeholder: Introdueix el text a mostrar... +tooltip: Consell +tooltip_placeholder: Introdueix el consell... +unlimited: Il·limitat +open_link_in: Obre l'enllaç a +new_tab: Nova pestanya +current_tab: Pestanya actual wysiwyg_options: + aligncenter: Alinea al centre + alignjustify: Justifica + alignleft: Alinea a l'esquerra + alignnone: Cap alineament + alignright: Alinea a la dreta + forecolor: Color de primer pla + backcolor: Color de fons + bold: Negreta + italic: Cursiva + underline: Subratllat + strikethrough: Tatxat + subscript: Subíndex + superscript: Superíndex codeblock: Codi + blockquote: Bloc de cita + bullist: Llista amb vinyetes + numlist: Llista ordenada + hr: Regle horitzontal + link: Afegeix / Edita l'enllaç + unlink: Elimina l'enllaç + media: Afegeix / Edita el mèdia + image: Afegeix / Edita la imatge + copy: Copia + cut: Retalla + paste: Enganxa + heading: Encapçalament + h1: Capçalera 1 + h2: Encapçalament 2 + h3: Encapçalament 3 + h4: Encapçalament 4 + h5: Encapçalament 5 + h6: Heading 6 + fontselect: Selecciona font + fontsizeselect: Selecciona la mida de la font + indent: Sagna + outdent: Redueix el sagnat + undo: Desfés + redo: Refés + remove: Elimina + removeformat: Elimina el format + selectall: Marca totes table: Taula + visualaid: Visualitza els elements invisibles + source_code: Edita el codi font + fullscreen: Pantalla completa + directionality: Direcció dropdown: Desplegable choices: Opcions +choices_option_configured_incorrectly: Opcions configurades incorrectament deselect: Deselecciona +deselect_all: Deselecciona-ho tot +other: Altres... +adding_user: Afegint usuari +unknown_user: Usuari desconegut +creating_in: 'Creant element a {collection}' +editing_in: 'Editant l''element a {collection}' +creating_unit: 'Creant {unit}' +editing_unit: 'Editant {unit}' editing_in_batch: 'Editant {count} elements' +no_options_available: Sense opcions disponibles +settings_data_model: Model de dades +settings_permissions: Rols i permisos +settings_project: Ajustaments del projecte +settings_webhooks: Webhooks +settings_presets: Predefinits i Marcadors +one_or_more_options_are_missing: Falta una o més opcions +scope: Abast +select: Selecciona... +layout: Disposició +tree_view: Vista d'arbre +changes_are_permanent: Els canvis són permanents +preset_name_placeholder: Serveix com a predeterminat quan és buit... +preset_search_placeholder: Cerca consulta... +editing_preset: Editant el predefinit +layout_preview: Vista preliminar de la disposició +layout_setup: Configuració de la disposició +unsaved_changes: Canvis no desats +unsaved_changes_copy: Estàs segur que vols sortir d'aquesta pàgina? +discard_changes: Descarta els canvis +keep_editing: Continua editant +page_help_collections_overview: '** Visió general de les col·leccions **: Llistes de totes les col·leccions a les quals tens accés.' +page_help_collections_collection: >- + ** Cerca elements **: Llista de tots els elements de {collection} als quals tens accés. Personalitza el disseny, els filtres i l’ordenació per adaptar-la a la teva vista i, fins i tot, desa els marcadors d’aquestes diferents configuracions per accedir-hi ràpidament. +page_help_collections_item: >- + ** Detall de l'element **: Un formulari per visualitzar i gestionar aquest element. Aquesta barra lateral també conté un historial complet de revisions i comentaris incrustats. +page_help_activity_collection: >- + ** Activitat de navegació **: Llista completa de tota l'activitat de contingut i sistema del vostre usuari. +page_help_docs_global: >- + ** Informació general de la documentació **: Documents adaptats específicament a la versió i l'esquema d'aquest projecte. +page_help_files_collection: >- + ** Biblioteca de fitxers **: Llista de tots els recursos de fitxers carregats en aquest projecte. Personalitza el disseny, els filtres i l’ordenació per adaptar-la a la teva vista i, fins i tot, desa els marcadors d’aquestes configuracions per accedir-hi ràpidament. +page_help_files_item: >- + ** Detall de fitxer **: Formulari per gestionar metadades de fitxers, editar el recurs original i actualitzar la configuració d'accés. +page_help_settings_project: "** Configuració del projecte **: Opcions de configuració global del vostre projecte." +page_help_settings_datamodel_collections: >- + ** Model de dades: Col·leccions **: Llista de totes les col·leccions disponibles. Això inclou col·leccions visibles, ocultes i del sistema, així com taules de bases de dades no gestionades que es poden afegir. +page_help_settings_datamodel_fields: >- + ** Model de dades: col·lecció **: Un formulari per gestionar aquesta col·lecció i els seus camps. +page_help_settings_roles_collection: '** Fons d''exploració **: Llista dels rols Administrador, públic i personalitzats.' +page_help_settings_roles_item: "** Detalls del rol **: Gestiona els permisos i altres paràmetres d'un rol." +page_help_settings_presets_collection: >- + ** Cerca predefinits **: Llista de tots els predefinits del projecte, inclosos: marcadors d'usuaris, de rol i globals, així com les vistes predeterminades. +page_help_settings_presets_item: >- + ** Detall de predefinit **: Un formulari per gestionar adreces d'interès i predefinits de col·lecció predeterminats. +page_help_settings_webhooks_collection: '** Examinar Webhooks **: Llista tots els webhooks del projecte.' +page_help_settings_webhooks_item: '** Detall de webhook **: Un formulari per crear i gestionar webhooks del projecte.' page_help_users_collection: '**Directori de l''usuari**.' +page_help_users_item: >- + ** Detall de l'usuari **: Gestiona la informació del compte o consulta la informació d'altres usuaris. +activity_feed: Canal d'activitat add_new: Afegir nou +create_new: Crea un nou all: Tots +none: Cap +no_layout_collection_selected_yet: No s'ha seleccionat cap disposició / col·lecció batch_delete_confirm: >- No s'ha seleccionat cap element | Esteu segur que voleu suprimir aquest element? No es pot desfer aquesta acció. | Esteu segur que voleu suprimir aquests {count} elements? No es pot desfer aquesta acció. cancel: Cancel · la collection: Coŀlecció collections: Col·leccions +singleton: Singleton +singleton_label: Tracta com un objecte únic +system_fields_locked: Els camps del sistema estan bloquejats i no poden editar-se fields: directus_activity: item: Clau primaria de l'ítem @@ -118,25 +737,44 @@ fields: user: Acció de comment: Comentari user_agent: Agent de l'usuari - ip: Adressa IP + ip: Adreça IP + revisions: Revisions directus_collections: collection: Coŀlecció + icon: Icona + note: Nota display_template: Mostra la plantilla + hidden: Amagat + singleton: Singleton + translations: Traduccions de noms de col·lecció + archive_app_filter: Filtre d'arxivat de l'aplicació + archive_value: Valor d'arxivat + unarchive_value: Valor per desarxivar sort_field: Camp d'ordenació + accountability: Traça d'activitat i revisió directus_files: + $thumbnail: Miniatura title: Títol description: Descripció tags: Etiquetes location: Localització + storage: Emmagatzematge + filename_disk: Nom del fitxer (Disc) + filename_download: Nom del fitxer (Descàrrega) metadata: Metadades + type: Tipus Mime filesize: Mida + modified_by: Modificat per + modified_on: Modificat el created_on: Creat el created_by: Creat per embed: Inclou uploaded_by: Pujat per + folder: Carpeta width: Amplada uploaded_on: Pujat el height: Alçada + charset: Conjunt de caràcters duration: Durada directus_users: first_name: Nom @@ -148,82 +786,555 @@ fields: title: Títol description: Descripció tags: Etiquetes + user_preferences: Preferències de l'usuari language: Idioma theme: Tema + theme_auto: Automàtic (basat en el sistema) + theme_light: Mode clar + theme_dark: Mode fosc tfa_secret: Autenticació de doble factor + admin_options: Opcions d'administrador status: Estat + status_draft: Esborrany + status_active: Activa + status_suspended: Suspès + status_archived: Arxivat role: Rol token: Token + token_placeholder: Entra un testimoni d'accés (token) segur... + last_page: Última pàgina + last_access: Últim accés + directus_settings: + project_name: Nom del projecte + project_url: URL del projecte + project_color: Color del projecte + project_logo: Logotip del projecte + public_pages: Pàgines públiques + public_foreground: Primer pla públic + public_background: Fons públic + public_note: Anotació de la vista pública + auth_password_policy: Política de contrasenyes + auth_login_attempts: Intents d'autenticació + files_and_thumbnails: Fitxers i miniatures + storage_default_folder: Carpeta predeterminada d'emmagatzematge + storage_asset_presets: Predefinits de recursos d'emmagatzematge + storage_asset_transform: Transformació de recursos d'emmagatzematge + overrides: Substitucións de l'aplicació + custom_css: CSS personalitzat directus_fields: + collection: Nom de la col·lecció + icon: Icona de la col·lecció + note: Nota + hidden: Amagat + singleton: Singleton translation: Traducció de noms de camps display_template: Plantilla directus_roles: name: Nom del Rol + icon: Icona del rol description: Descripció + app_access: Accés a l'aplicació + admin_access: Accés d'administrador + ip_access: Accés per IP + enforce_tfa: Requereix 2FA users: Usuaris al rol + module_list: Navegació dels mòduls + collection_list: Navegació de les col·leccions directus_webhooks: + name: Nom + method: Mètode status: Estat + data: Dades + data_label: Envia les dades de l'event + triggers: Disparadors + actions: Accions +field_options: + directus_settings: + security_divider_title: Seguretat + files_divider_title: Fitxers i miniatures + overrides_divider_title: Substitucións de l'aplicació + directus_activity: + login: Entra + create: Crear + update: Actualitzar + delete: Elimina + directus_collections: + track_activity_revisions: Traça l'activitat i revisions + only_track_activity: Només traça l'activitat + do_not_track_anything: No tracis res + collection_setup: Configuració de la col·lecció + singleton: Tracta com un objecte únic + language: Idioma + archive_divider: Arxiu + divider: Ordre + directus_roles: + fields: + icon_name: Icona + name_name: Nom + name_placeholder: Introdueix un títol... + collection_list: + fields: + type_name: Tipus + choices_start_open: Comença obert + collections_name: Col·leccions + directus_users: + preferences_divider: Preferències de l'usuari + dropdown_auto: Automàtic (basat en el sistema) + dropdown_light: Mode clar + dropdown_dark: Mode fosc + admin_divider: Opcions d'administrador + status_dropdown_draft: Esborrany + status_dropdown_active: Activa + status_dropdown_suspended: Suspès + status_dropdown_archived: Arxivat + token: Entra un testimoni d'accés (token) segur... + directus_webhooks: + status_options_active: Activa + status_options_inactive: Inactiva + data_label: Envia les dades de l'event + triggers_divider: Disparadors + actions_create: Crear + actions_update: Actualitzar + actions_delete: Elimina +no_fields_in_collection: 'Encara no hi ha camps a "{collection}"' +do_nothing: No facis res +generate_and_save_uuid: Genera i Desa UUID +save_current_user_id: Desa el ID d'usuari actual +save_current_user_role: Desa el rol d'usuari actual +save_current_datetime: Desa la Data/Hora actual +block: Block +inline: Inline comment: Comentari +relational_triggers: Disparadors relacionals +referential_action_field_label_m2o: Si s'esborra de {collection}... +referential_action_field_label_o2m: Si es deselecciona de {collection}... +referential_action_no_action: Evita l'eliminació +referential_action_cascade: Elimina l'element de {collection} (cascada) +referential_action_set_null: Nullifica el camp {field} +referential_action_set_default: Assigna el camp {field} al valor per defecte +choose_action: Tria una acció +continue_label: Continua +continue_as: >- + {name} està autenticat. Si no reconeixes aquest compte, polsa continua. +editing_role: 'Rol {role}' +creating_webhook: Creant el webhook +default_label: Predeterminat +delete_label: Elimina +delete_are_you_sure: >- + Aquesta acció és permanent i no es pot desfer. Estàs segur que vols continuar? +delete_field_are_you_sure: >- + Segur que vols eliminar el camp "{field}"? Aquesta acció no es pot desfer. description: Descripció done: Fet duplicate: Duplicat email: Correu electrònic embed: Inclou +fallback_icon: Icona de reserva field: Camp | Camps +file: Fitxer +file_library: Biblioteca de fitxers +forgot_password: He oblidat la contrasenya +hidden: Amagat +icon: Icona +info: Informació +normal: Normal +success: Èxit +warning: Advertencia +danger: Perill +junction_collection: Unió de col·leccions +latency: Latència +login: Entra +my_activity: La meva activitat +not_authenticated: No autenticat +authenticated: Autenticat +options: Opcions otp: Contrasenya d'un sol ús password: Contrasenya +permissions: Permisos +relationship: Relacions +reset: Restablir +reset_password: Restableix la contrasenya +revisions: Revisions +revert: Desfés +save: Desa +schema: Esquema +search: Cerca +select_existing: Selecciona existent +select_field_type: Selecciona el tipus de camp +select_interface: Selecciona la interfície +settings: Configuració +sign_in: Inicia sessió +sign_out: Tanca la sessió +sign_out_confirm: Estàs segur que vols tancar la sessió? +something_went_wrong: Alguna cosa no ha anat bé. +sort_direction: Direcció de l'ordenació +sort_asc: Ordre ascendent +sort_desc: Ordre descendent template: Plantilla +require_value_to_be_set: Requereix un valor +translation: Traducció +value: Valor +view_project: Veure el projecte +report_error: Notifica un error +start: Comença interfaces: + group-accordion: + name: Acordió + description: Mostra els camps o els grups com a seccions de l'acordió + start: Comença + all_closed: Tots tancats + first_opened: Primer obert + all_opened: Tots oberts + accordion_mode: Mode d'acordió + max_one_section_open: Màxim 1 secció oberta + presentation-links: + presentation-links: Botó d'enllaços + links: Enllaços + description: Botons d'enllaços configurables per a llançar URLs dinàmiques + style: Estil + primary: Primari + link: Enllaços + button: Botons + error: No es pot dur a terme l'acció + select-multiple-checkbox: + checkboxes: Camps de selecció (Checkboxes) + description: Selecciona entre múltiples opcions via camps de selecció + allow_other: Permet altres + show_more: 'Mostra {count} més' + items_shown: Elements mostrats + select-multiple-checkbox-tree: + name: Arbre de camps de selecció (checkboxes Tree) + description: Selecciona entre múltiples opcions via camps de selecció (checkboxes) + value_combining: Valor combinant + value_combining_note: Controla quin valor és emmagatzemant quan es fan les seleccions anidades. + show_all: Mostra-ho tot + show_selected: Mostra els seleccionts input-code: code: Codi + description: Escriu o comparteix snippets de codi + line_number: Número de línia + placeholder: Introdueix el codi aquí... system-collection: collection: Coŀlecció + description: Selecciona entre les següents col·leccions + include_system_collections: Inclou les col·leccions del sistema system-collections: collections: Col·leccions + description: Selecciona entre les següents col·leccions + include_system_collections: Inclou les col·leccions del sistema select-color: color: Color + description: Entra o selecciona un valor de color + placeholder: Tria un color... + preset_colors: Predefinit de colors + preset_colors_add_label: Afegeix un nou color... + name_placeholder: Introdueix un nom de color... datetime: datetime: Data i hora + description: Introdueix dates i hores + include_seconds: Inclou els segons + set_to_now: Fixa'l a "Ara" + use_24: Utilitza el format de 24 hores system-display-template: display-template: Mostra la plantilla + description: Barreja text estàtic i valors de camp dinàmic + collection_field: Camp de col·lecció + collection_field_not_setup: L'opció de camp de col·lecció no està configurada + select_a_collection: Selecciona una col·lecció presentation-divider: divider: Separador + description: Etiqueta i divideix els camps en seccions + title_placeholder: Introdueix un títol... + inline_title: Títol en línia + inline_title_label: Mostra el títol dins la línia + margin_top: Marge superior + margin_top_label: Incrementa el marge superior + select-dropdown: + description: Selecciona el valor del desplegable + choices_placeholder: Afegeix una nova opció + allow_other: Permet altres + allow_other_label: Permet altres valors + allow_none: No permetre cap + allow_none_label: Permet cap selecció + choices_name_placeholder: Introdueix un nom... + choices_value_placeholder: Introdueix un valor... + select-multiple-dropdown: + select-multiple-dropdown: Desplegable (Múltiple) + description: Selecciona múltiples valors des del desplegable + file: + file: Fitxer + description: Selecciona o carrega un fitxer files: files: Arxius + description: Selecciona o carrega múltiples fitxers + input-hash: + hash: Hash + description: Entra un valor per aplicar-hi hash + masked: Enmascarat + masked_label: Amaga els valors "cert" + select-icon: + icon: Icona + description: Selecciona la icona d'un desplegable + search_for_icon: Cerca una icona... + file-image: + image: Imatge + description: Selecciona o carrega una imatge + system-interface: + interface: Interfície + description: Selecciona una interfície existent + placeholder: Selecciona una interfície... + system-interface-options: + interface-options: Opcions d'Interfície + description: Un modal per la selecció de les opcions d'interfície + list-m2m: + many-to-many: Molts a molts + description: Selecciona múltiples elements d'unió relacionats select-dropdown-m2o: + many-to-one: Molts a un + description: Selecciona un únic element relacionat display_template: Mostra la plantilla + input-rich-text-md: + markdown: Markdown + description: Entra i previsualitza el markdown + customSyntax: Blocs personalitzats + customSyntax_label: Afegeix tipus de sintaxi personalitzats + customSyntax_add: Afegeix sintaxi personalitzada + box: Block / Inline + imageToken: Testimoni d'Imatge + imageToken_label: Quin testimoni (estàtic) a afegir a els origen de la imatge + map: + map: Mapa + description: Selecciona una localització al mapa + zoom: Zoom + geometry_type: Tipus geometria + geometry_format: Format geometria + default_view: Vista predeterminada + invalid_options: Opcions no vàlides + invalid_format: Format no vàlid ({format}) + unexpected_geometry: Esperat {expected}, obtingut {got}. + fit_bounds: Ajusta la vista a les dades + native: Nadiu + geojson: GeoJSON + lnglat: Longitud, Latitud + wkt: WKT + wkb: WKB + presentation-notice: + notice: Avís + description: Mostra un petit avís + text: Introdueix el contingut de l'avís aquí... + list-o2m: + one-to-many: Un a Molts + description: Selecciona múltiples elements relacionats + no_collection: No s'ha trobat la col·lecció + system-folder: + folder: Carpeta + description: Selecciona una carpeta + field_hint: Insereix els fitxers carregats recentment a la carpeta seleccionada. No afecta els fitxers existents seleccionats. + root_name: Arrel de la biblioteca de fitxers + system_default: Predeterminats del sistema + select-radio: + radio-buttons: Botons de selecció + description: Selecciona una de múltiples opcions + list: + repeater: Repetidor + description: Crea múltiples entrades de la mateixa estructura + edit_fields: Edita els camps + add_label: 'Etiqueta "Crea nou"' + field_name_placeholder: Introdueix nou camp... + field_note_placeholder: Introdueix una nota sobre el camp... + slider: + slider: Lliscador + description: Selecciona un número utilitzant el lliscador + always_show_value: Mostra el valor sempre tags: tags: Etiquetes + description: Selecciona o afegeix etiquetes + whitespace: Espai en blanc + hyphen: Substitueix per guionet + underscore: Substitueix per guió baix + remove: Elimina l'espai en blanc + capitalization: Majúscules + uppercase: Converteix majúscules + lowercase: Converteix minúscules + auto_formatter: Utilitza autoformatador del títol + alphabetize: Alfabètic + alphabetize_label: Força ordre alfabètic + add_tags: Afegeix etiquetes... + input: + input: Entrada + description: Entra manualment un valor + trim: Elimina espais + trim_label: Elimina espais de l'inici i el final + mask: Enmascarat + mask_label: Amaga el valor real + clear: Valor netejat + clear_label: Desa com una cadena buida + minimum_value: Valor mínim + maximum_value: Valor màxim + step_interval: Interval del pas + slug: Slugify + slug_label: Fes URL segura el valor entrat + input-multiline: + textarea: Textarea + description: Entra text multilínia + boolean: + toggle: Canvia + description: Canvia entre Encés i apagat + label_placeholder: Introdueix una etiqueta... + label_default: Habilitat translations: display_template: Mostra la plantilla + no_collection: No hi ha col·leccions + list-o2m-tree-view: + description: Vista d'arbre per a elements un-a-molts anidats recursivament + recursive_only: La interfície de vista d'arbre només funciona per a relacions recursives. user: + user: Usuari description: Seleccioneu un usuari existent + select_mode: Selecciona el mode modes: + auto: Auto dropdown: Desplegable + modal: Modal + input-rich-text-html: + wysiwyg: WYSIWYG + description: Un editor de text enriquit escrivint contingut HTML + toolbar: Barra d'eines + custom_formats: Formats personalitzats + options_override: Sobreescriptura d'opcions + input-autocomplete-api: + input-autocomplete-api: Entrada autocompleta (API) + description: Una cerca typeahead per valors d'API externes. + results_path: Camí dels resultats + value_path: Camí del Valor + trigger: Disparador + rate: Velocitat + group-raw: + name: Grup en brut + description: Mostra el contingut tal com és + group-detail: + name: Detall del grup + description: Mostra els camps com una secció plegable + show_header: Mostra la capçalera del grup + header_icon: Icona de la capçalera + header_color: Color de la capçalera + start_open: Comença obert + start_closed: Comená tancat displays: + boolean: + boolean: Boolean + description: Mostra els estats Activat i desactivat + label_on: Etiqueta activada + label_on_placeholder: Introdueix l'etiqueta activada... + label_off: Etiqueta desactivada + label_off_placeholder: Introdueix l'etiqueta desactivada... + icon_on: Icona activada + icon_off: Icona desactivada + color_on: Color activat + color_off: Color desactivat collection: collection: Coŀlecció + description: Mostra una col·lecció + icon_label: Mostra la icona de la col·lecció color: color: Color + description: Mostra un punt colorejat + default_color: Color predeterminat datetime: datetime: Data i hora + description: Mostra valors associats al temps format: Format + format_note: >- + El format personalitzat accepta __[Date Field Symbol Table](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)__ + long: Llarg + short: Curt + relative: Relatiu + relative_label: 'Mostra el temps relatiu, ex: fa 5 minuts' + file: + file: Fitxer + description: Mostra fitxers filesize: filesize: Mida del fitxer + description: Mostra la mida del fitxer + formatted-value: + formatted-value: Valor formatat + description: Mostra una versió formatada del text + format_title: Títol del format + format_title_label: Auto formata les minúscules i majúscules + bold_label: Utilitza text en negreta + formatted-json-value: + formatted-json-value: Valor JSON formatat + description: Mostra una versió formatada del l'objecte + icon: + icon: Icona + description: Mostra una icona + filled: Emplenat + filled_label: Utilitza una variant emplenada + image: + image: Imatge + description: Mostra una petita previsualització de la imatge + circle: Cercle + circle_label: Mostra com a cercle + labels: + labels: Etiquetes + description: Mostra un únic o bé una llista d'etiquetes + default_foreground: Primer pla predeterminat + default_background: Fons prederterminat + format_label: Formata cada etiqueta + show_as_dot: Mostra com a punt + choices_value_placeholder: Introdueix un valor... + choices_text_placeholder: Introdueix un text... + mime-type: + mime-type: TIpus MIME + description: Mostra el tipus MIME d'un fitxer + extension_only: Només l'extensió + extension_only_label: Mostra només l'extensió rating: rating: Puntuació + description: Visualitza un número d'estrelles relatiu al valor màxim + simple: Simple + simple_label: Mostra estrelles en un format simple + raw: + raw: Calor en brut + related-values: + related-values: Valors relacionats + description: Mostra valors relacionats user: + user: Usuari description: Mostra un usuari avatar: Foto + name: Nom both: Ambdós + circle_label: Mostra l'usuari en un cercle layouts: cards: cards: Targetes image_source: Recurs d'imatge + image_fit: Ajusta l'imatge + crop: Retalla + contain: Conté title: Títol subtitle: Subtítol tabular: tabular: Taula + fields: Camps + spacing: Espaiat comfortable: Còmoda compact: Compacta cozy: Acollidora + calendar: + calendar: Calendari + start_date_field: Camp de data d'inici + end_date_field: Camp de data de fi + map: + map: Mapa + basemap: Mapa base + layers: Capes + edit_custom_layers: Edita capes + cluster_options: Opcions de clustering + cluster: Activa el clustering + cluster_radius: Radi del cluster + cluster_minpoints: Mida mínima del clúster + cluster_maxzoom: Zoom màxim per clustering + field: Geometria + invalid_geometry: Geometria invàlida diff --git a/app/src/lang/translations/cs-CZ.yaml b/app/src/lang/translations/cs-CZ.yaml index dc49d3c73d..58487a4a65 100644 --- a/app/src/lang/translations/cs-CZ.yaml +++ b/app/src/lang/translations/cs-CZ.yaml @@ -20,15 +20,25 @@ #'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', #'Proxy', 'Intl' edit_field: Upravit pole +conditions: Podmínky +maps: Mapy item_revision: Revize položky duplicate_field: Duplikovat pole half_width: Poloviční šířka full_width: Plná šířka +group: Skupina +and: A +or: Nebo fill_width: Plná šířka field_name_translations: Překlady názvu polí enter_password_to_enable_tfa: Zadejte své heslo pro zapnutí dvoufázového ověřování add_field: Přidat pole role_name: Jméno role +branch: Větev +leaf: List +indeterminate: Neurčitý +edit_collection: Upravit kolekci +children: Potomci db_only_click_to_configure: 'Pouze databáze: Klikněte pro nastavení ' show_archived_items: Zobrazit archivované položky edited: Hodnota upravena @@ -61,6 +71,7 @@ delete_bookmark_copy: >- logoutReason: SIGN_OUT: Odhlášen SESSION_EXPIRED: Relace vypršela +public_label: Veřejný public_description: Určuje, která API data jsou dostupná bez ověřování. not_allowed: Nepovoleno directus_version: Verze Directusu @@ -103,7 +114,10 @@ validationError: all_access: Veškerý přístup no_access: Žádný přístup use_custom: Použít vlastní +nullable: Nulovatelný allow_null_value: Povolit NULL hodnotu +allow_multiple: Povolit více +allow_multiple_to_be_open: Povolit více otevřených enter_value_to_replace_nulls: Zadejte novou hodnotu, která nahradí všechny NULL, které jsou aktuálně v tomto poli. field_standard: Standardní field_m2o: M2O vazba @@ -115,13 +129,31 @@ field_permissions: Oprávnění pole field_validation: Validace pole permissions_for_role: 'Položky, které {role} role může {action}.' fields_for_role: 'Pole, které {role} role může {action}.' +hide_field_on_detail: Skrýt pole v detailu +show_field_on_detail: Zobrazit pole v detailu delete_field: Smazat pole +fields_and_layout: Pole a rozložení +field_create_success: 'Vytvořeno pole: "{field}"' +field_update_success: 'Aktualizováno pole: "{field}"' +duplicate_where_to: Kam chcete zkopírovat toto pole? language: Jazyk global: Globální admins_have_all_permissions: Administrátoři mají všechna oprávnění +camera: Fotoaparát +exposure: Expozice +shutter: Závěrka +iso: ISO +focal_length: Ohnisková vzdálenost +schema_setup_key: Název databázového sloupce tohoto pole a API klíč create_field: Vytvořit pole +creating_new_field: 'Nové pole ({collection})' +field_in_collection: '{field} ({collection})' +reset_page_preferences: Obnovit předvolby stránky hidden_field: Skryté pole hidden_on_detail: Skryté v detailu +disabled_editing_value: Zakázat úpravy hodnoty +key: Klíč +alias: Alias date: Datum text: Text auto_generate: Auto-generování @@ -318,6 +350,26 @@ fields: directus_webhooks: name: Jméno status: Stav +field_options: + directus_activity: + login: Login + create: Vytořit + update: Úpravy + delete: Smazat + directus_collections: + language: Jazyk + archive_divider: Archivovat + directus_roles: + fields: + icon_name: Ikona + name_name: Jméno + collection_list: + fields: + type_name: Typ + collections_name: Kategorie + directus_webhooks: + actions_create: Vytořit + actions_update: Úpravy comment: Komentář delete_field_are_you_sure: >- Opravdu chcete smazat pole "{field}"? Tuto akci nelze vrátit zpět. diff --git a/app/src/lang/translations/da-DK.yaml b/app/src/lang/translations/da-DK.yaml index 8cf8157dfc..dda0425e17 100644 --- a/app/src/lang/translations/da-DK.yaml +++ b/app/src/lang/translations/da-DK.yaml @@ -20,6 +20,7 @@ #'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', #'Proxy', 'Intl' edit_field: Rediger Felt +conditions: Betingelser item_revision: Genstandsrevision duplicate_field: Dupliker Felt half_width: Halv Bredde @@ -149,6 +150,7 @@ boolean: Boolean date: Dato text: Tekst auto_generate: Auto- Generér +new_field: 'Nyt felt' add_note: Tilføj en nyttig note til brugere... default_value: Standardværdi next: Næste @@ -161,8 +163,10 @@ clear_value: Nulstil værdi reset_to_default: Nulstil til standard undo_changes: Fortryd ændringer notifications: Notifikationer +show_all_activity: Vis al aktivitet page_not_found: Siden blev ikke fundet page_not_found_body: Den side, du leder efter, synes ikke at eksistere. +settings_update_success: Indstillinger opdateret title: Titel revision_delta_created: Oprettet revision_delta_created_externally: Oprettet Eksternt @@ -180,8 +184,15 @@ item_create_success: Element oprettet | Elementer oprettet item_update_success: Element opdateret | Elementer opdateret this_collection: Denne Samling related_collection: Relateret Samling +export_data: Eksporter data format: Format +last_page: Sidste side submit: Send +system: System +delete_comment: Slet kommentar +month: Måned +year: År +select_all: Vælg alle months: january: Januar february: Februar @@ -195,7 +206,14 @@ months: october: Oktober november: November december: December +drag_mode: Træktilstand +cancel_crop: Annuller beskæring +original: Original +url: URL +import_label: Importér +dimensions: Dimensioner created: Oprettet +modified: Ændret checksum: Kontrolsum owner: Ejer download: Download @@ -246,6 +264,7 @@ height: Højde wysiwyg_options: codeblock: Kode remove: Fjern + selectall: Vælg alle table: Tabel choices: Valgmuligheder deselect: Afmarkér @@ -310,6 +329,7 @@ fields: tfa_secret: To-faktor godkendelse status: Status role: Rolle + last_page: Sidste side directus_settings: project_name: Projektnavn directus_fields: @@ -323,6 +343,23 @@ fields: directus_webhooks: name: Navn status: Status +field_options: + directus_activity: + login: Log ind + create: Opret + update: Opdater + delete: Slet + directus_collections: + language: Sprog + archive_divider: Arkiver + directus_roles: + fields: + icon_name: Ikon + name_name: Navn + collections_name: Samlinger + directus_webhooks: + actions_create: Opret + actions_update: Opdater comment: Kommenter delete_field_are_you_sure: >- Er du sikker på du vil slette feltet "{field}"? Denne handling kan ikke fortrydes. diff --git a/app/src/lang/translations/de-DE.yaml b/app/src/lang/translations/de-DE.yaml index 8395acd136..05d091c1c7 100644 --- a/app/src/lang/translations/de-DE.yaml +++ b/app/src/lang/translations/de-DE.yaml @@ -20,15 +20,27 @@ #'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', #'Proxy', 'Intl' edit_field: Feld bearbeiten +conditions: Bedingungen +maps: Karten item_revision: Element Revision duplicate_field: Feld duplizieren half_width: Halbe Breite full_width: Volle Breite +limit: Limit +group: Gruppe +and: Und +or: Oder fill_width: Breite füllen field_name_translations: Feldnamen-Übersetzungen enter_password_to_enable_tfa: Geben Sie Ihr Passwort ein, um die Zwei-Faktor-Authentifizierung zu aktivieren add_field: Feld hinzufügen role_name: Rollenname +branch: Branch +leaf: Blatt +indeterminate: Unbestimmt +edit_collection: Sammlung bearbeiten +exclusive: Exklusive +children: Untergeordnete db_only_click_to_configure: 'Nur in der Datenbank: Zum Konfigurieren klicken ' show_archived_items: Zeige archivierte Elemente edited: Bearbeiteter Wert @@ -62,6 +74,7 @@ delete_bookmark_copy: >- logoutReason: SIGN_OUT: Abgemeldet SESSION_EXPIRED: Sitzung abgelaufen +public_label: Öffentlich public_description: Bestimmt, welche API-Daten ohne Authentifizierung zugänglich sind. not_allowed: Nicht erlaubt directus_version: Directus Version @@ -107,6 +120,8 @@ no_access: Kein Zugriff use_custom: Benutzerdefiniert nullable: Nullbar allow_null_value: NULL-Wert erlauben +allow_multiple: Mehrere zulassen +allow_multiple_to_be_open: Gleichzeitiges Öffnen erlauben enter_value_to_replace_nulls: Bitte geben Sie einen neuen Wert ein, um alle NULLs zu ersetzen, die derzeit in diesem Feld liegen. field_standard: Standard field_presentation: Darstellung & Aliasse @@ -117,6 +132,7 @@ field_m2a: M2A Beziehung field_o2m: O2M Beziehung field_m2m: M2M Beziehung field_translations: Übersetzungen +field_group: Feldgruppe item_permissions: Item Berechtigungen field_permissions: Feldberechtigungen field_validation: Feldüberprüfung @@ -169,6 +185,7 @@ time: Zeit timestamp: Zeitstempel uuid: UUID hash: Hash +geometry: Dimensionen not_available_for_type: Nicht verfügbar für diesen Typ create_translations: Übersetzungen erstellen auto_refresh: Auto-Aktualisierung @@ -299,6 +316,7 @@ drag_mode: Drag-Modus cancel_crop: Zuschneiden abbrechen original: Original url: URL +import_label: Importieren file_details: Dateiinformationen dimensions: Dimensionen size: Größe @@ -366,6 +384,7 @@ no_users_copy: Es gibt noch keine Benutzer in dieser Rolle. webhooks_count: 'Keine Webhooks | Ein Webhook | {count} Webhooks' no_webhooks_copy: Es gibt noch keine Webhooks. all_items: Alle Elemente +any: Irgendein csv: CSV no_collections: Keine Sammlungen create_collection: Sammlung erstellen @@ -376,6 +395,7 @@ display_template_not_setup: Die Anzeige-Vorlage ist falsch konfiguriert collection_field_not_setup: Das Sammlungsfeld ist falsch konfiguriert select_a_collection: Sammlung auswählen active: Aktiv +inactive: Inaktiv users: Benutzer activity: Aktivität webhooks: Webhooks @@ -388,6 +408,7 @@ documentation: Dokumentation sidebar: Seitenleiste duration: Dauer charset: Zeichensatz +second: Sekunde file_moved: Datei verschoben collection_created: Sammlung erstellt modified_on: Geändert am @@ -426,8 +447,10 @@ errors: USER_SUSPENDED: Benutzer gesperrt CONTAINS_NULL_VALUES: Feld enthält NULL-Werte UNKNOWN: Unerwarteter Fehler + UNPROCESSABLE_ENTITY: Unverarbeitbares Objekt INTERNAL_SERVER_ERROR: Unerwarteter Fehler NOT_NULL_VIOLATION: Der Wert darf nicht leer sein +security: Sicherheit value_hashed: Wert sicher gehashed bookmark_name: Name des Lesezeichens... create_bookmark: Lesezeichen erstellen @@ -479,6 +502,7 @@ color: Farbe circle: Kreis empty_item: Leeres Element log_in_with: 'Einloggen mit {provider}' +advanced_settings: Erweiterte Einstellungen advanced_filter: Erweiterter Filter delete_advanced_filter: Filter zurücksetzen change_advanced_filter_operator: Operator ändern @@ -495,6 +519,7 @@ operators: nnull: Ist nicht null contains: Enthält ncontains: Enthält nicht + starts_with: Beginnt mit nstarts_with: Beginnt nicht mit ends_with: Endet mit nends_with: Endet nicht mit @@ -504,6 +529,10 @@ operators: nempty: Ist nicht leer all: Enthält diese Schlüssel has: Enthält einige dieser Schlüssel + intersects: schneidet + nintersects: Schneidet nicht + intersects_bbox: Schneidet Begrenzung + nintersects_bbox: Schneidet nicht Begrenzung loading: Wird geladen... drop_to_upload: Zum Hochladen ablegen item: Element @@ -542,10 +571,12 @@ no_results_copy: Suchfilter anpassen oder löschen um Ergebnisse zu sehen. clear_filters: Filter löschen saves_automatically: Automatisch speichern role: Rolle +rule: Regel user: Benutzer no_presets: Keine Voreinstellungen no_presets_copy: Es wurden noch keine Voreinstellungen oder Lesezeichen gespeichert. no_presets_cta: Voreinstellung hinzufügen +presets_only: Nur Vorlagen create: Erstellen on_create: Beim Erstellen on_update: Beim Aktualisieren @@ -561,6 +592,7 @@ label: Label image_url: Bild-Url alt_text: Alternativ-Text media: Medien +quality: Qualität width: Breite height: Höhe source: Quelle @@ -692,6 +724,7 @@ no_layout_collection_selected_yet: Noch kein Layout oder keine Sammlung ausgewä batch_delete_confirm: >- Keine Elemente ausgewählt | Bist Du sicher, dass Du dieses Element löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden. | Bist Du sicher, dass Du diese {count} Elemente löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden. cancel: Abbrechen +no_upscale: Bilder nicht hochskalieren collection: Sammlung collections: Sammlungen singleton: Einzelnes Element @@ -755,27 +788,52 @@ fields: title: Titel description: Beschreibung tags: Stichwörter + user_preferences: Benutzereinstellungen language: Sprache theme: Design + theme_auto: Automatisch (Systemeinstellung verwenden) + theme_light: Heller Modus + theme_dark: Nachtmodus tfa_secret: Zwei-Faktor-Authentifizierung + admin_options: Admin-Optionen status: Status + status_draft: Entwurf + status_invited: Eingeladen status_active: Aktiv + status_suspended: Angehalten + status_archived: Archiviert role: Rolle token: Token + token_placeholder: Einen sicheren Zugangs-Token eingeben... last_page: Letzte Seite last_access: Letzter Zugriff directus_settings: + jpg: JPEG + png: PNG + webP: WebP + tiff: Tiff + basemaps_raster: Raster + basemaps_tile: Raster TileJSON + basemaps_style: Mapbox-Stil + mapbox_key: Mapbox Zugangs-Token + mapbox_placeholder: pk.eyJ1Ijo..... + transforms_note: Der Name der Sharp-Methode und die dazugehörigen Argumente. Auf https://sharp.pixelplumbing.com/api-constructor können weitere Informationen gefunden werden. + additional_transforms: Zusätzliche Umwandlungen project_name: Projektname project_url: Projekt URL project_color: Projekt Farbe project_logo: Projekt Logo + public_pages: Öffentliche Seiten public_foreground: Öffentlicher Vordergrund public_background: Öffentlicher Hintergrund public_note: Öffentliche Anmerkung auth_password_policy: Kennwortrichtlinie auth_login_attempts: Maximale Anmeldeversuche + files_and_thumbnails: Dateien & Vorschaubilder + storage_default_folder: Standardspeicherordner storage_asset_presets: Asset-Presets storage_asset_transform: Asset-Transformierung + overrides: App-Überschreibungen custom_css: Benutzerdefiniertes CSS directus_fields: collection: Sammlungsname @@ -798,12 +856,107 @@ fields: collection_list: Sammlungsnavigation directus_webhooks: name: Name + method: Methode status: Status + data: Daten + data_label: Ereignisdaten senden + triggers: Auslöser + actions: Aktionen field_options: + directus_settings: + project_name_placeholder: Mein Projekt... + project_logo_note: Login & Logo Hintergrund + public_note_placeholder: Eine kurze, öffentliche Nachricht welche die Formatierung mittels Markdown unterstützt... + security_divider_title: Sicherheit + auth_password_policy: + none_text: Keine - Nicht empfohlen + weak_text: Schwach - Minimum 8 Zeichen + strong_text: Stark - Groß / Klein / Zahlen / Spezialzeichen + storage_asset_presets: + fit: + contain_text: Enthalten (Seitenverhältnis beibehalten) + cover_text: Abdeckung (erzwingt exakte Grösse) + fit_text: Passform innen + outside_text: Passform außen + additional_transforms: Zusätzliche Umwandlungen + transforms_note: Der Name der Sharp-Methode und die dazugehörigen Argumente. Auf https://sharp.pixelplumbing.com/api-constructor können weitere Informationen gefunden werden. + mapbox_key: Mapbox Zugangs-Token + mapbox_placeholder: pk.eyJ1Ijo..... + basemaps_raster: Raster + basemaps_tile: Raster TileJSON + basemaps_style: Mapbox-Stil + files_divider_title: Dateien & Vorschaubilder + overrides_divider_title: App-Überschreibungen + directus_activity: + login: Login + create: Erstellen + update: Aktualisieren + delete: Löschen directus_collections: track_activity_revisions: Verfolge Aktivitäten und Änderungen only_track_activity: Nur Aktivitäten verfolgen do_not_track_anything: Nichts verfolgen + collection_setup: Einrichtung Sammlung + note_placeholder: Eine Beschreibung dieser Sammlung... + hidden_label: Innerhalb der App verstecken + singleton: Als einzelnes Objekt behandeln + language: Sprache + translation: Übersetzung eingeben... + archive_divider: Archivieren + archive_field: Wählen Sie ein Feld... + archive_app_filter: App-Archivfilter aktivieren + archive_value: Gesetzter Wert während der Archivierung... + unarchive_value: Gesetzter Wert während der Wiederherstellung... + divider: Sortieren + sort_field: Wählen Sie ein Feld... + accountability_divider: Verantwortung + directus_files: + title: Ein einzigartiger Titel... + description: Eine optionale Beschreibung... + location: Eine optionale Position... + storage_divider: Dateibenennung + filename_disk: Name auf Festplatte... + filename_download: Name während des Downloads... + directus_roles: + name: Der einzigartige Name für diese Rolle... + description: Eine Beschreibung von dieser Rolle... + ip_access: Erlaubte IP-Adressen hinzufügen, leer lassen um alle zuzulassen... + fields: + icon_name: Symbol + name_name: Name + name_placeholder: Titel eingeben... + link_name: Link + link_placeholder: Relative oder absolute URL... + collection_list: + group_name_addLabel: Neue Gruppe hinzufügen... + fields: + group_name: Gruppen-Name + group_placeholder: Diese Gruppe benennen... + type_name: Typ + choices_always: Immer geöffnet + choices_start_collapsed: Eingeklappt starten + collections_name: Sammlungen + collections_addLabel: Sammlung hinzufügen... + directus_users: + preferences_divider: Benutzereinstellungen + dropdown_auto: Automatisch (Systemeinstellung verwenden) + dropdown_light: Heller Modus + dropdown_dark: Nachtmodus + admin_divider: Admin-Optionen + status_dropdown_draft: Entwurf + status_dropdown_invited: Eingeladen + status_dropdown_active: Aktiv + status_dropdown_suspended: Angehalten + status_dropdown_archived: Archiviert + token: Einen sicheren Zugangs-Token eingeben... + directus_webhooks: + status_options_active: Aktiv + status_options_inactive: Inaktiv + data_label: Ereignisdaten senden + triggers_divider: Auslöser + actions_create: Erstellen + actions_update: Aktualisieren + actions_delete: Löschen no_fields_in_collection: 'Es gibt noch keine Felder in "{collection}"' do_nothing: Keine Aktion generate_and_save_uuid: UUID generieren und speichern @@ -821,10 +974,13 @@ referential_action_cascade: Lösche das {collection} Element (Cascade) referential_action_set_null: '{field} auf Null setzen' referential_action_set_default: '{field} au Standardwert setzen' choose_action: Aktion auswählen +continue_label: Weiter continue_as: >- {name} ist derzeit authentifiziert. Wenn du dieses Konto erkennst, drücke fortfahren. editing_role: '{role} Rolle' creating_webhook: Webhook erstellen +default_label: Standard +delete_label: Löschen delete_are_you_sure: >- Diese Aktion kann nicht rückgängig gemacht werden. Sind Sie sicher, dass Sie fortfahren möchten? delete_field_are_you_sure: >- @@ -876,11 +1032,17 @@ sort_direction: Sortierreihenfolge sort_asc: Aufsteigend sortieren sort_desc: Absteigend sortieren template: Vorlage +require_value_to_be_set: Wert muss gesetzt werden translation: Übersetzung value: Wert view_project: Projekt anzeigen report_error: Fehler melden +start: Start interfaces: + group-accordion: + name: Akkordeon + description: Felder oder Gruppen als Akkordeonabschnitte anzeigen + start: Start presentation-links: presentation-links: Button-Links links: Links @@ -1184,3 +1346,5 @@ layouts: calendar: Kalender start_date_field: Datumsfeld Start end_date_field: Datumsfeld Ende + map: + field: Dimensionen diff --git a/app/src/lang/translations/el-GR.yaml b/app/src/lang/translations/el-GR.yaml index 3243563ac0..93aae12c6c 100644 --- a/app/src/lang/translations/el-GR.yaml +++ b/app/src/lang/translations/el-GR.yaml @@ -225,6 +225,21 @@ fields: directus_webhooks: name: Όνομα status: Κατάσταση +field_options: + directus_activity: + create: Δημιουργία + update: Ενημέρωση + delete: Διαγραφή + directus_collections: + archive_divider: Αρχειοθέτηση + directus_roles: + fields: + icon_name: Εικονίδιο + name_name: Όνομα + collections_name: Συλλογές + directus_webhooks: + actions_create: Δημιουργία + actions_update: Ενημέρωση comment: Σχόλιο delete_field_are_you_sure: >- Είσαι σίγουρος ότι θέλεις να διαγράψεις το πεδίο "{field}"; Η ενέργεια αυτή δεν μπορεί να αναιρεθεί. diff --git a/app/src/lang/translations/en-US.yaml b/app/src/lang/translations/en-US.yaml index 03e3859565..07c0ffb751 100644 --- a/app/src/lang/translations/en-US.yaml +++ b/app/src/lang/translations/en-US.yaml @@ -20,6 +20,9 @@ # 'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', # 'Proxy', 'Intl' +published: Published +draft: Draft +archived: Archived edit_field: Edit Field conditions: Conditions maps: Maps @@ -39,6 +42,7 @@ role_name: Role Name branch: Branch leaf: Leaf indeterminate: Indeterminate +edit_collection: Edit Collection exclusive: Exclusive children: Children db_only_click_to_configure: 'Database Only: Click to Configure ' @@ -146,9 +150,9 @@ fields_for_role: 'Fields the {role} Role can {action}.' validation_for_role: 'Field {action} rules the {role} Role must obey.' presets_for_role: 'Field value defaults for the {role} Role.' presentation_and_aliases: Presentation & Aliases -revision_post_update: Here is what this item looked like after the update... -changes_made: These are the specific changes that were made... -no_relational_data: Keep in mind that this does not include relational data. +revision_post_update: Here is what this item looked like after the update. +changes_made: Below are the specific changes made in this revision. +no_relational_data: Keep in mind that relational data is not included here. hide_field_on_detail: Hide Field on Detail show_field_on_detail: Show Field on Detail delete_field: Delete Field @@ -387,7 +391,7 @@ no_files_copy: There are no files here. user_count: 'No Users | One User | {count} Users' no_users_copy: There are no users in this role yet. webhooks_count: 'No Webhooks | One Webhook | {count} Webhooks' -no_webhooks_copy: There are no webhooks yet. +no_webhooks_copy: No webhooks have been configured yet. Get started by creating one below. all_items: All Items any: Any csv: CSV @@ -736,11 +740,28 @@ batch_delete_confirm: >- No items have been selected | Are you sure you want to delete this item? This action can not be undone. | Are you sure you want to delete these {count} items? This action can not be undone. cancel: Cancel +no_upscale: Don't upscale images collection: Collection collections: Collections singleton: Singleton singleton_label: Treat as single object system_fields_locked: System fields are locked and can't be edited +directus_collection: + directus_activity: Accountability logs for all events + directus_collections: Additional collection configuration and metadata + directus_fields: Additional field configuration and metadata + directus_files: Metadata for all managed file assets + directus_folders: Provides virtual directories for files + directus_migrations: What version of the database you're using + directus_permissions: Access permissions for each role + directus_presets: Presets for collection defaults and bookmarks + directus_relations: Relationship configuration and metadata + directus_revisions: Data snapshots for all activity + directus_roles: Permission groups for system users + directus_sessions: User session information + directus_settings: Project configuration options + directus_users: System users for the platform + directus_webhooks: Configuration for event-based HTTP requests fields: directus_activity: item: Item Primary Key @@ -809,7 +830,7 @@ fields: admin_options: Admin Options status: Status status_draft: Draft - status_invited: Inactive + status_invited: Invited status_active: Active status_suspended: Suspended status_archived: Archived @@ -819,6 +840,17 @@ fields: last_page: Last Page last_access: Last Access directus_settings: + jpg: JPEG + png: PNG + webP: WebP + tiff: Tiff + basemaps_raster: Raster + basemaps_tile: Raster TileJSON + basemaps_style: Mapbox Style + mapbox_key: Mapbox Access Token + mapbox_placeholder: pk.eyJ1Ijo..... + transforms_note: The Sharp method name and its arguments. See https://sharp.pixelplumbing.com/api-constructor for more information. + additional_transforms: Additional Transformations project_name: Project Name project_url: Project URL project_color: Project Color @@ -863,11 +895,104 @@ fields: triggers: Triggers actions: Actions field_options: + directus_settings: + project_name_placeholder: My project... + project_logo_note: Login & Logo Background + public_note_placeholder: A short, public message that supports markdown formatting... + security_divider_title: Security + auth_password_policy: + none_text: None – Not Recommended + weak_text: Weak – Minimum 8 Characters + strong_text: Strong – Upper / Lowercase / Numbers / Special + storage_asset_presets: + fit: + contain_text: Contain (preserve aspect ratio) + cover_text: Cover (forces exact size) + fit_text: Fit inside + outside_text: Fit outside + additional_transforms: Additional Transformations + transforms_note: The Sharp method name and its arguments. See https://sharp.pixelplumbing.com/api-constructor for more information. + mapbox_key: Mapbox Access Token + mapbox_placeholder: pk.eyJ1Ijo..... + basemaps_raster: Raster + basemaps_tile: Raster TileJSON + basemaps_style: Mapbox Style + files_divider_title: Files & Thumbnails + overrides_divider_title: App Overrides + directus_activity: + login: Login + create: Create + update: Update + delete: Delete directus_collections: track_activity_revisions: Track Activity & Revisions only_track_activity: Only Track Activity do_not_track_anything: Do Not Track Anything + collection_setup: Collection Setup + note_placeholder: A description of this collection... + hidden_label: Hide within the App + singleton: Treat as single object + language: Language + translation: Enter a translation... + archive_divider: Archive + archive_field: Choose a field... + archive_app_filter: Enable App Archive Filter + archive_value: Value set when archiving... + unarchive_value: Value set when unarchiving... + divider: Sort + sort_field: Choose a field... + accountability_divider: Accountability + directus_files: + title: A unique title... + description: An optional description... + location: An optional location... + storage_divider: File Naming + filename_disk: Name on disk storage... + filename_download: Name when downloading... + directus_roles: + name: The unique name for this role... + description: A description of this role... + ip_access: Add allowed IP addresses, leave empty to allow all... + fields: + icon_name: Icon + name_name: Name + name_placeholder: Enter a title... + link_name: Link + link_placeholder: Relative or absolute URL... + collection_list: + group_name_addLabel: Add New Group... + fields: + group_name: Group Name + group_placeholder: Label this group... + type_name: Type + choices_always: Always Open + choices_start_open: Start Open + choices_start_collapsed: Start Collapsed + collections_name: Collections + collections_addLabel: Add Collection... + directus_users: + preferences_divider: User Preferences + dropdown_auto: Automatic (Based on System) + dropdown_light: Light Mode + dropdown_dark: Dark Mode + admin_divider: Admin Options + status_dropdown_draft: Draft + status_dropdown_invited: Invited + status_dropdown_active: Active + status_dropdown_suspended: Suspended + status_dropdown_archived: Archived + token: Enter a secure access token... + directus_webhooks: + status_options_active: Active + status_options_inactive: Inactive + data_label: Send Event Data + triggers_divider: Triggers + actions_create: Create + actions_update: Update + actions_delete: Delete + actions_login: Login no_fields_in_collection: 'There are no fields in "{collection}" yet' +no_value: No value do_nothing: Do Nothing generate_and_save_uuid: Generate and Save UUID save_current_user_id: Save Current User ID @@ -1309,6 +1434,9 @@ layouts: cluster_radius: Cluster radius cluster_minpoints: Cluster minimum size cluster_maxzoom: Maximum zoom for clustering - fit_data: Fit data to view bounds field: Geometry invalid_geometry: Invalid geometry + auto_location_filter: Always filter data to view bounds + search_this_area: Search this area + clear_data_filter: Clear Data Filter + clear_location_filter: Clear Location Filter diff --git a/app/src/lang/translations/es-419.yaml b/app/src/lang/translations/es-419.yaml index 9f09acc955..abe2f3062e 100644 --- a/app/src/lang/translations/es-419.yaml +++ b/app/src/lang/translations/es-419.yaml @@ -87,11 +87,11 @@ archive_confirm_count: >- Ningún registro seleccionado | ¿Estas seguro de querer archivar este registro? | ¿Estas seguro de querer archivar estos {count} registros? reset_system_permissions_to: 'Restablecer los permisos del sistema a:' reset_system_permissions_copy: Esta acción sobrescribirá cualquier permiso personalizado que haya aplicado a las colecciones del sistema. ¿Está seguro? -the_following_are_minimum_permissions: Los siguientes son permisos mínimos requeridos cuando "Acceso a App" está habilitado. Puede extender permisos más allá de esto, pero no más abajo. +the_following_are_minimum_permissions: Estos son los permisos mínimos requeridos cuando "Acceso a la App" está activado. Puedes ampliar los permisos un poco más, pero no menos que esto. app_access_minimum: Minimo Acceso a la Aplicación recommended_defaults: Recomendado preestablecidos unarchive: Desarchivar -unarchive_confirm: '¿Realmente desea desarchivar este elemento?' +unarchive_confirm: '¿Está seguro que desea desarchivar este elemento?' nested_files_folders_will_be_moved: Los archivos y directorios anidados serán movidos a un nivel superior. unknown_validation_errors: 'Ocurrieron errores de validación para los siguientes campos ocultos:' validationError: @@ -117,7 +117,7 @@ no_access: Sin acceso use_custom: Usar personalizado nullable: Deshabilitable allow_null_value: Permitir valor NULL -enter_value_to_replace_nulls: Por favor, introduzca un nuevo valor para reemplazar cualquier valor NULO actualmente dentro de este campo. +enter_value_to_replace_nulls: Ingrese un nuevo valor para reemplazar cualquier valor NULL que se encuentre actualmente en este campo. field_standard: Standar field_presentation: Presentación y Alias field_file: Un solo Archivo @@ -134,18 +134,18 @@ field_presets: Predefinidos para Campo permissions_for_role: 'Elementos que el Rol {role} puede {action}.' fields_for_role: 'Campos que el Rol {role} puede {action}.' validation_for_role: 'Reglas de campo al {action} que el Rol {role} tiene que obedecer.' -presets_for_role: 'Valores por defecto del Campo para el Rol {role}.' +presets_for_role: 'Valor del campo por defecto para el rol {role}.' presentation_and_aliases: Presentación y Alias -revision_post_update: Así se vería el elemento después de la actualización... +revision_post_update: Así es como se vería este artículo después de la actualización... changes_made: Estos son los cambios específicos que se hicieron... -no_relational_data: Tener en cuenta que esto no incluye datos relacionales. -hide_field_on_detail: Ocultar campo en el Detalle -show_field_on_detail: Mostrar campo en el Detalle +no_relational_data: Ten en cuenta que esto no incluye datos relacionales. +hide_field_on_detail: Oculto en Detalle +show_field_on_detail: Mostrar en Detalle delete_field: Borrar Campo fields_and_layout: Campos y Diseño field_create_success: 'Campo Creado: "{field}"' field_update_success: 'Campo Actualizado: "{field}"' -duplicate_where_to: '¿A dónde le gustaría duplicar este campo?' +duplicate_where_to: '¿Dónde le gustaría duplicar este campo?' language: Idioma global: Global admins_have_all_permissions: Los Administradores tienen todos los permisos @@ -158,9 +158,9 @@ schema_setup_key: Nombre de columna de este campo de la base de datos y clave AP create_field: Crear campo creating_new_field: 'Nuevo campo ({collection})' field_in_collection: '{field} ({collection})' -reset_page_preferences: Restablecer Preferencias de Página +reset_page_preferences: Reestablecer Preferencias de la Página hidden_field: Campo oculto -hidden_on_detail: Oculto en el Detalle +hidden_on_detail: Oculto en Detalle disabled_editing_value: Deshabilitar la edición del valor key: Llave alias: Alias @@ -184,12 +184,12 @@ create_translations: Crear traducción auto_refresh: Actualizar automáticamente refresh_interval: Intervalo de actualización no_refresh: No actualizar -refresh_interval_seconds: Refrescar Instantáneamente | Cada Segundo | Cada {seconds} Segundos +refresh_interval_seconds: Actualizar al Instante | Cada Segundo | Cada {seconds} Segundos refresh_interval_minutes: Cada Minuto | Cada {minutes} Minutos auto_generate: Auto Generado this_will_auto_setup_fields_relations: Esto configurará automáticamente todos los campos y relaciones requeridas. click_here: Clic aquí -to_manually_setup_translations: configurar manualmente las traducciones. +to_manually_setup_translations: para configurar manualmente las traducciones. click_to_manage_translated_fields: >- Aún no hay campos traducidos. Haga clic aquí para crearlos. | Hay un campo traducido. Haga clic aquí para administrarlo. | Hay {count} campos traducidos. Haga clic aquí para administrarlos. fields_group: Grupo de Campos @@ -233,7 +233,7 @@ raw_value: Valor sin interpretar edit_raw_value: Editar Valor Plano enter_raw_value: Ingresar valor plano... clear_value: Limpiar valor -reset_to_default: Restablecer ajustes +reset_to_default: Restablecer ajustes por defecto undo_changes: Deshacer cambios notifications: Notificaciones show_all_activity: Mostrar Toda La Actividad @@ -819,6 +819,32 @@ field_options: track_activity_revisions: Rastrear actividad y revisiones only_track_activity: Sólo seguimiento de actividad do_not_track_anything: No realizar seguimiento + collection_setup: Configuración de Colección + singleton: Tratar como un sólo objeto + language: Idioma + archive_divider: Archivar + divider: Ordenar + directus_activity: + login: Iniciar Sesión + create: Crear + update: Actualización + delete: Eliminar + directus_roles: + fields: + icon_name: Ícono + name_name: Nombre + name_placeholder: Ingresar un título... + collection_list: + fields: + type_name: Tipo + collections_name: Colecciones + directus_users: + status_dropdown_active: Activo + directus_webhooks: + status_options_active: Activo + actions_create: Crear + actions_update: Actualización + actions_delete: Eliminar no_fields_in_collection: 'Aún no hay campos en "{collection}"' do_nothing: No hacer nada generate_and_save_uuid: Generar y Guardar UUID diff --git a/app/src/lang/translations/es-CL.yaml b/app/src/lang/translations/es-CL.yaml index d6ac80d33a..7e017d8117 100644 --- a/app/src/lang/translations/es-CL.yaml +++ b/app/src/lang/translations/es-CL.yaml @@ -815,10 +815,36 @@ fields: name: Nombre status: Estado field_options: + directus_activity: + login: Iniciar sesión + create: Crear + update: Actualizar + delete: Eliminar directus_collections: track_activity_revisions: Rastrear actividad y revisiones only_track_activity: Sólo seguimiento de actividad do_not_track_anything: No realizar seguimiento + collection_setup: Configuración de Colección + singleton: Tratar como un sólo objeto + language: Idioma + archive_divider: Archivar + divider: Ordenar + directus_roles: + fields: + icon_name: Icono + name_name: Nombre + name_placeholder: Ingresar un título... + collection_list: + fields: + type_name: Tipo + collections_name: Colecciones + directus_users: + status_dropdown_active: Activo + directus_webhooks: + status_options_active: Activo + actions_create: Crear + actions_update: Actualizar + actions_delete: Eliminar no_fields_in_collection: 'Aún no hay campos en "{collection}"' do_nothing: No hacer nada generate_and_save_uuid: Generar y Guardar UUID diff --git a/app/src/lang/translations/es-ES.yaml b/app/src/lang/translations/es-ES.yaml index 65eb4c4ee2..c96ce53744 100644 --- a/app/src/lang/translations/es-ES.yaml +++ b/app/src/lang/translations/es-ES.yaml @@ -814,10 +814,36 @@ fields: name: Nombre status: Estado field_options: + directus_activity: + login: Iniciar Sesión + create: Crear + update: Actualización + delete: Eliminar directus_collections: track_activity_revisions: Rastrear actividad y revisiones only_track_activity: Sólo seguimiento de actividad do_not_track_anything: No realizar seguimiento + collection_setup: Configuración de Colección + singleton: Tratar como un sólo objeto + language: Idioma + archive_divider: Archivar + divider: Ordenar + directus_roles: + fields: + icon_name: Ícono + name_name: Nombre + name_placeholder: Ingresar un título... + collection_list: + fields: + type_name: Tipo + collections_name: Colecciones + directus_users: + status_dropdown_active: Activo + directus_webhooks: + status_options_active: Activo + actions_create: Crear + actions_update: Actualización + actions_delete: Eliminar no_fields_in_collection: 'Aún no hay campos en "{collection}"' do_nothing: No hacer nada generate_and_save_uuid: Generar y Guardar UUID diff --git a/app/src/lang/translations/et-EE.yaml b/app/src/lang/translations/et-EE.yaml index 9d66eb2f70..a8f89315c8 100644 --- a/app/src/lang/translations/et-EE.yaml +++ b/app/src/lang/translations/et-EE.yaml @@ -662,7 +662,7 @@ page_help_docs_global: >- page_help_files_collection: >- **Failikogu* - Näitab kõiki faile ja pilte selles projektis. Muuta saab vaateid, filtreid jm ning lisada järjehoidjaid page_help_files_item: >- - **Faili detailid* - Vorm, milles saab muuta faili metaandmeid, ligipääse jm + **Faili detailid* - Vorm, milles saab muuta faili metaandmeid, ligipääse jm page_help_settings_project: "**Projekti seaded** - Sinu projekti üldised seaded" page_help_settings_datamodel_collections: >- **Andmekogud** - Näitab kõiki andmekogusid (sh süsteemseid) @@ -797,10 +797,36 @@ fields: name: Nimi status: Staatus field_options: + directus_activity: + login: Logi sisse + create: Loo + update: Uuenda + delete: Kustuta directus_collections: track_activity_revisions: Salvesta muudatuste statistika only_track_activity: Jälgi ainult aktiivsust do_not_track_anything: Ära jälgi midagi + collection_setup: Andmekogu seadistus + singleton: Käsitle üksiku objektina + language: Keel + archive_divider: Arhiiv + divider: Sorteeri + directus_roles: + fields: + icon_name: Ikoon + name_name: Nimi + name_placeholder: Sisesta pealkiri... + collection_list: + fields: + type_name: Tüüp + collections_name: Kogud + directus_users: + status_dropdown_active: aktiivne + status_dropdown_archived: Arhiveeritud + directus_webhooks: + status_options_active: aktiivne + actions_create: Loo + actions_update: Uuenda no_fields_in_collection: 'Andmekogus "{collection}" puuduvad kirjed' do_nothing: Ära tee midagi generate_and_save_uuid: Genereeri ja salvesta UUID diff --git a/app/src/lang/translations/fi-FI.yaml b/app/src/lang/translations/fi-FI.yaml index ea47a52f0d..fa6bc294be 100644 --- a/app/src/lang/translations/fi-FI.yaml +++ b/app/src/lang/translations/fi-FI.yaml @@ -794,10 +794,35 @@ fields: name: Nimi status: Tila field_options: + directus_activity: + login: Kirjaudu sisään + create: Luo + update: Päivitä + delete: Poista directus_collections: track_activity_revisions: Seuraa tapahtumia ja versioita only_track_activity: Seuraa vain tapahtumia do_not_track_anything: Älä seuraa mitään + collection_setup: Kokoelman asetukset + singleton: Käsittele yhtenä objektina + language: Kieli + archive_divider: Arkistoi + divider: Järjestä + directus_roles: + fields: + icon_name: Kuvake + name_name: Nimi + name_placeholder: Syötä otsikko... + collection_list: + fields: + type_name: Tyyppi + collections_name: Kokoelmat + directus_users: + status_dropdown_active: Aktiivinen + directus_webhooks: + status_options_active: Aktiivinen + actions_create: Luo + actions_update: Päivitä no_fields_in_collection: 'Kokoelmassa "{collection}" ei ole vielä kenttiä' do_nothing: Älä tee mitään generate_and_save_uuid: Luo ja tallenna UUID diff --git a/app/src/lang/translations/fr-FR.yaml b/app/src/lang/translations/fr-FR.yaml index 7ec726b715..986c2f1420 100644 --- a/app/src/lang/translations/fr-FR.yaml +++ b/app/src/lang/translations/fr-FR.yaml @@ -21,10 +21,12 @@ #'Proxy', 'Intl' edit_field: Modifier le champ conditions: Conditions +maps: Cartes item_revision: Historique de l'article duplicate_field: Dupliquer le champ half_width: Demi-colonne full_width: Largeur de colonne +limit: Limite group: Groupe and: Et or: Ou @@ -71,6 +73,7 @@ delete_bookmark_copy: >- logoutReason: SIGN_OUT: Déconnecté SESSION_EXPIRED: Votre session a expiré +public_label: Public public_description: Contrôle quelles sont les données API disponibles sans authentification. not_allowed: Non autorisé directus_version: Version de Directus @@ -116,6 +119,8 @@ no_access: Aucun accès use_custom: Personnalisé nullable: Peut être nul allow_null_value: Autoriser la valeur NULL +allow_multiple: Autoriser Plusieurs +allow_multiple_to_be_open: Autoriser l'ouverture de plusieurs éléments enter_value_to_replace_nulls: Veuillez entrer une nouvelle valeur pour remplacer les NULL actuellement dans ce champ. field_standard: Standard field_presentation: Présentation & Alias @@ -179,6 +184,7 @@ time: Date et heure timestamp: Horodatage uuid: UUID (IDentifiant Unique Universel) hash: Hash +geometry: Géométrie not_available_for_type: Non disponible pour ce type create_translations: Créer des traductions auto_refresh: Actualisation automatique @@ -309,6 +315,7 @@ drag_mode: Mode Glisser/Déposer cancel_crop: Annuler le recadrage original: Initial url: URL +import_label: Importer file_details: Détails du fichier dimensions: Dimensions size: Taille @@ -376,6 +383,7 @@ no_users_copy: Il n'y a pas encore d'utilisateurs avec ce rôle. webhooks_count: 'Aucun Webhooks | 1 Webhook | {count} Webhooks' no_webhooks_copy: Il n'y a pas encore de webhooks. all_items: Tous les éléments +any: N'importe quel csv: CSV no_collections: Aucune collection create_collection: Créer une collection @@ -386,6 +394,7 @@ display_template_not_setup: L'option d'affichage du modèle est mal configurée collection_field_not_setup: L'option de champ de collection est mal configurée select_a_collection: Sélectionner une collection active: Actif +inactive: Inactif users: Utilisateurs activity: Activité webhooks: Webhooks @@ -398,6 +407,7 @@ documentation: Documentation sidebar: Barre latérale duration: Durée charset: Jeu de caractères +second: seconde file_moved: Fichier déplacé collection_created: Collection créée modified_on: Modifié le @@ -436,8 +446,10 @@ errors: USER_SUSPENDED: Utilisateur suspendu CONTAINS_NULL_VALUES: Le champ contient des valeurs nulles UNKNOWN: Erreur inconnue + UNPROCESSABLE_ENTITY: Cette entité ne peut être traitée INTERNAL_SERVER_ERROR: Erreur inconnue NOT_NULL_VIOLATION: La valeur ne peut pas être nulle +security: Sécurité value_hashed: Valeur hashée de manière sûre bookmark_name: Nom du favori... create_bookmark: Ajouter comme favori @@ -489,6 +501,7 @@ color: Couleur circle: Cercle empty_item: Élément vide log_in_with: 'Se connecter avec {provider}' +advanced_settings: Paramètres Avancés advanced_filter: Filtre avancé delete_advanced_filter: Supprimer le filtre change_advanced_filter_operator: Changer d'opérateur @@ -515,6 +528,10 @@ operators: nempty: N'est pas vide all: Contient ces clés has: Contient certaines de ces clés + intersects: Intersectes + nintersects: Ne se croise pas + intersects_bbox: Boîte englobante d'intersectes + nintersects_bbox: Ne pas intersecter la boîte englobante loading: Chargement… drop_to_upload: Déposer pour téléverser item: Élément @@ -558,6 +575,7 @@ user: Utilisateur no_presets: Aucun préréglage no_presets_copy: Aucun préréglage ou favori n'a encore été enregistré. no_presets_cta: Ajouter un préréglage +presets_only: Préréglages uniquement create: Créer on_create: À la Création on_update: À la mise à jour @@ -573,6 +591,7 @@ label: Étiquette image_url: Url de l’image alt_text: Texte alternatif media: Média +quality: Qualité width: Largeur height: Hauteur source: Source @@ -767,27 +786,50 @@ fields: title: Titre description: Description tags: Étiquettes + user_preferences: Préférences utilisateur language: Langue theme: Thème + theme_auto: Automatique (Basé sur le système) + theme_light: Mode Clair + theme_dark: Mode sombre tfa_secret: Authentification à deux facteurs + admin_options: Options d'Administration status: État + status_draft: Brouillon + status_invited: Invité·e status_active: Actif + status_suspended: Suspendu + status_archived: Archivé role: Rôle token: Token + token_placeholder: Entrez un jeton d'accès sécurisé... last_page: Dernière page last_access: Dernier accès directus_settings: + jpg: JPEG + png: PNG + webP: WebP + tiff: Tiff + basemaps_style: Style de Mapbox + mapbox_key: Jeton d'accès Mapbox + mapbox_placeholder: pk.eyJ1Ijo..... + transforms_note: Le nom de la méthode Sharp et ses arguments. Voir https://sharp.pixelplumbing.com/api-constructor pour plus d'informations. + additional_transforms: Transformations supplémentaires project_name: Nom du projet project_url: URL du projet project_color: Couleur du projet project_logo: Logo du projet + public_pages: Pages Publiques public_foreground: Image au premier plan de l'accueil public_background: Image en arrière-plan de l'accueil public_note: Message sur la page d'accueil auth_password_policy: Politique des mots de passe auth_login_attempts: Tentatives de connexion + files_and_thumbnails: Fichiers et miniatures + storage_default_folder: Dossier de stockage par défaut storage_asset_presets: Préréglages des fichiers dans le stockage storage_asset_transform: Transformation des fichiers du stockage + overrides: Remplacer les applications custom_css: Personnalisation du CSS directus_fields: collection: Nom de la Collection @@ -810,12 +852,96 @@ fields: collection_list: Navigation de la collection directus_webhooks: name: Nom + method: Méthode status: État + data: Données + data_label: Envoyer les données de l'événement + triggers: Déclencheurs + actions: Actions field_options: + directus_settings: + project_name_placeholder: Mon projet... + project_logo_note: Connexion et arrière-plan du logo + public_note_placeholder: Un message public court qui prend en charge le formatage des démarques... + security_divider_title: Sécurité + auth_password_policy: + weak_text: Faible - 8 caractères minimum + additional_transforms: Transformations supplémentaires + transforms_note: Le nom de la méthode Sharp et ses arguments. Voir https://sharp.pixelplumbing.com/api-constructor pour plus d'informations. + mapbox_key: Jeton d'accès Mapbox + mapbox_placeholder: pk.eyJ1Ijo..... + basemaps_style: Style de Mapbox + files_divider_title: Fichiers et miniatures + overrides_divider_title: Remplacer les applications + directus_activity: + login: Se connecter + create: Créer + update: Mettre à jour + delete: Supprimer directus_collections: track_activity_revisions: Enregistrer les activités et l'historique only_track_activity: Enregistrer seulement l'activité do_not_track_anything: Ne rien enregistrer + collection_setup: Configuration de la collection + note_placeholder: Une description de cette collection... + hidden_label: Cacher dans l'application + singleton: Est un objet unique + language: Langue + translation: Entrez une nouvelle traduction... + archive_divider: Archiver + archive_field: Choisissez un champ... + archive_app_filter: Activer le filtre des archives d'applications + archive_value: Valeur définie lors de l'archivage... + unarchive_value: Valeur définie lors de la désarchivage... + divider: Trier + sort_field: Choisissez un champ... + accountability_divider: Responsabilité + directus_files: + title: Un titre unique... + description: Une description optionnelle... + location: Un emplacement optionnel... + storage_divider: Nommage des fichiers + filename_disk: Nom sur le stockage disque... + filename_download: Nom lors du téléchargement... + directus_roles: + name: Le nom unique de ce rôle... + description: Une description pour ce rôle... + ip_access: Ajoutez des adresses IP autorisées, laissez vide pour tout autoriser... + fields: + icon_name: Icône + name_name: Nom + name_placeholder: Entrez un titre... + link_name: Lien + link_placeholder: URL relative ou absolue... + collection_list: + group_name_addLabel: Ajouter un nouveau groupe... + fields: + group_name: Nom du groupe + group_placeholder: Étiquetez ce groupe... + type_name: Type + choices_always: Toujours ouvert + collections_name: Collections + directus_users: + preferences_divider: Préférences utilisateur + dropdown_auto: Automatique (Basé sur le système) + dropdown_light: Mode Clair + dropdown_dark: Mode sombre + admin_divider: Options d'Administration + status_dropdown_draft: Brouillon + status_dropdown_invited: Invité·e + status_dropdown_active: Actif + status_dropdown_suspended: Suspendu + status_dropdown_archived: Archivé + token: Entrez un jeton d'accès sécurisé... + directus_webhooks: + status_options_active: Actif + status_options_inactive: Inactif + data_label: Envoyer les données de l'événement + triggers_divider: Déclencheurs + actions_create: Créer + actions_update: Mettre à jour + actions_delete: Supprimer + actions_login: Identifiant no_fields_in_collection: 'Il n''y a pas encore de champs dans "{collection}"' do_nothing: Ne rien faire generate_and_save_uuid: Générer et Enregistrer l'UUID @@ -833,10 +959,13 @@ referential_action_cascade: Supprimer l'objet {collection} (cascade) referential_action_set_null: Vider le champ {field} referential_action_set_default: Remettre {field} à sa valeur par défaut choose_action: Choisir une action +continue_label: Continuer continue_as: >- {name} est actuellement authentifié. Si vous reconnaissez ce compte, cliquez sur continuer. editing_role: 'Rôle {role}' creating_webhook: Création du Webhook +default_label: Défaut +delete_label: Supprimer delete_are_you_sure: >- Cette action est permanente et ne peut pas être annulée. Êtes-vous sûr de vouloir continuer ? delete_field_are_you_sure: >- @@ -888,11 +1017,22 @@ sort_direction: Ordre de tri sort_asc: Tri croissant sort_desc: Tri décroissant template: Modèle +require_value_to_be_set: Nécessite que la valeur soit définie translation: Traduction value: Valeur view_project: Voir le projet report_error: Signaler l'erreur +start: Début interfaces: + group-accordion: + name: Accordéon + description: Afficher les champs ou les groupes en tant que sections d'accordéon + start: Début + all_closed: Tous fermés + first_opened: Premier consulté + all_opened: Tous ouverts + accordion_mode: Mode Accordion + max_one_section_open: Max 1 section ouverte presentation-links: presentation-links: Bouton de liens links: Liens @@ -913,6 +1053,7 @@ interfaces: description: Choisissez entre plusieurs options via des cases à cocher imbriquées value_combining: Combinaison de valeurs value_combining_note: Contrôle la valeur stockée lorsque des sélections imbriquées sont faites. + show_all: Tout afficher input-code: code: Code description: Écrire ou partager des extraits de code @@ -1007,7 +1148,9 @@ interfaces: imageToken: Token d'image imageToken_label: Quel token (statique) ajouter aux sources d'images map: + map: Carte zoom: Zoom + native: Natif presentation-notice: notice: Remarque description: Afficher une courte remarque @@ -1098,6 +1241,8 @@ interfaces: value_path: Chemin de la valeur trigger: Déclencheur rate: Débit + group-raw: + name: Groupe brut displays: boolean: boolean: Booléen @@ -1204,3 +1349,6 @@ layouts: calendar: Calendrier start_date_field: Champ Date de début end_date_field: Champ Date de fin + map: + map: Carte + field: Géométrie diff --git a/app/src/lang/translations/hi-IN.yaml b/app/src/lang/translations/hi-IN.yaml index 2adeff2c5e..43b7d5a80f 100644 --- a/app/src/lang/translations/hi-IN.yaml +++ b/app/src/lang/translations/hi-IN.yaml @@ -240,6 +240,19 @@ fields: name: भूमिका नाम directus_webhooks: name: नाम +field_options: + directus_activity: + delete: डिलीट + directus_collections: + language: भाषा + archive_divider: आर्काइव + divider: सॉर्ट + directus_roles: + fields: + name_name: नाम + directus_webhooks: + actions_create: Create + actions_update: Update interfaces: presentation-links: primary: प्राइमरी diff --git a/app/src/lang/translations/hu-HU.yaml b/app/src/lang/translations/hu-HU.yaml index 240ac31938..f20d9a4e7e 100644 --- a/app/src/lang/translations/hu-HU.yaml +++ b/app/src/lang/translations/hu-HU.yaml @@ -20,22 +20,41 @@ #'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', #'Proxy', 'Intl' edit_field: Mező szerkesztése +conditions: Feltételek +maps: Térképek +item_revision: Nézet revíziója duplicate_field: Mező másolása half_width: Fél szélesség full_width: Teljes szélesség +limit: Korlát +group: Csoport +and: És +or: Vagy fill_width: Teret kitöltő field_name_translations: Lefordított mezőnevek -enter_password_to_enable_tfa: Írd be a jelszót a kétfaktoros ellenőrzés bekapcsolásához -add_field: Új mező +enter_password_to_enable_tfa: Írja be jelszavát a kétfaktoros ellenőrzés bekapcsolásához +add_field: Mező Hozzáadása role_name: Szerepkör neve +branch: Ág +leaf: Falevél / Leaf +indeterminate: Meghatározatlan +edit_collection: Gyűjtemény szerkesztése +exclusive: Kizárólagos +children: Gyermekek +db_only_click_to_configure: 'Csak az adatbázisban szerepel: Klikkeljen a beállításához ' show_archived_items: Archivált elemek megjelenítése +edited: Érték szerkesztve required: Kötelező -requires_value: Kötelező adat +required_for_app_access: Alkalmazás-hozzáféréshez szükséges +requires_value: Kötelező érték +create_preset: Alapértelmezés létrehozása create_role: Szerepkör létrehozása create_user: Felhasználó létrehozása create_webhook: Webhook létrehozása invite_users: Felhasználók meghívása +email_examples: "admin{'@'}example.com, user{'@'}example.com..." invite: Meghívás +email_already_invited: Az "{email}" e-mail címet már meghívták emails: Emailek connection_excellent: Kiváló kapcsolat connection_good: Jó kapcsolat @@ -55,6 +74,7 @@ delete_bookmark_copy: >- logoutReason: SIGN_OUT: Kijelentkezve SESSION_EXPIRED: A munkamenet lejárt +public_label: Nyilvános public_description: API adatok hozzáférése bejelentkezés nélkül. not_allowed: Nem engedélyezett directus_version: Directus verzió @@ -64,31 +84,74 @@ os_type: Operációs rendszer típusa os_version: Operációs rendszer verziója os_uptime: Operációs rendszer futásideje os_totalmem: Operációs rendszer memóriája -archive: Archívum +archive: Archiválás archive_confirm: Biztosan törölni szeretnéd ezt az elemet? archive_confirm_count: >- Egy elem sincs kiválasztva `| Biztosan archiválni akarod ezt az elemet? | Biztosan archiválni akarod ezt a {count} elemet? +reset_system_permissions_to: 'Rendszerjogosultságok visszaállítása a következőkre:' +reset_system_permissions_copy: Ez a művelet felülírja a rendszergyűjteményekre alkalmazott egyéni engedélyeid. Biztos vagy benne? +the_following_are_minimum_permissions: Az alábbi minimális engedélyek szükségesek, ha az "Alkalmazás-hozzáférés" engedélyezve van. A jogosultságok ezen felül kiterjeszthetőek, de nem szűkíthetőek. +app_access_minimum: App hozzáférés minimuma +recommended_defaults: Ajánlott alapértelmezések unarchive: Archiválás visszavonása unarchive_confirm: Biztosan vissza akarod vonni ennek az elemnek az archiválását? nested_files_folders_will_be_moved: Egymás alatti fájlok és könyvtárak egy szinttel feljebb kerülnek. +unknown_validation_errors: 'A következő rejtett mezőknél érvényesítési hibák merültek fel:' validationError: + eq: Az érték {valid} kell legyen + neq: Az érték nem lehet {invalid} + in: 'Lehetséges értékek: {valid}' + nin: 'Nem felvehető értékek: {invalid}' + contains: Az {substring} része kell legyen az értéknek + ncontains: A {substring} nem lehet része az értéknek + gt: Az értéknek nagyobbnak kell lennie, mint {valid} + gte: Az értéknek nagyobbnak vagy egyenlőnek kell lennie, mint {valid} + lt: Az értéknek kevesebbnek kell lennie, mint {valid} + lte: Az értéknek kevesebbnek vagy egyenlőnek kell lennie, mint {valid} + empty: Üres értéknek kell lennie + nempty: Nem lehet üres érték + null: null kell legyen + nnull: Az érték nem lehet null + required: Érték szükséges unique: Egyedi érték szükséges -all_access: Összes hozzáférés + regex: Az érték nem megfelelő formátumú +all_access: Teljes hozzáférés no_access: Nincs hozzáférés use_custom: Egyéni használata +nullable: Lehet null +allow_null_value: NULL érték engedélyezése +allow_multiple: Többszörös engedélyezése +allow_multiple_to_be_open: Nyitott Többszörös engedélyezése +enter_value_to_replace_nulls: Kérjük, adjon meg egy új értéket a mezőben jelenleg található NULL-ok cseréjére! field_standard: Normál +field_presentation: Prezentációk és Aliasok field_file: Egyetlen fájl field_files: Több fájl egyidejű hozzáadása field_m2o: M2O kapcsolat +field_m2a: M2A kapcsolat field_o2m: O2M kapcsolat field_m2m: M2M kapcsolat field_translations: Fordítások +field_group: Mezőcsoport item_permissions: Elem engedélyek field_permissions: Mező engedélyek +field_validation: Mező érvényesítés +field_presets: Mező alapérték +permissions_for_role: 'A {role} szerepkör elemei a következő funkciót hajthatják végre: {action}.' +fields_for_role: 'A {role} szerepkör mezői a következő funkciót hajthatják végre: {action}.' +validation_for_role: 'A {action} mező szabályozza a {role} szerepek által követendő viselkedést.' +presets_for_role: 'A {role} szerepkör mezőértékeinek alapértelmezései.' +presentation_and_aliases: Prezentációk és Aliasok +revision_post_update: Ez az elem így nézett ki a frissítés után... +changes_made: Ezek a konkrét változtatások, melyeket elvégeztünk... +no_relational_data: Ne feledd, ez nem tartalmaz relációs adatokat! +hide_field_on_detail: Mező elrejtése a Részleteken +show_field_on_detail: Mező megjelenítése a Részleteken delete_field: Mező törlése fields_and_layout: Mezők és elrendezések field_create_success: 'Létrehozott mező: "{field}"' field_update_success: 'Módosított mező: "{field}"' +duplicate_where_to: Hová szeretné duplikálni ezt a mezőt? language: Nyelv global: Globális admins_have_all_permissions: Az adminisztrátorok minden hozzáféréssel rendelkeznek @@ -97,12 +160,14 @@ exposure: Megvilágítás shutter: Exponálógomb iso: ISO focal_length: Fókusztávolság +schema_setup_key: Ezen mező adatbázis-oszlopának neve és API-kulcsa create_field: Mező létrehozása creating_new_field: 'Új mező ({collection})' +field_in_collection: '{field} ({collection})' reset_page_preferences: Oldalbeállítások visszaállítása hidden_field: Rejtett mező hidden_on_detail: Elrejtés a részleteknél -disabled_editing_value: Szerkesztés tiltása +disabled_editing_value: Érték szerkeszthetőségének kikapcsolása key: Kulcs alias: Álnév bigInteger: Nagy egész számok @@ -113,14 +178,21 @@ decimal: Tizedes szám float: Törtszám integer: Egész szám json: JSON +xml: XML string: Karaktersor text: Szöveg time: Idő timestamp: Időbélyeg uuid: UUID hash: Hash +geometry: Geometria not_available_for_type: Ehhez a típushoz nem elérhető create_translations: Fordítás létrehozása +auto_refresh: Automatikus frissítés +refresh_interval: Frissítés időköz +no_refresh: Ne frissítsd +refresh_interval_seconds: Azonnali frissítés | Minden másodpercben | {seconds} másodpercenként +refresh_interval_minutes: Minden percben | {minutes} percenként auto_generate: Automatikusan generált this_will_auto_setup_fields_relations: Ez automatikusan létrehozza az összes szükséges mezőt és kapcsolatokat. click_here: Kattints ide @@ -128,51 +200,76 @@ to_manually_setup_translations: a fordítások kézi elkészítéséhez. click_to_manage_translated_fields: >- Ezek a mezők még nem lettek lefordítva. Kattints ide, hogy létrehozd őket. | Egy mező van lefordítva. Kattints ide a szerkesztéshez. | {count} mező van lefordítva. Kattints ide a szerkesztésükhöz. fields_group: Mező csoport +no_collections_found: Nem található gyűjtemény. +new_data_alert: 'A következők jönnek létre az Adatmodelledben:' +search_collection: Keresés a Gyűjteményben... new_field: 'Új mező' new_collection: 'Új gyűjtemény' -add_m2o_to_collection: 'Egy Sok-az-Egyhez kapcsolat hozzáadása a {collection} gyűjteményhez' -add_o2m_to_collection: 'Egy Egy-a-Sokhoz kapcsolat hozzáadása a {collection} gyűjteményhez' -add_m2m_to_collection: 'Egy Sok-a-Sokhoz kapcsolat hozzáadása a {collection} gyűjteményhez' +add_m2o_to_collection: 'Sok-az-Egyhez kapcsolat hozzáadása a {collection} gyűjteményhez' +add_o2m_to_collection: 'Egy-a-Sokhoz kapcsolat hozzáadása a {collection} gyűjteményhez' +add_m2m_to_collection: 'Sok-a-Sokhoz kapcsolat hozzáadása a {collection} gyűjteményhez' choose_a_type: Válassz típust... determined_by_relationship: A kapcsolat által meghatározott -add_note: Segítő jelzés a kitöltő felhasználónak... +add_note: Felhasználóknak szóló hasznos megjegyzés hozzáadása... default_value: Alapértelmezett érték standard_field: Általános mező single_file: Egyetlen fájl multiple_files: Több fájl egyidejű hozzáadása +m2o_relationship: Több az Egyhez kapcsolat +o2m_relationship: Egy a Többhöz kapcsolat +m2m_relationship: Több a Többhöz kapcsolat +m2a_relationship: M2A kapcsolat (polimorf asszociációs leképezéshez több táblából) +invalid_item: Érvénytelen elem next: Következő field_name: Mezőnév translations: Fordítások note: Jegyzet +enter_a_value: Adjon meg egy értéket... +enter_a_placeholder: Adjon meg egy helyőrzőt... length: Hossz +precision_scale: Pontosság és lépték readonly: Csak olvasható unique: Egyedi updated_on: Frissítve updated_by: Frissítette primary_key: Elsődleges kulcs +foreign_key: Idegen Kulcs finish_setup: Beállítás befejezése dismiss: Elutasít -edit_raw_value: Érték szerkesztése -enter_raw_value: Érték megadása... +raw_value: Nyers érték +edit_raw_value: Nyers érték szerkesztése +enter_raw_value: Nyers érték megadása... clear_value: Érték törlése reset_to_default: Visszaállítás alaphelyzetbe undo_changes: Változások visszavonása notifications: Értesítések show_all_activity: Összes tevékenység mutatása page_not_found: Az oldal nem található +page_not_found_body: Úgy tűnik, hogy a keresett oldal nem létezik. confirm_revert: Visszavonás elfogadása +confirm_revert_body: Ez visszaállítja az elemet a kiválasztott állapotba. display: Megjelenés settings_update_success: Beállítások frissítve title: Cím revision_delta_created: Létrehozva +revision_delta_created_externally: Külsőleg létrehozva +revision_delta_updated: 'Egy mező frissítve | {count} mező frissítve' revision_delta_deleted: Törölt revision_delta_reverted: Visszaállítva revision_delta_other: Változat +revision_delta_by: '{date} {user} által' private_user: Privát felhasználó +revision_preview: Felülvizsgálati előnézet +updates_made: Végrehajtott frissítések leave_comment: Hozzászólás írása... +post_comment_success: Közzétett hozzászólások +item_create_success: Létrehozott Elem | Létrehozott Elemek +item_update_success: Frissített Elem | Frissített Elemek +item_delete_success: Törölt Elem | Törölt Elemek this_collection: Ez a gyűjtemény related_collection: Kapcsolódó gyűjtemény related_collections: Kapcsolódó gyűjtemények +translations_collection: Fordításgyűjtemény languages_collection: Nyelvek gyűjteménye export_data: Adatok exportálása format: Formátum @@ -194,6 +291,11 @@ today: Ma yesterday: Tegnap delete_comment: Hozzászólás törlése date-fns_date: PPP +date-fns_time: 'h:mm:ss a' +date-fns_time_no_seconds: 'h:mm a' +date-fns_date_short: 'MMM d, u' +date-fns_time_short: 'h:mma' +date-fns_date_short_no_year: MMM. d. month: hónap year: év select_all: Összes kijelölése @@ -214,6 +316,7 @@ drag_mode: Fogd és vidd mód cancel_crop: Körbevágás megszakítása original: Eredeti url: URL +import_label: Importálás file_details: Fájl adatok dimensions: Méretek size: Méret @@ -229,6 +332,12 @@ open: Megnyitás open_in_new_window: Megnyitás új ablakban foreground_color: Előtérszín background_color: Háttérszín +upload_from_device: Fájl feltöltése Eszközről +choose_from_library: Fájl választása Könyvtárból +import_from_url: Fájl importálása URL-ből +replace_from_device: Fájl cseréje Eszközről +replace_from_library: Fájl cseréje Könyvtárból +replace_from_url: Fájl cseréje URL-ből no_file_selected: Nincs kijelölt fájl download_file: Fájl letöltése collection_key: Gyűjtemény kulcsa @@ -238,12 +347,19 @@ type: Típus creating_new_collection: Új gyűjtemény létrehozása created_by: Készítette created_on: Létrehozva +creating_collection_info: Nevezze el a Gyűjteményt és állítsa be az egyedi "kulcs" mezőt... +creating_collection_system: Engedélyezze és nevezze át ezen opcionális mezők bármelyikét! +auto_increment_integer: Automatikusan növekvő egész szám generated_uuid: Generált UUID manual_string: Kézzel beírt szöveg save_and_create_new: Mentés és létrehozás save_and_stay: Mentés és marad save_as_copy: Mentés másolatként -add_existing: Létező tartalom hozzáadása +add_existing: Létező hozzáadása +creating_items: Elemek létrehozása +enable_create_button: Létrehozás gomb engedélyezése +selecting_items: Elemek kiválasztása +enable_select_button: Kiválasztás gomb engedélyezése comments: Hozzászólások no_comments: Még nincs hozzászólás click_to_expand: Kattintson a kibontáshoz @@ -254,8 +370,10 @@ disabled: Letiltva information: Információ report_bug: Hibajelentés request_feature: Funkció ajánlása +interface_not_found: 'Hiányzó interfész: "{interface}".' reset_interface: Felhasználói felület visszaállítása reset_display: Megjelenés alaphelyzetbe +list-m2a: Builder (M2A) item_count: 'Nincsenek elemek | Egy elem | {count} elem' no_items_copy: Nincsenek még elemek ebben a gyűjteményben. file_count: 'Nincsenek fájlok | Egy fájl | {count} file' @@ -265,6 +383,7 @@ no_users_copy: Ez a szerepkör még egy felhasználóhoz sem lett hozzárendelve webhooks_count: 'Nincsenek webhookok | Egy webhook | {count} webhook' no_webhooks_copy: Még nincsenek webhookok. all_items: Minden elem +any: Bármely csv: CSV no_collections: Nincsenek gyűjtemények create_collection: Gyűjtemény létrehozása @@ -274,7 +393,8 @@ relationship_not_setup: Ez a kapcsolat nincs megfelelően beállítva display_template_not_setup: A megjelenítő sablon nem megfelelően van beállítva collection_field_not_setup: A gyűjtemény mező opció nem megfelelően van beállítva select_a_collection: Gyűjtemény kiválasztása -active: aktív +active: Aktív +inactive: Inaktív users: Felhasználók activity: Tevékenységek webhooks: Webhookok @@ -287,11 +407,13 @@ documentation: Dokumentáció sidebar: Oldalsáv duration: Időtartam charset: Karakterkészlet +second: másodperc file_moved: A fájl átmozgatva collection_created: Gyűjtemény létrehozva modified_on: Változtatás dátuma card_size: Kártya mérete sort_field: Rendezési mező +add_sort_field: Rendezési mező hozzáadása sort: Rendezés status: Állapot remove: Eltávolítás @@ -315,22 +437,36 @@ errors: FORBIDDEN: Tilos INVALID_CREDENTIALS: Hibás felhasználónév vagy jelszó INVALID_OTP: Helytelen az egyszer használatos jelszó + INVALID_PAYLOAD: Érvénytelen payload INVALID_QUERY: Érvénytelen lekérdezés + ITEM_LIMIT_REACHED: Elérte az elemek maximális számát ITEM_NOT_FOUND: Elem nem található ROUTE_NOT_FOUND: Nem található + RECORD_NOT_UNIQUE: Ismétlődő értéket észleltem + USER_SUSPENDED: Felhasználó felfüggesztve + CONTAINS_NULL_VALUES: A mező null értékeket tartalmaz UNKNOWN: Váratlan hiba történt + UNPROCESSABLE_ENTITY: Feldolgozhatatlan entitás INTERNAL_SERVER_ERROR: Váratlan hiba történt + NOT_NULL_VIOLATION: Az érték nem lehet null +security: Biztonság +value_hashed: Biztonságosan hashelt érték bookmark_name: Könyvjelző neve... create_bookmark: Könyvjelző létrehozása edit_bookmark: Könyvjelző szerkesztése bookmarks: Könyvjelzők +presets: Alapbeállítások unexpected_error: Váratlan hiba történt +unexpected_error_copy: Váratlan hiba történt. Kérjük, próbálja meg később újra! copy_details: Részletek másolása no_app_access: Nincs alkalmazás hozzáférés +no_app_access_copy: Ez a felhasználó nem jogosult az admin alkalmazás használatára. +password_reset_sent: Küldtünk egy biztonságos linket a jelszó visszaállításához +password_reset_successful: Sikeres jelszó-visszaállítás back: Vissza editing_image: Kép szerkesztése square: Négyzet -free: Ingyenes +free: Szabadon választott flip_horizontal: Vízszintes tükrözés flip_vertical: Függőleges tükrözés aspect_ratio: Méretarány @@ -339,17 +475,23 @@ all_users: Összes felhasználó delete_collection: Gyűjtemény törlése update_collection_success: Módosított gyűjtemények delete_collection_success: Törölt gyűjtemények +start_end_of_count_items: '{start}-{end} {count} elemből' +start_end_of_count_filtered_items: '{start}-{end} {count} szűrt elemből' one_item: '1 elem' one_filtered_item: '1 szűrt elem' +delete_collection_are_you_sure: >- + Biztos, hogy törölni szeretné ezt a gyűjteményt? Ez törli a gyűjteményt a benne lévő összes elemével együtt. Ez a művelet végleges. collections_shown: Megjelenített gyűjtemények visible_collections: Látható gyűjtemények -hidden_collections: Elrejtett gyűjtemények +hidden_collections: Rejtett gyűjtemények show_hidden_collections: Rejtett gyűjtemények mutatása -hide_hidden_collections: Rejtett gyűjtemények rejtése +hide_hidden_collections: Rejtett gyűjtemények elrejtése unmanaged_collections: Nem beállított gyűjtemények system_collections: Rendszer gyűjtemények +placeholder: Helyőrző icon_left: Ikon balra icon_right: Ikon jobbra +count_other_revisions: '{count} egyéb módosítás' font: Betűtípus sans_serif: Sans Serif serif: Talpas @@ -358,8 +500,11 @@ divider: Elválasztó color: Szín circle: Kör empty_item: Üres elem +log_in_with: 'Bejelentkezés {provider} profillal' +advanced_settings: Haladó beállítások advanced_filter: Bővített szűrés delete_advanced_filter: Szűrő törlése +change_advanced_filter_operator: Operátor cseréje operators: eq: Egyenlő neq: Nem egyenlő @@ -373,14 +518,24 @@ operators: nnull: nem null contains: Tartalmaz ncontains: nem tartalmaz + starts_with: Ezzel kezdődik + nstarts_with: Nem ezzel kezdődik + ends_with: Ezzel végződik + nends_with: Nem ezzel végződik between: között nbetween: nincs ezek között empty: üres nempty: nem üres all: Tartalmazza ezeket a kulcsokat has: Néhányat ezek közül a kulcsok közül tartalmaz + intersects: Metszi + nintersects: Nem metszi + intersects_bbox: Metszi a határoló dobozt + nintersects_bbox: Nem metszi a határoló dobozt loading: Betöltés... drop_to_upload: Húzd ide a feltöltéshez +item: Elem +items: Elemek upload_file: Fájl feltöltése upload_file_indeterminate: Fájl feltöltése... upload_file_success: Fájl feltöltve @@ -389,6 +544,7 @@ upload_files_success: '{count} fájl feltöltve' upload_pending: Feltöltés folyamatban drag_file_here: Dobj ide egy fájlt click_to_browse: Kattints a böngészéshez +interface_options: Interfész opciók layout_options: Elrendezési beállítások rows: Sorok columns: Oszlopok @@ -412,10 +568,12 @@ no_results_copy: Módosítsd vagy töröld a keresési szűrőket a találatoké clear_filters: Szűrök törlése saves_automatically: Automatikus mentés role: Szerepkör +rule: Szabály user: Felhasználó no_presets: Presetek no_presets_copy: Még egy preset vagy könyvjelző sem lett elmentve. -no_presets_cta: Preset hozzáadása +no_presets_cta: Alapértelmezés hozzáadása +presets_only: Csak alapbeállítások create: Létrehozás on_create: Létrehozáskor on_update: Módosításkor @@ -428,12 +586,27 @@ toggle: Váltás icon_on: Ikon be icon_off: Ikon ki label: Címke +image_url: Kép URL +alt_text: Alternatív szöveg +media: Média +quality: Minőség width: Szélesség height: Magasság +source: Forrás +url_placeholder: Írjon be egy url-t... +display_text: Szöveg megjelenítése +display_text_placeholder: Írjon be egy megjelenítendő szöveget... +tooltip: Buboréksúgó +tooltip_placeholder: Írjon be egy buboréksúgó szöveget... +unlimited: Korlátlan +open_link_in: Hivatkozás megnyitása itt +new_tab: Új fül +current_tab: Jelenlegi fül wysiwyg_options: aligncenter: Középre igazítás alignjustify: Sorkizárt alignleft: Balra igazítás + alignnone: Rendezés nélkül alignright: Jobbra igazítás forecolor: Előtérszín backcolor: Háttérszín @@ -448,7 +621,10 @@ wysiwyg_options: bullist: Felsorolás lista numlist: Számozott lista hr: Vízszintes vonal + link: Link hozzáadása/szerkesztése unlink: Hivatkozás törlése + media: Média hozzáadása/szerkesztése + image: Kép hozzáadása/szerkesztése copy: Másolás cut: Kivágás paste: Beillesztés @@ -470,45 +646,96 @@ wysiwyg_options: selectall: Összes kijelölése table: Táblázat visualaid: Láthatatlan elemek mutatása + source_code: Forráskód szerkesztése fullscreen: Teljes képernyő + directionality: Irányítottság dropdown: Legördülő lista choices: Lehetőségek +choices_option_configured_incorrectly: Helytelenül konfigurált választási ehetőségek deselect: Kijelölés megszüntetése deselect_all: Összes kijelölés törlése other: Egyéb... -adding_user: Hozzáadott felhasználó +adding_user: Felhasználó hozzáadása unknown_user: Ismeretlen felhasználó creating_in: 'Elem hozzáadása ide: {collection}' editing_in: 'Elem szerkesztése itt: {collection}' +creating_unit: '{unit} létrehozása' +editing_unit: '{unit} szerkesztése' editing_in_batch: '{count} elem szerkesztése' no_options_available: Nincs választási lehetőség -settings_data_model: Adat model +settings_data_model: Adatmodell settings_permissions: Szerepkörök és jogosultságok settings_project: Projekt beállításai settings_webhooks: Webhookok +settings_presets: Alapértelmezések és Könyvjelzők +one_or_more_options_are_missing: Egy vagy több opció hiányzik scope: Hatókör +select: Válasszon... layout: Elrendezés +tree_view: Fa nézet +changes_are_permanent: A változtatások nem visszavonhatóak +preset_name_placeholder: Alapértelmezettként szolgál, ha üres... +preset_search_placeholder: Keresési lekérdezés... +editing_preset: Alapértelmezés szerkesztése +layout_preview: Elrendezés előnézet layout_setup: Elrendezés beállítás unsaved_changes: Nem mentett módosítások unsaved_changes_copy: Biztos, hogy elhagyja az oldalt? discard_changes: Változtatások elvetése keep_editing: Szerkesztés folytatása +page_help_collections_overview: '**Gyűjtemények áttekintése** - Az összes ön által hozzáférhető Gyűjtemény listája.' +page_help_collections_collection: >- + **Elemek böngészése** - Az összes {collection} elemet listázza, amelyhez hozzáférhet. Testre szabhatja az elrendezést, a szűrőket, a rendezési nézetet, és akár könyvjelzőket is menthet a különböző konfigurációkról a gyors hozzáférés érdekében. +page_help_collections_item: >- + **Elem részletei** - Ezen elem megtekintésére és kezelésére szolgáló űrlap. Ez az oldalsáv tartalmazza a revíziók teljes előzményeit és a beágyazott megjegyzéseket is. +page_help_activity_collection: >- + **Böngészési tevékenység** - A felhasználók összes rendszer- és tartalomtevékenységének átfogó listája. +page_help_docs_global: >- + **Dokumentációs áttekintés** - Kifejezetten ezen projekt verziójára és sémájára szabott dokumentumok. +page_help_files_collection: >- + **Fájlok könyvtára** - A projektbe feltöltött összes fájleszköz listája. Testre szabhatja az elrendezést, a szűrőket, a rendezési nézetet, és akár könyvjelzőket is menthet a különböző konfigurációkról a gyors hozzáférés érdekében. +page_help_files_item: >- + **File Részletek** - A fájl metaadatainak kezelésére, az eredeti eszköz szerkesztésére és a hozzáférési beállítások frissítésére szolgáló űrlap. +page_help_settings_project: "**Projektbeállítások** - A projektje globális konfigurációs beállításai." +page_help_settings_datamodel_collections: >- + **Adatmodell: Gyűjtemények** - Az összes elérhető gyűjtemény listája. Ide tartoznak a látható, rejtett és rendszergyűjtemények, valamint a hozzáadható nem kezelt adatbázis táblák. +page_help_settings_datamodel_fields: >- + **Adatmodell: Gyűjtemény** - Egy űrlap, ezen gyűjtemény és mezőinek kezelésére. +page_help_settings_roles_collection: '**Szerepkörök böngészése** - A Rendszergazda, a Nyilvános és az egyéni Felhasználói Szerepkörök listája.' +page_help_settings_roles_item: "**Szerepkör Részletei** - A szerepkörök jogosultságainak és egyéb beállításainak kezelése." +page_help_settings_presets_collection: >- + **Alapbeállítások Böngészése** - A projektben lévő összes alapbeállítás listázása, beleértve: a felhasználói, szerepköri és globális könyvjelzőket, valamint az alapértelmezett nézeteket. +page_help_settings_presets_item: >- + **Alapbeállítások Részletei** - Könyvjelzők és alapértelmezett gyűjteménybeállítások kezelésére szolgáló űrlap. +page_help_settings_webhooks_collection: '**Webhookok Böngészése** — A projektben található összes webhook listája.' +page_help_settings_webhooks_item: '**Webhook Részletei** — A projekt webhookjainak létrehozására és kezelésére szolgáló űrlap.' +page_help_users_collection: '**Felhasználók Könyvtára** - A projekt összes rendszerfelhasználójának listája.' +page_help_users_item: >- + **Felhasználó részletei** - Kezelje fiókinformációit, vagy tekintse meg más felhasználók adatait. +activity_feed: Tevékenységi Hírfolyam add_new: Új hozzáadása create_new: Új létrehozása all: Összes none: Egyik sem +no_layout_collection_selected_yet: Még nem választott elrendezést/gyűjteményt batch_delete_confirm: >- Nincs elem kiválasztva | Biztosan törölni szeretnéd az elemet? A művelet nem visszavonható. | Biztosan törölni szeretnéd az összes elemet ({count} db)? A művelet nem visszavonható. cancel: Mégse +no_upscale: Ne méretezze fel a képeket collection: Gyűjtemény collections: Gyűjtemények singleton: Egyedüli +singleton_label: Egyetlen objektumként kezelendő +system_fields_locked: A rendszermezők zároltak és nem szerkeszthetők fields: directus_activity: item: Elem elsődleges kulcsa action: Művelet collection: Gyűjtemény + timestamp: Művelet időpontja + user: Művelet kezdeményezője comment: Hozzászólás + user_agent: Felhasználói ügynök ip: IP cím revisions: Változatok directus_collections: @@ -520,12 +747,16 @@ fields: singleton: Egyedüli archive_app_filter: Szűrő archíválása archive_value: Archív érték + unarchive_value: Archiválás megszüntetése utáni érték sort_field: Rendezési mező + accountability: Tevékenység és revízió nyomkövetése directus_files: + $thumbnail: Miniatűr title: Cím description: Leírás tags: Cimkék location: Pozíció + storage: Tároló filename_disk: Fájlnév (tárhely) filename_download: Fájlnév (letöltött) metadata: Metadata @@ -553,23 +784,52 @@ fields: title: Cím description: Leírás tags: Cimkék + user_preferences: Felhasználói beállítások language: Nyelv theme: Téma + theme_auto: Automatikus (A rendszeren alapuló) + theme_light: Világos mód + theme_dark: Sötét mód tfa_secret: Kétlépcsős hitelesítés + admin_options: Admin beállítások status: Állapot - status_active: aktív + status_draft: Vázlat + status_invited: Meghívott + status_active: Aktív + status_suspended: Felfüggesztve + status_archived: Archiválva role: Szerepkör token: Token + token_placeholder: Írja be a biztonságos hozzáférés tokenjét... last_page: Utolsó oldal last_access: Utolsó hozzáférés directus_settings: + jpg: JPEG + png: PNG + webP: WebP + tiff: Tiff + basemaps_raster: Raszter + basemaps_tile: Raszter TileJSON + basemaps_style: Mapbox stílus + mapbox_key: Mapbox hozzáférési token + mapbox_placeholder: pk.eyJ1Ijo..... + transforms_note: 'A Sharp módszer neve és argumentumai. További információért lásd: https://sharp.pixelplumbing.com/api-constructor' + additional_transforms: További transzformációk project_name: Projekt neve project_url: Projekt webcíme project_color: Projekt színe project_logo: Projekt logója + public_pages: Nyilvános oldalak public_foreground: Nyilvános előtér public_background: Nyilvános háttér public_note: Nyilvános megjegyzés + auth_password_policy: Hitelesítési jelszóházirend + auth_login_attempts: Bejelentkezési kísérletek + files_and_thumbnails: Fájlok és miniatűrök + storage_default_folder: Tároló alapértelmezett mappája + storage_asset_presets: Tárolóeszköz alapbeállításai + storage_asset_transform: Tárolóeszközök transzformáció + overrides: Alkalmazás felülbírálások custom_css: Egyéni CSS directus_fields: collection: Gyűjtemény neve @@ -581,19 +841,143 @@ fields: display_template: Sablon directus_roles: name: Szerepkör neve + icon: Szerep ikonja description: Leírás app_access: Alkalmazás hozzáférés admin_access: Admin hozzáférés ip_access: IP cím + enforce_tfa: 2FA megkövetelése + users: Felhasználók a szerepkörben collection_list: Gyűjtemény navigáció directus_webhooks: name: Név + method: Metódus status: Állapot + data: Adat + data_label: Eseményadatok küldése + triggers: Triggerek + actions: Műveletek +field_options: + directus_settings: + project_name_placeholder: Saját projektem... + project_logo_note: Bejelentkezés és logó háttere + public_note_placeholder: Egy rövid, nyilvános üzenet, mely támogatja a markdown formázást... + security_divider_title: Biztonság + auth_password_policy: + none_text: Nincs - Nem ajánlott + weak_text: Gyenge - Minimum 8 karakter + strong_text: Erős - Nagybetű / Kisbetű / Számok / Speciális karakterek + storage_asset_presets: + fit: + contain_text: Tartalmazza (a képarány megőrzése) + cover_text: Fedje (pontos méretre kényszerítés) + fit_text: Illeszkedjen bele + outside_text: Illeszkedjen külsőleg + additional_transforms: További transzformációk + transforms_note: 'A Sharp módszer neve és argumentumai. További információért lásd: https://sharp.pixelplumbing.com/api-constructor' + mapbox_key: Mapbox hozzáférési token + mapbox_placeholder: pk.eyJ1Ijo..... + basemaps_raster: Raszter + basemaps_tile: Raszter TileJSON + basemaps_style: Mapbox stílus + files_divider_title: Fájlok és miniatűrök + overrides_divider_title: Alkalmazás felülbírálások + directus_activity: + login: Belépés + create: Létrehozás + update: Frissítés + delete: Törlés + directus_collections: + track_activity_revisions: Tevékenységek és módosítások nyomon követése + only_track_activity: Csak a tevékenység nyomon követése + do_not_track_anything: Minden nyomkövetés tiltása + collection_setup: Gyűjtemény beállítások + note_placeholder: A gyűjtemény leírása pár szóban... + hidden_label: Legyen rejtett az alkalmazásban + singleton: Egyetlen objektumként kezelendő + language: Nyelv + translation: Fordítás megadása... + archive_divider: Archiválás + archive_field: Mező kiválasztása... + archive_app_filter: Alkalmazás archívum szűrőjének engedélyezése + archive_value: Archiváláskor beállított érték... + unarchive_value: Az archiválás visszavonásakor beállított érték... + divider: Rendezés + sort_field: Mező kiválasztása... + directus_files: + title: Egy egyedi cím... + description: Egy opcionális leírás... + location: Egy opcionális helyszín... + storage_divider: Fájl elnevezése + filename_download: Elnevezés letöltés esetén... + directus_roles: + name: E szerepkör egyedi neve... + description: A szerepkör leírása... + ip_access: Adja hozzá az engedélyezett IP-címeket, minden IP engedélyezéséhez hagyja üresen... + fields: + icon_name: Ikon + name_name: Név + name_placeholder: Írjon be egy címet... + link_name: Hivatkozás + link_placeholder: Relatív vagy abszolút URL... + collection_list: + group_name_addLabel: Új csoport hozzáadása... + fields: + group_name: Csoportnév + group_placeholder: Címkézze fel ezt a csoportot... + type_name: Típus + choices_always: Mindig nyitva + choices_start_open: Indíts nyitva! + choices_start_collapsed: Kezdetben összecsukva + collections_name: Gyűjtemények + collections_addLabel: Gyűjtemény hozzáadása... + directus_users: + preferences_divider: Felhasználói beállítások + dropdown_auto: Automatikus (A rendszeren alapuló) + dropdown_light: Világos mód + dropdown_dark: Sötét mód + admin_divider: Admin beállítások + status_dropdown_draft: Vázlat + status_dropdown_invited: Meghívott + status_dropdown_active: Aktív + status_dropdown_suspended: Felfüggesztve + status_dropdown_archived: Archiválva + token: Írja be a biztonságos hozzáférés tokenjét... + directus_webhooks: + status_options_active: Aktív + status_options_inactive: Inaktív + data_label: Eseményadatok küldése + triggers_divider: Triggerek + actions_create: Létrehozás + actions_update: Frissítés + actions_delete: Törlés + actions_login: Bejelentkezés no_fields_in_collection: 'A(z) {collection} még nem tartalmaz mezőt' do_nothing: Ne tegyen semmit +generate_and_save_uuid: UUID generálása és mentése +save_current_user_id: Jelenlegi felhasználói azonosító mentése +save_current_user_role: Jelenlegi felhasználói szerepkör mentése +save_current_datetime: Aktuális dátum/idő mentése block: Blokk +inline: Inline comment: Hozzászólás +relational_triggers: Relációs triggerek +referential_action_field_label_m2o: '{collection} törlésekor...' +referential_action_field_label_o2m: '{collection} kiválasztásának megszüntetésekor...' +referential_action_no_action: A törlés megakadályozása +referential_action_cascade: A {collection} elem kaszkád törlése +referential_action_set_null: A {field} mező állapotának nullra állítása +referential_action_set_default: '{field} beállítása alapértelmezett értékére' +choose_action: Művelet választása +continue_label: Folytatás +continue_as: >- + {név} jelenleg hitelesített. Ha felismeri ezt a fiókot, nyomja meg a folytatás gombot! +editing_role: '{role} szerepkör' creating_webhook: Webhook létrehozása +default_label: Alapértelmezett +delete_label: Törlés +delete_are_you_sure: >- + Ez a művelet végleges és visszavonhatatlan. Biztos, hogy folytatni szeretné? delete_field_are_you_sure: >- Biztosan törölni szeretnéd ezt a mezőt ({field})? Ha törlöd, nem lehet visszaállítani. description: Leírás @@ -601,6 +985,7 @@ done: Kész duplicate: Duplikálás email: Email embed: Beágyazás +fallback_icon: Tartalék ikon field: mező | mezők file: File file_library: Könyvtár @@ -612,6 +997,8 @@ normal: Normál success: Sikeres warning: Figyelmeztetés danger: Veszély +junction_collection: Csomópont gyűjtemény +latency: Késleltetés login: Belépés my_activity: Saját tevékenységek not_authenticated: Nem hitelesített @@ -640,32 +1027,94 @@ sort_direction: Rendezés iránya sort_asc: Növekvő sorrend sort_desc: Csökkenő sorrend template: Sablon +require_value_to_be_set: Az érték beállítása kötelező translation: Fordítás value: Érték view_project: Projekt megtekintése report_error: Hiba jelentése interfaces: + group-accordion: + name: Harmonika + description: Mezőcsoportok megjelenítése harmonika nézetben + all_closed: Mind bezárva + first_opened: Első nyitva + all_opened: Mind nyitva + accordion_mode: Harmonika mód + max_one_section_open: Max. egy nyitott szakasz presentation-links: + presentation-links: Gomb hivatkozások + links: Linkek + description: Konfigurálható linkgombok dinamikus URL-ek indításához + style: Stílus primary: Elsődleges + link: Linkek + button: Gombok + error: A művelet nem hajtható végre + select-multiple-checkbox: + checkboxes: Jelölőnégyzetek + description: Válasszon akár több lehetőséget is a jelölőnégyzetek segítségével + allow_other: Egyéb engedélyezése + show_more: 'További {count} mutatása' + items_shown: Megjelenített elemek + select-multiple-checkbox-tree: + name: Jelölőnégyzetek (Fa) + description: Válasszon akár több lehetőséget is az egymásba ágyazott jelölőnégyzetek segítségével + value_combining: Érték kombinálása + value_combining_note: Szabályozza, hogy a beágyazott kiválasztáskor milyen érték kerüljön tárolásra. + show_all: Mind megjelenítése + show_selected: Kiválasztottak megjelenítése input-code: code: Kód + description: Kódrészletek írása vagy megosztása + line_number: Sorok számozása + placeholder: Kód beírása... system-collection: collection: Gyűjtemény + description: Választás meglévő gyűjteményekből + include_system_collections: Beleértve a rendszergyűjteményeket system-collections: collections: Gyűjtemények + description: Választás meglévő gyűjteményekből + include_system_collections: Beleértve a rendszergyűjteményeket select-color: color: Szín + description: Írjon be vagy válasszon egy színértéket + placeholder: Szín választása... + preset_colors: Alapértelmezett színek + preset_colors_add_label: Új szín hozzáadása... + name_placeholder: Szín elnevezése... datetime: datetime: Dátum és idő description: Dátum és idő beállítása + include_seconds: Beleértve a másodperceket set_to_now: Mai nap use_24: 24 órás formátum használata system-display-template: display-template: Megjelenítési sablon + description: Statikus szöveg és dinamikus mezőértékek keverése + collection_field: Gyűjteménymező collection_field_not_setup: A gyűjtemény mező opció nem megfelelően van beállítva select_a_collection: Gyűjtemény kiválasztása presentation-divider: divider: Elválasztó + description: A mezők felcímkézése és szakaszokra osztása + title_placeholder: Írjon be egy címet... + inline_title: Inline cím + inline_title_label: Cím megjelenítése a soron belül + margin_top: Felső margó + margin_top_label: Felső margó növelése + select-dropdown: + description: Válasszon értéket legördülő listából + choices_placeholder: Új lehetőség hozzáadása + allow_other: Egyéb engedélyezése + allow_other_label: Egyéb érték engedélyezése + allow_none: Egyik sem engedélyezett + allow_none_label: Nem kötelező választani + choices_name_placeholder: Adjon meg egy nevet... + choices_value_placeholder: Adjon meg egy értéket... + select-multiple-dropdown: + select-multiple-dropdown: Legördülő lista (többválasztós) + description: Válasszon több értéket legördülő listából file: file: File description: Válasszon vagy töltsön fel fájlt @@ -674,45 +1123,158 @@ interfaces: description: Válasszon vagy töltsön fel fájlokat input-hash: hash: Hash + description: Adjon meg egy hashelni kívánt értéket + masked: Maszkolt + masked_label: Valós értékek elrejtése select-icon: icon: Ikon + description: Válasszon egy ikont legördülő listából + search_for_icon: Ikon keresése... file-image: image: Kép + description: Válasszon vagy töltsön fel egy képet system-interface: interface: Kezelőfelület + description: Már létező interfész kiválasztása + placeholder: Válasszon egy interfészt... + system-interface-options: + interface-options: Interfész opciók + description: Egy modál egy interfész opcióinak kiválasztásához + list-m2m: + many-to-many: Sok a sokhoz + description: Több kapcsolódó csomóponti elem kiválasztása select-dropdown-m2o: + many-to-one: Sok az egyhez + description: Egyetlen kapcsolódó elem kiválasztása display_template: Megjelenítési sablon + input-rich-text-md: + markdown: Markdown + description: Markdown bevitele és előnézete + customSyntax: Egyedi blokkok + customSyntax_label: Egyéni szintaxistípusok hozzáadása + customSyntax_add: Egyéni szintaxis hozzáadása + box: Block / Inline + imageToken: Kép token + imageToken_label: Milyen (statikus) tokent csatoljunk a képforrásokhoz map: + map: Térkép + description: Hely megadása térképen zoom: Nagyítás + geometry_type: Geometria típusa + geometry_format: Geometria formátuma + default_view: Alapértelmezett nézet + invalid_options: Érvénytelen opciók + invalid_format: Érvénytelen formátum ({format}) + unexpected_geometry: 'Elvárt: {expected}, kapott: {got}.' + fit_bounds: Nézet hozzáigazítása az adathoz + native: Natív + geojson: GeoJSON + lnglat: Hosszúság, Szélesség + wkt: WKT + wkb: WKB + presentation-notice: + notice: Értesítés + description: Rövid értesítés megjelenítése + text: Írja ide az értesítés tartalmát... + list-o2m: + one-to-many: Egy a sokhoz + description: Több kapcsolódó elem választása + no_collection: A gyűjtemény nem található system-folder: folder: Könyvtár + description: Válasszon egy mappát + field_hint: Az újonnan feltöltött fájlokat a kiválasztott mappába helyezi. Nem érinti a kiválasztott, de már létező fájlokat. + root_name: Fájlok gyökérkönyvtára + system_default: Rendszer alapértelmezései + select-radio: + radio-buttons: Rádiógombok + description: Válasszon egyet a több lehetőség közül + list: + repeater: Repeater + description: Ugyanazon struktúra több bejegyzésének létrehozása + edit_fields: Mezők szerkesztése + add_label: '"Új létrehozása" címke' + field_name_placeholder: Mezőnév megadása... + field_note_placeholder: Megjegyzés hozzáadása mezőhöz... slider: slider: Csúszka + description: Válasszon számot csúszka segítségével + always_show_value: Mindig mutassa az értéket tags: tags: Cimkék description: Címke megadása vagy kiválasztása whitespace: Szóköz + hyphen: Helyettesítse kötőjellel + underscore: Helyettesítse aláhúzással + remove: Szóközök eltávolítása + capitalization: Nagybetűs írásmód uppercase: Nagybetűsre konvertálás lowercase: Kisbetűsre konvertálás + auto_formatter: Cím automatikus formázó használata alphabetize: ABC sorrend alphabetize_label: ABC sorrend kényszerítése - add_tags: Címke hozzáadása... + add_tags: Címkék hozzáadása... + input: + input: Bevitel + description: Manuálisan határozzon meg egy értéket! + trim: Vágás + mask: Maszkolt + mask_label: Valós érték elrejtése + clear: Törölt érték + clear_label: Mentés üres karakterláncként + minimum_value: Minimumérték + maximum_value: Maximumérték + step_interval: Lépésköz + slug: Slugify + slug_label: A beírt érték URL-kompatibilissá tétel + input-multiline: + textarea: Szövegmező + description: Többsoros egyszerű szöveg (multiline plain-text) bevitele boolean: toggle: Váltás + description: Be- és kikapcsolás közti váltás + label_placeholder: Címke beírása... label_default: Engedélyezve translations: display_template: Megjelenítési sablon no_collection: Nincsenek gyűjtemények + list-o2m-tree-view: + description: Fa nézet egymásba ágyazott rekurzív egy a sokhoz elemekhez + recursive_only: A fa nézet felület csak rekurzív kapcsolatok esetén működik. user: user: Felhasználó + description: Válasszon ki egy létező directus felhasználót! select_mode: Kiválasztott mód modes: auto: Automatikus dropdown: Legördülő lista modal: Ablak + input-rich-text-html: + wysiwyg: WYSIWYG + description: Rich-text editor HTML tartalom írásához + toolbar: Eszköztár + custom_formats: Egyéni formátumok + options_override: Beállítások felülírása + input-autocomplete-api: + input-autocomplete-api: Automatikus kiegészítés (API) + description: Külső API-értékek keresési típusvezetője (automatikus kiegészítés). + results_path: Eredmények útvonala + value_path: Érték útvonala + trigger: Eseményindító + rate: Értékelés + group-raw: + description: A mezők megjelenítése a jelenlegi állapotnak megfelelően + group-detail: + description: A mezők renderelése összecsukható szakaszként + show_header: Csoportfejléc megjelenítése + header_icon: Fejléc ikon + header_color: Fejléc színe + start_open: Indíts nyitva! + start_closed: Indíts zárva! displays: boolean: boolean: Logikai + description: Be- és kikapcsolt állapotok megjelenítése label_on: Címke be label_on_placeholder: Címke megadása... label_off: Címke ki @@ -731,37 +1293,58 @@ displays: default_color: Alapértelmezett szín datetime: datetime: Dátum és idő + description: Az idővel kapcsolatos értékek megjelenítése format: Formátum + format_note: >- + Az egyéni formátum elfogadja a __[Date Field Symbol Table](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)__ long: Hosszú short: Rövid relative: Relatív + relative_label: 'Relatív idő megjelenítése, pl.: 5 perccel ezelőtt' file: file: File description: Fájlok megjelenítése filesize: filesize: Fájlméret + description: Fájl méretének megjelenítése formatted-value: formatted-value: Formázott érték + description: A szöveg formázott változatának megjelenítése + format_title: Cím formázása + format_title_label: Automatikus nagybetű / kisbetű bold_label: Vastag stílus használata formatted-json-value: formatted-json-value: Formázott JSON érték + description: Az objektum formázott változatának megjelenítése icon: icon: Ikon + description: Egy ikon megjelenítése filled: Kitöltve + filled_label: Használja a kitöltött változatot image: image: Kép + description: Egy apró előnézeti kép megjelenítése circle: Kör + circle_label: Megjelenítés körszegéllyel labels: labels: Feliratok + description: Egyetlen címke vagy címkék listájának megjelenítése default_foreground: Alapértelmezett előtér default_background: Alapértelmezett háttér format_label: Mindegyik címke formázása + show_as_dot: Pontszerű megjelenítés + choices_value_placeholder: Adjon meg egy értéket... choices_text_placeholder: Szöveg megadása... mime-type: mime-type: MIME típus + description: A fájl MIME-típusának megjelenítése + extension_only: Csak a kiterjesztés + extension_only_label: Csak a fájlkiterjesztés megjelenítése rating: rating: Értékelés + description: Egy számérték viszonylagos megjelenítése max-értékéhez képest, csillagokkal ábrázolva simple: Egyszerű + simple_label: Csillagok megjelenítése egyszerű formátumban raw: raw: Nyers érték related-values: @@ -769,9 +1352,11 @@ displays: description: Hasonló értékek megjelenítése user: user: Felhasználó + description: Egy directus felhasználó megjelenítése avatar: Profilkép name: Név both: Mindkettő + circle_label: Felhasználó megjelenítése körszegéllyel layouts: cards: cards: Kártya @@ -787,3 +1372,22 @@ layouts: spacing: Sorköz comfortable: Kényelmes compact: Kompakt + cozy: Kényelmes + calendar: + calendar: Naptár + start_date_field: Kezdő dátum mező + end_date_field: Záró dátum mező + map: + map: Térkép + basemap: Alaptérkép + layers: Rétegek + edit_custom_layers: Rétegek szerkesztése + cluster_options: Klaszterezés beállításai + cluster: Klaszterezés aktiválása + cluster_radius: Klaszter sugara + cluster_minpoints: Minimális klaszter méret + cluster_maxzoom: Maximális zoom a klaszterezéshez + field: Geometria + invalid_geometry: Érvénytelen geometria + auto_location_filter: Mindig szűrje az adatokat a határok megtekintéséhez + search_this_area: Keresés ezen a területen diff --git a/app/src/lang/translations/id-ID.yaml b/app/src/lang/translations/id-ID.yaml index cc0421097e..a4fe9b723f 100644 --- a/app/src/lang/translations/id-ID.yaml +++ b/app/src/lang/translations/id-ID.yaml @@ -539,6 +539,27 @@ fields: directus_webhooks: name: Nama status: Status +field_options: + directus_activity: + login: Masuk + create: Buat + update: Perbarui + delete: Hapus + directus_collections: + language: Bahasa + archive_divider: Arsip + divider: Sortir + directus_roles: + fields: + icon_name: Ikon + name_name: Nama + collection_list: + fields: + type_name: Tipe + collections_name: Pengumpulan + directus_webhooks: + actions_create: Buat + actions_update: Perbarui comment: Komentar delete_field_are_you_sure: >- Apakah Anda yakin Anda ingin menghapus bidang "{field}" ini? Tindakan ini tidak dapat dikembalikan. diff --git a/app/src/lang/translations/it-IT.yaml b/app/src/lang/translations/it-IT.yaml index 99890cdefd..c25530b025 100644 --- a/app/src/lang/translations/it-IT.yaml +++ b/app/src/lang/translations/it-IT.yaml @@ -21,10 +21,12 @@ #'Proxy', 'Intl' edit_field: Modifica campo conditions: Condizioni +maps: Mappe item_revision: Revisione elemento duplicate_field: Campo duplicato half_width: Metà larghezza full_width: Larghezza Massima +limit: Limite group: Gruppo and: E or: O @@ -36,6 +38,7 @@ role_name: Nome ruolo branch: Ramo leaf: Foglia indeterminate: Indeterminato +edit_collection: Modifica raccolta exclusive: Esclusivo children: Figli db_only_click_to_configure: 'Solo Database: Fare clic per configurare ' @@ -171,17 +174,18 @@ bigInteger: Big Integer boolean: Boolean date: Data datetime: DateTime -decimal: Decimale +decimal: Decimal float: Float integer: Integer json: JSON xml: XML string: String -text: Testo +text: Text time: Ora timestamp: Timestamp uuid: UUID hash: Hash +geometry: Geometria not_available_for_type: Non disponibile per questo typo create_translations: Crea traduzioni auto_refresh: Aggiornamento automatico @@ -380,6 +384,7 @@ no_users_copy: Non ci sono ancora utenti in questo ruolo. webhooks_count: 'Nessun webhook | 1 Webhook | {count} Webhook' no_webhooks_copy: Non ci sono ancora webhook. all_items: Tutti gli Elementi +any: Qualsiasi csv: CSV no_collections: Nessuna Raccolta create_collection: Crea Raccolta @@ -497,6 +502,7 @@ color: Colore circle: Cerchio empty_item: Elemento Vuoto log_in_with: 'Accedi con {provider}' +advanced_settings: Impostazioni avanzate advanced_filter: Filtro Avanzato delete_advanced_filter: Elimina filtro change_advanced_filter_operator: Cambia Operatore @@ -523,6 +529,10 @@ operators: nempty: Non è vuoto all: Contiene queste chiavi has: Contiene alcune di queste chiavi + intersects: Interseca + nintersects: Non interseca + intersects_bbox: Interseca il riquadro di delimitazione + nintersects_bbox: Non interseca il riquadro di delimitazione loading: Caricamento in corso... drop_to_upload: Rilascia per caricare item: Elemento @@ -714,6 +724,7 @@ no_layout_collection_selected_yet: Nessun layout/raccolta ancora selezionato batch_delete_confirm: >- Nessun elemento è stato selezionato | Sei sicuro di voler eliminare questo articolo? Questa azione non può essere annullata. | Sei sicuro di voler eliminare questi {count} elementi? Questa azione non può essere annullata. cancel: Annulla +no_upscale: Non ingrandire le immagini collection: Raccolta collections: Raccolte singleton: Singleton @@ -747,7 +758,7 @@ fields: $thumbnail: Miniatura title: Titolo description: Descrizione - tags: Tags + tags: Tag location: Percorso storage: Archiviazione filename_disk: Nome File (Disco) @@ -776,7 +787,7 @@ fields: location: Percorso title: Titolo description: Descrizione - tags: Tags + tags: Tag user_preferences: Preferenze utente language: Linguaggio theme: Tema @@ -787,7 +798,7 @@ fields: admin_options: Opzioni Amministratore status: Stato status_draft: Bozza - status_invited: Disattivato + status_invited: Invitato status_active: Attivo status_suspended: Sospeso status_archived: Archiviato @@ -797,6 +808,17 @@ fields: last_page: Ultima Pagina last_access: Ultimo Acesso directus_settings: + jpg: JPEG + png: PNG + webP: WebP + tiff: Tiff + basemaps_raster: Raster + basemaps_tile: Raster TileJSON + basemaps_style: Stile Mapbox + mapbox_key: Access Token di Mapbox + mapbox_placeholder: pk.eyJ1Ijo..... + transforms_note: Il nome del metodo di Sharp e i suoi argomenti. Vedi https://sharp.pixelplumbing.com/api-constructor per maggiori informazioni. + additional_transforms: Trasformazioni aggiuntive project_name: Nome del progetto project_url: URL del Progetto project_color: Colore del Progetto @@ -841,10 +863,102 @@ fields: triggers: Trigger actions: Azioni field_options: + directus_settings: + project_name_placeholder: Il mio progetto... + project_logo_note: Sfondo per l'accesso e il logo + public_note_placeholder: Un breve messaggio pubblico che supporta la formattazione markdown... + security_divider_title: Sicurezza + auth_password_policy: + none_text: Nessuna – non consigliato + weak_text: Debole – minimo 8 caratteri + strong_text: Forte – maiuscole / minuscole / numeri / simboli + storage_asset_presets: + fit: + contain_text: Contieni (preserva le proporzioni) + cover_text: Ricopri (forza dimensione esatta) + fit_text: Adatta all'interno + outside_text: Adatta all'esterno + additional_transforms: Trasformazioni aggiuntive + transforms_note: Il nome del metodo di Sharp e i suoi argomenti. Vedi https://sharp.pixelplumbing.com/api-constructor per maggiori informazioni. + mapbox_key: Access Token di Mapbox + mapbox_placeholder: pk.eyJ1Ijo..... + basemaps_raster: Raster + basemaps_tile: Raster TileJSON + basemaps_style: Stile Mapbox + files_divider_title: File e miniature + overrides_divider_title: Personalizzazioni App + directus_activity: + login: Accesso + create: Creare + update: Aggiornare + delete: Elimina directus_collections: track_activity_revisions: Traccia Attività e Revisioni only_track_activity: Traccia solo Attività do_not_track_anything: Non Tracciare nulla + collection_setup: Configurazione Raccolta + note_placeholder: Una descrizione di questa raccolta... + hidden_label: Nascondi nell'app + singleton: Tratta come oggetto singolo + language: Linguaggio + translation: Inserisci una traduzione... + archive_divider: Archivio + archive_field: Scegli un campo... + archive_app_filter: Abilita filtro archiviati in app + archive_value: Valore impostato all'archiviazione... + unarchive_value: Valore impostato al ripristino... + divider: Ordina + sort_field: Scegli un campo... + accountability_divider: Autorizzazioni + directus_files: + title: Un titolo unico... + description: Una descrizione opzionale... + location: Una posizione opzionale... + storage_divider: Denominazione File + filename_disk: Nome sul disco... + filename_download: Nome al download... + directus_roles: + name: Nome unico per questo ruolo... + description: Una descrizione per questo ruolo... + ip_access: Aggiungi gli indirizzi IP consentiti, lascia vuoto per consentirli tutti... + fields: + icon_name: Icona + name_name: Nome + name_placeholder: Inserisci un titolo... + link_name: Link + link_placeholder: URL relativo o assoluto... + collection_list: + group_name_addLabel: Aggiungi Nuovo Gruppo... + fields: + group_name: Nome del gruppo + group_placeholder: Etichetta questo gruppo... + type_name: Tipo + choices_always: Sempre aperto + choices_start_open: Inizia aperto + choices_start_collapsed: Inizia chiuse + collections_name: Raccolte + collections_addLabel: Aggiungi raccolta... + directus_users: + preferences_divider: Preferenze utente + dropdown_auto: Automatico (in base al sistema) + dropdown_light: Tema chiaro + dropdown_dark: Tema scuro + admin_divider: Opzioni Amministratore + status_dropdown_draft: Bozza + status_dropdown_invited: Invitato + status_dropdown_active: Attivo + status_dropdown_suspended: Sospeso + status_dropdown_archived: Archiviato + token: Inserisci un token di accesso sicuro... + directus_webhooks: + status_options_active: Attivo + status_options_inactive: Disattivato + data_label: Invia dati evento + triggers_divider: Trigger + actions_create: Creare + actions_update: Aggiornare + actions_delete: Elimina + actions_login: Accedi no_fields_in_collection: 'Non ci sono ancora campi in "{collection}"' do_nothing: Non Fare Niente generate_and_save_uuid: Genera e Salva UUID @@ -924,6 +1038,7 @@ require_value_to_be_set: Richiedi di impostare il valore translation: Traduzione value: Valore view_project: Visualizza Progetto +weeks: { } report_error: Segnala l'errore start: Inizio interfaces: @@ -1052,7 +1167,21 @@ interfaces: imageToken: Token Immagine imageToken_label: Token (statico) da aggiungere alle sorgenti delle immagini map: + map: Mappa + description: Seleziona una posizione su una mappa zoom: Zoom + geometry_type: Tipo di geometria + geometry_format: Formato geometria + default_view: Vista predefinita + invalid_options: Opzioni non valide + invalid_format: Formato non valido ({format}) + unexpected_geometry: Atteso {expected}, ricevuto {got}. + fit_bounds: Adatta la vista ai dati + native: Nativa + geojson: GeoJSON + lnglat: Longitudine, Latitudine + wkt: WKT + wkb: WKB presentation-notice: notice: Avviso description: Mostra un avviso breve @@ -1082,13 +1211,13 @@ interfaces: description: Seleziona un numero usando un cursore always_show_value: Mostra sempre il valore tags: - tags: Tags + tags: Tag description: Seleziona o aggiungi tag whitespace: Spazio hyphen: Sostituisci con trattino underscore: Sostituisci con trattino basso remove: Rimuovi gli spazi - capitalization: Capitalizzazione + capitalization: Maiuscole/minuscole uppercase: Converti Maiuscole lowercase: Converti Minuscole auto_formatter: Usa la formattazione automatica del titolo @@ -1261,3 +1390,17 @@ layouts: calendar: Calendario start_date_field: Campo Data inizio end_date_field: Campo Data fine + map: + map: Mappa + basemap: Mappa base + layers: Livelli + edit_custom_layers: Modifica i livelli + cluster_options: Opzioni di clustering + cluster: Attiva il clustering + cluster_radius: Raggio del Cluster + cluster_minpoints: Dimensione minima del cluster + cluster_maxzoom: Zoom massimo per il clustering + field: Geometria + invalid_geometry: Geometria non valida + auto_location_filter: Filtra sempre i dati al rettangolo di selezione + search_this_area: Cerca in quest'area diff --git a/app/src/lang/translations/ja-JP.yaml b/app/src/lang/translations/ja-JP.yaml index c3764b514b..1fec170fb1 100644 --- a/app/src/lang/translations/ja-JP.yaml +++ b/app/src/lang/translations/ja-JP.yaml @@ -382,6 +382,29 @@ fields: directus_webhooks: name: 名前 status: ステータス +field_options: + directus_activity: + login: ログイン + create: 作成 + update: 更新 + delete: 削除 + directus_collections: + collection_setup: コレクションの作成 + singleton: シングルオブジェクトに設定する + language: 言語 + archive_divider: アーカイブ + divider: 並べ替え + directus_roles: + fields: + icon_name: アイコン + name_name: 名前 + collection_list: + fields: + type_name: タイプ + collections_name: コレクション + directus_webhooks: + actions_create: 作成 + actions_update: 更新 no_fields_in_collection: 'まだ「{collection}」にフィールドがありません' do_nothing: 何もしない save_current_user_role: 現在のユーザーロールを保存 diff --git a/app/src/lang/translations/ka-GE.yaml b/app/src/lang/translations/ka-GE.yaml index 6730648cca..5c6e0a2e21 100644 --- a/app/src/lang/translations/ka-GE.yaml +++ b/app/src/lang/translations/ka-GE.yaml @@ -56,6 +56,11 @@ fields: description: აღწერა directus_webhooks: status: სტატუსი +field_options: + directus_activity: + create: შექმნა + directus_webhooks: + actions_create: შექმნა comment: კომენტარი description: აღწერა email: ელ-ფოსტა diff --git a/app/src/lang/translations/ko-KR.yaml b/app/src/lang/translations/ko-KR.yaml index 5624108f00..b24df7eab2 100644 --- a/app/src/lang/translations/ko-KR.yaml +++ b/app/src/lang/translations/ko-KR.yaml @@ -144,6 +144,13 @@ fields: translation: 항목 명 번역 directus_roles: name: 권한 이름 +field_options: + directus_collections: + language: 언어 + directus_roles: + collection_list: + fields: + type_name: 타입 interfaces: select-dropdown: choices_value_placeholder: 값 입력 diff --git a/app/src/lang/translations/lt-LT.yaml b/app/src/lang/translations/lt-LT.yaml index e250416392..ce6db90101 100644 --- a/app/src/lang/translations/lt-LT.yaml +++ b/app/src/lang/translations/lt-LT.yaml @@ -737,6 +737,31 @@ fields: directus_webhooks: name: Pavadinimas status: Būsena +field_options: + directus_activity: + login: Prisijungti + create: Kurti + update: Atnaujinti + directus_collections: + collection_setup: Kolekcijos konfigūracija + singleton: Laikyti vienu objektu + language: Kalba + archive_divider: Archyvuoti + divider: Rūšiuoti + directus_roles: + fields: + icon_name: Ikona + name_name: Pavadinimas + collection_list: + fields: + type_name: Tipas + collections_name: Kolekcijos + directus_users: + status_dropdown_active: Aktyvus + directus_webhooks: + status_options_active: Aktyvus + actions_create: Kurti + actions_update: Atnaujinti no_fields_in_collection: 'Kolekcijoje "{collection}" dar nėra elementų' do_nothing: Nieko nedaryti generate_and_save_uuid: Generuoti ir išsaugoti UUID diff --git a/app/src/lang/translations/mn-MN.yaml b/app/src/lang/translations/mn-MN.yaml index 8829ec5935..e198be5eb0 100644 --- a/app/src/lang/translations/mn-MN.yaml +++ b/app/src/lang/translations/mn-MN.yaml @@ -83,6 +83,9 @@ fields: translation: Талбарын нэрний орчуулга directus_roles: name: Үүргийн нэр +field_options: + directus_collections: + archive_divider: Архивлах interfaces: presentation-links: primary: Үндсэн diff --git a/app/src/lang/translations/ms-MY.yaml b/app/src/lang/translations/ms-MY.yaml index 895aa81225..2eba71e4d9 100644 --- a/app/src/lang/translations/ms-MY.yaml +++ b/app/src/lang/translations/ms-MY.yaml @@ -150,6 +150,19 @@ fields: directus_webhooks: name: Nama status: Status +field_options: + directus_activity: + login: Log masuk + create: Cipta + update: Kemaskini + delete: Padam + directus_roles: + fields: + name_name: Nama + collections_name: Koleksi-koleksi + directus_webhooks: + actions_create: Cipta + actions_update: Kemaskini comment: Komen delete_field_are_you_sure: >- Adakah anda pasti ingin memadam medan "{field}"? Tindakan ini tidak dapat dibatalkan. diff --git a/app/src/lang/translations/nl-NL.yaml b/app/src/lang/translations/nl-NL.yaml index ff0373545a..37423c695c 100644 --- a/app/src/lang/translations/nl-NL.yaml +++ b/app/src/lang/translations/nl-NL.yaml @@ -815,10 +815,34 @@ fields: name: Naam status: Status field_options: + directus_activity: + login: Login + create: Maak + update: Updaten + delete: Verwijder directus_collections: track_activity_revisions: Track Activiteiten & Revisies only_track_activity: Track alleen activiteiten do_not_track_anything: Niets tracken + collection_setup: Collectie setup + singleton: Behandel als één object + language: Taal + archive_divider: Archiveren + divider: Sorteren + directus_roles: + fields: + icon_name: Icoon + name_name: Naam + collection_list: + fields: + type_name: Type + collections_name: Collecties + directus_users: + status_dropdown_active: Actief + directus_webhooks: + status_options_active: Actief + actions_create: Maak + actions_update: Updaten no_fields_in_collection: 'Er zijn nog geen velden in "{collection}"' do_nothing: Niets doen generate_and_save_uuid: Genereer een UUID diff --git a/app/src/lang/translations/no-NO.yaml b/app/src/lang/translations/no-NO.yaml index 823e71fbb7..b513981c68 100644 --- a/app/src/lang/translations/no-NO.yaml +++ b/app/src/lang/translations/no-NO.yaml @@ -438,6 +438,27 @@ fields: directus_webhooks: name: Navn status: Status +field_options: + directus_activity: + login: Logg inn + create: Opprett + update: Oppdater + delete: Slett + directus_collections: + language: Språk + archive_divider: Arkiv + divider: Sorter + directus_roles: + fields: + icon_name: Ikon + name_name: Navn + collections_name: Kolleksjoner + directus_users: + status_dropdown_active: Aktiv + directus_webhooks: + status_options_active: Aktiv + actions_create: Opprett + actions_update: Oppdater comment: Kommenter delete_field_are_you_sure: >- Er du sikker på at du vil slette feltet "{field}"? Handlingen kan ikke angres.. diff --git a/app/src/lang/translations/pl-PL.yaml b/app/src/lang/translations/pl-PL.yaml index 32b46240be..800d077b65 100644 --- a/app/src/lang/translations/pl-PL.yaml +++ b/app/src/lang/translations/pl-PL.yaml @@ -20,11 +20,15 @@ #'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', #'Proxy', 'Intl' edit_field: Edytuj pole +maps: Mapy item_revision: Wersja elementu duplicate_field: Duplikuj pole half_width: Połowa szerokości full_width: Pełna szerokość +limit: Limit group: Grupa +and: I +or: Lub fill_width: Szerokość wypełnienia field_name_translations: Tłumaczenie nazwy pola enter_password_to_enable_tfa: Wprowadź hasło, aby włączyć uwierzytelnianie dwuetapowe @@ -33,6 +37,7 @@ role_name: Nazwa roli branch: Gałąź leaf: Arkusz indeterminate: Nieokreślony +edit_collection: Edytuj kolekcję exclusive: Ekskluzywny children: Podrzędne db_only_click_to_configure: 'Tylko baza danych: Kliknij, aby skonfigurować ' @@ -68,6 +73,7 @@ delete_bookmark_copy: >- logoutReason: SIGN_OUT: Wylogowano SESSION_EXPIRED: Sesja wygasła +public_label: Publiczny public_description: Kontroluje dostępność danych API bez uwierzytelniania. not_allowed: Niedozwolone directus_version: Wersja aplikacji Directus @@ -113,6 +119,8 @@ no_access: Brak dostępu use_custom: Użyj niestandardowych nullable: Bez wartości allow_null_value: Zezwalaj na wartość NULL +allow_multiple: Zezwalaj na wiele +allow_multiple_to_be_open: Zezwalaj na otwieranie wielu enter_value_to_replace_nulls: Wprowadź nową wartość, aby zastąpić wszystkie pola NULL. field_standard: Standard field_presentation: Prezentacja i aliasy @@ -176,6 +184,7 @@ time: Czas timestamp: Sygnatura czasowa uuid: UUID hash: Hash +geometry: Geometria not_available_for_type: Niedostępne dla tego typu create_translations: Utwórz tłumaczenia auto_refresh: Automatyczne odświeżanie @@ -306,6 +315,7 @@ drag_mode: Tryb przeciągania cancel_crop: Anuluj przycinanie original: Oryginalny url: Adres URL +import_label: Importuj file_details: Szczegóły pliku dimensions: Wymiary size: Wielkość @@ -373,6 +383,7 @@ no_users_copy: Nie ma jeszcze użytkowników w tej roli. webhooks_count: 'Brak webhooków | Jeden Webhook | {count} Webhooków' no_webhooks_copy: Brak webhooków. all_items: Wszystkie elementy +any: Dowolny csv: CSV no_collections: Brak kolekcji create_collection: Stwórz kolekcję @@ -383,6 +394,7 @@ display_template_not_setup: Opcja wyświetlania szablonu jest nieprawidłowo sko collection_field_not_setup: Opcja pola kolekcji jest nieprawidłowo skonfigurowana select_a_collection: Wybierz kolekcje active: Aktywne +inactive: Nieaktywny users: Użytkownicy activity: Aktywność webhooks: Webhooki @@ -486,6 +498,7 @@ color: Kolor circle: Okrąg empty_item: Pusty element log_in_with: 'Zaloguj się za pomocą {provider}' +advanced_settings: Zaawansowane Ustawienia advanced_filter: Filtr zaawansowany delete_advanced_filter: Usuń filtr change_advanced_filter_operator: Zmień operatora @@ -766,6 +779,7 @@ fields: language: Język theme: Motyw tfa_secret: Uwierzytelnianie dwuetapowe + admin_options: Opcje administratora status: Status status_active: Aktywne role: Rola @@ -808,10 +822,37 @@ fields: name: Nazwa status: Status field_options: + directus_activity: + login: Zaloguj się + create: Stwórz + update: Zaktualizuj + delete: Usuń directus_collections: track_activity_revisions: Śledź aktywność i rewizje only_track_activity: Tylko Śledź Aktywność do_not_track_anything: Nie śledź niczego + collection_setup: Ustawienia kolekcji + singleton: Traktuj jako pojedynczy obiekt + language: Język + archive_divider: Archiwum + divider: Sortuj + directus_roles: + fields: + icon_name: Ikona + name_name: Nazwa + name_placeholder: Wprowadź tytuł... + collection_list: + fields: + type_name: Typ + collections_name: Kolekcje + directus_users: + admin_divider: Opcje administratora + status_dropdown_active: Aktywne + directus_webhooks: + status_options_active: Aktywne + status_options_inactive: Nieaktywny + actions_create: Stwórz + actions_update: Zaktualizuj no_fields_in_collection: 'Nie ma jeszcze żadnych pól w "{collection}"' do_nothing: Nic nie rób generate_and_save_uuid: Generuj i zapisz UUID @@ -1197,3 +1238,5 @@ layouts: calendar: Kalendarz start_date_field: Pole daty rozpoczęcia end_date_field: Pole daty zakończenia + map: + field: Geometria diff --git a/app/src/lang/translations/pt-BR.yaml b/app/src/lang/translations/pt-BR.yaml index b87ef80d69..3d9b693ae7 100644 --- a/app/src/lang/translations/pt-BR.yaml +++ b/app/src/lang/translations/pt-BR.yaml @@ -19,12 +19,20 @@ #'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array', 'Map', 'Set', 'WeakMap', 'WeakSet', #'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', #'Proxy', 'Intl' +published: Publicado +draft: Rascunho +archived: Arquivado edit_field: Editar campo +conditions: Condições +maps: Mapas item_revision: Revisão do item duplicate_field: Campo duplicado half_width: Meia largura full_width: Largura total +limit: Limite group: Grupos +and: E +or: Ou fill_width: Preencher field_name_translations: Traduções Dos Nomes Dos Campos enter_password_to_enable_tfa: Digite sua senha para ativar a Autenticação em Duas Etapas @@ -33,9 +41,10 @@ role_name: Nome do cargo branch: Branch leaf: Folha indeterminate: Indeterminado +edit_collection: Editar Coleção exclusive: Exclusivo children: Filhos -db_only_click_to_configure: 'Apenas o banco de dados: clique para configurar' +db_only_click_to_configure: 'Apenas o banco de dados: clique para configurar ' show_archived_items: Mostrar itens arquivados edited: Valor editado required: Obrigatório @@ -44,7 +53,7 @@ requires_value: Requer valor create_preset: Criar predefinição create_role: Criar Função create_user: Criar usuário -create_webhook: Criar Webhook +create_webhook: Criar Webgancho invite_users: Convidar Usuários email_examples: "admin{'@'}exemplo.com, usuário{'@'}exemplo.com..." invite: Convidar @@ -66,8 +75,9 @@ delete_bookmark: Deletar Marcador delete_bookmark_copy: >- Tem certeza que deseja deletar o marcador "{bookmark}"? Esta ação não pode ser desfeita. logoutReason: - SIGN_OUT: Deslogado + SIGN_OUT: Desconectado SESSION_EXPIRED: Sessão expirada +public_label: Publico public_description: Controla quais dados da API estão disponíveis sem autenticação. not_allowed: Não permitido directus_version: Versão do Directus @@ -75,20 +85,20 @@ node_version: Versão Node node_uptime: Node Uptime os_type: Tipo de SO os_version: Versão do SO -os_uptime: Uptime do SO +os_uptime: Tempo de atividade do SO os_totalmem: Memória do SO archive: Arquivar archive_confirm: Tem certeza de que deseja arquivar este item? archive_confirm_count: >- Nenhum Item Selecionado | Tem certeza que deseja arquivar este item? | Você tem certeza que deseja arquivar estes {count} itens? -reset_system_permissions_to: 'Redefinir permissões do sistema para:' -reset_system_permissions_copy: Esta ação irá sobrescrever quaisquer permissões personalizadas que você pode ter aplicado às coleções do sistema. Tem certeza? -the_following_are_minimum_permissions: As seguintes são as permissões mínimas necessárias quando o "Acesso ao Aplicativo" estiver ativado. Você pode estender as permissões além disto, mas não abaixo. -app_access_minimum: Acesso ao App Mínimo +reset_system_permissions_to: 'Redefinir Permissões do Sistema Para:' +reset_system_permissions_copy: Esta ação irá sobrescrever quaisquer permissões personalizadas que você pode ter aplicado às coleções do sistema. Você tem certeza? +the_following_are_minimum_permissions: As permissões seguintes, são as mínimas necessárias quando o "Acesso ao Aplicativo" estiver ativado. Você pode estender as permissões além disso, mas não abaixo. +app_access_minimum: Acesso Mínimo ao App recommended_defaults: Padrões Recomendados unarchive: Desarquivar unarchive_confirm: Tem certeza de que deseja desarquivar este item? -nested_files_folders_will_be_moved: Arquivos e pastas aninhadas serão movidos a um nível acima. +nested_files_folders_will_be_moved: Arquivos e pastas aninhadas serão movidos para um nível acima. unknown_validation_errors: 'Houve erros de validação para os seguintes campos ocultos:' validationError: eq: O valor tem de ser {valid} @@ -113,6 +123,8 @@ no_access: Sem acesso use_custom: Usar Personalizado nullable: Aceita nulos allow_null_value: Permitir valores NULL +allow_multiple: Permitir Múltiplos +allow_multiple_to_be_open: Permissão para abrir vários enter_value_to_replace_nulls: Por favor digite um novo valor que substituirá todos os NULLs presentes neste campo. field_standard: Padrão field_presentation: Apresentação e pseudônimos @@ -176,6 +188,7 @@ time: Hora timestamp: Timestamp uuid: UUID hash: Hash +geometry: Geometria not_available_for_type: Não disponível para este tipo create_translations: Criar Traduções auto_refresh: Atualização automática @@ -306,6 +319,7 @@ drag_mode: Modo de arrastar cancel_crop: Cancelar Recorte original: Original url: URL +import_label: Importar file_details: Detalhes do Arquivo dimensions: Dimensões size: Tamanho @@ -373,6 +387,7 @@ no_users_copy: Não há usuários neste cargo ainda. webhooks_count: 'Nenhum Webhook | Um Webhook | {count} Webhooks' no_webhooks_copy: Ainda não possui Webhooks. all_items: Todos os itens +any: Qualquer csv: CSV no_collections: Sem coleções create_collection: Criar Coleção @@ -383,6 +398,7 @@ display_template_not_setup: A opção do modelo de exibição está configurada collection_field_not_setup: A opção do campo de coleção está configurada incorretamente select_a_collection: Selecione uma coleção active: Ativo +inactive: Inativo users: Usuários activity: Atividade webhooks: Webhooks @@ -395,6 +411,7 @@ documentation: Documentação sidebar: Barra lateral duration: Duração charset: Codificação de caracteres +second: segundo file_moved: Arquivo movido collection_created: Coleção criada modified_on: Modificado em @@ -433,8 +450,10 @@ errors: USER_SUSPENDED: Usuário suspenso CONTAINS_NULL_VALUES: O campo contém valores nulos UNKNOWN: Erro inesperado + UNPROCESSABLE_ENTITY: Entidade não processável INTERNAL_SERVER_ERROR: Erro inesperado NOT_NULL_VIOLATION: Valor não pode ser nulo +security: Segurança value_hashed: Valor secretamente salvo em hash bookmark_name: Nome do marcador... create_bookmark: Criar marcador @@ -486,6 +505,7 @@ color: Cor circle: Círculo empty_item: Item vazio log_in_with: 'Entrar com {provider}' +advanced_settings: Configurações Avançadas advanced_filter: Filtro avançado delete_advanced_filter: Excluir Filtro change_advanced_filter_operator: Modificar Operador @@ -512,6 +532,10 @@ operators: nempty: Não está vazio all: Contém essas chaves has: Contém algumas dessas chaves + intersects: Interseções + nintersects: Não interceptado + intersects_bbox: Cruza a caixa delimitadora + nintersects_bbox: Não cruza a caixa delimitadora loading: Carregando... drop_to_upload: Solte para enviar item: Item @@ -550,10 +574,12 @@ no_results_copy: Ajuste ou limpe os filtros de busca para ver resultados. clear_filters: Limpar filtros saves_automatically: Salva automaticamente role: Função +rule: Regra user: Usuário no_presets: Sem predefinições no_presets_copy: Nenhuma predefinição ou marcador foi salvo ainda. no_presets_cta: Adicionar predefinição +presets_only: Apenas predefinições create: Criar on_create: Ao Criar on_update: Pós-alteração @@ -569,6 +595,7 @@ label: Rótulo image_url: URL da Imagem alt_text: Texto Alternativo media: Mídia +quality: Qualidade width: Largura height: Altura source: Código @@ -763,27 +790,46 @@ fields: title: Título description: Descrição tags: Tags + user_preferences: Preferências do Usuário language: Idioma theme: Tema + theme_auto: Automático (Baseado no Sistema) + theme_light: Modo Claro + theme_dark: Modo Escuro tfa_secret: Autenticação em Duas Etapas + admin_options: Opções do Admin status: Status + status_draft: Rascunho + status_invited: Convidado status_active: Ativo + status_suspended: Suspenso + status_archived: Arquivado role: Função token: Token + token_placeholder: Digite um token de acesso seguro... last_page: Última página last_access: Último acesso directus_settings: + jpg: JPEG + png: PNG + webP: WebP + tiff: Tiff + additional_transforms: Transformações Adicionais project_name: Nome do projeto project_url: URL do projeto project_color: Cor do projeto project_logo: Logotipo do projeto + public_pages: Páginas Públicas public_foreground: Primeiro plano público public_background: Fundo público public_note: Nota pública auth_password_policy: Política de senha para autenticação auth_login_attempts: Tentativas de autenticação + files_and_thumbnails: Arquivos e Miniaturas + storage_default_folder: Pasta de Armazenamento Padrão storage_asset_presets: Predefinições de armazenamento storage_asset_transform: Transformação de arquivos + overrides: Substituições de Aplicativos custom_css: CSS Personalizado directus_fields: collection: Nome da coleção @@ -806,12 +852,63 @@ fields: collection_list: Navegação da Coleção directus_webhooks: name: Nome + method: Método status: Status + data: Dados + data_label: Enviar Dados do Evento + triggers: Gatilhos + actions: Ações field_options: + directus_settings: + security_divider_title: Segurança + auth_password_policy: + none_text: Nenhum – Não Recomendado + additional_transforms: Transformações Adicionais + files_divider_title: Arquivos e Miniaturas + overrides_divider_title: Substituições de Aplicativos + directus_activity: + login: Entrar + create: Criar + update: Alteração + delete: Excluir directus_collections: track_activity_revisions: Rastrear Atividades e Revisões only_track_activity: Acompanhar apenas Atividade do_not_track_anything: Não rastreie nada + collection_setup: Configurando coleção + singleton: Tratar como objeto único + language: Idioma + archive_divider: Arquivar + divider: Ordenar + directus_roles: + fields: + icon_name: Ícone + name_name: Nome + name_placeholder: Insira um título... + collection_list: + fields: + type_name: Tipo + collections_name: Coleções + directus_users: + preferences_divider: Preferências do Usuário + dropdown_auto: Automático (Baseado no Sistema) + dropdown_light: Modo Claro + dropdown_dark: Modo Escuro + admin_divider: Opções do Admin + status_dropdown_draft: Rascunho + status_dropdown_invited: Convidado + status_dropdown_active: Ativo + status_dropdown_suspended: Suspenso + status_dropdown_archived: Arquivado + token: Digite um token de acesso seguro... + directus_webhooks: + status_options_active: Ativo + status_options_inactive: Inativo + data_label: Enviar Dados do Evento + triggers_divider: Gatilhos + actions_create: Criar + actions_update: Alteração + actions_delete: Excluir no_fields_in_collection: 'Ainda não há campos em "{collection}"' do_nothing: Não fazer nada generate_and_save_uuid: Gerar e salvar UUID @@ -829,10 +926,13 @@ referential_action_cascade: Excluir o item {collection} (cascade) referential_action_set_null: Anular o campo {field} referential_action_set_default: Definir {field} para o seu valor padrão choose_action: Escolha uma ação +continue_label: Continuar continue_as: >- {name} está autenticado. Se você reconhece esta conta, clique em continuar. editing_role: 'Cargo {role}' creating_webhook: Criando Webhook +default_label: Padrão +delete_label: Excluir delete_are_you_sure: >- Esta ação é permanente e não pode ser desfeita. Tem certeza de que deseja prosseguir? delete_field_are_you_sure: >- @@ -884,11 +984,19 @@ sort_direction: Direção da ordenação sort_asc: Ordenação Crescente sort_desc: Ordenação Decrescente template: Template +require_value_to_be_set: Valor a ser definido é requerido translation: Tradução value: Valor view_project: Visualizar projeto report_error: Relatar erro +start: Início interfaces: + group-accordion: + name: Sanfonada + description: Exibir campos ou grupos como seções sanfonadas + start: Início + all_closed: Todos Fechados + first_opened: Primeiro Aberto presentation-links: presentation-links: Links do botão links: Links @@ -1004,6 +1112,12 @@ interfaces: imageToken_label: Qual token (estático) a ser adicionado nas imagens map: zoom: Zoom + geometry_type: Tipo de geometria + geometry_format: Formato da geometria + default_view: Visualização padrão + invalid_options: Opções inválidas + invalid_format: Formato inválido ({format}) + unexpected_geometry: Esperado {expected}, obtido {got}. presentation-notice: notice: Aviso description: Mostrar um breve aviso @@ -1201,3 +1315,5 @@ layouts: calendar: Calendário start_date_field: Campo de Data de Início end_date_field: Campo de Data Final + map: + field: Geometria diff --git a/app/src/lang/translations/pt-PT.yaml b/app/src/lang/translations/pt-PT.yaml index be757ad0ed..70836b798c 100644 --- a/app/src/lang/translations/pt-PT.yaml +++ b/app/src/lang/translations/pt-PT.yaml @@ -637,10 +637,28 @@ fields: name: Nome status: Estado field_options: + directus_activity: + create: Criar + update: Atualizar + delete: Remover directus_collections: track_activity_revisions: Rastrear Atividades e Revisões only_track_activity: Apenas rastrear Atividade do_not_track_anything: Não rastrear nada + collection_setup: Configuração da coleção + language: Idioma + archive_divider: Arquivar + divider: Ordenar + directus_roles: + fields: + name_name: Nome + collection_list: + fields: + type_name: Tipo + directus_users: + status_dropdown_active: Ativar + directus_webhooks: + status_options_active: Ativar no_fields_in_collection: 'Ainda não há nenhum campo em "{collection}"' do_nothing: Não fazer nada generate_and_save_uuid: Gerar e guardar UUID diff --git a/app/src/lang/translations/ro-RO.yaml b/app/src/lang/translations/ro-RO.yaml index 76cd76eda2..2498ca9eab 100644 --- a/app/src/lang/translations/ro-RO.yaml +++ b/app/src/lang/translations/ro-RO.yaml @@ -162,6 +162,17 @@ fields: description: Descriere directus_webhooks: status: Stare +field_options: + directus_activity: + create: Creează + directus_collections: + archive_divider: Arhivează + directus_roles: + fields: + icon_name: Iconiță + collections_name: Colecții + directus_webhooks: + actions_create: Creează comment: Comentariu delete_field_are_you_sure: >- Sunteţi sigur că doriţi să ştergeţi câmpul "{field}"? Această acţiune nu poate fi modificată ulterior. diff --git a/app/src/lang/translations/ru-RU.yaml b/app/src/lang/translations/ru-RU.yaml index edd2687788..46b25f41e0 100644 --- a/app/src/lang/translations/ru-RU.yaml +++ b/app/src/lang/translations/ru-RU.yaml @@ -21,17 +21,22 @@ #'Proxy', 'Intl' edit_field: Редактировать поле conditions: Условия +maps: Карты item_revision: Редакция duplicate_field: Клонировать поле half_width: Полширины full_width: Вся ширина +limit: Лимит group: Группа +and: и +or: или fill_width: В ширину -field_name_translations: Переводы Названия Поля -enter_password_to_enable_tfa: Введите свой пароль для включения Двухфакторной Аутентификации +field_name_translations: Переводы названия поля +enter_password_to_enable_tfa: Введите свой пароль для включения двухфакторной аутентификации add_field: Добавить поле role_name: Название роли branch: Ветка +edit_collection: Изменить коллекцию children: Дочерние элементы db_only_click_to_configure: 'Только База данных: Нажмите для Настройки ' show_archived_items: Показать элементы в архиве @@ -44,13 +49,14 @@ create_role: Создать роль create_user: Создать пользователя create_webhook: Создать веб-хук invite_users: Пригласить пользователей +email_examples: "admin{'@'}example.com, user{'@'}example.com..." invite: Пригласить email_already_invited: На адрес "{email}" уже было отправлено приглашение emails: Email-адреса connection_excellent: Отличное Подключение -connection_good: Хорошее Подключение -connection_fair: Нормальное Подключение -connection_poor: Плохое Подключение +connection_good: Хорошее соединение +connection_fair: Сносное соединение +connection_poor: Плохое соединение primary: Первичный rename_folder: Переименовать папку delete_folder: Удалить папку @@ -61,7 +67,7 @@ rename_bookmark: Переименовать закладку update_bookmark: Обновить закладку delete_bookmark: Удалить закладку delete_bookmark_copy: >- - Вы уверены, что хотите удалить закладку "{bookmark}"? Это действие не может быть отменено. + Точно удалить закладку «{bookmark}»? Ее нельзя будет восстановить. logoutReason: SIGN_OUT: Вы вышли SESSION_EXPIRED: Сессия истекла @@ -81,6 +87,7 @@ archive_confirm_count: >- Элементы не выбраны | Вы уверены, что хотите архивировать этот элемент? | Вы уверены, что хотите архивировать эти {count} элементов? reset_system_permissions_to: 'Сбросить системные разрешения для:' reset_system_permissions_copy: Это перезапишет любые индивидуальные разрешения, которые вы могли задать системным коллекциям. Вы уверены? +the_following_are_minimum_permissions: Ниже приведены разрешения, требуемые при включенном доступе к приложению. Их можно расширить, но не сократить. app_access_minimum: Минимальный доступ приложения recommended_defaults: Рекомендуемые значения по умолчанию unarchive: Извлечь их архива @@ -111,13 +118,15 @@ use_custom: Использовать Свой nullable: Разрешить NULL значение allow_null_value: Разрешить NULL значение allow_multiple: Разрешить несколько +allow_multiple_to_be_open: Можно открыть несколько +enter_value_to_replace_nulls: Пожалуйста, введите новое значение для замены всех NULL в текущем поле. field_standard: Стандарт field_presentation: Представление и Алиасы field_file: Один Файл -field_files: Несколько Файлов +field_files: Несколько файлов field_m2o: Отношение M2O field_m2a: Отношение M2A -field_o2m: Связка O2M +field_o2m: Один ко многим field_m2m: Отношение M2M field_translations: Переводы field_group: Группа полей @@ -142,7 +151,7 @@ field_update_success: 'Обновлено поле: "{field}"' duplicate_where_to: Куда вы хотите дублировать это поле? language: Язык global: Глобальный -admins_have_all_permissions: Администраторы имеют все права +admins_have_all_permissions: У администраторов есть все права camera: Камера exposure: Экспозиция shutter: Затвор @@ -172,11 +181,13 @@ time: Время timestamp: Отметка времени uuid: UUID hash: Хэш +geometry: Геометрия not_available_for_type: Недоступно для этого Типа create_translations: Создать Переводы auto_refresh: Авто обновление refresh_interval: Интервал обновления no_refresh: Не обновлять +refresh_interval_seconds: Мгновенное обновление | Каждую секунду | Каждые {seconds} секунд refresh_interval_minutes: Каждую минуту | Каждые {minutes} минут auto_generate: Авто-Генерация this_will_auto_setup_fields_relations: Это автоматически настроит все необходимые поля и отношения. @@ -193,12 +204,12 @@ add_m2o_to_collection: 'Добавить Many-to-One в "{collection}"' add_o2m_to_collection: 'Добавить One-to-Many в "{collection}"' add_m2m_to_collection: 'Добавить Many-to-Many в "{collection}"' choose_a_type: Выберите Тип... -determined_by_relationship: Определяется Связкой +determined_by_relationship: Определяется отношением add_note: Добавить заметку для пользователей... default_value: Значение По умолчанию standard_field: Стандартное Поле single_file: Один Файл -multiple_files: Несколько Файлов +multiple_files: Несколько файлов m2o_relationship: Отношение Многие к Одному o2m_relationship: Связка One to Many m2m_relationship: Отношение Многие ко Многим @@ -275,11 +286,11 @@ today: Сегодня yesterday: Вчера delete_comment: Удалить комментарий date-fns_date: PPP -date-fns_time: 'h:mm:ss a' -date-fns_time_no_seconds: 'h:mm a' -date-fns_date_short: 'MMM d, u' -date-fns_time_short: 'h:mma' -date-fns_date_short_no_year: MMM d +date-fns_time: 'HH:mm:ss' +date-fns_time_no_seconds: 'HH:mm' +date-fns_date_short: 'd MMM u' +date-fns_time_short: 'HH:mm' +date-fns_date_short_no_year: d MMM month: Месяц year: Год select_all: Выбрать Все @@ -368,6 +379,7 @@ no_users_copy: В этой роли пока нет пользователей. webhooks_count: 'Нет Веб-хуков | Один Веб-хук | {count} Веб-хуков' no_webhooks_copy: Веб-хуков пока нет. all_items: Все Элементы +any: Любой csv: CSV no_collections: Нет Коллекций create_collection: Создать Коллекцию @@ -378,6 +390,7 @@ display_template_not_setup: Опция шаблона отображения н collection_field_not_setup: Опция поля коллекции настроена некорректно select_a_collection: Выберите Коллекцию active: Активный +inactive: Неактивный users: Пользователи activity: Активность webhooks: Веб-хуки @@ -431,6 +444,7 @@ errors: UNPROCESSABLE_ENTITY: Необрабатываемый объект INTERNAL_SERVER_ERROR: Неожиданная Ошибка NOT_NULL_VIOLATION: Значение не может быть null +security: Безопасность value_hashed: Значение Безопасно Хэшировано bookmark_name: Название закладки... create_bookmark: Создать Закладку @@ -457,6 +471,7 @@ delete_collection: Удалить Коллекцию update_collection_success: Коллекция Обновлена delete_collection_success: Коллекция Удалена start_end_of_count_items: '{start}-{end} из {count} элементов' +start_end_of_count_filtered_items: '{start}-{end} из {count} отфильтрованных элементов' one_item: '1 элемент' one_filtered_item: '1 отфильтрованный элемент' delete_collection_are_you_sure: >- @@ -480,6 +495,7 @@ color: Цвет circle: Круг empty_item: Пустой Элемент log_in_with: 'Войти с помощью {provider}' +advanced_settings: Дополнительные настройки advanced_filter: Расширенный Фильтр delete_advanced_filter: Удалить Фильтр change_advanced_filter_operator: Изменить Оператор @@ -506,6 +522,8 @@ operators: nempty: Не пустой all: Содержит эти ключи has: Содержит некоторые из этих ключей + intersects: Пересекает + nintersects: Не пересекает loading: Загрузка... drop_to_upload: Перетащите для Загрузки item: Элемент @@ -562,6 +580,7 @@ label: Метка image_url: URL изображения alt_text: Альтернативный текст media: Медиа +quality: Качество width: Ширина height: Высота source: Источник @@ -725,10 +744,10 @@ fields: title: Заголовок description: Описание tags: Теги - location: Местоположение - storage: Память + location: Локация + storage: Место хранения filename_disk: Имя файла (на диске) - filename_download: Имя файла (Загруженое) + filename_download: Имя файла (при загрузке) metadata: Метаданные type: MIME-тип filesize: Размер файла @@ -750,31 +769,49 @@ fields: email: Email password: Пароль avatar: Аватар - location: Местоположение + location: Локация title: Заголовок description: Описание tags: Теги + user_preferences: Пользовательские настройки language: Язык theme: Тема + theme_auto: Автоматически (как в системе) + theme_light: Светлая тема + theme_dark: Тёмная тема tfa_secret: Двухфакторная аутентификация + admin_options: Настройки администратора status: Статус + status_draft: Черновик + status_invited: Приглашен(а) status_active: Активный + status_suspended: Приостановлен + status_archived: В архиве role: Роль - token: Жетон + token: Ключ доступа + token_placeholder: Укажите достаточно защищенный ключ доступа last_page: Последняя Страница last_access: Последний Доступ directus_settings: - project_name: Имя Проекта - project_url: URL Проекта + jpg: JPEG + png: PNG + webP: WebP + tiff: Tiff + project_name: Название проекта + project_url: URL проекта project_color: Цвет Проекта - project_logo: Логотип Проекта - public_foreground: Публичный Передний План - public_background: Публичный Фон - public_note: Публичное Примечание - auth_password_policy: Политика Паролей Входа - auth_login_attempts: Попыток Входа + project_logo: Логотип проекта + public_pages: Публичные страницы + public_foreground: Изображение на переднем плане + public_background: Фоновое изображение + public_note: Заметка + auth_password_policy: Минимальная сложность пароля + auth_login_attempts: Попыток войти + files_and_thumbnails: Файлы и миниатюры + storage_default_folder: Основная папка storage_asset_presets: Пресеты Элементов Хранилища storage_asset_transform: Преобразование Элементов Хранилища + overrides: Переопределить заводские настройки custom_css: Пользовательский CSS directus_fields: collection: Название Коллекции @@ -782,7 +819,7 @@ fields: note: Заметка hidden: Скрыто singleton: Синглтон - translation: Переводы Названия Поля + translation: Переводы названия поля display_template: Шаблон directus_roles: name: Название роли @@ -797,12 +834,69 @@ fields: collection_list: Навигация по Коллекциям directus_webhooks: name: Имя + method: Метод status: Статус + data: Данные + data_label: Отправлять данные о событии + triggers: Триггеры + actions: Действия field_options: + directus_settings: + security_divider_title: Безопасность + files_divider_title: Файлы и миниатюры + overrides_divider_title: Переопределить заводские настройки + directus_activity: + login: Войти + create: Создать + update: Обновить + delete: Удалить directus_collections: track_activity_revisions: Отслеживание активности и изменений only_track_activity: Отслеживать только активность do_not_track_anything: Ничего не отслеживать + collection_setup: Настройки Коллекции + singleton: Считать одним объектом + language: Язык + translation: Введите перевод... + archive_divider: Архив + archive_field: Выберите поле... + divider: Сортировать + sort_field: Выберите поле... + directus_files: + title: Уникальное название... + description: Описание (необязательно)... + directus_roles: + fields: + icon_name: Иконка + name_name: Имя + name_placeholder: Введите название... + collection_list: + fields: + type_name: Тип + choices_start_open: Раскрыта + collections_name: Коллекции + collections_addLabel: Добавить коллекцию... + directus_users: + preferences_divider: Пользовательские настройки + dropdown_auto: Автоматически (как в системе) + dropdown_light: Светлая тема + dropdown_dark: Тёмная тема + admin_divider: Настройки администратора + status_dropdown_draft: Черновик + status_dropdown_invited: Приглашен(а) + status_dropdown_active: Активный + status_dropdown_suspended: Приостановлен + status_dropdown_archived: В архиве + token: Укажите достаточно защищенный ключ доступа + directus_webhooks: + status_options_active: Активный + status_options_inactive: Неактивный + data_label: Отправлять данные о событии + triggers_divider: Триггеры + actions_create: Создать + actions_update: Обновить + actions_delete: Удалить + actions_login: Войти no_fields_in_collection: 'В коллекции "{collection}" пока нет полей' do_nothing: Ничего не Делать generate_and_save_uuid: Создать и Сохранить UUID @@ -944,6 +1038,7 @@ interfaces: select_a_collection: Выберите Коллекцию presentation-divider: divider: Разделитель + description: Разделить поля на секции с заголовками title_placeholder: Введите название... inline_title: Строчный заголовок inline_title_label: Показать заголовок внутри строки @@ -953,7 +1048,7 @@ interfaces: description: Выберите значение из выпадающего списка choices_placeholder: Добавить вариант allow_other: Разрешить другие - allow_other_label: Допускаются и другие значения + allow_other_label: Разрешить другие варианты allow_none: Может быть пустым allow_none_label: Можно не указывать значение choices_name_placeholder: Введите название... @@ -1000,18 +1095,34 @@ interfaces: customSyntax_add: Добавить произвольный синтаксис box: Блок / Строчный элемент imageToken: Ключ изображения + imageToken_label: Статичный ключ, добавляемый к адресам изображений map: + map: Карта + description: Указать место на карте zoom: Масштаб + geometry_type: Тип геометрии + geometry_format: Формат + default_view: Вид по умолчанию + invalid_options: Некорректные настройки + invalid_format: Некорректный формат ({format}) + unexpected_geometry: Ожидалось {expected}, а встретилось {got}. + native: Родной + geojson: GeoJSON + lnglat: Долгота, широта (Lon, Lat) + wkt: WKT + wkb: WKB presentation-notice: notice: Уведомление description: Показать короткое уведомление text: Введите содержание уведомления здесь... list-o2m: one-to-many: Один ко многим + description: Выберите связанные элементы no_collection: Коллекция не найдена system-folder: folder: Папка description: Выбрать папку + field_hint: Папка для новых файлов. Ранее загруженные файлы останутся на месте. root_name: Корень файловой библиотеки system_default: Системные настройки по умолчанию select-radio: @@ -1066,6 +1177,9 @@ interfaces: translations: display_template: Шаблон отображения no_collection: Нет Коллекций + list-o2m-tree-view: + description: В виде дерева рекурсивно вложенных элементов + recursive_only: Дерево можно выбрать для рекурсивных отношений user: user: Пользователь description: Выберите существующего пользователя directus @@ -1082,12 +1196,17 @@ interfaces: options_override: Переопределение параметров input-autocomplete-api: input-autocomplete-api: Автоматическое дополнение ввода (API) + results_path: Путь к результатам value_path: Путь значения trigger: Триггер + rate: Рейтинг group-detail: + description: Отображать поля как сворачиваемую секцию show_header: Показывать заголовок группы header_icon: Иконка заголовка header_color: Цвет заголовка + start_open: Раскрыта + start_closed: Свёрнута displays: boolean: boolean: Логическое @@ -1192,3 +1311,16 @@ layouts: calendar: Календарь start_date_field: Поле даты начала end_date_field: Поле даты окончания + map: + map: Карта + basemap: Основа карты + layers: Слои + edit_custom_layers: Редактировать слои + cluster_options: Настройки кластеров + cluster: Использовать кластеры + cluster_radius: Радиус кластера + cluster_minpoints: Минимальный размер кластера + cluster_maxzoom: Максимальный масштаб для кластеров + field: Геометрия + invalid_geometry: Некорректная геометрия + search_this_area: Искать в этой области diff --git a/app/src/lang/translations/sl-SI.yaml b/app/src/lang/translations/sl-SI.yaml index 4ba81cc0d2..147deb762a 100644 --- a/app/src/lang/translations/sl-SI.yaml +++ b/app/src/lang/translations/sl-SI.yaml @@ -21,10 +21,12 @@ #'Proxy', 'Intl' edit_field: Uredi polje conditions: Pogoji +maps: Zemljevidi item_revision: Revizija postavke duplicate_field: Podvajanje polja half_width: Polovična širina full_width: Polna širina +limit: Omejitev group: Skupina and: in or: ali @@ -182,6 +184,7 @@ time: Ura timestamp: Časovni žig uuid: UUID hash: Hash +geometry: Geometrija not_available_for_type: Ni na razpolago za ta tip create_translations: Ustvari nove prevode auto_refresh: Samodejno osveži @@ -380,6 +383,7 @@ no_users_copy: Ta vloga še nima uporabnikov. webhooks_count: 'Ni prožilnikov | En prožilnik | Dva prožilnika | Trije prožilniki | Štirje prožilniki | {count} prožilnikov' no_webhooks_copy: Prožilniki ne obstajajo. all_items: Vsi zapisi +any: Katerokoli csv: CSV no_collections: Ni zbirk create_collection: Ustvari zbirko @@ -497,6 +501,7 @@ color: Barva circle: Krog empty_item: Prazen element log_in_with: 'Prijava z {provider}' +advanced_settings: Napredne nastavitve advanced_filter: Napredni filter delete_advanced_filter: Izbriši filter change_advanced_filter_operator: Spremeni operacijo @@ -523,6 +528,10 @@ operators: nempty: Ni prazno all: Vsebuje naslednje ključe has: Vsebuje nekatere ključe + intersects: Presek + nintersects: Ni preseka + intersects_bbox: Seka meje + nintersects_bbox: Ne seka mej loading: Nalaganje ... drop_to_upload: Spustite za prenos item: Zapis @@ -787,7 +796,6 @@ fields: admin_options: Administracijske opcije status: Stanje status_draft: Osnutek - status_invited: Neaktivno status_active: Aktivno status_suspended: Zaustavljeno status_archived: Arhivirano @@ -841,10 +849,53 @@ fields: triggers: Sprožilci actions: Dejanja field_options: + directus_settings: + security_divider_title: Varnost + files_divider_title: Datoteke & sličice + overrides_divider_title: Aplikacijske preglasitve + directus_activity: + login: Prijava + create: Ustvari + update: Posodobi + delete: Izbriši directus_collections: track_activity_revisions: Sledi aktivnosti in revizije only_track_activity: Sledi samo aktivnosti do_not_track_anything: Ne sledi ničesar + collection_setup: Nastavitve zbirke + singleton: Obravnavaj kot en objekt + language: Jezik + archive_divider: Arhiviraj + divider: Razvrsti + directus_roles: + fields: + icon_name: Ikona + name_name: Ime + name_placeholder: Vnesite naslov ... + collection_list: + fields: + type_name: Tip + choices_start_open: Začni odprto + collections_name: Zbirke + directus_users: + preferences_divider: Uporabniške nastavitve + dropdown_auto: Avtomatsko (sistemsko) + dropdown_light: Svetla tema + dropdown_dark: Temna tema + admin_divider: Administracijske opcije + status_dropdown_draft: Osnutek + status_dropdown_active: Aktivno + status_dropdown_suspended: Zaustavljeno + status_dropdown_archived: Arhivirano + token: Vnesite žeton ... + directus_webhooks: + status_options_active: Aktivno + status_options_inactive: Neaktivno + data_label: Pošlji podatke dogodka + triggers_divider: Sprožilci + actions_create: Ustvari + actions_update: Posodobi + actions_delete: Izbriši no_fields_in_collection: 'Zbirka {collection} še nima polj' do_nothing: Ne stori ničesar generate_and_save_uuid: Ustvari in shrani UUID @@ -1052,7 +1103,21 @@ interfaces: imageToken: Žeton slike imageToken_label: Kateri statični žeton se naj doda k sliki map: + map: Zemljevid + description: Izberite lokacijo zoom: Povečaj + geometry_type: Tip geometrije + geometry_format: Geometrijski format + default_view: Privzeti pogled + invalid_options: Neveljavne možnosti + invalid_format: Neveljavna oblika ({format}) + unexpected_geometry: Pričakovano {expected}, vsebina {got}. + fit_bounds: Prilagodi pogled podatkom + native: Izviren + geojson: GeoJSON + lnglat: Dolžina, višina + wkt: WKT + wkb: WKB presentation-notice: notice: Obvestilo description: Pokaži kratko obvestilo @@ -1261,3 +1326,15 @@ layouts: calendar: Koledar start_date_field: Polje začetnega datuma end_date_field: Polje končnega datuma + map: + map: Zemljevid + basemap: Osnovni zemljevid + layers: Plasti + edit_custom_layers: Uredi plasti + cluster_options: Možnosti gruče + cluster: Aktiviraj gruče + cluster_radius: Polmer gruče + cluster_minpoints: Minimalna velikost gruče + cluster_maxzoom: Največja povečava za gruče + field: Geometrija + invalid_geometry: Neveljavna geometrija diff --git a/app/src/lang/translations/sr-CS.yaml b/app/src/lang/translations/sr-CS.yaml index c2f9db4acd..8305cbfa9b 100644 --- a/app/src/lang/translations/sr-CS.yaml +++ b/app/src/lang/translations/sr-CS.yaml @@ -797,10 +797,35 @@ fields: name: Ime status: Status field_options: + directus_activity: + login: Prijavi se + create: Kreiraj + update: Ažuriranje + delete: Obriši directus_collections: track_activity_revisions: Prati Aktivnost & Revizije only_track_activity: Samo Prati Aktivnost do_not_track_anything: Bez praćenja + collection_setup: Podešavanje Kolekcije + singleton: Tretiraj kao jedinstven objekat + language: Jezik + archive_divider: Arhiviraj + divider: Sortiranje + directus_roles: + fields: + icon_name: Ikonica + name_name: Ime + name_placeholder: Unesi naslov... + collection_list: + fields: + type_name: Tip + collections_name: Kolekcije + directus_users: + status_dropdown_active: Aktivan + directus_webhooks: + status_options_active: Aktivan + actions_create: Kreiraj + actions_update: Ažuriranje no_fields_in_collection: 'Trenutno ne postoji nijedno polje u "{collection}" kolekciji' do_nothing: Ne radi ništa generate_and_save_uuid: Generiši i Sačuvaj UUID diff --git a/app/src/lang/translations/sr-SP.yaml b/app/src/lang/translations/sr-SP.yaml index a86f54f2b8..b59420ecd6 100644 --- a/app/src/lang/translations/sr-SP.yaml +++ b/app/src/lang/translations/sr-SP.yaml @@ -139,6 +139,17 @@ fields: directus_webhooks: name: Име status: Стање +field_options: + directus_activity: + create: Креирај + directus_collections: + language: Језик + archive_divider: Архивирај + directus_roles: + fields: + name_name: Име + directus_webhooks: + actions_create: Креирај comment: Коментар description: Опис done: Завршено diff --git a/app/src/lang/translations/sv-SE.yaml b/app/src/lang/translations/sv-SE.yaml index 204a946ef0..6fa92382fc 100644 --- a/app/src/lang/translations/sv-SE.yaml +++ b/app/src/lang/translations/sv-SE.yaml @@ -772,6 +772,32 @@ fields: directus_webhooks: name: Namn status: Status +field_options: + directus_activity: + login: Logga in + create: Skapa + update: Uppdatera + delete: Radera + directus_collections: + collection_setup: Konfiguration av kollektion + singleton: Behandla som ett enskilt objekt + language: Språk + archive_divider: Arkivera + divider: Sortera + directus_roles: + fields: + icon_name: Ikon + name_name: Namn + collection_list: + fields: + type_name: Typ + collections_name: Kollektioner + directus_users: + status_dropdown_active: Aktiv + directus_webhooks: + status_options_active: Aktiv + actions_create: Skapa + actions_update: Uppdatera no_fields_in_collection: 'Det finns inga fält i "{collection}" ännu' do_nothing: Gör ingenting generate_and_save_uuid: Generera och spara UUID diff --git a/app/src/lang/translations/th-TH.yaml b/app/src/lang/translations/th-TH.yaml index ba96ae7dae..f4b4cdcd58 100644 --- a/app/src/lang/translations/th-TH.yaml +++ b/app/src/lang/translations/th-TH.yaml @@ -817,10 +817,34 @@ fields: name: ชื่อ status: สถานะ field_options: + directus_activity: + login: ลงชื่อเข้าใช้ + create: สร้าง + update: แก้ไข directus_collections: track_activity_revisions: ติดตามกิจกรรมและการแก้ไข only_track_activity: ติดตามกิจกรรมเท่านั้น do_not_track_anything: อย่าติดตามอะไรเลย + collection_setup: การตั้งค่าคอลเลกชัน + singleton: ทำเสมือนว่ามีออปเจ็คเดียว + language: ภาษา + archive_divider: เก็บถาวร + divider: เรียง + directus_roles: + fields: + icon_name: ไอคอน + name_name: ชื่อ + name_placeholder: ป้อนชื่อเรื่อง + collection_list: + fields: + type_name: ชนิด + collections_name: คอลเลกชัน + directus_users: + status_dropdown_active: เปิดใช้งาน + directus_webhooks: + status_options_active: เปิดใช้งาน + actions_create: สร้าง + actions_update: แก้ไข no_fields_in_collection: 'ยังไม่มีฟิลด์ใน {collection}' do_nothing: ไม่ต้องทำอะไร generate_and_save_uuid: สร้างและบันทึก UUID โดยอัตโนมัติ diff --git a/app/src/lang/translations/tr-TR.yaml b/app/src/lang/translations/tr-TR.yaml index 882fc8ecc1..d3d3cd29ba 100644 --- a/app/src/lang/translations/tr-TR.yaml +++ b/app/src/lang/translations/tr-TR.yaml @@ -630,6 +630,30 @@ fields: directus_webhooks: name: İsim status: Durum +field_options: + directus_activity: + login: Oturum Aç + create: Oluştur + update: Güncelle + delete: Sil + directus_collections: + language: Dil + archive_divider: Arşiv + divider: Sırala + directus_roles: + fields: + icon_name: İkon + name_name: İsim + collection_list: + fields: + type_name: Tür + collections_name: Koleksiyonlar + directus_users: + status_dropdown_active: Aktif + directus_webhooks: + status_options_active: Aktif + actions_create: Oluştur + actions_update: Güncelle do_nothing: Hiçbir Şey Yapma block: Blok comment: Yorum diff --git a/app/src/lang/translations/uk-UA.yaml b/app/src/lang/translations/uk-UA.yaml index 1fd38aa051..b6dfa60d53 100644 --- a/app/src/lang/translations/uk-UA.yaml +++ b/app/src/lang/translations/uk-UA.yaml @@ -20,25 +20,75 @@ #'SIMD', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Generator', 'GeneratorFunction', 'Reflect', #'Proxy', 'Intl' edit_field: Редагувати поле +conditions: Умови +maps: Мапи +item_revision: Перегляд об'єкта duplicate_field: Дублювати поле +half_width: Ширина ряду на половину full_width: Повна ширина +limit: Обмеження +group: Група +and: Й +or: Або +fill_width: Ширина заповнення +field_name_translations: Переклад назви поля +enter_password_to_enable_tfa: Введіть свій пароль, щоб увімкнути двофакторну аутентифікацію add_field: Додати поле role_name: Назва ролі +branch: Гілка +leaf: Відгалуження +indeterminate: Не вдалось визначити +edit_collection: Редагувати збірку +exclusive: Винятково +children: Дочірні елементи +db_only_click_to_configure: 'Можливе використання лише бази даних: натисніть для налаштування ' +show_archived_items: Показати архівні елементи +edited: Значення відредаговано required: Обов'язкове +required_for_app_access: Необхідно для доступу в додаток +requires_value: Потребує значення +create_preset: Створити шаблон create_role: Створити роль +create_user: Новий користувач +create_webhook: Створити вебхук +invite_users: Запросити друзів +email_examples: "admin{'@'}example.com, user{'@'}example.com..." invite: Запросити +email_already_invited: Користувача "{email}" вже запрошено +emails: Електронні адреси +connection_excellent: Ідеальне з'єднання +connection_good: Хороше з'єднання +connection_fair: Нормальне з'єднання +connection_poor: Слабке з'єднання +primary: Стандартно +rename_folder: Перейменувати папку +delete_folder: Видалити папку +prefix: Префікс +suffix: Суфікс +reset_bookmark: Скинути закладку rename_bookmark: Перейменувати закладку update_bookmark: Оновити закладку delete_bookmark: Видалити закладку +delete_bookmark_copy: >- + Ви впевнені, що хочете видалити закладку "{bookmark}"? Цю дію не можливо буде скасувати. logoutReason: SIGN_OUT: Виконано вихід SESSION_EXPIRED: Час сеансу минув +public_label: Загальнодоступний +public_description: Контролює, які дані API є доступними без входу в особистий кабінет. +not_allowed: Заборонено directus_version: Версія Directus node_version: Версія Node node_uptime: Час роботи Node os_type: Тип ОС os_version: Версія ОС os_uptime: Час роботи ОС +os_totalmem: Пам'ять операційної системи +archive: Архів +archive_confirm: Ви впевнені, що хочете додати цей файл до архіву? +archive_confirm_count: >- + Жодного елементу не обрано | Ви впевнені, що хочете додати цей файл до архіву? | Ви впевнені, що хочете додати ці {count} файлів до архіву? +reset_system_permissions_to: 'Скинути налаштування до:' validationError: eq: Значення повинно бути {valid} neq: Значення не може бути {invalid} @@ -217,6 +267,7 @@ fields: directus_fields: note: Примітка hidden: Приховано + translation: Переклад назви поля display_template: Шаблон directus_roles: name: Назва ролі @@ -224,6 +275,23 @@ fields: directus_webhooks: name: Назва status: Статус +field_options: + directus_activity: + login: Вхід + create: Створити + update: Оновити + delete: Видалити + directus_collections: + language: Мова + archive_divider: Архів + directus_roles: + fields: + icon_name: Іконка + name_name: Назва + collections_name: Колекції + directus_webhooks: + actions_create: Створити + actions_update: Оновити comment: Коментар delete_field_are_you_sure: >- Ви впевнені, що хочете видалити поле "{field}"? Цю дію не можливо скасувати. @@ -267,6 +335,8 @@ template: Шаблон translation: Переклад value: Значення interfaces: + presentation-links: + primary: Стандартно input-code: code: Код system-collection: diff --git a/app/src/lang/translations/vi-VN.yaml b/app/src/lang/translations/vi-VN.yaml index de3021c665..58c176bb8e 100644 --- a/app/src/lang/translations/vi-VN.yaml +++ b/app/src/lang/translations/vi-VN.yaml @@ -552,6 +552,29 @@ fields: directus_webhooks: name: Tên status: Trạng thái +field_options: + directus_activity: + login: Đăng nhập + create: Tạo + update: Cập nhật + directus_collections: + language: Ngôn ngữ + archive_divider: Lưu trữ + divider: Sắp xếp + directus_roles: + fields: + icon_name: Biểu tượng + name_name: Tên + collection_list: + fields: + type_name: Kiểu + collections_name: Danh mục + directus_users: + status_dropdown_active: Kích hoạt + directus_webhooks: + status_options_active: Kích hoạt + actions_create: Tạo + actions_update: Cập nhật comment: Bình luận delete_field_are_you_sure: >- Bạn có chắc chắn muốn xóa trường "{field}"? Dữ liệu đã xóa sẽ không thể phục hồi lại được. diff --git a/app/src/lang/translations/zh-CN.yaml b/app/src/lang/translations/zh-CN.yaml index 286fa6672d..5d1ed01e5b 100644 --- a/app/src/lang/translations/zh-CN.yaml +++ b/app/src/lang/translations/zh-CN.yaml @@ -21,10 +21,12 @@ #'Proxy', 'Intl' edit_field: 编辑 conditions: 条件 +maps: 地图 item_revision: 项目调整 duplicate_field: 复制 half_width: 半宽 full_width: 全宽 +limit: 限制 group: 群组 and: 和 or: 或 @@ -71,6 +73,7 @@ delete_bookmark_copy: >- logoutReason: SIGN_OUT: 注销 SESSION_EXPIRED: 会话过期 +public_label: 公开 public_description: 设置哪些API无需身份验证。 not_allowed: 不允许 directus_version: Directus 版本 @@ -116,6 +119,8 @@ no_access: 禁止访问 use_custom: 自定义 nullable: 允许为空 allow_null_value: 允许NULL值 +allow_multiple: 允许多个 +allow_multiple_to_be_open: 允许打开多个 enter_value_to_replace_nulls: 请输入一个新的值来替换当前在此字段内的任何NULL。 field_standard: 基本设置 field_presentation: 别名 @@ -179,6 +184,7 @@ time: 时间 timestamp: Timestamp uuid: UUID hash: Hash +geometry: 几何 not_available_for_type: 此类型不可用 create_translations: 创建翻译 auto_refresh: 自动刷新 @@ -309,6 +315,7 @@ drag_mode: 拖动模式 cancel_crop: 取消裁剪。 original: 原始 url: URl +import_label: 导入 file_details: 文件信息 dimensions: 尺寸 size: 大小 @@ -376,6 +383,7 @@ no_users_copy: 该角色下还没有用户。 webhooks_count: '没有 Web钩子 | 一个钩子 | {count} 个Web钩子' no_webhooks_copy: 还没有任何web钩子。 all_items: 所有项目 +any: 任意 csv: CSV no_collections: 没有任何集合 create_collection: 创建集合 @@ -386,6 +394,7 @@ display_template_not_setup: 显示的模板选项配置错误 collection_field_not_setup: 集合的字段选项配置错误 select_a_collection: 选择一个集合 active: 激活 +inactive: 非活跃 users: 用户 activity: 活动 webhooks: Webhook @@ -398,6 +407,7 @@ documentation: 文档 sidebar: 侧边栏 duration: 持续时间 charset: 字符集 +second: 秒 file_moved: 文件已移动 collection_created: 集合已创建 modified_on: 修改于 @@ -439,6 +449,7 @@ errors: UNPROCESSABLE_ENTITY: 不可处理的实体 INTERNAL_SERVER_ERROR: 未知错误 NOT_NULL_VIOLATION: 值不能为null +security: 安全 value_hashed: 哈希值 bookmark_name: 书签名称 create_bookmark: 创建书签 @@ -490,6 +501,7 @@ color: 颜色 circle: 圆圈 empty_item: 空项 log_in_with: '使用 {provider} 登录' +advanced_settings: 高级设置 advanced_filter: 高级过滤器 delete_advanced_filter: 删除过滤器 change_advanced_filter_operator: 更改操作者 @@ -516,6 +528,10 @@ operators: nempty: 不是空的 all: 包含这些关键词 has: 包含其中之一的关键词 + intersects: 相交 + nintersects: 不相交 + intersects_bbox: 相交边框 + nintersects_bbox: 没有相交边框 loading: 正在载入... drop_to_upload: 拖拽文件并上传 item: 条目 @@ -559,6 +575,7 @@ user: 用户 no_presets: 无预设 no_presets_copy: 尚未保存预设或书签。 no_presets_cta: 添加预设 +presets_only: 仅预设 create: 创建 on_create: 创建时 on_update: 在更新操作上 @@ -574,6 +591,7 @@ label: 标签 image_url: 图片 Url alt_text: 替代文本 media: Media +quality: 质量 width: 宽 height: 高度 source: 来源 @@ -768,13 +786,22 @@ fields: title: 标题 description: 描述 tags: 标签 + user_preferences: 用户设置 language: 语言 theme: 主题 + theme_auto: 自动 (系统主题) + theme_light: 亮色模式 + theme_dark: 暗色模式 tfa_secret: 2FA(双因素身份验证) + admin_options: 管理员设置 status: 状态 + status_draft: 草稿 status_active: 激活 + status_suspended: 已停用 + status_archived: 已存档 role: 角色 token: Token + token_placeholder: 请输入安全访问令牌... last_page: 尾页 last_access: 上次访问 directus_settings: @@ -782,13 +809,17 @@ fields: project_url: 项目URL project_color: 项目颜色 project_logo: 项目 Logo + public_pages: 公开页面 public_foreground: 公共前景 public_background: 公共背景 public_note: 公共便笺 auth_password_policy: 认证密码策略 auth_login_attempts: 尝试认证登录次数 + files_and_thumbnails: 文件和缩略图 + storage_default_folder: 默认存储文件夹 storage_asset_presets: 存储资产预设 storage_asset_transform: 存储资产转换 + overrides: 应用设置覆盖 custom_css: 自定义 CSS directus_fields: collection: 集合名称 @@ -811,12 +842,59 @@ fields: collection_list: 集合导航 directus_webhooks: name: 名称 + method: 方法 status: 状态 + data: 数据 + data_label: 发送事件数据 + triggers: 触发条件 + actions: 操作 field_options: + directus_settings: + security_divider_title: 安全 + files_divider_title: 文件和缩略图 + overrides_divider_title: 应用设置覆盖 + directus_activity: + login: 登录 + create: 创建 + update: 更新 + delete: 删除 directus_collections: track_activity_revisions: 跟踪活动和历史修改版本 only_track_activity: 仅跟踪活动 do_not_track_anything: 不跟踪任何内容 + collection_setup: 集合设置 + singleton: 视为单个对象 + language: 语言 + archive_divider: 存档 + divider: 排序 + directus_roles: + fields: + icon_name: 图标 + name_name: 名称 + name_placeholder: 输入标题... + collection_list: + fields: + type_name: 类型 + collections_name: 集合 + directus_users: + preferences_divider: 用户设置 + dropdown_auto: 自动 (系统主题) + dropdown_light: 亮色模式 + dropdown_dark: 暗色模式 + admin_divider: 管理员设置 + status_dropdown_draft: 草稿 + status_dropdown_active: 激活 + status_dropdown_suspended: 已停用 + status_dropdown_archived: 已存档 + token: 请输入安全访问令牌... + directus_webhooks: + status_options_active: 激活 + status_options_inactive: 非活跃 + data_label: 发送事件数据 + triggers_divider: 触发条件 + actions_create: 创建 + actions_update: 更新 + actions_delete: 删除 no_fields_in_collection: '集合 {collection} 中没有任何字段' do_nothing: 什么都不做 generate_and_save_uuid: 生成并保存 UUID @@ -892,11 +970,21 @@ sort_direction: 排序 sort_asc: 升序 sort_desc: 降序 template: 模板 +require_value_to_be_set: 需要设置值 translation: 翻译 value: 值 view_project: 查看项目 report_error: 报告错误 +start: 开始 interfaces: + group-accordion: + name: 折叠 + description: 在折叠面板中显示字段或分组 + start: 开始 + all_closed: 全部已关闭 + first_opened: 最早打开 + all_opened: 全部已打开 + accordion_mode: 折叠模式 presentation-links: presentation-links: 按钮链接 links: 链接 @@ -917,6 +1005,8 @@ interfaces: description: 通过复选框选择多个选项 value_combining: 组合值 value_combining_note: 控制嵌套选择时存储的值。 + show_all: 显示全部 + show_selected: 显示所选 input-code: code: 代码 description: 编辑或分享代码片段 @@ -1011,7 +1101,20 @@ interfaces: imageToken: 图像Token imageToken_label: 添加到源图片的Token map: + map: 地图 + description: 选择地图上的位置 zoom: 缩放 + geometry_type: 几何类型 + geometry_format: 几何格式 + default_view: 默认视图 + invalid_options: 无效选项 + invalid_format: 无效格式 ({format}) + unexpected_geometry: 需要 {expected},但收到 {got}。 + native: 原生 + geojson: GeoJSON + lnglat: 经度、纬度 + wkt: WKT + wkb: WKB presentation-notice: notice: 提示 description: 显示一个快捷提示 @@ -1209,3 +1312,6 @@ layouts: calendar: 日历 start_date_field: 开始日期 end_date_field: 结束日期 + map: + map: 地图 + field: 几何 diff --git a/app/src/lang/translations/zh-TW.yaml b/app/src/lang/translations/zh-TW.yaml index 6d1bc1b86e..64d0cc4a6e 100644 --- a/app/src/lang/translations/zh-TW.yaml +++ b/app/src/lang/translations/zh-TW.yaml @@ -544,6 +544,27 @@ fields: directus_webhooks: name: 名稱 status: 狀態 +field_options: + directus_activity: + login: 登入 + create: 新建 + update: 更新 + delete: 刪除 + directus_collections: + language: 語言 + archive_divider: 封存 + divider: 排序 + directus_roles: + fields: + icon_name: 圖示 + name_name: 名稱 + collection_list: + fields: + type_name: 類型 + collections_name: 資料集 + directus_webhooks: + actions_create: 新建 + actions_update: 更新 no_fields_in_collection: '"{collection}" 中還沒有任何欄位' do_nothing: 無動作 generate_and_save_uuid: 產生並儲存 UUID diff --git a/app/src/layouts/calendar/actions.vue b/app/src/layouts/calendar/actions.vue index a5071a48b7..de658c0cf0 100644 --- a/app/src/layouts/calendar/actions.vue +++ b/app/src/layouts/calendar/actions.vue @@ -7,16 +7,19 @@ diff --git a/app/src/layouts/calendar/calendar.vue b/app/src/layouts/calendar/calendar.vue index 5e23b22d1e..3325cd3527 100644 --- a/app/src/layouts/calendar/calendar.vue +++ b/app/src/layouts/calendar/calendar.vue @@ -1,34 +1,38 @@ diff --git a/app/src/layouts/calendar/index.ts b/app/src/layouts/calendar/index.ts index d0cc4c2ba9..59399f0893 100644 --- a/app/src/layouts/calendar/index.ts +++ b/app/src/layouts/calendar/index.ts @@ -8,7 +8,7 @@ import { getFieldsFromTemplate } from '@/utils/get-fields-from-template'; import getFullcalendarLocale from '@/utils/get-fullcalendar-locale'; import { renderPlainStringTemplate } from '@/utils/render-string-template'; import { unexpectedError } from '@/utils/unexpected-error'; -import { Field, Filter, Item } from '@directus/shared/types'; +import { Field, Item } from '@directus/shared/types'; import { defineLayout } from '@directus/shared/utils'; import { Calendar, CalendarOptions as FullCalendarOptions, EventInput } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; @@ -21,16 +21,8 @@ import CalendarActions from './actions.vue'; import CalendarLayout from './calendar.vue'; import CalendarOptions from './options.vue'; import CalendarSidebar from './sidebar.vue'; - -type LayoutOptions = { - template?: string; - startDateField?: string; - endDateField?: string; - viewInfo?: { - type: string; - startDateStr: string; - }; -}; +import useSync from '@/composables/use-sync'; +import { LayoutOptions } from './types'; export default defineLayout({ id: 'calendar', @@ -42,15 +34,18 @@ export default defineLayout({ sidebar: CalendarSidebar, actions: CalendarActions, }, - setup(props) { + setup(props, { emit }) { const { t, locale } = useI18n(); - const calendarEl = ref(); const calendar = ref(); const appStore = useAppStore(); - const { selection, collection, searchQuery, layoutOptions, filters } = toRefs(props); + const layoutOptions = useSync(props, 'layoutOptions', emit); + const filters = useSync(props, 'filters', emit); + const searchQuery = useSync(props, 'searchQuery', emit); + + const { selection, collection } = toRefs(props); const { primaryKeyField, fields: fieldsInCollection } = useCollection(collection); @@ -60,7 +55,7 @@ export default defineLayout({ }) ); - const filtersWithCalendarView = computed(() => { + const filtersWithCalendarView = computed(() => { if (!calendar.value || !startDateField.value) return filters.value; return [ @@ -84,12 +79,12 @@ export default defineLayout({ const template = computed({ get() { - return layoutOptions.value?.template; + return layoutOptions.value?.template ?? null; }, - set(newTemplate: string | undefined) { + set(newTemplate: string | null) { layoutOptions.value = { ...(layoutOptions.value || {}), - template: newTemplate, + template: newTemplate ?? undefined, }; }, }); @@ -108,12 +103,12 @@ export default defineLayout({ const startDateField = computed({ get() { - return layoutOptions.value?.startDateField; + return layoutOptions.value?.startDateField ?? null; }, - set(newStartDateField: string | undefined) { + set(newStartDateField: string | null) { layoutOptions.value = { ...(layoutOptions.value || {}), - startDateField: newStartDateField, + startDateField: newStartDateField ?? undefined, }; }, }); @@ -124,12 +119,12 @@ export default defineLayout({ const endDateField = computed({ get() { - return layoutOptions.value?.endDateField; + return layoutOptions.value?.endDateField ?? null; }, - set(newEndDateField: string | undefined) { + set(newEndDateField: string | null) { layoutOptions.value = { ...(layoutOptions.value || {}), - endDateField: newEndDateField, + endDateField: newEndDateField ?? undefined, }; }, }); @@ -201,9 +196,9 @@ export default defineLayout({ const endpoint = collection.value.startsWith('directus') ? collection.value.substring(9) - : `/collections/${collection.value}`; + : `collections/${collection.value}`; - router.push(`${endpoint}/${primaryKey}`); + router.push(`/${endpoint}/${primaryKey}`); } }, async eventChange(info) { @@ -223,7 +218,7 @@ export default defineLayout({ try { await api.patch(`${endpoint}/${info.event.id}`, itemChanges); - } catch (err) { + } catch (err: any) { unexpectedError(err); } }, @@ -266,7 +261,6 @@ export default defineLayout({ }); return { - calendarEl, items, loading, error, @@ -294,8 +288,8 @@ export default defineLayout({ } } - function createCalendar() { - calendar.value = new Calendar(calendarEl.value!, fullFullCalendarOptions.value); + function createCalendar(calendarElement: HTMLElement) { + calendar.value = new Calendar(calendarElement, fullFullCalendarOptions.value); calendar.value.on('datesSet', (args) => { viewInfo.value = { diff --git a/app/src/layouts/calendar/options.vue b/app/src/layouts/calendar/options.vue index 8c27200961..ddf9621562 100644 --- a/app/src/layouts/calendar/options.vue +++ b/app/src/layouts/calendar/options.vue @@ -1,34 +1,59 @@ diff --git a/app/src/layouts/calendar/sidebar.vue b/app/src/layouts/calendar/sidebar.vue index 805bebe44d..290a7e101f 100644 --- a/app/src/layouts/calendar/sidebar.vue +++ b/app/src/layouts/calendar/sidebar.vue @@ -1,23 +1,43 @@ diff --git a/app/src/layouts/calendar/types.ts b/app/src/layouts/calendar/types.ts new file mode 100644 index 0000000000..4950607d3e --- /dev/null +++ b/app/src/layouts/calendar/types.ts @@ -0,0 +1,9 @@ +export type LayoutOptions = { + template?: string; + startDateField?: string; + endDateField?: string; + viewInfo?: { + type: string; + startDateStr: string; + }; +}; diff --git a/app/src/layouts/cards/actions.vue b/app/src/layouts/cards/actions.vue index b41d1c5ce6..d8e8f91160 100644 --- a/app/src/layouts/cards/actions.vue +++ b/app/src/layouts/cards/actions.vue @@ -1,20 +1,25 @@ diff --git a/app/src/layouts/cards/cards.vue b/app/src/layouts/cards/cards.vue index 452a1e26ec..c599ba9d9b 100644 --- a/app/src/layouts/cards/cards.vue +++ b/app/src/layouts/cards/cards.vue @@ -2,9 +2,9 @@