Files
kupshop/bundles/KupShop/I18nBundle/Util/AutomaticImport/AutoTranslateUtil.php
2025-08-02 16:30:27 +02:00

204 lines
8.9 KiB
PHP

<?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();
}
}