Cache website translations in Redis

This commit is contained in:
juvdiaz 2026-06-10 10:31:16 -06:00
parent a3b6c79664
commit c0b3eeef83
1 changed files with 193 additions and 8 deletions

View File

@ -18,6 +18,147 @@ function clean_translate_text(string $value, int $maxLength = 4000): string {
return $value;
}
function redis_encode_command(array $parts): string {
$command = '*' . count($parts) . "\r\n";
foreach ($parts as $part) {
$value = (string) $part;
$command .= '$' . strlen($value) . "\r\n" . $value . "\r\n";
}
return $command;
}
function redis_read_line($socket): ?string {
$line = fgets($socket);
if ($line === false) {
return null;
}
return rtrim($line, "\r\n");
}
function redis_read_response($socket): mixed {
$prefix = fgetc($socket);
if ($prefix === false) {
return null;
}
if ($prefix === '+') {
return redis_read_line($socket);
}
if ($prefix === '-' || $prefix === ':') {
redis_read_line($socket);
return null;
}
if ($prefix === '$') {
$lengthLine = redis_read_line($socket);
if ($lengthLine === null) {
return null;
}
$length = (int) $lengthLine;
if ($length < 0) {
return null;
}
$value = '';
while (strlen($value) < $length) {
$chunk = fread($socket, $length - strlen($value));
if ($chunk === false || $chunk === '') {
return null;
}
$value .= $chunk;
}
fread($socket, 2);
return $value;
}
if ($prefix === '*') {
$countLine = redis_read_line($socket);
if ($countLine === null) {
return null;
}
$count = (int) $countLine;
if ($count < 0) {
return null;
}
$items = [];
for ($i = 0; $i < $count; $i++) {
$items[] = redis_read_response($socket);
}
return $items;
}
return null;
}
function redis_connect(): mixed {
$host = trim((string) getenv('TRANSLATION_REDIS_HOST'));
if ($host === '') {
return null;
}
$port = (int) (getenv('TRANSLATION_REDIS_PORT') ?: 6379);
$socket = @fsockopen($host, $port, $errno, $errstr, 0.2);
if (!$socket) {
return null;
}
stream_set_timeout($socket, 1);
return $socket;
}
function translation_cache_key(string $prefix, string $model, string $targetLang, string $text): string {
$safePrefix = preg_replace('/[^a-zA-Z0-9:_-]/', '', $prefix) ?: 'translation:v1';
return $safePrefix . ':' . hash('sha256', $model) . ':' . $targetLang . ':' . hash('sha256', $text);
}
function translation_cache_read(array $keys): array {
if (!$keys) {
return [];
}
$socket = redis_connect();
if (!$socket) {
return [];
}
fwrite($socket, redis_encode_command(array_merge(['MGET'], $keys)));
$values = redis_read_response($socket);
fclose($socket);
if (!is_array($values) || count($values) !== count($keys)) {
return [];
}
$cached = [];
foreach ($keys as $index => $key) {
if (is_string($values[$index])) {
$cached[$key] = clean_translate_text($values[$index]);
}
}
return $cached;
}
function translation_cache_write(array $items): void {
if (!$items) {
return;
}
$ttl = (int) (getenv('TRANSLATION_CACHE_TTL_SECONDS') ?: 7776000);
if ($ttl < 300) {
$ttl = 300;
}
$socket = redis_connect();
if (!$socket) {
return;
}
foreach ($items as $key => $value) {
fwrite($socket, redis_encode_command(['SETEX', $key, (string) $ttl, $value]));
redis_read_response($socket);
}
fclose($socket);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
translate_response(405, ['error' => 'Method not allowed']);
}
@ -52,14 +193,47 @@ if (!$texts || count($texts) > 240 || $totalLength > 80000) {
translate_response(400, ['error' => 'Invalid translation batch']);
}
$ollamaHost = rtrim(getenv('OLLAMA_HOST') ?: 'http://192.168.100.68:11434', '/');
$ollamaModel = getenv('OLLAMA_MODEL') ?: 'llama3.2:3b';
$cachePrefix = getenv('TRANSLATION_CACHE_PREFIX') ?: 'translation:v1';
$cacheKeys = [];
foreach ($texts as $index => $text) {
$cacheKeys[$index] = translation_cache_key($cachePrefix, $ollamaModel, $targetLang, $text);
}
$cachedTranslations = translation_cache_read(array_values(array_unique($cacheKeys)));
$cleanTranslations = [];
$missTexts = [];
$missPositions = [];
foreach ($texts as $index => $text) {
$cacheKey = $cacheKeys[$index];
if (array_key_exists($cacheKey, $cachedTranslations)) {
$cleanTranslations[$index] = $cachedTranslations[$cacheKey];
continue;
}
$missPositions[] = $index;
$missTexts[] = $text;
}
if (!$missTexts) {
ksort($cleanTranslations);
echo json_encode([
'translations' => array_values($cleanTranslations),
'model' => $ollamaModel,
'cache' => [
'hits' => count($texts),
'misses' => 0,
],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
$prompt = "Translate each item to {$targetName}.\n"
. "Return ONLY a valid JSON array of translated strings in the same order, no explanations, no markdown.\n"
. "Example input: [\"Hello\", \"How are you\"]\n"
. "Example output: [\"Bonjour\", \"Comment allez-vous\"]\n\n"
. 'Input: ' . json_encode($texts, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
. 'Input: ' . json_encode($missTexts, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ollamaHost = rtrim(getenv('OLLAMA_HOST') ?: 'http://192.168.100.68:11434', '/');
$ollamaModel = getenv('OLLAMA_MODEL') ?: 'llama3.2:3b';
$ollamaPayload = json_encode([
'model' => $ollamaModel,
'prompt' => $prompt,
@ -96,19 +270,30 @@ if (!is_array($ollamaResponse) || !isset($ollamaResponse['response']) || !is_str
$translationJson = trim(str_replace(['```json', '```'], '', $ollamaResponse['response']));
$translations = json_decode($translationJson, true);
if (!is_array($translations) || count($translations) !== count($texts)) {
if (!is_array($translations) || count($translations) !== count($missTexts)) {
translate_response(502, ['error' => 'Unexpected translation response']);
}
$cleanTranslations = [];
foreach ($translations as $translation) {
foreach ($translations as $index => $translation) {
if (!is_string($translation) && !is_numeric($translation)) {
translate_response(502, ['error' => 'Unexpected translation item']);
}
$cleanTranslations[] = clean_translate_text((string) $translation);
$originalPosition = $missPositions[$index];
$cleanTranslations[$originalPosition] = clean_translate_text((string) $translation);
}
$cacheWrites = [];
foreach ($missPositions as $position) {
$cacheWrites[$cacheKeys[$position]] = $cleanTranslations[$position];
}
translation_cache_write($cacheWrites);
ksort($cleanTranslations);
echo json_encode([
'translations' => $cleanTranslations,
'translations' => array_values($cleanTranslations),
'model' => $ollamaModel,
'cache' => [
'hits' => count($texts) - count($missTexts),
'misses' => count($missTexts),
],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);