first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace KupShop\I18nBundle\Util\AutomaticImport;
use KupShop\I18nBundle\Translations\ParametersListTranslation;
use KupShop\I18nBundle\Translations\ParametersTranslation;
use KupShop\I18nBundle\Translations\ProductsTranslation;
use KupShop\I18nBundle\Util\TranslationEngine;
use KupShop\I18nBundle\Util\TranslationLocator;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use Query\Operator;
class AutoTranslateUtil
{
private const AUTO_TRANSLATE_KEY = 'AUTO_TRANSLATE';
private array $translationsCache = [];
public function __construct(
private readonly TranslationEngine $translationEngine,
private readonly TranslationLocator $translationLocator,
) {
}
/**
* Fce ktera preklada zakladni data produktu.
*
* Vezme `$product` a projde pole, ktere jsou prekladatelne a pokud je potreba, tak je prelozi.
*/
public function preprocessProductData(array $config, array $product): array
{
// ulozim si puvodni produkt, abych ho pripadne mohl pouzit pokud bych potreboval
$product['original'] = $product;
// nactu si vsechny fieldy, ktere jsou pro produkt prekladatelne
$translatableColumns = $this->translationLocator->getTranslation(ProductsTranslation::class)->getColumns();
$translatableData = array_filter($product, fn ($k) => in_array($k, array_keys($translatableColumns)), ARRAY_FILTER_USE_KEY);
// vsechno si prelozim do vychoziho jazyka - v tomhle jazuce totiz chci ukladat produkt do DB
$result = $this->translate($translatableData, $config['source'], $this->getDefaultLanguageId());
// prerazim data produktu vychozim jazykem
foreach ($result as $field => $value) {
$product[$field] = $value;
}
// a ted jdu nagenerovat preklady (ulozi se mi to do prekladovy tabulky)
foreach ($config['to'] as $toLanguage) {
if ($toLanguage === $this->getDefaultLanguageId()) {
continue;
}
// do vsech poli, kde chci spustit automaticky preklad, si ulozim AUTO_TRANSLATE hodnotu, kdy na konci aut. importu vsechny tyhle radky projdu a prelozim
// automaticky preklad se provede az pozde pomoci `processAutoTranslate` metody
$product['translations'][$toLanguage]['products'] = $toLanguage === $config['source'] ? $translatableData : array_map(fn ($x) => self::AUTO_TRANSLATE_KEY, $translatableData);
}
return $product;
}
/**
* Fce ktera preklada nazev parametru a hodnotu parametru.
*
* Bohuzel to musi byt oddelene od `preprocessProductData`, protoze zalozeni parametru a jeho hodnoty se vola uz
* behem parsovani dat, takze by mi to jinak zalozilo parametry a hodnoty v tom jazyce, ve kterym feed je.
*
* Tohle se zavola v ramci parsovani feedu a pripravi to data parametru. Provede se preklad parametru a hodnoty do vychoziho
* jazyka a zaroven se nageneruji zaznamy do prekladove tabulky.
*/
public function preprocessParameterData(array $config, string $parameterName, string $parameterValue, array &$product): array
{
// provedu preklad parametru a hodnoty do vychoziho jazyka
$result = $this->translate(['name' => $parameterName, 'value' => $parameterValue], $config['source'], $this->getDefaultLanguageId());
foreach ($config['to'] as $toLanguage) {
if ($toLanguage === $this->getDefaultLanguageId()) {
continue;
}
// generuju zaznamy pro prekladove tabulky parametru
// pokud jeste neexistuje definice pro dany nazev parametru, tak si ji vytvorim at pak muzu pridavat jen hodnoty
if (!isset($product['translations'][$toLanguage]['parameters'][$result['name']])) {
$product['translations'][$toLanguage]['parameters'][$result['name']] = [
'name' => $toLanguage === $config['source'] ? $parameterName : self::AUTO_TRANSLATE_KEY,
'values' => [],
];
}
// pokud jeste neexistuje dana hodnota, tak ji pridam do `values` s AUTO_TRANSLATE, kdy preklad se provede az pozdeji pomoci `processAutoTranslate`
if (!isset($product['translations'][$toLanguage]['parameters'][$result['name']]['values'][$result['value']])) {
$product['translations'][$toLanguage]['parameters'][$result['name']]['values'][$result['value']] = $toLanguage === $config['source'] ? $parameterValue : self::AUTO_TRANSLATE_KEY;
}
}
// vracim parametr a hodnotu ve vychozim jazyce
return [$result['name'], $result['value']];
}
public function processAutoTranslate(): void
{
$this->processAutoTranslateForTranslation(ProductsTranslation::class);
$this->processAutoTranslateForTranslation(ParametersTranslation::class);
$this->processAutoTranslateForTranslation(ParametersListTranslation::class);
}
/**
* Fce ktera automaticky prelozi zaznamy s `AUTO_TRANSLATE_KEY` v prekladove tabulce.
*/
protected function processAutoTranslateForTranslation(string $translationClass): void
{
$translation = $this->translationLocator->getTranslation($translationClass);
// query builder pro nacteni prekladovych zaznamu
$qb = sqlQueryBuilder()
->select('o.id, t.id_language')
->from($translation->getTableName(), 'o')
->join('o', $translation->getTranslationTableName(), 't', "o.id = t.{$translation->getForeignKeyColumn()}")
->groupBy('o.id, t.id_language');
$orX = [];
// zaselectuju si vsechny prekladove sloupce
foreach ($translation->getColumns() as $column => $_) {
// potrebuju sloupec od originalniho objektu (ve vychozim jazyce)
// a potrebuju sloupec z prekladu
$qb->addSelect("o.{$column}, t.{$column} as 'translated/{$column}'");
// zaroven chci selectovat jen takove radky, ktere obsahuji `AUTO_TRANSLATE_KEY`
$orX[] = Operator::equals(["t.{$column}" => self::AUTO_TRANSLATE_KEY]);
}
$qb->andWhere(Operator::orX($orX));
foreach ($qb->execute() as $item) {
// z radku s datama si zafiltruju vsechny pole, ktere chci prekladat (maji hodnotu AUTO_TRANSLATE_KEY)
// vznikne mi pole [['field' => null], ...]
$translatableColumns = Mapping::mapKeys(array_filter($item, fn ($x) => $x === self::AUTO_TRANSLATE_KEY), fn ($k, $v) => [explode('/', $k)[1], null]);
// ted uz chci provest preklad
$translated = $this->translate(
// tady jeste z $item vezmu vsechny original pole podle prekladatelnych poli v $translatableColumns
array_filter($item, fn ($k) => in_array($k, array_keys($translatableColumns)), ARRAY_FILTER_USE_KEY),
$this->getDefaultLanguageId(),
$item['id_language']
);
// ulozim preklad a mam hotovo
$translation->saveSingleObject($item['id_language'], $item['id'], $translated);
}
}
/**
* Fce pro provedeni prekladu hodnot.
*
* Generuje si i lokalni cache, aby se pro stejnou hodnotu nemusel volat preklad pres API vicekrat.
*/
private function translate(array $data, string $source, string $language): array
{
$translated = [];
$langKey = "{$source}-{$language}";
foreach ($data as $key => $value) {
// prazdnou hodnotu nedava smysl prekladat a nejak s ni pracovat
if (empty($value)) {
$translated[$key] = null;
continue;
}
// pokud je zdrojovy jazyk stejny jako jazyk do ktereho chci prekladat, tak nic neprekladam
if ($source === $language) {
$translated[$key] = $value;
continue;
}
// pokud mam preklad uz v lokalni cache, tak ho pouziju
if (!empty($this->translationsCache[$langKey][$value])) {
$translated[$key] = $this->translationsCache[$langKey][$value];
continue;
}
// ciselnou hodnotu nema smysl prekladat
if (is_numeric($value)) {
$this->translationsCache[$langKey][$value] = $value;
} else {
// zavolam preklad hodnoty a ulozim si ho do lokalni cache abych stejnou hodnotu neprekladal v jednom behu aut. importu vicekrat
$this->translationsCache[$langKey][$value] = $this->translationEngine->getTranslation($value, $source, $language)['translatedText'] ?? $value;
}
// do $translated ulozim prelozenou hodnotu
$translated[$key] = $this->translationsCache[$langKey][$value];
}
return $translated;
}
private function getDefaultLanguageId(): string
{
return Contexts::get(LanguageContext::class)->getDefaultId();
}
}

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace KupShop\I18nBundle\Util;
use KupShop\CatalogBundle\Util\ReviewsUtil;
use KupShop\I18nBundle\Translations\ReviewsTranslation;
use Query\Operator;
class AutomaticReviewsTranslation
{
public const LIMIT_REVIEWS_CHAR_LENGTH = 500;
/**
* @var ReviewsTranslation
*/
private $reviewsTranslation;
/**
* @var TranslationEngine
*/
private $translationEngine;
public function translateReviews(
string $toLanguage,
?int $limitReviews = null,
?string $fromLanguage = null,
): array {
if (empty($limitReviews)) {
$dbcfg = \Settings::getDefault();
$limitReviews = intval($dbcfg['automatic_translate']['limit_reviews']) ?? 10;
}
$translateCharacters = 0;
$addedReviews = [];
if ($reviews = $this->getReviewsForTranslation($toLanguage, $limitReviews, $fromLanguage)) {
foreach ($reviews as $review) {
if (!empty($addedReviews[$review['id_product']]) && (($review['actualCount'] + $addedReviews[$review['id_product']]) >= $limitReviews)) {
continue;
}
if (!empty($translate = $this->prepareDataForReview($review))) {
$translateData = $this->translationEngine->getTranslationMulti($translate, $review['id_language'], $toLanguage);
if (!empty($translateData) && $this->reviewsTranslation->saveSingleObject($toLanguage, $review['id'], $translateData)) {
!empty($addedReviews[$review['id_product']]) ? $addedReviews[$review['id_product']]++ : $addedReviews[$review['id_product']] = 1;
foreach ($translate as $text) {
$translateCharacters += strlen($text);
}
}
}
}
}
if ($responseReviews = $this->getReviewResponseForTranslation()) {
foreach ($responseReviews as $review) {
if (!empty($translate = $this->prepareDataForReview($review))) {
$translateData = $this->translationEngine->getTranslationMulti($translate, $review['id_language_from'], $review['id_language_to']);
if (!empty($translateData)) {
sqlQueryBuilder()->update('reviews_translations')->directValues([
'response' => $translateData['response'],
])->andWhere(Operator::equals(['id' => $review['id']]))->execute();
}
}
}
}
return ['characters' => $translateCharacters, 'add_reviews' => $addedReviews];
}
protected function getReviewResponseForTranslation(): array
{
return sqlQueryBuilder()->select('rt.id as id, r.id_language as id_language_from, rt.id_language as id_language_to, r.response')
->from('reviews_translations', 'rt')
->innerJoin('rt', 'reviews', 'r', 'r.id=rt.id_review')
->andWhere(Operator::isNotNull('r.response'))
->andWhere(Operator::isNull('rt.response'))->execute()->fetchAllAssociative();
}
public function getReviewsForTranslation(
string $toLanguage,
?int $limitReviews = null,
?string $fromLanguage = null,
) {
if (empty($limitReviews)) {
$dbcfg = \Settings::getDefault();
$limitReviews = $dbcfg['automatic_translate']['limit_reviews'] ?? 10;
}
$doNotTranslate = [ReviewsUtil::RANK_DECLINED, ReviewsUtil::RANK_UNCONFIRMED];
$subQuery = sqlQueryBuilder()->select('count(DISTINCT r2.id) as pocet')
->from('reviews', 'r2')
->leftJoin('r2', 'reviews_translations', 't', 'r2.id = t.id_review')
->where('r.id_product = r2.id_product')
->andWhere('t.id_language = :toLanguage or r2.id_language = :toLanguage')
->andWhere(Operator::not(Operator::inIntArray($doNotTranslate, 'r2.figure')))
->groupBy('id_product')
->having('pocet >= :limitReviews')
->setParameter('toLanguage', $toLanguage)
->setParameter('limitReviews', $limitReviews)
->setParameter('dontTranslate', $doNotTranslate)
->setParameter('limitChars', self::LIMIT_REVIEWS_CHAR_LENGTH);
$qb = sqlQueryBuilder()->select('r.id, r.id_product, r.pros, r.cons, r.summary, r.response, r.id_language')
->from('reviews', 'r')
->leftJoin('r', 'reviews_translations', 'rt', 'r.id=rt.id_review')
->andWhere(Operator::not(Operator::exists($subQuery)))
->andWhere(Operator::not(Operator::equals(['r.id_language' => $toLanguage])))
->andWhere(Operator::not(Operator::inIntArray($doNotTranslate, 'r.figure')))
->andWhere('IFNULL(CHAR_LENGTH(r.pros),0) < :limitChars')
->andWhere('IFNULL(CHAR_LENGTH(r.cons),0) < :limitChars')
->andWhere('IFNULL(CHAR_LENGTH(r.summary),0) < :limitChars')
->andWhere('IFNULL(CHAR_LENGTH(r.response),0) < :limitChars')
->andWhere('NOT EXISTS (SELECT 1
FROM reviews_translations rt2
WHERE rt2.id_review = r.id and rt2.id_language = :toLanguage)'
)
->andWhere(
'CHAR_LENGTH(CONCAT(IFNULL(r.pros,""),IFNULL(r.cons,""),IFNULL(r.summary,""),IFNULL(r.response,""))) > 1',
)
->setParameter('toLanguage', $toLanguage)
->setParameter('limitReviews', $limitReviews)
->setParameter('limitChars', self::LIMIT_REVIEWS_CHAR_LENGTH)
->addOrderBy('FIELD(r.figure, '.ReviewsUtil::RANK_CONFIRMED.','.ReviewsUtil::RANK_TOP.')', 'DESC')
->addOrderBy('r.date', 'DESC');
if ($fromLanguage) {
$qb->andWhere(Operator::equals(['r.id_language' => $fromLanguage]));
}
$actualCount = sqlQueryBuilder()->select('IFNULL(count(DISTINCT r2.id),0) as actualCount')
->from('reviews', 'r2')
->leftJoin('r2', 'reviews_translations', 't', 'r2.id = t.id_review')
->where('r.id_product = r2.id_product')
->andWhere('t.id_language = :toLanguage or r2.id_language = :toLanguage')
->andWhere(Operator::not(Operator::inIntArray($doNotTranslate, 'r2.figure')))
->groupBy('id_product');
$qb->addSubselect($actualCount, 'actualCount');
return $qb->execute()->fetchAllAssociative();
}
private function prepareDataForReview(array $review): array
{
$translate = [];
if (!empty($review['pros'])) {
$translate['pros'] = $review['pros'];
}
if (!empty($review['cons'])) {
$translate['cons'] = $review['cons'];
}
if (!empty($review['summary'])) {
$translate['summary'] = $review['summary'];
}
if (!empty($review['response'])) {
$translate['response'] = $review['response'];
}
return $translate;
}
/**
* @required
*/
public function setReviewsTranslation(ReviewsTranslation $reviewsTranslation)
{
$this->reviewsTranslation = $reviewsTranslation;
}
/**
* @required
*/
public function setTranslationEngine(TranslationEngine $translationEngine)
{
$this->translationEngine = $translationEngine;
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace KupShop\I18nBundle\Util;
use KupShop\KupShopBundle\Util\Compat\SymfonyBridge;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class IPGeoLocator
{
// caching server for geoPlugin server
protected $host;
// the default base currency
protected $currency = 'CZK';
protected $cache;
protected $proxyCache;
/**
* @var RequestStack
*/
protected $requestStack;
protected $logger;
public function __construct(RequestStack $requestStack, LoggerInterface $logger)
{
$this->requestStack = $requestStack;
$this->logger = $logger;
$urlSuffix = '/geoip?ip={IP}&base_currency={CURRENCY}&id_shop={ID_SHOP}';
$this->host = (isRunningOnCluster() ? 'geoip.services' : 'http://geoip.wpj.cz').$urlSuffix;
}
/**
* Gets geographical info from users request. When proxy is enabled, it tries to take the info from the headers that the proxy set.
*
* @param $fullInfo bool when false, guaranteed IPGeoLocatorResult attributes are only ip, countryCode and currencyCode. When more information is needed (e.g. latitude/longitude), fullInfo parameter has to be true
*
* @return IPGeoLocatorResult
*/
public function getInfo(bool $fullInfo = false)
{
$result = null;
if (findModule(\Modules::PROXY_CACHE) && !$fullInfo) {
$result = $this->getInfoFromProxy();
}
if (!$result) {
$result = $this->getGeoLocatorInfo();
}
return $result;
}
protected function getGeoLocatorInfo()
{
if ($this->cache) {
return $this->cache;
}
$ip = $this->getClientIp();
$data = $this->locate($ip);
return $this->cache = $this->createResult($data);
}
protected function getInfoFromProxy()
{
if ($this->proxyCache) {
return $this->proxyCache;
}
$countryCode = $this->getRequest()->headers->get('Cdn-Requestcountrycode');
if (!$countryCode) {
return false;
}
$currencyCode = (new \NumberFormatter('en_'.$countryCode, \NumberFormatter::CURRENCY))->getTextAttribute(\NumberFormatter::CURRENCY_CODE);
if (!$currencyCode || $currencyCode == 'XXX') {
return false;
}
$data = [
'geoplugin_countryCode' => $countryCode,
'geoplugin_currencyCode' => $currencyCode,
'ip' => $this->getClientIp(),
];
return $this->proxyCache = $this->createResult($data);
}
protected function getClientIp()
{
return $this->getRequest()->getClientIp();
}
protected function getRequest()
{
$request = $this->requestStack->getMainRequest();
if (is_null($request)) {
$request = SymfonyBridge::getCurrentRequest();
}
return $request;
}
protected function fetch($host)
{
// use cURL to fetch data
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $host);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_USERAGENT, 'geoPlugin PHP Class v1.0');
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1000);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
protected function locate($ip)
{
$host = str_replace('{IP}', $ip, $this->host);
$host = str_replace('{CURRENCY}', $this->currency, $host);
$host = str_replace('{ID_SHOP}', getShopUniqueName(), $host);
$response = $this->fetch($host);
return json_decode($response, true);
}
/**
* @param $data array
*
* @return IPGeoLocatorResult
*/
protected function createResult($data)
{
$result = new IPGeoLocatorResult();
// set the geoPlugin vars
$result->ip = $data['ip'] ?? null;
$result->city = $data['geoplugin_city'] ?? null;
$result->region = $data['geoplugin_region'] ?? null;
$result->areaCode = $data['geoplugin_areaCode'] ?? null;
$result->dmaCode = $data['geoplugin_dmaCode'] ?? null;
$result->countryCode = $data['geoplugin_countryCode'] ?? null;
$result->countryName = $data['geoplugin_countryName'] ?? null;
$result->continentCode = $data['geoplugin_continentCode'] ?? null;
$result->latitude = $data['geoplugin_latitude'] ?? null;
$result->longitude = $data['geoplugin_longitude'] ?? null;
$result->currencyCode = $data['geoplugin_currencyCode'] ?? null;
$result->currencySymbol = $data['geoplugin_currencySymbol'] ?? null;
$result->currencyConverter = $data['geoplugin_currencyConverter'] ?? null;
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace KupShop\I18nBundle\Util;
class IPGeoLocatorResult
{
// initiate the geoPlugin vars
public $ip;
public $city;
public $region;
public $areaCode;
public $dmaCode;
public $countryCode;
public $countryName;
public $continentCode;
public $latitude;
public $longitude;
public $currencyCode;
public $currencySymbol;
public $currencyConverter;
}

View File

@@ -0,0 +1,81 @@
<?php
namespace KupShop\I18nBundle\Util;
use KupShop\I18nBundle\Entity\Language;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\Locale\LanguageSwitcher;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\Routing\RouterInterface;
class LanguageAwareRoutingCacheWarmer implements CacheWarmerInterface
{
/**
* @var LanguageContext
*/
private $languageContext;
/**
* @var RouterInterface
*/
private $router;
/**
* @var LanguageSwitcher
*/
private $languageSwitcher;
public function __construct(LanguageContext $languageContext, RouterInterface $router, LanguageSwitcher $languageSwitcher)
{
$this->languageContext = $languageContext;
$this->router = $router;
$this->languageSwitcher = $languageSwitcher;
}
/**
* Checks whether this warmer is optional or not.
*
* Optional warmers can be ignored on certain conditions.
*
* A warmer should return true if the cache can be
* generated incrementally and on-demand.
*
* @return bool true if the warmer is optional, false otherwise
*/
public function isOptional()
{
return true;
}
/**
* Warms up the cache.
*
* @param string $cacheDir The cache directory
*/
public function warmUp($cacheDir)
{
$languages = [];
try {
$languages = $this->languageContext->getSupported();
} catch (\Exception $e) {
// Try to take languages from env
if (getenv('LANGUAGES')) {
$languages = Mapping::mapKeys(explode(',', getenv('LANGUAGES')), function ($index, $lang) {
$language = new Language();
$language->setId($lang);
return [$lang, $language];
});
$reflection = new \ReflectionClass($this->languageContext);
$property = $reflection->getProperty('supported');
$property->setAccessible(true);
$property->setValue($this->languageContext, $languages);
}
}
foreach ($languages as $language) {
$this->router->generateInLanguage($language->getId(), 'home');
$this->router->matchInLanguage($language->getId(), '/');
}
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace KupShop\I18nBundle\Util\Locale;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\HTTPUtil;
use KupShop\KupShopBundle\Util\System\ControllerUtil;
use KupShop\KupShopBundle\Views\RouteAwareResponderInterface;
use KupShop\LocalePrefixBundle\Util\LocalePrefixUtil;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
class LanguageAwareUrlGenerator
{
public function __construct(
private readonly RouterInterface $router,
private readonly LanguageContext $languageContext,
private readonly ControllerUtil $controllerUtil,
private readonly ContextManager $contextManager,
private readonly RequestStack $requestStack,
private readonly ?LocalePrefixUtil $localePrefixUtil = null,
) {
}
public function generateInLanguage(string $language, string $path = '/'): string
{
$request = $this->createRequest($path);
return $this->withMainRequest($request, function () use ($request, $language) {
$fallback = true;
if ($match = $this->controllerUtil->getControllerByRequest($request)) {
if ($url['path'] = $this->getCorrectUrlFromController($request, $match, $language)) {
return HTTPUtil::http_build_url($url);
}
$fallback = false;
try {
$url['path'] = $pathInLanguage = $this->router->generateInLanguage($language, $match['_route'], $match['_route_params']);
$url = HTTPUtil::http_build_url($url);
if (!$url || empty($pathInLanguage)) {
$fallback = true;
}
} catch (\Exception) {
$fallback = true;
}
}
if ($fallback) {
$url = $this->router->generateInLanguage($language, 'home');
}
$this->languageContext->activate($language);
return $url;
});
}
private function getCorrectUrlFromController(Request $request, array $match, string $language): ?string
{
$controller = $this->controllerUtil->instantiateController($match['_controller']);
$controllerReturn = clone $this->controllerUtil->callController($request, $controller);
if (!($controllerReturn instanceof RouteAwareResponderInterface)) {
return null;
}
try {
$url = $this->contextManager->activateContexts(
[LanguageContext::class => $language],
fn (): ?string => $controllerReturn->getCorrectUrl(),
);
} catch (NotFoundHttpException) {
$url = null;
}
return $url === null ? $this->router->generateInLanguage($language, 'home') : $url;
}
private function createRequest(string $uri): Request
{
$request = Request::create($uri);
$request->attributes->set('path', ltrim(parse_url($uri)['path'] ?? '/', '/'));
if (findModule(\Modules::LOCALE_PREFIX)) {
$this->localePrefixUtil->setRequestAttribute($request);
}
if ($this->requestStack->getMainRequest()->hasSession()) {
$request->setSession(
$this->requestStack->getMainRequest()->getSession()
);
}
return $request;
}
private function withMainRequest(Request $request, callable $fn): mixed
{
$prevRequests = [];
while ($this->requestStack->getCurrentRequest()) {
// no-op: intentionally discarding all requests so request in args will be main request
$prevRequests[] = $this->requestStack->pop();
}
// reverse, so last pop request is first
$prevRequests = array_reverse($prevRequests);
// push arg request to requests so it becomes main request
$this->requestStack->push($request);
$result = $fn();
// remove arg request
$this->requestStack->pop();
// restore prev requests
foreach ($prevRequests as $prevRequest) {
$this->requestStack->push($prevRequest);
}
return $result;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace KupShop\I18nBundle\Util;
use KupShop\I18nBundle\Entity\Currency;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Contexts;
class PriceConverter
{
protected $currencies = [];
public function convert($fromCurrency, $toCurrency, $price): \Decimal
{
return $this->convertPrice(
$this->getCurrency($fromCurrency),
$this->getCurrency($toCurrency),
\Decimal::create($price)
);
}
public function convertPrice(Currency $fromCurrency, Currency $toCurrency, \Decimal $price): \Decimal
{
if ($fromCurrency->getId() == $toCurrency->getId()) {
return $price;
}
$price = $price->mul($fromCurrency->getRate());
$price = $price->div($toCurrency->getRate());
return $price;
}
public function getCurrency($currency): ?Currency
{
if ($currency instanceof Currency) {
return $currency;
}
return $this->getCurrencies()[$currency] ?? null;
}
private function getCurrencies(): array
{
if (empty($this->currencies)) {
$this->currencies = Contexts::get(CurrencyContext::class)->getAll();
}
return $this->currencies;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace KupShop\I18nBundle\Util;
use KupShop\I18nBundle\Translations\TranslationCustomDataInterface;
trait TranslationCustomDataTrait
{
protected $customDataColumns = [];
public function getColumns(): array
{
$columns = $this->columns;
if ($this instanceof TranslationCustomDataInterface) {
$columns['data'] = [];
}
return $columns;
}
public function getCustomDataColumns(): array
{
return $this->customDataColumns;
}
public function handleSaveCustomData($customData, $actualData, &$values): bool
{
$data = array_filter(array_merge(json_decode($actualData, true) ?: [], $customData));
$values['data'] = !empty($data) ? json_encode($data) : null;
unset($values['custom_data']);
return true;
}
public function setCustomDataColumns($columns)
{
if (!is_array($columns)) {
$columns = [];
}
$customDataColumns = array_merge($this->customDataColumns, $columns);
foreach ($customDataColumns as $column => &$config) {
$config['custom_data'] = true;
$config['field'] = 'data';
}
$this->customDataColumns = $customDataColumns;
}
}

View File

@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace KupShop\I18nBundle\Util;
use DeepL\DeepLException;
use DeepL\Translator;
class TranslationEngine
{
private const GOOGLE_CHUNK = 3900;
private static string $googleApiKey = 'AIzaSyCERmGVBqiuzN74C9l-CEMA08KwIgSQY_Y';
private static string $deepLAuthKey = '491e37c3-4dc9-ed7b-7188-536b0598f034';
public function getTranslation(string $text, string $sourceLanguage, string $language, bool $html = false): array
{
if (isFunctionalTests()) {
$responseData['translatedText'] = $sourceLanguage.'->'.$language.': '.$text;
return $responseData;
}
$deepl = (\Settings::getDefault()->use_deepl ?? 'N');
getLogger()->notice('TranslationEngine:translate', ['char_count' => mb_strlen($text), 'from' => $sourceLanguage, 'to' => $language, 'deepl' => $deepl]);
if ($deepl == 'Y') {
return $this->getTranslationWithDeepL($text, $sourceLanguage, $language, $html);
}
return $this->getTranslationWithGoogle($text, $sourceLanguage, $language);
}
public function getTranslationMulti(array $textMulti, string $sourceLanguage, string $language): array
{
foreach ($textMulti as $key => $text) {
if (empty($text)) {
continue;
}
$translation = $this->getTranslation($text, $sourceLanguage, $language);
if (empty($translation['error'])) {
$textMulti[$key] = $translation['translatedText'];
} else {
unset($textMulti[$key]);
}
}
return $textMulti;
}
private function getTranslationWithGoogle(string $text, string $sourceLanguage, string $language): array
{
// Google Translate nezná de-AT nebo de-CH, zná jen de-DE
// potřebuju to teda, aby se použila němčina - jinak to nefunguje vubec
$langAlias = [
'at' => 'de',
'ch' => 'de',
];
foreach ([&$sourceLanguage, &$language] as &$lang) {
if (isset($langAlias[$lang])) {
$lang = $langAlias[$lang];
}
}
$baseUrl = 'https://www.googleapis.com/language/translate/v2?key='.self::$googleApiKey; // .'&q='.rawurlencode($text).'&source='.$sourceLanguage.'&target='.$language
$textChunks = $this->splitTextIntoChunks($text);
$translatedChunks = [];
foreach ($textChunks as $chunk) {
$url = $baseUrl.'&q='.rawurlencode($chunk).'&source='.$sourceLanguage.'&target='.$language;
$handle = curl_init($url);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($handle);
$responseData = json_decode($response ?: '{}', true);
if (isset($responseData['error']) || curl_error($handle)) {
curl_close($handle);
return [
'error' => curl_error($handle) ?: ($responseData['error'] ?? 'Unknown error'),
'translatedText' => '',
];
}
$translatedChunks[] = html_entity_decode($responseData['data']['translations'][0]['translatedText'] ?? '', ENT_QUOTES);
curl_close($handle);
}
// Combine all translated chunks into a single result
$translatedText = implode(' ', $translatedChunks);
// Replace malformed sequences
$translatedText = str_replace(
['> , ', '> .'],
['>, ', '>.'],
$translatedText
);
return [
'error' => '',
'translatedText' => $translatedText,
];
}
private function getTranslationWithDeepL(string $text, string $sourceLanguage, string $language, bool $html = false): array
{
$responseData = ['translatedText' => ''];
try {
$translator = new Translator(self::$deepLAuthKey);
$language = $this->deeplLanguageMapping($language);
$sourceLanguage = $this->deeplLanguageMapping($sourceLanguage, true);
$options = [];
if ($html) {
$options['tag_handling'] = 'html';
$options['non_splitting_tags'] = ['strong', 'em', 'b', 'i'];
}
$result = $translator->translateText($text, $sourceLanguage, $language, $options);
if (!empty($result->text)) {
$responseData['translatedText'] = $result->text;
}
} catch (DeepLException $exception) {
$responseData['error'] = $exception->getMessage();
}
return $responseData;
}
protected function deeplLanguageMapping(string $language, bool $source = false): string
{
switch ($language) {
case 'en':
// if source language is 'en' => keep it 'en' otherwise use 'en-US'
return $source ? 'en' : 'en-US';
case 'at':
return 'de';
}
return $language;
}
protected function splitTextIntoChunks(string $text): array
{
$chunks = [];
$currentChunk = '';
// Split text into words to preserve boundaries
$words = preg_split('/(\s+)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($words as $word) {
// Add word to the current chunk if it fits
if (mb_strlen($currentChunk.$word) <= self::GOOGLE_CHUNK) {
$currentChunk .= $word;
} else {
// Save the current chunk and start a new one
$chunks[] = trim($currentChunk);
$currentChunk = $word;
}
}
// Add the remaining chunk
if (!empty($currentChunk)) {
$chunks[] = trim($currentChunk);
}
return $chunks;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace KupShop\I18nBundle\Util;
use KupShop\I18nBundle\Exception\TranslationException;
use KupShop\I18nBundle\Translations\BaseTranslation;
use Symfony\Component\DependencyInjection\ServiceLocator;
class TranslationLocator
{
private $locator;
private $servicesList;
public function __construct(ServiceLocator $locator, array $servicesList)
{
$this->locator = $locator;
$this->servicesList = $servicesList;
}
public function getTranslations(): array
{
$result = [];
foreach ($this->servicesList as $service) {
$shortName = explode('\\', $service);
$shortName = end($shortName);
$result[$shortName] = $service;
}
return $result;
}
public function getTranslation(string $class): BaseTranslation
{
if (!$this->locator->has($class)) {
throw new TranslationException('Translation class not found');
}
return $this->locator->get($class);
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace KupShop\I18nBundle\Util;
use KupShop\ContentBundle\View\RedirectView;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use Query\Operator;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class TranslationUtil
{
public const FIGURE_INDETERMINATE = 'indeterminate';
public function __construct(
private readonly TranslationLocator $translationLocator,
private readonly RedirectView $redirectView,
) {
$this->redirectView->setLinkPrefixChangeLanguage(false);
}
public function updateTranslationsFigure(string $translationClass, int $objectId, array $figureData, $figureField = 'figure'): bool
{
$translation = $this->translationLocator->getTranslation($translationClass);
foreach ($figureData as $langId => $value) {
$translation->saveSingleObject(
$langId,
$objectId,
[$figureField => $value === self::FIGURE_INDETERMINATE ? null : $value]
);
}
return true;
}
public function getTranslationsFigure(string $translationClass, mixed $objectIDs, $figureField = 'figure'): array
{
$objectId = $objectIDs;
if (!is_array($objectId)) {
$objectId = [$objectId];
}
$objectId = array_filter($objectId);
if (empty($objectId)) {
return [];
}
$translation = $this->translationLocator->getTranslation($translationClass);
$columnId = "t.{$translation->getForeignKeyColumn()}";
$qb = sqlQueryBuilder()
->select("{$columnId} as id", 't.id_language', "t.{$figureField} figure")
->from($translation->getTranslationTableName(), 't')
->where(Operator::inIntArray($objectId, $columnId))
->groupBy($columnId, 't.id_language');
$result = [];
foreach ($qb->execute() as $item) {
$result[$item['id']][$item['id_language']] = $item['figure'] !== null ? $item['figure'] : self::FIGURE_INDETERMINATE;
}
// doplnim chybejici jazyky (nemusi existovat radek s prekladem, ale ja potrebuju mit nastavenou INDETERMINATE hodnotu)
foreach ($objectId as $id) {
foreach ($this->getTranslationLanguages() as $langId) {
if (!isset($result[$id][$langId])) {
$result[$id][$langId] = self::FIGURE_INDETERMINATE;
}
}
}
if (!is_array($objectIDs)) {
return $result[$objectIDs] ?? [];
}
return $result;
}
public function getTranslationLanguages(): array
{
$languageContext = Contexts::get(LanguageContext::class);
return array_filter(array_map(fn ($x) => $x->getId(), $languageContext->getSupported()), fn ($x) => $x !== $languageContext->getDefaultId());
}
/**
* @return string translated link URL or <b>empty string on failure</b>
*/
public function translateLink(string $language, string $objectType, mixed $objectID): string
{
$this->redirectView
->setLang($language)
->setType($objectType)
->setId($objectID);
try {
return $this->redirectView->getUrl();
} catch (NotFoundHttpException) {
return '';
}
}
public function translateLinksInHTML(string $language, string $html, ?array &$errors = null): string
{
libxml_use_internal_errors(true);
$dom = new \DOMDocument();
$dom->loadHTML('<?xml encoding="UTF-8"><div content>'.$html.'</div>', LIBXML_HTML_NODEFDTD);
$xpath = new \DOMXPath($dom);
$nodeList = $xpath->query('//a[@data-id][@data-type]');
foreach ($nodeList ?: [] as $anchor) {
if (!($anchor instanceof \DOMElement)) {
continue;
}
$type = $anchor->getAttribute('data-type');
$id = $anchor->getAttribute('data-id');
$link = $this->translateLink($language, $type, $id);
$anchor->setAttribute('href', $link);
}
$innerHTML = '';
foreach ($xpath->query('//div[@content]')->item(0)->childNodes as $childNode) {
$innerHTML .= $dom->saveHTML($childNode);
}
$errors = libxml_get_errors();
libxml_use_internal_errors(false);
return $innerHTML;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace KupShop\I18nBundle\Util;
class TranslationsMenuUtil
{
private $translationLocator;
public function __construct(TranslationLocator $translationLocator)
{
$this->translationLocator = $translationLocator;
}
public function getTranslationsMenu()
{
$menuCls = new \AdminBarMenu();
$menu = $menuCls->getMenu();
$menu = array_combine(array_column($menu, 'name'), $menu);
$translate_menu = $menu['translate']['submenu'];
$translate_menu[] = ['name' => 'translate.translateSlidersImages'];
$this->getTranslationsClasses($translate_menu);
return $translate_menu;
}
protected function getTranslationsClasses(&$menu)
{
$translations = $this->translationLocator->getTranslations();
foreach ($menu as $key => &$menu_item) {
if (($menu_item['name'] == 'translationsStats') || ($menu_item['name'] == 'languageCheck') || ($menu_item['name'] == 'languageCheckAdmin')) {
unset($menu[$key]);
continue;
}
if (!empty($menu_item['submenu'])) {
$this->getTranslationsClasses($menu_item['submenu']);
} else {
$listType = str_replace('translate.', '', $menu_item['name']);
$className = str_replace('translate.translate', '', $menu_item['name']).'Translation';
$menu_item['listType'] = $listType;
$menu_item['className'] = $translations[$className] ?? '';
}
}
return $menu;
}
public function getTranslationsServices()
{
return $this->translationLocator->getTranslations();
}
}