Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Populate contao_ Symfony translations into $GLOBALS['TL_LANG'] #6518

Merged
merged 12 commits into from Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions core-bundle/config/services.yaml
Expand Up @@ -51,6 +51,7 @@ services:
- '%kernel.project_dir%'
- '@database_connection'
- '@contao.framework'
- '@translator'
- '@contao.intl.locales'

contao.controller_resolver:
Expand Down
16 changes: 13 additions & 3 deletions core-bundle/contao/library/Contao/System.php
Expand Up @@ -11,6 +11,7 @@
namespace Contao;

use Contao\CoreBundle\Config\Loader\XliffFileLoader;
use Contao\CoreBundle\Translation\MessageCatalogue;
use Contao\CoreBundle\Util\LocaleUtil;
use Contao\Database\Installer;
use Symfony\Component\DependencyInjection\Container;
Expand Down Expand Up @@ -444,8 +445,9 @@ public static function loadLanguageFile($strName, $strLanguage=null, $blnNoCache
$arrCreateLangs = ($strLanguage == 'en') ? array('en') : array('en', $strLanguage);

// Prepare the XLIFF loader
$xlfLoader = new XliffFileLoader(static::getContainer()->getParameter('kernel.project_dir'), true);
$strCacheDir = static::getContainer()->getParameter('kernel.cache_dir');
$container = static::getContainer();
$xlfLoader = new XliffFileLoader($container->getParameter('kernel.project_dir'), true);
$strCacheDir = $container->getParameter('kernel.cache_dir');

// Load the language(s)
foreach ($arrCreateLangs as $strCreateLang)
Expand All @@ -458,7 +460,7 @@ public static function loadLanguageFile($strName, $strLanguage=null, $blnNoCache
else
{
// Find the given filename either as .php or .xlf file
$finder = static::getContainer()->get('contao.resource_finder')->findIn('languages/' . $strCreateLang)->name('/^' . $strName . '\.(php|xlf)$/');
$finder = $container->get('contao.resource_finder')->findIn('languages/' . $strCreateLang)->name('/^' . $strName . '\.(php|xlf)$/');

/** @var SplFileInfo $file */
foreach ($finder as $file)
Expand All @@ -477,6 +479,14 @@ public static function loadLanguageFile($strName, $strLanguage=null, $blnNoCache
throw new \RuntimeException(sprintf('Invalid language file extension: %s', $file->getExtension()));
}
}

// Also populate $GLOBALS['TL_LANG'] with the Symfony translations of the 'contao_' domains.
$catalogue = $container->get('translator', ContainerInterface::NULL_ON_INVALID_REFERENCE)?->getCatalogue($strLanguage);

if ($catalogue instanceof MessageCatalogue)
{
$catalogue->populateGlobals('contao_' . $strName);
}
}
}

Expand Down
38 changes: 30 additions & 8 deletions core-bundle/src/Cache/ContaoCacheWarmer.php
Expand Up @@ -18,6 +18,7 @@
use Contao\CoreBundle\Config\ResourceFinderInterface;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Intl\Locales;
use Contao\CoreBundle\Translation\MessageCatalogue;
use Contao\DcaExtractor;
use Contao\Model;
use Doctrine\DBAL\Connection;
Expand All @@ -29,6 +30,8 @@
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

