Skip to content

Commit

Permalink
Merge 298f240 into 452f412
Browse files Browse the repository at this point in the history
  • Loading branch information
samwilson committed Jan 7, 2019
2 parents 452f412 + 298f240 commit c950581
Show file tree
Hide file tree
Showing 16 changed files with 181 additions and 68 deletions.
58 changes: 52 additions & 6 deletions assets/translate.js
Expand Up @@ -9,22 +9,29 @@ $( function () {
return;
}
function onSelectTargetLang( language ) {
// Save the language name and code in the widget.
var $imgElement, newImageUrl;
// 1. Save the language name and code in the widget.
this.setLabel( $.uls.data.languages[ language ][ 2 ] );
this.setData( language );
// Also switch what's displayed in the form when a new language is selected in the ULS.
this.setValue( language );
// 2. Switch what's displayed in the form when a new language is selected in the ULS.
$( '.translation-fields .oo-ui-fieldLayout' ).each( function () {
var field = OO.ui.infuse( $( this ) ).getField();
if ( appConfig.translations[ field.data.nodeId ] &&
appConfig.translations[ field.data.nodeId ][ language ]
var field = OO.ui.infuse( $( this ) ).getField(),
tspanId = field.data[ 'tspan-id' ];
if ( appConfig.translations[ tspanId ] &&
appConfig.translations[ tspanId ][ language ]
) {
// If there's a translation available, set the field's value.
field.setValue( appConfig.translations[ field.data.nodeId ][ language ].text );
field.setValue( appConfig.translations[ tspanId ][ language ].text );
} else {
// Otherwise, blank the field.
field.setValue( '' );
}
} );
// 3. Update the image.
$imgElement = $( '.image img' );
newImageUrl = $imgElement.attr( 'src' ).replace( /[a-z_-]*\.png.*$/, language + '.png' );
$imgElement.attr( 'src', newImageUrl );
}
targetLangButton = OO.ui.infuse( $targetLangButton );
targetLangButton.$element.uls( {
Expand Down Expand Up @@ -68,3 +75,42 @@ $( function () {
} );
} );
} );

/**
* When a translation field is changed, update the image preview.
*/
$( function () {
$( '.translation-fields .oo-ui-fieldLayout .oo-ui-inputWidget' ).each( function () {
var inputWiget = OO.ui.infuse( $( this ) ),
$imgElement = $( '.image img' ),
targetLangWidget = OO.ui.infuse( $( '.target-lang-widget' ) ),
targetLangCode = targetLangWidget.getValue(),
requestParams = {},
updatePreviewImage = function () {
// Go through all fields and construct the request parameters.
$( '.translation-fields .oo-ui-fieldLayout' ).each( function () {
var fieldLayout = OO.ui.infuse( $( this ) ),
tspanId = fieldLayout.getField().data[ 'tspan-id' ];
requestParams[ tspanId ] = fieldLayout.getField().getValue();
} );
// Update the image.
$.ajax( {
type: 'POST',
url: appConfig.baseUrl + 'api/translate/' + $imgElement.data( 'filename' ) + '/' + targetLangCode,
data: requestParams,
success: function ( result ) {
$imgElement.attr( 'src', result.imageSrc );
},
error: function () {
OO.ui.alert( $.i18n( 'preview-error-occurred' ) );
}
} );
};
// Update the preview image on field blur and after two seconds of no typing.
inputWiget.$input.on( 'blur', updatePreviewImage );
inputWiget.on( 'change', OO.ui.debounce( updatePreviewImage, 2000 ) );
} );
// Trigger a change, to force a refresh of the preview on page load (to catch browser-cached
// input values).
$( '.translation-fields .oo-ui-fieldLayout .oo-ui-inputWidget input' ).trigger( 'blur' );
} );
1 change: 1 addition & 0 deletions i18n/en.json
Expand Up @@ -33,6 +33,7 @@
"download-button-label": "Download",
"download-or-upload": "or",
"translation-image-alt": "The image that's currently being translated. No description available.",
"preview-error-occurred": "An error occurred when fetching the preview. Please continue translating, but if this error persists do report a bug (via the link in the footer below).",

