<?php /** * Éditorial SEO multi-lang (phase A) : titles, metas, leads data-driven. * FR : délègue à seo_fr_copy (qualité 5F inchangée). * EN / DE / ES / IT : templates parallèles, données réelles uniquement (pas de LLM). * UTF-8 sans BOM. */ require_once __DIR__ . '/seo_fr_copy.php'; if (is_file(__DIR__ . '/seo_country_labels_i18n.php')) { require_once __DIR__ . '/seo_country_labels_i18n.php'; } if (is_file(__DIR__ . '/seo_i18n_stats.php')) { require_once __DIR__ . '/seo_i18n_stats.php'; } if (!function_exists('seo_copy_clip')) { function seo_copy_clip($text, $max = 158) { return seo_fr_clip($text, $max); } } if (!function_exists('seo_copy_lower')) { function seo_copy_lower($s) { return seo_fr_lower($s); } } if (!function_exists('seo_copy_ucfirst')) { function seo_copy_ucfirst($s) { return seo_fr_ucfirst($s); } } if (!function_exists('seo_copy_and_word')) { /** Conjonction de liste (« et », « and », & ). */ function seo_copy_and_word($lang) { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : (string)$lang; $map = array( 'fr' => 'et', 'en' => 'and', 'de' => 'und', 'es' => 'y', 'it' => 'e', ); return isset($map[$lang]) ? $map[$lang] : 'and'; } } if (!function_exists('seo_copy_join_list')) { /** * Jointure naturelle : a | a et b | a, b et c * @param string $lang * @param string[] $items */ function seo_copy_join_list($lang, array $items) { $items = array_values(array_filter(array_map('trim', $items), function ($x) { return $x !== ''; })); $n = count($items); if ($n === 0) { return ''; } if ($n === 1) { return $items[0]; } $and = seo_copy_and_word($lang); if ($n === 2) { return $items[0] . ' ' . $and . ' ' . $items[1]; } $last = $items[$n - 1]; return implode(', ', array_slice($items, 0, -1)) . ' ' . $and . ' ' . $last; } } if (!function_exists('seo_copy_sport_ops_key')) { /** Code spot catalogue ’! clé sports.json (kite|wingfoil|windsurf|surf). */ function seo_copy_sport_ops_key($code) { $c = strtolower(trim((string)$code)); if ($c === 'kite') { return 'kite'; } if ($c === 'wing' || $c === 'wingfoil') { return 'wingfoil'; } if ($c === 'wind' || $c === 'windsurf') { return 'windsurf'; } if ($c === 'surf') { return 'surf'; } return null; } } if (!function_exists('seo_copy_sport_phrase')) { /** * Libellé sport pour phrase éditoriale (minuscules / phrase). * @return string */ function seo_copy_sport_phrase($lang, $code) { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : (string)$lang; $key = seo_copy_sport_ops_key($code); if ($key === null) { return ''; } if (function_exists('seo_i18n_sport_labels')) { $lab = seo_i18n_sport_labels($lang, $key); if (is_array($lab)) { if (!empty($lab['phrase'])) { return (string)$lab['phrase']; } if (!empty($lab['adj'])) { return (string)$lab['adj']; } if (!empty($lab['name'])) { return seo_copy_lower((string)$lab['name']); } } } /* Fallback FR-like court */ $fb = array('kite' => 'kitesurf', 'wingfoil' => 'wingfoil', 'windsurf' => 'windsurf', 'surf' => 'surf'); return isset($fb[$key]) ? $fb[$key] : $key; } } if (!function_exists('seo_copy_sports_from_spots')) { /** * Codes sports présents (ordre kite / wing / wind / surf). * @return string[] codes catalogue kite|wing|wind|surf */ function seo_copy_sports_from_spots(array $spots) { $order = array('kite', 'wing', 'wind', 'surf'); $seen = array(); foreach ($spots as $s) { if (!is_array($s) || empty($s['sports']) || !is_array($s['sports'])) { continue; } foreach ($s['sports'] as $sp) { $sp = strtolower(trim((string)$sp)); if (in_array($sp, $order, true)) { $seen[$sp] = true; } } } $out = array(); foreach ($order as $c) { if (isset($seen[$c])) { $out[] = $c; } } return $out; } } if (!function_exists('seo_copy_sports_phrase')) { /** * Phrase sports agrégée (ex. « kitesurfing, wing foiling and surfing »). * @param bool $titleCase premiere lettre majuscule */ function seo_copy_sports_phrase($lang, array $spots, $titleCase = false) { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : (string)$lang; if ($lang === 'fr') { if (!function_exists('seo_geo_area_sports_phrase_fr')) { require_once __DIR__ . '/seo_geo_area.php'; } $p = function_exists('seo_geo_area_sports_phrase_fr') ? seo_geo_area_sports_phrase_fr($spots) : ''; return ($titleCase && $p !== '') ? seo_copy_ucfirst($p) : $p; } $labs = array(); foreach (seo_copy_sports_from_spots($spots) as $code) { $lab = seo_copy_sport_phrase($lang, $code); if ($lab !== '' && !in_array($lab, $labs, true)) { $labs[] = $lab; } } $p = seo_copy_join_list($lang, $labs); return ($titleCase && $p !== '') ? seo_copy_ucfirst($p) : $p; } } if (!function_exists('seo_copy_area_where')) { /** * Complément de lieu pour zone / admin (hors FR : préposition simple + label). */ function seo_copy_area_where($lang, array $areaMeta, $kind = 'department') { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : (string)$lang; $label = isset($areaMeta['label']) ? trim((string)$areaMeta['label']) : ''; if ($lang === 'fr') { $where = isset($areaMeta['prep_phrase']) ? trim((string)$areaMeta['prep_phrase']) : ''; if ($where !== '') { return $where; } if ($label !== '' && function_exists('seo_geo_area_phrase_fr')) { return seo_geo_area_phrase_fr($kind, $label); } return $label !== '' ? ('dans ' . $label) : ''; } if ($label === '') { return ''; } if ($kind === 'admin') { if ($lang === 'es') { return 'en ' . $label; } if ($lang === 'it') { return 'a ' . $label; } if ($lang === 'de') { return 'in ' . $label; } return 'in ' . $label; } /* department / overseas_zone */ if ($lang === 'es') { return 'en ' . $label; } if ($lang === 'it') { return 'in ' . $label; } /* en, de */ return 'in ' . $label; } } if (!function_exists('seo_copy_spot_has_stats')) { function seo_copy_spot_has_stats(array $spot) { $sid = isset($spot['id']) ? (string)$spot['id'] : ''; if ($sid === '' || !function_exists('seo_stats_load_spot_payload') || !function_exists('seo_stats_map_seo_sport_to_ops')) { return false; } $seoSports = isset($spot['sports']) && is_array($spot['sports']) ? $spot['sports'] : array(); foreach ($seoSports as $ss) { $ops = seo_stats_map_seo_sport_to_ops($ss); if ($ops === null) { continue; } $payload = seo_stats_load_spot_payload($sid, $ops); if (is_array($payload)) { return true; } } return false; } } if (!function_exists('seo_copy_spot_compact_title')) { /** * Title spot compact : "{Nom} : {sports} | Thespot2be" (d"70, jamais « & »). */ function seo_copy_spot_compact_title($lang, $name, array $sportsCodes, $sportsFormatted = '') { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : (string)$lang; if ($lang === 'fr') { return seo_fr_spot_compact_title($name, $sportsCodes, $sportsFormatted); } $name = trim((string)$name); $brand = ' | Thespot2be'; $labs = array(); foreach ($sportsCodes as $code) { $lab = seo_copy_sport_phrase($lang, $code); if ($lab !== '' && !in_array($lab, $labs, true)) { $labs[] = $lab; } } if (count($labs) === 0 && trim((string)$sportsFormatted) !== '') { $raw = seo_copy_lower(trim((string)$sportsFormatted)); $raw = str_replace(array(' and ', ' und ', ' y ', ' e ', ' et ', ' / ', '/'), array(', ', ', ', ', ', ', ', ', ', ', ', ', '), $raw); foreach (preg_split('/\s*,\s*/', $raw) as $p) { $p = trim($p); if ($p !== '' && !in_array($p, $labs, true)) { $labs[] = $p; } } } if (count($labs) === 0) { $fallbackGlide = array( 'en' => 'watersports', 'de' => 'Wassersport', 'es' => 'deportes náuticos', 'it' => 'sport acquatici', ); $labs[] = isset($fallbackGlide[$lang]) ? $fallbackGlide[$lang] : 'watersports'; } $join = function (array $items) use ($lang) { return seo_copy_join_list($lang, $items); }; $sep = ' : '; $strlen = function ($s) { return function_exists('mb_strlen') ? mb_strlen($s, 'UTF-8') : strlen($s); }; $candidate = $name . $sep . $join($labs) . $brand; if ($strlen($candidate) <= 70) { return $candidate; } for ($n = count($labs) - 1; $n >= 1; $n--) { $candidate = $name . $sep . $join(array_slice($labs, 0, $n)) . $brand; if ($strlen($candidate) <= 70) { return $candidate; } } $fallback = $name . $brand; if ($strlen($fallback) <= 70) { return $fallback; } return $name; } } if (!function_exists('seo_copy_spot_editorial')) { /** * @return array{title:string,meta:string,lead:string,h1:string} */ function seo_copy_spot_editorial($lang, array $spot, $locationLabel, $sportsFormatted) { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : (string)$lang; if ($lang === 'fr') { return seo_fr_spot_editorial($spot, $locationLabel, $sportsFormatted); } $name = isset($spot['name']) ? trim((string)$spot['name']) : ''; $sportsFormatted = trim((string)$sportsFormatted); $loc = trim((string)$locationLabel); $h1 = $name; $codes = isset($spot['sports']) && is_array($spot['sports']) ? $spot['sports'] : array(); $title = seo_copy_spot_compact_title($lang, $name, $codes, $sportsFormatted); $hasStats = seo_copy_spot_has_stats($spot); $sportsLow = $sportsFormatted !== '' ? seo_copy_lower($sportsFormatted) : ''; if ($sportsLow === '') { $labs = array(); foreach ($codes as $c) { $lab = seo_copy_sport_phrase($lang, $c); if ($lab !== '') { $labs[] = $lab; } } $sportsLow = seo_copy_join_list($lang, $labs); } /* Meta + lead par langue */ $meta = ''; $leadParts = array(); if ($lang === 'en') { $meta = 'Spot ' . $name; if ($loc !== '') { $meta .= ' (' . $loc . ')'; } if ($sportsLow !== '') { $meta .= ' : ' . $sportsLow; } $meta .= $hasStats ? '  operating statistics on Thespot2be' : ' on Thespot2be'; if ($sportsLow !== '' && $loc !== '') { $leadParts[] = $name . ' is a ' . $sportsLow . ' spot in ' . $loc . '.'; } elseif ($sportsLow !== '') { $leadParts[] = $name . ' is a ' . $sportsLow . ' spot.'; } elseif ($loc !== '') { $leadParts[] = $name . ' is a spot in ' . $loc . '.'; } if ($hasStats) { $leadParts[] = 'See operating statistics (today, month, history) below.'; } } elseif ($lang === 'de') { $meta = 'Spot ' . $name; if ($loc !== '') { $meta .= ' (' . $loc . ')'; } if ($sportsLow !== '') { $meta .= ' : ' . $sportsLow; } $meta .= $hasStats ? '  Betriebsstatistiken auf Thespot2be' : ' auf Thespot2be'; if ($sportsLow !== '' && $loc !== '') { $leadParts[] = $name . ' ist ein ' . $sportsLow . '-Spot in ' . $loc . '.'; } elseif ($sportsLow !== '') { $leadParts[] = $name . ' ist ein ' . $sportsLow . '-Spot.'; } elseif ($loc !== '') { $leadParts[] = $name . ' ist ein Spot in ' . $loc . '.'; } if ($hasStats) { $leadParts[] = 'Betriebsstatistiken (heute, Monat, Historie) siehe unten.'; } } elseif ($lang === 'es') { $meta = 'Spot ' . $name; if ($loc !== '') { $meta .= ' (' . $loc . ')'; } if ($sportsLow !== '') { $meta .= ' : ' . $sportsLow; } $meta .= $hasStats ? '  estadísticas de funcionamiento en Thespot2be' : ' en Thespot2be'; if ($sportsLow !== '' && $loc !== '') { $leadParts[] = $name . ' es un spot de ' . $sportsLow . ' situado en ' . $loc . '.'; } elseif ($sportsLow !== '') { $leadParts[] = $name . ' es un spot de ' . $sportsLow . '.'; } elseif ($loc !== '') { $leadParts[] = $name . ' es un spot situado en ' . $loc . '.'; } if ($hasStats) { $leadParts[] = 'Consulta las estadísticas de funcionamiento (hoy, mes, historial) más abajo.'; } } else { /* it */ $meta = 'Spot ' . $name; if ($loc !== '') { $meta .= ' (' . $loc . ')'; } if ($sportsLow !== '') { $meta .= ' : ' . $sportsLow; } $meta .= $hasStats ? '  statistiche di funzionamento su Thespot2be' : ' su Thespot2be'; if ($sportsLow !== '' && $loc !== '') { $leadParts[] = $name . ' è uno spot di ' . $sportsLow . ' situato a ' . $loc . '.'; } elseif ($sportsLow !== '') { $leadParts[] = $name . ' è uno spot di ' . $sportsLow . '.'; } elseif ($loc !== '') { $leadParts[] = $name . ' è uno spot situato a ' . $loc . '.'; } if ($hasStats) { $leadParts[] = 'Consulta le statistiche di funzionamento (oggi, mese, storico) qui sotto.'; } } return array( 'title' => $title, 'meta' => seo_copy_clip($meta . '.'), 'lead' => implode(' ', $leadParts), 'h1' => $h1, ); } } if (!function_exists('seo_copy_country_editorial')) { /** * @return array{title:string,meta:string,lead:string,h1:string} */ function seo_copy_country_editorial($lang, $countrySlug, $countryLabel, array $spots) { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : (string)$lang; if ($lang === 'fr') { return seo_fr_country_editorial($countrySlug, $countryLabel, $spots); } $n = count($spots); $sportsTitle = seo_copy_sports_phrase($lang, $spots, true); $sportsLow = seo_copy_sports_phrase($lang, $spots, false); $where = function_exists('seo_country_where_phrase') ? seo_country_where_phrase($lang, $countrySlug !== '' ? $countrySlug : $countryLabel) : ('in ' . $countryLabel); $hasStats = false; if (function_exists('seo_country_stats_available_sports')) { $hasStats = count(seo_country_stats_available_sports($countrySlug)) > 0; } $spotWord = array( 'en' => array('spot', 'spots'), 'de' => array('Spot', 'Spots'), 'es' => array('spot', 'spots'), 'it' => array('spot', 'spot'), ); $sw = isset($spotWord[$lang]) ? $spotWord[$lang] : array('spot', 'spots'); $spotLabel = ($n === 1) ? $sw[0] : $sw[1]; if ($lang === 'en') { $title = ($sportsTitle !== '') ? ($sportsTitle . ' ' . $where . ' : spots and statistics') : ('Spots ' . $where . ' : statistics and beaches'); if ($n > 0 && $sportsLow !== '') { $metaParts = array($n . ' ' . $spotLabel . ' for ' . $sportsLow . ' ' . $where); } elseif ($n > 0) { $metaParts = array($n . ' ' . $spotLabel . ' ' . $where); } else { $metaParts = array('Spots ' . $where); } $metaParts[] = $hasStats ? 'operating statistics (today, month, history)' : 'spot list on Thespot2be'; if ($n > 0 && $sportsLow !== '') { $lead = 'Explore the ' . $n . ' ' . $sportsLow . ' spots listed ' . $where . ($hasStats ? ', with operating statistics and current tops.' : '.'); } elseif ($n > 0) { $lead = 'Explore the ' . $n . ' spots listed ' . $where . '.'; } else { $lead = 'Find spots listed ' . $where . ' on Thespot2be.'; } } elseif ($lang === 'de') { $title = ($sportsTitle !== '') ? ($sportsTitle . ' ' . $where . ' : Spots und Statistiken') : ('Spots ' . $where . ' : Statistiken und Strände'); if ($n > 0 && $sportsLow !== '') { $metaParts = array($n . ' ' . $spotLabel . ' für ' . $sportsLow . ' ' . $where); } elseif ($n > 0) { $metaParts = array($n . ' ' . $spotLabel . ' ' . $where); } else { $metaParts = array('Spots ' . $where); } $metaParts[] = $hasStats ? 'Betriebsstatistiken (heute, Monat, Historie)' : 'Spotliste auf Thespot2be'; if ($n > 0 && $sportsLow !== '') { $lead = 'Entdecken Sie die ' . $n . ' ' . $sportsLow . '-Spots ' . $where . ($hasStats ? ', mit Betriebsstatistiken und aktuellen Tops.' : '.'); } elseif ($n > 0) { $lead = 'Entdecken Sie die ' . $n . ' Spots ' . $where . '.'; } else { $lead = 'Finden Sie Spots ' . $where . ' auf Thespot2be.'; } } elseif ($lang === 'es') { $title = ($sportsTitle !== '') ? ($sportsTitle . ' ' . $where . ' : spots y estadísticas') : ('Spots ' . $where . ' : estadísticas y playas'); if ($n > 0 && $sportsLow !== '') { $metaParts = array($n . ' ' . $spotLabel . ' de ' . $sportsLow . ' ' . $where); } elseif ($n > 0) { $metaParts = array($n . ' ' . $spotLabel . ' ' . $where); } else { $metaParts = array('Spots ' . $where); } $metaParts[] = $hasStats ? 'estadísticas de funcionamiento (hoy, mes, historial)' : 'lista de spots en Thespot2be'; if ($n > 0 && $sportsLow !== '') { $lead = 'Explora los ' . $n . ' spots de ' . $sportsLow . ' referenciados ' . $where . ($hasStats ? ', con estadísticas de funcionamiento y tops del momento.' : '.'); } elseif ($n > 0) { $lead = 'Explora los ' . $n . ' spots referenciados ' . $where . '.'; } else { $lead = 'Encuentra spots referenciados ' . $where . ' en Thespot2be.'; } } else { /* it */ $title = ($sportsTitle !== '') ? ($sportsTitle . ' ' . $where . ' : spot e statistiche') : ('Spot ' . $where . ' : statistiche e spiagge'); if ($n > 0 && $sportsLow !== '') { $metaParts = array($n . ' ' . $spotLabel . ' di ' . $sportsLow . ' ' . $where); } elseif ($n > 0) { $metaParts = array($n . ' ' . $spotLabel . ' ' . $where); } else { $metaParts = array('Spot ' . $where); } $metaParts[] = $hasStats ? 'statistiche di funzionamento (oggi, mese, storico)' : 'elenco spot su Thespot2be'; if ($n > 0 && $sportsLow !== '') { $lead = 'Esplora i ' . $n . ' spot di ' . $sportsLow . ' recensiti ' . $where . ($hasStats ? ', con statistiche di funzionamento e top del momento.' : '.'); } elseif ($n > 0) { $lead = 'Esplora i ' . $n . ' spot recensiti ' . $where . '.'; } else { $lead = 'Trova gli spot recensiti ' . $where . ' su Thespot2be.'; } } return array( 'title' => $title, 'meta' => seo_copy_clip(implode('. ', $metaParts) . '.'), 'lead' => $lead, 'h1' => $title, ); } } if (!function_exists('seo_copy_area_editorial')) { /** * Zone / subdivision. * @param string $kind department|admin|overseas_zone * @return array{title:string,meta:string,h1:string} */ function seo_copy_area_editorial($lang, array $spots, array $areaMeta, $kind = 'department') { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : (string)$lang; if ($lang === 'fr') { return seo_fr_area_editorial($spots, $areaMeta, $kind); } $n = count($spots); $sportsLow = seo_copy_sports_phrase($lang, $spots, false); $where = seo_copy_area_where($lang, $areaMeta, $kind); $hasStats = false; if (function_exists('seo_area_stats_available_sports') && isset($areaMeta['country_scope'], $areaMeta['area_id'])) { $hasStats = count(seo_area_stats_available_sports($areaMeta['country_scope'], $areaMeta['area_id'])) > 0; } if ($lang === 'en') { $title = ($sportsLow !== '') ? ('Spots for ' . $sportsLow . ' ' . $where) : ('Spots ' . $where); $meta = $n . ' spot' . ($n === 1 ? '' : 's'); if ($sportsLow !== '') { $meta .= ' for ' . $sportsLow; } $meta .= ' ' . $where; $meta .= $hasStats ? ' : statistics and tops on Thespot2be' : ' on Thespot2be'; } elseif ($lang === 'de') { $title = ($sportsLow !== '') ? ('Spots für ' . $sportsLow . ' ' . $where) : ('Spots ' . $where); $meta = $n . ' Spot' . ($n === 1 ? '' : 's'); if ($sportsLow !== '') { $meta .= ' für ' . $sportsLow; } $meta .= ' ' . $where; $meta .= $hasStats ? ' : Statistiken und Tops auf Thespot2be' : ' auf Thespot2be'; } elseif ($lang === 'es') { $title = ($sportsLow !== '') ? ('Spots de ' . $sportsLow . ' ' . $where) : ('Spots ' . $where); $meta = $n . ' spot' . ($n === 1 ? '' : 's'); if ($sportsLow !== '') { $meta .= ' de ' . $sportsLow; } $meta .= ' ' . $where; $meta .= $hasStats ? ' : estadísticas y tops en Thespot2be' : ' en Thespot2be'; } else { /* it */ $title = ($sportsLow !== '') ? ('Spot di ' . $sportsLow . ' ' . $where) : ('Spot ' . $where); $meta = $n . ' spot'; if ($sportsLow !== '') { $meta .= ' di ' . $sportsLow; } $meta .= ' ' . $where; $meta .= $hasStats ? ' : statistiche e top su Thespot2be' : ' su Thespot2be'; } return array( 'title' => $title, 'meta' => seo_copy_clip($meta . '.'), 'h1' => $title, ); } }