Gowhich

Durban's Blog

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.4

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

泛型

泛型约束

我之前分享的一个例子中,有时候想操作某类型的一组值,并且知道这组值具有什么样的属性。在loggingIdentity例子中,我们想访问arg的length属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

1
2
3
4
5
6
7
8
9
interface LengthDefine {
length: number;
}

function loggingIdentity<T extends LengthDefine>(arg: T): T {
console.log(arg.length);

return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

1
loggingIdentity(3);

运行后会遇到如下错误提示

1
2
⨯ Unable to compile TypeScript:
src/generics_5.ts(11,17): error TS2345: Argument of type '3' is not assignable to parameter of type 'LengthDefine'.

我们需要传入符合约束类型的值,必须包含必须的属性:

1
loggingIdentity({length: 10, value:3});

运行后会得到如下结果

1
2
$ npx ts-node src/generics_5.ts
10

在泛型约束中使用类型参数

我们可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束。

1
2
3
4
5
6
7
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}

let x = {a:1, b:2, c:3, d:4};
getProperty(x, "a"); // 正常
getProperty(x, "m"); // 异常

运行后得到如下错误信息

1
2
3
$ npx ts-node src/generics_5.ts
⨯ Unable to compile TypeScript:
src/generics_5.ts(21,16): error TS2345: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

在泛型里使用类类型

在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,

1
2
3
function create<T> (c: {new(): T;}): T {
return new c();
}

一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Keeper1 {
hasMask: boolean;
}

class Keeper2 {
nameTag: string;
}

class Keeper3 {
numLength: number;
}

class ChildrenKeeper1 extends Keeper3 {
keeper: Keeper1;
}

class ChildrenKeeper2 extends Keeper3 {
keeper: Keeper2;
}

function createInstance<A extends Keeper3> (c: new() => A): A {
return new c();
}

console.log(createInstance(ChildrenKeeper1));
console.log(createInstance(ChildrenKeeper2));

运行后得到如下输出

1
2
3
$ npx ts-node src/generics_5.ts
ChildrenKeeper1 {}
ChildrenKeeper2 {}

感觉没在实际应用中使用,很鸡肋呀

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.5

首先我们要知道如何在命令行下连接,了解了之后就清楚大概的原理了
命令行连接的方式如下

1
sftp -P port user@host

如果端口号默认是22的话就不需要端口号的参数,如下

1
sftp user@host

连接进去之后sftp支持大多数的linux命令,如ls、cd等,但是注意并不是所有的命令都支持,毕竟这是个文件传输的功能,没有太多复杂的命令
上面了解之后我们看下PHP中如何链接

连接sftp

1
2
3
4
5
6
7
8
9
10
11
12
$conf = [
'channelId' => '',
'host' => '',
'port' => '',
'user' => '',
'password' => ''
];
$conn = ssh2_connect($conf['host'], $conf['port']);

if (!ssh2_auth_password($conn, $conf['user'], $conf['password'])) {
var_dump('ftps 连接失败');
}

获取远程文件

第一步没有问题,说明我们已经进去了sftp里面,然后就可以进行文件下载

设置要获取的远程文件

1
$remotFile = '/file/xxx/xxx/xxx.txt';

配置本地存储文件的路径

1
2
3
4
5
6
7
8
9
$localPath = '/storage/data';

// 创建文件夹
if (!is_dir($localPath)) {
$dir = mkdir($localPath, 0777, true);
if (!$dir) {
return false;
}
}

设置本地要存储的文件

1
2
3
4
5
6
7
8
9
// 如果文件已存在就覆盖
$localFile = 'xxxxx.txt';

$localRealFile = $localPath . '/' . $localFile;

// 如果文件存在则删除,当然这里也可以根据需求进行修改
if (is_file($localRealFile)) {
unlink($localRealFile);
}

最后拉取文件并写到本地

1
2
3
4
5
6
$sftp = ssh2_sftp($conn);

$resource = "ssh2.sftp://{$sftp}" . $remotFile;

//远程文件 拷贝到本地
copy($resource, $localRealFile);

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.3

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

泛型

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用(<>)括起泛型类型,跟在类名后面。

1
2
3
4
5
6
7
8
class GenerateT<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}

let generateT1 = new GenerateT<number>();
generateT1.zeroValue = 1
generateT1.add = (x, y) => { return x + y; }

GenerateNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

1
2
3
4
5
6
7
8
9
10
class GenerateT<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}

let generateT2 = new GenerateT<string>();
generateT2.zeroValue = "";
generateT2.add = (x, y) => { return x + y; }

console.log(generateT2.add(generateT2.zeroValue, "test"));

与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

我们在类那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.4

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.2

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

泛型

泛型类型

上一篇文章的分享,我们创建了identity通用函数,可以适用于不同的类型。 在这次分享中分享一下函数本身的类型,以及如何创建泛型接口。泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样,如下

1
2
3
4
5
function identity<T> (arg: T) : T {
return arg;
}

let otherIdentity: <T> (arg: T) => T = identity;

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以,如下

1
2
3
4
5
function identity<T> (arg: T) : T {
return arg;
}

let other1Identity: <U> (arg: U) => U = identity;

我们还可以使用带有调用签名的对象字面量来定义泛型函数,如下

1
2
3
4
5
function identity<T> (arg: T) : T {
return arg;
}

let other2Identity: { <U>(arg: U): U } = identity;

这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口,如下

1
2
3
4
5
6
7
8
9
function identity<T> (arg: T) : T {
return arg;
}

interface GenerateIdentityFunc {
<U> (arg: U): U;
}

let other3Identity: GenerateIdentityFunc = identity;

一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string>而不只是Dictionary)。 这样接口里的其它成员也能知道这个参数的类型了。

1
2
3
4
5
6
7
8
9
function identity<T> (arg: T) : T {
return arg;
}

interface GenerateIdentityFunc1<U> {
(arg: U): U
}

let other4Identity: GenerateIdentityFunc1<number> = identity;

注意,我们的示例做了少许改动。 不再描述泛型函数,而是把非泛型函数签名作为泛型类型一部分。 当我们使用 GenerateIdentityFunc1的时候,还得传入一个类型参数来指定泛型类型(这里是:number),锁定了之后代码里使用的类型。 对于描述哪部分类型属于泛型部分来说,理解何时把参数放在调用签名里和何时放在接口上是很有帮助的。

除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.3

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.1

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

泛型

使用泛型变量

使用泛型创建像上篇分享提到的identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

看下之前identity例子:

1
2
3
function identity<T>(arg: T): T {
return arg
}

如果我们想同时打印出arg的长度。 我们很可能会这样做:

1
2
3
4
function loggingIdentity<T>(arg: T): T {
// console.log(arg.length); // no length property
return arg
}

如果这么做,编译器会报错说我们使用了arg的.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}

使用过其它语言的话,你可能对这种语法已经很熟悉了。 在下一次的分享,会介绍如何创建自定义泛型像 Array一样。

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.2

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.2.6

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

泛型

简介

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

泛型的简单使用

下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是 echo命令。

不用泛型的话,这个函数可能是下面这样:

1
2
3
function identity(arg: number): number {
return arg;
}

或者

