| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/includes/ |
Upload File : |
<?php
declare(strict_types=1);
/** V14 — shared glossary protection, server-side only; no secret keys. */
function p14_builtin_terms(): array {
return [
'Moja Polana Smaków','Mojej Polany Smaków','Moją Polanę Smaków','Polana Smaków','Polany Smaków','Andrzej Polan','Andrzeja Polana','Andrzejem Polanem','Andrzejowi Polanowi',
'Bożego Ciała','ul. Bożego Ciała','ulica Bożego Ciała','ulicy Bożego Ciała',
'Kazimierz','Kazimierza','Kazimierzu','Kazimierzem',
'Kraków','Krakowa','Krakowie','Krakowem',
'Winnica Wieliczka','Wieliczka','Małopolska','Małopolsce',
'Krakowskie Przedmieście','Cydr Bzik', 'Cuvee Regis',
];
}
function p14_schema(): void {
db()->exec("CREATE TABLE IF NOT EXISTS polana_v14_protected_terms (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
term VARCHAR(250) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_term(term)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
}
function p14_terms(): array {
static $cache=null;
if(is_array($cache))return $cache;
$terms=p14_builtin_terms();
// Installed custom terms are optional; the built-in protection works without schema migration.
try{
foreach(db()->query('SELECT term FROM polana_v14_protected_terms') as $row){
$term=trim((string)$row['term']);if($term!=='')$terms[]=$term;
}
}catch(Throwable $e){/* Installer enables custom terms; do not fail existing translations. */}
$terms=array_values(array_unique($terms));
usort($terms,static fn($a,$b)=>mb_strlen($b,'UTF-8')<=>mb_strlen($a,'UTF-8'));
return $cache=$terms;
}
function p14_found_terms(string $text): array {
$out=[];
foreach(p14_terms() as $term){
if(preg_match('~(?<![\\p{L}\\p{N}])'.preg_quote($term,'~').'(?![\\p{L}\\p{N}])~iu',$text))$out[]=$term;
}
return $out;
}
function p14_missing_terms(string $original,string $translation): array {
$missing=[];
foreach(p14_found_terms($original) as $term){
if(!preg_match('~(?<![\\p{L}\\p{N}])'.preg_quote($term,'~').'(?![\\p{L}\\p{N}])~iu',$translation))$missing[]=$term;
}
return $missing;
}
function p14_translate(TklmTranslateClient $api,string $text,string $lang): string {
if(!in_array($lang,['en','de','es'],true))throw new InvalidArgumentException('Język nieobsługiwany');
if(trim($text)==='')return '';
$terms=p14_terms();
// Protect longest names first; ordinary words such as "ulica", "restauracja" remain translatable.
$street='(?:ul\\.|ulica|ulicy|al\\.|aleja|alei)\\s+[A-ZĄĆĘŁŃÓŚŹŻ][\\p{L}\\p{M}.\\x27-]{1,}(?:\\s+[A-ZĄĆĘŁŃÓŚŹŻ][\\p{L}\\p{M}.\\x27-]{1,}){0,3}(?:\\s+\\d+[A-Za-z]?)?';
$pattern='~(?<![\\p{L}\\p{N}])(?:'.$street.'|'.implode('|',array_map(static fn($s)=>preg_quote($s,'~'),$terms)).')(?![\\p{L}\\p{N}])~iu';
$map=[];$index=0;
$protected=preg_replace_callback($pattern,static function($m)use(&$map,&$index){
++$index;$token='ZXQTKLM'.sprintf('%05d',$index).'ZXQ';
$map[$token]=$m[0];return $token;
},$text);
if(!is_string($protected))throw new RuntimeException('Błąd kodowania nazwy własnej');
// Also keep web addresses and e-mails byte-for-byte, never translate them.
$protected=preg_replace_callback('~https?://[^\\s<>"\']+|[\\p{L}\\p{N}._%+\\-]+@[\\p{L}\\p{N}.\\-]+\\.[A-Za-z]{2,}~iu',
static function($m)use(&$map,&$index){
++$index;$token='ZXQTKLM'.sprintf('%05d',$index).'ZXQ';$map[$token]=$m[0];return $token;
},$protected);
if(!is_string($protected))throw new RuntimeException('Błąd ochrony adresu');
// A stand-alone proper name must not be sent to a translator at all.
if(preg_match('~^(?:\\s*ZXQTKLM\\d{5}ZXQ\\s*[,;.:\-–—]?\\s*)+$~u',$protected))return $text;
$answer=$api->translate($protected,'pl',$lang);
$result=trim((string)($answer['translatedText']??''));
if($result==='')throw new RuntimeException('Puste tłumaczenie');
foreach($map as $token=>$original){
// Fail closed: do not write a result when the translation engine changes a placeholder.
$matches=[];
$n=preg_match_all('~'.preg_quote($token,'~').'~i',$result,$matches);
if($n!==1){
// Translators sometimes change protected markers; do NOT save a damaged result.
// Translate separately around protected names, restoring them byte-for-byte.
return p14_translate_segments($api,$protected,$map,$lang);
}
$result=preg_replace_callback('~'.preg_quote($token,'~').'~i',static fn($m)=>$original,$result,1);
if(!is_string($result))throw new RuntimeException('Błąd przywrócenia nazwy');
}
return trim($result);
}
/** Safe fallback when engine modifies placeholders: translate only spans between names. */
function p14_translate_segments(TklmTranslateClient $api,string $protected,array $map,string $lang):string {
if(!$map)throw new RuntimeException('Nie udało się zachować tłumaczenia');
$parts=preg_split('/(ZXQTKLM\d{5}ZXQ)/u',$protected,-1,PREG_SPLIT_DELIM_CAPTURE);
if(!is_array($parts))throw new RuntimeException('Niepoprawny tekst do ponownego tłumaczenia');
if(count($parts)>25)throw new RuntimeException('Zbyt dużo nazw chronionych w jednej frazie — wymagana korekta ręczna');
$output='';
foreach($parts as $piece){
if(isset($map[$piece])){$output.=$map[$piece];continue;}
if($piece==='')continue;
if(!preg_match('/\p{L}/u',$piece)){$output.=$piece;continue;}
if(strlen($piece)>4500)throw new RuntimeException('Za długi fragment przy zabezpieczaniu nazw własnych');
preg_match('/^(\s*)(.*?)(\s*)$/us',$piece,$m);
$body=trim((string)($m[2]??$piece));
if($body===''){$output.=$piece;continue;}
$response=$api->translate($body,'pl',$lang);
$result=trim((string)($response['translatedText']??''));
if($result==='')throw new RuntimeException('API nie zwróciło tłumaczenia fragmentu');
$output.=($m[1]??'').$result.($m[3]??'');
}
foreach($map as $name){if(!str_contains($output,$name))throw new RuntimeException('Nie udało się odtworzyć chronionej nazwy: '.$name);}
return trim($output);
}