電影流水帳(2020/05/11~2020/05/31)

Q'Orianka
Q’orianka Kilcher 飾演 Dora and the Lost City of Gold 裡守護黃金城的公主。
  • Secret Superstar (IMDB, Wikipedia),台譯:隱藏的大明星。
  • Dora and the Lost City of Gold (IMDB, Wikipedia),台譯:朵拉與失落的黃金城。

Secret Superstart

早上吃早餐時,轉到Fox Movie,剛好「隱藏的大明星」剩下30分鐘。茵希雅去了孟買,跟沙克帝在告別,沙克帝跟她說,我雖然每次被提名都沒得獎,但像你這樣的才華,就跟汽水裡的泡泡一樣,是會自己浮上去的,會慢慢的發光發熱。沒多久,場景帶到茵希雅家裡,茵希雅跟媽媽說,我去了孟買找了沙克帝,我覺得你應該要離婚,我可以靠唱歌來養你跟弟弟。媽媽很驚訝也有點生氣,告訴他你怎麼可以自己去孟買,回來還叫我離婚。對,茵希雅長期受到丈夫的暴力對待,兒女都看不下去。這場爭執在母親斥責女兒後結束,茵希雅跑回房間去,這時奶奶走了過來,跟茵希雅說了她媽媽當時懷她的事情。本來在知道懷的是女兒以後,是決定要墮胎的,但是茵希雅的媽媽逃走躲起來,到了十個月後才抱著孩子回來求情說,下次一定會生個男生的,現在孩子也生下來了,我們就撫養她吧。

茵希雅這時才知道當初媽媽並不是她所想的那樣懦弱,而是鼓了很大的勇氣逃走,才生下她。於是她放下對媽媽的怨,配合媽媽一起準備搬家的東西,再過一陣子全家跟父親一起去沙烏地阿拉伯工作。電視上一直在播報著「隱藏的大明星」的報導,這個女孩蒙著面,彈著吉他唱歌的影片透過Youtube頻道在全國各地走紅,但是沒有人知道她是誰,只有她的好友跟沙克帝知道。但電視上一直在播放著,媽媽也或多或少猜到了,她沒有多說什麼,只是看著最近女兒落寞的神情,也想著自己的處境,心疼,但是好像也不能做些什麼。在茵希雅學期結束後,一家人前往機場搭飛機,在排隊辦理登機程序,快輪到他們到櫃檯時,父親內急去上了廁所。就輪到他們了,可是媽媽不知所措,櫃檯人員本來要他們先讓後面的辦理,但茵希雅挺身而出,成熟的跟櫃檯人員應對,辦完手續。

上了飛機,茵希雅的位子上有人佔了她的位子,茵希雅婉轉的告知,並請他移座。在一旁的媽媽看到自己的女兒,深深的覺得自己女兒已經長大,可以成熟的應對了。到了孟買轉機,要託運行李,父親不願意多付吉他託運的錢。媽媽冷靜的跟父親說明,這是女兒的夢想,請他也一起託運,但是父親不肯,還威嚇她。接下來的一幕,讓我眼眶含著眼淚看完。媽媽不退縮,再次說明自己的立場,父親還是不願意,媽媽這時拿出了文件簽名,然後告知了自己女兒的事情,以及自己等一下要怎麼做。簡單的說,你如果動手打我,這裡有攝影機,而且你這樣做也會影響到你工作。我們婚姻就走到這裡,你就帶著你媽去沙烏地阿拉伯吧,等你回來,我們再來談後續的事情。說完,媽媽就帶著茵希雅跟弟弟走了。

後續,他們去參加了頒獎典禮,茵希雅得到了獎項,在台上發表了感人的演說。大概就這樣吧,很感人的故事,仔細想想,我碰到這種母愛、父愛的故事都很容易留下眼淚。

Dora and the Lost City of Gold

我覺得導演跟編劇的安排很好,直接把 Dora 拉到國高中的年紀,這樣可以讓演員的年紀不會那麼突兀。同時,也設定讓 Dora 從小在叢林長大,到了國高中才到城市裡唸書,讓小女孩經歷青少年對於交友的困擾,也讓 Dora 除了長大之外,還保有純真的一面。