1
2
3
function identity(arg: any): any {
return arg
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 “类型变量”,它是一种特殊的变量,只用于表示类型而不是值。

1
2
3
function identity<T>(arg: T): T {
return arg;
}

我们给identity添加了类型变量T。 T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数,如下:

1
2
3
4
5
6
7
function identity<T>(arg: T): T {
return arg;
}

let output = identity<string>("someString");
console.log(output);
console.log(typeof output);

运行后输出结果如下

1
2
3
$ npx ts-node src/generics_1.ts
someString
string

这里我们明确的指定了T是string类型,并做为一个参数传给函数,使用了<>括起来而不是()。第二种方法更普遍。利用了类型推论 – 即编译器会根据传入的参数自动地帮助我们确定T的类型,如下

1
2
3
4
5
6
7
function identity<T>(arg: T): T {
return arg;
}

let output = identity("other someString");
console.log(output);
console.log(typeof output);

运行后输出结果如下

1
2
3
$ npx ts-node src/generics_1.ts
someString
string

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看other someString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.3.1

我自己在写项目的时候,不喜欢使用php自身的模板,主要是各种PHP标签让我烦,而且对Html的标签兼容也不够友好,所以我后面采用了twig模板,配置之类的也是很方便,写起来也很顺手,但是在Yii2语言国际化翻译这块就遇到了坑,当我们指定文件类型,除了处理php扩展的之外,也处理twig扩展的文件的时候,就不会解析twig中的内容,因为不符合PHP的标签处理逻辑,在PHP中我们使用Yii::t(),但是在twig中使用的是Yii.t()这个函数在translator的配置中,显得很乏力,而且看源码也可以发现,实际上也只处理php文件,网上找了很多针对这个问题的处理方式,似乎也没有几个使用的。现在看下我是如何解决的

第一步 显示修改i18n配置

将twig扩展加入进去,修改后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
return [
'color' => null,
'interactive' => true,
'help' => null,
'sourcePath' => '@app',
'messagePath' => '@app/messages',
'languages' => ['zh-CN', 'ru-RU'],
'translator' => 'Yii::t', // 翻译器
'sort' => false,
'overwrite' => true,
'removeUnused' => false,
'markUnused' => true,
'except' => [
'.svn',
'.git',
'.gitignore',
'.gitkeep',
'.hgignore',
'.hgkeep',
'/messages',
'/BaseYii.php',
'vendor',
'tests',
'runtime',
'migrations',
],
'only' => [
'*.php',
'*.twig', // 添加模板扩展
],
'format' => 'php',
'db' => 'db',
'sourceMessageTable' => '{{%source_message}}',
'messageTable' => '{{%message}}',
'catalog' => 'messages',
'ignoreCategories' => [],
'phpFileHeader' => '',
'phpDocBlock' => null,
];

第二步 继承重写

创建文件app/commands/TranslatorController.php,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<?php

namespace app\commands;

use yii\console\controllers\MessageController;
use yii\helpers\Console;

/**
* Extracts messages to be translated from source files.
*
* @author durban.zhang <xx@xx>
*/
class TranslatorController extends MessageController
{
public function init()
{
parent::init();
}

/**
* This command echoes what you have entered as the message.
* @param string $message the message to be echoed.
*/
protected function extractMessages($fileName, $translator, $ignoreCategories = [])
{
$messages = [];

$extInfo = pathinfo($fileName, PATHINFO_EXTENSION);

if ('twig' == $extInfo) {
$coloredFileName = Console::ansiFormat($fileName, [Console::FG_CYAN]);
$this->stdout("Extracting messages from $coloredFileName...\n");
$subject = file_get_contents($fileName);

$preg = '/\{\{ Yii\.t\(\'(.*?)\', \'(.*?)\'\) \}\}/';

$content = preg_replace($preg, "<?php Yii::t('$1', '$2'); ?>", $subject);

$tokens = token_get_all($content);

foreach ((array) $translator as $currentTranslator) {
$translatorTokens = token_get_all('<?php ' . $currentTranslator);

array_shift($translatorTokens);

$messages = array_merge_recursive(
$messages,
$this->extractMessagesFromTokens(
$tokens,
$translatorTokens,
$ignoreCategories));
}

$this->stdout("\n");
} else {
$messages = parent::extractMessages($fileName, $translator, $ignoreCategories);
}

return $messages;
}

protected function extractMessagesFromTokens(array $tokens, array $translatorTokens, array $ignoreCategories)
{
$messages = [];
$translatorTokensCount = count($translatorTokens);
$matchedTokensCount = 0;
$buffer = [];
$pendingParenthesisCount = 0;foreach ($tokens as $token) {
// finding out translator call
if ($matchedTokensCount < $translatorTokensCount) {
if ($this->tokensEqual($token, $translatorTokens[$matchedTokensCount])) {
$matchedTokensCount++;
} else {
$matchedTokensCount = 0;
}
} elseif ($matchedTokensCount === $translatorTokensCount) {
// translator found

// end of function call
if ($this->tokensEqual(')', $token)) {
$pendingParenthesisCount--;

if (0 === $pendingParenthesisCount) {
// end of translator call or end of something that we can't extract
if (isset($buffer[0][0], $buffer[1], $buffer[2][0]) && T_CONSTANT_ENCAPSED_STRING === $buffer[0][0] && ',' === $buffer[1] && T_CONSTANT_ENCAPSED_STRING === $buffer[2][0]) {
// is valid call we can extract
$category = stripcslashes($buffer[0][1]);
$category = mb_substr($category, 1, -1);

if (!$this->isCategoryIgnored($category, $ignoreCategories)) {
$message = stripcslashes($buffer[2][1]);
$message = mb_substr($message, 1, -1);

$messages[$category][] = $message;
}

$nestedTokens = array_slice($buffer, 3);
if (count($nestedTokens) > $translatorTokensCount) {
// search for possible nested translator calls
$messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($nestedTokens, $translatorTokens, $ignoreCategories));
}
} else {
// invalid call or dynamic call we can't extract
$line = Console::ansiFormat($this->getLine($buffer), [Console::FG_CYAN]);
$skipping = Console::ansiFormat('Skipping line', [Console::FG_YELLOW]);
$this->stdout("$skipping $line. Make sure both category and message are static strings.\n");
}

// prepare for the next match
$matchedTokensCount = 0;
$pendingParenthesisCount = 0;
$buffer = [];
} else {
$buffer[] = $token;
}
} elseif ($this->tokensEqual('(', $token)) {
// count beginning of function call, skipping translator beginning
if ($pendingParenthesisCount > 0) {
$buffer[] = $token;
}
$pendingParenthesisCount++;
} elseif (isset($token[0]) && !in_array($token[0], [T_WHITESPACE, T_COMMENT])) {
// ignore comments and whitespaces
$buffer[] = $token;
}
}
}

return $messages;
}
}