class ContaoCacheWarmer implements CacheWarmerInterface
{
Expand All @@ -44,6 +47,7 @@ public function __construct(
private readonly string $projectDir,
private readonly Connection $connection,
private readonly ContaoFramework $framework,
private readonly TranslatorInterface&TranslatorBagInterface $translator,
Locales $locales,
) {
$this->locales = $locales->getEnabledLocaleIds();
Expand Down Expand Up @@ -135,14 +139,32 @@ private function generateLanguageCache(string $cacheDir): void
->name("/^$name\\.(php|xlf)$/")
;

try {
$dumper->dump(
iterator_to_array($subfiles),
Path::join('languages', $language, "$name.php"),
['type' => $language],
);
} catch (\OutOfBoundsException) {
continue;
$dumper->dump(
iterator_to_array($subfiles),
Path::join('languages', $language, "$name.php"),
['type' => $language],
);
}

// Also cache Symfony translations of the 'contao_' domains.
$catalogue = $this->translator->getCatalogue($language);

if ($catalogue instanceof MessageCatalogue) {
foreach (array_unique($catalogue->getDomains()) as $domain) {
$php = $catalogue->getGlobalsString($domain);

if (!$php) {
continue;
}

$name = substr($domain, 7);
$path = Path::join($cacheDir, 'contao', 'languages', $language, $name.'.php');

if (\in_array($name, $processed, true)) {
$this->filesystem->appendToFile($path, "\n".$php);
} else {
$this->filesystem->dumpFile($path, "<?php\n\n".$php);
}
}
}
}
Expand Down
101 changes: 6 additions & 95 deletions core-bundle/src/Config/Loader/XliffFileLoader.php
Expand Up @@ -12,6 +12,7 @@

namespace Contao\CoreBundle\Config\Loader;

use Contao\CoreBundle\Translation\LegacyGlobalsProcessor;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Filesystem\Path;

Expand Down Expand Up @@ -73,12 +74,14 @@ private function getPhpFromFileNode(\DOMElement $fileNode, string $tagName): str
continue;
}

$chunks = $this->getChunksFromUnit($unit);
$chunks = LegacyGlobalsProcessor::getPartsFromKey($unit->getAttribute('id'));
$value = $this->fixClosingTags($node->item(0));

$return .= $this->getStringRepresentation($chunks, $value);
$return .= LegacyGlobalsProcessor::getStringRepresentation($chunks, $value);

$this->addGlobal($chunks, $value);
if ($this->addToGlobals) {
LegacyGlobalsProcessor::addGlobal($chunks, $value);
}
}

return $return;
Expand All @@ -104,96 +107,4 @@ private function fixClosingTags(\DOMNode $node): string
{
return str_replace('</ em>', '</em>', $node->nodeValue);
}

/**
* Splits the ID attribute and returns the chunks.
*/
private function getChunksFromUnit(\DOMElement $unit): array
{
$chunks = explode('.', $unit->getAttribute('id'));

// Handle keys with dots
if (preg_match('/tl_layout\.[a-z]+\.css\./', $unit->getAttribute('id'))) {
$chunks = [$chunks[0], $chunks[1].'.'.$chunks[2], $chunks[3]];
}

return $chunks;
}

/**
* Returns a string representation of the global PHP language array.
*/
private function getStringRepresentation(array $chunks, string $value): string
{
return match (\count($chunks)) {
2 => sprintf(
"\$GLOBALS['TL_LANG']['%s'][%s] = %s;\n",
$chunks[0],
$this->quoteKey($chunks[1]),
$this->quoteValue($value),
),
3 => sprintf(
"\$GLOBALS['TL_LANG']['%s'][%s][%s] = %s;\n",
$chunks[0],
$this->quoteKey($chunks[1]),
$this->quoteKey($chunks[2]),
$this->quoteValue($value),
),
4 => sprintf(
"\$GLOBALS['TL_LANG']['%s'][%s][%s][%s] = %s;\n",
$chunks[0],
$this->quoteKey($chunks[1]),
$this->quoteKey($chunks[2]),
$this->quoteKey($chunks[3]),
$this->quoteValue($value),
),
default => throw new \OutOfBoundsException('Cannot load less than 2 or more than 4 levels in XLIFF language files.'),
};
}

/**
* Adds the labels to the global PHP language array.
*/
private function addGlobal(array $chunks, string $value): void
{
if (!$this->addToGlobals) {
return;
}

$data = &$GLOBALS['TL_LANG'];

foreach ($chunks as $key) {
if (!\is_array($data)) {
$data = [];
}

$data = &$data[$key];
}

$data = $value;
}