故事蠻簡單的,看了前面 30 分鐘,大致上可以猜到後面的劇情。Dora 從小在叢林長大,受到父母的薰陶,對於冒險這件事情非常有興趣。表哥 Diego 小時候也是住在叢林,但並沒有住很久,之後就搬去城市唸書了。Dora 的父母接受了委託,要去調查事情,而 Dora 則被送到表哥所在的城市去唸書。Dora 在學校裡算是一個怪咖,不是很受歡迎,表哥 Diego 則對此感到困擾。某天校外教學,Dora 跟表哥以及其他兩個同學被湊成一組,然後就被壞人捉走了。壞人是為了黃金城而來的,他們想要挾持 Dora ,威脅 Dora 父母帶他們去找黃金城。Dora 跟其他三人被帶到叢林裡,被一個自稱是 Dora 父母朋友的教授救了,5個人就在叢林裡探險尋找黃金城。經過冒險的橋段之後,原來那個自稱是 Dora 父母朋友的教授就是壞人,利用好警察、壞警察的手法,設計 Dora 帶他們去找到黃金城。可是,Dora 也不是笨蛋,早就留了一手,讓壞人掉入陷阱,被叢林守護者捉住。最後,黃金城是找到了,可是也因為觸發機關的關係而被封住了。

整個故事就是很有家庭歡樂的氣氛,蠻適合陪孩子一起看的。

iperf3 測試網路速度

閱讀 指令式的網路速度測試工具 iPerf3 ,揪出網路頻寬真實的一面 以後做的小摘錄。

簡單說用法,因為是要測試內部網路的網路速度,所以要兩台電腦。一台執行

iperf3 -s

一台執行

iperf3 -c <server_ip>

這樣就可以了。也有公用的伺服器可以測,這樣就要一台電腦就行 (用 ipef3 -c <server_ip>),亞洲區只有兩台,一台在中亞(哈薩克),一台在印尼,要注意,使用公用伺服器的話,測出來的速度並不是網路內部電腦間的速度,而是電腦到Router到外部伺服器的速度。

flutter stable/beta 並存

簡單說,有專案因為怕被影響到,只想用 stable 來建置,不想用 beta,可是又想要試驗 flutter web ,而 flutter web 又只有 beta channel 才有,所以想讓他們並存。

其實意外的簡單,就只是放在不同目錄,只要在使用的時候小心,不要用到不對的 flutter 就行,在 terminal 直接下指令的話,會需要特別注意。

# 切到你要放 flutter beta channel 的資料夾
git clone https://github.com/flutter/flutter.git flutter-beta -b beta
cd flutter-beta/bin
./flutter precache
./flutter config —enable-web
./flutter devices

在 Android Studio 建立 flutter project 時,會問 flutter SDK 路徑,把路徑指到剛剛放 flutter beta channel 的位置,就可以了。

Visual Studio Code 的話,需要改 workspace preferences ,改裡面的 dart.flutterSdkPath (來源1 / 來源2 )

"settings": {
"dart.flutterSdkPath": "/Users/youruser/flutter-beta"
}

直接用指令的話,就要確定 PATH 是對的,這時候可以預先寫好環境變數檔案,然後用 source 來處理

# flutter-stable.env
export PATH=/Users/youruser/flutter-stable:$PATH
# flutter-beta.env
export PATH=/Users/youruser/flutter-beta:$PATH
# 切換到 stable
source flutter-stable.env

# 切換到 beta
source flutter-beta.env

紀錄一下 MacOS 的中文輸入法

之前有找過一次了,今天又再找一次,這次把找的過程記錄下來。那這次為什麼要找呢?是因為小麥注音輸入法不知道為什麼當機終止了,選不到這個輸入法,想說找穩定的輸入法來使用。

之前換小麥注音輸入法的主因是因為我實在是無法適應 MacOS 的內建注音輸入法,用了一陣子還是無法適應,特別是打注音符號,就斷然放棄。

