From ed40987fea50b2602d9f2904796a995e9a40d140 Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Tue, 8 Aug 2023 17:02:13 +0200 Subject: [PATCH] As the tests shown, that the custom parameter breaks browser based caching, we now enforce the 'traditional url' approach and added cache control information for the browser --- .../TransformTranslationRequestMiddleware.php | 46 ----------------- Classes/Http/TranslationRequestMiddleware.php | 50 ++++++++++++++++--- Configuration/Objects.yaml | 22 +++----- Configuration/Settings.yaml | 11 ++-- Configuration/Testing/Settings.yaml | 5 +- README.md | 16 +++--- .../Functional/TranslationMiddlewareTest.php | 35 ++++--------- 7 files changed, 77 insertions(+), 108 deletions(-) delete mode 100644 Classes/Http/TransformTranslationRequestMiddleware.php diff --git a/Classes/Http/TransformTranslationRequestMiddleware.php b/Classes/Http/TransformTranslationRequestMiddleware.php deleted file mode 100644 index c43395c..0000000 --- a/Classes/Http/TransformTranslationRequestMiddleware.php +++ /dev/null @@ -1,46 +0,0 @@ -headerName = $headerName; - $this->reactOnPath = $reactOnPath; - $this->translateGetParam = $translateGetParam; - } - - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - if ($this->reactOnPath !== null && $request->getUri()->getPath() === $this->reactOnPath) { - $request = $request->withAddedHeader( - $this->headerName, - $request->getQueryParams()[$this->translateGetParam] ?? '' - ); - } - return $handler->handle($request); - } -} diff --git a/Classes/Http/TranslationRequestMiddleware.php b/Classes/Http/TranslationRequestMiddleware.php index d25bc3d..a91b47e 100644 --- a/Classes/Http/TranslationRequestMiddleware.php +++ b/Classes/Http/TranslationRequestMiddleware.php @@ -8,6 +8,7 @@ use GuzzleHttp\Psr7\Response; use Neos\Cache\Exception as CacheException; use Neos\Cache\Exception\InvalidDataException; use Neos\Cache\Frontend\StringFrontend; +use Neos\Flow\Annotations as Flow; use Neos\Flow\I18n\Detector; use Neos\Flow\I18n\Service; use Neos\Flow\I18n\Xliff\Service\XliffFileProvider; @@ -22,7 +23,17 @@ class TranslationRequestMiddleware implements MiddlewareInterface /** * @var string */ - protected string $headerName; + protected string $reactOnPath; + + /** + * @var string + */ + protected string $getParameterName; + + /** + * @var int + */ + protected int $browserCacheMaxAge; /** * @var Service @@ -45,13 +56,17 @@ class TranslationRequestMiddleware implements MiddlewareInterface protected StringFrontend $responseCache; public function __construct( - string $headerName, + string $reactOnPath, + string $getParameterName, + int $browserCacheMaxAge, Service $i18nService, XliffFileProvider $fileProvider, Detector $detector, StringFrontend $responseCache ) { - $this->headerName = $headerName; + $this->reactOnPath = $reactOnPath; + $this->getParameterName = $getParameterName; + $this->browserCacheMaxAge = $browserCacheMaxAge; $this->i18nService = $i18nService; $this->fileProvider = $fileProvider; $this->detector = $detector; @@ -68,7 +83,7 @@ class TranslationRequestMiddleware implements MiddlewareInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if ($request->hasHeader($this->headerName)) { + if ($request->getUri()->getPath() === $this->reactOnPath) { return $this->createResponse($request); } return $handler->handle($request); @@ -88,7 +103,7 @@ class TranslationRequestMiddleware implements MiddlewareInterface $bestMatching = $this->i18nService->findBestMatchingLocale($wishedLocale); $this->i18nService->getConfiguration()->setCurrentLocale($bestMatching); } - $idPatternList = $request->getHeader($this->headerName)[0] ?? ''; + $idPatternList = $request->getQueryParams()[$this->getParameterName] ?? ''; $cacheId = $this->i18nService->getConfiguration()->getCurrentLocale() . '_' . \sha1(\serialize($idPatternList)); if ($this->responseCache->has($cacheId)) { $response = $this->responseCache->get($cacheId); @@ -97,7 +112,16 @@ class TranslationRequestMiddleware implements MiddlewareInterface $response = \json_encode($result, \JSON_THROW_ON_ERROR); $this->responseCache->set($cacheId, $response); } - return new Response(200, ['Content-Type' => 'application/json'], $response); + return new Response( + 200, + [ + 'Content-Type' => 'application/json', + 'Cache-Control' => 'max-age=' . $this->browserCacheMaxAge . ', must-revalidate', + 'Last-Modified' => $this->getCacheDate(), + 'Vary' => 'Accept, Accept-Language' + ], + $response + ); } /** @@ -140,4 +164,18 @@ class TranslationRequestMiddleware implements MiddlewareInterface } return $result; } + + /** + * @return string + */ + protected function getCacheDate(): string + { + // the translation file monitor will flush our complete cache, each time a file is modified. That way it resets + // the last modified date here, too. + if (false === $lastModified = $this->responseCache->get('lastModified')) { + $lastModified = (new \DateTimeImmutable())->format(\DATE_RFC7231); + $this->responseCache->set('lastModified', $lastModified); + } + return $lastModified; + } } diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml index 196f3a3..18221ee 100644 --- a/Configuration/Objects.yaml +++ b/Configuration/Objects.yaml @@ -1,13 +1,3 @@ -DigiComp.FlowTranslationEndpoint:TransformTranslationRequestMiddleware: - className: "DigiComp\\FlowTranslationEndpoint\\Http\\TransformTranslationRequestMiddleware" - arguments: - 1: - setting: "DigiComp.FlowTranslationEndpoint.headerName" - 2: - setting: "DigiComp.FlowTranslationEndpoint.replaceRoutedEndpoint.reactOnPath" - 3: - setting: "DigiComp.FlowTranslationEndpoint.replaceRoutedEndpoint.translateGetParam" - DigiComp.FlowTranslationEndpoint:TranslationResponseCache: className: "Neos\\Cache\\Frontend\\StringFrontend" factoryObjectName: "Neos\\Flow\\Cache\\CacheManager" @@ -21,12 +11,16 @@ DigiComp.FlowTranslationEndpoint:TranslationRequestMiddleware: autowiring: true arguments: 1: - setting: "DigiComp.FlowTranslationEndpoint.headerName" + setting: "DigiComp.FlowTranslationEndpoint.reactOnPath" 2: - object: "Neos\\Flow\\I18n\\Service" + setting: "DigiComp.FlowTranslationEndpoint.getParameterName" 3: - object: "Neos\\Flow\\I18n\\Xliff\\Service\\XliffFileProvider" + setting: "DigiComp.FlowTranslationEndpoint.browserCacheMaxAge" 4: - object: "Neos\\Flow\\I18n\\Detector" + object: "Neos\\Flow\\I18n\\Service" 5: + object: "Neos\\Flow\\I18n\\Xliff\\Service\\XliffFileProvider" + 6: + object: "Neos\\Flow\\I18n\\Detector" + 7: object: "DigiComp.FlowTranslationEndpoint:TranslationResponseCache" diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 727af5a..5953567 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -2,16 +2,13 @@ Neos: Flow: http: middlewares: - translationReplace: - position: "before translation" - middleware: "DigiComp.FlowTranslationEndpoint:TransformTranslationRequestMiddleware" translation: position: "start 100" middleware: "DigiComp.FlowTranslationEndpoint:TranslationRequestMiddleware" DigiComp: FlowTranslationEndpoint: - replaceRoutedEndpoint: - reactOnPath: ~ - translateGetParam: "idPatterns" - headerName: "X-Translation-Request" + reactOnPath: '/xliff-units' + getParameterName: "idPatterns" + # default is 6 minutes, so we "expect" in average 3 minutes of "old" translation in worst case + browserCacheMaxAge: 360 diff --git a/Configuration/Testing/Settings.yaml b/Configuration/Testing/Settings.yaml index 5afa1eb..806032f 100644 --- a/Configuration/Testing/Settings.yaml +++ b/Configuration/Testing/Settings.yaml @@ -1,5 +1,4 @@ DigiComp: FlowTranslationEndpoint: - replaceRoutedEndpoint: - reactOnPath: 'testing/translate' - headerName: "X-Translation-Request" + reactOnPath: 'testing/translate' + getParameterName: "idPatterns" diff --git a/README.md b/README.md index 632bf41..7a11702 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ This package is designed to help bringing needed translations to javascript comp Other solutions, which would generate files available for usage in client scope, have the disadvantage that one would have to repeat the relativ complex overriding and merging logic of Flow. With this endpoint you can get the same content, as you would get, if you call the translation service with your translation id. -The main components are a `CurrentHtmlLangViewHelper`, which is intended to be used to fill the `lang` attribute of the `html` tag, so the frontend knows, which language is currently active (and is good practice anyway) and a `TranslationRequestMiddleware`, which will respond to any request, which have a `X-Translation-Request` header, carrying a pattern of translation ids which should be returned. +The main components are a `CurrentHtmlLangViewHelper`, which is intended to be used to fill the `lang` attribute of the `html` tag, so the frontend knows, which language is currently active (and is good practice anyway) and a `TranslationRequestMiddleware`, which will respond to any request, where the request path equals `DigiComp.FlowTranslationEndpoint.reactOnPath` (Default: "/xliff-units"), and search for unit patterns in the `DigiComp.FlowTranslationEndpoint.getParameterName` (Default: "idPatterns"). For example: ```` -X-Translation-Request: Neos.Flow:Main|authentication.* +GET /xliff-units?idPatterns=Neos.Flow:Main|authentication.* ```` would return all translation keys from the main unit of `Neos.Flow` starting with "authentication" and would look like that: @@ -34,9 +34,11 @@ Your JavaScript could look like that: ```javascript async function translate(idPatterns) { - const response = await fetch(document.location, {headers: { - 'X-Translation-Request': idPatterns, - 'Accept-Language': document.documentElement.lang + const uri = new URL('/xliff-units', document.location); + uri.searchParams.set('idPatterns', idPatterns); + const response = await fetch(uri, {headers: { + 'Accept': 'application/json', + 'Accept-Language': document.documentElement.lang, }}); if (! response.ok) { return Promise.reject('Unexpected server response'); @@ -44,5 +46,5 @@ async function translate(idPatterns) { return await response.json(); } ``` - -If, for whatever reason, you prefer to have a "traditional" single endpoint, which works without a custom header, you can set `DigiComp.FlowTranslationEndpoint.replaceRoutedEndpoint.reactOnPath`. At this point a second middleware (`TransformGetTranslationRequestMiddleware`) transforms all incoming requests and the GET parameter `idPatterns` (you can change names in Settings.yaml). +Last but not least: +Do not forget to have a lot of fun. diff --git a/Tests/Functional/TranslationMiddlewareTest.php b/Tests/Functional/TranslationMiddlewareTest.php index 0e93ddc..bfb3f05 100644 --- a/Tests/Functional/TranslationMiddlewareTest.php +++ b/Tests/Functional/TranslationMiddlewareTest.php @@ -20,31 +20,6 @@ class TranslationMiddlewareTest extends FunctionalTestCase $this->serverRequestFactory = $this->objectManager->get(ServerRequestFactoryInterface::class); } - /** - * @test - */ - public function itRespondsToRequestsWithTheConfiguredHeader(): void - { - $request = $this->serverRequestFactory->createServerRequest('GET', 'dummyUrl'); - $request = $request - ->withHeader('X-Translation-Request', 'DigiComp.FlowTranslationEndpoint:Test|.*') - ->withHeader('Accept-Language', 'en'); - $response = $this->browser->sendRequest($request); - static::assertEquals( - '{"DigiComp.FlowTranslationEndpoint:Test":{"key1":"en_key1"}}', - (string)$response->getBody() - ); - $request = $this->serverRequestFactory->createServerRequest('GET', 'dummyUrl'); - $request = $request - ->withHeader('X-Translation-Request', 'DigiComp.FlowTranslationEndpoint:Test|.*') - ->withHeader('Accept-Language', 'de'); - $response = $this->browser->sendRequest($request); - static::assertEquals( - '{"DigiComp.FlowTranslationEndpoint:Test":{"key1":"de_key1"}}', - (string)$response->getBody() - ); - } - /** * @test */ @@ -59,5 +34,15 @@ class TranslationMiddlewareTest extends FunctionalTestCase '{"DigiComp.FlowTranslationEndpoint:Test":{"key1":"en_key1"}}', (string)$response->getBody() ); + + $request = $this->serverRequestFactory->createServerRequest('GET', 'testing/translate'); + $request = $request + ->withQueryParams(['idPatterns' => 'DigiComp.FlowTranslationEndpoint:Test|.*']) + ->withHeader('Accept-Language', 'de'); + $response = $this->browser->sendRequest($request); + static::assertEquals( + '{"DigiComp.FlowTranslationEndpoint:Test":{"key1":"de_key1"}}', + (string)$response->getBody() + ); } }