angular-translate読み直し
半年前にまとめたもの。こっちに転載。
これは何か
- 実際に仕事でi18n対応した時に調べたことのメモと設定例
- https://angular-translate.github.io/docs/#/guide を読んで、必要そうな情報を逆引きっぽくまとめたもの
設定例
/*@ngInject*/ export function translateConfig($translateProvider) { $translateProvider // angular-translate-loader-static-files // assets/lang/[langKey].json から非同期で言語ファイルを読み込む .useStaticFilesLoader({ prefix: 'assets/lang/', suffix: '.json' }) // 言語keyの設定 .registerAvailableLanguageKeys(['en', 'ja'], { // en_US などは en と認識させる 'en_*': 'en', 'ja_*': 'ja', // そのほかの言語keyは全て en にする '*': 'en' }) // ブラウザ環境から優先言語を自動取得 // (以前に設定されたkeyがあれば、そちらを優先する <- angular-translate-storage-local利用時) .determinePreferredLanguage() // enable escaping of HTML // https://angular-translate.github.io/docs/#/guide/19_security .useSanitizeValueStrategy('escape') // angular-translate-handler-log .useMissingTranslationHandlerLog() // angular-translate-storage-local .useLocalStorage(); }
以下3つのextensionを利用している
- angular-translate-loader-static-files
- 非同期読み込み
- angular-translate-handler-log
- 対応するメッセージがない場合にエラーを表示
- angular-translate-storage-local
逆引き
言語ファイル(JSON)の書き方
- 通常:
{ "TRANSLATION_ID": "This is a concrete translation for a specific language." }
- ショートカット:
// bar.foo でアクセス可 { "bar": { "foo": { "foo": "This is my text." } } }
- リンク(いい機能だなあ):
{ "SOME_NAMESPACE": { "OK_TEXT": "OK" }, "ANOTHER_NAMESPACE": { "OK_TEXT": "@:SOME_NAMESPACE.OK_TEXT" } }
参考: https://angular-translate.github.io/docs/#/guide/02_getting-started
$translate利用時に、非同期読み込みされたメッセージを反映
- Two-way binding されないので 明示的に処理を書く必要がある
$translateChangeSuccess
を利用
/*@ngInject*/ app.controller('Ctrl', ($scope, $translate, $rootScope) => { $rootScope.$on('$translateChangeSuccess', () => { $translate('HEADLINE').then( // 成功 translation => $scope.headline = translation, // 失敗 translationId => $scope.headline = translationId ); }); });
参考: https://angular-translate.github.io/docs/#/guide/03_using-translate-service
変数を利用
言語JSONファイル:
{ "TRANSLATION_ID": "{{username}} is logged in." }
$translate利用時
$translate('TRANSLATION_ID', { username: 'PascalPrecht' });
$filter利用時
パターン1:
<div>{{ 'TRANSLATION_ID' | translate:'{ username: "PascalPrecht" }' }}</div>
パターン2:
<div>{{ 'TRANSLATION_ID' | translate:translationData }}</div>
angular.module('myApp') /*@ngInject*/ .controller('Ctrl', ($scope) => { $scope.translationData = { username: 'PascalPrecht' }; });
そのほかの方法もある
参考: https://angular-translate.github.io/docs/#/guide/06_variable-replacement
設定されている言語を取得
$rootScope.$on('$translateChangeEnd', (event, args) => { // args.language }); // or $translate.proposedLanguage() || $translate.use() // proposedLanguage() -> 非同期読み込み中の言語keyを取得
利用言語を設定する
preferredLanguage
を利用- ブラウザ環境から優先言語を自動取得する場合は
determinePreferredLanguage
を利用- (内部的に
navigator.languages[0]
navigator.language
などを利用している)
- (内部的に
参考: https://angular-translate.github.io/docs/#/guide/07_multi-language
en_US
en_SG
などをen
として扱う
registerAvailableLanguageKeys
を利用
参考: https://angular-translate.github.io/docs/#/guide/09_language-negotiation
Unit Testing
非同期読み込みをしている場合、ローダー書き換える
beforeEach(module('app', ($provide, $translateProvider) => { $provide.factory('customLoader', $q => () => $q.resolve() ); $translateProvider.useLoader('customLoader'); }));
アプリを起動させる時の流れ
- we register a asynchronous loader
- we define our preferred language
- $translate service is instantiated the first time it gets injected
- angular-translate notices that there’s no language locally available
- it looks if there’s a registered asynchronous loader
- the asynchronous loader is called with the preferred language locale
- the translation data is loaded and ready to be used
参考: https://angular-translate.github.io/docs/#/guide/22_unit-testing-with-angular-translate
タイピングの練習
最近はじめた。仕事の前のウォーミングアップにちょうどよかったりする。
webpack2にした
今の開発環境ではwebpackに任せているタスクが少ないため、かなり簡単に移行できた。
grunt-webpack
を利用してるのでこれもv2に変更。
GitHub - webpack-contrib/grunt-webpack: integrate webpack into grunt build process
webpackで処理が止まってしまったので、keepalive: true
を追加して完了。
(以前から必要だったような気もしないではない)
ユニットテスト(Karma)もTypeScriptで: karma-webpack編
以前の記事
ユニットテスト(Karma)もTypeScriptで - ryotah’s blog
これだと、モジュールのimportができないねえ。
というわけでkarma-webpackを試そうと思います。
GitHub - webpack-contrib/karma-webpack: Use webpack with karma.
テスト環境は Karma Chrome Jasmine。
// karma.conf.js var webpackConfig = require('./webpack.config'); module.exports = function(config) { config.set({ mime: { 'text/x-typescript': ['ts','tsx'] }, basePath: '', frameworks: ['jasmine'], patterns to load in the browser files: [ 'test/**/*.spec.ts' ], exclude: [ ], preprocessors: { 'test/**/*.spec.ts': ['webpack'], }, webpack: { module: webpackConfig.module, resolve: webpackConfig.resolve }, reporters: ['progress'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, concurrency: Infinity }) }
基本的にはkarm init
でkarma.conf.jsを生成した後、 preprocessors
とwebpack
の部分を追加するだけでおk。
これに加えて、Chromeでテストが実行されなかったので mime: {'text/x-typescript': ['ts','tsx']}
の設定も追加しています。
ng test ends with "Executed 0 of 0 ERROR" · Issue #2838 · angular/angular-cli · GitHub
他のファイルも参考程度に。
. ├── karma.conf.js ├── node_modules ├── package.json ├── src ├── test ├── tsconfig.json └── webpack.config.js
// package.json { "name": "webpack2-karm", // (省略) "devDependencies": { "@types/jasmine": "^2.5.43", "jasmine-core": "^2.5.2", "karma": "^1.5.0", "karma-chrome-launcher": "^2.0.0", "karma-jasmine": "^1.1.0", "karma-webpack": "^2.0.2", "ts-loader": "^2.0.1", "typescript": "^2.2.1", "webpack": "^2.2.1" } }
// tsconfig.json { "compilerOptions": { "target": "es5", "module": "commonjs" }, "exclude": [ "node_modules" ] }
// webpack.config.js var path = require('path'); module.exports = { entry: './src/foo', output: { path: path.resolve(__dirname, 'dist'), filename: 'foo.bundle.js' }, resolve: { extensions: ['.ts', '.tsx', '.js'] }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader' } ] } }
npmパッケージで開発して公開
このビデオで基本的な流れがわかる。
利用したコマンドの一覧がまとまっているGistもある https://gist.github.com/rockbot/d62fbd256a12b54dac08c00b738e4158
npm link
npm publish --access=public
npm version minor
など。
より詳細な話はこの記事がよさそう。peerDependenciesの話とか。
Reactコンポーネントをnpmパッケージとして開発する - Hatena Developer Blog
VS CodeにTypeScript用のエクステンションを追加
してみた。
globalに展開していたライブラリ群をimportにするために、importer系エクステンションを追加。
いくつか試してみて、追加したものは以下2つ。
一番賢くimportしてくれたのが、TypeScript Importer。(少なくとも自分の環境下では)
TypeScript HeroはAdd import to current file
とAdd all missing imports for the open document
が便利そう。
ui-routerを読み直し
(1年前にまとめたもの。こっちに転載。最新のUI-Routerについては Angular UI-Router v1さわる - ryotah’s blog から)
動機
- ui-router のこと、実はあまり把握していなかった
- Nested States & Views とか Multiple & Named Views とか一応使っているけどあんまりよくわかっていない
- ひとまず angular-ui/ui-router Wiki に目を通すか ← イマココ
書いてあること
- 仕事で利用したTips
- angular-ui/ui-router Wiki の以下ページから、役立ちそうなものと面白そうなもの抜粋
仕事で利用したTips
遷移時にデータを渡したい
- How to pass custom data in $state.go() in angular-ui-router? · Issue #1949 · angular-ui/ui-router
- stateConfig の params を利用する
- additional non-url parameters を定義できる
// foo.ts $stateProvider .state('foo', { url: '/foo', template: '<foo></foo>', params: { // non-url parameter data: null, } }) // bar.controller.ts go() { this.$state.go('foo', { data: { // ... }, }); }
動的にtemplateUrlを変更したい
$templateProvider
を使う
$stateProvider .state('foo', { url: 'foo', controller: 'FooController', /*@ngInject*/ templateProvider: function($templateFactory, BarService) { return $templateFactory.fromConfig({ templateUrl: BarService.getBool() ? 'a.html' : 'b.html' }); } });
履歴、location bar のコントロール
location - {boolean=true|string=} - If true will update the url in the location bar, if false will not. If string, must be “replace”, which will update url and also replace last history record.
- http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state
- urlを変えたい、履歴も変えたい (true)
- 何も変更したくない (false)
- urlを変えたいが、遷移したことを履歴に残したくない (replace)
遷移先を途中で変更する
$stateProvider.state の resolve 内で遷移先を変更する
resolve: { /*@ngInject*/ data: function($stateParams, $state, $q, $timeout) { const data = $stateParams.data; if (data) { return "something"; } // go to xxx if no data $timeout(() => { $state.go("xxx"); }, 0); return $q.defer('error message'); } }
副作用なく、location barのクエリだけを変更したい
$state.go('.', { query: "query" }, { notify: false });
- $stateChangeStart と $stateChangeSuccess events を broadcast しない
ui-sref, ui-sref-active
- ui-sref - You just need to be aware that the path is relative to the state that the link lives in
- ui-sref-active
- A directive working alongside ui-sref to add classes to an element when the related ui-sref directive’s state is active
State Manager
- Resolve
- onEnter and onExit callbacks
Nested States and Nested Views
Nested Statesの設定
Dot Notationによる設定
$stateProvider .state('contacts', {}) .state('contacts.list', {});
<a ui-sref="contacts">Go Contacts</a> <a ui-sref="contacts.list">Go Contacts List</a>
Parent Propertyによる設定
$stateProvider .state('contacts', {}) .state('list', { parent: 'contacts' });
<a ui-sref="contacts">Go Contacts</a> <!-- "contacts.list" にはならない --> <a ui-sref="list">Go Contacts List</a>
stateに設定している名称は違うが、両方とも contacts(親)、list(子)の関係になっている。 このへんちょっとややこしい。
親Statesから引き継げるもの
Abstract States
子Stateを持つことができるが遷移できないState。
利用例として考えられるもの:
- urlを子Stateに追加したい場合
- ui-viewを含むtemplateを子Stateで利用したい場合
- Resolved dependencies via
resolve
を子Stateで利用したい場合 - Custom data properties via
data
を子Stateで利用したい場合 - To run an onEnter or onExit function that may modify the application in someway.
Multiple Named Views
Absolute Names
viewname@statename
の形式を利用すると ui-view を絶対名で指定できる。
例えば、Nested States だけど Views を入れ子にしたくない場合、以下のような書き方が可能。
<div ng-app="ngapp"> <div ui-view></div> <div ui-view="list"></div> </div>
$stateProvider .state('state1', { url: '/state1', template: 'state1' }) .state('state1.list', { url: '/list', views: { // このケースのように State がルートの場合、@の後ろには何もかかない 'list@': { template: 'state2' } } });
URL Routing
- Basic Parameters
/user/:id
,/user/{id}
,/user/{id:int}
- Query Parameters
/contacts?myParam1&myParam2
- Absolute Routes
url: '^/list'
のようにすると、Nested State でも Url は結合されない
$stateParams
は親Stateのパラメーターなどは取得できない(自分自身のパラメーターのみ取得可能)- 親Stateで resolve すれば取得可能
$stateProvider.state('contacts.detail', { url: '/contacts/:contactId', controller: function($stateParams){ $stateParams.contactId //*** Exists! ***// }, resolve:{ contactId: ['$stateParams', function($stateParams){ return $stateParams.contactId; }] } }).state('contacts.detail.subitem', { url: '/item/:itemId', controller: function($stateParams, contactId){ contactId //*** Exists! ***// $stateParams.itemId //*** Exists! ***// } })