言歸正傳,目前可以找到的幾個:

  1. 小麥注音
  2. Yahoo!奇摩注音輸入法
  3. 超注音
  4. 香草輸入法 (沒有注音)
  5. 自然注音輸入法
  6. 鼠鬚管

小麥注音

用 homebrew 就可以安裝:brew install mcbopomofo

目前免費,沒有智慧選字,也沒有詞庫,按 shift+, 可以輸入全形標點符號。 網址:https://github.com/openvanilla/McBopomofo

Yahoo!奇摩注音輸入法

安裝需要手動,homebrew 以前有這個 cask,但是後來被移出去了。

目前免費,有智慧選字,輸入法程式沒有持續更新,Yahoo 已經將這整個輸入法開放原始碼了,安裝包是 zonble 大大熱心去編譯並打包的 (zonble大大就是OpenVilla香草輸入法/小麥注音輸入法的開發者)。 網址:https://github.com/zonble/ykk_installer/

按 shift+, 可以輸入全形標點符號

介紹:

超注音

安裝要繞個路,透過 Google Play,我猜想這是開發者要收費的緣故。價格:500 NTD

安裝方法:

  1. 使用 Android 手機,去 Google play 安裝 (超注音 for macOS)
  2. 開啟 app,裡面有操作說明,簡單的說,這包 apk 裡面有超注音的安裝程式,你要依照操作說明把這個安裝程式放到你的 macos 機器上,然後安裝。

網址:https://www.superkbd.com/

參考資料:超注音 for macOS 開放下載!為 Mac 再添一款輸入法

香草輸入法

安裝:brew install openvanilla

如果你需要倉頡、簡易(速成)、大易、行列、符號與日文假名、粵拼等輸入法,裝這個就對了,不過我沒用過。

網址:https://openvanilla.org/

自然注音輸入法

老牌的輸入法,要上官方網站購買,網址:https://www.goingpro.me/products

價格 2000 NTD,如果你有雙系統需求,可以考慮購買 Win+Mac 共通版,價格 3500 NTD,可以安裝在三台電腦上。

鼠鬚管

感謝推友 Ralphsun73221 的推薦。鼠鬚管就是只有注音的 RIME (Linux版是中州韻,Windows 版是小狼毫)。

安裝:brew cask install squirrel

介紹:【RIME 鼠鬚管注音版】Mac 中最好用的注音輸入法!

最終的選擇

之前是使用小麥注音輸入法,這個先保留,不移除。現在則是安裝了 Yahoo!奇摩注音輸入法來試用看看,未來如果不好用,再來考慮安裝鼠鬚管或是購買超注音或是購買自然注音輸入法。

剛剛使用,注意到一點,選字的游標位置不太一樣:

  • 小麥注音輸入法跟內建的注音輸入法,選字是選前個位置的字。
  • Windows/Linux 上的輸入法跟Yahoo!奇摩注音輸入法,選字是選後面位置的字。

筆記:Best practices for REST API design

來源:Best practices for REST API design

下面是看完以後,我的整理、摘要以及一點自己的想法:

  • 盡可能使用 JSON (application/json),除非有特殊需求,才來考慮使用其他格式,例如 form (上傳檔案) 或是 xml
  • 使用名詞,而非動詞:這樣才能搭配 HTTP verbs 的 POST/DELETE/GET/PUT,POST 放新資料到伺服器上,DELETE 移除資料,GET 取得資料,PUT 是更新資料
  • 時使用複數,表示取得一堆資料。
  • 階層物件可以使用巢狀資源,例如 /articles/article_id/comments/ 。P.S. 碰到這種情況其實也可以考慮使用 GraphQL
  • 漂亮的處理錯誤並回傳錯誤代碼,盡可能利用 HTTP 狀態碼而不要另外建立新的錯誤代碼。伺服器端在回傳錯誤時,可以帶上錯誤訊息,讓客戶端便於判斷、處理。
  • 允許篩選、排序跟分頁。篩選跟分頁可以避免一次拿過多資料,導致伺服器傳輸過多資料,也可以提升速度,拿的少,自然就快,對吧~
  • 安全性,SSL/TLS 是必要的,再來就是處理好使用者能存取的資源權限,這部份是實作 API 時必須要考慮到的。
  • 要用 cache,實作時要想,這資料會很常變動嗎?會很常被索取嗎?不常變動又很常被索取,就放到 cache 吧。
  • 幫 API 編上版號,這可以避免影響到舊有的 API,一方面也可以單純化,不用考慮到過多相容性的問題。例如:/v1, /v2