"download-icon-alt": "An icon indicating file-download",
"pick-an-image-title": "Pick an image",
Expand Down
1 change: 1 addition & 0 deletions i18n/qqq.json
Expand Up @@ -33,6 +33,7 @@
"download-button-label": "Label for the button that downloads the translated SVG to the user's local computer.",
"download-or-upload": "Label positioned between the download and the upload buttons.",
"translation-image-alt": "Alt text for the image that's currently being translated. Doesn't explain the image, just explains that we don't explain it.",
"preview-error-occurred": "Error message displayed in a popup when fetching a preview failed. The only option is 'OK' to close the popup.",

"download-icon-alt": "Alt text for the download icon.",
"pick-an-image-title": "The title of the 'pick an image' step of the tutorial box.",
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/assets/entrypoints.json
Expand Up @@ -5,7 +5,7 @@
"assets/app.3acc02e0.css"
],
"js": [
"assets/app.bd58f67a.js"
"assets/app.b885261f.js"
]
}
}
Expand Down
1 change: 1 addition & 0 deletions public/assets/i18n/app/en.json
Expand Up @@ -33,6 +33,7 @@
"download-button-label": "Download",
"download-or-upload": "or",
"translation-image-alt": "The image that's currently being translated. No description available.",
"preview-error-occurred": "An error occurred when fetching the preview. Please continue translating, but if this error persists do report a bug (via the link in the footer below).",

"download-icon-alt": "An icon indicating file-download",
"pick-an-image-title": "Pick an image",
Expand Down
1 change: 1 addition & 0 deletions public/assets/i18n/app/qqq.json
Expand Up @@ -33,6 +33,7 @@
"download-button-label": "Label for the button that downloads the translated SVG to the user's local computer.",
"download-or-upload": "Label positioned between the download and the upload buttons.",
"translation-image-alt": "Alt text for the image that's currently being translated. Doesn't explain the image, just explains that we don't explain it.",
"preview-error-occurred": "Error message displayed in a popup when fetching a preview failed. The only option is 'OK' to close the popup.",

"download-icon-alt": "Alt text for the download icon.",
"pick-an-image-title": "The title of the 'pick an image' step of the tutorial box.",
Expand Down
2 changes: 1 addition & 1 deletion public/assets/manifest.json
@@ -1,6 +1,6 @@
{
"assets/app.css": "assets/app.3acc02e0.css",
"assets/app.js": "assets/app.bd58f67a.js",
"assets/app.js": "assets/app.b885261f.js",
"assets/grabbing.cur": "assets/a8c874b93b3d848f39a71260c57e3863.cur",
"assets/grab.cur": "assets/b06c243f534d9c5461d16528156cd5a8.cur",
"assets/i18n/app/af.json": "assets/i18n/app/af.json",
Expand Down
105 changes: 67 additions & 38 deletions src/Controller/ApiController.php
Expand Up @@ -8,8 +8,11 @@
use App\Service\FileCache;
use App\Service\Renderer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;

/**
Expand All @@ -31,45 +34,87 @@ public function __construct(FileCache $cache, Renderer $svgRenderer)
}

/**
* Serve a PNG rendering of the given SVG in the given language.
* Serve a PNG rendering of the given SVG in the given language (without any user-provided
* translation strings).
*
* @Route("/api/file/{filename}/{lang}.png", name="api_file", methods="GET")
*
* @param string $filename
* @param string $lang
* @return Response
*/
public function getFile(string $filename, string $lang): Response
{
$filename = Title::normalize($filename);
return $this->serveContent($this->cache->getPath($filename), $lang);
$content = $this->svgRenderer->render($this->cache->getPath($filename), $lang);
return new Response($content, 200, [
'Content-Type' => 'image/png',
'X-File-Hash' => sha1($content),
]);
}

/**
* Serve a PNG rendering of the given SVG in the given language, based on the POSTed translations.
* @Route("/api/file/{filename}/{lang}.png", name="api_file_translated", methods="POST")
* @param string $filename
* @param Request $request
* @return Response
* Get a full filesystem path to a temporary PNG file.
* @param string $filename The base SVG filename.
* @param string $key They unique key to append to the filename.
* @return string
*/
public function getFileWithTranslations(string $filename, string $lang, Request $request): Response
protected function getTempPngFilename(string $filename, string $key): string
{
$filename = Title::normalize($filename);
$json = $request->getContent();
if ('' === $json) {
return $this->getFile($filename, $lang);
}
return $this->cache->fullPath($filename.'_'.$key.'.png');
}

$translations = \GuzzleHttp\json_decode($json, true);
/**
* Take POST data, write new translations into the SVG file, render a resultant PNG out to the
* filesystem, and return an identifier for that PNG file.
*
* @Route("/api/translate/{filename}/{lang}", name="api_file_request", methods="POST")
*/
public function requestFileWIthTranslations(string $filename, string $lang, Request $request): Response
{
// Get the SVG file, and add in the new translations.
$filename = Title::normalize($filename);
$path = $this->cache->getPath($filename);
$file = new SvgFile($path);
$file->switchToTranslationSet($translations);

// Write SVG file to filesystem, so it can be converted by rsvg. Use a unique filename
// because multiple users can be translating the same file at the same time (whereas
// getFile() above only ever serves the one version of a file).
$tmpSvgFilename = $this->cache->getTempSvgFile();
$file->saveToPath($tmpSvgFilename);
$translations = $request->request->all();
$file->setTranslations($lang, $translations);

// Write the modified SVG out to the filesystem, named with a unique key. This is necessary
// both because multiple people could be translating the same file at the same time, and
// also means we can use the same key for the rendered PNG file. The key is generated from
// the translation set so that the
$fileKey = md5(serialize($translations));
$tempPngFilename = $this->getTempPngFilename($filename, $fileKey);
$file->saveToPath($tempPngFilename);

// Render the modified SVG to PNG, and return it's URL.
$renderedPngContents = $this->svgRenderer->render($tempPngFilename, $lang);
file_put_contents($tempPngFilename, $renderedPngContents);
$relativeUrl = $this->generateUrl('api_file_translated', [
'filename' => $filename,
'key' => $fileKey,
'lang' => $lang,
]);
return new JsonResponse(['imageSrc' => $relativeUrl]);
}

return $this->serveContent($tmpSvgFilename, $lang);
/**
* Get a request for a already-rendered, custom-translated PNG (identified by a key), and
* return that file.
*
* @Route("/api/file/{filename}/{lang}/{key}.png", name="api_file_translated", methods="GET")
*
* @param string $filename
* @param Request $request
* @return Response
*/
public function getFileWithTranslations(string $filename, string $lang, string $key, Request $request): Response
{
$tempPngFilename = $this->getTempPngFilename($filename, $key);
if (file_exists($tempPngFilename)) {
return new BinaryFileResponse($tempPngFilename);
}
throw new NotFoundHttpException();
}

/**
Expand Down Expand Up @@ -103,20 +148,4 @@ public function getLanguages(string $fileName): Response

return $this->json($langs);
}

/**
* Serves an SVG as a PNG.
*
* @param string $svgFilename Full path to the SVG file to convert and serve as a PNG.
* @param string $lang The language to use for the SVG's text.
* @return Response
*/
private function serveContent(string $svgFilename, string $lang): Response
{
$content = $this->svgRenderer->render($svgFilename, $lang);
return new Response($content, 200, [
'Content-Type' => 'image/png',
'X-File-Hash' => sha1($content),
]);
}
}
13 changes: 8 additions & 5 deletions src/Controller/TranslateController.php
Expand Up @@ -122,16 +122,19 @@ public function translate(
// Messages.
$translations = $svgFile->getInFileTranslations();
$formFields = [];
foreach ($translations as $nodeId => $langs) {
foreach ($translations as $tspanId => $translation) {
$fieldValue = isset($translation[$targetLang->getValue()])
? $translation[$targetLang->getValue()]['text']
: '';
$inputWidget = new TextInputWidget([
'name' => 'translation-field-'.$nodeId,
'value' => isset($langs[$targetLang->getValue()]) ? $langs[$targetLang->getValue()]['text'] : '',
'data' => ['nodeId' => $nodeId],
'name' => 'translation-field-'.$tspanId,
'value' => $fieldValue,
'data' => ['tspan-id' => $tspanId],
]);
$field = new FieldLayout(
$inputWidget,
[
'label' => $langs[$sourceLang->getValue()]['text'],
'label' => $translation[$sourceLang->getValue()]['text'],
'infusable' => true,
]
);
Expand Down
32 changes: 32 additions & 0 deletions src/Model/Svg/SvgFile.php
Expand Up @@ -430,6 +430,38 @@ public function getInFileTranslations(): array
return $this->inFileTranslations;
}

/**
* Set translations for a single language.
* @param string $lang Language code of the translations being provided.
* @param string[] $translations Array of tspan-IDs to message texts.
* @return string[][]
*/
public function setTranslations(string $lang, array $translations): array
{
// Load the existing translation structure, and go through it swapping the messages.
$inFileTranslations = $this->getInFileTranslations();
$filteredTextNodes = $this->getFilteredTextNodes();
foreach ($translations as $tspanId => $msg) {
// Set up the tspan node (including adding in the new message).
if (!isset($inFileTranslations[$tspanId][$lang])) {
$inFileTranslations[$tspanId][$lang] = $inFileTranslations[$tspanId]['fallback'];
$inFileTranslations[$tspanId][$lang]['id'] .= "-$lang";
}
if (!empty($msg)) {
$inFileTranslations[$tspanId][$lang]['text'] = $msg;
}
// Set up the text node (if this is a new language).
$textId = $inFileTranslations[$tspanId][$lang]['data-parent'];
if (!isset($filteredTextNodes[$textId][$lang])) {
$filteredTextNodes[$textId][$lang] = $filteredTextNodes[$textId]['fallback'];
$filteredTextNodes[$textId][$lang]['id'] .= "-$lang";
}
}
$allTranslations = array_merge($inFileTranslations, $filteredTextNodes);
// Add the updated translations back into the file.
return $this->switchToTranslationSet($allTranslations);
}

/**
* Try to return $this->savedLanguages (a list of languages which have one or more
* translations in-file). If it is not cached, analyse the SVG and hence generate it.
Expand Down
14 changes: 2 additions & 12 deletions src/Service/FileCache.php
Expand Up @@ -71,7 +71,7 @@ protected function tick(): void
return;
}

foreach (glob($this->fullPath('*.svg')) as $fileName) {
foreach (glob($this->fullPath('*.*')) as $fileName) {
$this->statFile($fileName);
}
}
Expand Down Expand Up @@ -103,18 +103,8 @@ protected function statFile(string $fileName): bool
* @param string $fileName
* @return string
*/
protected function fullPath(string $fileName): string
public function fullPath(string $fileName): string
{
return $this->directory.DIRECTORY_SEPARATOR.$fileName;
}

/**
* Create a file with a unique .svg filename, with access permission set to 0600,
* and return its name.
* @return string
*/
public function getTempSvgFile(): string
{
return tempnam($this->directory, 'temp_').'.svg';
}
}
1 change: 1 addition & 0 deletions templates/base.html.twig
Expand Up @@ -75,6 +75,7 @@

<script type="text/javascript">
var appConfig = {
baseUrl: "{{ asset('') }}",
assetsPath: "{{ asset('assets') }}",
{# This var is used for the Universal Language Selector. #}
languages: {{ all_langs()|json_encode|raw }}
Expand Down
4 changes: 3 additions & 1 deletion templates/translate.html.twig
Expand Up @@ -29,7 +29,9 @@
{{ download_button|raw }}
</div>
<div class="image">
<img src="{{ path('api_file', {filename: filename, lang: target_lang}) }}" alt="{{ msg('translation-image-alt') }}" />
<img src="{{ path('api_file', {filename: filename, lang: target_lang}) }}"
alt="{{ msg('translation-image-alt') }}"
data-filename="{{ filename }}" />
</div>
</div>
</form>
Expand Down

0 comments on commit c950581

Please sign in to comment.