Dodatkowe wersje językowe z AI

This commit is contained in:
2026-09-25 18:04:28 +02:00
parent a381ad09d8
commit 4fde92d323
23 changed files with 1231 additions and 148 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
addefc0876999fc32822c6f3368af33520f89f31de548559e069be12cc2685fb
+16 -132
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/menu_helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode([
@@ -29,138 +31,9 @@ if ($lang === '' || !isset($languages[$lang])) {
$langConfig = $languages[$lang];
$source = (string) ($langConfig['source'] ?? 'upstream');
$ttl = max(60, (int) ($config['ttl_seconds'] ?? 1800));
$ttl = max(60, (int) ($config['ttl_seconds'] ?? 86400));
$timeout = max(3, (int) ($config['timeout_seconds'] ?? 12));
$cacheDir = __DIR__ . '/cache';
$cacheFile = $cacheDir . '/menu_' . $lang . '.json';
if (!is_dir($cacheDir)) {
@mkdir($cacheDir, 0755, true);
}
function readMenuCache(string $cacheFile): ?array
{
if (!is_file($cacheFile)) {
return null;
}
$raw = file_get_contents($cacheFile);
if ($raw === false || $raw === '') {
return null;
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : null;
}
function writeMenuCache(string $cacheFile, array $payload): void
{
file_put_contents(
$cacheFile,
json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
LOCK_EX
);
}
function fetchUpstreamMenu(string $url, int $timeout): ?array
{
$body = false;
$httpCode = 0;
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno !== 0) {
$body = false;
}
} else {
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'header' => "Accept: application/json\r\n",
'ignore_errors' => true,
],
]);
$body = @file_get_contents($url, false, $context);
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
$httpCode = (int) $m[1];
}
}
if ($body === false || ($httpCode > 0 && ($httpCode < 200 || $httpCode >= 300))) {
return null;
}
$decoded = json_decode($body, true);
return is_array($decoded) ? $decoded : null;
}
function mapUpstreamMenu(array $raw, string $lang, string $source): array
{
$categories = [];
foreach ($raw['categories'] ?? [] as $cat) {
if (!is_array($cat)) {
continue;
}
$id = trim((string) ($cat['id'] ?? ''));
$name = trim((string) ($cat['name'] ?? ''));
if ($id === '' || $name === '') {
continue;
}
$categories[] = ['id' => $id, 'name' => $name];
}
$sections = [];
foreach ($raw['tables'] ?? [] as $table) {
if (!is_array($table)) {
continue;
}
$sectionName = trim((string) ($table['name'] ?? ''));
if ($sectionName === '') {
continue;
}
$items = [];
foreach ($table['items'] ?? [] as $item) {
if (!is_array($item)) {
continue;
}
$items[] = [
'position' => (string) ($item['id'] ?? ''),
'categoryId' => (string) ($item['category'] ?? ''),
'tag' => '',
'image' => (string) ($item['image'] ?? ''),
'title' => trim((string) ($item['name'] ?? '')),
'description' => trim((string) ($item['name_extra'] ?? '')),
'price' => (string) ($item['price'] ?? ''),
];
}
$sections[] = [
'categoryName' => $sectionName,
'items' => $items,
];
}
return [
'status' => 'success',
'lang' => $lang,
'source' => $source,
'cachedAt' => gmdate('c'),
'categories' => $categories,
'sections' => $sections,
];
}
$cacheFile = menuCachePath($lang);
$cached = readMenuCache($cacheFile);
$now = time();
@@ -177,13 +50,24 @@ if ($isFresh) {
if ($source === 'ai') {
if ($cached) {
// Stary AI-cache jest OK dłużej niż TTL — cron i tak odświeża raz/dzień.
echo json_encode($cached, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
$from = strtolower((string) ($langConfig['from'] ?? 'pl'));
$fallback = readMenuCache(menuCachePath($from));
if ($fallback) {
$fallback['lang'] = $lang;
$fallback['source'] = 'ai_fallback_pl';
echo json_encode($fallback, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
http_response_code(503);
echo json_encode([
'status' => 'error',
'message' => 'AI menu not available yet',
'message' => 'AI menu not available yet — run scripts/refresh_menu_cache.php',
'lang' => $lang,
], JSON_UNESCAPED_UNICODE);
exit;
+315
View File
@@ -0,0 +1,315 @@
<?php
declare(strict_types=1);
/**
* Wspólne funkcje cache/upstream menu (api/menu.php + scripts/refresh_menu_cache.php).
*/
function menuCacheDir(): string
{
$dir = __DIR__ . '/cache';
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
return $dir;
}
function menuCachePath(string $lang): string
{
$safe = preg_replace('/[^a-z0-9_-]/i', '', strtolower($lang)) ?: 'pl';
return menuCacheDir() . '/menu_' . $safe . '.json';
}
function menuFingerprintPath(): string
{
return menuCacheDir() . '/menu_pl_fingerprint.txt';
}
function readMenuCache(string $cacheFile): ?array
{
if (!is_file($cacheFile)) {
return null;
}
$raw = file_get_contents($cacheFile);
if ($raw === false || $raw === '') {
return null;
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : null;
}
function writeMenuCache(string $cacheFile, array $payload): void
{
file_put_contents(
$cacheFile,
json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
LOCK_EX
);
}
function fetchUpstreamMenu(string $url, int $timeout): ?array
{
$body = false;
$httpCode = 0;
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno !== 0) {
$body = false;
}
} else {
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'header' => "Accept: application/json\r\n",
'ignore_errors' => true,
],
]);
$body = @file_get_contents($url, false, $context);
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
$httpCode = (int) $m[1];
}
}
if ($body === false || ($httpCode > 0 && ($httpCode < 200 || $httpCode >= 300))) {
return null;
}
$decoded = json_decode($body, true);
return is_array($decoded) ? $decoded : null;
}
function mapUpstreamMenu(array $raw, string $lang, string $source): array
{
$categories = [];
foreach ($raw['categories'] ?? [] as $cat) {
if (!is_array($cat)) {
continue;
}
$id = trim((string) ($cat['id'] ?? ''));
$name = trim((string) ($cat['name'] ?? ''));
if ($id === '' || $name === '') {
continue;
}
$categories[] = ['id' => $id, 'name' => $name];
}
$sections = [];
foreach ($raw['tables'] ?? [] as $table) {
if (!is_array($table)) {
continue;
}
$sectionName = trim((string) ($table['name'] ?? ''));
if ($sectionName === '') {
continue;
}
$items = [];
foreach ($table['items'] ?? [] as $item) {
if (!is_array($item)) {
continue;
}
$items[] = [
'position' => (string) ($item['id'] ?? ''),
'categoryId' => (string) ($item['category'] ?? ''),
'tag' => '',
'image' => (string) ($item['image'] ?? ''),
'title' => trim((string) ($item['name'] ?? '')),
'description' => trim((string) ($item['name_extra'] ?? '')),
'price' => (string) ($item['price'] ?? ''),
];
}
$sections[] = [
'categoryName' => $sectionName,
'items' => $items,
];
}
$payload = [
'status' => 'success',
'lang' => $lang,
'source' => $source,
'cachedAt' => gmdate('c'),
'categories' => $categories,
'sections' => $sections,
];
$payload['contentFingerprint'] = menuContentFingerprint($payload);
return $payload;
}
/**
* Stabilny fingerprint treści tekstowych (bez cen/obrazków/daty cache).
*/
function menuContentFingerprint(array $menu): string
{
$parts = [];
foreach ($menu['categories'] ?? [] as $cat) {
if (!is_array($cat)) {
continue;
}
$parts[] = 'c|' . ($cat['id'] ?? '') . '|' . ($cat['name'] ?? '');
}
foreach ($menu['sections'] ?? [] as $section) {
if (!is_array($section)) {
continue;
}
$catName = (string) ($section['categoryName'] ?? '');
$parts[] = 's|' . $catName;
foreach ($section['items'] ?? [] as $item) {
if (!is_array($item)) {
continue;
}
$parts[] = implode('|', [
'i',
(string) ($item['categoryId'] ?? ''),
(string) ($item['position'] ?? ''),
(string) ($item['title'] ?? ''),
(string) ($item['description'] ?? ''),
$catName,
]);
}
}
sort($parts, SORT_STRING);
return hash('sha256', implode("\n", $parts));
}
/**
* Mapa pozycji do tłumaczenia: key => [title, description, categoryName, categoryId, position, ...]
*/
function menuCollectTranslatableUnits(array $menu): array
{
$units = [];
$categoryNames = [];
foreach ($menu['categories'] ?? [] as $cat) {
if (!is_array($cat)) {
continue;
}
$id = (string) ($cat['id'] ?? '');
if ($id === '') {
continue;
}
$categoryNames[$id] = (string) ($cat['name'] ?? '');
$units['cat:' . $id] = [
'type' => 'category',
'id' => $id,
'text' => (string) ($cat['name'] ?? ''),
];
}
foreach ($menu['sections'] ?? [] as $sIdx => $section) {
if (!is_array($section)) {
continue;
}
$catName = (string) ($section['categoryName'] ?? '');
$units['sec:' . $sIdx] = [
'type' => 'section',
'index' => (int) $sIdx,
'text' => $catName,
];
foreach ($section['items'] ?? [] as $item) {
if (!is_array($item)) {
continue;
}
$cid = (string) ($item['categoryId'] ?? '');
$pos = (string) ($item['position'] ?? '');
$key = 'item:' . $cid . ':' . $pos;
$units[$key] = [
'type' => 'item',
'categoryId' => $cid,
'position' => $pos,
'title' => (string) ($item['title'] ?? ''),
'description' => (string) ($item['description'] ?? ''),
'hash' => hash('sha256', ($item['title'] ?? '') . "\0" . ($item['description'] ?? '') . "\0" . $catName),
];
}
}
return $units;
}
function menuApplyTranslations(array $plMenu, array $translations, string $lang): array
{
$out = $plMenu;
$out['lang'] = $lang;
$out['source'] = 'ai';
$out['cachedAt'] = gmdate('c');
$out['sourceFingerprint'] = $plMenu['contentFingerprint'] ?? menuContentFingerprint($plMenu);
foreach ($out['categories'] ?? [] as $i => $cat) {
if (!is_array($cat)) {
continue;
}
$id = (string) ($cat['id'] ?? '');
$key = 'cat:' . $id;
if (isset($translations[$key]['text'])) {
$out['categories'][$i]['name'] = (string) $translations[$key]['text'];
}
}
foreach ($out['sections'] ?? [] as $sIdx => $section) {
if (!is_array($section)) {
continue;
}
$secKey = 'sec:' . $sIdx;
if (isset($translations[$secKey]['text'])) {
$out['sections'][$sIdx]['categoryName'] = (string) $translations[$secKey]['text'];
}
foreach ($section['items'] ?? [] as $iIdx => $item) {
if (!is_array($item)) {
continue;
}
$cid = (string) ($item['categoryId'] ?? '');
$pos = (string) ($item['position'] ?? '');
$key = 'item:' . $cid . ':' . $pos;
if (isset($translations[$key]['title'])) {
$out['sections'][$sIdx]['items'][$iIdx]['title'] = (string) $translations[$key]['title'];
}
if (array_key_exists('description', $translations[$key] ?? [])) {
$out['sections'][$sIdx]['items'][$iIdx]['description'] = (string) $translations[$key]['description'];
}
}
}
$out['contentFingerprint'] = menuContentFingerprint($out);
return $out;
}
function loadMenuAiConfig(): ?array
{
$path = __DIR__ . '/../config/menu_ai.local.php';
if (!is_file($path)) {
return null;
}
$cfg = require $path;
return is_array($cfg) ? $cfg : null;
}