从上面的代码可以看出,实现了两个方法
extractMessagesextractMessagesFromTokens
本来是不需要实现extractMessagesFromTokens这个方法的,但是父类中的private,试过了实例化类,但是需要传递需要的参数,为了避免出现问题,暂时没用实例化的方式。
最后extractMessagesFromTokens 这个只是父类的copy版本。那么重点就在extractMessages
判断如果文件是twig文件,则进行处理,处理逻辑我写的也很简单,暂时能解决问题

1
2
3
$preg = '/\{\{ Yii\.t\(\'(.*?)\', \'(.*?)\'\) \}\}/';
$content = preg_replace($preg, "<?php Yii::t('$1', '$2'); ?>", $subject);
$tokens = token_get_all($content);

主要是将Yii.t转为了含有PHP标签的字符串,实际上父类也只是针对代码进行字符串的过滤处理,以此类推的话,如果有其他模板的话也可以采用此方法

到这里就处理完了,

但是执行的命令不是下面这个

1
./yii message/extract @app/config/i18n.php

而是下面这个

1
./yii translator/extract @app/config/i18n.php

OK,问题解决,如您有其他方案,请指教

如何实现语言国际自动化,大家可能觉得自动化,是不是不需要配置就自动切换,这个思路我之前也想过,能不能根据IP来判断地理位置然后确定其语言,网上找过一个”IpToCountry”相关的,有兴趣的可以搜索出来看看,他会提供一个ip对照的表,每隔一段时间会更新一次,不过这个暂时没做,后面考虑尝试下

这里我们说下Yii2如何实现,因为我们上面的一篇文章分享做了语言国际化的配置,也将对应的语言翻译了出来,下面就是需要根据条件来做切换
从配置文件我们知道只需要更改language这个配置值就可以了,但是要在哪里更改呢。这里我的操作步骤如下

我想整体对项目的所有内容进行语言国际化,唯一我能想到的是修改控制器,有的说是修改 入口文件,我觉得修改入口文件有点破坏框架结构了。

第一步 创建一个AppController

继承yii/web/Controller,实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;

class AppController extends Controller
{
public function init()
{
if (isset(Yii::$app->session['_lang'])) {
Yii::$app->language = Yii::$app->session['_lang'];
}

parent::init();
}
}

第二步 将所有自己的控制器都继承AppController

做类似如下操作,我这里只是举了一个BlogController的例子

1
class BlogController extends AppController

第三步 实现设置多语言的处理

我这里在我的控制器SiteController加了如下Action

1
2
3
4
5
6
7
8
9
public function actionLanguage($language)
{
Yii::$app->session['_lang'] = $language;
$redirectUrl = Yii::$app->request->headers['Referer'];
if (!$redirectUrl) {
$redirectUrl = Yii::$app->homeUrl;
}
return $this->redirect($redirectUrl);
}

每次需要修改语言的话,只要将语言传入actionLanguage,就会更改session中_lang的值,然后每个控制器在调用的时候都会先去更改项目的language

