Compare commits

..

9 Commits

Author SHA1 Message Date
Dan Brown
8e0edb63c7 Merge branch 'development' into vectors 2026-01-07 11:14:53 +00:00
Dan Brown
bb08f62327 Vectors: Finished core fetch & display functionality 2025-08-22 12:59:32 +01:00
Dan Brown
8eef5a1ee7 Vectors: Updated query response to use server-side-events
Allowing the vector query results and the LLM response to each come back
over the same HTTP request at two different times via a somewhat
standard.

Uses a package for JS SSE client, since native browser client does not
support over POST, which is probably important for this endpoint as we
don't want crawlers or other bots abusing this via accidentally.
2025-08-21 16:03:55 +01:00
Dan Brown
88ccd9e5b9 Vectors: Split out vector search and llm query runs
Added a formal object type to carry across vector search results.
Added permission application and entity combining with vector search
results.
Also updated namespace from vectors to queries.
2025-08-21 12:14:52 +01:00
Dan Brown
2c3100e401 Vectors: Started front-end work, moved to own controller 2025-08-19 15:19:04 +01:00
Dan Brown
54f883e815 Improved vector text chunking 2025-08-19 11:04:14 +01:00
Dan Brown
e611b3239e Vectors: Added command to regenerate for all
Also made models configurable.
Tested system scales via 86k vector entries.
2025-08-17 09:43:07 +01:00
Dan Brown
b9ecf55e1f Vectors: Got basic LLM querying working using vector search context 2025-08-17 09:43:07 +01:00
Dan Brown
2d5548240a Vectors: Built content vector indexing system 2025-08-17 09:43:00 +01:00
100 changed files with 1306 additions and 734 deletions

View File

@@ -521,10 +521,3 @@ setiawan setiawan (culture.setiawan) :: Indonesian
Donald Mac Kenzie (kiuman) :: Norwegian Bokmal
Gabriel Silver (GabrielBSilver) :: Hebrew
Tomas Darius Davainis (Tomasdd) :: Lithuanian
CriedHero :: Chinese Simplified
Henrik (henrik2105) :: Norwegian Bokmal
FoW (fofwisdom) :: Korean
serinf-lauza :: French
Diyan Nikolaev (nikolaev.diyan) :: Bulgarian
Shadluk Avan (quldosh) :: Uzbek
Marci (MartonPoto) :: Hungarian

6
.gitignore vendored
View File

