From ac9a65945f6c9eb917b4449ad7d83b4402b4cea7 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Sun, 17 Sep 2023 13:29:06 +0100 Subject: [PATCH 1/4] Locales: Performed cleanup and alignment of locale handling - Reduced app settings down to what's required. - Used new view-shared $locale object instead of using globals via config. - Aligned language used to default on "locale" instead of mixing locale/language. For #4501 --- app/Config/app.php | 9 +- app/Http/Middleware/Localization.php | 31 ++----- app/Translation/LocaleDefinition.php | 45 ++++++++++ ...{LanguageManager.php => LocaleManager.php} | 84 ++++++++++--------- app/Users/Models/User.php | 4 +- resources/views/layouts/base.blade.php | 4 +- resources/views/layouts/export.blade.php | 2 +- resources/views/layouts/plain.blade.php | 4 +- .../pages/parts/markdown-editor.blade.php | 2 +- .../pages/parts/wysiwyg-editor.blade.php | 4 +- resources/views/users/create.blade.php | 2 +- resources/views/users/edit.blade.php | 2 +- .../vendor/notifications/email.blade.php | 2 +- tests/LanguageTest.php | 1 + 14 files changed, 116 insertions(+), 80 deletions(-) create mode 100644 app/Translation/LocaleDefinition.php rename app/Translation/{LanguageManager.php => LocaleManager.php} (70%) diff --git a/app/Config/app.php b/app/Config/app.php index 3a843c512..dcd3ffc31 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -83,10 +83,10 @@ return [ 'timezone' => env('APP_TIMEZONE', 'UTC'), // Default locale to use + // A default variant is also stored since Laravel can overwrite + // app.locale when dynamically setting the locale in-app. 'locale' => env('APP_LANG', 'en'), - - // Locales available - 'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'de_informal', 'el', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ka', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ro', 'ru', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'], + 'default_locale' => env('APP_LANG', 'en'), // Application Fallback Locale 'fallback_locale' => 'en', @@ -94,9 +94,6 @@ return [ // Faker Locale 'faker_locale' => 'en_GB', - // Enable right-to-left text control. - 'rtl' => false, - // Auto-detect the locale for public users // For public users their locale can be guessed by headers sent by their // browser. This is usually set by users in their browser settings. diff --git a/app/Http/Middleware/Localization.php b/app/Http/Middleware/Localization.php index 47723e242..0be0b77eb 100644 --- a/app/Http/Middleware/Localization.php +++ b/app/Http/Middleware/Localization.php @@ -2,17 +2,14 @@ namespace BookStack\Http\Middleware; -use BookStack\Translation\LanguageManager; -use Carbon\Carbon; +use BookStack\Translation\LocaleManager; use Closure; class Localization { - protected LanguageManager $languageManager; - - public function __construct(LanguageManager $languageManager) - { - $this->languageManager = $languageManager; + public function __construct( + protected LocaleManager $localeManager + ) { } /** @@ -25,22 +22,12 @@ class Localization */ public function handle($request, Closure $next) { - // Get and record the default language in the config - $defaultLang = config('app.locale'); - config()->set('app.default_locale', $defaultLang); + // Share details of the user's locale for use in views + $userLocale = $this->localeManager->getForUser(user()); + view()->share('locale', $userLocale); - // Get the user's language and record that in the config for use in views - $userLang = $this->languageManager->getUserLanguage($request, $defaultLang); - config()->set('app.lang', str_replace('_', '-', $this->languageManager->getIsoName($userLang))); - - // Set text direction - if ($this->languageManager->isRTL($userLang)) { - config()->set('app.rtl', true); - } - - app()->setLocale($userLang); - Carbon::setLocale($userLang); - $this->languageManager->setPhpDateTimeLocale($userLang); + // Set locale for system components + $this->localeManager->setAppLocale($userLocale); return $next($request); } diff --git a/app/Translation/LocaleDefinition.php b/app/Translation/LocaleDefinition.php new file mode 100644 index 000000000..fe8640109 --- /dev/null +++ b/app/Translation/LocaleDefinition.php @@ -0,0 +1,45 @@ +<?php + +namespace BookStack\Translation; + +class LocaleDefinition +{ + public function __construct( + protected string $appName, + protected string $isoName, + protected bool $isRtl + ) { + } + + /** + * Provide the BookStack-specific locale name. + */ + public function appLocale(): string + { + return $this->appName; + } + + /** + * Provide the ISO-aligned locale name. + */ + public function isoLocale(): string + { + return $this->isoName; + } + + /** + * Returns a string suitable for the HTML "lang" attribute. + */ + public function htmlLang(): string + { + return str_replace('_', '-', $this->isoName); + } + + /** + * Returns a string suitable for the HTML "dir" attribute. + */ + public function htmlDirection(): string + { + return $this->isRtl ? 'rtl' : 'ltr'; + } +} diff --git a/app/Translation/LanguageManager.php b/app/Translation/LocaleManager.php similarity index 70% rename from app/Translation/LanguageManager.php rename to app/Translation/LocaleManager.php index f3432d038..171555293 100644 --- a/app/Translation/LanguageManager.php +++ b/app/Translation/LocaleManager.php @@ -3,17 +3,18 @@ namespace BookStack\Translation; use BookStack\Users\Models\User; +use Carbon\Carbon; use Illuminate\Http\Request; -class LanguageManager +class LocaleManager { /** - * Array of right-to-left language options. + * Array of right-to-left locale options. */ - protected array $rtlLanguages = ['ar', 'fa', 'he']; + protected array $rtlLocales = ['ar', 'fa', 'he']; /** - * Map of BookStack language names to best-estimate ISO and windows locale names. + * Map of BookStack locale names to best-estimate ISO and windows locale names. * Locales can often be found by running `locale -a` on a linux system. * Windows locales can be found at: * https://docs.microsoft.com/en-us/cpp/c-runtime-library/language-strings?view=msvc-170. @@ -29,8 +30,8 @@ class LanguageManager 'da' => ['iso' => 'da_DK', 'windows' => 'Danish'], 'de' => ['iso' => 'de_DE', 'windows' => 'German'], 'de_informal' => ['iso' => 'de_DE', 'windows' => 'German'], - 'en' => ['iso' => 'en_GB', 'windows' => 'English'], 'el' => ['iso' => 'el_GR', 'windows' => 'Greek'], + 'en' => ['iso' => 'en_GB', 'windows' => 'English'], 'es' => ['iso' => 'es_ES', 'windows' => 'Spanish'], 'es_AR' => ['iso' => 'es_AR', 'windows' => 'Spanish'], 'et' => ['iso' => 'et_EE', 'windows' => 'Estonian'], @@ -46,8 +47,8 @@ class LanguageManager 'ko' => ['iso' => 'ko_KR', 'windows' => 'Korean'], 'lt' => ['iso' => 'lt_LT', 'windows' => 'Lithuanian'], 'lv' => ['iso' => 'lv_LV', 'windows' => 'Latvian'], - 'nl' => ['iso' => 'nl_NL', 'windows' => 'Dutch'], 'nb' => ['iso' => 'nb_NO', 'windows' => 'Norwegian (Bokmal)'], + 'nl' => ['iso' => 'nl_NL', 'windows' => 'Dutch'], 'pl' => ['iso' => 'pl_PL', 'windows' => 'Polish'], 'pt' => ['iso' => 'pt_PT', 'windows' => 'Portuguese'], 'pt_BR' => ['iso' => 'pt_BR', 'windows' => 'Portuguese'], @@ -56,47 +57,40 @@ class LanguageManager 'sk' => ['iso' => 'sk_SK', 'windows' => 'Slovak'], 'sl' => ['iso' => 'sl_SI', 'windows' => 'Slovenian'], 'sv' => ['iso' => 'sv_SE', 'windows' => 'Swedish'], + 'tr' => ['iso' => 'tr_TR', 'windows' => 'Turkish'], 'uk' => ['iso' => 'uk_UA', 'windows' => 'Ukrainian'], 'uz' => ['iso' => 'uz_UZ', 'windows' => 'Uzbek'], 'vi' => ['iso' => 'vi_VN', 'windows' => 'Vietnamese'], 'zh_CN' => ['iso' => 'zh_CN', 'windows' => 'Chinese (Simplified)'], 'zh_TW' => ['iso' => 'zh_TW', 'windows' => 'Chinese (Traditional)'], - 'tr' => ['iso' => 'tr_TR', 'windows' => 'Turkish'], ]; /** - * Get the language specifically for the currently logged-in user if available. + * Get the BookStack locale string for the given user. */ - public function getUserLanguage(Request $request, string $default): string + protected function getLocaleForUser(User $user): string { - try { - $user = user(); - } catch (\Exception $exception) { - return $default; - } + $default = config('app.default_locale'); if ($user->isGuest() && config('app.auto_detect_locale')) { - return $this->autoDetectLocale($request, $default); + return $this->autoDetectLocale(request(), $default); } return setting()->getUser($user, 'language', $default); } /** - * Get the language for the given user. + * Get a locale definition for the current user. */ - public function getLanguageForUser(User $user): string + public function getForUser(User $user): LocaleDefinition { - $default = config('app.locale'); - return setting()->getUser($user, 'language', $default); - } + $localeString = $this->getLocaleForUser($user); - /** - * Check if the given BookStack language value is a right-to-left language. - */ - public function isRTL(string $language): bool - { - return in_array($language, $this->rtlLanguages); + return new LocaleDefinition( + $localeString, + $this->getIsoName($localeString), + in_array($localeString, $this->rtlLocales), + ); } /** @@ -105,7 +99,8 @@ class LanguageManager */ protected function autoDetectLocale(Request $request, string $default): string { - $availableLocales = config('app.locales'); + $availableLocales = array_keys($this->localeMap); + foreach ($request->getLanguages() as $lang) { if (in_array($lang, $availableLocales)) { return $lang; @@ -116,29 +111,40 @@ class LanguageManager } /** - * Get the ISO version of a BookStack language name. + * Get the ISO version of a BookStack locale. */ - public function getIsoName(string $language): string + protected function getIsoName(string $locale): string { - return $this->localeMap[$language]['iso'] ?? $language; + return $this->localeMap[$locale]['iso'] ?? $locale; + } + + /** + * Sets the active locale for system level components. + */ + public function setAppLocale(LocaleDefinition $locale): void + { + app()->setLocale($locale->appLocale()); + Carbon::setLocale($locale->isoLocale()); + $this->setPhpDateTimeLocale($locale); } /** * Set the system date locale for localized date formatting. * Will try both the standard locale name and the UTF8 variant. */ - public function setPhpDateTimeLocale(string $language): void + public function setPhpDateTimeLocale(LocaleDefinition $locale): void { - $isoLang = $this->localeMap[$language]['iso'] ?? ''; - $isoLangPrefix = explode('_', $isoLang)[0]; + $appLocale = $locale->appLocale(); + $isoLocale = $this->localeMap[$appLocale]['iso'] ?? ''; + $isoLocalePrefix = explode('_', $isoLocale)[0]; $locales = array_values(array_filter([ - $isoLang ? $isoLang . '.utf8' : false, - $isoLang ?: false, - $isoLang ? str_replace('_', '-', $isoLang) : false, - $isoLang ? $isoLangPrefix . '.UTF-8' : false, - $this->localeMap[$language]['windows'] ?? false, - $language, + $isoLocale ? $isoLocale . '.utf8' : false, + $isoLocale ?: false, + $isoLocale ? str_replace('_', '-', $isoLocale) : false, + $isoLocale ? $isoLocalePrefix . '.UTF-8' : false, + $this->localeMap[$appLocale]['windows'] ?? false, + $appLocale, ])); if (!empty($locales)) { diff --git a/app/Users/Models/User.php b/app/Users/Models/User.php index 232ea8832..78411e0d4 100644 --- a/app/Users/Models/User.php +++ b/app/Users/Models/User.php @@ -12,7 +12,7 @@ use BookStack\Api\ApiToken; use BookStack\App\Model; use BookStack\App\Sluggable; use BookStack\Entities\Tools\SlugGenerator; -use BookStack\Translation\LanguageManager; +use BookStack\Translation\LocaleManager; use BookStack\Uploads\Image; use Carbon\Carbon; use Exception; @@ -346,7 +346,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function getLanguage(): string { - return app()->make(LanguageManager::class)->getLanguageForUser($this); + return app()->make(LocaleManager::class)->getForUser($this)->appLocale(); } /** diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 8875788a6..0fd12b70f 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -1,6 +1,6 @@ <!DOCTYPE html> -<html lang="{{ config('app.lang') }}" - dir="{{ config('app.rtl') ? 'rtl' : 'ltr' }}" +<html lang="{{ $locale->htmlLang() }}" + dir="{{ $locale->htmlDirection() }}" class="{{ setting()->getForCurrentUser('dark-mode-enabled') ? 'dark-mode ' : '' }}"> <head> <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title> diff --git a/resources/views/layouts/export.blade.php b/resources/views/layouts/export.blade.php index e041d8dea..eb2397a75 100644 --- a/resources/views/layouts/export.blade.php +++ b/resources/views/layouts/export.blade.php @@ -1,5 +1,5 @@ <!doctype html> -<html lang="{{ config('app.lang') }}"> +<html lang="{{ $locale->htmlLang() }}"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>@yield('title')</title> diff --git a/resources/views/layouts/plain.blade.php b/resources/views/layouts/plain.blade.php index 043d8aa48..360c404c1 100644 --- a/resources/views/layouts/plain.blade.php +++ b/resources/views/layouts/plain.blade.php @@ -1,6 +1,6 @@ <!DOCTYPE html> -<html lang="{{ config('app.lang') }}" - dir="{{ config('app.rtl') ? 'rtl' : 'ltr' }}" +<html lang="{{ $locale->htmlLang() }}" + dir="{{ $locale->htmlDirection() }}" class="@yield('document-class')"> <head> <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title> diff --git a/resources/views/pages/parts/markdown-editor.blade.php b/resources/views/pages/parts/markdown-editor.blade.php index c488f0e11..18a418f10 100644 --- a/resources/views/pages/parts/markdown-editor.blade.php +++ b/resources/views/pages/parts/markdown-editor.blade.php @@ -1,6 +1,6 @@ <div id="markdown-editor" component="markdown-editor" option:markdown-editor:page-id="{{ $model->id ?? 0 }}" - option:markdown-editor:text-direction="{{ config('app.rtl') ? 'rtl' : 'ltr' }}" + option:markdown-editor:text-direction="{{ $locale->htmlDirection() }}" option:markdown-editor:image-upload-error-text="{{ trans('errors.image_upload_error') }}" option:markdown-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}" class="flex-fill flex code-fill"> diff --git a/resources/views/pages/parts/wysiwyg-editor.blade.php b/resources/views/pages/parts/wysiwyg-editor.blade.php index b7cd1bdaa..ca6b6da8a 100644 --- a/resources/views/pages/parts/wysiwyg-editor.blade.php +++ b/resources/views/pages/parts/wysiwyg-editor.blade.php @@ -3,9 +3,9 @@ @endpush <div component="wysiwyg-editor" - option:wysiwyg-editor:language="{{ config('app.lang') }}" + option:wysiwyg-editor:language="{{ $locale->htmlLang() }}" option:wysiwyg-editor:page-id="{{ $model->id ?? 0 }}" - option:wysiwyg-editor:text-direction="{{ config('app.rtl') ? 'rtl' : 'ltr' }}" + option:wysiwyg-editor:text-direction="{{ $locale->htmlDirection() }}" option:wysiwyg-editor:image-upload-error-text="{{ trans('errors.image_upload_error') }}" option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}" class="flex-fill flex"> diff --git a/resources/views/users/create.blade.php b/resources/views/users/create.blade.php index 540d7bd6a..0edae1d82 100644 --- a/resources/views/users/create.blade.php +++ b/resources/views/users/create.blade.php @@ -14,7 +14,7 @@ <div class="setting-list"> @include('users.parts.form') - @include('users.parts.language-option-row', ['value' => old('setting.language') ?? config('app.default_locale')]) + @include('users.parts.language-option-row', ['value' => old('setting.language') ?? config('app.locale')]) </div> <div class="form-group text-right"> diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 4e31e785d..23cf87684 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -33,7 +33,7 @@ </div> </div> - @include('users.parts.language-option-row', ['value' => setting()->getUser($user, 'language', config('app.default_locale'))]) + @include('users.parts.language-option-row', ['value' => $user->getLanguage())]) </div> <div class="text-right"> diff --git a/resources/views/vendor/notifications/email.blade.php b/resources/views/vendor/notifications/email.blade.php index f5d9c328d..1b81c6fc6 100644 --- a/resources/views/vendor/notifications/email.blade.php +++ b/resources/views/vendor/notifications/email.blade.php @@ -1,5 +1,5 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html lang="{{ config('app.lang') }}"> +<html lang="{{ $locale->htmlLang() }}"> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> diff --git a/tests/LanguageTest.php b/tests/LanguageTest.php index a66227ff2..b6a7d1e87 100644 --- a/tests/LanguageTest.php +++ b/tests/LanguageTest.php @@ -78,6 +78,7 @@ class LanguageTest extends TestCase public function test_rtl_config_set_if_lang_is_rtl() { $this->asEditor(); + // TODO - Alter $this->assertFalse(config('app.rtl'), 'App RTL config should be false by default'); setting()->putUser($this->users->editor(), 'language', 'ar'); $this->get('/'); From 8994c1b9d9dca616c74cdcdb17cc4722203e3299 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Sun, 17 Sep 2023 16:20:21 +0100 Subject: [PATCH 2/4] Locales: More use of locale objects, Addressed failing tests --- .../Notifications/UserInviteNotification.php | 12 +++++------ .../Messages/BaseActivityNotification.php | 7 ++++--- .../Messages/CommentCreationNotification.php | 18 ++++++++--------- .../Messages/PageCreationNotification.php | 16 +++++++-------- .../Messages/PageUpdateNotification.php | 18 ++++++++--------- app/App/MailNotification.php | 5 +++-- app/Translation/LocaleDefinition.php | 8 ++++++++ app/Translation/LocaleManager.php | 12 ++++++++++- app/Users/Models/User.php | 7 ++++--- resources/views/layouts/base.blade.php | 4 ++-- resources/views/layouts/export.blade.php | 2 +- resources/views/layouts/plain.blade.php | 4 ++-- resources/views/users/create.blade.php | 2 +- resources/views/users/edit.blade.php | 17 ++++++++++------ .../vendor/notifications/email.blade.php | 4 ++-- tests/LanguageTest.php | 20 +++++++++---------- tests/User/UserManagementTest.php | 2 +- 17 files changed, 92 insertions(+), 66 deletions(-) diff --git a/app/Access/Notifications/UserInviteNotification.php b/app/Access/Notifications/UserInviteNotification.php index b453fc95d..dacce574f 100644 --- a/app/Access/Notifications/UserInviteNotification.php +++ b/app/Access/Notifications/UserInviteNotification.php @@ -16,12 +16,12 @@ class UserInviteNotification extends MailNotification public function toMail(User $notifiable): MailMessage { $appName = ['appName' => setting('app-name')]; - $language = $notifiable->getLanguage(); + $locale = $notifiable->getLocale(); - return $this->newMailMessage($language) - ->subject(trans('auth.user_invite_email_subject', $appName, $language)) - ->greeting(trans('auth.user_invite_email_greeting', $appName, $language)) - ->line(trans('auth.user_invite_email_text', [], $language)) - ->action(trans('auth.user_invite_email_action', [], $language), url('/register/invite/' . $this->token)); + return $this->newMailMessage($locale) + ->subject($locale->trans('auth.user_invite_email_subject', $appName)) + ->greeting($locale->trans('auth.user_invite_email_greeting', $appName)) + ->line($locale->trans('auth.user_invite_email_text')) + ->action($locale->trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token)); } } diff --git a/app/Activity/Notifications/Messages/BaseActivityNotification.php b/app/Activity/Notifications/Messages/BaseActivityNotification.php index 414859091..322df5d94 100644 --- a/app/Activity/Notifications/Messages/BaseActivityNotification.php +++ b/app/Activity/Notifications/Messages/BaseActivityNotification.php @@ -5,6 +5,7 @@ namespace BookStack\Activity\Notifications\Messages; use BookStack\Activity\Models\Loggable; use BookStack\Activity\Notifications\MessageParts\LinkedMailMessageLine; use BookStack\App\MailNotification; +use BookStack\Translation\LocaleDefinition; use BookStack\Users\Models\User; use Illuminate\Bus\Queueable; @@ -35,12 +36,12 @@ abstract class BaseActivityNotification extends MailNotification /** * Build the common reason footer line used in mail messages. */ - protected function buildReasonFooterLine(string $language): LinkedMailMessageLine + protected function buildReasonFooterLine(LocaleDefinition $locale): LinkedMailMessageLine { return new LinkedMailMessageLine( url('/preferences/notifications'), - trans('notifications.footer_reason', [], $language), - trans('notifications.footer_reason_link', [], $language), + $locale->trans('notifications.footer_reason'), + $locale->trans('notifications.footer_reason_link'), ); } } diff --git a/app/Activity/Notifications/Messages/CommentCreationNotification.php b/app/Activity/Notifications/Messages/CommentCreationNotification.php index 5db61b04b..094ab30b7 100644 --- a/app/Activity/Notifications/Messages/CommentCreationNotification.php +++ b/app/Activity/Notifications/Messages/CommentCreationNotification.php @@ -17,17 +17,17 @@ class CommentCreationNotification extends BaseActivityNotification /** @var Page $page */ $page = $comment->entity; - $language = $notifiable->getLanguage(); + $locale = $notifiable->getLocale(); - return $this->newMailMessage($language) - ->subject(trans('notifications.new_comment_subject', ['pageName' => $page->getShortName()], $language)) - ->line(trans('notifications.new_comment_intro', ['appName' => setting('app-name')], $language)) + return $this->newMailMessage($locale) + ->subject($locale->trans('notifications.new_comment_subject', ['pageName' => $page->getShortName()])) + ->line($locale->trans('notifications.new_comment_intro', ['appName' => setting('app-name')])) ->line(new ListMessageLine([ - trans('notifications.detail_page_name', [], $language) => $page->name, - trans('notifications.detail_commenter', [], $language) => $this->user->name, - trans('notifications.detail_comment', [], $language) => strip_tags($comment->html), + $locale->trans('notifications.detail_page_name') => $page->name, + $locale->trans('notifications.detail_commenter') => $this->user->name, + $locale->trans('notifications.detail_comment') => strip_tags($comment->html), ])) - ->action(trans('notifications.action_view_comment', [], $language), $page->getUrl('#comment' . $comment->local_id)) - ->line($this->buildReasonFooterLine($language)); + ->action($locale->trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id)) + ->line($this->buildReasonFooterLine($locale)); } } diff --git a/app/Activity/Notifications/Messages/PageCreationNotification.php b/app/Activity/Notifications/Messages/PageCreationNotification.php index 110829469..da028aa8c 100644 --- a/app/Activity/Notifications/Messages/PageCreationNotification.php +++ b/app/Activity/Notifications/Messages/PageCreationNotification.php @@ -14,16 +14,16 @@ class PageCreationNotification extends BaseActivityNotification /** @var Page $page */ $page = $this->detail; - $language = $notifiable->getLanguage(); + $locale = $notifiable->getLocale(); - return $this->newMailMessage($language) - ->subject(trans('notifications.new_page_subject', ['pageName' => $page->getShortName()], $language)) - ->line(trans('notifications.new_page_intro', ['appName' => setting('app-name')], $language)) + return $this->newMailMessage($locale) + ->subject($locale->trans('notifications.new_page_subject', ['pageName' => $page->getShortName()])) + ->line($locale->trans('notifications.new_page_intro', ['appName' => setting('app-name')], $locale)) ->line(new ListMessageLine([ - trans('notifications.detail_page_name', [], $language) => $page->name, - trans('notifications.detail_created_by', [], $language) => $this->user->name, + $locale->trans('notifications.detail_page_name') => $page->name, + $locale->trans('notifications.detail_created_by') => $this->user->name, ])) - ->action(trans('notifications.action_view_page', [], $language), $page->getUrl()) - ->line($this->buildReasonFooterLine($language)); + ->action($locale->trans('notifications.action_view_page'), $page->getUrl()) + ->line($this->buildReasonFooterLine($locale)); } } diff --git a/app/Activity/Notifications/Messages/PageUpdateNotification.php b/app/Activity/Notifications/Messages/PageUpdateNotification.php index 8e2d27fa4..1c8155d29 100644 --- a/app/Activity/Notifications/Messages/PageUpdateNotification.php +++ b/app/Activity/Notifications/Messages/PageUpdateNotification.php @@ -14,17 +14,17 @@ class PageUpdateNotification extends BaseActivityNotification /** @var Page $page */ $page = $this->detail; - $language = $notifiable->getLanguage(); + $locale = $notifiable->getLocale(); - return $this->newMailMessage($language) - ->subject(trans('notifications.updated_page_subject', ['pageName' => $page->getShortName()], $language)) - ->line(trans('notifications.updated_page_intro', ['appName' => setting('app-name')], $language)) + return $this->newMailMessage($locale) + ->subject($locale->trans('notifications.updated_page_subject', ['pageName' => $page->getShortName()])) + ->line($locale->trans('notifications.updated_page_intro', ['appName' => setting('app-name')])) ->line(new ListMessageLine([ - trans('notifications.detail_page_name', [], $language) => $page->name, - trans('notifications.detail_updated_by', [], $language) => $this->user->name, + $locale->trans('notifications.detail_page_name') => $page->name, + $locale->trans('notifications.detail_updated_by') => $this->user->name, ])) - ->line(trans('notifications.updated_page_debounce', [], $language)) - ->action(trans('notifications.action_view_page', [], $language), $page->getUrl()) - ->line($this->buildReasonFooterLine($language)); + ->line($locale->trans('notifications.updated_page_debounce')) + ->action($locale->trans('notifications.action_view_page'), $page->getUrl()) + ->line($this->buildReasonFooterLine($locale)); } } diff --git a/app/App/MailNotification.php b/app/App/MailNotification.php index 8c57b5621..50b7f69a7 100644 --- a/app/App/MailNotification.php +++ b/app/App/MailNotification.php @@ -2,6 +2,7 @@ namespace BookStack\App; +use BookStack\Translation\LocaleDefinition; use BookStack\Users\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -32,9 +33,9 @@ abstract class MailNotification extends Notification implements ShouldQueue /** * Create a new mail message. */ - protected function newMailMessage(string $language = ''): MailMessage + protected function newMailMessage(?LocaleDefinition $locale = null): MailMessage { - $data = ['language' => $language ?: null]; + $data = ['locale' => $locale ?? user()->getLocale()]; return (new MailMessage())->view([ 'html' => 'vendor.notifications.email', diff --git a/app/Translation/LocaleDefinition.php b/app/Translation/LocaleDefinition.php index fe8640109..85d36afa0 100644 --- a/app/Translation/LocaleDefinition.php +++ b/app/Translation/LocaleDefinition.php @@ -42,4 +42,12 @@ class LocaleDefinition { return $this->isRtl ? 'rtl' : 'ltr'; } + + /** + * Translate using this locate. + */ + public function trans(string $key, array $replace = []): string + { + return trans($key, $replace, $this->appLocale()); + } } diff --git a/app/Translation/LocaleManager.php b/app/Translation/LocaleManager.php index 171555293..cf93aff06 100644 --- a/app/Translation/LocaleManager.php +++ b/app/Translation/LocaleManager.php @@ -27,6 +27,7 @@ class LocaleManager 'bs' => ['iso' => 'bs_BA', 'windows' => 'Bosnian (Latin)'], 'ca' => ['iso' => 'ca', 'windows' => 'Catalan'], 'cs' => ['iso' => 'cs_CZ', 'windows' => 'Czech'], + 'cy' => ['iso' => 'cy_GB', 'windows' => 'Welsh'], 'da' => ['iso' => 'da_DK', 'windows' => 'Danish'], 'de' => ['iso' => 'de_DE', 'windows' => 'German'], 'de_informal' => ['iso' => 'de_DE', 'windows' => 'German'], @@ -44,6 +45,7 @@ class LocaleManager 'id' => ['iso' => 'id_ID', 'windows' => 'Indonesian'], 'it' => ['iso' => 'it_IT', 'windows' => 'Italian'], 'ja' => ['iso' => 'ja', 'windows' => 'Japanese'], + 'ka' => ['iso' => 'ka_GE', 'windows' => 'Georgian'], 'ko' => ['iso' => 'ko_KR', 'windows' => 'Korean'], 'lt' => ['iso' => 'lt_LT', 'windows' => 'Lithuanian'], 'lv' => ['iso' => 'lv_LV', 'windows' => 'Latvian'], @@ -99,7 +101,7 @@ class LocaleManager */ protected function autoDetectLocale(Request $request, string $default): string { - $availableLocales = array_keys($this->localeMap); + $availableLocales = $this->getAllAppLocales(); foreach ($request->getLanguages() as $lang) { if (in_array($lang, $availableLocales)) { @@ -151,4 +153,12 @@ class LocaleManager setlocale(LC_TIME, $locales[0], ...array_slice($locales, 1)); } } + + /** + * Get all the available app-specific level locale strings. + */ + public function getAllAppLocales(): array + { + return array_keys($this->localeMap); + } } diff --git a/app/Users/Models/User.php b/app/Users/Models/User.php index 78411e0d4..39236c7e4 100644 --- a/app/Users/Models/User.php +++ b/app/Users/Models/User.php @@ -12,6 +12,7 @@ use BookStack\Api\ApiToken; use BookStack\App\Model; use BookStack\App\Sluggable; use BookStack\Entities\Tools\SlugGenerator; +use BookStack\Translation\LocaleDefinition; use BookStack\Translation\LocaleManager; use BookStack\Uploads\Image; use Carbon\Carbon; @@ -342,11 +343,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon } /** - * Get the system language for this user. + * Get the locale for this user. */ - public function getLanguage(): string + public function getLocale(): LocaleDefinition { - return app()->make(LocaleManager::class)->getForUser($this)->appLocale(); + return app()->make(LocaleManager::class)->getForUser($this); } /** diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 0fd12b70f..ca8570d36 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -1,6 +1,6 @@ <!DOCTYPE html> -<html lang="{{ $locale->htmlLang() }}" - dir="{{ $locale->htmlDirection() }}" +<html lang="{{ $locale?->htmlLang() ?? config('app.default_locale') }}" + dir="{{ $locale?->htmlDirection() ?? 'auto' }}" class="{{ setting()->getForCurrentUser('dark-mode-enabled') ? 'dark-mode ' : '' }}"> <head> <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title> diff --git a/resources/views/layouts/export.blade.php b/resources/views/layouts/export.blade.php index eb2397a75..c2c3880e4 100644 --- a/resources/views/layouts/export.blade.php +++ b/resources/views/layouts/export.blade.php @@ -1,5 +1,5 @@ <!doctype html> -<html lang="{{ $locale->htmlLang() }}"> +<html lang="{{ $locale?->htmlLang() ?? config('app.default_locale') }}"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>@yield('title')</title> diff --git a/resources/views/layouts/plain.blade.php b/resources/views/layouts/plain.blade.php index 360c404c1..7ce9078e2 100644 --- a/resources/views/layouts/plain.blade.php +++ b/resources/views/layouts/plain.blade.php @@ -1,6 +1,6 @@ <!DOCTYPE html> -<html lang="{{ $locale->htmlLang() }}" - dir="{{ $locale->htmlDirection() }}" +<html lang="{{ $locale?->htmlLang() ?? config('app.default_locale') }}" + dir="{{ $locale?->htmlDirection() ?? 'auto' }}" class="@yield('document-class')"> <head> <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title> diff --git a/resources/views/users/create.blade.php b/resources/views/users/create.blade.php index 0edae1d82..dafc623e1 100644 --- a/resources/views/users/create.blade.php +++ b/resources/views/users/create.blade.php @@ -14,7 +14,7 @@ <div class="setting-list"> @include('users.parts.form') - @include('users.parts.language-option-row', ['value' => old('setting.language') ?? config('app.locale')]) + @include('users.parts.language-option-row', ['value' => old('language') ?? config('app.default_locale')]) </div> <div class="form-group text-right"> diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 23cf87684..832186930 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -16,7 +16,8 @@ <div class="grid half gap-xl"> <div> - <label for="user-avatar" class="setting-list-label">{{ trans('settings.users_avatar') }}</label> + <label for="user-avatar" + class="setting-list-label">{{ trans('settings.users_avatar') }}</label> <p class="small">{{ trans('settings.users_avatar_desc') }}</p> </div> <div> @@ -33,13 +34,15 @@ </div> </div> - @include('users.parts.language-option-row', ['value' => $user->getLanguage())]) + @include('users.parts.language-option-row', ['value' => old('language') ?? $user->getLocale()->appLocale()]) </div> <div class="text-right"> - <a href="{{ url(userCan('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a> + <a href="{{ url(userCan('users-manage') ? "/settings/users" : "/") }}" + class="button outline">{{ trans('common.cancel') }}</a> @if($authMethod !== 'system') - <a href="{{ url("/settings/users/{$user->id}/delete") }}" class="button outline">{{ trans('settings.users_delete') }}</a> + <a href="{{ url("/settings/users/{$user->id}/delete") }}" + class="button outline">{{ trans('settings.users_delete') }}</a> @endif <button class="button" type="submit">{{ trans('common.save') }}</button> </div> @@ -60,7 +63,8 @@ </div> <div class="text-m-right"> @if($user->id === user()->id) - <a href="{{ url('/mfa/setup') }}" class="button outline">{{ trans('settings.users_mfa_configure') }}</a> + <a href="{{ url('/mfa/setup') }}" + class="button outline">{{ trans('settings.users_mfa_configure') }}</a> @endif </div> </div> @@ -84,7 +88,8 @@ class="button small outline">{{ trans('settings.users_social_disconnect') }}</button> </form> @else - <a href="{{ url("/login/service/{$driver}") }}" aria-label="{{ trans('settings.users_social_connect') }} - {{ $driver }}" + <a href="{{ url("/login/service/{$driver}") }}" + aria-label="{{ trans('settings.users_social_connect') }} - {{ $driver }}" class="button small outline">{{ trans('settings.users_social_connect') }}</a> @endif </div> diff --git a/resources/views/vendor/notifications/email.blade.php b/resources/views/vendor/notifications/email.blade.php index 1b81c6fc6..8e922fba5 100644 --- a/resources/views/vendor/notifications/email.blade.php +++ b/resources/views/vendor/notifications/email.blade.php @@ -159,7 +159,7 @@ $style = [ <tr> <td style="{{ $fontFamily }}"> <p style="{{ $style['paragraph-sub'] }}"> - {{ trans('common.email_action_help', ['actionText' => $actionText], $language) }} + {{ $locale->trans('common.email_action_help', ['actionText' => $actionText]) }} </p> <p style="{{ $style['paragraph-sub'] }}"> @@ -187,7 +187,7 @@ $style = [ <p style="{{ $style['paragraph-sub'] }}"> © {{ date('Y') }} <a style="{{ $style['anchor'] }}" href="{{ url('/') }}" target="_blank">{{ setting('app-name') }}</a>. - {{ trans('common.email_rights', [], $language) }} + {{ $locale->trans('common.email_rights') }} </p> </td> </tr> diff --git a/tests/LanguageTest.php b/tests/LanguageTest.php index b6a7d1e87..6b6856184 100644 --- a/tests/LanguageTest.php +++ b/tests/LanguageTest.php @@ -3,6 +3,7 @@ namespace Tests; use BookStack\Activity\ActivityType; +use BookStack\Translation\LocaleManager; class LanguageTest extends TestCase { @@ -17,12 +18,12 @@ class LanguageTest extends TestCase $this->langs = array_diff(scandir(lang_path('')), ['..', '.']); } - public function test_locales_config_key_set_properly() + public function test_locales_list_set_properly() { - $configLocales = config('app.locales'); - sort($configLocales); + $appLocales = $this->app->make(LocaleManager::class)->getAllAppLocales(); + sort($appLocales); sort($this->langs); - $this->assertEquals(implode(':', $configLocales), implode(':', $this->langs), 'app.locales configuration variable does not match those found in lang files'); + $this->assertEquals(implode(':', $this->langs), implode(':', $appLocales), 'app.locales configuration variable does not match those found in lang files'); } // Not part of standard phpunit test runs since we sometimes expect non-added langs. @@ -75,14 +76,13 @@ class LanguageTest extends TestCase } } - public function test_rtl_config_set_if_lang_is_rtl() + public function test_views_use_rtl_if_rtl_language_is_set() { - $this->asEditor(); - // TODO - Alter - $this->assertFalse(config('app.rtl'), 'App RTL config should be false by default'); + $this->asEditor()->withHtml($this->get('/'))->assertElementExists('html[dir="ltr"]'); + setting()->putUser($this->users->editor(), 'language', 'ar'); - $this->get('/'); - $this->assertTrue(config('app.rtl'), 'App RTL config should have been set to true by middleware'); + + $this->withHtml($this->get('/'))->assertElementExists('html[dir="rtl"]'); } public function test_unknown_lang_does_not_break_app() diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index a6d869b2f..93d35f5d0 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -215,7 +215,7 @@ class UserManagementTest extends TestCase { $langs = ['en', 'fr', 'hr']; foreach ($langs as $lang) { - config()->set('app.locale', $lang); + config()->set('app.default_locale', $lang); $resp = $this->asAdmin()->get('/settings/users/create'); $this->withHtml($resp)->assertElementExists('select[name="language"] option[value="' . $lang . '"][selected]'); } From b42e8cdb638c711a42d7d7df52238f92c537f2dc Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Sun, 17 Sep 2023 17:35:00 +0100 Subject: [PATCH 3/4] Locales: Fixed errors occuring for PHP < 8.2 --- app/Entities/Tools/ExportFormatter.php | 39 ++++++++++++------------ resources/views/layouts/base.blade.php | 4 +-- resources/views/layouts/export.blade.php | 2 +- resources/views/layouts/plain.blade.php | 4 +-- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/app/Entities/Tools/ExportFormatter.php b/app/Entities/Tools/ExportFormatter.php index 9e4d63cf7..6779797d1 100644 --- a/app/Entities/Tools/ExportFormatter.php +++ b/app/Entities/Tools/ExportFormatter.php @@ -16,18 +16,11 @@ use Throwable; class ExportFormatter { - protected ImageService $imageService; - protected PdfGenerator $pdfGenerator; - protected CspService $cspService; - - /** - * ExportService constructor. - */ - public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator, CspService $cspService) - { - $this->imageService = $imageService; - $this->pdfGenerator = $pdfGenerator; - $this->cspService = $cspService; + public function __construct( + protected ImageService $imageService, + protected PdfGenerator $pdfGenerator, + protected CspService $cspService + ) { } /** @@ -36,13 +29,14 @@ class ExportFormatter * * @throws Throwable */ - public function pageToContainedHtml(Page $page) + public function pageToContainedHtml(Page $page): string { $page->html = (new PageContent($page))->render(); $pageHtml = view('exports.page', [ 'page' => $page, 'format' => 'html', 'cspContent' => $this->cspService->getCspMetaTagValue(), + 'locale' => user()->getLocale(), ])->render(); return $this->containHtml($pageHtml); @@ -53,7 +47,7 @@ class ExportFormatter * * @throws Throwable */ - public function chapterToContainedHtml(Chapter $chapter) + public function chapterToContainedHtml(Chapter $chapter): string { $pages = $chapter->getVisiblePages(); $pages->each(function ($page) { @@ -64,6 +58,7 @@ class ExportFormatter 'pages' => $pages, 'format' => 'html', 'cspContent' => $this->cspService->getCspMetaTagValue(), + 'locale' => user()->getLocale(), ])->render(); return $this->containHtml($html); @@ -74,7 +69,7 @@ class ExportFormatter * * @throws Throwable */ - public function bookToContainedHtml(Book $book) + public function bookToContainedHtml(Book $book): string { $bookTree = (new BookContents($book))->getTree(false, true); $html = view('exports.book', [ @@ -82,6 +77,7 @@ class ExportFormatter 'bookChildren' => $bookTree, 'format' => 'html', 'cspContent' => $this->cspService->getCspMetaTagValue(), + 'locale' => user()->getLocale(), ])->render(); return $this->containHtml($html); @@ -92,13 +88,14 @@ class ExportFormatter * * @throws Throwable */ - public function pageToPdf(Page $page) + public function pageToPdf(Page $page): string { $page->html = (new PageContent($page))->render(); $html = view('exports.page', [ 'page' => $page, 'format' => 'pdf', 'engine' => $this->pdfGenerator->getActiveEngine(), + 'locale' => user()->getLocale(), ])->render(); return $this->htmlToPdf($html); @@ -109,7 +106,7 @@ class ExportFormatter * * @throws Throwable */ - public function chapterToPdf(Chapter $chapter) + public function chapterToPdf(Chapter $chapter): string { $pages = $chapter->getVisiblePages(); $pages->each(function ($page) { @@ -121,6 +118,7 @@ class ExportFormatter 'pages' => $pages, 'format' => 'pdf', 'engine' => $this->pdfGenerator->getActiveEngine(), + 'locale' => user()->getLocale(), ])->render(); return $this->htmlToPdf($html); @@ -131,7 +129,7 @@ class ExportFormatter * * @throws Throwable */ - public function bookToPdf(Book $book) + public function bookToPdf(Book $book): string { $bookTree = (new BookContents($book))->getTree(false, true); $html = view('exports.book', [ @@ -139,6 +137,7 @@ class ExportFormatter 'bookChildren' => $bookTree, 'format' => 'pdf', 'engine' => $this->pdfGenerator->getActiveEngine(), + 'locale' => user()->getLocale(), ])->render(); return $this->htmlToPdf($html); @@ -194,7 +193,7 @@ class ExportFormatter /** @var DOMElement $iframe */ foreach ($iframes as $iframe) { $link = $iframe->getAttribute('src'); - if (strpos($link, '//') === 0) { + if (str_starts_with($link, '//')) { $link = 'https:' . $link; } @@ -240,7 +239,7 @@ class ExportFormatter foreach ($linksOutput[0] as $index => $linkMatch) { $oldLinkString = $linkMatch; $srcString = $linksOutput[2][$index]; - if (strpos(trim($srcString), 'http') !== 0) { + if (!str_starts_with(trim($srcString), 'http')) { $newSrcString = url($srcString); $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString); $htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent); diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index ca8570d36..f303aff26 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -1,6 +1,6 @@ <!DOCTYPE html> -<html lang="{{ $locale?->htmlLang() ?? config('app.default_locale') }}" - dir="{{ $locale?->htmlDirection() ?? 'auto' }}" +<html lang="{{ isset($locale) ? $locale->htmlLang() : config('app.default_locale') }}" + dir="{{ isset($locale) ? $locale->htmlDirection() : 'auto' }}" class="{{ setting()->getForCurrentUser('dark-mode-enabled') ? 'dark-mode ' : '' }}"> <head> <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title> diff --git a/resources/views/layouts/export.blade.php b/resources/views/layouts/export.blade.php index c2c3880e4..eb2397a75 100644 --- a/resources/views/layouts/export.blade.php +++ b/resources/views/layouts/export.blade.php @@ -1,5 +1,5 @@ <!doctype html> -<html lang="{{ $locale?->htmlLang() ?? config('app.default_locale') }}"> +<html lang="{{ $locale->htmlLang() }}"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>@yield('title')</title> diff --git a/resources/views/layouts/plain.blade.php b/resources/views/layouts/plain.blade.php index 7ce9078e2..a3ee74143 100644 --- a/resources/views/layouts/plain.blade.php +++ b/resources/views/layouts/plain.blade.php @@ -1,6 +1,6 @@ <!DOCTYPE html> -<html lang="{{ $locale?->htmlLang() ?? config('app.default_locale') }}" - dir="{{ $locale?->htmlDirection() ?? 'auto' }}" +<html lang="{{ isset($locale) ? $locale->htmlLang() : config('app.default_locale') }}" + dir="{{ isset($locale) ? $locale->htmlDirection() : 'auto' }}" class="@yield('document-class')"> <head> <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title> From 78bf11cf653ea9a9268ccb013d444ff60fd375c5 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Sun, 17 Sep 2023 22:02:12 +0100 Subject: [PATCH 4/4] Locales: Removed a lot of existing locale handling There was a lot of locale handling to get correct/expected date formatting within the app. Carbon now has built-in locale content rather than us needing to target specific system locales. This also removes setting locale via Carbon directly. Carbon registers its own Laravel service provider which seems to accurately pull the correct locale from the app. For #4555 --- app/Http/Middleware/Localization.php | 2 +- app/Translation/LocaleManager.php | 137 +++++++++------------------ 2 files changed, 47 insertions(+), 92 deletions(-) diff --git a/app/Http/Middleware/Localization.php b/app/Http/Middleware/Localization.php index 0be0b77eb..94d06a627 100644 --- a/app/Http/Middleware/Localization.php +++ b/app/Http/Middleware/Localization.php @@ -27,7 +27,7 @@ class Localization view()->share('locale', $userLocale); // Set locale for system components - $this->localeManager->setAppLocale($userLocale); + app()->setLocale($userLocale->appLocale()); return $next($request); } diff --git a/app/Translation/LocaleManager.php b/app/Translation/LocaleManager.php index cf93aff06..825eae7a4 100644 --- a/app/Translation/LocaleManager.php +++ b/app/Translation/LocaleManager.php @@ -3,7 +3,6 @@ namespace BookStack\Translation; use BookStack\Users\Models\User; -use Carbon\Carbon; use Illuminate\Http\Request; class LocaleManager @@ -14,57 +13,55 @@ class LocaleManager protected array $rtlLocales = ['ar', 'fa', 'he']; /** - * Map of BookStack locale names to best-estimate ISO and windows locale names. + * Map of BookStack locale names to best-estimate ISO locale names. * Locales can often be found by running `locale -a` on a linux system. - * Windows locales can be found at: - * https://docs.microsoft.com/en-us/cpp/c-runtime-library/language-strings?view=msvc-170. * - * @var array<string, array{iso: string, windows: string}> + * @var array<string, string> */ protected array $localeMap = [ - 'ar' => ['iso' => 'ar', 'windows' => 'Arabic'], - 'bg' => ['iso' => 'bg_BG', 'windows' => 'Bulgarian'], - 'bs' => ['iso' => 'bs_BA', 'windows' => 'Bosnian (Latin)'], - 'ca' => ['iso' => 'ca', 'windows' => 'Catalan'], - 'cs' => ['iso' => 'cs_CZ', 'windows' => 'Czech'], - 'cy' => ['iso' => 'cy_GB', 'windows' => 'Welsh'], - 'da' => ['iso' => 'da_DK', 'windows' => 'Danish'], - 'de' => ['iso' => 'de_DE', 'windows' => 'German'], - 'de_informal' => ['iso' => 'de_DE', 'windows' => 'German'], - 'el' => ['iso' => 'el_GR', 'windows' => 'Greek'], - 'en' => ['iso' => 'en_GB', 'windows' => 'English'], - 'es' => ['iso' => 'es_ES', 'windows' => 'Spanish'], - 'es_AR' => ['iso' => 'es_AR', 'windows' => 'Spanish'], - 'et' => ['iso' => 'et_EE', 'windows' => 'Estonian'], - 'eu' => ['iso' => 'eu_ES', 'windows' => 'Basque'], - 'fa' => ['iso' => 'fa_IR', 'windows' => 'Persian'], - 'fr' => ['iso' => 'fr_FR', 'windows' => 'French'], - 'he' => ['iso' => 'he_IL', 'windows' => 'Hebrew'], - 'hr' => ['iso' => 'hr_HR', 'windows' => 'Croatian'], - 'hu' => ['iso' => 'hu_HU', 'windows' => 'Hungarian'], - 'id' => ['iso' => 'id_ID', 'windows' => 'Indonesian'], - 'it' => ['iso' => 'it_IT', 'windows' => 'Italian'], - 'ja' => ['iso' => 'ja', 'windows' => 'Japanese'], - 'ka' => ['iso' => 'ka_GE', 'windows' => 'Georgian'], - 'ko' => ['iso' => 'ko_KR', 'windows' => 'Korean'], - 'lt' => ['iso' => 'lt_LT', 'windows' => 'Lithuanian'], - 'lv' => ['iso' => 'lv_LV', 'windows' => 'Latvian'], - 'nb' => ['iso' => 'nb_NO', 'windows' => 'Norwegian (Bokmal)'], - 'nl' => ['iso' => 'nl_NL', 'windows' => 'Dutch'], - 'pl' => ['iso' => 'pl_PL', 'windows' => 'Polish'], - 'pt' => ['iso' => 'pt_PT', 'windows' => 'Portuguese'], - 'pt_BR' => ['iso' => 'pt_BR', 'windows' => 'Portuguese'], - 'ro' => ['iso' => 'ro_RO', 'windows' => 'Romanian'], - 'ru' => ['iso' => 'ru', 'windows' => 'Russian'], - 'sk' => ['iso' => 'sk_SK', 'windows' => 'Slovak'], - 'sl' => ['iso' => 'sl_SI', 'windows' => 'Slovenian'], - 'sv' => ['iso' => 'sv_SE', 'windows' => 'Swedish'], - 'tr' => ['iso' => 'tr_TR', 'windows' => 'Turkish'], - 'uk' => ['iso' => 'uk_UA', 'windows' => 'Ukrainian'], - 'uz' => ['iso' => 'uz_UZ', 'windows' => 'Uzbek'], - 'vi' => ['iso' => 'vi_VN', 'windows' => 'Vietnamese'], - 'zh_CN' => ['iso' => 'zh_CN', 'windows' => 'Chinese (Simplified)'], - 'zh_TW' => ['iso' => 'zh_TW', 'windows' => 'Chinese (Traditional)'], + 'ar' => 'ar', + 'bg' => 'bg_BG', + 'bs' => 'bs_BA', + 'ca' => 'ca', + 'cs' => 'cs_CZ', + 'cy' => 'cy_GB', + 'da' => 'da_DK', + 'de' => 'de_DE', + 'de_informal' => 'de_DE', + 'el' => 'el_GR', + 'en' => 'en_GB', + 'es' => 'es_ES', + 'es_AR' => 'es_AR', + 'et' => 'et_EE', + 'eu' => 'eu_ES', + 'fa' => 'fa_IR', + 'fr' => 'fr_FR', + 'he' => 'he_IL', + 'hr' => 'hr_HR', + 'hu' => 'hu_HU', + 'id' => 'id_ID', + 'it' => 'it_IT', + 'ja' => 'ja', + 'ka' => 'ka_GE', + 'ko' => 'ko_KR', + 'lt' => 'lt_LT', + 'lv' => 'lv_LV', + 'nb' => 'nb_NO', + 'nl' => 'nl_NL', + 'pl' => 'pl_PL', + 'pt' => 'pt_PT', + 'pt_BR' => 'pt_BR', + 'ro' => 'ro_RO', + 'ru' => 'ru', + 'sk' => 'sk_SK', + 'sl' => 'sl_SI', + 'sv' => 'sv_SE', + 'tr' => 'tr_TR', + 'uk' => 'uk_UA', + 'uz' => 'uz_UZ', + 'vi' => 'vi_VN', + 'zh_CN' => 'zh_CN', + 'zh_TW' => 'zh_TW', ]; /** @@ -90,7 +87,7 @@ class LocaleManager return new LocaleDefinition( $localeString, - $this->getIsoName($localeString), + $this->localeMap[$localeString] ?? $localeString, in_array($localeString, $this->rtlLocales), ); } @@ -112,48 +109,6 @@ class LocaleManager return $default; } - /** - * Get the ISO version of a BookStack locale. - */ - protected function getIsoName(string $locale): string - { - return $this->localeMap[$locale]['iso'] ?? $locale; - } - - /** - * Sets the active locale for system level components. - */ - public function setAppLocale(LocaleDefinition $locale): void - { - app()->setLocale($locale->appLocale()); - Carbon::setLocale($locale->isoLocale()); - $this->setPhpDateTimeLocale($locale); - } - - /** - * Set the system date locale for localized date formatting. - * Will try both the standard locale name and the UTF8 variant. - */ - public function setPhpDateTimeLocale(LocaleDefinition $locale): void - { - $appLocale = $locale->appLocale(); - $isoLocale = $this->localeMap[$appLocale]['iso'] ?? ''; - $isoLocalePrefix = explode('_', $isoLocale)[0]; - - $locales = array_values(array_filter([ - $isoLocale ? $isoLocale . '.utf8' : false, - $isoLocale ?: false, - $isoLocale ? str_replace('_', '-', $isoLocale) : false, - $isoLocale ? $isoLocalePrefix . '.UTF-8' : false, - $this->localeMap[$appLocale]['windows'] ?? false, - $appLocale, - ])); - - if (!empty($locales)) { - setlocale(LC_TIME, $locales[0], ...array_slice($locales, 1)); - } - } - /** * Get all the available app-specific level locale strings. */