第四步 前端UI修改

通过在前端加个修改的逻辑,方便前端访问者进行更改语言设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="j#" data-target="#" class="dropdown-toggle" data-toggle="dropdown">
{{ Yii.t('app', 'Language') }}
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li
class="{% if app.session['_lang'] == 'zh-CN' %}active{% endif %}"
>
<a
href="{{ url(['site/language'], { 'language': 'zh-CN' })}}"
>{{ Yii.t('app', 'Chinese') }}</a>
</li>
<li
class="{% if app.session['_lang'] == 'en-US' %}active{% endif %}"
>
<a
href="{{ url(['site/language'], { 'language': 'en-US' })}}"
>{{ Yii.t('app', 'English') }}</a>
</li>
</ul>
</li>
</ul>

我这里使用的Twig模板。

到这里就都设置完了,可以正常切换语言了。后面如果又加了新的内容进去的话,可以直接执行上文中提到的命令

1
./yii message/extract @app/config/i18n.php

如有不理解的地方可以加群详细了解

最近想将博客做成支持多语言的,还好Yii2支持这个功能,于是查看了下官方的文档,哎,看了半天不知道干嘛用的,于是各种百度,Google的搜索,最终才明白原来很简单,只是官方写的太复杂

下面介绍下具体的使用步骤,具体介绍我就不写了,官方写比我清楚,我就写怎么使用

第一步 创建i18n配置文件

1
./yii message/config @app/config/i18.php // yii 在项目目录下 Yii2创建的时候自动生成的

执行完命令之后会在项目根目录config下创建一个i18n.php文件

为什么要创建这个文件,因为我们为了多语言处理,需要生成一个对应的映射文件,只要生成就好了,稍后的配置程序会自动调用处理

第二步 修改配置规则

打开config/i18n.php,看下生成的配置文件的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
return [
'color' => null,
'interactive' => true,
'help' => null,
'sourcePath' => '@yii',
'messagePath' => '@yii/messages',
'languages' => [],
'translator' => 'Yii::t',
'sort' => false,
'overwrite' => true,
'removeUnused' => false,
'markUnused' => true,
'except' => [
'.svn',
'.git',
'.gitignore',
'.gitkeep',
'.hgignore',
'.hgkeep',
'/messages',
'/BaseYii.php',
],
'only' => [
'*.php',
],
'format' => 'php',
'db' => 'db',
'sourceMessageTable' => '{{%source_message}}',
'messageTable' => '{{%message}}',
'catalog' => 'messages',
'ignoreCategories' => [],
'phpFileHeader' => '',
'phpDocBlock' => null,
];

修改后的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
return [
'color' => null,
'interactive' => true,
'help' => null,
'sourcePath' => '@app',
'messagePath' => '@app/messages',
'languages' => ['zh-CN', 'ru-RU'],
'translator' => 'Yii::t',
'sort' => false,
'overwrite' => true,
'removeUnused' => false,
'markUnused' => true,
'except' => [
'.svn',
'.git',
'.gitignore',
'.gitkeep',
'.hgignore',
'.hgkeep',
'/messages',
'/BaseYii.php',
'vendor',
],
'only' => [
'*.php',
],
'format' => 'php',
'db' => 'db',
'sourceMessageTable' => '{{%source_message}}',
'messageTable' => '{{%message}}',
'catalog' => 'messages',
'ignoreCategories' => [],
'phpFileHeader' => '',
'phpDocBlock' => null,
];

我这里只改了两个地方

1
2
3
'sourcePath' => '@app', // 将@yii改为@app 只处理我们自己应用中的代码
'messagePath' => '@app/messages', // 将@yii/messages改为@app/messages 将需要翻译的字段提取出来要放的目录
'languages' => ['zh-CN', 'ru-RU'], // 要翻译成目标的语言,我这里定义了一个"中文"和"俄语"

1
2
3
4
5
6
7
8
9
10
11
'except' => [
'.svn',
'.git',
'.gitignore',
'.gitkeep',
'.hgignore',
'.hgkeep',
'/messages',
'/BaseYii.php',
'vendor', // 将vendor目录下的过滤掉,不然可能太多了
],

第三步 生成翻译配置文件

执行下面的命令

1
./yii message/extract @app/config/i18n.php