Angular i18n 的另外一個選擇 ngx-translate

Angular i18n 的官方作法並不讓人滿意,我後來找到 ngx-translate

先說明一下他的作法,他把這些翻譯好的字詞放到 JSON 翻譯檔去,在執行時就可以透過 http client 去拉取回來作動態的替換。

好處是,程式只要建置一次,不需要針對個別語言再次建置,網頁伺服器那邊也不需要特別寫設定去處理,而且可以做到動態切換語言。壞處是會有額外的 HTTP 請求,會增加流量。

ngx-translate 額外好的地方是,他提供了相應的工具、plugin,相當的方便。

那麼怎麼使用呢?我推薦看這篇【Angular】ngx-translate 多語系實務應用 ,他這篇的缺點是萃取字串的部分是手動,我建議萃取字串的部分可以用原作者 biesbjergngx-translate-extract  ,就不用自己找字串找的太累。

先進行安裝

npm install @ngx-translate/core —save
npm install @ngx-translate/http-loader --save

然後改 app.module.ts

import { TranslateModule, TranslateLoader, TranslateCompiler } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { HttpClientModule, HttpClient } from '@angular/common/http';

// … 省略 …

// 這主要是告訴 ngx-translate 翻譯檔該怎麼載入,用 TranslateHttpLoader 是表示以 HTTP 方式去下載、載入
export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

// … 省略 …

// 設定
const translateConfig = {
  defaultLanguage: 'en-US',  // 預設是英文
  loader: {
    provide: TranslateLoader,
    useFactory: HttpLoaderFactory,  // 前面寫的 Factory
    deps: [HttpClient]
  },
  compiler: {
    provide: TranslateCompiler,
    useClass: TranslateMessageFormatCompiler
  }
};

@NgModule({
  // …
  imports: [
    BrowserAnimationsModule,
    BrowserModule,
    TranslateModule.forRoot(translateConfig),  // 模組帶設定
    // …
  ],
  // …
})

在 HTML 裡,使用

// 方法 1
{{ ‘your_translation_key’ | translate }}

// 方法 2
<div [translate]=“‘your_translation_key’”></div>

// 方法 3,適用於字串要用 HTML
<div [innerHTML]="'HELLO' | translate"></div>

他還可以帶參數,只是這邊我看不太懂,暫時也還用不到。

在程式裡,使用

// 先匯入
import {TranslateService} from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';

// … 省略 …
export class AppComponent implements OnInit {
  l10n__title = '';

  constructor(public translate: TranslateService) {
  }

  ngOnInit(): void {
    const self = this;

    // 讓 ngx-translate-extract 可以抓到字串用的
    _('your_translation_key');

    // 取得字串都要使用 translate.get() 來預先取得
    this.translate.get('your_translation_key').subscribe((res: string) => {
      self.l10n__title = res;
    });
  }
}

這裡要特別說明一點,也是我當初用的時候搞錯的地方,就是上面用到的 your_translation_key 並不是字串,而是你自定義的代碼,裡面有使用 ‘.’ 的時候,在產出的 JSON 裡會變成 nested object ,舉個例子,假設這個 key 是 login.title,那麼 JSON 翻譯檔就會是

{
  "login": {
    "title": ""
  }
}

接下來講 ngx-translate-extract,ngx-translate-extract 的安裝

npm install @biesbjerg/ngx-translate-extract --save-dev

裝好以後,在 package.json 的 scripts 裡加入 (要產生什麼語言,請替換 {en,da,de,fi,nb,nl,sv} 這個字串,以繁體中文來說,是zh-TW或zh-Hant,只有英文跟繁體中文的話,就放 {en-US,zh-TW}。 )

