ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、视频、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
语言类:thinkphp\library\think\Lang.php **作用说明:** 作用是实现多语言自动转换,有时候我们的系统需要国际化,客户可能是多国客户; 这些对于一些提示语,页面显示的文字都需要显示成对应国家的语言,默认是中文,其次是英文,然后还可以配置任意国家的语言包; 语言的转换机制就是: 代码中使用 英文为标准,然后通过一个语言函数 `__`(),框架去获取当前需要什么语言包,然后加载这个语言包(其实就是一个大数组),在里面找到这个英文(索引)所对应的值(对应的语言内容), 返回这个值; ``` namespace think; ``` ``` /** * @var array 语言数据 */ private static $lang = []; /** * @var string 语言作用域 * 默认就是标准简体中文 */ private static $range = 'zh-cn'; /** * @var string 语言自动侦测的变量 * TP在自动检查客户端语言时所使用的语言参数名称 */ protected static $langDetectVar = 'lang'; /** * @var string 语言 Cookie 变量 * TP在自动检查客户端 Cookie语言时所使用的语言参数名称 */ protected static $langCookieVar = 'think_var'; /** * @var int 语言 Cookie 的过期时间 */ protected static $langCookieExpire = 3600; /** * @var array 允许语言列表 * 空数组表示允许任何语言,如果不是空数组,则只允许使用在数组中的语言 */ protected static $allowLangList = []; /** * @var array Accept-Language 转义为对应语言包名称 系统默认配置 * 某些语言含有几种名称写法,使用这个数组进行转化成标准写法 */ protected static $acceptLanguage = ['zh-hans-cn' => 'zh-cn']; ``` **设定/获取语言域range():** TP把语言数据存放在$lang数组中,然后给每种语言分配一个分组,也就是第一维数组的索引就是语言名称,然后把这个语言的数据存放在这个数组里面,第二维数组的索引就是语言定义, 语言数组的结构如下: ``` $lang = [ '语言名称':[ '语言定义':'值', ] ]; ``` ``` /** * 设定/获取 当前的语言 * @access public * @param string $range 语言作用域 * @return string */ public static function range($range = '') { if ($range) { self::$range = $range; } return self::$range; } ``` *注意:range 本身是一个PHP内置函数: 根据范围创建数组,包含指定的元素.这里不要搞错了.我们在创建函数名称时,原则之一就是不要跟内置函数重名,这里TP命名得不够好;* 用法:Lang::range('zh-cn') ; 如果不传参数,就是获取当前的语言域; TP在thinkphp\library\think\App.php 的run() 中进行了运行: ``` // 默认语言 Lang::range($config['default_lang']); ``` 设置了配置中的语言域; **设置语言定义set():** 语言定义是不区分大小写的,全部会被转为小写; 一般的语言定义都是定义在文件里面的,如果要在代码中定义也可以, 但一般不太推荐. `set($name, $value = null, $range = '')` 参数: 定义名称,值,语言域; 定义一条时,$name是字符串,如果是批量定义,则是数组, $name如果是数组,则数组索引代表 定义名称,元素值代表值, 如果原来语言数组已经存在相同的 定义名称,则会被新的覆盖掉; ``` /** * 设置语言定义(不区分大小写) * @access public * @param string|array $name 语言变量 * @param string $value 语言值 * @param string $range 语言作用域 * @return mixed */ public static function set($name, $value = null, $range = '') { $range = $range ?: self::$range; if (!isset(self::$lang[$range])) { self::$lang[$range] = []; } if (is_array($name)) { //array_change_key_case() 将 array 数组中的所有键名改为全小写(默认)或大写。本函数不改变数字索引。 //所有的语言定义都是小写的,所以不区分大小写 //当两个数组相加时,会把第二个数组的值添加到第一个数组上,相同索引的值以第一个数组为准 return self::$lang[$range] = array_change_key_case($name) + self::$lang[$range]; } return self::$lang[$range][strtolower($name)] = $value; } ``` **多语言机制的语言文件的加载,`load`()函数:** 在thinkphp\library\think\App.php的run 函数中, ``` // 默认语言 Lang::range($config['default_lang']); // 开启多语言机制 检测当前语言 $config['lang_switch_on'] && Lang::detect(); $request->langset(Lang::range()); // 加载系统语言包 Lang::load([ //先加载TP框架的语言文件 THINK_PATH . 'lang' . DS . $request->langset() . EXT, //再加载系统的语言文件,但FA并没有设置系统级别的语言文件夹 APP_PATH . 'lang' . DS . $request->langset() . EXT, ]); ``` 先根据配置文件,设定 默认语言; 如果开启了多语言,就侦测一下当前需要什么语言,然后去到lang文件夹,加载这个与这个语言名称一样的文件. TP框架会一层层地去加载语言文件, 框架->系统->模块->控制器: 1是TP框架层次,这个文件是:thinkphp\lang\zh-cn.php, 2然后加载应用层次的语言文件,但FA并没有写,所以这里加载的是一个不存在的文件,不存在的话就会跳过, 3加载模块层次的语言文件,在thinkphp\library\think\App.php的init函数里面:`Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT);` 4然后是控制器层次的语言文件,在application\common\controller\Backend.php(每个模块的基类都有)的:`$this->loadlang($controllername);` 直接Lang::load(),不带参数,则是返回当前语言域的整个语言数组; **获取语言定义get():** get方法里面需要特别理解TP是 如何替换语言定义里面的占位字符的. ``` /** * 获取语言定义(不区分大小写) * @access public * @param string|null $name 语言变量 * @param array $vars 变量替换 * @param string $range 语言作用域 * @return mixed */ public static function get($name = null, $vars = [], $range = '') { $range = $range ?: self::$range; // 空参数返回所有定义 if (empty($name)) { return self::$lang[$range]; } $key = strtolower($name); $value = isset(self::$lang[$range][$key]) ? self::$lang[$range][$key] : $name; // 变量解析 if (!empty($vars) && is_array($vars)) { /** * Notes: * 为了检测的方便,数字索引的判断仅仅是参数数组的第一个元素的key为数字0 * 如果是第一个元素key是0,后面key是关联索引也不管了. 因为php并没有能够直接判断一个数组是不是纯数字索引的函数 * 如果专门去每个key去循环判断,不是不可以但是有点浪费资源 * 数字索引采用的是系统的 sprintf 函数替换,用法请参考 sprintf 函数 :https://www.php.net/manual/en/function.sprintf.php */ //key() 函数返回数组中内部指针指向的当前单元的键名。 但它不会移动指针。如果内部指针超过了元素列表尾部,或者数组是空的,key() 会返回 null。 :https://www.php.net/manual/zh/function.key.php //这里返回的是第一个元素,因为内部指针没有被用过,所以指向第一个元素 if (key($vars) === 0) { // 数字索引解析 //array_unshift() 将传入的单元插入到 array 数组的开头。注意单元是作为整体被插入的,因此传入单元将保持同样的顺序。所有的数值键名将修改为从零开始重新计数,所有的文字键名保持不变。 array_unshift($vars, $value); //sprintf — 返回格式化字符串(这个函数比较复杂,需要去看官方文档) :https://www.php.net/manual/zh/function.sprintf.php //这里的作用就是:把$vars里面的值,替换到语言值中,比如语言值:这里有%d个,变成:这里有6个. $value = call_user_func_array('sprintf', $vars); } else { // 关联索引解析 //array_keys — 返回数组中部分的或所有的键名 $replace = array_keys($vars); //TP编写语言文件的格式: //'directory {:path} creation failed' => '目录 {:path} 创建失败!', //循环里面有两对{},因为外面一对是对应{:path}的外面这对,里面包裹变量的那对,其实是用来限定变量名字的, //没有{}包住变量的话,php会认为'v}' ,是变量的名字; //如果使用单引号,则可以写成:'{:'.$v.'}'; foreach ($replace as &$v) { $v = "{:{$v}}"; } //https://www.php.net/manual/zh/function.str-replace.php //这里的作用:把replace里面的值所对应的$value字符串里面的位置,替换成vars里面的值 //例如:replace里面第一个元素值是: {:name} ,vars里面第一个元素值是: 张三, value是:你好,{:name}; //最终得到的value是: 你好,张三; //这里替换的顺序是按顺序替换,不是根据元素的key来一一对应替换的; //PHP官方文档说法:如果 search 和 replace 都是数组,它们的值将会被依次处理; //注意在TP这里,$replace是search,是代表要搜索的内容, 跟PHP官方说法相反,PHP官方说法更清晰易理解 $value = str_replace($replace, $vars, $value); } } return $value; } ``` **TP的语言函数lang()(也就是Lang::get)和FA的语言函数__()(两个下划线)的比较说明:** TP的lang函数是助手函数,在thinkphp\helper.php 中定义 ``` function lang($name, $vars = [], $lang = '') { return Lang::get($name, $vars, $lang); } ``` 其实就是调用Lang的get; FA的语言函数__(),在application\common.php中定义 ``` function __($name, $vars = [], $lang = '') { if (is_numeric($name) || !$name) { return $name; } if (!is_array($vars)) { $vars = func_get_args(); array_shift($vars); $lang = ''; } return \think\Lang::get($name, $vars, $lang); } ``` 最终还是调用了 Lang的get; 只不过FA做了一些前置判断: 差别1:FA如果是数字或者"否"值, 就原样返回, 但TP的机制是 如果是空值(`empty`)就返回这个语言的所有语言定义,是整个语言域的数组,也就是中文的话,就整个中文语言数组,这个语言数组非常大,如果是不存在(!isset)对应的值,就原样返回; 这样做的好处:语言数组的key不可能是数字, 如果key是数字,就直接原样返回就是了.不需要浪费系统资源了; 如果这个key是空值,也不应该返回对应语言域的整个数组,这样的返回有什么意义?拿到整个域数组然后在自己进行加工?不如说这只是TP作者预留下来的一个获取整个语言域数组的方法,但是这个跟load函数实现了一样的功能,有点多余; 差别2:FA会对vars参数进行预先处理: ``` if (!is_array($vars)) { //func_get_args — 返回一个包含函数参数列表的数字索引数组 //作用就是把$name,$vars,$lang 合并到一个数组中,而vars是一个字符串 $vars = func_get_args(); //array_shift() 将 array 的第一个单元移出并作为结果返回,将 array 的长度减一并将所有其它单元向前移动一位。所有的数字键名将改为从零开始计数,文字键名将不变。 //作用:把$name移出数组 array_shift($vars); //作用:给语言域设成默认,因为这里把第二个参数开始的参数都当作是替换的值,你传lang值过来也会被当作是替换值,也就不存在lang值了,这里要设定一个空值,不然后面的语言域就对应错了,语言域会对应第三个参数,但是根本不存在这个语言域啊; //关于这一点,FA文档有说明,如果你第二个参数vars不是数组那么你就会丧失指定语言域的能力. //关于FA对于TP的Lang::get的二次封装,我认为是多此一举,反而变得不够严谨. //为了多那么一点参数写法的"自由度",却丧失了指定语言域的能力, //而且我不认为这个自由是好的,我更喜欢严谨地以数组形式传递替换值,避免搞错 $lang = ''; } ``` 我的结论是: FA的预处理基本是多余的.使用TP的设计就可以了. **自动侦测客户的语言detect():** TP框架会从3个层次去侦查客户到底是要使用哪种语言,优先级依次递减; 第一层:直接从URL中获取语言参数,如果存在这个参数就把这个参数的值作为语言; 第二层:从cookie中获取; 第三层:从http的header中获取 `Accept-Language`的值,这个值写法是: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2,[具体解释](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Language) PHP的 `$_SERVER` 中有这个 'HTTP_ACCEPT_LANGUAGE'当前请求头中 `Accept-Language:` 项的内容。 然后使用正则匹配出里面的第一个符合的语言名称,就当作是默认语言域; *注意:TP在这里并没有去考虑 `Accept-Language`的q参数的权重作用,而是谁排最前面就选谁.而这也符合一般把权重高的语言写前面的惯用写法;* 关于第一层,我们需要注意一点:我们不能在url中带lang 这个名字的参数,否则会TP认为你是使用这个语言的; 因为'lang'是TP默认的语言变量名,当然你也可以自己去修改,'think_var'是cookie默认的 语言变量名; ``` /** * 自动侦测设置获取语言选择 * @access public * @return string */ public static function detect() { $langSet = ''; if (isset($_GET[self::$langDetectVar])) { // url 中设置了语言变量,默认语言变量名langDetectVar是lang, 如果url中带有lang=xxx 这样的,就认为是xxx语言的环境,所以lang这个名称不能用于自定义的参数名称 $langSet = strtolower($_GET[self::$langDetectVar]); } elseif (isset($_COOKIE[self::$langCookieVar])) { // Cookie 中设置了语言变量 $langSet = strtolower($_COOKIE[self::$langCookieVar]); } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // 自动侦测浏览器语言 //这个正则的解释: a至z 或者 数字 或者 - 组成的, 至少有一个字符(也就是不能为空) ,以前面这些条件开头的,忽略大小写的 //例子:123zh-Hans-CN,zh--CN,zh-CN,zh-cN. 这些都是符合的 //正则中有\d,表示数字,但实际上根本不存在有数字在其中的标准语言名称 preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches); //这里为什么要第二个元素,需要了解preg_match函数的用法 https://www.php.net/manual/zh/function.preg-match.php //0号元素是完整匹配到的文本(是长的), 1号开始是按文本顺序子匹配到的文本(是短的), //反正就把第一个匹配到的短的文本当作是语言 $langSet = strtolower($matches[1]); $acceptLangs = Config::get('header_accept_lang');//这个header_accept_lang并没有在config文件中,如果有需要可以自己添加 //进行一下语言转义,比如 'zh-hans-cn' => 'zh-cn',这两个都是指简体中文 if (isset($acceptLangs[$langSet])) { $langSet = $acceptLangs[$langSet]; } elseif (isset(self::$acceptLanguage[$langSet])) { $langSet = self::$acceptLanguage[$langSet]; } } // 合法的语言 //是否在允许的语言列表中,如果在 就把语言域设置成这个语言 if (empty(self::$allowLangList) || in_array($langSet, self::$allowLangList)) { self::$range = $langSet ?: self::$range; } return self::$range; } ``` **获取语言定义的值has():** `has($name, $range = '')` 如果不存在,则返回false; ``` /** * 获取语言定义(不区分大小写) * @access public * @param string|null $name 语言变量 * @param string $range 语言作用域 * @return mixed */ public static function has($name, $range = '') { $range = $range ?: self::$range; return isset(self::$lang[$range][strtolower($name)]); } ``` 还有4个设置参数的函数,这几个函数几乎不会用到: **设置语言自动侦测的变量** ` public static function setLangDetectVar($var)` : 这个 `langDetectVar`是在侦测客户端语言时,在url层次使用的变量名称,默认是 lang, 一般就不要去改这个了;而且TP框架本身没有调用过这个函数; ``` /** * 设置语言自动侦测的变量 * @access public * @param string $var 变量名称 * @return void */ public static function setLangDetectVar($var) { self::$langDetectVar = $var; } ``` **设置语言的 cookie 保存变量** `public static function setLangCookieVar($var)` : 这个是侦测客户端语言时在cookie层使用的变量名称,默认是 `think_var`;而且TP框架本身没有调用过这个函数; ``` /** * 设置语言的 cookie 保存变量 * @access public * @param string $var 变量名称 * @return void */ public static function setLangCookieVar($var) { self::$langCookieVar = $var; } ``` **设置语言的 cookie 的过期时间** ` public static function setLangCookieExpire($expire)` ,默认3600 秒,几乎不会改动这个参数;而且TP框架本身没有调用过这个函数; ``` /** * 设置语言的 cookie 的过期时间 * @access public * @param string $expire 过期时间 * @return void */ public static function setLangCookieExpire($expire) { self::$langCookieExpire = $expire; } ``` **设置允许的语言列表** ` public static function setAllowLangList($list)` : 允许的语言列表, list 是数组,索引是数字,值是 语言标准名称. 如果表是empty的或者侦测到客户端语言是在这个表中的,就将语言域设置成这个语言,如果不在这里表中,则不会修改当前语言域等于是忽略. 默认是空表,而且TP框架本身没有调用过这个函数; ``` /** * 设置允许的语言列表 * @access public * @param array $list 语言列表 * @return void */ public static function setAllowLangList($list) { self::$allowLangList = $list; } ```