<?php
namespace App\Shared\Infrastructure\Translation;
use App\Infraestructure\Service\Translator\TranslationService;
use App\Shared\Infrastructure\ChatGpt\ChatGptService;
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
use Doctrine\Persistence\Mapping\MappingException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class DynamicTranslatorDecorator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
{
const TRANSLATIONS_CACHE = '/translation_cache.json';
private $translatorCache;
private $domainBdCache = [];
public function __construct(
private TranslatorInterface $translator,
private TranslationService $translationService,
private ChatGptService $chatGptService,
private $projectDir,
private $repositories
)
{
if (!($translator instanceof TranslatorBagInterface && $translator instanceof LocaleAwareInterface)) {
throw new \InvalidArgumentException('The translator must implement TranslatorBagInterface and LocaleAwareInterface');
}
$fileTempTranslations = @file_get_contents($this->projectDir. self::TRANSLATIONS_CACHE);
$this->translatorCache = $fileTempTranslations ? json_decode($fileTempTranslations, true) : [];
}
public function trans($id, array $parameters = [], $domain = "messages", $locale = null) {
$fallbackLocale = $_ENV['LOCALE'] ?? 'es';
$locale = $locale ?? $this->translator->getLocale();
$domain = $this->correctDomain($domain);
$translation = $this->translator->trans($id, $parameters, $domain, $locale);
$isUsingFallback = $this->isUsingFallback($id, $locale, $fallbackLocale, $domain);
$isTranslationMissing = $this->isTranslationMissing($id, $locale, $fallbackLocale, $domain);
if (($isUsingFallback or $isTranslationMissing) and in_array($locale, ['es', 'eu'])) {
$translation = $this->handleLocaleSpecificTranslation($id, $parameters, $domain, $locale, $fallbackLocale);
$this->updateTranslationCache($id, $translation, $locale, $domain);
}
return $translation;
}
private function handleLocaleSpecificTranslation($id, $parameters, $domain, $locale, $fallbackLocale) {
$isUsingFallback = $this->isUsingFallback($id, $locale, $fallbackLocale, $domain);
$isTranslationMissing = $this->isTranslationMissing($id, $locale, $fallbackLocale, $domain);
if ($traduccion = $this->fetchFallbackFromCache($id, $domain, $locale)) {
return $traduccion;
}
if ($locale === "eu") {
$sourceCache = $this->fetchFallbackFromCache($id, $domain, $fallbackLocale);
if (!$sourceCache and $isTranslationMissing){ }
else {
$source = $sourceCache ?? $this->translator->trans($id, $parameters, $domain, $fallbackLocale);
if ($traduccion = $this->translationService->translate($source, "es-eu")) return $traduccion;
}
} elseif ($locale === "es") {
if ($traduccion = $this->fetchFallbackFromDatabase($id, $locale, $domain)) {
return $traduccion;
}
elseif (
preg_match("/((^|\.)label\.|(^|\.)placeholder\.|(^|\.)form\.|^document_)/i", $id)
and $traduccion = $this->fetchChatGptTranslation($id)
) {
return $traduccion;
}
}
return $this->translator->trans($id, $parameters, $domain, $locale);
}
private function fetchFallbackFromCache($id, $domain, $locale)
{
try {
$traducciones = array_filter($this->translatorCache, fn($t) => $t['source'] == $id and $t['domain'] == $domain and $t['locale'] == $locale);
if (count($traducciones) > 0) {
return reset($traducciones)["target"];
} else {
return null;
}
} catch (\Exception $e) {}
}
private function correctDomain($domain) {
return strpos($domain, "xml") !== false || $domain === null ? "messages" : $domain;
}
private function updateTranslationCache($source, $target, $locale, $domain) {
if ($this->fetchFallbackFromCache($source, $domain, $locale)) { return; }
if ($source == $target or !$target) { return; }
$this->translatorCache[] = ['source' => $source, 'target' => $target, 'locale' => $locale, 'domain' => $domain, 'save' => true];
$translatorCacheFilter = array_filter($this->translatorCache, fn($t) => $t['save']);
file_put_contents($this->projectDir. self::TRANSLATIONS_CACHE, json_encode($translatorCacheFilter));
}
private function isTranslationAvailable($id, $locale, $domain = 'messages'): bool
{
$catalogue = $this->translator->getCatalogue($locale);
return $catalogue->has($id, $domain);
}
private function isUsingFallback($id, $locale, $fallbackLocale, $domain = 'messages'): bool
{
$catalogue = $this->translator->getCatalogue($locale);
if ($this->hasInCurrentCatalogue($catalogue, $id, $domain)) {
return false;
}
$fallbackCatalogue = $catalogue->getFallbackCatalogue();
if ($fallbackCatalogue && $this->hasInCurrentCatalogue($fallbackCatalogue, $id, $domain)) {
return true;
}
return false;
}
private function hasInCurrentCatalogue($catalogue, $id, $domain)
{
return isset($catalogue->all($domain)[$id]);
}
private function isTranslationMissing($id, $locale, $fallbackLocale, $domain = 'messages'): bool
{
return !$this->isTranslationAvailable($id, $locale, $domain) && !$this->isTranslationAvailable($id, $fallbackLocale, $domain);
}
public function getCatalogue($locale = null)
{
return $this->translator->getCatalogue($locale);
}
public function setLocale($locale)
{
$this->translator->setLocale($locale);
}
public function getLocale()
{
return $this->translator->getLocale();
}
private function fetchChatGptTranslation($id)
{
try {
$prompt = <<<PROMPT
For the string '$id', give me a label translate in Spanish.
The label's description starts from the last period onwards; the rest is not useful.
The response will be in a JSON object in the following format:
{
['translate' => //translation in Spanish of the label, 'observations' => 'XXXXXX']
}
PROMPT;
$traduccionResponse = $this->chatGptService->__invoke($prompt, true);
if (!isset($traduccionResponse['translate'])) {
$traduccionResponse = reset($traduccionResponse);
}
$traduccion = @ucfirst($traduccionResponse['translate']) ?? null;
} catch (\Exception $e) {}
return $traduccion;
}
private function fetchFallbackFromDatabase($source, $locale, $domain)
{
if ($locale === 'es' and !isset($this->domainBdCache[$domain])) {
$fs = new Filesystem();
$metadata = null;
foreach ($this->repositories as $repository) {
try {
//TODO Tiene daniel en load escuchando un handler y produce que el tramite aleatoria lanza la notificacion
// if (!($object = $repository->findOneBy(['translationDomain' => $domain]))) continue;
// $metadata = $object->getMetadata();
break;
} catch (UnrecognizedField|MappingException $e) {
}
}
if (isset($metadata)) {
$filePath = $this->projectDir . "/translations/$domain.$locale.xlf";
$fs->dumpFile($filePath, $metadata->getTranslations());
}
if ($fs->exists($filePath)) {
$doc = new \DOMDocument();
$doc->load($filePath);
foreach ($doc->getElementsByTagName('trans-unit') as $transUnit) {
$this->translatorCache[] = [
'source' => $transUnit->getElementsByTagName('source')->item(0)->nodeValue,
'target' => $transUnit->getElementsByTagName('target')->item(0)->nodeValue,
'locale' => $locale,
'domain' => $domain,
'save' => false
];
}
$this->domainBdCache[$domain] = true;
}
}
return $this->fetchFallbackFromCache($source, $domain, $locale);
}
}