$value) { $localized[$key] = $value; } return $localized; } function localize_recipes(array $recipes, string $lang): array { return array_map(function ($recipe) use ($lang) { return localize_recipe($recipe, $lang); }, $recipes); } function filter_recipes(array $recipes, ?string $query = null, ?string $tag = null, string $lang = 'en'): array { $query = $query ? strtolower($query) : null; $tag = $tag ? strtolower($tag) : null; $localized = localize_recipes($recipes, $lang); return array_values(array_filter($localized, function ($recipe) use ($query, $tag) { $matchesQuery = true; if ($query) { $haystack = strtolower(($recipe['title'] ?? '') . ' ' . ($recipe['description'] ?? '')); $matchesQuery = strpos($haystack, $query) !== false; } $matchesTag = true; if ($tag) { $tags = array_map('strtolower', $recipe['tags'] ?? []); $matchesTag = in_array($tag, $tags, true); } return $matchesQuery && $matchesTag; })); } function format_minutes(int $minutes): string { if ($minutes < 60) { return $minutes . ' min'; } $hours = intdiv($minutes, 60); $mins = $minutes % 60; return $hours . ' hr' . ($hours > 1 ? 's ' : ' ') . ($mins ? $mins . ' min' : ''); } function e(?string $value): string { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } function normalize_asset_path(string $path): string { if (!str_starts_with($path, '/assets/')) { return $path; } $absolute = __DIR__ . $path; if (file_exists($absolute)) { return $path; } $dir = dirname($absolute); $target = basename($absolute); if (!is_dir($dir)) { return $path; } foreach (scandir($dir) ?: [] as $entry) { if (strcasecmp($entry, $target) === 0) { return rtrim(dirname($path), '/') . '/' . $entry; } } return $path; } /** * Build a URL to the current path with a specific language parameter, preserving other query params. */ function lang_url(string $lang): string { $params = $_GET; $params['lang'] = $lang; $path = strtok($_SERVER['REQUEST_URI'], '?') ?: '/'; return $path . '?' . http_build_query($params); }