// package.json
...
"scripts": {
  "i18n:init": "ngx-translate-extract --input ./src --output ./src/assets/i18n/template.json --key-as-default-value --replace --format json",
  "i18n:extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/{en,da,de,fi,nb,nl,sv}.json --clean --format json"
}
...

然後建立存放字串的資料夾:mkdir -p src/assets/i18n,執行 npm run i18n:extract 以後,就可以在 src/assets/i18n 裡看到翻譯檔了。

bfg

因為 git repository 裡有敏感資訊,為了安全起見,必須要把這些敏感資訊移掉。
找到這兩篇:

裡面大同小異,主要提到兩個方法,一個是 git filter-branch,一個是 bfg,這邊只介紹 bfg。 bfg 是一個相對於 git filter-branch 來說簡單、快速的工具,除了可以清理敏感資訊之外,也可以清掉超級大的檔案。

MacOS 的安裝可以用 brew:brew install bfg

使用步驟如下:

  1. 移除掉你要處理的檔案或是先清理掉你要處理的檔案內容,然後提交、推送到 repository 去。這段官方有特別說明 (在 Your current files are sacred… 這個小節),我是已經做完下面步驟,上 gitlab 網頁看檔案怎麼沒消失,花了點時間才在網頁上注意到這段,所以這個步驟要先作!!
  2. 切到另外一個目錄,例如 $HOME/tmp,對 repository 作 mirror,這樣拿下來的檔案結構不是一般的檔案佈局,而是 .git 下的佈局:git clone –mirror your_repository.git ,這時候 $HOME/tmp 下會有一個 your_repository.git
  3. 幾種使用方法:
    • 刪除檔案:bfg –delete-files your_file your_repository.git
    • 移除超過指定大小的檔案:bfg –strip-blobs-bigger-than 50M your_repository.git
    • 清理敏感資訊:先新增一個 passwords.txt,裡面放置要移除的文字,範例可以參考這個 gist ,然後執行 bfg –replace-text passwords.txt your_repository.txt
    • 刪除資料夾:bfg –delete-folders .git –delete-files .git –no-blob-protection your_repository.git
  4. 切到 your_repository.git 下,依照指示執行 git reflog expire –expire=now –all && git gc –prune=now –aggressive
  5. 推送回去:git push
  6. 大功告成

把敏感設定的內容跟 Angular 程式分離

以我用過 Django / Docker 的經驗,一般都是利用環境變數來作分離,程式在執行時,會去讀取環境變數來當作設定,盡量不依靠設定檔,進而達到 12 factor –  config 的要求。

到了 Angular ,就需要做出調整。Angular 是需要先進行建置產出 javascript / html / css ,再把這些產出的檔案放到網頁伺服器,瀏覽器再去下載,所以完全沒辦法使用環境變數作為設定,必須要把設定寫到檔案裡,再做建置。那設定寫到檔案的話,其實我們都知道,這些敏感設定內容不適合放到 git repository,即使是 private repository,這樣會有安全上的風險。那這樣就兩難了啊?

說來很妙,我是覺得官方早就應該有比較好的作法了,但是,並沒有。不管怎麼樣,我還是找到解決方法了,基本上跟我想的也差不多。

這兩篇文章的作法大同小異,這邊作簡單的說明:

  1. 移除 src/environment/environment.ts
  2. 新增一個腳本,這個腳本會讀取環境變數,並產生一個新的 src/environment/environment.ts
  3. 修改 package.json
    1. 在 scripts 裡增加 config,這個 config 會去執行步驟 2 的腳本,用以產生 environment.ts
    2. 修改 scripts 裡的 start / build,在執行原來的指令前,先執行 npm run config 去產生 environment.ts

下面兩個片段是關鍵的改動部分:

// set-env.ts
// 放在專案根目錄下
import { writeFile } from 'fs';
import { name, version } from './package.json';


// Configure Angular `environment.ts` file path
const targetPath = './src/environments/environment.ts';


