语言类: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;
}
```
- FA的JS调用机制说明
- FA的JS之Fast.api逐个详解
- FA页面渲染时后端传递数据给前端的方式
- FA的ajax查询数据的前后台流程
- FA特有的函数解释
- FA的鉴权Auth类
- extend\fast\Auth.php详解
- application\admin\library\Auth.php详解
- application\common\library\Auth.php详解
- FA的Token机制
- FA管理员(后台)的权限机制
- FA用户(前台和API)的权限机制
- FA在前台模板文件中进行鉴权
- FA的登录页面
- TP类Hook:钩子机制
- TP类Lang:多语言机制
- TP类Config:参数配置机制
- TP类Request:请求类
- TP的模型关联详解
- think-queue队列组件
- Queue.php
- \queue\Connector.php
- \queue\connector\Redis.php
- \queue\Job.php
- queue\job\Redis.php
- PHP规则:正则表达式
- PHP规则:闭包与匿名函数
- 项目架构说明
- 代码架构
- TP数据库where条件的各种写法
