first commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
178
bundles/KupShop/I18nBundle/Util/AutomaticReviewsTranslation.php
Normal file
178
bundles/KupShop/I18nBundle/Util/AutomaticReviewsTranslation.php
Normal 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;
|
||||
}
|
||||
}
|
||||
165
bundles/KupShop/I18nBundle/Util/IPGeoLocator.php
Normal file
165
bundles/KupShop/I18nBundle/Util/IPGeoLocator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
21
bundles/KupShop/I18nBundle/Util/IPGeoLocatorResult.php
Normal file
21
bundles/KupShop/I18nBundle/Util/IPGeoLocatorResult.php
Normal 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;
|
||||
}
|
||||
@@ -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(), '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
51
bundles/KupShop/I18nBundle/Util/PriceConverter.php
Normal file
51
bundles/KupShop/I18nBundle/Util/PriceConverter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
176
bundles/KupShop/I18nBundle/Util/TranslationEngine.php
Normal file
176
bundles/KupShop/I18nBundle/Util/TranslationEngine.php
Normal 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;
|
||||
}
|
||||
}
|
||||
41
bundles/KupShop/I18nBundle/Util/TranslationLocator.php
Normal file
41
bundles/KupShop/I18nBundle/Util/TranslationLocator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
140
bundles/KupShop/I18nBundle/Util/TranslationUtil.php
Normal file
140
bundles/KupShop/I18nBundle/Util/TranslationUtil.php
Normal 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;
|
||||
}
|
||||
}
|
||||
53
bundles/KupShop/I18nBundle/Util/TranslationsMenuUtil.php
Normal file
53
bundles/KupShop/I18nBundle/Util/TranslationsMenuUtil.php
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user