mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-09 18:40:06 +00:00
Updated JS translations to be inserted from back-end
Removes old awkward JS translations endpoint. New system still a little akward in code but not now in process. Also extracted out page editors into their own files. Closes #1258
This commit is contained in:
parent
6fa093d9d0
commit
15c39c1976
13 changed files with 138 additions and 102 deletions
app
resources
assets/js
views
routes
|
@ -91,35 +91,6 @@ class HomeController extends Controller
|
||||||
return view('common.home', $commonData);
|
return view('common.home', $commonData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a js representation of the current translations
|
|
||||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function getTranslations()
|
|
||||||
{
|
|
||||||
$locale = app()->getLocale();
|
|
||||||
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
|
|
||||||
|
|
||||||
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
|
|
||||||
$resp = cache($cacheKey);
|
|
||||||
} else {
|
|
||||||
$translations = [
|
|
||||||
// Get only translations which might be used in JS
|
|
||||||
'common' => trans('common'),
|
|
||||||
'components' => trans('components'),
|
|
||||||
'entities' => trans('entities'),
|
|
||||||
'errors' => trans('errors')
|
|
||||||
];
|
|
||||||
$resp = 'window.translations = ' . json_encode($translations);
|
|
||||||
cache()->put($cacheKey, $resp, 120);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response($resp, 200, [
|
|
||||||
'Content-Type' => 'application/javascript'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get custom head HTML, Used in ajax calls to show in editor.
|
* Get custom head HTML, Used in ajax calls to show in editor.
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
|
|
|
@ -39,6 +39,14 @@ class AppServiceProvider extends ServiceProvider
|
||||||
return "<?php echo icon($expression); ?>";
|
return "<?php echo icon($expression); ?>";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Blade::directive('exposeTranslations', function($expression) {
|
||||||
|
return "<?php \$__env->startPush('translations'); ?>" .
|
||||||
|
"<?php foreach({$expression} as \$key): ?>" .
|
||||||
|
'<meta name="translation" key="<?php echo e($key); ?>" value="<?php echo e(trans($key)); ?>">' . "\n" .
|
||||||
|
"<?php endforeach; ?>" .
|
||||||
|
'<?php $__env->stopPush(); ?>';
|
||||||
|
});
|
||||||
|
|
||||||
// Allow longer string lengths after upgrade to utf8mb4
|
// Allow longer string lengths after upgrade to utf8mb4
|
||||||
Schema::defaultStringLength(191);
|
Schema::defaultStringLength(191);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ window.$events = eventManager;
|
||||||
// Translation setup
|
// Translation setup
|
||||||
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
|
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
|
||||||
import Translations from "./services/translations"
|
import Translations from "./services/translations"
|
||||||
const translator = new Translations(window.translations);
|
const translator = new Translations();
|
||||||
window.trans = translator.get.bind(translator);
|
window.trans = translator.get.bind(translator);
|
||||||
window.trans_choice = translator.getPlural.bind(translator);
|
window.trans_choice = translator.getPlural.bind(translator);
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,20 @@ class Translator {
|
||||||
* @param translations
|
* @param translations
|
||||||
*/
|
*/
|
||||||
constructor(translations) {
|
constructor(translations) {
|
||||||
this.store = translations;
|
this.store = new Map();
|
||||||
|
this.parseTranslations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse translations out of the page and place into the store.
|
||||||
|
*/
|
||||||
|
parseTranslations() {
|
||||||
|
const translationMetaTags = document.querySelectorAll('meta[name="translation"]');
|
||||||
|
for (let tag of translationMetaTags) {
|
||||||
|
const key = tag.getAttribute('key');
|
||||||
|
const value = tag.getAttribute('value');
|
||||||
|
this.store.set(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,7 +33,7 @@ class Translator {
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
get(key, replacements) {
|
get(key, replacements) {
|
||||||
let text = this.getTransText(key);
|
const text = this.getTransText(key);
|
||||||
return this.performReplacements(text, replacements);
|
return this.performReplacements(text, replacements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,26 +46,26 @@ class Translator {
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getPlural(key, count, replacements) {
|
getPlural(key, count, replacements) {
|
||||||
let text = this.getTransText(key);
|
const text = this.getTransText(key);
|
||||||
let splitText = text.split('|');
|
const splitText = text.split('|');
|
||||||
|
const exactCountRegex = /^{([0-9]+)}/;
|
||||||
|
const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
|
||||||
let result = null;
|
let result = null;
|
||||||
let exactCountRegex = /^{([0-9]+)}/;
|
|
||||||
let rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
|
|
||||||
|
|
||||||
for (let i = 0, len = splitText.length; i < len; i++) {
|
for (const i = 0, len = splitText.length; i < len; i++) {
|
||||||
let t = splitText[i];
|
const t = splitText[i];
|
||||||
|
|
||||||
// Parse exact matches
|
// Parse exact matches
|
||||||
let exactMatches = t.match(exactCountRegex);
|
const exactMatches = t.match(exactCountRegex);
|
||||||
if (exactMatches !== null && Number(exactMatches[1]) === count) {
|
if (exactMatches !== null && Number(exactMatches[1]) === count) {
|
||||||
result = t.replace(exactCountRegex, '').trim();
|
result = t.replace(exactCountRegex, '').trim();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse range matches
|
// Parse range matches
|
||||||
let rangeMatches = t.match(rangeRegex);
|
const rangeMatches = t.match(rangeRegex);
|
||||||
if (rangeMatches !== null) {
|
if (rangeMatches !== null) {
|
||||||
let rangeStart = Number(rangeMatches[1]);
|
const rangeStart = Number(rangeMatches[1]);
|
||||||
if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {
|
if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {
|
||||||
result = t.replace(rangeRegex, '').trim();
|
result = t.replace(rangeRegex, '').trim();
|
||||||
break;
|
break;
|
||||||
|
@ -74,14 +87,10 @@ class Translator {
|
||||||
* @returns {String|Object}
|
* @returns {String|Object}
|
||||||
*/
|
*/
|
||||||
getTransText(key) {
|
getTransText(key) {
|
||||||
let splitKey = key.split('.');
|
const value = this.store.get(key);
|
||||||
let value = splitKey.reduce((a, b) => {
|
|
||||||
return a !== undefined ? a[b] : a;
|
|
||||||
}, this.store);
|
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
console.log(`Translation with key "${key}" does not exist`);
|
console.warn(`Translation with key "${key}" does not exist`);
|
||||||
value = key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
|
@ -95,10 +104,10 @@ class Translator {
|
||||||
*/
|
*/
|
||||||
performReplacements(string, replacements) {
|
performReplacements(string, replacements) {
|
||||||
if (!replacements) return string;
|
if (!replacements) return string;
|
||||||
let replaceMatches = string.match(/:([\S]+)/g);
|
const replaceMatches = string.match(/:([\S]+)/g);
|
||||||
if (replaceMatches === null) return string;
|
if (replaceMatches === null) return string;
|
||||||
replaceMatches.forEach(match => {
|
replaceMatches.forEach(match => {
|
||||||
let key = match.substring(1);
|
const key = match.substring(1);
|
||||||
if (typeof replacements[key] === 'undefined') return;
|
if (typeof replacements[key] === 'undefined') return;
|
||||||
string = string.replace(match, replacements[key]);
|
string = string.replace(match, replacements[key]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,14 +13,17 @@
|
||||||
<link rel="stylesheet" href="{{ versioned_asset('dist/styles.css') }}">
|
<link rel="stylesheet" href="{{ versioned_asset('dist/styles.css') }}">
|
||||||
<link rel="stylesheet" media="print" href="{{ versioned_asset('dist/print-styles.css') }}">
|
<link rel="stylesheet" media="print" href="{{ versioned_asset('dist/print-styles.css') }}">
|
||||||
|
|
||||||
<!-- Scripts -->
|
|
||||||
<script src="{{ baseUrl('/translations') }}"></script>
|
|
||||||
|
|
||||||
@yield('head')
|
@yield('head')
|
||||||
|
|
||||||
|
<!-- Custom Styles & Head Content -->
|
||||||
@include('partials.custom-styles')
|
@include('partials.custom-styles')
|
||||||
@include('partials.custom-head')
|
@include('partials.custom-head')
|
||||||
|
|
||||||
@stack('head')
|
@stack('head')
|
||||||
|
|
||||||
|
<!-- Translations for JS -->
|
||||||
|
@stack('translations')
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="@yield('body-class')">
|
<body class="@yield('body-class')">
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
<div page-comments page-id="{{ $page->id }}" class="comments-list">
|
<div page-comments page-id="{{ $page->id }}" class="comments-list">
|
||||||
|
|
||||||
|
@exposeTranslations([
|
||||||
|
'entities.comment_updated_success',
|
||||||
|
'entities.comment_deleted_success',
|
||||||
|
'entities.comment_created_success',
|
||||||
|
'entities.comment_count',
|
||||||
|
])
|
||||||
|
|
||||||
<div comment-count-bar class="grid half left-focus v-center no-row-gap">
|
<div comment-count-bar class="grid half left-focus v-center no-row-gap">
|
||||||
<h5 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h5>
|
<h5 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h5>
|
||||||
@if (count($page->comments) === 0)
|
@if (count($page->comments) === 0)
|
||||||
|
|
|
@ -1,4 +1,13 @@
|
||||||
<div id="image-manager" image-type="{{ $imageType }}" uploaded-to="{{ $uploaded_to ?? 0 }}">
|
<div id="image-manager" image-type="{{ $imageType }}" uploaded-to="{{ $uploaded_to ?? 0 }}">
|
||||||
|
|
||||||
|
@exposeTranslations([
|
||||||
|
'components.image_delete_success',
|
||||||
|
'components.image_upload_success',
|
||||||
|
'errors.server_upload_limit',
|
||||||
|
'components.image_upload_remove',
|
||||||
|
'components.file_upload_timeout',
|
||||||
|
])
|
||||||
|
|
||||||
<div overlay v-cloak @click="hide">
|
<div overlay v-cloak @click="hide">
|
||||||
<div class="popup-body" @click.stop="">
|
<div class="popup-body" @click.stop="">
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
|
|
||||||
<div class="flex-fill flex">
|
<div class="flex-fill flex">
|
||||||
<form action="{{ $page->getUrl() }}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
|
<form action="{{ $page->getUrl() }}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
|
||||||
|
{{ csrf_field() }}
|
||||||
|
|
||||||
@if(!isset($isDraft))
|
@if(!isset($isDraft))
|
||||||
<input type="hidden" name="_method" value="PUT">
|
<input type="hidden" name="_method" value="PUT">
|
||||||
@endif
|
@endif
|
||||||
|
|
|
@ -18,6 +18,17 @@
|
||||||
|
|
||||||
@if(userCan('attachment-create-all'))
|
@if(userCan('attachment-create-all'))
|
||||||
<div toolbox-tab-content="files" id="attachment-manager" page-id="{{ $page->id ?? 0 }}">
|
<div toolbox-tab-content="files" id="attachment-manager" page-id="{{ $page->id ?? 0 }}">
|
||||||
|
|
||||||
|
@exposeTranslations([
|
||||||
|
'entities.attachments_file_uploaded',
|
||||||
|
'entities.attachments_file_updated',
|
||||||
|
'entities.attachments_link_attached',
|
||||||
|
'entities.attachments_updated_success',
|
||||||
|
'errors.server_upload_limit',
|
||||||
|
'components.image_upload_remove',
|
||||||
|
'components.file_upload_timeout',
|
||||||
|
])
|
||||||
|
|
||||||
<h4>{{ trans('entities.attachments') }}</h4>
|
<h4>{{ trans('entities.attachments') }}</h4>
|
||||||
<div class="px-l files">
|
<div class="px-l files">
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
<div class="page-editor flex-fill flex" id="page-editor"
|
<div class="page-editor flex-fill flex" id="page-editor"
|
||||||
drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}"
|
drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}"
|
||||||
drawio-enabled="{{ config('services.drawio') ? 'true' : 'false' }}"
|
drawio-enabled="{{ config('services.drawio') ? 'true' : 'false' }}"
|
||||||
|
@ -8,7 +7,14 @@
|
||||||
page-new-draft="{{ $model->draft ?? 0 }}"
|
page-new-draft="{{ $model->draft ?? 0 }}"
|
||||||
page-update-draft="{{ $model->isDraft ?? 0 }}">
|
page-update-draft="{{ $model->isDraft ?? 0 }}">
|
||||||
|
|
||||||
{{ csrf_field() }}
|
@exposeTranslations([
|
||||||
|
'entities.pages_editing_draft',
|
||||||
|
'entities.pages_editing_page',
|
||||||
|
'errors.page_draft_autosave_fail',
|
||||||
|
'entities.pages_editing_page',
|
||||||
|
'entities.pages_draft_discarded',
|
||||||
|
'entities.pages_edit_set_changelog',
|
||||||
|
])
|
||||||
|
|
||||||
{{--Header Bar--}}
|
{{--Header Bar--}}
|
||||||
<div class="primary-background-light toolbar page-edit-toolbar">
|
<div class="primary-background-light toolbar page-edit-toolbar">
|
||||||
|
@ -65,57 +71,12 @@
|
||||||
|
|
||||||
{{--WYSIWYG Editor--}}
|
{{--WYSIWYG Editor--}}
|
||||||
@if(setting('app-editor') === 'wysiwyg')
|
@if(setting('app-editor') === 'wysiwyg')
|
||||||
<div wysiwyg-editor class="flex-fill flex">
|
@include('pages.wysiwyg-editor', ['model' => $model])
|
||||||
<textarea id="html-editor" name="html" rows="5" v-pre
|
|
||||||
@if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if($errors->has('html'))
|
|
||||||
<div class="text-neg text-small">{{ $errors->first('html') }}</div>
|
|
||||||
@endif
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{--Markdown Editor--}}
|
{{--Markdown Editor--}}
|
||||||
@if(setting('app-editor') === 'markdown')
|
@if(setting('app-editor') === 'markdown')
|
||||||
<div v-pre id="markdown-editor" markdown-editor class="flex-fill flex code-fill">
|
@include('pages.markdown-editor', ['model' => $model])
|
||||||
|
|
||||||
<div class="markdown-editor-wrap active">
|
|
||||||
<div class="editor-toolbar">
|
|
||||||
<span class="float left editor-toolbar-label">{{ trans('entities.pages_md_editor') }}</span>
|
|
||||||
<div class="float right buttons">
|
|
||||||
@if(config('services.drawio'))
|
|
||||||
<button class="text-button" type="button" data-action="insertDrawing">@icon('drawing'){{ trans('entities.pages_md_insert_drawing') }}</button>
|
|
||||||
| 
|
|
||||||
@endif
|
|
||||||
<button class="text-button" type="button" data-action="insertImage">@icon('image'){{ trans('entities.pages_md_insert_image') }}</button>
|
|
||||||
|
|
|
||||||
<button class="text-button" type="button" data-action="insertLink">@icon('link'){{ trans('entities.pages_md_insert_link') }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div markdown-input class="flex flex-fill">
|
|
||||||
<textarea id="markdown-editor-input" name="markdown" rows="5"
|
|
||||||
@if($errors->has('markdown')) class="text-neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="markdown-editor-wrap">
|
|
||||||
<div class="editor-toolbar">
|
|
||||||
<div class="editor-toolbar-label">{{ trans('entities.pages_md_preview') }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="markdown-display page-content">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="html"/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@if($errors->has('markdown'))
|
|
||||||
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
|
|
||||||
@endif
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
42
resources/views/pages/markdown-editor.blade.php
Normal file
42
resources/views/pages/markdown-editor.blade.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<div v-pre id="markdown-editor" markdown-editor class="flex-fill flex code-fill">
|
||||||
|
@exposeTranslations([
|
||||||
|
'errors.image_upload_error',
|
||||||
|
])
|
||||||
|
|
||||||
|
<div class="markdown-editor-wrap active">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<span class="float left editor-toolbar-label">{{ trans('entities.pages_md_editor') }}</span>
|
||||||
|
<div class="float right buttons">
|
||||||
|
@if(config('services.drawio'))
|
||||||
|
<button class="text-button" type="button" data-action="insertDrawing">@icon('drawing'){{ trans('entities.pages_md_insert_drawing') }}</button>
|
||||||
|
| 
|
||||||
|
@endif
|
||||||
|
<button class="text-button" type="button" data-action="insertImage">@icon('image'){{ trans('entities.pages_md_insert_image') }}</button>
|
||||||
|
|
|
||||||
|
<button class="text-button" type="button" data-action="insertLink">@icon('link'){{ trans('entities.pages_md_insert_link') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div markdown-input class="flex flex-fill">
|
||||||
|
<textarea id="markdown-editor-input" name="markdown" rows="5"
|
||||||
|
@if($errors->has('markdown')) class="text-neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="markdown-editor-wrap">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<div class="editor-toolbar-label">{{ trans('entities.pages_md_preview') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="markdown-display page-content">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="html"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@if($errors->has('markdown'))
|
||||||
|
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
|
||||||
|
@endif
|
13
resources/views/pages/wysiwyg-editor.blade.php
Normal file
13
resources/views/pages/wysiwyg-editor.blade.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<div wysiwyg-editor class="flex-fill flex">
|
||||||
|
|
||||||
|
@exposeTranslations([
|
||||||
|
'errors.image_upload_error',
|
||||||
|
])
|
||||||
|
|
||||||
|
<textarea id="html-editor" name="html" rows="5" v-pre
|
||||||
|
@if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($errors->has('html'))
|
||||||
|
<div class="text-neg text-small">{{ $errors->first('html') }}</div>
|
||||||
|
@endif
|
|
@ -1,6 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
Route::get('/translations', 'HomeController@getTranslations');
|
|
||||||
Route::get('/robots.txt', 'HomeController@getRobots');
|
Route::get('/robots.txt', 'HomeController@getRobots');
|
||||||
|
|
||||||
// Authenticated routes...
|
// Authenticated routes...
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue