From 6f8ad9b76e7cac0fd1a0f778705dd0c7e008fe5b Mon Sep 17 00:00:00 2001 From: fipwmaqzufheoxq92ebc <29818044+fipwmaqzufheoxq92ebc@users.noreply.github.com> Date: Mon, 31 Aug 2020 19:11:51 +0200 Subject: [PATCH] Locales: use http-accept-language or cookie (#976) * Locales: use http-accept-language or "language"-cookie * Add user-setting "locale" Rename CULTURE to DEFAULT_LOCALE * Use LocaleMiddleware also in dev mode * CORS: don't require authentication on OPTIONS * Use a standard user-settings-control and start a new generic user settings page, not a separate page for the locale setting * Fixed (broken by myself) link-return handling * Clarify language settings * Removed unneeded files * Better user settings icon * Added localization hints Co-authored-by: Bernd Bestel --- .devtools/transifex_download.bat | 1 + .tx/config | 6 ++ README.md | 2 +- app.php | 12 +++ config-dist.php | 2 +- controllers/BaseController.php | 2 +- controllers/UsersController.php | 12 +++ localization/en/locales.po | 79 ++++++++++++++++++++ localization/locales.pot | 101 ++++++++++++++++++++++++++ localization/strings.pot | 9 +++ middleware/CorsMiddleware.php | 22 ++++-- middleware/LocaleMiddleware.php | 67 +++++++++++++++++ public/js/grocy.js | 26 +++++++ public/viewjs/usersettings.js | 1 + routes.php | 12 ++- services/BaseService.php | 2 +- services/DatabaseService.php | 2 +- services/DemoDataGeneratorService.php | 2 +- services/LocalizationService.php | 6 ++ views/layout/default.blade.php | 7 +- views/usersettings.blade.php | 30 ++++++++ 21 files changed, 383 insertions(+), 20 deletions(-) create mode 100644 localization/en/locales.po create mode 100644 localization/locales.pot create mode 100644 middleware/LocaleMiddleware.php create mode 100644 public/viewjs/usersettings.js create mode 100644 views/usersettings.blade.php diff --git a/.devtools/transifex_download.bat b/.devtools/transifex_download.bat index ac3536ab..c6cb8f9d 100644 --- a/.devtools/transifex_download.bat +++ b/.devtools/transifex_download.bat @@ -7,4 +7,5 @@ copy /Y localization\en\component_translations.po localization\en_GB\component_t copy /Y localization\en\chore_period_types.po localization\en_GB\chore_period_types.po copy /Y localization\en\chore_assignment_types.po localization\en_GB\chore_assignment_types.po copy /Y localization\en\permissions.po localization\en_GB\permissions.po +copy /Y localization\en\locales.po localization\en_GB\locales.po popd diff --git a/.tx/config b/.tx/config index ee8b0313..ec2f9c1d 100644 --- a/.tx/config +++ b/.tx/config @@ -48,3 +48,9 @@ file_filter = localization//permissions.po source_file = localization/permissions.pot source_lang = en type = PO + +[grocy.locales] +file_filter = localization//locales.po +source_file = localization/locales.pot +source_lang = en +type = PO diff --git a/README.md b/README.md index d39eb9df..cd2c2cd3 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ If you run grocy on Linux, there is also `update.sh` (remember to make the scrip ## Localization grocy is fully localizable - the default language is English (integrated into code), a German localization is always maintained by me. You can easily help translating grocy at https://www.transifex.com/grocy/grocy, if your language is incomplete or not available yet. -(Language can be changed in `data/config.php`, e. g. `Setting('CULTURE', 'it');`) +(The default language can be set in `data/config.php`, e. g. `Setting('DEFAULT_LOCALE', 'it');` and there is also a user setting (see the user settings page) to set a different language per user) The [pre-release demo](https://demo-prerelease.grocy.info) is available for any translation which is at least 80 % complete and will pull the translations from Transifex 10 minutes past every hour, so you can have a kind of instant preview of your contributed translations. Thank you! diff --git a/app.php b/app.php index 7680c71c..42d304cd 100644 --- a/app.php +++ b/app.php @@ -1,5 +1,6 @@ setBasePath(GROCY_BASE_PATH); } +if (GROCY_MODE === 'production' || GROCY_MODE === 'dev') +{ + $app->add(new \Grocy\Middleware\LocaleMiddleware($container)); +} +else { + define('GROCY_LOCALE', GROCY_DEFAULT_LOCALE); +} + +$authMiddlewareClass = GROCY_AUTH_CLASS; +$app->add(new $authMiddlewareClass($container, $app->getResponseFactory())); // Add default middleware $app->addRoutingMiddleware(); $errorMiddleware = $app->addErrorMiddleware(true, false, false); @@ -70,4 +81,5 @@ $errorMiddleware->setDefaultErrorHandler( new \Grocy\Controllers\ExceptionController($app, $container) ); +$app->add(new CorsMiddleware($app->getResponseFactory())); $app->run(); diff --git a/config-dist.php b/config-dist.php index 44e0f8e5..dc651cc8 100644 --- a/config-dist.php +++ b/config-dist.php @@ -21,7 +21,7 @@ Setting('MODE', 'production'); # Either "en" or "de" or the directory name of # one of the other available localization folders in the "/localization" directory -Setting('CULTURE', 'en'); +Setting('DEFAULT_LOCALE', 'en'); # This is used to define the first day of a week for calendar views in the frontend, # leave empty to use the locale default diff --git a/controllers/BaseController.php b/controllers/BaseController.php index 8538fe6c..a24b9363 100644 --- a/controllers/BaseController.php +++ b/controllers/BaseController.php @@ -110,7 +110,7 @@ class BaseController protected function getLocalizationService() { - return LocalizationService::getInstance(GROCY_CULTURE); + return LocalizationService::getInstance(GROCY_LOCALE); } protected function getApplicationservice() diff --git a/controllers/UsersController.php b/controllers/UsersController.php index 30d3a860..de03bbd0 100644 --- a/controllers/UsersController.php +++ b/controllers/UsersController.php @@ -44,4 +44,16 @@ class UsersController extends BaseController ->where('parent IS NULL')->where('user_id', $args['userId']), ]); } + + public function UserSettings(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + return $this->renderPage($response, 'usersettings', [ + 'languages' => array_filter(scandir(__DIR__.'/../localization'), function ($item){ + if($item == "." || $item == "..") + return false; + return is_dir(__DIR__.'/../localization/'.$item); + }) + + ]); + } } diff --git a/localization/en/locales.po b/localization/en/locales.po new file mode 100644 index 00000000..3143e30c --- /dev/null +++ b/localization/en/locales.po @@ -0,0 +1,79 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: http://www.transifex.com/grocy/grocy/language/en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" +"PO-Revision-Date: 2019-05-01T17:59:17+00:00\n" +"Language: en\n" +"X-Domain: grocy/locales\n" + +msgid "cs" +msgstr "Czech" + +msgid "da" +msgstr "Danish" + +msgid "de" +msgstr "German" + +msgid "el_GR" +msgstr "Greek" + +msgid "en" +msgstr "English" + +msgid "en_GB" +msgstr "English (Great Britain)" + +msgid "es" +msgstr "Spanish" + +msgid "fr" +msgstr "French" + +msgid "hu" +msgstr "Hungarian" + +msgid "it" +msgstr "Italian" + +msgid "ja" +msgstr "Japanese" + +msgid "ko_KR" +msgstr "Korean" + +msgid "nl" +msgstr "Dutch" + +msgid "no" +msgstr "Norwegian" + +msgid "pl" +msgstr "Polish" + +msgid "pt_BR" +msgstr "Portuguese (Brazil)" + +msgid "pt_PT" +msgstr "Portuguese (Portugal)" + +msgid "ru" +msgstr "Russian" + +msgid "sk_SK" +msgstr "Slovak" + +msgid "sv_SE" +msgstr "Swedish" + +msgid "tr" +msgstr "Turkish" + +msgid "zh_TW" +msgstr "Chinese (Taiwan)" diff --git a/localization/locales.pot b/localization/locales.pot new file mode 100644 index 00000000..c490101f --- /dev/null +++ b/localization/locales.pot @@ -0,0 +1,101 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: http://www.transifex.com/grocy/grocy/language/en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" +"PO-Revision-Date: 2019-05-01T17:59:17+00:00\n" +"Language: en\n" +"X-Domain: grocy/locales\n" + +# Czech +msgid "cs" +msgstr "" + +# Danish +msgid "da" +msgstr "" + +# German +msgid "de" +msgstr "" + +# Greek +msgid "el_GR" +msgstr "" + +# English +msgid "en" +msgstr "" + +# English (Great Britain) +msgid "en_GB" +msgstr "" + +# Spanish +msgid "es" +msgstr "" + +# French +msgid "fr" +msgstr "" + +# Hungarian +msgid "hu" +msgstr "" + +# Italian +msgid "it" +msgstr "" + +# Japanese +msgid "ja" +msgstr "" + +# Korean +msgid "ko_KR" +msgstr "" + +# Dutch +msgid "nl" +msgstr "" + +# Norwegian +msgid "no" +msgstr "" + +# Polish +msgid "pl" +msgstr "" + +# Portuguese (Brazil) +msgid "pt_BR" +msgstr "" + +# Portuguese (Portugal) +msgid "pt_PT" +msgstr "" + +# Russian +msgid "ru" +msgstr "" + +# Slovak +msgid "sk_SK" +msgstr "" + +# Swedish +msgid "sv_SE" +msgstr "" + +# Turkish +msgid "tr" +msgstr "" + +# Chinese (Taiwan) +msgid "zh_TW" +msgstr "" diff --git a/localization/strings.pot b/localization/strings.pot index 6cea03cf..e76abe32 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1891,3 +1891,12 @@ msgstr "" msgid "If you think this is a bug, please report it" msgstr "" + +msgid "Language" +msgstr "" + +msgid "User settings" +msgstr "" + +msgid "Default" +msgstr "" diff --git a/middleware/CorsMiddleware.php b/middleware/CorsMiddleware.php index 14f6eb3e..f6d98690 100644 --- a/middleware/CorsMiddleware.php +++ b/middleware/CorsMiddleware.php @@ -2,28 +2,40 @@ namespace Grocy\Middleware; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface as RequestHandler; use Psr\Http\Message\ResponseInterface as Response; use Slim\Routing\RouteContext; -class CorsMiddleware extends BaseMiddleware +class CorsMiddleware { + /** + * @var ResponseFactoryInterface + */ + private $responseFactory; + + public function __construct(ResponseFactoryInterface $responseFactory) + { + $this->responseFactory = $responseFactory; + } + public function __invoke(Request $request, RequestHandler $handler): Response { - $response = $handler->handle($request); + if ($request->getMethod() == "OPTIONS") + $response = $this->responseFactory->createResponse(200); + else { + $response = $handler->handle($request); + } //$routeContext = RouteContext::fromRequest($request); //$routingResults = $routeContext->getRoutingResults(); //$methods = $routingResults->getAllowedMethods(); //$requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers'); - $response = $handler->handle($request); - $response = $response->withHeader('Access-Control-Allow-Origin', '*'); $response = $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); $response = $response->withHeader('Access-Control-Allow-Headers', '*'); - $response = $response->withStatus(204); return $response; } diff --git a/middleware/LocaleMiddleware.php b/middleware/LocaleMiddleware.php new file mode 100644 index 00000000..e431ce7c --- /dev/null +++ b/middleware/LocaleMiddleware.php @@ -0,0 +1,67 @@ +getLocale($request); + define('GROCY_LOCALE', $locale); + return $handler->handle($request); + } + + protected function getLocale(Request $request) + { + if(GROCY_AUTHENTICATED) + { + $locale = UsersService::getInstance()->GetUserSetting(GROCY_USER_ID, 'locale'); + if (isset($locale) && !empty($locale)) { + if (in_array($locale, scandir(__DIR__ . '/../localization'))) { + return $locale; + } + } + } + + $langs = join(',', $request->getHeader('Accept-Language')); + + // src: https://gist.github.com/spolischook/0cde9c6286415cddc088 + $prefLocales = array_reduce( + explode(',', $langs), + function ($res, $el) { + list($l, $q) = array_merge(explode(';q=', $el), [1]); + $res[$l] = (float) $q; + return $res; + }, []); + arsort($prefLocales); + + $availableLocales = scandir(__DIR__ . '/../localization'); + foreach ($prefLocales as $locale => $q) { + if(in_array($locale, $availableLocales)) + { + return $locale; + } + // e.g. en_GB + if(in_array(substr($locale, 0, 5), $availableLocales)) + { + return substr($locale, 0, 5); + } + // e.g: cs + // or en + // or de + if(in_array(substr($locale, 0, 2), $availableLocales)) + { + return substr($locale, 0, 2); + } + } + return GROCY_DEFAULT_LOCALE; + } +} diff --git a/public/js/grocy.js b/public/js/grocy.js index dd8ca04e..aedc8da6 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -670,3 +670,29 @@ $(Grocy.UserPermissions).each(function(index, item) $('.permission-' + item.permission_name).addClass('disabled').addClass('not-allowed'); } }); +$('a.link-return').not(".btn").each(function () { + var base = $(this).data('href'); + if(base.contains('?')) + { + $(this).attr('href', base + '&returnto' + encodeURIComponent(location.pathname)); + } + else{ + $(this).attr('href', base + '?returnto=' + encodeURIComponent(location.pathname)); + } + +}) + +$(document).on("click", "a.btn.link-return", function(e) +{ + e.preventDefault(); + + var link = GetUriParam("returnto"); + if (!link || !link.length > 0) + { + location.href = $(e.currentTarget).attr("href"); + } + else + { + location.href = U(link); + } +}); diff --git a/public/viewjs/usersettings.js b/public/viewjs/usersettings.js new file mode 100644 index 00000000..068b43cb --- /dev/null +++ b/public/viewjs/usersettings.js @@ -0,0 +1 @@ +$("#locale").val(Grocy.UserSettings.locale); diff --git a/routes.php b/routes.php index 6b12055e..d7272a09 100644 --- a/routes.php +++ b/routes.php @@ -6,9 +6,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Slim\Routing\RouteCollectorProxy; use Grocy\Middleware\JsonMiddleware; -use Grocy\Middleware\CorsMiddleware; -$authMiddlewareClass = GROCY_AUTH_CLASS; $app->group('', function(RouteCollectorProxy $group) { @@ -34,6 +32,7 @@ $app->group('', function(RouteCollectorProxy $group) $group->get('/users', '\Grocy\Controllers\UsersController:UsersList'); $group->get('/user/{userId}', '\Grocy\Controllers\UsersController:UserEditForm'); $group->get('/user/{userId}/permissions', '\Grocy\Controllers\UsersController:PermissionList'); + $group->get('/usersettings', '\Grocy\Controllers\UsersController:UserSettings'); // Stock routes if (GROCY_FEATURE_FLAG_STOCK) @@ -136,7 +135,7 @@ $app->group('', function(RouteCollectorProxy $group) $group->get('/api', '\Grocy\Controllers\OpenApiController:DocumentationUi'); $group->get('/manageapikeys', '\Grocy\Controllers\OpenApiController:ApiKeysList'); $group->get('/manageapikeys/new', '\Grocy\Controllers\OpenApiController:CreateNewApiKey'); -})->add(new $authMiddlewareClass($container, $app->getResponseFactory())); +}); $app->group('/api', function(RouteCollectorProxy $group) { @@ -259,11 +258,10 @@ $app->group('/api', function(RouteCollectorProxy $group) $group->get('/calendar/ical', '\Grocy\Controllers\CalendarApiController:Ical')->setName('calendar-ical'); $group->get('/calendar/ical/sharing-link', '\Grocy\Controllers\CalendarApiController:IcalSharingLink'); } -})->add(JsonMiddleware::class) -->add(new $authMiddlewareClass($container, $app->getResponseFactory())); +})->add(JsonMiddleware::class); // Handle CORS preflight OPTIONS requests $app->options('/api/{routes:.+}', function(Request $request, Response $response): Response { - return $response; -})->add(CorsMiddleware::class); + return $response->withStatus(204); +}); diff --git a/services/BaseService.php b/services/BaseService.php index 781fd339..b5c98df0 100644 --- a/services/BaseService.php +++ b/services/BaseService.php @@ -35,7 +35,7 @@ class BaseService protected function getLocalizationService() { - return LocalizationService::getInstance(GROCY_CULTURE); + return LocalizationService::getInstance(GROCY_LOCALE); } protected function getStockservice() diff --git a/services/DatabaseService.php b/services/DatabaseService.php index 1770917b..81866bd6 100644 --- a/services/DatabaseService.php +++ b/services/DatabaseService.php @@ -22,7 +22,7 @@ class DatabaseService { if (GROCY_MODE === 'demo' || GROCY_MODE === 'prerelease') { - return GROCY_DATAPATH . '/grocy_' . GROCY_CULTURE . '.db'; + return GROCY_DATAPATH . '/grocy_' . GROCY_DEFAULT_LOCALE . '.db'; } return GROCY_DATAPATH . '/grocy.db'; diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 58eaeb7e..e74edb75 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -9,7 +9,7 @@ class DemoDataGeneratorService extends BaseService public function __construct() { parent::__construct(); - $this->LocalizationService = new LocalizationService(GROCY_CULTURE); + $this->LocalizationService = new LocalizationService(GROCY_DEFAULT_LOCALE); } protected $LocalizationService; diff --git a/services/LocalizationService.php b/services/LocalizationService.php index 35d30c23..0c409d36 100644 --- a/services/LocalizationService.php +++ b/services/LocalizationService.php @@ -61,6 +61,8 @@ class LocalizationService $this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/strings.pot')); $this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/userfield_types.pot')); $this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/permissions.pot')); + $this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/locales.pot')); + if (GROCY_MODE !== 'production') { @@ -96,6 +98,10 @@ class LocalizationService { $this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/permissions.po")); } + if (file_exists(__DIR__ . "/../localization/$culture/locales.po")) + { + $this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/locales.po")); + } if (GROCY_MODE !== 'production' && file_exists(__DIR__ . "/../localization/$culture/demo_data.po")) { $this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/demo_data.po")); diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 0796eaae..4319226b 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -1,5 +1,5 @@ - + @@ -52,7 +52,7 @@ Grocy.BaseUrl = '{{ $U('/') }}'; Grocy.CurrentUrlRelative = "/" + window.location.href.split('?')[0].replace(Grocy.BaseUrl, ""); Grocy.ActiveNav = '@yield('activeNav', '')'; - Grocy.Culture = '{{ GROCY_CULTURE }}'; + Grocy.Culture = '{{ GROCY_LOCALE }}'; Grocy.Currency = '{{ GROCY_CURRENCY }}'; Grocy.CalendarFirstDayOfWeek = '{{ GROCY_CALENDAR_FIRST_DAY_OF_WEEK }}'; Grocy.CalendarShowWeekNumbers = {{ BoolToString(GROCY_CALENDAR_SHOW_WEEK_OF_YEAR) }}; @@ -416,6 +416,9 @@ @if(GROCY_FEATURE_FLAG_TASKS)  {{ $__t('Tasks settings') }} @endif + + {{ $__t('User settings') }} + @if(GROCY_SHOW_AUTH_VIEWS)  {{ $__t('Manage users') }} diff --git a/views/usersettings.blade.php b/views/usersettings.blade.php new file mode 100644 index 00000000..e0ea1415 --- /dev/null +++ b/views/usersettings.blade.php @@ -0,0 +1,30 @@ +@extends('layout.default') + +@section('title', $__t('User settings')) +@section('activeNav', '') +@section('viewJsName', 'usersettings') + +@section('content') +
+
+

@yield('title')

+
+
+
+
+
+ +
+ + +
+ + {{ $__t('OK') }} +
+
+@stop