// 載入 colors 模組,主要是用來顯示
const colors = require('colors');


// 可以把敏感內容放到 .env 檔案裡,nodejs 會把內容讀出來,放到環境變數裡
// 也可以在執行 npm run 之前,先設定好環境變數
const result = require('dotenv').config();
if (result.error) {
  // throw result.error;
}


// 原來 environment.ts 裡的內容,把該替換的,用 template literals 來替換

// https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Template_literals
const envConfigFile = `export const environment = {
  production: ${process.env.PRODUCTION},
  VERSION: '${version}’,
};

`;


// 顯示
console.log(colors.magenta('The file `environment.ts` will be written with the following content: \n'));
console.log(colors.grey(envConfigFile)); writeFile(targetPath, envConfigFile, (err) => {
  if (err) {
    throw console.error(err);
  } else {
    console.log(colors.magenta(`Angular environment.ts file generated correctly at ${targetPath} \n`));
  }
});
// package.json
// 只摘錄重要的部分
{
  “scripts”: {
    "config": "ts-node -O '{\"module\": \"commonjs\"}' ./set-env.ts",
    "start": "npm run config && ng serve",
    "build": "npm run config && ng build”
  }
}

電影流水帳(2020/05/01~2020/05/10)

Emma Stone
Emma Stone,飾演 Zombieland: Double Tap 裡的 Wichita
  • J’ai perdu mon corps (IMDB, Wikipedia),英譯:I lost my body,台譯:隻手探險。
  • Zombieland: Double Tap (IMDB, Wikipedia),台譯:屍樂園-髒比雙拼。

J’ai perdu mon corps

這是部故事很有意思的電影,只是有點小悶。

故事從一隻手開始,這隻手莫名其妙的會動,然後開始他的旅程。在這個旅程中,開始穿插著他主人的故事。原來這隻手的主人 Naoufel 是個孤兒,小時候學過鋼琴,然後因為意外,父母車上吵架發生了車禍,導致 Naoufel 變成了孤兒。後來長大以後,Naoufel 當了披薩外送員,某次外送,他外送到一個女孩子家,可是遲到了,女孩子有點兒生氣,但還是原諒了他,他們透過對講機聊起了天。Naoufel 對這女孩子很有好感,依據聊天的內容,用了點詭計取得了她的名字 – Gabrielle ,以及上班地點。接著 Naoufel 跟蹤 Gabrielle,得知她有個叔叔,她的叔叔似乎需要人手,Naoufel 毛遂自薦,當了她叔叔的學徒,然後跟 Gabrielle 有了進一步的認識以及交往。就在 Naoufel 進一步告訴了 Gabrielle 自己是怎麼接近她以及想要進一步交往時,Gabrielle 拒絕了他。Naoufel 很難過,他去了朋友的舞會,喝的爛醉。隔天早上宿醉的他回到 Gabrielle 叔叔的工廠,要開始工作,可是,意外往往就是這麼發生。宿醉的 Naoufel 一個不小心,讓自己的手被鋸斷了,Naoufel 被送到醫院,這隻手則是意外的動了起來。

這隻手最後是找到了他的主人,只是 Naoufel 已經決定離去,他跑到屋頂去。Gabrielle 來找 Naoufel 時,沒看到 Naoufel ,也跟著到了屋頂,想說會不會在之前告白的地方。雪開始下了,在屋頂除了 Naoufel 蓋的小木屋、覆蓋著雪的屋頂地上的腳印之外,只剩下之前 Naoufel 很珍藏的錄音機。她撿起來邊聽著這錄音,一邊找尋著 Naoufel 。在邊聽的時候,知道了 Naoufel 為什麼一直聽這錄音,同時也聽到了Naoufel 留給她的話。從足跡來看,原本以為 Naoufel 是跳樓自盡,但搭配著錄音機裡的聲音,她知道 Naoufel 是成功的從這屋頂跳到另外一棟大樓去了…

Zombieland: Double Tap

失望的續集電影,故事普普通通。

