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
This commit is contained in:
parent
38e45f5d8f
commit
ed40987fea
7 changed files with 77 additions and 108 deletions
|
@ -1,46 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DigiComp\FlowTranslationEndpoint\Http;
|
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
|
||||||
|
|
||||||
class TransformTranslationRequestMiddleware implements MiddlewareInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected string $headerName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string|null
|
|
||||||
*/
|
|
||||||
protected ?string $reactOnPath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected string $translateGetParam;
|
|
||||||
|
|
||||||
public function __construct(string $headerName, ?string $reactOnPath, string $translateGetParam)
|
|
||||||
{
|
|
||||||
$this->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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,6 +8,7 @@ use GuzzleHttp\Psr7\Response;
|
||||||
use Neos\Cache\Exception as CacheException;
|
use Neos\Cache\Exception as CacheException;
|
||||||
use Neos\Cache\Exception\InvalidDataException;
|
use Neos\Cache\Exception\InvalidDataException;
|
||||||
use Neos\Cache\Frontend\StringFrontend;
|
use Neos\Cache\Frontend\StringFrontend;
|
||||||
|
use Neos\Flow\Annotations as Flow;
|
||||||
use Neos\Flow\I18n\Detector;
|
use Neos\Flow\I18n\Detector;
|
||||||
use Neos\Flow\I18n\Service;
|
use Neos\Flow\I18n\Service;
|
||||||
use Neos\Flow\I18n\Xliff\Service\XliffFileProvider;
|
use Neos\Flow\I18n\Xliff\Service\XliffFileProvider;
|
||||||
|
@ -22,7 +23,17 @@ class TranslationRequestMiddleware implements MiddlewareInterface
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected string $headerName;
|
protected string $reactOnPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected string $getParameterName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected int $browserCacheMaxAge;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Service
|
* @var Service
|
||||||
|
@ -45,13 +56,17 @@ class TranslationRequestMiddleware implements MiddlewareInterface
|
||||||
protected StringFrontend $responseCache;
|
protected StringFrontend $responseCache;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $headerName,
|
string $reactOnPath,
|
||||||
|
string $getParameterName,
|
||||||
|
int $browserCacheMaxAge,
|
||||||
Service $i18nService,
|
Service $i18nService,
|
||||||
XliffFileProvider $fileProvider,
|
XliffFileProvider $fileProvider,
|
||||||
Detector $detector,
|
Detector $detector,
|
||||||
StringFrontend $responseCache
|
StringFrontend $responseCache
|
||||||
) {
|
) {
|
||||||
$this->headerName = $headerName;
|
$this->reactOnPath = $reactOnPath;
|
||||||
|
$this->getParameterName = $getParameterName;
|
||||||
|
$this->browserCacheMaxAge = $browserCacheMaxAge;
|
||||||
$this->i18nService = $i18nService;
|
$this->i18nService = $i18nService;
|
||||||
$this->fileProvider = $fileProvider;
|
$this->fileProvider = $fileProvider;
|
||||||
$this->detector = $detector;
|
$this->detector = $detector;
|
||||||
|
@ -68,7 +83,7 @@ class TranslationRequestMiddleware implements MiddlewareInterface
|
||||||
*/
|
*/
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
{
|
{
|
||||||
if ($request->hasHeader($this->headerName)) {
|
if ($request->getUri()->getPath() === $this->reactOnPath) {
|
||||||
return $this->createResponse($request);
|
return $this->createResponse($request);
|
||||||
}
|
}
|
||||||
return $handler->handle($request);
|
return $handler->handle($request);
|
||||||
|
@ -88,7 +103,7 @@ class TranslationRequestMiddleware implements MiddlewareInterface
|
||||||
$bestMatching = $this->i18nService->findBestMatchingLocale($wishedLocale);
|
$bestMatching = $this->i18nService->findBestMatchingLocale($wishedLocale);
|
||||||
$this->i18nService->getConfiguration()->setCurrentLocale($bestMatching);
|
$this->i18nService->getConfiguration()->setCurrentLocale($bestMatching);
|
||||||
}
|
}
|
||||||
$idPatternList = $request->getHeader($this->headerName)[0] ?? '';
|
$idPatternList = $request->getQueryParams()[$this->getParameterName] ?? '';
|
||||||
$cacheId = $this->i18nService->getConfiguration()->getCurrentLocale() . '_' . \sha1(\serialize($idPatternList));
|
$cacheId = $this->i18nService->getConfiguration()->getCurrentLocale() . '_' . \sha1(\serialize($idPatternList));
|
||||||
if ($this->responseCache->has($cacheId)) {
|
if ($this->responseCache->has($cacheId)) {
|
||||||
$response = $this->responseCache->get($cacheId);
|
$response = $this->responseCache->get($cacheId);
|
||||||
|
@ -97,7 +112,16 @@ class TranslationRequestMiddleware implements MiddlewareInterface
|
||||||
$response = \json_encode($result, \JSON_THROW_ON_ERROR);
|
$response = \json_encode($result, \JSON_THROW_ON_ERROR);
|
||||||
$this->responseCache->set($cacheId, $response);
|
$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 $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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
DigiComp.FlowTranslationEndpoint:TranslationResponseCache:
|
||||||
className: "Neos\\Cache\\Frontend\\StringFrontend"
|
className: "Neos\\Cache\\Frontend\\StringFrontend"
|
||||||
factoryObjectName: "Neos\\Flow\\Cache\\CacheManager"
|
factoryObjectName: "Neos\\Flow\\Cache\\CacheManager"
|
||||||
|
@ -21,12 +11,16 @@ DigiComp.FlowTranslationEndpoint:TranslationRequestMiddleware:
|
||||||
autowiring: true
|
autowiring: true
|
||||||
arguments:
|
arguments:
|
||||||
1:
|
1:
|
||||||
setting: "DigiComp.FlowTranslationEndpoint.headerName"
|
setting: "DigiComp.FlowTranslationEndpoint.reactOnPath"
|
||||||
2:
|
2:
|
||||||
object: "Neos\\Flow\\I18n\\Service"
|
setting: "DigiComp.FlowTranslationEndpoint.getParameterName"
|
||||||
3:
|
3:
|
||||||
object: "Neos\\Flow\\I18n\\Xliff\\Service\\XliffFileProvider"
|
setting: "DigiComp.FlowTranslationEndpoint.browserCacheMaxAge"
|
||||||
4:
|
4:
|
||||||
object: "Neos\\Flow\\I18n\\Detector"
|
object: "Neos\\Flow\\I18n\\Service"
|
||||||
5:
|
5:
|
||||||
|
object: "Neos\\Flow\\I18n\\Xliff\\Service\\XliffFileProvider"
|
||||||
|
6:
|
||||||
|
object: "Neos\\Flow\\I18n\\Detector"
|
||||||
|
7:
|
||||||
object: "DigiComp.FlowTranslationEndpoint:TranslationResponseCache"
|
object: "DigiComp.FlowTranslationEndpoint:TranslationResponseCache"
|
||||||
|
|
|
@ -2,16 +2,13 @@ Neos:
|
||||||
Flow:
|
Flow:
|
||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
translationReplace:
|
|
||||||
position: "before translation"
|
|
||||||
middleware: "DigiComp.FlowTranslationEndpoint:TransformTranslationRequestMiddleware"
|
|
||||||
translation:
|
translation:
|
||||||
position: "start 100"
|
position: "start 100"
|
||||||
middleware: "DigiComp.FlowTranslationEndpoint:TranslationRequestMiddleware"
|
middleware: "DigiComp.FlowTranslationEndpoint:TranslationRequestMiddleware"
|
||||||
|
|
||||||
DigiComp:
|
DigiComp:
|
||||||
FlowTranslationEndpoint:
|
FlowTranslationEndpoint:
|
||||||
replaceRoutedEndpoint:
|
reactOnPath: '/xliff-units'
|
||||||
reactOnPath: ~
|
getParameterName: "idPatterns"
|
||||||
translateGetParam: "idPatterns"
|
# default is 6 minutes, so we "expect" in average 3 minutes of "old" translation in worst case
|
||||||
headerName: "X-Translation-Request"
|
browserCacheMaxAge: 360
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
DigiComp:
|
DigiComp:
|
||||||
FlowTranslationEndpoint:
|
FlowTranslationEndpoint:
|
||||||
replaceRoutedEndpoint:
|
|
||||||
reactOnPath: 'testing/translate'
|
reactOnPath: 'testing/translate'
|
||||||
headerName: "X-Translation-Request"
|
getParameterName: "idPatterns"
|
||||||
|
|
16
README.md
16
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.
|
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:
|
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:
|
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
|
```javascript
|
||||||
async function translate(idPatterns) {
|
async function translate(idPatterns) {
|
||||||
const response = await fetch(document.location, {headers: {
|
const uri = new URL('/xliff-units', document.location);
|
||||||
'X-Translation-Request': idPatterns,
|
uri.searchParams.set('idPatterns', idPatterns);
|
||||||
'Accept-Language': document.documentElement.lang
|
const response = await fetch(uri, {headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Accept-Language': document.documentElement.lang,
|
||||||
}});
|
}});
|
||||||
if (! response.ok) {
|
if (! response.ok) {
|
||||||
return Promise.reject('Unexpected server response');
|
return Promise.reject('Unexpected server response');
|
||||||
|
@ -44,5 +46,5 @@ async function translate(idPatterns) {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
Last but not least:
|
||||||
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).
|
Do not forget to have a lot of fun.
|
||||||
|
|
|
@ -20,31 +20,6 @@ class TranslationMiddlewareTest extends FunctionalTestCase
|
||||||
$this->serverRequestFactory = $this->objectManager->get(ServerRequestFactoryInterface::class);
|
$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
|
* @test
|
||||||
*/
|
*/
|
||||||
|
@ -59,5 +34,15 @@ class TranslationMiddlewareTest extends FunctionalTestCase
|
||||||
'{"DigiComp.FlowTranslationEndpoint:Test":{"key1":"en_key1"}}',
|
'{"DigiComp.FlowTranslationEndpoint:Test":{"key1":"en_key1"}}',
|
||||||
(string)$response->getBody()
|
(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()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue