angular-translate読み直し

半年前にまとめたもの。こっちに転載。


これは何か

設定例

/*@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
    • 前回選択した言語コードを保存
    • (fallback で cookie を利用しているので angular-translate-storage-cookie も読み込まれる)

逆引き

言語ファイル(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にした

Migrating from v1 to v2

今の開発環境では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を生成した後、 preprocessorswebpackの部分を追加するだけでお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パッケージで開発して公開

このビデオで基本的な流れがわかる。

www.youtube.com

利用したコマンドの一覧がまとまっている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 fileAdd 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

遷移時にデータを渡したい

// 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.

遷移先を途中で変更する

$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

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! ***//  
   }
})