@@ -8,10 +8,10 @@ Homestead.yaml
.idea
npm-debug.log
yarn-error.log
/public/dist/*.map
/public/dist
/public/plugins
/public/css/*.map
/public/js/*.map
/public/css
/public/js
/public/bower
/public/build/
/public/favicon.ico

View File

@@ -22,6 +22,18 @@ return [
// Callback URL for social authentication methods
'callback_url' => env('APP_URL', false),
// LLM Service
// Options: openai
'llm' => env('LLM_SERVICE', ''),
// OpenAI API-compatible service details
'openai' => [
'endpoint' => env('OPENAI_ENDPOINT', 'https://api.openai.com'),
'key' => env('OPENAI_KEY', ''),
'embedding_model' => env('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small'),
'query_model' => env('OPENAI_QUERY_MODEL', 'gpt-4o'),
],
'github' => [
'client_id' => env('GITHUB_APP_ID', false),
'client_secret' => env('GITHUB_APP_SECRET', false),

View File

@@ -0,0 +1,46 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
use BookStack\Search\Queries\SearchVector;
use BookStack\Search\Queries\StoreEntityVectorsJob;
use Illuminate\Console\Command;
class RegenerateVectorsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-vectors';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Re-index vectors for all content in the system';
/**
* Execute the console command.
*/
public function handle(EntityProvider $entityProvider)
{
// TODO - Add confirmation before run regarding deletion/time/effort/api-cost etc...
SearchVector::query()->delete();
$types = $entityProvider->all();
foreach ($types as $type => $typeInstance) {
$this->info("Creating jobs to store vectors for {$type} data...");
/** @var Entity[] $entities */
$typeInstance->newQuery()->chunkById(100, function ($entities) {
foreach ($entities as $entity) {
dispatch(new StoreEntityVectorsJob($entity));
}
});
}
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace BookStack\Search\Queries;
use BookStack\Activity\Models\Tag;
use BookStack\Entities\Models\Entity;
use BookStack\Search\Queries\Services\VectorQueryService;
use Illuminate\Support\Facades\DB;
class EntityVectorGenerator
{
public function __construct(
protected VectorQueryServiceProvider $vectorQueryServiceProvider
) {
}
public function generateAndStore(Entity $entity): void
{
$vectorService = $this->vectorQueryServiceProvider->get();
$text = $this->entityToPlainText($entity);
$chunks = $this->chunkText($text);
$embeddings = $this->chunksToEmbeddings($chunks, $vectorService);
$this->deleteExistingEmbeddingsForEntity($entity);
$this->storeEmbeddings($embeddings, $chunks, $entity);
}
protected function deleteExistingEmbeddingsForEntity(Entity $entity): void
{
SearchVector::query()
->where('entity_type', '=', $entity->getMorphClass())
->where('entity_id', '=', $entity->id)
->delete();
}
protected function storeEmbeddings(array $embeddings, array $textChunks, Entity $entity): void
{
$toInsert = [];
foreach ($embeddings as $index => $embedding) {
$text = $textChunks[$index];
$toInsert[] = [
'entity_id' => $entity->id,
'entity_type' => $entity->getMorphClass(),
'embedding' => DB::raw('VEC_FROMTEXT("[' . implode(',', $embedding) . ']")'),
'text' => $text,
];
}
$chunks = array_chunk($toInsert, 500);
foreach ($chunks as $chunk) {
SearchVector::query()->insert($chunk);
}
}
/**
* @param string[] $chunks
* @return float[] array
*/
protected function chunksToEmbeddings(array $chunks, VectorQueryService $vectorQueryService): array
{
$embeddings = [];
foreach ($chunks as $index => $chunk) {
$embeddings[$index] = $vectorQueryService->generateEmbeddings($chunk);
}
return $embeddings;
}
/**
* @return string[]
*/
protected function chunkText(string $text): array
{
return (new TextChunker(500, ["\n", '.', ' ', '']))->chunk($text);
}
protected function entityToPlainText(Entity $entity): string
{
$tags = $entity->tags()->get();
$tagText = $tags->map(function (Tag $tag) {
return $tag->name . ': ' . $tag->value;
})->join('\n');
return $entity->name . "\n{$tagText}\n" . $entity->{$entity->textField};
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace BookStack\Search\Queries;
use Exception;
class LlmQueryRunner
{
public function __construct(
protected VectorQueryServiceProvider $vectorQueryServiceProvider,
) {
}
/**
* Run a query against the configured LLM to produce a text response.
* @param VectorSearchResult[] $vectorResults
* @throws Exception
*/
public function run(string $query, array $vectorResults): string
{
$queryService = $this->vectorQueryServiceProvider->get();
$matchesText = array_values(array_map(fn (VectorSearchResult $result) => $result->matchText, $vectorResults));
return $queryService->query($query, $matchesText);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace BookStack\Search\Queries;
use BookStack\Http\Controller;
use BookStack\Search\SearchRunner;
use Illuminate\Http\Request;
class QueryController extends Controller
{
public function __construct(
protected SearchRunner $searchRunner,
) {
// TODO - Check via testing
$this->middleware(function ($request, $next) {
if (!VectorQueryServiceProvider::isEnabled()) {
$this->showPermissionError('/');
}
return $next($request);
});
}
/**
* Show the view to start a vector/LLM-based query search.
*/
public function show(Request $request)
{
$query = $request->get('ask', '');
// TODO - Set page title
return view('search.query', [
'query' => $query,
]);
}
/**
* Perform a vector/LLM-based query search.
*/
public function run(Request $request, VectorSearchRunner $searchRunner, LlmQueryRunner $llmRunner)
{
// TODO - Rate limiting
$query = $request->get('query', '');
return response()->eventStream(function () use ($query, $searchRunner, $llmRunner) {
$results = $query ? $searchRunner->run($query) : [];
$entities = [];
foreach ($results as $result) {
$entityKey = $result->entity->getMorphClass() . ':' . $result->entity->id;
if (!isset($entities[$entityKey])) {
$entities[$entityKey] = $result->entity;
}
}
yield ['view' => view('entities.list', ['entities' => $entities])->render()];
yield ['result' => $llmRunner->run($query, $results)];
});
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace BookStack\Search\Queries;
use BookStack\Permissions\Models\JointPermission;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property string $entity_type
* @property int $entity_id
* @property string $text
* @property string $embedding
*/
class SearchVector extends Model
{
public $timestamps = false;
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
->whereColumn('search_vectors.entity_type', '=', 'joint_permissions.entity_type');
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace BookStack\Search\Queries\Services;
use BookStack\Http\HttpRequestService;
class OpenAiVectorQueryService implements VectorQueryService
{
protected string $key;
protected string $endpoint;
protected string $embeddingModel;
protected string $queryModel;
public function __construct(
protected array $options,
protected HttpRequestService $http,
) {
// TODO - Some kind of validation of options
$this->key = $this->options['key'] ?? '';
$this->endpoint = $this->options['endpoint'] ?? '';
$this->embeddingModel = $this->options['embedding_model'] ?? '';
$this->queryModel = $this->options['query_model'] ?? '';
}
protected function jsonRequest(string $method, string $uri, array $data): array
{
$fullUrl = rtrim($this->endpoint, '/') . '/' . ltrim($uri, '/');
$client = $this->http->buildClient(30);
$request = $this->http->jsonRequest($method, $fullUrl, $data)
->withHeader('Authorization', 'Bearer ' . $this->key);
$response = $client->sendRequest($request);
return json_decode($response->getBody()->getContents(), true);
}
public function generateEmbeddings(string $text): array
{
$response = $this->jsonRequest('POST', 'v1/embeddings', [
'input' => $text,
'model' => $this->embeddingModel,
]);
return $response['data'][0]['embedding'];
}
public function query(string $input, array $context): string
{
$formattedContext = implode("\n", $context);
$response = $this->jsonRequest('POST', 'v1/chat/completions', [
'model' => $this->queryModel,
'messages' => [
[
'role' => 'developer',
'content' => 'You are a helpful assistant providing search query responses. Be specific, factual and to-the-point in response. Don\'t try to converse or continue the conversation.'
],
[
'role' => 'user',
'content' => "Provide a response to the below given QUERY using the below given CONTEXT. The CONTEXT is split into parts via lines. Ignore any nonsensical lines of CONTEXT.\nQUERY: {$input}\n\nCONTEXT: {$formattedContext}",
]
],
]);
return $response['choices'][0]['message']['content'] ?? '';
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace BookStack\Search\Queries\Services;
interface VectorQueryService
{
/**
* Generate embedding vectors from the given chunk of text.
* @return float[]
*/
public function generateEmbeddings(string $text): array;
/**
* Query the LLM service using the given user input, and
* relevant context text retrieved locally via a vector search.
* Returns the response output text from the LLM.
*
* @param string[] $context
*/
public function query(string $input, array $context): string;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace BookStack\Search\Queries;
use BookStack\Entities\Models\Entity;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class StoreEntityVectorsJob implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
protected Entity $entity
) {
}
/**
* Execute the job.
*/
public function handle(EntityVectorGenerator $generator): void
{
$generator->generateAndStore($this->entity);
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace BookStack\Search\Queries;
use InvalidArgumentException;
/**
* Splits a given string into smaller chunks based on specified delimiters
* and a predefined maximum chunk size. This will work through the given delimiters
* to break down text further and further to fit into the chunk size.
*
* The last delimiter is always an empty string to ensure text can always be broken down.
*/
class TextChunker
{
public function __construct(
protected int $chunkSize,
protected array $delimiterOrder,
) {
if (count($this->delimiterOrder) === 0 || $this->delimiterOrder[count($this->delimiterOrder) - 1] !== '') {
$this->delimiterOrder[] = '';
}
if ($this->chunkSize < 1) {
throw new InvalidArgumentException('Chunk size must be greater than 0');
}
}
public function chunk(string $text): array
{
$delimiter = $this->delimiterOrder[0];
$delimiterLength = strlen($delimiter);
$lines = ($delimiter === '') ? str_split($text, $this->chunkSize) : explode($delimiter, $text);
$cChunk = ''; // Current chunk
$cLength = 0; // Current chunk length
$chunks = []; // Chunks to return
$lDelim = ''; // Last delimiter
foreach ($lines as $index => $line) {
$lineLength = strlen($line);
if ($cLength + $lineLength + $delimiterLength <= $this->chunkSize) {
$cChunk .= $line . $delimiter;
$cLength += $lineLength + $delimiterLength;
$lDelim = $delimiter;
} else if ($lineLength <= $this->chunkSize) {
$chunks[] = trim($cChunk, $delimiter);
$cChunk = $line . $delimiter;
$cLength = $lineLength + $delimiterLength;
$lDelim = $delimiter;
} else {
$subChunks = new static($this->chunkSize, array_slice($this->delimiterOrder, 1));
$subDelimiter = $this->delimiterOrder[1] ?? '';
$subDelimiterLength = strlen($subDelimiter);
foreach ($subChunks->chunk($line) as $subChunk) {
$chunkLength = strlen($subChunk);
if ($cLength + $chunkLength + $subDelimiterLength <= $this->chunkSize) {
$cChunk .= $subChunk . $subDelimiter;
$cLength += $chunkLength + $subDelimiterLength;
$lDelim = $subDelimiter;
} else {
$chunks[] = trim($cChunk, $lDelim);
$cChunk = $subChunk . $subDelimiter;
$cLength = $chunkLength + $subDelimiterLength;
$lDelim = $subDelimiter;
}
}
}
}
if ($cChunk !== '') {
$chunks[] = trim($cChunk, $lDelim);
}
return $chunks;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace BookStack\Search\Queries;
use BookStack\Http\HttpRequestService;
use BookStack\Search\Queries\Services\OpenAiVectorQueryService;
use BookStack\Search\Queries\Services\VectorQueryService;
class VectorQueryServiceProvider
{
public function __construct(
protected HttpRequestService $http,
) {
}
public function get(): VectorQueryService
{
$service = $this->getServiceName();
if ($service === 'openai') {
return new OpenAiVectorQueryService(config('services.openai'), $this->http);
}
throw new \Exception("No '{$service}' LLM service found");
}
protected static function getServiceName(): string
{
return strtolower(config('services.llm'));
}
public static function isEnabled(): bool
{
return !empty(static::getServiceName());
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace BookStack\Search\Queries;
use BookStack\Entities\Models\Entity;
readonly class VectorSearchResult
{
public function __construct(
public Entity $entity,
public float $distance,
public string $matchText
) {
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace BookStack\Search\Queries;
use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
use Exception;
class VectorSearchRunner
{
public function __construct(
protected VectorQueryServiceProvider $vectorQueryServiceProvider,
protected PermissionApplicator $permissions,
protected MixedEntityListLoader $entityLoader,
) {
}
/**
* Run a vector search query to find results across entities.
* @return VectorSearchResult[]
* @throws Exception
*/
public function run(string $query): array
{
$queryService = $this->vectorQueryServiceProvider->get();
$queryVector = $queryService->generateEmbeddings($query);
// TODO - Test permissions applied
$topMatchesQuery = SearchVector::query()->select('text', 'entity_type', 'entity_id')
->selectRaw('VEC_DISTANCE_COSINE(VEC_FROMTEXT("[' . implode(',', $queryVector) . ']"), embedding) as distance')
->orderBy('distance', 'asc')
->having('distance', '<', 0.6)
->limit(10);
$query = $this->permissions->restrictEntityRelationQuery($topMatchesQuery, 'search_vectors', 'entity_id', 'entity_type');
$topMatches = $query->get();
$this->entityLoader->loadIntoRelations($topMatches->all(), 'entity', true);
$results = [];
foreach ($topMatches as $match) {
if ($match->relationLoaded('entity')) {
$results[] = new VectorSearchResult(
$match->getRelation('entity'),
$match->getAttribute('distance'),
$match->getAttribute('text'),
);
}
}
return $results;
}
}

View File

@@ -6,6 +6,7 @@ use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Queries\QueryPopular;
use BookStack\Entities\Tools\SiblingFetcher;
use BookStack\Http\Controller;
use BookStack\Search\Queries\VectorSearchRunner;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;

View File

@@ -6,6 +6,8 @@ use BookStack\Activity\Models\Tag;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Search\Queries\StoreEntityVectorsJob;
use BookStack\Search\Queries\VectorQueryServiceProvider;
use BookStack\Util\HtmlDocument;
use DOMNode;
use Illuminate\Database\Eloquent\Builder;
@@ -25,7 +27,7 @@ class SearchIndex
public static string $softDelimiters = ".-";
public function __construct(
protected EntityProvider $entityProvider
protected EntityProvider $entityProvider,
) {
}
@@ -37,6 +39,10 @@ class SearchIndex
$this->deleteEntityTerms($entity);
$terms = $this->entityToTermDataArray($entity);
$this->insertTerms($terms);
if (VectorQueryServiceProvider::isEnabled()) {
dispatch(new StoreEntityVectorsJob($entity));
}
}
/**
@@ -47,9 +53,15 @@ class SearchIndex
public function indexEntities(array $entities): void
{
$terms = [];
$vectorQueryEnabled = VectorQueryServiceProvider::isEnabled();
foreach ($entities as $entity) {
$entityTerms = $this->entityToTermDataArray($entity);
array_push($terms, ...$entityTerms);
if ($vectorQueryEnabled) {
dispatch(new StoreEntityVectorsJob($entity));
}
}
$this->insertTerms($terms);

485
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// TODO - Handle compatibility with older databases that don't support vectors
Schema::create('search_vectors', function (Blueprint $table) {
$table->string('entity_type', 100);
$table->integer('entity_id');
$table->text('text');
$table->index(['entity_type', 'entity_id']);
});
$table = DB::getTablePrefix() . 'search_vectors';
// TODO - Vector size might need to be dynamic
DB::statement("ALTER TABLE {$table} ADD COLUMN (embedding VECTOR(1536) NOT NULL)");
DB::statement("ALTER TABLE {$table} ADD VECTOR INDEX (embedding) DISTANCE=cosine");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('search_vectors');
}
};

View File

@@ -1 +1 @@
8c5c595184876ce12928f309aa3c9b571987c5e44c69c57c9cb17487f62bd709
22e02ee72d21ff719c1073abbec8302f8e2096ba6d072e133051064ed24b45b1

View File

@@ -760,13 +760,6 @@ Copyright: Copyright (c) 2014-present Fabien Potencier
Source: https://github.com/symfony/var-dumper.git
Link: https://symfony.com
-----------
thecodingmachine/safe
License: MIT
License File: vendor/thecodingmachine/safe/LICENSE
Copyright: Copyright (c) 2018 TheCodingMachine
Source: https://github.com/thecodingmachine/safe.git
Link: https://github.com/thecodingmachine/safe.git
-----------
tijsverkoyen/css-to-inline-styles
License: BSD-3-Clause
License File: vendor/tijsverkoyen/css-to-inline-styles/LICENSE.md

View File

@@ -6,7 +6,7 @@
*/
return [
'failed' => 'Въведените данни не съвпадат с информацията в системата.',
'failed' => 'Въведените удостоверителни данни не съвпадат с нашите записи.',
'throttle' => 'Твърде много опити за влизане. Опитайте пак след :seconds секунди.',
// Login & Register
@@ -65,7 +65,7 @@ return [
'email_confirm_thanks_desc' => 'Почакайте малко, обработвайки потвърждението ви. Ако не сте пренасочени след 3 секунди, то натиснете долу връзката "Продължаване", за да продължите.',
'email_not_confirmed' => 'Имейл адресът не е потвърден',
'email_not_confirmed_text' => 'Вашият имейл адрес все още не е потвърден.',
'email_not_confirmed_text' => 'Вашият емейл адрес все още не е потвърден.',
'email_not_confirmed_click_link' => 'Моля да последвате линка, който ви беше изпратен непосредствено след регистрацията.',
'email_not_confirmed_resend' => 'Ако не откривате писмото, може да го изпратите отново като попълните формуляра по-долу.',
'email_not_confirmed_resend_button' => 'Изпрати отново емейла за потвърждение',
@@ -91,7 +91,7 @@ return [
'mfa_option_totp_title' => 'Мобилно приложение',
'mfa_option_totp_desc' => 'За да използваш многофакторно удостоверяване, ще ти трябва мобилно приложение, което поддържа временни еднократни пароли (TOTP), като например Google Authenticator, Authy или Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Резервни кодове',
'mfa_option_backup_codes_desc' => 'Генерира набор от еднократни резервни кодове, които ще въвеждате при влизане, за да потвърдите самоличността си. Уверете се, че ги съхранявате на безопасно и сигурно място.',
'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',
'mfa_gen_confirm_and_enable' => 'Потвърди и включи',
'mfa_gen_backup_codes_title' => 'Настройка на резервни кодове',
'mfa_gen_backup_codes_desc' => 'Запази този лист с кодове на сигурно място. Когато достъпваш системата, ще можеш да използваш един от тези кодове като вторичен механизъм за удостоверяване.',

View File

@@ -6,7 +6,7 @@ return [
// Buttons
'cancel' => 'Отказ',
'close' => 'Затвори',
'close' => 'Close',
'confirm' => 'Потвърждаване',
'back' => 'Назад',
'save' => 'Запис',
@@ -20,7 +20,7 @@ return [
'description' => 'Описание',
'role' => 'Роля',
'cover_image' => 'Образ на корицата',
'cover_image_description' => 'Изображението трябва да е около 440x250 px. Тъй като ще се мащабира и изрязва автоматично спрямо нуждите на интерфейса, крайните размери при показване може да се различават.',
'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.',
// Actions
'actions' => 'Действия',
@@ -30,8 +30,8 @@ return [
'create' => 'Създаване',
'update' => 'Обновяване',
'edit' => 'Редактиране',
'archive' => 'Архивирай',
'unarchive' => 'Разархивирай',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => 'Сортиране',
'move' => 'Преместване',
'copy' => 'Копиране',
@@ -44,7 +44,7 @@ return [
'remove' => 'Премахване',
'add' => 'Добавяне',
'configure' => 'Конфигуриране',
'manage' => 'Управлявай',
'manage' => 'Manage',
'fullscreen' => 'Цял екран',
'favourite' => 'Любимо',
'unfavourite' => 'Не е любимо',
@@ -54,7 +54,7 @@ return [
'filter_clear' => 'Изчистване на филтрите',
'download' => 'Изтегляне',
'open_in_tab' => 'Отваряне в раздел',
'open' => 'Отвори',
'open' => 'Open',
// Sort Options
'sort_options' => 'Опции за сортиране',
@@ -111,5 +111,5 @@ return [
'terms_of_service' => 'Условия на услугата',
// OpenSearch
'opensearch_description' => 'Търси :appName',
'opensearch_description' => 'Search :appName',
];

View File

@@ -13,7 +13,7 @@ return [
'cancel' => 'Отказ',
'save' => 'Запис',
'close' => 'Затваряне',
'apply' => 'Приложи',
'apply' => 'Apply',
'undo' => 'Отмяна',
'redo' => 'Повтаряне',
'left' => 'Вляво',

View File

@@ -10,7 +10,7 @@ return [
// Auth
'error_user_exists_different_creds' => 'Потребител с емайл :email вече съществува но с други данни.',
'auth_pre_register_theme_prevention' => 'Потребителски профил не може да бъде създаден с посочената информация',
'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',
'email_already_confirmed' => 'Емейлът вече беше потвърден. Моля опитрайте да влезете.',
'email_confirmation_invalid' => 'Този код за достъп не е валиден или вече е бил използван, Моля опитай да се регистрираш отново.',
'email_confirmation_expired' => 'Кодът за потвърждение изтече, нов емейл за потвърждение беше изпратен.',
@@ -37,7 +37,7 @@ return [
'social_driver_not_found' => 'Кодът за връзка със социалната мрежа не съществува',
'social_driver_not_configured' => 'Социалните настройки на твоя :socialAccount не са конфигурирани правилно.',
'invite_token_expired' => 'Твоята покана е изтекла. Вместо това може да пробваш да възстановиш паролата на профила си.',
'login_user_not_found' => 'Потребител за това действие не може да бъде намерено.',
'login_user_not_found' => 'A user for this action could not be found.',
// System
'path_not_writable' => 'Не може да се качи файл в :filePath. Увери се на сървъра, че в пътя може да се записва.',

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'Nelze načíst ZIP soubor.',
'import_zip_cant_decode_data' => 'Nelze najít a dekódovat data.json v archivu ZIP.',
'import_zip_no_data' => 'ZIP archiv neobsahuje knihy, kapitoly nebo stránky.',
'import_zip_data_too_large' => 'Obsah souboru data.json v archivu ZIP překračuje maximální povolenou velikost.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'Importování ZIP selhalo s chybami:',
'import_zip_failed_notification' => 'Nepodařilo se naimportovat ZIP soubor.',
'import_perms_books' => 'Chybí vám požadovaná oprávnění k vytvoření knih.',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'Nahrávání :attribute se nezdařilo.',
'zip_file' => ':attribute musí odkazovat na soubor v archivu ZIP.',
'zip_file_size' => 'Soubor :attribute nesmí překročit :size MB.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => ':attribute musí odkazovat na soubor typu :validTypes, nalezen :foundType.',
'zip_model_expected' => 'Očekáván datový objekt, ale nalezen „:type“.',
'zip_unique' => ':attribute musí být jedinečný pro typ objektu v archivu ZIP.',

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'No se pudo leer el archivo ZIP.',
'import_zip_cant_decode_data' => 'No se pudo encontrar y decodificar el archivo data.json. en el archivo ZIP.',
'import_zip_no_data' => 'Los datos del archivo ZIP no contienen ningún libro, capítulo o contenido de página.',
'import_zip_data_too_large' => 'El contenido del ZIP data.json excede el tamaño máximo de carga configurado.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'Error al validar la importación del ZIP con errores:',
'import_zip_failed_notification' => 'Error al importar archivo ZIP.',
'import_perms_books' => 'Le faltan los permisos necesarios para crear libros.',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'El archivo no ha podido subirse. Es posible que el servidor no acepte archivos de este tamaño.',
'zip_file' => 'El :attribute necesita hacer referencia a un archivo dentro del ZIP.',
'zip_file_size' => 'El archivo :attribute no debe exceder :size MB.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'El :attribute necesita hacer referencia a un archivo de tipo :validTypes, encontrado :foundType.',
'zip_model_expected' => 'Se esperaba un objeto de datos, pero se encontró ":type".',
'zip_unique' => 'El :attribute debe ser único para el tipo de objeto dentro del ZIP.',

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'No se pudo leer el archivo ZIP.',
'import_zip_cant_decode_data' => 'No se pudo encontrar ni decodificar el contenido del archivo ZIP data.json.',
'import_zip_no_data' => 'Los datos del archivo ZIP no tienen un libro, un capítulo o contenido de página en su contenido.',
'import_zip_data_too_large' => 'El contenido del ZIP data.json excede el tamaño máximo de carga configurado.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'Error al validar la importación del ZIP con los errores:',
'import_zip_failed_notification' => 'Error al importar archivo ZIP.',
'import_perms_books' => 'Le faltan los permisos necesarios para crear libros.',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'El archivo no se pudo subir. Puede ser que el servidor no acepte archivos de este tamaño.',
'zip_file' => 'El :attribute necesita hacer referencia a un archivo dentro del ZIP.',
'zip_file_size' => 'El archivo :attribute no debe exceder :size MB.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'El :attribute necesita hacer referencia a un archivo de tipo :validTypes, encontrado :foundType.',
'zip_model_expected' => 'Se esperaba un objeto de datos, pero se encontró ":type".',
'zip_unique' => 'El :attribute debe ser único para el tipo de objeto dentro del ZIP.',

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'ZIP-faili lugemine ebaõnnestus.',
'import_zip_cant_decode_data' => 'ZIP-failist ei leitud data.json sisu.',
'import_zip_no_data' => 'ZIP-failist ei leitud raamatute, peatükkide või lehtede sisu.',
'import_zip_data_too_large' => 'ZIP-faili data.json sisu ületab rakenduses seadistatud maksimaalse failisuuruse.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'Imporditud ZIP-faili valideerimine ebaõnnestus vigadega:',
'import_zip_failed_notification' => 'ZIP-faili importimine ebaõnnestus.',
'import_perms_books' => 'Sul puuduvad õigused raamatute lisamiseks.',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'Faili üleslaadimine ebaõnnestus. Server ei pruugi sellise suurusega faile vastu võtta.',
'zip_file' => ':attribute peab viitama failile ZIP-arhiivi sees.',
'zip_file_size' => 'Fail :attribute ei tohi olla suurem kui :size MB.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => ':attribute peab viitama :validTypes tüüpi failile, leiti :foundType.',
'zip_model_expected' => 'Oodatud andmete asemel leiti ":type".',
'zip_unique' => ':attribute peab olema ZIP-arhiivi piires objekti tüübile unikaalne.',

View File

@@ -54,7 +54,7 @@ return [
'import_continue_desc' => 'Examinez le contenu à importer à partir du fichier ZIP téléchargé. Lorsque vous êtes prêt, lancez l\'importation pour ajouter son contenu à ce système. Le fichier d\'importation ZIP téléchargé sera automatiquement supprimé si l\'importation est réussie.',
'import_details' => 'Détails de l\'importation',
'import_run' => 'Exécuter Importation',
'import_size' => ':size Taille du fichier ZIP à importer',
'import_size' => ':size taille du ZIP d\'import',
'import_uploaded_at' => ':relativeTime téléchargé',
'import_uploaded_by' => 'Téléchargé par',
'import_location' => 'Emplacement de l\'importation',
@@ -330,13 +330,13 @@ return [
// Editor Sidebar
'toggle_sidebar' => 'Afficher/masquer la barre latérale',
'page_tags' => 'Étiquettes de la page',
'chapter_tags' => 'Étiquettes du chapitre',
'book_tags' => 'Étiquettes du livre',
'shelf_tags' => 'Étiquettes de l\'étagère',
'tag' => 'Étiquette',
'tags' => 'Étiquettes',
'tags_index_desc' => 'Les étiquettes peuvent être mises sur le contenu pour appliquer une forme flexible de catégorisation. Les étiquettes peuvent avoir à la fois une clé et une valeur, la valeur étant facultative. Une fois appliqué, le contenu peut ensuite être interrogé à laide du nom et de la valeur de létiquette.',
'page_tags' => 'Mots-clés de la page',
'chapter_tags' => 'Mots-clés du chapitre',
'book_tags' => 'Mots-clés du livre',
'shelf_tags' => 'Mots-clés de l\'étagère',
'tag' => 'Mot-clé',
'tags' => 'Mots-clés',
'tags_index_desc' => 'Les tags peuvent être appliqués au contenu du système pour appliquer une forme flexible de catégorisation. Les tags peuvent avoir à la fois une clé et une valeur, la valeur étant facultative. Une fois appliqué, le contenu peut ensuite être interrogé à laide du nom et de la valeur du tag.',
'tag_name' => 'Nom de létiquette',
'tag_value' => 'Valeur du mot-clé (optionnel)',
'tags_explain' => "Ajouter des mots-clés pour catégoriser votre contenu.",

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'Impossible de lire le fichier ZIP.',
'import_zip_cant_decode_data' => 'Impossible de trouver et de décoder le contenu ZIP data.json.',
'import_zip_no_data' => 'Les données du fichier ZIP n\'ont pas de livre, de chapitre ou de page attendus.',
'import_zip_data_too_large' => 'Le contenu du fichier ZIP pour data.json dépasse la taille maximale de téléversement autorisée.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'L\'importation du ZIP n\'a pas été validée avec les erreurs :',
'import_zip_failed_notification' => 'Impossible d\'importer le fichier ZIP.',
'import_perms_books' => 'Vous n\'avez pas les permissions requises pour créer des livres.',

View File

@@ -4,15 +4,15 @@
*/
return [
'new_comment_subject' => 'Nouveau commentaire sur la page : :pageName',
'new_comment_subject' => 'Nouveau commentaire sur la page: :pageName',
'new_comment_intro' => 'Un utilisateur a commenté une page dans :appName:',
'new_page_subject' => 'Nouvelle page: :pageName',
'new_page_intro' => 'Une nouvelle page a été créée dans :appName:',
'updated_page_subject' => 'Page mise à jour: :pageName',
'updated_page_intro' => 'Une page a été mise à jour dans :appName:',
'updated_page_debounce' => 'Pour éviter de nombreuses notifications, pendant un certain temps, vous ne recevrez pas de notifications pour d\'autres modifications de cette page par le même éditeur.',
'comment_mention_subject' => 'Vous avez été mentionné dans un commentaire sur la page : :pageName',
'comment_mention_intro' => 'Vous avez été mentionné dans un commentaire sur :appName:',
'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',
'comment_mention_intro' => 'You were mentioned in a comment on :appName:',
'detail_page_name' => 'Nom de la page :',
'detail_page_path' => 'Chemin de la page :',

View File

@@ -23,7 +23,7 @@ return [
'notifications_desc' => 'Contrôlez les notifications par e-mail que vous recevez lorsque certaines activités sont effectuées dans le système.',
'notifications_opt_own_page_changes' => 'Notifier lors des modifications des pages que je possède',
'notifications_opt_own_page_comments' => 'Notifier lorsque les pages que je possède sont commentées',
'notifications_opt_comment_mentions' => 'Notifier lorsque je suis mentionné dans un commentaire',
'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment',
'notifications_opt_comment_replies' => 'Notifier les réponses à mes commentaires',
'notifications_save' => 'Enregistrer les préférences',
'notifications_update_success' => 'Les préférences de notification ont été mises à jour !',

View File

@@ -17,14 +17,14 @@ return [
'app_features_security' => 'Fonctionnalités et sécurité',
'app_name' => 'Nom de l\'application',
'app_name_desc' => 'Ce nom est affiché dans l\'en-tête et les e-mails.',
'app_name_header' => 'Afficher le nom dans l\'en-tête',
'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
'app_public_access' => 'Accès public',
'app_public_access_desc' => 'L\'activation de cette option permettra aux visiteurs, qui ne sont pas connectés, d\'accéder au contenu de votre instance BookStack.',
'app_public_access_desc_guest' => 'L\'accès pour les visiteurs publics peut être contrôlé par l\'utilisateur "Guest".',
'app_public_access_toggle' => 'Autoriser l\'accès public',
'app_public_viewing' => 'Accepter l\'affichage public des pages ?',
'app_secure_images' => 'Ajout d\'image sécurisé',
'app_secure_images_toggle' => 'Activer l\'ajout d\'image sécurisée',
'app_secure_images_toggle' => 'Activer l\'ajout d\'image sécurisé',
'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.',
'app_default_editor' => 'Éditeur de page par défaut',
'app_default_editor_desc' => 'Sélectionnez l\'éditeur qui sera utilisé par défaut lors de l\'édition de nouvelles pages. Cela peut être remplacé au niveau de la page où les permissions sont autorisées.',
@@ -75,8 +75,8 @@ return [
'reg_confirm_restrict_domain_placeholder' => 'Aucune restriction en place',
// Sorting Settings
'sorting' => 'Listes et tri',
'sorting_book_default' => 'Tri des livres par défaut',
'sorting' => 'Lists & Sorting',
'sorting_book_default' => 'Default Book Sort Rule',
'sorting_book_default_desc' => 'Sélectionnez le tri par défaut à mettre en place sur les nouveaux livres. Cela naffectera pas les livres existants, et peut être redéfini dans les livres.',
'sorting_rules' => 'Règles de tri',
'sorting_rules_desc' => 'Ce sont les opérations de tri qui peuvent être appliquées au contenu du système.',
@@ -103,8 +103,8 @@ return [
'sort_rule_op_updated_date' => 'Date de mise à jour',
'sort_rule_op_chapters_first' => 'Chapitres en premier',
'sort_rule_op_chapters_last' => 'Chapitres en dernier',
'sorting_page_limits' => 'Limite d\'affichage par page',
'sorting_page_limits_desc' => 'Définissez le nombre déléments à afficher par page dans les différentes listes du système. En général, un nombre plus faible offre de meilleures performances, tandis quun nombre plus élevé réduit le besoin de naviguer entre plusieurs pages. Il est recommandé dutiliser un multiple pair de 3 (18, 24, 30, etc.).',
'sorting_page_limits' => 'Per-Page Display Limits',
'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.',
// Maintenance settings
'maint' => 'Maintenance',
@@ -197,13 +197,13 @@ return [
'role_import_content' => 'Importer le contenu',
'role_editor_change' => 'Changer l\'éditeur de page',
'role_notifications' => 'Recevoir et gérer les notifications',
'role_permission_note_users_and_roles' => 'Ces autorisations permettront également l\'accès à la consultation et la recherche des utilisateurs et des rôles dans le système.',
'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',
'role_asset' => 'Permissions des ressources',
'roles_system_warning' => 'Sachez que l\'accès à l\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. N\'attribuez uniquement des rôles avec ces permissions qu\'à des utilisateurs de confiance.',
'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions',
'role_asset_admins' => 'Les administrateurs ont automatiquement accès à tous les contenus mais les options suivantes peuvent afficher ou masquer certaines options de l\'interface.',
'role_asset_image_view_note' => 'Cela concerne la visibilité dans le gestionnaire d\'images. L\'accès réel des fichiers d\'image téléchargés dépendra de l\'option de stockage d\'images du système.',
'role_asset_users_note' => 'Ces autorisations permettront également l\'accès à la consultation et la recherche des utilisateurs dans le système.',
'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',
'role_all' => 'Tous',
'role_own' => 'Propres',
'role_controlled_by_asset' => 'Contrôlé par les ressources les ayant envoyés',
@@ -270,7 +270,7 @@ return [
'user_api_token_name_desc' => 'Donnez à votre jeton un nom lisible pour l\'identifier plus tard.',
'user_api_token_expiry' => 'Date d\'expiration',
'user_api_token_expiry_desc' => 'Définissez une date à laquelle ce jeton expire. Après cette date, les demandes effectuées à l\'aide de ce jeton ne fonctionneront plus. Le fait de laisser ce champ vide entraînera une expiration dans 100 ans.',
'user_api_token_create_secret_message' => 'Immédiatement après la création de ce jeton, un "ID de jeton" et "Secret de jeton" sera généré et affiché. Le secret ne sera affiché qu\'une seule fois, alors assurez-vous de copier la valeur dans un endroit sûr et sécurisé avant de continuer.',
'user_api_token_create_secret_message' => 'Immédiatement après la création de ce jeton, un "ID de jeton" "et" Secret de jeton "sera généré et affiché. Le secret ne sera affiché qu\'une seule fois, alors assurez-vous de copier la valeur dans un endroit sûr et sécurisé avant de continuer.',
'user_api_token' => 'Jeton API',
'user_api_token_id' => 'Token ID',
'user_api_token_id_desc' => 'Il s\'agit d\'un identifiant généré par le système non modifiable pour ce jeton qui devra être fourni dans les demandes d\'API.',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'Le fichier n\'a pas pu être envoyé. Le serveur peut ne pas accepter des fichiers de cette taille.',
'zip_file' => 'L\'attribut :attribute doit référencer un fichier dans le ZIP.',
'zip_file_size' => 'Le fichier :attribute ne doit pas dépasser :size Mo.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => ':attribute doit référencer un fichier de type :validTypes, trouvé :foundType.',
'zip_model_expected' => 'Objet de données attendu, mais ":type" trouvé.',
'zip_unique' => 'L\'attribut :attribute doit être unique pour le type d\'objet dans le ZIP.',

View File

@@ -85,12 +85,12 @@ return [
'webhook_delete_notification' => 'Webhook sikeresen törölve',
// Imports
'import_create' => 'import elkészült',
'import_create_notification' => 'Az import sikeresen feltöltötve',
'import_run' => 'import frissítve',
'import_run_notification' => 'A tartalmat sikeresen importáltam.',
'import_delete' => 'import törölve',
'import_delete_notification' => 'Az import sikeresen törölve',
'import_create' => 'created import',
'import_create_notification' => 'Import successfully uploaded',
'import_run' => 'updated import',
'import_run_notification' => 'Content successfully imported',
'import_delete' => 'deleted import',
'import_delete_notification' => 'Import successfully deleted',
// Users
'user_create' => 'létrehozta a felhasználót',

View File

@@ -30,8 +30,8 @@ return [
'create' => 'Létrehozás',
'update' => 'Frissítés',
'edit' => 'Szerkesztés',
'archive' => 'Archiválás',
'unarchive' => 'Archiválás visszavonása',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => 'Rendezés',
'move' => 'Áthelyezés',
'copy' => 'Másolás',
@@ -111,5 +111,5 @@ return [
'terms_of_service' => 'Felhasználási feltételek',
// OpenSearch
'opensearch_description' => 'Keresés :appName',
'opensearch_description' => 'Search :appName',
];

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'Impossibile leggere il file ZIP.',
'import_zip_cant_decode_data' => 'Impossibile trovare e decodificare il contenuto ZIP data.json.',
'import_zip_no_data' => 'I dati del file ZIP non hanno il contenuto previsto di libri, capitoli o pagine.',
'import_zip_data_too_large' => 'Il contenuto ZIP data.json supera la dimensione massima di upload configurata nell\'applicazione.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'L\'importazione ZIP non è stata convalidata con errori:',
'import_zip_failed_notification' => 'Impossibile importare il file ZIP.',
'import_perms_books' => 'Non hai i permessi necessari per creare libri.',

View File

@@ -11,8 +11,8 @@ return [
'updated_page_subject' => 'Pagina aggiornata: :pageName',
'updated_page_intro' => 'Una pagina è stata aggiornata in :appName:',
'updated_page_debounce' => 'Per evitare una massa di notifiche, per un po\' non ti verranno inviate notifiche per ulteriori modifiche a questa pagina dallo stesso editor.',
'comment_mention_subject' => 'Sei stato menzionato in un commento nella pagina: :pageName',
'comment_mention_intro' => 'Sei stato menzionato in un commento su :appName:',
'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',
'comment_mention_intro' => 'You were mentioned in a comment on :appName:',
'detail_page_name' => 'Nome della pagina:',
'detail_page_path' => 'Percorso della pagina:',

View File

@@ -23,7 +23,7 @@ return [
'notifications_desc' => 'Controlla le notifiche email che ricevi quando viene eseguita una determinata attività all\'interno del sistema.',
'notifications_opt_own_page_changes' => 'Notifica in caso di modifiche alle pagine che possiedo',
'notifications_opt_own_page_comments' => 'Notifica i commenti sulle pagine che possiedo',
'notifications_opt_comment_mentions' => 'Avvisami quando vengo menzionato in un commento',
'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment',
'notifications_opt_comment_replies' => 'Notificare le risposte ai miei commenti',
'notifications_save' => 'Salva preferenze',
'notifications_update_success' => 'Le preferenze di notifica sono state aggiornate!',

View File

@@ -197,13 +197,13 @@ return [
'role_import_content' => 'Importa contenuto',
'role_editor_change' => 'Cambiare editor di pagina',
'role_notifications' => 'Ricevere e gestire le notifiche',
'role_permission_note_users_and_roles' => 'Queste autorizzazioni forniranno tecnicamente anche la visibilità e la ricerca di utenti e ruoli nel sistema.',
'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',
'role_asset' => 'Permessi entità',
'roles_system_warning' => 'Siate consapevoli che l\'accesso a uno dei tre permessi qui sopra può consentire a un utente di modificare i propri privilegi o i privilegi di altri nel sistema. Assegna ruoli con questi permessi solo ad utenti fidati.',
'role_asset_desc' => 'Questi permessi controllano l\'accesso predefinito alle entità. I permessi in libri, capitoli e pagine sovrascriveranno questi.',
'role_asset_admins' => 'Gli amministratori hanno automaticamente accesso a tutti i contenuti ma queste opzioni possono mostrare o nascondere le opzioni della UI.',
'role_asset_image_view_note' => 'Questo si riferisce alla visibilità all\'interno del gestore delle immagini. L\'accesso effettivo ai file di immagine caricati dipenderà dall\'opzione di archiviazione delle immagini di sistema.',
'role_asset_users_note' => 'Queste autorizzazioni forniranno tecnicamente anche la visibilità e la ricerca di utenti nel sistema.',
'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',
'role_all' => 'Tutti',
'role_own' => 'Propri',
'role_controlled_by_asset' => 'Controllato dall\'entità in cui sono caricati',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'Il file non può essere caricato. Il server potrebbe non accettare file di questa dimensione.',
'zip_file' => 'L\'attributo :attribute deve fare riferimento a un file all\'interno dello ZIP.',
'zip_file_size' => 'Il file :attribute non deve superare :size MB.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'Il campo :attribute deve fare riferimento a un file di tipo :validTypes, trovato :foundType.',
'zip_model_expected' => 'Oggetto dati atteso ma ":type" trovato.',
'zip_unique' => 'L\'attributo :attribute deve essere univoco per il tipo di oggetto all\'interno dello ZIP.',

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'ZIPファイルを読み込めません。',
'import_zip_cant_decode_data' => 'ZIPファイル内に data.json が見つからないかデコードできませんでした。',
'import_zip_no_data' => 'ZIPファイルのデータにブック、チャプター、またはページコンテンツがありません。',
'import_zip_data_too_large' => 'ZIPに含まれる data.json が、アプリケーションで設定された最大アップロードサイズを超えています。',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'エラーによりインポートZIPの検証に失敗しました:',
'import_zip_failed_notification' => 'ZIP ファイルのインポートに失敗しました。',
'import_perms_books' => 'ブックを作成するために必要な権限がありません。',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'ファイルをアップロードできませんでした。サーバーがこのサイズのファイルを受け付けていない可能性があります。',
'zip_file' => ':attribute はZIP 内のファイルを参照する必要があります。',
'zip_file_size' => ':attribute は :size MB を超えてはいけません。',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => ':attribute は種別 :validType のファイルを参照する必要がありますが、種別 :foundType となっています。',
'zip_model_expected' => 'データオブジェクトが期待されますが、":type" が見つかりました。',
'zip_unique' => 'ZIP内のオブジェクトタイプに :attribute が一意である必要があります。',

View File

@@ -33,7 +33,7 @@ return [
'book_create_from_chapter' => '챕터를 책으로 변환',
'book_create_from_chapter_notification' => '챕터가 책으로 성공적으로 변환되었습니다.',
'book_update' => '업데이트된 책',
'book_update_notification' => '책 수정함',
'book_update_notification' => '책이 성공적으로 업데이트되었습니다.',
'book_delete' => '삭제된 책',
'book_delete_notification' => '책이 성공적으로 삭제되었습니다.',
'book_sort' => '책 정렬',
@@ -50,9 +50,9 @@ return [
'bookshelf_delete_notification' => '책장이 성공적으로 삭제되었습니다.',
// Revisions
'revision_restore' => '복원한 수정본',
'revision_delete' => '삭제한 수정본',
'revision_delete_notification' => '수정본을 잘 삭제함',
'revision_restore' => '버전 복구',
'revision_delete' => '버전 삭제',
'revision_delete_notification' => '버전 삭제 성공',
// Favourites
'favourite_add_notification' => '":name" 을 북마크에 추가하였습니다.',

View File

@@ -17,7 +17,7 @@ return [
'logout' => '로그아웃',
'name' => '이름',
'username' => '용자',
'username' => '용자 이름',
'email' => '전자우편 주소',
'password' => '비밀번호',
'password_confirm' => '비밀번호 확인',

View File

@@ -30,8 +30,8 @@ return [
'create' => '만들기',
'update' => '바꾸기',
'edit' => '수정',
'archive' => '보관',
'unarchive' => '보관 해제',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => '정렬',
'move' => '이동',
'copy' => '복사',
@@ -43,31 +43,31 @@ return [
'reset' => '리셋',
'remove' => '제거',
'add' => '추가',
'configure' => '구성',
'configure' => '설정',
'manage' => '관리',
'fullscreen' => '전체화면',
'favourite' => '즐겨찾기',
'unfavourite' => '즐겨찾기 해제',
'next' => '다음',
'previous' => '이전',
'filter_active' => '적용 필터:',
'filter_clear' => '필터 해제',
'filter_active' => '적용 :',
'filter_clear' => '모든 필터 해제',
'download' => '내려받기',
'open_in_tab' => '탭에서 열기',
'open' => '열기',
'open' => '열기 ',
// Sort Options
'sort_options' => '정렬 기준',
'sort_direction_toggle' => '순서 반전',
'sort_ascending' => '오름차순',
'sort_descending' => '내림차순',
'sort_name' => '이름',
'sort_name' => '제목',
'sort_default' => '기본값',
'sort_created_at' => '만든 날짜',
'sort_updated_at' => '갱신한 날짜',
'sort_updated_at' => '수정한 날짜',
// Misc
'deleted_user' => '삭제한 용자',
'deleted_user' => '삭제한 용자',
'no_activity' => '활동 없음',
'no_items' => '항목 없음',
'back_to_top' => '맨 위로',
@@ -75,8 +75,8 @@ return [
'toggle_details' => '내용 보기',
'toggle_thumbnails' => '썸네일 보기',
'details' => '정보',
'grid_view' => '격자로 보기',
'list_view' => '목록으로 보기',
'grid_view' => '격자 형식으로 보기',
'list_view' => '리스트 형식으로 보기',
'default' => '기본 설정',
'breadcrumb' => '탐색 경로',
'status' => '상태',

View File

@@ -8,7 +8,7 @@ return [
'image_select' => '이미지 선택',
'image_list' => '이미지 목록',
'image_details' => '이미지 상세정보',
'image_upload' => '이미지 올려두기',
'image_upload' => '이미지 업로드',
'image_intro' => '여기에서 이전에 시스템에 업로드한 이미지를 선택하고 관리할 수 있습니다.',
'image_intro_upload' => '이미지 파일을 이 창으로 끌어다 놓거나 위의 \'이미지 업로드\' 버튼을 사용하여 새 이미지를 업로드합니다.',
'image_all' => '모든 이미지',
@@ -17,7 +17,7 @@ return [
'image_page_title' => '이 문서에서 쓰고 있는 이미지',
'image_search_hint' => '이미지 이름 검색',
'image_uploaded' => '올림 :uploadedDate',
'image_uploaded_by' => ':userName 이용자가 올려둠',
'image_uploaded_by' => '업로드 :userName',
'image_uploaded_to' => ':pageLink 로 업로드됨',
'image_updated' => '갱신일 :updateDate',
'image_load_more' => '더 보기',

View File

@@ -16,8 +16,8 @@ return [
'recently_viewed' => '최근에 본 목록',
'recent_activity' => '최근 활동 기록',
'create_now' => '바로 만들기',
'revisions' => '수정본',
'meta_revision' => '수정본 #:revisionCount',
'revisions' => '버전',
'meta_revision' => '버전 #:revisionCount',
'meta_created' => '생성 :timeLength',
'meta_created_name' => '생성 :timeLength, :user',
'meta_updated' => '수정 :timeLength',
@@ -115,7 +115,7 @@ return [
'shelves_create' => '책꽂이 만들기',
'shelves_popular' => '많이 읽은 책꽂이',
'shelves_new' => '새로운 책꽂이',
'shelves_new_action' => '새 책꽂이',
'shelves_new_action' => '새로운 책꽂이',
'shelves_popular_empty' => '많이 읽은 책꽂이 목록',
'shelves_new_empty' => '새로운 책꽂이 목록',
'shelves_save' => '저장',
@@ -148,7 +148,7 @@ return [
'books_popular' => '많이 읽은 책',
'books_recent' => '최근에 읽은 책',
'books_new' => '새로운 책',
'books_new_action' => '새 책',
'books_new_action' => '새로운 책',
'books_popular_empty' => '많이 읽은 책 목록',
'books_new_empty' => '새로운 책 목록',
'books_create' => '책 만들기',
@@ -200,7 +200,7 @@ return [
'chapters' => '챕터',
'x_chapters' => '챕터 :count개|총 :count개',
'chapters_popular' => '많이 읽은 챕터',
'chapters_new' => '새',
'chapters_new' => '새로운 챕터',
'chapters_create' => '챕터 만들기',
'chapters_delete' => '챕터 삭제하기',
'chapters_delete_named' => ':chapterName(을)를 지웁니다.',
@@ -221,11 +221,11 @@ return [
'chapter_sort_book' => '책 정렬하기',
// Pages
'page' => '페이지',
'pages' => '페이지',
'page' => '문서',
'pages' => '문서',
'x_pages' => '문서 :count개|총 :count개',
'pages_popular' => '많이 읽은 문서',
'pages_new' => '새 페이지',
'pages_new' => '새로운 문서',
'pages_attachments' => '첨부',
'pages_navigation' => '목차',
'pages_delete' => '문서 삭제하기',
@@ -272,7 +272,7 @@ return [
'pages_md_insert_drawing' => '드로잉 추가',
'pages_md_show_preview' => '미리보기 표시',
'pages_md_sync_scroll' => '미리보기 스크롤 동기화',
'pages_md_plain_editor' => '플레인텍스트 편집기',
'pages_md_plain_editor' => 'Plaintext editor',
'pages_drawing_unsaved' => '저장되지 않은 드로잉 발견',
'pages_drawing_unsaved_confirm' => '이전에 실패한 드로잉 저장 시도에서 저장되지 않은 드로잉 데이터가 발견되었습니다. 이 저장되지 않은 드로잉을 복원하고 계속 편집하시겠습니까?',
'pages_not_in_chapter' => '챕터에 있는 문서가 아닙니다.',
@@ -290,10 +290,10 @@ return [
'pages_revision_restored_from' => '#:id; :summary에서 복구함',
'pages_revisions_created_by' => '만든 사용자',
'pages_revisions_date' => '수정한 날짜',
'pages_revisions_number' => '#',
'pages_revisions_number' => 'No.',
'pages_revisions_sort_number' => '수정 번호',
'pages_revisions_numbered' => '수정본 #:id',
'pages_revisions_numbered_changes' => '수정본 #:id에서 바꾼 부분',
'pages_revisions_numbered' => '수정본 :id',
'pages_revisions_numbered_changes' => '수정본 :id에서 바꾼 부분',
'pages_revisions_editor' => '편집기 유형',
'pages_revisions_changelog' => '설명',
'pages_revisions_changes' => '바꾼 부분',
@@ -310,7 +310,7 @@ return [
'pages_pointer_toggle_link' => '퍼머링크 모드, 포함 태그를 표시하려면 누릅니다.',
'pages_pointer_toggle_include' => '태그 포함 모드, 퍼머링크를 표시하려면 누릅니다.',
'pages_permissions_active' => '문서 권한 허용함',
'pages_initial_revision' => '최초 게시',
'pages_initial_revision' => '처음 판본',
'pages_references_update_revision' => '시스템에서 내부 링크 자동 업데이트',
'pages_initial_name' => '제목 없음',
'pages_editing_draft_notification' => ':timeDiff에 초안 문서입니다.',
@@ -330,27 +330,27 @@ return [
// Editor Sidebar
'toggle_sidebar' => '사이드바 토글',
'page_tags' => '페이지 태그',
'chapter_tags' => '장 태그',
'book_tags' => '책 태그',
'shelf_tags' => '책꽂이 태그',
'tag' => '태그',
'tags' => '태그',
'page_tags' => '문서 태그',
'chapter_tags' => '챕터 꼬리표',
'book_tags' => '책 꼬리표',
'shelf_tags' => '책꽂이 꼬리표',
'tag' => '꼬리표',
'tags' => '꼬리표',
'tags_index_desc' => '태그를 시스템 내의 콘텐츠에 적용하여 유연한 형태의 분류를 적용할 수 있습니다. 태그는 키와 값을 모두 가질 수 있으며 값은 선택 사항입니다. 태그가 적용되면 태그 이름과 값을 사용하여 콘텐츠를 쿼리할 수 있습니다.',
'tag_name' => '태그 이름',
'tag_name' => '꼬리표 이름',
'tag_value' => '리스트 값 (선택 사항)',
'tags_explain' => "문서를 더 잘 분류하려면 태그를 추가하세요.\n태그에 값을 할당하여 더욱 체계적으로 구성할 수 있습니다.",
'tags_add' => '다른 태그 추가하기',
'tags_remove' => '이 태그 제거하기',
'tags_usages' => '전체 태그 이용량',
'tags_assigned_pages' => '| 페이지 태그 할당 |',
'tags_assigned_chapters' => '| 장 태그 할당 |',
'tags_assigned_books' => '| 책 태그 할당 |',
'tags_assigned_shelves' => '| 책꽂이 태그 할당 |',
'tags_add' => '꼬리표 추가',
'tags_remove' => '꼬리표 삭제',
'tags_usages' => '모든 꼬리표',
'tags_assigned_pages' => '문서에 꼬리표 지정함',
'tags_assigned_chapters' => '챕터에 꼬리표 지정함',
'tags_assigned_books' => '책 태그 지정함',
'tags_assigned_shelves' => '책꽂이에 꼬리표 지정함',
'tags_x_unique_values' => ':count 중복 없는 값',
'tags_all_values' => '모든 값',
'tags_view_tags' => '태그 보기',
'tags_view_existing_tags' => '기존 태그 보기',
'tags_view_tags' => '꼬리표 보기',
'tags_view_existing_tags' => '사용 중인 꼬리표 보기',
'tags_list_empty_hint' => '태그는 에디터 사이드바나 책, 챕터 또는 책꽂이 정보 편집에서 지정할 수 있습니다.',
'attachments' => '첨부 파일',
'attachments_explain' => '파일이나 링크를 첨부하세요. 정보 탭에 나타납니다.',
@@ -403,7 +403,7 @@ return [
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_save' => '등록',
'comment_new' => '새 의견',
'comment_new' => '새로운 댓글',
'comment_created' => '댓글 등록함 :createDiff',
'comment_updated' => ':username(이)가 댓글 수정함 :updateDiff',
'comment_updated_indicator' => '업데이트됨',
@@ -416,14 +416,14 @@ return [
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => '이 댓글을 지울 건가요?',
'comment_in_reply_to' => ':commentId(을)를 향한 답글',
'comment_reference' => '참조',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_editor_explain' => '이 페이지에 남겨진 댓글은 다음과 같습니다. 저장된 페이지를 볼 때 댓글을 추가하고 관리할 수 있습니다.',
// Revision
'revision_delete_confirm' => '이 수정본을 지울 건가요?',
'revision_restore_confirm' => '이 버전을 되돌릴 건가요? 현재 페이지는 대체됩니다.',
'revision_cannot_delete_latest' => '최신 수정본은 지울 수 없습니다.',
'revision_cannot_delete_latest' => '현재 버전본은 지울 수 없습니다.',
// Copy view
'copy_consider' => '항목을 복사할 때 다음을 고려하세요.',

View File

@@ -18,13 +18,13 @@ return [
'app_name' => '애플리케이션 이름 (사이트 제목)',
'app_name_desc' => '이 이름은 헤더와 시스템에서 보낸 모든 이메일에 표시됩니다.',
'app_name_header' => '헤더에 이름 표시',
'app_public_access' => '공개 접근',
'app_public_access_desc' => '이 옵션을 활성화하면 로그인하지 않은 방문자가 BookStack 인스턴스의 내용에 접근할 수 있습니다.',
'app_public_access' => '사이트 공개',
'app_public_access_desc' => '이 옵션을 활성화하면 로그인하지 않은 방문자도 이 서버의 콘텐츠에 액세스할 수 있습니다.',
'app_public_access_desc_guest' => '일반 방문자의 액세스는 "Guest" 사용자를 통해 제어할 수 있습니다.',
'app_public_access_toggle' => '공개 액세스 허용',
'app_public_viewing' => '공개 열람을 허용할까요?',
'app_secure_images' => '보안 강화하여 이미지 올려두기',
'app_secure_images_toggle' => '보안 강화하여 이미지 올려두기 활성화',
'app_secure_images' => '보안 강화 이미지 업로드',
'app_secure_images_toggle' => '보안 강화 이미지 업로드 사용',
'app_secure_images_desc' => '성능상의 이유로 모든 이미지는 공개됩니다. 이 옵션은 이미지 URL 앞에 추측하기 어려운 임의의 문자열을 추가합니다. 쉽게 액세스할 수 없도록 디렉토리 인덱스가 활성화되어 있지 않은지 확인하세요.',
'app_default_editor' => '기본 페이지 편집기',
'app_default_editor_desc' => '새 페이지를 편집할 때 기본으로 사용될 편집기를 선택합니다. 권한을 갖고 있다면 페이지마다 다르게 적용될 수 있습니다.',
@@ -157,7 +157,7 @@ return [
'audit_event_filter_no_filter' => '필터 없음',
'audit_deleted_item' => '삭제한 항목',
'audit_deleted_item_name' => '이름: :name',
'audit_table_user' => '용자',
'audit_table_user' => '용자',
'audit_table_event' => '이벤트',
'audit_table_related' => '관련 항목 또는 세부 사항',
'audit_table_ip' => 'IP 주소',
@@ -167,7 +167,7 @@ return [
// Role Settings
'roles' => '역할',
'role_user_roles' => '용자 역할',
'role_user_roles' => '용자 역할',
'roles_index_desc' => '역할은 사용자를 그룹화하고 구성원에게 시스템 권한을 제공하기 위해 사용됩니다. 사용자가 여러 역할의 구성원인 경우 부여된 권한이 중첩되며 모든 권한을 상속받게 됩니다.',
'roles_x_users_assigned' => ':count 명의 사용자가 할당됨|:count 명의 사용자가 할당됨',
'roles_x_permissions_provided' => ':count 개의 권한|:count 개의 권한',
@@ -186,7 +186,7 @@ return [
'role_mfa_enforced' => '다중 인증 필요',
'role_external_auth_id' => '외부 인증 계정',
'role_system' => '시스템 권한',
'role_manage_users' => '용자 관리하기',
'role_manage_users' => '용자 관리',
'role_manage_roles' => '권한 관리',
'role_manage_entity_permissions' => '문서별 권한 관리',
'role_manage_own_entity_permissions' => '직접 만든 문서별 권한 관리',
@@ -217,7 +217,7 @@ return [
'user_profile' => '사용자 프로필',
'users_add_new' => '사용자 만들기',
'users_search' => '사용자 검색',
'users_latest_activity' => '최근 활동',
'users_latest_activity' => '마지막 활동',
'users_details' => '사용자 정보',
'users_details_desc' => '메일 주소로 로그인합니다.',
'users_details_desc_no_email' => '사용자 이름을 바꿉니다.',

View File

@@ -30,8 +30,8 @@ return [
'create' => 'Opprett',
'update' => 'Oppdater',
'edit' => 'Rediger',
'archive' => 'Arkiver',
'unarchive' => 'Av-arkiver',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => 'Sortér',
'move' => 'Flytt',
'copy' => 'Kopier',

View File

@@ -63,10 +63,10 @@ return [
'import_delete_desc' => 'Dette vil slette den opplastede importen av ZIP-filen og kan ikke angres.',
'import_errors' => 'Import feil',
'import_errors_desc' => 'Feil oppstod under importforsøket:',
'breadcrumb_siblings_for_page' => 'Naviger relaterte sider',
'breadcrumb_siblings_for_chapter' => 'Naviger relaterte kapitler',
'breadcrumb_siblings_for_book' => 'Naviger relaterte bøker',
'breadcrumb_siblings_for_bookshelf' => 'Naviger relaterte hyller',
'breadcrumb_siblings_for_page' => 'Navigate siblings for page',
'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',
'breadcrumb_siblings_for_book' => 'Navigate siblings for book',
'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',
// Permissions and restrictions
'permissions' => 'Tilganger',
@@ -252,7 +252,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Urørt innhold)',
'pages_edit_switch_to_wysiwyg' => 'Bytt til WYSIWYG tekstredigering',
'pages_edit_switch_to_new_wysiwyg' => 'Bytt til ny WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(under Beta-testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_set_changelog' => 'Angi endringslogg',
'pages_edit_enter_changelog_desc' => 'Gi en kort beskrivelse av endringene dine',
'pages_edit_enter_changelog' => 'Se endringslogg',
@@ -272,7 +272,7 @@ return [
'pages_md_insert_drawing' => 'Sett inn tegning',
'pages_md_show_preview' => 'Forhåndsvisning',
'pages_md_sync_scroll' => 'Synkroniser forhåndsvisningsrulle',
'pages_md_plain_editor' => 'Redigeringsverktøy for klartekst',
'pages_md_plain_editor' => 'Plaintext editor',
'pages_drawing_unsaved' => 'Ulagret tegning funnet',
'pages_drawing_unsaved_confirm' => 'Ulagret tegningsdata ble funnet fra en tidligere mislykket lagring. Vil du gjenopprette og fortsette å redigere denne ulagrede tegningen?',
'pages_not_in_chapter' => 'Siden tilhører ingen kapittel',
@@ -397,11 +397,11 @@ return [
'comment' => 'Kommentar',
'comments' => 'Kommentarer',
'comment_add' => 'Skriv kommentar',
'comment_none' => 'Ingen kommentarer å vise',
'comment_none' => 'No comments to display',
'comment_placeholder' => 'Skriv en kommentar her',
'comment_thread_count' => ':count Kommentar Tråd|:count Kommentar Tråder',
'comment_archived_count' => ':count Arkivert',
'comment_archived_threads' => 'Arkiverte tråder',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_save' => 'Publiser kommentar',
'comment_new' => 'Ny kommentar',
'comment_created' => 'kommenterte :createDiff',
@@ -410,14 +410,14 @@ return [
'comment_deleted_success' => 'Kommentar fjernet',
'comment_created_success' => 'Kommentar skrevet',
'comment_updated_success' => 'Kommentar endret',
'comment_archive_success' => 'Kommentar arkivert',
'comment_unarchive_success' => 'Kommentar uarkivert',
'comment_view' => 'Vis kommentar',
'comment_jump_to_thread' => 'Gå til tråd',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => 'Er du sikker på at du vil fjerne kommentaren?',
'comment_in_reply_to' => 'Som svar til :commentId',
'comment_reference' => 'Referanse',
'comment_reference_outdated' => '(Utdatert)',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_editor_explain' => 'Her er kommentarene som er på denne siden. Kommentarer kan legges til og administreres når du ser på den lagrede siden.',
// Revision

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'Kunne ikke lese ZIP-filen.',
'import_zip_cant_decode_data' => 'Kunne ikke finne og dekode ZIP data.json innhold.',
'import_zip_no_data' => 'ZIP-fildata har ingen forventet bok, kapittel eller sideinnhold.',
'import_zip_data_too_large' => 'ZIP data.json innholdet overskrider maksimal filstørrelse for opplasting.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'Import av ZIP feilet i å validere med feil:',
'import_zip_failed_notification' => 'Kunne ikke importere ZIP-fil.',
'import_perms_books' => 'Du mangler nødvendige tillatelser for å lage bøker.',

View File

@@ -11,8 +11,8 @@ return [
'updated_page_subject' => 'Oppdatert side: :pageName',
'updated_page_intro' => 'En side er oppdatert i :appName:',
'updated_page_debounce' => 'For å forhindre mange varslinger, vil du ikke få nye varslinger for endringer på denne siden fra samme forfatter.',
'comment_mention_subject' => 'Du har blitt nevnt i en kommentar på siden: :pageName',
'comment_mention_intro' => 'Du har blitt nevnt i en kommentar på :appName:',
'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',
'comment_mention_intro' => 'You were mentioned in a comment on :appName:',
'detail_page_name' => 'Sidenavn:',
'detail_page_path' => 'Side bane:',

View File

@@ -23,7 +23,7 @@ return [
'notifications_desc' => 'Kontroller e-postvarslene du mottar når en bestemt aktivitet utføres i systemet.',
'notifications_opt_own_page_changes' => 'Varsle ved endringer til sider jeg eier',
'notifications_opt_own_page_comments' => 'Varsle om kommentarer på sider jeg eier',
'notifications_opt_comment_mentions' => 'Varsle når jeg blir nevnt i en kommentar',
'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment',
'notifications_opt_comment_replies' => 'Varsle ved svar på mine kommentarer',
'notifications_save' => 'Lagre innstillinger',
'notifications_update_success' => 'Varslingsinnstillingene er oppdatert!',

View File

@@ -75,8 +75,8 @@ return [
'reg_confirm_restrict_domain_placeholder' => 'Ingen begrensninger er satt',
// Sorting Settings
'sorting' => 'Lister & Sortering',
'sorting_book_default' => 'Standard regel for boksortering',
'sorting' => 'Lists & Sorting',
'sorting_book_default' => 'Default Book Sort Rule',
'sorting_book_default_desc' => 'Velg standard sorteringsregelen som skal brukes for nye bøker. Dette vil ikke påvirke eksisterende bøker, og kan overstyres per bok.',
'sorting_rules' => 'Sorteringsregler',
'sorting_rules_desc' => 'Dette er forhåndsdefinerte sorteringsoperasjoner som kan brukes på innhold i systemet.',
@@ -91,20 +91,20 @@ return [
'sort_rule_details_desc' => 'Angi et navn for denne sorteringsregelen, som vil vises i lister når brukerne velger en sorteringsmetode.',
'sort_rule_operations' => 'Sorteringsoperasjoner',
'sort_rule_operations_desc' => 'Konfigurer sorteringshandlinger ved å flytte dem fra listen over tilgjengelige operasjoner. Ved bruk vil operasjonene bli brukt i rekkefølge, fra topp til bunn. Eventuelle endringer gjort her vil bli brukt for alle tildelte bøker når du lagrer.',
'sort_rule_available_operations' => 'Tilgjengelige operasjoner',
'sort_rule_available_operations_empty' => 'Ingen gjenværende operasjoner',
'sort_rule_configured_operations' => 'Konfigurerte operasjoner',
'sort_rule_available_operations' => 'Available Operations',
'sort_rule_available_operations_empty' => 'No operations remaining',
'sort_rule_configured_operations' => 'Configured Operations',
'sort_rule_configured_operations_empty' => 'Dra/legg til operasjoner fra listen "Tilgjengelige operasjoner"',
'sort_rule_op_asc' => '(Stigende)',
'sort_rule_op_desc' => '(Synkende)',
'sort_rule_op_asc' => '(Asc)',
'sort_rule_op_desc' => '(Desc)',
'sort_rule_op_name' => 'Navn - Alfabetisk',
'sort_rule_op_name_numeric' => 'Navn - Numerisk',
'sort_rule_op_created_date' => 'Dato opprettet',
'sort_rule_op_updated_date' => 'Dato oppdatert',
'sort_rule_op_name_numeric' => 'Name - Numeric',
'sort_rule_op_created_date' => 'Created Date',
'sort_rule_op_updated_date' => 'Updated Date',
'sort_rule_op_chapters_first' => 'Kapitler først',
'sort_rule_op_chapters_last' => 'Kapitler sist',
'sorting_page_limits' => 'Visningsgrenser for hver side',
'sorting_page_limits_desc' => 'Angi hvor mange elementer som skal vises på hver side i ulike lister i systemet. Et lavere antall vil vanligvis gi bedre ytelse, mens et høyere antall reduserer behovet for å bla gjennom mange sider. Det er anbefalt å bruke en multiplikasjon av 3 som gir partall (18, 24, 30 osv.).',
'sorting_page_limits' => 'Per-Page Display Limits',
'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.',
// Maintenance settings
'maint' => 'Vedlikehold',
@@ -197,13 +197,13 @@ return [
'role_import_content' => 'Import innhold',
'role_editor_change' => 'Endre sideredigering',
'role_notifications' => 'Motta og administrere varslinger',
'role_permission_note_users_and_roles' => 'Disse tillatelsene vil teknisk sett også gi mulighet til å se & søke etter brukere & roller i systemet.',
'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',
'role_asset' => 'Eiendomstillatelser',
'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.',
'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.',
'role_asset_admins' => 'Administratorer får automatisk tilgang til alt innhold, men disse alternativene kan vise eller skjule UI-alternativer.',
'role_asset_image_view_note' => 'Dette gjelder synlighet innenfor bilde-administrasjonen. Faktisk tilgang på opplastede bildefiler vil være avhengig av valget for systemlagring av bildet.',
'role_asset_users_note' => 'Disse tillatelsene vil teknisk sett også gi mulighet til å se & søke etter brukere i systemet.',
'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',
'role_all' => 'Alle',
'role_own' => 'Egne',
'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'kunne ikke lastes opp, tjeneren støtter ikke filer av denne størrelsen.',
'zip_file' => 'Attributtet :attribute må henvises til en fil i ZIP.',
'zip_file_size' => 'Filen :attribute må ikke overstige :size MB.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'Attributtet :attribute må referere en fil av typen :validTypes, som ble funnet :foundType.',
'zip_model_expected' => 'Data objekt forventet, men ":type" funnet.',
'zip_unique' => 'Attributtet :attribute må være unikt for objekttypen i ZIP.',

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'Kon het Zip-bestand niet lezen.',
'import_zip_cant_decode_data' => 'Kon de data.json Zip-inhoud niet vinden of decoderen.',
'import_zip_no_data' => 'Zip-bestand bevat niet de verwachte boek, hoofdstuk of pagina-inhoud.',
'import_zip_data_too_large' => 'De inhoud van data.json in de ZIP overschrijdt de ingestelde maximum upload grootte.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'De validatie van het Zip-bestand is mislukt met de volgende fouten:',
'import_zip_failed_notification' => 'Importeren van het Zip-bestand is mislukt.',
'import_perms_books' => 'Je mist de vereiste machtigingen om boeken te maken.',

View File

@@ -11,8 +11,8 @@ return [
'updated_page_subject' => 'Aangepaste pagina: :pageName',
'updated_page_intro' => 'Een pagina werd aangepast in :appName:',
'updated_page_debounce' => 'Om een stortvloed aan meldingen te voorkomen, zul je een tijdje geen meldingen ontvangen voor verdere bewerkingen van deze pagina door dezelfde redacteur.',
'comment_mention_subject' => 'Je bent vermeld in een opmerking op pagina: :pageName',
'comment_mention_intro' => 'Je bent vermeld in een opmerking in :appName:',
'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',
'comment_mention_intro' => 'You were mentioned in a comment on :appName:',
'detail_page_name' => 'Pagina Naam:',
'detail_page_path' => 'Paginapad:',

View File

@@ -23,7 +23,7 @@ return [
'notifications_desc' => 'Bepaal welke e-mailmeldingen je ontvangt wanneer bepaalde activiteiten in het systeem worden uitgevoerd.',
'notifications_opt_own_page_changes' => 'Geef melding bij wijzigingen aan pagina\'s waarvan ik de eigenaar ben',
'notifications_opt_own_page_comments' => 'Geef melding van opmerkingen op pagina\'s waarvan ik de eigenaar ben',
'notifications_opt_comment_mentions' => 'Geef een melding wanneer ik word vermeld in een opmerking',
'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment',
'notifications_opt_comment_replies' => 'Geef melding van reacties op mijn opmerkingen',
'notifications_save' => 'Voorkeuren opslaan',
'notifications_update_success' => 'Voorkeuren voor meldingen zijn bijgewerkt!',

View File

@@ -197,13 +197,13 @@ return [
'role_import_content' => 'Importeer inhoud',
'role_editor_change' => 'Wijzig pagina bewerker',
'role_notifications' => 'Meldingen ontvangen & beheren',
'role_permission_note_users_and_roles' => 'Deze machtigingen geven technisch gezien toegang tot het weergeven van gebruikers & rollen binnen het systeem.',
'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',
'role_asset' => 'Asset Machtigingen',
'roles_system_warning' => 'Wees ervan bewust dat toegang tot een van de bovengenoemde drie machtigingen een gebruiker in staat kan stellen zijn eigen machtigingen of de machtigingen van anderen in het systeem kan wijzigen. Wijs alleen rollen toe met deze machtigingen aan vertrouwde gebruikers.',
'role_asset_desc' => 'Deze machtigingen bepalen de standaard toegang tot de assets binnen het systeem. Machtigingen op boeken, hoofdstukken en pagina\'s overschrijven deze instelling.',
'role_asset_admins' => 'Beheerders krijgen automatisch toegang tot alle inhoud, maar deze opties kunnen gebruikersinterface opties tonen of verbergen.',
'role_asset_image_view_note' => 'Dit heeft betrekking op de zichtbaarheid binnen de afbeeldingsbeheerder. De werkelijke toegang tot geüploade afbeeldingsbestanden hangt af van de gekozen opslagmethode.',
'role_asset_users_note' => 'Deze machtigingen geven technisch gezien toegang tot het weergeven van gebruikers binnen het systeem.',
'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',
'role_all' => 'Alles',
'role_own' => 'Eigen',
'role_controlled_by_asset' => 'Gecontroleerd door de asset waar deze is geüpload',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'Het bestand kon niet worden geüpload. De server accepteert mogelijk geen bestanden van deze grootte.',
'zip_file' => 'Het \':attribute\' veld moet verwijzen naar een bestand in de ZIP.',
'zip_file_size' => 'Het bestand :attribute mag niet groter zijn dan :size MB.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'Het \':attribute\' veld moet verwijzen naar een bestand met het type :validTypes, vond :foundType.',
'zip_model_expected' => 'Dataobject verwacht maar vond ":type".',
'zip_unique' => ':attribute moet uniek zijn voor het objecttype binnen de ZIP.',

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => 'Не вдалося прочитати ZIP-файл.',
'import_zip_cant_decode_data' => 'Не вдалося знайти і розшифрувати контент ZIP data.json.',
'import_zip_no_data' => 'ZIP-файл не містить очікуваної книги, глави або вмісту сторінки.',
'import_zip_data_too_large' => 'Вміст ZIP data.json перевищує налаштований максимальний розмір додатка.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'Не вдалося виконати перевірку ZIP-адреси із помилками:',
'import_zip_failed_notification' => 'Не вдалося імпортувати ZIP-файл.',
'import_perms_books' => 'У Вас не вистачає необхідних прав для створення книг.',

View File

@@ -11,8 +11,8 @@ return [
'updated_page_subject' => 'Оновлено сторінку: :pageName',
'updated_page_intro' => 'Оновлено сторінку у :appName:',
'updated_page_debounce' => 'Для запобігання кількості сповіщень, деякий час ви не будете відправлені повідомлення для подальших змін на цій сторінці тим самим редактором.',
'comment_mention_subject' => 'Вас згадали в коментарях на сторінці: :pageName',
'comment_mention_intro' => 'Вас згадали в коментарі до :appName:',
'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',
'comment_mention_intro' => 'You were mentioned in a comment on :appName:',
'detail_page_name' => 'Назва сторінки:',
'detail_page_path' => 'Шлях до сторінки:',

View File

@@ -23,7 +23,7 @@ return [
'notifications_desc' => 'Контролюйте сповіщення по електронній пошті, які ви отримуєте, коли виконується певна активність у системі.',
'notifications_opt_own_page_changes' => 'Повідомляти при змінах сторінок якими я володію',
'notifications_opt_own_page_comments' => 'Повідомляти при коментарях на моїх сторінках',
'notifications_opt_comment_mentions' => 'Сповіщати, якщо мене згадали у коментарі',
'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment',
'notifications_opt_comment_replies' => 'Повідомляти про відповіді на мої коментарі',
'notifications_save' => 'Зберегти налаштування',
'notifications_update_success' => 'Налаштування сповіщень було оновлено!',

View File

@@ -75,8 +75,8 @@ return [
'reg_confirm_restrict_domain_placeholder' => 'Не встановлено обмежень',
// Sorting Settings
'sorting' => 'Списки і сортування',
'sorting_book_default' => 'Типовий порядок сортування книги',
'sorting' => 'Lists & Sorting',
'sorting_book_default' => 'Default Book Sort Rule',
'sorting_book_default_desc' => 'Виберіть правило сортування за замовчуванням для застосування нових книг. Це не вплине на існуючі книги, і може бути перевизначено для кожної книги.',
'sorting_rules' => 'Сортувати правила',
'sorting_rules_desc' => 'Це попередньо визначені операції сортування, які можуть бути застосовані до вмісту в системі.',
@@ -103,8 +103,8 @@ return [
'sort_rule_op_updated_date' => 'Дата оновлення',
'sort_rule_op_chapters_first' => 'Спочатку розділи',
'sort_rule_op_chapters_last' => 'Розділи останні',
'sorting_page_limits' => 'Обмеження відображення сторінок',
'sorting_page_limits_desc' => 'Кількість елементів для відображення в різних списках в системі. Зазвичай менша кількість буде більш продуктивною, в той час як більша кількість уникає необхідність натискання на кілька сторінок. Рекомендується використовувати парне кратне 3 (18, 24, 30 тощо).',
'sorting_page_limits' => 'Per-Page Display Limits',
'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.',
// Maintenance settings
'maint' => 'Обслуговування',
@@ -197,13 +197,13 @@ return [
'role_import_content' => 'Імпортувати вміст',
'role_editor_change' => 'Змінити редактор сторінок',
'role_notifications' => 'Отримувати та керувати повідомленнями',
'role_permission_note_users_and_roles' => 'Ці дозволи технічно також забезпечать видимість і пошук ролей у системі.',
'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',
'role_asset' => 'Дозволи',
'roles_system_warning' => 'Майте на увазі, що доступ до будь-якого з вищезазначених трьох дозволів може дозволити користувачеві змінювати власні привілеї або привілеї інших в системі. Ролі з цими дозволами призначайте лише довіреним користувачам.',
'role_asset_desc' => 'Ці дозволи контролюють стандартні доступи всередині системи. Права на книги, розділи та сторінки перевизначать ці дозволи.',
'role_asset_admins' => 'Адміністратори автоматично отримують доступ до всього вмісту, але ці параметри можуть відображати або приховувати параметри інтерфейсу користувача.',
'role_asset_image_view_note' => 'Це стосується видимості в менеджері зображень. Фактичний доступ завантажуваних зображень буде залежний від опції зберігання системних зображень.',
'role_asset_users_note' => 'Ці дозволи технічно також забезпечать видимість і пошук користувачів і ролей у системі.',
'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',
'role_all' => 'Все',
'role_own' => 'Власне',
'role_controlled_by_asset' => 'Контролюється за об\'єктом, до якого вони завантажуються',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => 'Не вдалося завантажити файл. Сервер може не приймати файли такого розміру.',
'zip_file' => 'Поле :attribute повинне вказувати файл в ZIP.',
'zip_file_size' => 'Файл :attribute не повинен перевищувати :size МБ.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'Поле :attribute повинне посилатись на файл типу :validtypes, знайдений :foundType.',
'zip_model_expected' => 'Очікувався об’єкт даних, але знайдено ":type".',
'zip_unique' => 'Поле :attribute має бути унікальним для типу об\'єкта в ZIP.',

View File

@@ -91,7 +91,7 @@ return [
'mfa_option_totp_title' => 'Mobil ilova',
'mfa_option_totp_desc' => 'Kop faktorli autentifikatsiyadan foydalanish uchun sizga Google Authenticator, Authy yoki Microsoft Authenticator kabi OTPni qollab-quvvatlaydigan mobil ilova kerak boladi.',
'mfa_option_backup_codes_title' => 'Zaxira kodlari',
'mfa_option_backup_codes_desc' => 'Shaxsingizni tasdiqlash uchun tizimga kirishda kiritadigan bir martalik zaxira kodlari to\'plamini yaratadi. Bularni xavfsiz va ishonchli joyda saqlang.',
'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',
'mfa_gen_confirm_and_enable' => 'Tasdiqlash va yoqish',
'mfa_gen_backup_codes_title' => 'Zaxira kodlarini sozlash',
'mfa_gen_backup_codes_desc' => 'Quyidagi kodlar royxatini xavfsiz joyda saqlang. Tizimga kirishda siz kodlardan birini ikkinchi autentifikatsiya mexanizmi sifatida ishlatishingiz mumkin.',

View File

@@ -105,11 +105,11 @@ return [
'url' => ':attribute URL formatida emas.',
'uploaded' => 'Faylni yuklashda xatolik. Server bunday hajmdagi faylllarni yuklamasligi mumkin.',
'zip_file' => ':attribute ZIP ichidagi faylga havola qilishi kerak.',
'zip_file_size' => ':attribute fayli :size MB dan oshmasligi kerak.',
'zip_file_mime' => ':attribute :validTypes turidagi faylga havola qilishi kerak, lekin :foundType turida keldi.',
'zip_model_expected' => 'Ma\'lumotlar obyekti kutilmoqda, ammo ":type" topildi.',
'zip_unique' => ':attribute ZIP ichidagi obyekt turi uchun noyob bo\'lishi kerak.',
'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',
'zip_model_expected' => 'Data object expected but ":type" found.',
'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',
// Custom validation lines
'custom' => [

View File

@@ -28,22 +28,22 @@ return [
'chapter_move_notification' => '章节移动成功',
// Books
'book_create' => '书已创建',
'book_create_notification' => '成功创建书',
'book_create_from_chapter' => '将章节转换为书',
'book_create_from_chapter_notification' => '章节已成功转换为书',
'book_update' => '书已更新',
'book_update_notification' => '书更新成功',
'book_delete' => '书已删除',
'book_delete_notification' => '书删除成功',
'book_sort' => '书已排序',
'book_sort_notification' => '书重新排序成功',
'book_create' => '书已创建',
'book_create_notification' => '成功创建书',
'book_create_from_chapter' => '将章节转换为书',
'book_create_from_chapter_notification' => '章节已成功转换为书',
'book_update' => '书已更新',
'book_update_notification' => '书更新成功',
'book_delete' => '书已删除',
'book_delete_notification' => '书删除成功',
'book_sort' => '书已排序',
'book_sort_notification' => '书重新排序成功',
// Bookshelves
'bookshelf_create' => '书架已创建',
'bookshelf_create_notification' => '书架创建成功',
'bookshelf_create_from_book' => '将书转换为书架',
'bookshelf_create_from_book_notification' => '书已成功转换为书架',
'bookshelf_create_from_book' => '将书转换为书架',
'bookshelf_create_from_book_notification' => '书已成功转换为书架',
'bookshelf_update' => '书架已更新',
'bookshelf_update_notification' => '书架更新成功',
'bookshelf_delete' => '书架已删除',

View File

@@ -121,79 +121,79 @@ return [
'shelves_save' => '保存书架',
'shelves_books' => '书籍已在此书架里',
'shelves_add_books' => '将书籍加入此书架',
'shelves_drag_books' => '拖动下面的书将其添加到此书架',
'shelves_empty_contents' => '这个书架没有分配书',
'shelves_edit_and_assign' => '编辑书架以分配书',
'shelves_drag_books' => '拖动下面的书将其添加到此书架',
'shelves_empty_contents' => '这个书架没有分配书',
'shelves_edit_and_assign' => '编辑书架以分配书',
'shelves_edit_named' => '编辑书架 :name',
'shelves_edit' => '编辑书架',
'shelves_delete' => '删除书架',
'shelves_delete_named' => '删除书架 :name',
'shelves_delete_explain' => "此操作将删除书架 ”:name”。书架中的书不会被删除。",
'shelves_delete_explain' => "此操作将删除书架 ”:name”。书架中的书不会被删除。",
'shelves_delete_confirmation' => '您确定要删除此书架吗?',
'shelves_permissions' => '书架权限',
'shelves_permissions_updated' => '书架权限已更新',
'shelves_permissions_active' => '书架权限已启用',
'shelves_permissions_cascade_warning' => '书架上的权限不会自动应用到书架里的书上,这是因为书可以在多个书架上存在。使用下面的选项可以将权限复制到书架里的书上。',
'shelves_permissions_create' => '书架创建权限仅用于使用下面的操作将权限复制到子书。这个权限不是用来控制创建书籍的。',
'shelves_copy_permissions_to_books' => '将权限复制到书',
'shelves_permissions_cascade_warning' => '书架上的权限不会自动应用到书架里的书上,这是因为书可以在多个书架上存在。使用下面的选项可以将权限复制到书架里的书上。',
'shelves_permissions_create' => '书架创建权限仅用于使用下面的操作将权限复制到子书。这个权限不是用来控制创建书籍的。',
'shelves_copy_permissions_to_books' => '将权限复制到书',
'shelves_copy_permissions' => '复制权限',
'shelves_copy_permissions_explain' => '此操作会将此书架的当前权限设置应用于其中包含的所有书上。 启用前请确保已保存对此书架权限的任何更改。',
'shelves_copy_permission_success' => '书架权限已复制到 :count 本书上',
'shelves_copy_permissions_explain' => '此操作会将此书架的当前权限设置应用于其中包含的所有书上。 启用前请确保已保存对此书架权限的任何更改。',
'shelves_copy_permission_success' => '书架权限已复制到 :count 本书上',
// Books
'book' => '书',
'books' => '书',
'book' => '书',
'books' => '书',
'x_books' => ':count 本书',
'books_empty' => '不存在已创建的书',
'books_popular' => '热门书',
'books_popular' => '热门书',
'books_recent' => '最近的书',
'books_new' => '新书',
'books_new_action' => '新书',
'books_popular_empty' => '最受欢迎的书将出现在这里。',
'books_new_empty' => '最近创建的书将出现在这里。',
'books_create' => '创建书',
'books_delete' => '删除书',
'books_delete_named' => '删除书「:bookName」',
'books_delete_explain' => '此操作将删除书 “:bookName”。书中的所有的章节和页面都会被删除。',
'books_delete_confirmation' => '您确定要删除此书吗?',
'books_edit' => '编辑书',
'books_edit_named' => '编辑书「:bookName」',
'books_popular_empty' => '最受欢迎的书将出现在这里。',
'books_new_empty' => '最近创建的书将出现在这里。',
'books_create' => '创建书',
'books_delete' => '删除书',
'books_delete_named' => '删除书「:bookName」',
'books_delete_explain' => '此操作将删除书 “:bookName”。书中的所有的章节和页面都会被删除。',
'books_delete_confirmation' => '您确定要删除此书吗?',
'books_edit' => '编辑书',
'books_edit_named' => '编辑书「:bookName」',
'books_form_book_name' => '书名',
'books_save' => '保存书',
'books_permissions' => '书权限',
'books_permissions_updated' => '书权限已更新',
'books_save' => '保存书',
'books_permissions' => '书权限',
'books_permissions_updated' => '书权限已更新',
'books_empty_contents' => '本书目前没有页面或章节。',
'books_empty_create_page' => '创建页面',
'books_empty_sort_current_book' => '排序当前书',
'books_empty_sort_current_book' => '排序当前书',
'books_empty_add_chapter' => '添加章节',
'books_permissions_active' => '书权限已启用',
'books_permissions_active' => '书权限已启用',
'books_search_this' => '搜索这本书',
'books_navigation' => '书导航',
'books_sort' => '排序书内容',
'books_navigation' => '书导航',
'books_sort' => '排序书内容',
'books_sort_desc' => '在书籍内部移动章节与页面以重组内容;支持添加其他书籍,实现跨书籍便捷移动章节与页面;还可设置自动排序规则,在内容发生变更时自动对本书内容进行排序。',
'books_sort_auto_sort' => '自动排序选项',
'books_sort_auto_sort_active' => '自动排序已激活:::sortName',
'books_sort_named' => '排序书「:bookName」',
'books_sort_named' => '排序书「:bookName」',
'books_sort_name' => '按名称排序',
'books_sort_created' => '创建时间排序',
'books_sort_updated' => '按更新时间排序',
'books_sort_chapters_first' => '章节正序',
'books_sort_chapters_last' => '章节倒序',
'books_sort_show_other' => '显示其他书',
'books_sort_show_other' => '显示其他书',
'books_sort_save' => '保存新顺序',
'books_sort_show_other_desc' => '在此添加其他书进入排序界面,这样就可以轻松跨书重新排序。',
'books_sort_show_other_desc' => '在此添加其他书进入排序界面,这样就可以轻松跨书重新排序。',
'books_sort_move_up' => '上移',
'books_sort_move_down' => '下移',
'books_sort_move_prev_book' => '移动到上一书',
'books_sort_move_next_book' => '移动到下一书',
'books_sort_move_prev_book' => '移动到上一书',
'books_sort_move_next_book' => '移动到下一书',
'books_sort_move_prev_chapter' => '移动到上一章节',
'books_sort_move_next_chapter' => '移动到下一章节',
'books_sort_move_book_start' => '移动到书开头',
'books_sort_move_book_end' => '移动到书结尾',
'books_sort_move_book_start' => '移动到书开头',
'books_sort_move_book_end' => '移动到书结尾',
'books_sort_move_before_chapter' => '移动到章节前',
'books_sort_move_after_chapter' => '移至章节后',
'books_copy' => '复制书',
'books_copy_success' => '书已成功复制',
'books_copy' => '复制书',
'books_copy_success' => '书已成功复制',
// Chapters
'chapter' => '章节',
@@ -218,7 +218,7 @@ return [
'chapters_permissions_active' => '章节权限已启用',
'chapters_permissions_success' => '章节权限已更新',
'chapters_search_this' => '从本章节搜索',
'chapter_sort_book' => '排序书',
'chapter_sort_book' => '排序书',
// Pages
'page' => '页面',
@@ -332,7 +332,7 @@ return [
'toggle_sidebar' => '切换侧边栏',
'page_tags' => '页面标签',
'chapter_tags' => '章节标签',
'book_tags' => '书标签',
'book_tags' => '书标签',
'shelf_tags' => '书架标签',
'tag' => '标签',
'tags' => '标签',
@@ -345,13 +345,13 @@ return [
'tags_usages' => '标签总使用量',
'tags_assigned_pages' => '有这个标签的页面',
'tags_assigned_chapters' => '有这个标签的章节',
'tags_assigned_books' => '有这个标签的书',
'tags_assigned_books' => '有这个标签的书',
'tags_assigned_shelves' => '有这个标签的书架',
'tags_x_unique_values' => ':count 个不重复项目',
'tags_all_values' => '所有值',
'tags_view_tags' => '查看标签',
'tags_view_existing_tags' => '查看已有的标签',
'tags_list_empty_hint' => '您可以在页面编辑器的侧边栏添加标签,或者在编辑书、章节、书架时添加。',
'tags_list_empty_hint' => '您可以在页面编辑器的侧边栏添加标签,或者在编辑书、章节、书架时添加。',
'attachments' => '附件',
'attachments_explain' => '上传一些文件或附加一些链接显示在您的网页上。这些在页面的侧边栏中可见。',
'attachments_explain_instant_save' => '这里的更改将立即保存。',
@@ -390,7 +390,7 @@ return [
'profile_created_content' => '已创建内容',
'profile_not_created_pages' => ':userName尚未创建任何页面',
'profile_not_created_chapters' => ':userName尚未创建任何章节',
'profile_not_created_books' => ':userName尚未创建任何书',
'profile_not_created_books' => ':userName尚未创建任何书',
'profile_not_created_shelves' => ':userName 尚未创建任何书架',
// Comments
@@ -435,13 +435,13 @@ return [
// Conversions
'convert_to_shelf' => '转换为书架',
'convert_to_shelf_contents_desc' => '你可以将这本书转换为具有相同内容的新书架。本书中的章节将被转换为书。如果这本书包含有任何不在章节分类中的页面,那么将会有一本单独的书包含这些页面,这本书也将成为新书架的一部分。',
'convert_to_shelf_permissions_desc' => '在这本书上设置的任何权限都将复制到所有未强制执行权限的新书架和新子书上。请注意,书架上的权限不会像书那样继承到内容物上。',
'convert_book' => '转换书',
'convert_book_confirm' => '您确定要转换此书吗?',
'convert_to_shelf_contents_desc' => '你可以将这本书转换为具有相同内容的新书架。本书中的章节将被转换为书。如果这本书包含有任何不在章节分类中的页面,那么将会有一本单独的书包含这些页面,这本书也将成为新书架的一部分。',
'convert_to_shelf_permissions_desc' => '在这本书上设置的任何权限都将复制到所有未强制执行权限的新书架和新子书上。请注意,书架上的权限不会像书那样继承到内容物上。',
'convert_book' => '转换书',
'convert_book_confirm' => '您确定要转换此书吗?',
'convert_undo_warning' => '这可不能轻易撤消。',
'convert_to_book' => '转换为书',
'convert_to_book_desc' => '您可以将此章节转换为具有相同内容的新书。此章节中设置的任何权限都将复制到新书上,但从父书继承的任何权限都不会被复制,这可能会导致访问控制发生变化。',
'convert_to_book' => '转换为书',
'convert_to_book_desc' => '您可以将此章节转换为具有相同内容的新书。此章节中设置的任何权限都将复制到新书上,但从父书继承的任何权限都不会被复制,这可能会导致访问控制发生变化。',
'convert_chapter' => '转换章节',
'convert_chapter_confirm' => '您确定要转换此章节吗?',
@@ -469,8 +469,8 @@ return [
'watch_detail_new' => '已关注新页面',
'watch_detail_updates' => '已关注新页面和更新',
'watch_detail_comments' => '已关注新页面、更新和评论',
'watch_detail_parent_book' => '已关注—继承自父书',
'watch_detail_parent_book_ignore' => '已忽略—继承自父书',
'watch_detail_parent_book' => '已关注—继承自父书',
'watch_detail_parent_book_ignore' => '已忽略—继承自父书',
'watch_detail_parent_chapter' => '已关注—继承自父章节',
'watch_detail_parent_chapter_ignore' => '已忽略—继承自父章节',
];

View File

@@ -68,11 +68,11 @@ return [
// Entities
'entity_not_found' => '未找到项目',
'bookshelf_not_found' => '未找到书架',
'book_not_found' => '未找到书',
'book_not_found' => '未找到书',
'page_not_found' => '未找到页面',
'chapter_not_found' => '未找到章节',
'selected_book_not_found' => '选中的书未找到',
'selected_book_chapter_not_found' => '未找到所选的书或章节',
'selected_book_chapter_not_found' => '未找到所选的书或章节',
'guests_cannot_save_drafts' => '访客不能保存草稿',
// Users

View File

@@ -55,7 +55,7 @@ return [
'link_color' => '默认链接颜色',
'content_colors_desc' => '为页面组织层次结构中的所有元素设置颜色。为了便于阅读,建议选择与默认颜色亮度相似的颜色。',
'bookshelf_color' => '书架颜色',
'book_color' => '书颜色',
'book_color' => '书颜色',
'chapter_color' => '章节颜色',
'page_color' => '页面颜色',
'page_draft_color' => '页面草稿颜色',
@@ -188,8 +188,8 @@ return [
'role_system' => '系统权限',
'role_manage_users' => '管理用户',
'role_manage_roles' => '管理角色与角色权限',
'role_manage_entity_permissions' => '管理所有书、章节和页面的权限',
'role_manage_own_entity_permissions' => '管理自己的书、章节和页面的权限',
'role_manage_entity_permissions' => '管理所有书、章节和页面的权限',
'role_manage_own_entity_permissions' => '管理自己的书、章节和页面的权限',
'role_manage_page_templates' => '管理页面模板',
'role_access_api' => '访问系统 API',
'role_manage_settings' => '管理 App 设置',

View File

@@ -109,7 +109,7 @@ return [
'import_zip_cant_read' => '無法讀取 ZIP 檔案。',
'import_zip_cant_decode_data' => '無法尋找並解碼 ZIP data.json 內容。',
'import_zip_no_data' => 'ZIP 檔案資料沒有預期的書本、章節或頁面內容。',
'import_zip_data_too_large' => 'ZIP 檔案 data.json 的內容超過了設定的應用程式最大上傳大小。',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => '匯入 ZIP 驗證失敗,發生錯誤:',
'import_zip_failed_notification' => '匯入 ZIP 檔案失敗。',
'import_perms_books' => '您缺乏建立書本所需的權限。',

View File

@@ -11,8 +11,8 @@ return [
'updated_page_subject' => '頁面更新::pageName',
'updated_page_intro' => ':appName: 中的一個頁面已被更新',
'updated_page_debounce' => '為了防止出現大量通知,一段時間內您不會收到同一編輯者再次編輯本頁面的通知。',
'comment_mention_subject' => '您在以下頁面的留言中被提及::pageName',
'comment_mention_intro' => '您再在 :appName: 的留言中被提及',
'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName',
'comment_mention_intro' => 'You were mentioned in a comment on :appName:',
'detail_page_name' => '頁面名稱:',
'detail_page_path' => '頁面路徑:',

View File

@@ -23,7 +23,7 @@ return [
'notifications_desc' => '控制在系統有特定活動時,是否要接收電子郵件通知',
'notifications_opt_own_page_changes' => '當我的頁面有異動時發送通知',
'notifications_opt_own_page_comments' => '當我的頁面有評論時發送通知',
'notifications_opt_comment_mentions' => '當我在留言中被提及時通知我',
'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment',
'notifications_opt_comment_replies' => '當我的評論有新的回覆時發送通知',
'notifications_save' => '儲存偏好設定',
'notifications_update_success' => '通知設定已更新',

View File

@@ -75,8 +75,8 @@ return [
'reg_confirm_restrict_domain_placeholder' => '尚未設定限制',
// Sorting Settings
'sorting' => '清單與排序',
'sorting_book_default' => '預設書籍排序規則',
'sorting' => 'Lists & Sorting',
'sorting_book_default' => 'Default Book Sort Rule',
'sorting_book_default_desc' => '選取要套用至新書籍的預設排序規則。這不會影響現有書籍,並可按書籍覆寫。',
'sorting_rules' => '排序規則',
'sorting_rules_desc' => '這些是預先定義的排序作業,可套用於系統中的內容。',
@@ -103,8 +103,8 @@ return [
'sort_rule_op_updated_date' => '更新日期',
'sort_rule_op_chapters_first' => '第一章',
'sort_rule_op_chapters_last' => '最後一章',
'sorting_page_limits' => '每頁顯示限制',
'sorting_page_limits_desc' => '設定系統內各類清單每頁顯示的項目數量。通常較低的數量能提升效能表現,而較高的數量則可避免使用者需點擊翻閱多頁。建議採用 3 的整數倍數(如 18、24、30 等)。',
'sorting_page_limits' => 'Per-Page Display Limits',
'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.',
// Maintenance settings
'maint' => '維護',
@@ -198,13 +198,13 @@ return [
'role_import_content' => '匯入內容',
'role_editor_change' => '重設頁面編輯器',
'role_notifications' => '管理和接收通知',
'role_permission_note_users_and_roles' => '這些權限在技術上亦將提供系統內使用者與角色的能見度及搜尋功能。',
'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.',
'role_asset' => '資源權限',
'roles_system_warning' => '請注意,有上述三項權限中的任一項的使用者都可以更改自己或系統中其他人的權限。有這些權限的角色只應分配給受信任的使用者。',
'role_asset_desc' => '對系統內資源的預設權限將由這裡的權限控制。若有單獨設定在書本、章節和頁面上的權限,將會覆寫這裡的權限設定。',
'role_asset_admins' => '管理員會自動取得對所有內容的存取權,但這些選項可能會顯示或隱藏使用者介面的選項。',
'role_asset_image_view_note' => '這與圖像管理器中的可見性有關。已經上傳的圖片的實際訪問取決於系統圖像存儲選項。',
'role_asset_users_note' => '這些權限在技術上亦將提供系統內使用者的能見度及搜尋功能。',
'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.',
'role_all' => '全部',
'role_own' => '擁有',
'role_controlled_by_asset' => '依據隸屬的資源來決定',

View File

@@ -106,7 +106,7 @@ return [
'uploaded' => '無法上傳文檔案, 伺服器可能不接受此大小的檔案。',
'zip_file' => ':attribute 需要參照 ZIP 中的檔案。',
'zip_file_size' => '檔案 :attribute 不能超過 :size MB',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => ':attribute 需要參照類型為 :validTypes 的檔案,找到 :foundType。',
'zip_model_expected' => '預期為資料物件,但找到「:type」。',
'zip_unique' => '對於 ZIP 中的物件類型,:attribute 必須是唯一的。',

22
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
"@types/jest": "^30.0.0",
"codemirror": "^6.0.2",
"eventsource-client": "^1.1.4",
"idb-keyval": "^6.2.2",
"markdown-it": "^14.1.0",
"markdown-it-task-lists": "^2.1.1",
@@ -4797,6 +4798,27 @@
"node": ">=0.10.0"
}
},
"node_modules/eventsource-client": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/eventsource-client/-/eventsource-client-1.2.0.tgz",
"integrity": "sha512-kDI75RSzO3TwyG/K9w1ap8XwqSPcwi6jaMkNulfVeZmSeUM49U8kUzk1s+vKNt0tGrXgK47i+620Yasn1ccFiw==",
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",

View File

@@ -53,6 +53,7 @@
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
"@types/jest": "^30.0.0",
"codemirror": "^6.0.2",
"eventsource-client": "^1.1.4",
"idb-keyval": "^6.2.2",
"markdown-it": "^14.1.0",
"markdown-it-task-lists": "^2.1.1",

33
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

32
public/dist/code.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -56,11 +56,6 @@ Big thanks to these companies for supporting the project.
<img width="480" src="https://www.bookstackapp.com/images/sponsors/diagramsnet.png" alt="Diagrams.net">
</a></td>
</tr>
<tr>
<td align="center"><a href="https://www.onyx.app/?utm_source=bookstack" target="_blank">
<img width="420" src="https://www.bookstackapp.com/images/sponsors/onyx.png" alt="onyx.app">
</a></td>
</tr>
</tbody></table>
#### Bronze Sponsors

View File

@@ -45,6 +45,7 @@ export {PagePicker} from './page-picker';
export {PermissionsTable} from './permissions-table';
export {Pointer} from './pointer';
export {Popup} from './popup';
export {QueryManager} from './query-manager';
export {SettingAppColorScheme} from './setting-app-color-scheme';
export {SettingColorPicker} from './setting-color-picker';
export {SettingHomepageControl} from './setting-homepage-control';

View File

@@ -0,0 +1,77 @@
import {Component} from "./component";
export class QueryManager extends Component {
protected input!: HTMLTextAreaElement;
protected generatedLoading!: HTMLElement;
protected generatedDisplay!: HTMLElement;
protected contentLoading!: HTMLElement;
protected contentDisplay!: HTMLElement;
protected form!: HTMLFormElement;
protected fieldset!: HTMLFieldSetElement;
setup() {
this.input = this.$refs.input as HTMLTextAreaElement;
this.form = this.$refs.form as HTMLFormElement;
this.fieldset = this.$refs.fieldset as HTMLFieldSetElement;
this.generatedLoading = this.$refs.generatedLoading;
this.generatedDisplay = this.$refs.generatedDisplay;
this.contentLoading = this.$refs.contentLoading;
this.contentDisplay = this.$refs.contentDisplay;
this.setupListeners();
// Start lookup if a query is set
if (this.input.value.trim() !== '') {
this.runQuery();
}
}
protected setupListeners(): void {
// Handle form submission
this.form.addEventListener('submit', event => {
event.preventDefault();
this.runQuery();
});
// Allow Ctrl+Enter to run a query
this.input.addEventListener('keydown', event => {
if (event.key === 'Enter' && event.ctrlKey && this.input.value.trim() !== '') {
this.runQuery();
}
});
}
protected async runQuery(): Promise<void> {
this.contentLoading.hidden = false;
this.generatedLoading.hidden = false;
this.contentDisplay.innerHTML = '';
this.generatedDisplay.innerHTML = '';
this.fieldset.disabled = true;
const query = this.input.value.trim();
const url = new URL(window.location.href);
url.searchParams.set('ask', query);
window.history.pushState({}, '', url.toString());
const es = window.$http.eventSource('/query', 'POST', {query});
let messageCount = 0;
for await (const {data, event, id} of es) {
messageCount++;
if (messageCount === 1) {
// Entity results
this.contentDisplay.innerHTML = JSON.parse(data).view;
this.contentLoading.hidden = true;
} else if (messageCount === 2) {
// LLM Output
this.generatedDisplay.innerText = JSON.parse(data).result;
this.generatedLoading.hidden = true;
} else {
es.close();
break;
}
}
this.fieldset.disabled = false;
}
}

View File

@@ -1,3 +1,5 @@
import {createEventSource, EventSourceClient} from "eventsource-client";
type ResponseData = Record<any, any>|string;
type RequestOptions = {
@@ -59,7 +61,6 @@ export class HttpManager {
}
createXMLHttpRequest(method: string, url: string, events: Record<string, (e: Event) => void> = {}): XMLHttpRequest {
const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content');
const req = new XMLHttpRequest();
for (const [eventName, callback] of Object.entries(events)) {
@@ -68,7 +69,7 @@ export class HttpManager {
req.open(method, url);
req.withCredentials = true;
req.setRequestHeader('X-CSRF-TOKEN', csrfToken || '');
req.setRequestHeader('X-CSRF-TOKEN', this.getCSRFToken());
return req;
}
@@ -95,12 +96,11 @@ export class HttpManager {
requestUrl = urlObj.toString();
}
const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content') || '';
const requestOptions: RequestInit = {...options, credentials: 'same-origin'};
requestOptions.headers = {
...requestOptions.headers || {},
baseURL: window.baseUrl(''),
'X-CSRF-TOKEN': csrfToken,
'X-CSRF-TOKEN': this.getCSRFToken(),
};
const response = await fetch(requestUrl, requestOptions);
@@ -191,6 +191,27 @@ export class HttpManager {
return this.dataRequest('DELETE', url, data);
}
eventSource(url: string, method: string = 'GET', body: object = {}): EventSourceClient {
if (!url.startsWith('http')) {
url = window.baseUrl(url);
}
return createEventSource({
url,
method,
body: JSON.stringify(body),
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': this.getCSRFToken(),
}
});
}
protected getCSRFToken(): string {
return document.querySelector('meta[name=token]')?.getAttribute('content') || '';
}
/**
* Parse the response text for an error response to a user
* presentable string. Handles a range of errors responses including

View File

@@ -601,3 +601,29 @@ input.shortcut-input {
max-width: 120px;
height: auto;
}
.query-form {
display: flex;
flex-direction: row;
gap: vars.$m;
textarea {
font-size: 1.4rem;
height: 100px;
box-shadow: vars.$bs-card;
border-radius: 8px;
color: #444;
}
button {
align-self: start;
margin: 0;
font-size: 1.6rem;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
textarea:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}

View File

@@ -0,0 +1,52 @@
@extends('layouts.simple')
@section('body')
<div component="query-manager" class="container small pt-xxl">
<div class="card content-wrap auto-height">
<h1 class="list-heading">Start a Query</h1>
<form action="{{ url('/query') }}"
refs="query-manager@form"
title="Run Query"
method="post">
<fieldset class="query-form" refs="query-manager@fieldset">
<textarea name="query"
refs="query-manager@input"
class="input-fill-width"
rows="5"
placeholder="Enter a query"
autocomplete="off">{{ $query }}</textarea>
<button class="button icon">@icon('search')</button>
</fieldset>
</form>
</div>
<div class="card content-wrap auto-height pb-xl">
<h2 class="list-heading">Generated Response</h2>
<div refs="query-manager@generated-loading" hidden>
@include('common.loading-icon')
</div>
<p refs="query-manager@generated-display">
<span class="text-muted italic">
When you run a query, the relevant content found & shown below will be used to help generate a smart machine generated response.
</span>
</p>
</div>
<div class="card content-wrap auto-height pb-xl">
<h2 class="list-heading">Relevant Content</h2>
<div refs="query-manager@content-loading" hidden>
@include('common.loading-icon')
</div>
<div class="book-contents">
<div refs="query-manager@content-display" class="entity-list">
<p class="text-muted italic mx-m">
Start a query to find relevant matching content.
The items shown here reflect those used to help provide the above response.
</p>
</div>
</div>
</div>
</div>
@stop

View File

@@ -11,6 +11,7 @@ use BookStack\Exports\Controllers as ExportControllers;
use BookStack\Http\Middleware\VerifyCsrfToken;
use BookStack\Permissions\PermissionsController;
use BookStack\References\ReferenceController;
use BookStack\Search\Queries\QueryController;
use BookStack\Search\SearchController;
use BookStack\Settings as SettingControllers;
use BookStack\Sorting as SortingControllers;
@@ -196,6 +197,11 @@ Route::middleware('auth')->group(function () {
Route::get('/search/entity-selector-templates', [SearchController::class, 'templatesForSelector']);
Route::get('/search/suggest', [SearchController::class, 'searchSuggestions']);
// Queries
Route::get('/query', [QueryController::class, 'show']);
Route::get('/query/run', [QueryController::class, 'run']); // TODO - Development only, remove
Route::post('/query', [QueryController::class, 'run']);
// User Search
Route::get('/search/users/select', [UserControllers\UserSearchController::class, 'forSelect']);
Route::get('/search/users/mention', [UserControllers\UserSearchController::class, 'forMentions']);

View File

@@ -0,0 +1,47 @@
<?php
namespace Search;
use BookStack\Search\Queries\TextChunker;
use Tests\TestCase;
class TextChunkerTest extends TestCase
{
public function test_it_chunks_text()
{
$chunker = new TextChunker(3, []);
$chunks = $chunker->chunk('123456789');
$this->assertEquals(['123', '456', '789'], $chunks);
}
public function test_chunk_size_must_be_greater_than_zero()
{
$this->expectException(\InvalidArgumentException::class);
$chunker = new TextChunker(-5, []);
}
public function test_it_works_through_given_delimiters()
{
$chunker = new TextChunker(5, ['-', '.', '']);
$chunks = $chunker->chunk('12-3456.789abcdefg');
$this->assertEquals(['12', '3456', '789ab', 'cdefg'], $chunks);
}
public function test_it_attempts_to_pack_chunks()
{
$chunker = new TextChunker(8, [' ', '']);
$chunks = $chunker->chunk('123 456 789 abc def');
$this->assertEquals(['123 456', '789 abc', 'def'], $chunks);
}
public function test_it_attempts_to_pack_using_subchunks()
{
$chunker = new TextChunker(8, [' ', '-', '']);
$chunks = $chunker->chunk('123 456-789abc');
$this->assertEquals(['123 456', '789abc'], $chunks);
}
}

View File

@@ -1 +1 @@
v25.12.2
v26.01-dev