故事的一開始,Little Rock 厭倦了四人一起的生活,Columbus 向 Wichita 求婚,Wichita 不願意正面回應,就跟 Little Rock 離開了。Wichita 跟 Little Rock 在路上遇到一個男孩子 Berkeley,Little Rock 為了擺脫姊姊,在半夜裡丟下 Wichita ,跟 Berkeley 一起上路走了。隔天醒來的 Wichita 感到錯愕,只能回去找 Columbus 跟 Tallahassee 。被 Wichita 跟 Little Rock 丟下的 Columbus 跟 Tallahassee 去了購物中心,意外遇到一個沒被感染的女孩 Madison ,帶了她回去。Columbus 跟 Madison 發生了關係,回來的 Wichita 感情很受傷,才離開沒兩天,Columbus 居然就跟別的女人上床了,而且還是個傻大姊。

回來的 Wichita 表明了很擔心 Little Rock ,Columbus 跟 Tallahassee 雖然不喜歡之前被丟棄,但基於家人的立場,還是決定幫忙 Wichita,於是四人就出發去找 Little Rock 了。一開始沒多久,Madison 就疑似被僵屍咬到,看起來就是要變成僵屍,Columbus 只能痛下殺手。三人繼續旅程,循著線索去找 Little Rock ,然後他們來到一間擺放著貓王遺物的旅館,在這裡他們遇到 Nevada 。聊了以後,才知道 Nevada 經營著這家旅館,並且保存著貓王的遺物。因為同樣愛貓王的關係,Tallahassee 跟 Nevada 還蠻契合的。隔天,兩個人 Flagstaff 跟 Albuquerque 來找 Nevada,相互打過照面以後,發現這兩個人意外的跟 Columbus 與 Tallahassee 蠻相似的,一個老愛碎碎念,一個愛殺僵屍。這時候僵屍突然殺過來,Flagstaff 跟 Albuquerque 出去殺退僵屍,只是回來時,兩人已經被感染。Nevada 等人只好幹掉 Flagstaff 跟 Albuquerque ,隨後 Columbus, Tallahassee 跟 Wichita 告別 Nevada 繼續上路。上路以後,意外遇到 Madison ,所以 Madison 真的只是過敏,而不是被僵屍咬了。四人再次會合,上路去找 Little Rock。終於,他們根據 Nevada 的線索,找到了 Little Rock 所在地。Little Rock 所到的地方是一群愛好和平的人所居住的村落,所有人都不能帶武器。他們進去想要勸退 Little Rock,可是看來 Little Rock 心意已決。Tallahassee 說既然已經找到 Little Rock,我任務也結束了,那我要離開囉,就獨自開車離去。

Tallahassee 在月夜下孤身一人很愜意的開著車,突然他看到一群僵屍往 Little Rock 的所在地跑去,他覺得大事不妙,就回去示警。四人決定設下陷阱來對付僵屍,經過一番大戰,眼看四人就要命喪僵屍手下,Nevada 開著車過來救了四人。五個人依據僵屍的行動模式重新擬定了作戰策略,跟住在這邊的人一起對抗僵屍,最後打敗了僵屍。

故事裡,有兩個地方讓我覺得很有趣,第一個是喜歡立下規則的 Columbus 居然打破了自己的規則,他沒有殺死 Madison,而是對空鳴槍把她嚇走,不過這也讓他們後續能有交通工具繼續旅程,這或許是編劇想告訴我們的,「有時候打破規則也無妨」。第二個是,兩人遇到跟自己個性相似的人,Tallahassee 是很不爽,而 Columbus 則是有惺惺相惜的感覺,那我呢?我要是看到跟自己相似的人是會生氣?覺得這人不長進?還是會覺得太好了,這樣應該比較好溝通?這倒是蠻值得好好想想的。

Angular i18n

Angular 官方提供的套件以及教學:Angular Internalization (i18n)

