Files
freeshop/includes/Lang.php
T
2026-08-14 10:55:06 +08:00

208 lines
6.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 多语言(i18n)系统
*
* 使用 JSON 语言包,支持动态切换。
* 用法:
* echo __('common.home'); // "首页"
* echo __('product.buy_now'); // "立即购买"
* echo __('sign.points_gained', 5); // "获得 5 积分"(支持 {points} 占位符)
* Lang::setLocale('en_US'); // 切换语言
*/
if (!defined('IN_APP')) exit('Forbidden');
class Lang
{
/** @var string 当前语言代码 */
private static $locale = 'zh_CN';
/** @var array 已加载的语言包缓存 [code => 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);
}
}