private function quoteKey(string $key): int|string
{
if ('0' === $key) {
return 0;
}

if (is_numeric($key)) {
return (int) $key;
}

return "'".str_replace("'", "\\'", $key)."'";
}

private function quoteValue(string $value): string
{
$value = str_replace("\n", '\n', $value);

if (str_contains($value, '\n')) {
return '"'.str_replace(['$', '"'], ['\\$', '\\"'], $value).'"';
}

return "'".str_replace("'", "\\'", $value)."'";
}
}
70 changes: 70 additions & 0 deletions core-bundle/src/Translation/LegacyGlobalsProcessor.php
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Translation;

final class LegacyGlobalsProcessor
{
/**
* Splits the translation key and returns the parts.
*/
public static function getPartsFromKey(string $key): array
{
// Split the key into chunks allowing escaped dots (\.) and backslashes (\\)
preg_match_all('/(?:\\\\[\\\\.]|[^.])++/', $key, $matches);

$parts = preg_replace('/\\\\([\\\\.])/', '$1', $matches[0]);

// Handle keys with dots in tl_layout
if (preg_match('/tl_layout\.[a-z]+\.css\./', $key)) {
$parts = [$parts[0], $parts[1].'.'.$parts[2], ...array_splice($parts, 3)];
}

return $parts;
}

/**
* Returns a string representation of the global PHP language array.
*/
public static function getStringRepresentation(array $parts, string $value): string
{
if (!$parts) {
return '';
}

$string = "\$GLOBALS['TL_LANG']";

foreach ($parts as $part) {
$string .= '['.var_export($part, true).']';
}

return $string.' = '.var_export($value, true).";\n";
}

/**
* Adds the labels to the global PHP language array.
*/
public static function addGlobal(array $parts, string $value): void
{
$data = &$GLOBALS['TL_LANG'];

foreach ($parts as $key) {
if (!\is_array($data)) {
$data = [];
}

$data = &$data[$key];
}

$data = $value;
}
}
44 changes: 40 additions & 4 deletions core-bundle/src/Translation/MessageCatalogue.php
Expand Up @@ -139,6 +139,45 @@ public function addResource(ResourceInterface $resource): void
$this->inner->addResource($resource);
}

/**
* Populates $GLOBALS['TL_LANG'] with all translations for the given domain.
*/
public function populateGlobals(string $domain): void
{
if (!$this->isContaoDomain($domain)) {
return;
}

$translations = $this->inner->all($domain);

foreach ($translations as $k => $v) {
$parts = LegacyGlobalsProcessor::getPartsFromKey($k);

LegacyGlobalsProcessor::addGlobal($parts, $v);
}
}

/**
* Returns the $GLOBALS['TL_LANG'] PHP string representation for all translations of a given domain.
*/
public function getGlobalsString(string $domain): string
{
if (!$this->isContaoDomain($domain)) {
return '';
}

$translations = $this->inner->all($domain);
$return = '';

foreach ($translations as $k => $v) {
$parts = LegacyGlobalsProcessor::getPartsFromKey($k);

$return .= LegacyGlobalsProcessor::getStringRepresentation($parts, $v);
}

return $return;
}

private function isContaoDomain(string|null $domain): bool
{
return str_starts_with($domain ?? '', 'contao_');
Expand All @@ -159,10 +198,7 @@ private function loadMessage(string $id, string $domain): string|null
*/
private function getFromGlobals(string $id): string|null
{
// Split the ID into chunks allowing escaped dots (\.) and backslashes (\\)
preg_match_all('/(?:\\\\[\\\\.]|[^.])++/', $id, $matches);

$parts = preg_replace('/\\\\([\\\\.])/', '$1', $matches[0]);
$parts = LegacyGlobalsProcessor::getPartsFromKey($id);
$item = &$GLOBALS['TL_LANG'];

foreach ($parts as $part) {
Expand Down