文章裡一大堆,簡單整理如下:

  1. 安裝:ng add @angular/localize
    • 這個步驟會幫你在 polyfills.ts 裡加入必要的 import
  2. 使用,這部份分為 HTML 跟程式
    • HTML,在需要多國語言的標籤加入 i18n 的屬性
      • <span i18n>Hello world</span>
      • <span i18n=“@@span_hello”>Hello world</span> ,用 i18n=“@@span_hello” 的好處是,產出的翻譯檔裡不會是一個隨意的數字,而是一個對開發者來說比較明確的名稱。
      • <input i18n-placeholder> 這個是有些屬性本身需要多國語言的,就在前面加上 “i18n-“
    • 程式,字串前方加上 $localize ,並且改用 back quote,例如:$localize`hello world`
  3. 萃取,用 ng xi18n –output-path src/i18n 就可以把 HTML 裡有標 i18n 的字串萃取到 src/i18n/messages.xlf 裡
  4. 翻譯,先把上個步驟取得的 messages.xlf 複製為 messages.zh_Hant.xlf ,再去編輯。這邊提供一個簡易的網頁工具 – tiny-translator,在處理上會方便很多。開啟以後,要先建立專案,然後上傳 .xlf 檔案,接著就可以進行翻譯了。翻譯好,再下載下來即可。
  5. 合併,程式開發中難免會有增刪,每次用 xi18n 基本上都是重新萃取一次,等於是又要再搞一次合併的功夫,這太累。tiny-translator 的作者有提供另外一個工具 – xliffmerge 
    • 安裝:npm install -g ngx-i18nsupport
    • 在 package.json 的 scripts 裡加入 “extract-i18n”: “ng xi18n –output-path src/i18n && xliffmerge –profile xliffmerge.json en de” ,裡面的 en, de 等 locale 請依照自己的需求作調整
    • 新增 xliffmerge.json ,這個檔案請參考後面。
    • 使用 npm run extract-i18n 就可以自動萃取字串並且作合併了。
  6. 專案的建置,主要是修改 angular.json,有三個部分:
    • projects / your_project_name 加入 “i18n”: {“sourceLocale”: “en”, “locales”: {“de”: “src/i18n/messages.de.xlf”}}
    • projects / your_project_name / architect / build / configurations 裡加入 “de”: {“localize”: [“de”]}
    • projects / your_project_name / architect / serve / configurations 裡加入 “de”: {“browserTarget”: “ng-hosting:build:de”}
  7. 要執行 ng build / ng serve 時,就可以用
    • ng build –configuration=production,de
    • ng serve –configuration=de
  8. 補充一個我覺得很重要的部分,就是一個語言要建置一次,所以一般的佈署會是這樣的,建置好 zh ,放在 zh/ 目錄下,建置好 de,放在 de/ 目錄下,然後在 nginx/apache 的設定裡去依照 header 的 language 去導向到對應的目錄去。這篇 Deploying an i18n Angular app with angular-cli 的後面有教怎麼去設定 apache / nginx。

看到這邊,你可能會想,那程式裡標上 $localize 的字串呢?嗯,ng xi18n 並不會把這些字串萃取出來 (issue),所以這部份得自己手動處理 😣 

P.S. 用 ngx-i18nsupport 的 tooling 可以把上面講的簡化掉,像是加入 npm package、在 package.json 加入 extract-i18n 、在 angular.json 加入設定等等,一次就搞定了,我是已經用了才看到這個 tooling ,有點相見恨晚。

// xliffmerge.json
{
  "xliffmergeOptions": {
    "srcDir": "src/i18n",
    "genDir": "src/i18n",
    "i18nFile": "messages.xlf",
    "i18nBaseFile": "messages",
    "i18nFormat": "xlf",
    "encoding": "UTF-8",
    "defaultLanguage": "en",
    "languages": ["en", "de"],
    "removeUnusedIds": true,
    "supportNgxTranslate": false,
    "ngxTranslateExtractionPattern": "@@|ngx-translate",
    "useSourceAsTarget": true,
    "targetPraefix": "",
    "targetSuffix": "",
    "beautifyOutput": false,
    "preserveOrder": true,
    "allowIdChange": false,
    "autotranslate": false,
    "apikey": "",
    "apikeyfile": "",
    "verbose": false,
    "quiet": false
  }
}