mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-05 16:49:47 +03:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e0edb63c7 | ||
|
|
bb08f62327 | ||
|
|
8eef5a1ee7 | ||
|
|
88ccd9e5b9 | ||
|
|
2c3100e401 | ||
|
|
54f883e815 | ||
|
|
e611b3239e | ||
|
|
b9ecf55e1f | ||
|
|
2d5548240a |
7
.github/translators.txt
vendored
7
.github/translators.txt
vendored
@@ -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
6
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
46
app/Console/Commands/RegenerateVectorsCommand.php
Normal file
46
app/Console/Commands/RegenerateVectorsCommand.php
Normal 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
89
app/Search/Queries/EntityVectorGenerator.php
Normal file
89
app/Search/Queries/EntityVectorGenerator.php
Normal 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};
|
||||
}
|
||||
}
|
||||
26
app/Search/Queries/LlmQueryRunner.php
Normal file
26
app/Search/Queries/LlmQueryRunner.php
Normal 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);
|
||||
}
|
||||
}
|
||||
61
app/Search/Queries/QueryController.php
Normal file
61
app/Search/Queries/QueryController.php
Normal 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)];
|
||||
});
|
||||
}
|
||||
}
|
||||
26
app/Search/Queries/SearchVector.php
Normal file
26
app/Search/Queries/SearchVector.php
Normal 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');
|
||||
}
|
||||
}
|
||||
66
app/Search/Queries/Services/OpenAiVectorQueryService.php
Normal file
66
app/Search/Queries/Services/OpenAiVectorQueryService.php
Normal 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'] ?? '';
|
||||
}
|
||||
}
|
||||
21
app/Search/Queries/Services/VectorQueryService.php
Normal file
21
app/Search/Queries/Services/VectorQueryService.php
Normal 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;
|
||||
}
|
||||
30
app/Search/Queries/StoreEntityVectorsJob.php
Normal file
30
app/Search/Queries/StoreEntityVectorsJob.php
Normal 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);
|
||||
}
|
||||
}
|
||||
79
app/Search/Queries/TextChunker.php
Normal file
79
app/Search/Queries/TextChunker.php
Normal 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;
|
||||
}
|
||||
}
|
||||
38
app/Search/Queries/VectorQueryServiceProvider.php
Normal file
38
app/Search/Queries/VectorQueryServiceProvider.php
Normal 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());
|
||||
}
|
||||
}
|
||||
17
app/Search/Queries/VectorSearchResult.php
Normal file
17
app/Search/Queries/VectorSearchResult.php
Normal 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
|
||||
) {
|
||||
}
|
||||
}
|
||||
54
app/Search/Queries/VectorSearchRunner.php
Normal file
54
app/Search/Queries/VectorSearchRunner.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
485
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
8c5c595184876ce12928f309aa3c9b571987c5e44c69c57c9cb17487f62bd709
|
||||
22e02ee72d21ff719c1073abbec8302f8e2096ba6d072e133051064ed24b45b1
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' => 'Запази този лист с кодове на сигурно място. Когато достъпваш системата, ще можеш да използваш един от тези кодове като вторичен механизъм за удостоверяване.',
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -13,7 +13,7 @@ return [
|
||||
'cancel' => 'Отказ',
|
||||
'save' => 'Запис',
|
||||
'close' => 'Затваряне',
|
||||
'apply' => 'Приложи',
|
||||
'apply' => 'Apply',
|
||||
'undo' => 'Отмяна',
|
||||
'redo' => 'Повтаряне',
|
||||
'left' => 'Вляво',
|
||||
|
||||
@@ -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. Увери се на сървъра, че в пътя може да се записва.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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é à l’aide 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é à l’aide 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.",
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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 :',
|
||||
|
||||
@@ -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 !',
|
||||
|
||||
@@ -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 n’affectera 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 qu’un nombre plus élevé réduit le besoin de naviguer entre plusieurs pages. Il est recommandé d’utiliser 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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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:',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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' => 'ブックを作成するために必要な権限がありません。',
|
||||
|
||||
@@ -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 が一意である必要があります。',
|
||||
|
||||
@@ -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" 을 북마크에 추가하였습니다.',
|
||||
|
||||
@@ -17,7 +17,7 @@ return [
|
||||
'logout' => '로그아웃',
|
||||
|
||||
'name' => '이름',
|
||||
'username' => '이용자명',
|
||||
'username' => '사용자 이름',
|
||||
'email' => '전자우편 주소',
|
||||
'password' => '비밀번호',
|
||||
'password_confirm' => '비밀번호 확인',
|
||||
|
||||
@@ -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' => '상태',
|
||||
|
||||
@@ -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' => '더 보기',
|
||||
|
||||
@@ -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' => '항목을 복사할 때 다음을 고려하세요.',
|
||||
|
||||
@@ -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' => '사용자 이름을 바꿉니다.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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:',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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:',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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' => 'У Вас не вистачає необхідних прав для створення книг.',
|
||||
|
||||
@@ -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' => 'Шлях до сторінки:',
|
||||
|
||||
@@ -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' => 'Налаштування сповіщень було оновлено!',
|
||||
|
||||
@@ -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' => 'Контролюється за об\'єктом, до якого вони завантажуються',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -91,7 +91,7 @@ return [
|
||||
'mfa_option_totp_title' => 'Mobil ilova',
|
||||
'mfa_option_totp_desc' => 'Ko‘p faktorli autentifikatsiyadan foydalanish uchun sizga Google Authenticator, Authy yoki Microsoft Authenticator kabi OTPni qo‘llab-quvvatlaydigan mobil ilova kerak bo‘ladi.',
|
||||
'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 ro‘yxatini xavfsiz joyda saqlang. Tizimga kirishda siz kodlardan birini ikkinchi autentifikatsiya mexanizmi sifatida ishlatishingiz mumkin.',
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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' => '书架已删除',
|
||||
|
||||
@@ -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' => '已忽略—继承自父章节',
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 设置',
|
||||
|
||||
@@ -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' => '您缺乏建立書本所需的權限。',
|
||||
|
||||
@@ -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' => '頁面路徑:',
|
||||
|
||||
@@ -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' => '通知設定已更新',
|
||||
|
||||
@@ -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' => '依據隸屬的資源來決定',
|
||||
|
||||
@@ -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
22
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
33
public/dist/app.js
vendored
File diff suppressed because one or more lines are too long
32
public/dist/code.js
vendored
32
public/dist/code.js
vendored
File diff suppressed because one or more lines are too long
1
public/dist/export-styles.css
vendored
1
public/dist/export-styles.css
vendored
File diff suppressed because one or more lines are too long
3
public/dist/legacy-modes.js
vendored
3
public/dist/legacy-modes.js
vendored
File diff suppressed because one or more lines are too long
28
public/dist/markdown.js
vendored
28
public/dist/markdown.js
vendored
File diff suppressed because one or more lines are too long
1
public/dist/styles.css
vendored
1
public/dist/styles.css
vendored
File diff suppressed because one or more lines are too long
32
public/dist/wysiwyg.js
vendored
32
public/dist/wysiwyg.js
vendored
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
77
resources/js/components/query-manager.ts
Normal file
77
resources/js/components/query-manager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
52
resources/views/search/query.blade.php
Normal file
52
resources/views/search/query.blade.php
Normal 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
|
||||
@@ -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']);
|
||||
|
||||
47
tests/Search/TextChunkerTest.php
Normal file
47
tests/Search/TextChunkerTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user