網站多語言進入點自動導向需平衡使用者偏好、SEO、快取與使用者控制權。本文提供完整檢測策略:HTTP Header、GeoIP、Cookie 覆寫、URL 語言前綴。
/en/, /zh-TW/ (最高優先,SEO 友善、可快取)<?php
class LanguageDetector {
private array $supportedLanguages = ['en', 'zh-TW', 'zh-CN', 'ja', 'ko'];
private string $defaultLanguage = 'en';
private string $cookieName = 'preferred_lang';
public function detect(): string {
// 1. URL 前綴 (由路由層處理,這裡假設已解析)
if (!empty($_GET['_lang']) && $this->isSupported($_GET['_lang'])) {
return $this->setPreference($_GET['_lang']);
}
// 2. Cookie 偏好
if (!empty($_COOKIE[$this->cookieName]) && $this->isSupported($_COOKIE[$this->cookieName])) {
return $_COOKIE[$this->cookieName];
}
// 3. Session 偏好
if (session_status() === PHP_SESSION_ACTIVE) {
if (!empty($_SESSION[$this->cookieName]) && $this->isSupported($_SESSION[$this->cookieName])) {
return $_SESSION[$this->cookieName];
}
}
// 4. Accept-Language 解析
$acceptLang = $this->parseAcceptLanguage();
if ($acceptLang) return $this->setPreference($acceptLang);
// 5. GeoIP 國家代碼映射
$geoLang = $this->geoIpToLanguage();
if ($geoLang) return $this->setPreference($geoLang);
// 6. 預設語言
return $this->defaultLanguage;
}
private function parseAcceptLanguage(): ?string {
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
if (!$header) return null;
// 解析 q-value 權重
// 範例: "zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7"
preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})?)\s*(?:;\s*q\s*=\s*(1|0\.\d+))?/i', $header, $matches);
$langs = [];
foreach ($matches[1] as $i => $lang) {
$q = isset($matches[2][$i]) && $matches[2][$i] !== '' ? (float)$matches[2][$i] : 1.0;
$langs[] = ['lang' => strtolower($lang), 'q' => $q];
}
// 依權重排序
usort($langs, fn($a, $b) => $b['q'] <=> $a['q']);
// 匹配支援語言 (精確 > 語言代碼)
foreach ($langs as $item) {
$lang = $item['lang'];
// 精確匹配
if (in_array($lang, $this->supportedLanguages, true)) return $lang;
// 語言代碼匹配 (zh-TW → zh)
$primary = explode('-', $lang)[0];
foreach ($this->supportedLanguages as $supported) {
if (str_starts_with($supported, $primary . '-')) return $supported;
}
}
return null;
}
private function geoIpToLanguage(): ?string {
// 需整合 MaxMind GeoIP2 或 IP2Location
// 這裡示範邏輯
$countryCode = $this->getCountryCodeFromIp($_SERVER['REMOTE_ADDR']);
if (!$countryCode) return null;
$map = [
'TW' => 'zh-TW', 'HK' => 'zh-TW', 'MO' => 'zh-TW',
'CN' => 'zh-CN', 'SG' => 'zh-CN', 'MY' => 'zh-CN',
'JP' => 'ja', 'KR' => 'ko',
'US' => 'en', 'GB' => 'en', 'AU' => 'en', 'CA' => 'en',
];
return $map[$countryCode] ?? null;
}
private function getCountryCodeFromIp(string $ip): ?string {
// 整合 GeoIP2 資料庫
// $reader = new GeoIp2\Database\Reader('/path/to/GeoLite2-Country.mmdb');
// return $reader->country($ip)->country->isoCode;
return null; // 佔位
}
private function isSupported(string $lang): bool {
return in_array($lang, $this->supportedLanguages, true);
}
private function setPreference(string $lang): string {
setcookie($this->cookieName, $lang, [
'expires' => time() + 365 * 86400,
'path' => '/',
'httponly' => true,
'secure' => true,
'samesite' => 'Lax',
]);
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION[$this->cookieName] = $lang;
}
return $lang;
}
}
?>
<?php
// public/index.php 或前端控制器
require 'vendor/autoload.php';
$detector = new LanguageDetector();
$lang = $detector->detect();
// 重導向至語言前綴 URL (避免重複內容 SEO 問題)
$currentUri = $_SERVER['REQUEST_URI'];
if (!preg_match('/^\/(?:' . implode('|', $detector->getSupportedLanguages()) . ')\//', $currentUri)) {
$redirectUrl = '/' . $lang . $currentUri;
header('Location: ' . $redirectUrl, true, 302); // 302 暫時重導
exit;
}
// 設定全域語言常數
define('CURRENT_LANG', $lang);
// 載入語言包、設定 locale 等...
?>
<script>
function detectLanguage() {
// 1. URL 路徑
const pathLang = window.location.pathname.match(/^\/(en|zh-TW|zh-CN|ja|ko)\//);
if (pathLang) return pathLang[1];
// 2. Cookie
const cookieLang = document.cookie.match(/preferred_lang=([^;]+)/);
if (cookieLang) return decodeURIComponent(cookieLang[1]);
// 3. navigator.language / navigator.languages
const supported = ['en', 'zh-TW', 'zh-CN', 'ja', 'ko'];
const prefs = navigator.languages || [navigator.language || navigator.userLanguage];
for (const lang of prefs) {
const lower = lang.toLowerCase();
if (supported.includes(lower)) return lower;
const primary = lower.split('-')[0];
const match = supported.find(s => s.startsWith(primary + '-'));
if (match) return match;
}
return 'en'; // 預設
}
const userLang = detectLanguage();
// 切換 i18n 語言包、更新 UI...
| 項目 | 做法 |
|---|---|
| hreflang | <link rel="alternate" hreflang="zh-TW" href="https://example.com/zh-TW/page"> |
| Canonical | 每頁指定標準 URL (含語言前綴) |
| Sitemap | 包含所有語言版本 URL |
| 重導向 | 302 (非 301),避免快取錯誤語言版本 |
| 快取 | Vary: Accept-Language, Cookie 區分快取 |
<?php
function renderLanguageSwitcher(string $currentLang, array $supported): string {
$html = '';
foreach ($supported as $lang) {
$nativeNames = [
'en' => 'English', 'zh-TW' => '繁體中文',
'zh-CN' => '简体中文', 'ja' => '日本語', 'ko' => '한국어'
];
$active = $lang === $currentLang ? ' active' : '';
$url = '/' . $lang . $_SERVER['REQUEST_URI'];
$html .= '';
$html .= $nativeNames[$lang] ?? $lang;
$html .= '';
}
$html .= '';
return $html;
}
?>
多語言導向核心原則:URL 為唯一真實來源,Accept-Language/GeoIP 僅作初次訪問預設,使用者明確選擇優先,並提供可見、可切換的語言選單。