SetLocale.php 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. <?php
  2. namespace App\Http\Middleware;
  3. use Closure;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Support\Facades\App;
  6. use Illuminate\Support\Facades\Cookie;
  7. use Symfony\Component\HttpFoundation\Response;
  8. class SetLocale
  9. {
  10. /**
  11. * Handle an incoming request.
  12. *
  13. * @param Closure(Request): (Response) $next
  14. */
  15. public function handle(Request $request, Closure $next): Response
  16. {
  17. // 支持的语言(config 里是 en / zh-Hans 这类带大小写的写法)
  18. $supportedLocales = array_keys(config('mint.languages'));
  19. $defaultLocale = config('app.locale', 'en');
  20. // 1️⃣ URL 参数 ?lang=
  21. $locale = $this->matchLocale($request->query('lang'), $supportedLocales);
  22. // 2️⃣ Cookie
  23. if (! $locale) {
  24. $locale = $this->matchLocale(Cookie::get('language'), $supportedLocales);
  25. }
  26. // 3️⃣ 浏览器 / 操作系统语言(Accept-Language 由浏览器按系统界面语言生成)
  27. if (! $locale) {
  28. $locale = $this->getBrowserLocale($request, $supportedLocales);
  29. }
  30. // 4️⃣ 兜底
  31. $locale = $locale ?? $defaultLocale;
  32. // 5️⃣ 应用语言
  33. App::setLocale($locale);
  34. // 6️⃣ 持久化(可选)
  35. session()->put('locale', $locale);
  36. Cookie::queue('language', $locale, 60 * 24 * 365); // 1 年
  37. return $next($request);
  38. }
  39. /**
  40. * 从 Accept-Language 头中解析浏览器语言
  41. *
  42. * @param string[] $supportedLocales
  43. */
  44. protected function getBrowserLocale(Request $request, array $supportedLocales): ?string
  45. {
  46. $acceptLanguage = $request->header('Accept-Language');
  47. if (! $acceptLanguage) {
  48. return null;
  49. }
  50. // zh-CN,zh;q=0.9,en;q=0.8 → 按 q 值从高到低排序
  51. $languages = [];
  52. foreach (explode(',', $acceptLanguage) as $part) {
  53. $pieces = explode(';', trim($part));
  54. $tag = strtolower(trim($pieces[0]));
  55. if ($tag === '' || $tag === '*') {
  56. continue;
  57. }
  58. $quality = 1.0;
  59. if (isset($pieces[1]) && preg_match('/q=([0-9.]+)/', $pieces[1], $matches)) {
  60. $quality = (float) $matches[1];
  61. }
  62. $languages[] = ['tag' => $tag, 'quality' => $quality];
  63. }
  64. usort($languages, fn ($a, $b) => $b['quality'] <=> $a['quality']);
  65. foreach ($languages as $language) {
  66. if ($locale = $this->matchLocale($language['tag'], $supportedLocales)) {
  67. return $locale;
  68. }
  69. }
  70. return null;
  71. }
  72. /**
  73. * 把一个语言标签匹配到受支持的语言,匹配不上返回 null。
  74. * 大小写不敏感;zh-CN / zh-SG 归为简体,zh-TW / zh-HK / zh-MO 归为繁体;
  75. * 其余按主语言回退,如 th-TH → th。
  76. *
  77. * @param string[] $supportedLocales
  78. */
  79. protected function matchLocale(?string $tag, array $supportedLocales): ?string
  80. {
  81. if (! $tag) {
  82. return null;
  83. }
  84. $tag = strtolower(str_replace('_', '-', trim($tag)));
  85. // lowercase => 原始写法,便于大小写不敏感匹配
  86. $lookup = array_combine(array_map('strtolower', $supportedLocales), $supportedLocales);
  87. // 精确匹配:en、zh-hans……
  88. if (isset($lookup[$tag])) {
  89. return $lookup[$tag];
  90. }
  91. // 中文按地区码判断简繁
  92. if (str_starts_with($tag, 'zh')) {
  93. $traditional = str_contains($tag, 'hant')
  94. || (bool) preg_match('/\-(tw|hk|mo)\b/', $tag);
  95. $chinese = $traditional ? 'zh-hant' : 'zh-hans';
  96. return $lookup[$chinese] ?? null;
  97. }
  98. // 主语言回退:th-th → th
  99. $primary = explode('-', $tag)[0];
  100. return $lookup[$primary] ?? null;
  101. }
  102. }