data] */ private static $cache = []; /** @var string 语言文件目录 */ private static $dir; /** * 设置语言目录(通常在 init.php 中调用一次)。 */ public static function setDir($dir) { self::$dir = rtrim($dir, '/\\'); } /** * 获取当前语言代码。 */ public static function getLocale() { return self::$locale; } /** * 切换语言。 * @param string $locale 如 'zh_CN', 'en_US' * @return bool 是否成功 */ public static function setLocale($locale) { $file = self::$dir . DIRECTORY_SEPARATOR . $locale . '.json'; if (!file_exists($file)) return false; self::$locale = $locale; // 清除旧缓存 if (isset(self::$cache[$locale])) unset(self::$cache[$locale]); return true; } /** * 从 cookie / session / 参数自动检测并设置语言。 */ public static function detectLocale() { // 1. Session(由 lang.php 设置,优先级最高,保证整站会话内一致) if (!empty($_SESSION['lang'])) { $lang = preg_replace('/[^a-zA-Z0-9_]/', '', $_SESSION['lang']); if (self::setLocale($lang)) return; } // 2. URL 参数 ?lang=xx(一次性切换,写入 cookie + session) if (!empty($_GET['lang'])) { $lang = preg_replace('/[^a-zA-Z0-9_]/', '', $_GET['lang']); if (self::setLocale($lang)) { $_SESSION['lang'] = $lang; setcookie('fnw_lang', $lang, time() + 31536000, '/'); return; } } // 3. Cookie if (!empty($_COOKIE['fnw_lang'])) { $lang = $_COOKIE['fnw_lang']; if (self::setLocale($lang)) return; } // 4. 默认中文 self::$locale = 'zh_CN'; } /** * 翻译函数(核心)。 * * @param string $key 点分隔键,如 'common.home' 或 'hero.title' * @param mixed ...$args 占位参数(按顺序替换 {0}, {1}... 或命名参数) * @return string 翻译后的文本,找不到则返回 key 本身 */ public static function trans($key, ...$args) { $data = self::load(self::$locale); $value = self::getNested($data, $key); if ($value === null) { // 回退到中文 if (self::$locale !== 'zh_CN') { $dataZh = self::load('zh_CN'); $value = self::getNested($dataZh, $key); } if ($value === null) return $key; } // 替换占位符 if (!empty($args)) { $value = self::replacePlaceholders($value, $args); } return $value; } /** * 获取所有可用语言列表。 * @return array [['code'=>'zh_CN','name'=>'简体中文'], ...] */ public static function available() { $list = []; if (!is_dir(self::$dir)) return $list; foreach (glob(self::$dir . DIRECTORY_SEPARATOR . '*.json') as $f) { $code = pathinfo($f, PATHINFO_FILENAME); $data = json_decode(file_get_contents($f), true); if (isset($data['_meta']['name'])) { $list[] = [ 'code' => $code, 'name' => $data['_meta']['name'], 'direction' => $data['_meta']['direction'] ?? 'ltr', ]; } } return $list; } /** * 获取当前语言的 HTML lang 属性值。 */ public static function htmlLang() { $map = ['zh_CN' => 'zh-CN', 'en_US' => 'en']; return $map[self::$locale] ?? substr(self::$locale, 0, 2); } // ─── 内部方法 ─── /** * 加载语言包(带缓存)。 */ private static function load($locale) { if (isset(self::$cache[$locale])) return self::$cache[$locale]; $file = self::$dir . DIRECTORY_SEPARATOR . $locale . '.json'; if (!file_exists($file)) return []; $json = file_get_contents($file); self::$cache[$locale] = json_decode($json, true) ?: []; return self::$cache[$locale]; } /** * 从嵌套数组中用点分隔 key 取值。 */ private static function getNested($arr, $key) { $keys = explode('.', $key); $val = $arr; foreach ($keys as $k) { if (!is_array($val) || !array_key_exists($k, $val)) return null; $val = $val[$k]; } return is_string($val) || is_numeric($val) ? (string)$val : null; } /** * 替换文本中的占位符。 * 支持 {named} 和 {0}, {1} 格式。 */ private static function replacePlaceholders($text, $args) { // 命名参数: __('key', ['points' => 5]) 或 __('key', 5) if (count($args) === 1 && is_array($args[0])) { $map = $args[0]; foreach ($map as $k => $v) { $text = str_replace('{' . $k . '}', $v, $text); } } else { // 位置参数: {0}, {1} foreach ($args as $i => $v) { $text = str_replace('{' . $i . '}', $v, $text); } } return $text; } } /** * 全局翻译函数快捷方式。 * * @param string $key 翻译键 * @param mixed ...$args 占位参数 * @return string */ if (!function_exists('__')) { function __($key, ...$args) { return Lang::trans($key, ...$args); } }