执行完之后会在messages目录下(如果没有messages目录的话需要手动创建下)得到如下的目录结构

1
2
3
4
├── ru-RU
│ └── app.php
└── zh-CN
└── app.php

提示下再做这个操作之前,需要在自己的项目中有类似Yii:t()的调用,比如我这里在components/HeaderWidget.php这个文件中

1
Yii::t('app', 'Home')

这里的app的作用是用来进行文件分类的,我这里暂时没有计划生成的时候会将所有需要翻译的字段放在app开头命名的php文件app.php文件中
如果像下面这样调用的话

1
Yii::t('appp', 'Home')

会生成一个appp.php的文件

第四步 翻译配置文件

看下中文的翻译文件messages/zh-CN/app.php,我的是下面这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
return [
'Archive' => '',
'Autokid' => '',
'Blog' => '',
'Ctime' => '',
'IP地址' => '',
'UserAgent' => '',
'主题' => '',
'内容' => '',
'姓名' => '',
'日期' => '',
'邮箱地址' => '',
'页面路径' => '',
'Home' => '首页', // 右边的键值对应 Yii::t('app', 'Home')中的Home,只需要在value中写入需要的汉字就可以了。
];

第五步 修改目标国际化语言

修改配置文件

1
'language' => 'zh-CN', // 指定为要翻译的语言

再打开网页,就可以看到已经翻译成了对应需要的语言,当然这样的配置很不灵活,如果是部署多态机器并通过域名或者其他方式来实现的话,也是可以的,这里的话我建议如下方式

创建自己的Controller,然后将语言配置放在Session中,通过获取Session中的语言来更换全站的语言。具体见后面分享

提示,网站很多地方提到要加个配置,官方也是

1
2
3
4
5
6
7
8
9
10
11
12
'i18n' => [
'translations' => [
'app*' => [
'class' => 'yii\i18n\PhpMessageSource',
'basePath' => '@app/messages',
'sourceLanguage' => 'en-US',
'fileMap' => [
'app' => 'app.php',
],
],
],
],

我这边在配置的时候没有加,运行也都是正常的,如有遇到问题,可以一起沟通交流下。

项目实践仓库

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.2.5

为了保证后面的学习演示需要安装下ts-node,这样后面的每个操作都能直接运行看到输出的结果。

1
npm install -D ts-node

后面自己在练习的时候可以这样使用

1
npx ts-node 脚本路径

函数

重载

JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。如下实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: any): any {
if (typeof x == "object") {
let pickCard = Math.floor(Math.random() * x.length);
return pickCard;
} else if (typeof x == 'number') {
let pickedSuit = Math.floor(x / 13);
return {
suit: suits[pickedSuit],
card: x % 13,
}
}
}

let myDeck = [
{
suit: "diamands",
card: 2,
},
{
suit: 'spades',
card: 10,
},
{
suit: 'hearts',
card: 4
}
]

let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);

console.log('card: ' + pickedCard1.card + ' of ' + pickedCard1.suit);
console.log('card: ' + pickedCard2.card + ' of ' + pickedCard2.suit);

运行后得到类型如下结果

1
2
3
$ npx ts-node src/function_7.ts
card: 2 of diamands
card: 2 of spades

pickCard方法根据传入参数的不同会返回两种不同的类型。 如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。 如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载 pickCard函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string, card: number}[]): number;
function pickCard(x: number): {suit: string, card: number};
function pickCard(x: any): any {
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
} else if (typeof x == 'number') {
let pickedSuit = Math.floor(x / 13);
return {
suit: suits[pickedSuit],
card: x % 13,
}
}
}

let myDeck = [
{
suit: "diamands",
card: 2,
},
{
suit: 'spades',
card: 10,
},
{
suit: 'hearts',
card: 4
}
]

let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);

console.log('card: ' + pickedCard1.card + ' of ' + pickedCard1.suit);
console.log('card: ' + pickedCard2.card + ' of ' + pickedCard2.suit);

得到的结果类似如下

1
2
3
$ npx ts-node src/function_7.ts
card: 10 of spades
card: 2 of spades

这样改变后,重载的pickCard函数在调用的时候会进行正确的类型检查。

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function pickCard(x: any): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard会产生错误。

本实例结束实践项目地址

1
2
https://github.com/durban89/typescript_demo.git
tag: 1.2.6
0%