KASHIMURA Blog

Webサービス開発のこと、個人的なことを書いているブログ

Restyleを利用してスタイルを定数化する #ReactNative

f:id:kasssssy:20200419020827p:plain

はじめに

React Summitで紹介されていた@shopify/restyleを触ってみました。
@shopify/restyleは、基礎となるスタイルを定数化してコンポーネントの役割ごとに色や間隔のサイズを柔軟に指定できるようにするライブラリのようです。

ReactNativeでスタイルを実装する場合、Stylesheetを定義されているかと思いますが、@shopify/restyleを利用することでStyleSheet.create()からおさらばできて、かつ、スタイルの実装がシンプルになります。

また、Typescriptで実装されていて型が強制されるので、定義していないスタイルがあたることを防いでくれます。

セットアップ

$ npx react-native init RestyleSample --template react-native-template-typescript
$ yarn add @shopify/restyle
$ cd ios && pod install && cd ../

テーマを定義

テーマオブジェクトを実装します。

app/theme.tsx

  • const palette = {}で利用する色を定義します。
  • const theme = {}でグローバルテーマオブジェクトを定義し、colors、spacing、breakpointsを設定します。この3つは必須です。
  • colorsは、コンポーネントの中で利用する為に名前付きで色を定義しています。
  • spacingは、marginなどに指定するサイズを定義します。ちなみに、キーはTシャツのサイズ(s,m,l)を使うのがいいと公式が言ってます。
  • breakpoints は、画面サイズの最小幅を定義します。(本記事では使いません)
  • textVariantsは、この後でてくるTextコンポーネントで利用するデザインを定義しています。colorにはcolorsのキーを指定することで値を取得することができます。

Box と Text を定義

BoxとTextという既存のコンポーネントが用意されています。
この2つに利用するテーマを指定して定義します。

app/Box.tsx

app/Text.tsx

App.tsx を編集

定義したテーマを利用するには、ThemeProviderに利用するテーマを渡してルートコンポーネントを囲む必要があります。

  • BoxのbackgroundColorに指定しているmainBackgroundColorは、テーマオブジェクトのcolorsに定義した色になります。
  • Textのvariantには、テーマオブジェクトのtextVariantsに定義したスタイルを指定します。
  • BoxとTextに指定しているプロパティはライブラリが用意したものになります。それぞれ指定できるものが異なるので、公式サイトを確認するのをおすすめします。
  • Boxで利用できるRestyle Function->backgroundColor, opacity, visible, layout, spacing, border, shadow, position
  • Textで利用できるRestyle Function-> color, opacity, visible, typography, textShadow, spacing

この時点で動作確認をすると、
ちゃんとそれぞれのスタイルが反映されているのが確認できます。 f:id:kasssssy:20200419010650p:plain

ダークモードに対応させる

theme.tsxにダークモード用のテーマを定義します。以下を追記します。

ダークモードを切り替えるられるようにする為、App.tsxを以下のように修正します。

  • useState()をつかってダークモードか判断できるようにします。
  • <ThemeProvider theme={darkMode ? darkTheme : theme}> で利用するテーマが切り替わるようにしています。
  • <Box margin={'l'}>lは、テーマオブジェクトのspacingで定義したものを指定しています。

実行してみると、ちゃんと切り替わることが確認できました。

f:id:kasssssy:20200419015146g:plain

さいごに

React Summitで紹介されていたので、軽い気持ちで触ってみましたが、よさげだったので記事にしました。
スタイルの重複を最小限にすることができるので実装が楽になると思います。
Boxのようなコンポーネントを独自で定義できたり、コンポーネントの外でテーマを利用するためにフックが用意されていたりするので、公式サイトを覗いてみるのをおすすめします!

本記事で実装したコードはGitHubに上げました。

最後まで読んでいただきありがとうございました!

参考

GitHub - Shopify/restyle: A type-enforced system for building UI components in React Native with TypeScript.

React Summit Remote Edition まとめ(前編) | naturalclar.dev

ReactNativeアプリを多言語化する方法 (react-intl)

f:id:kasssssy:20200414234520p:plain

はじめに

アプリの国際化に伴い、多言語に対応する必要が出てくると思います。
Reactを使っている場合、react-intlを利用すると簡単に実現することが可能です。

この記事では以下のことをやります。

  • React Nativeで作成したアプリを多言語に対応させる
  • 端末の言語設定に沿って翻訳する
  • 英語と日本語に対応する
  • 英語の翻訳情報がない場合は日本語が表示されるようにする
  • サポート外言語が設定されている場合は日本語が表示されるようにする

react-intlとは?

Reactアプリを国際化するためのライブラリです。
文字列、日付、数字など様々なフォーマットに対応しています。
フックが用意されていたり、結構柔軟に対応できます。

環境情報

  • macOS
  • React Native (0.60.5) 
  • Typescript (^3.8.3)
  • react-intl (^4.1.1)
  • yarn
  • cocoapods

セットアップ

サンプルプロジェクト作成

$ npx react-native init LocalizationSample --version 0.60.5  --template react-native-template-typescript
$ yarn add react-intl
$ yarn add intl
$ yarn add lodash @types/lodash
$ yarn add react-native-localize
$ cd ios
$ pod install
  • intlは、ReactNativeでreact-intlを利用する時に必要なので入れます。
  • react-native-localizeは、端末から言語設定を取得するために入れます。
  • lodashは、あとでincludesメソッドを使いたいので入れます。

動作確認

$ cd LocalizationSample
$ npx react-native run-ios
$ npx react-native run-android

iosandroidのシュミレーター で「Welcome to React」と表示されたらOKです。
androidは先にシュミレータを起動しておく必要があります

ロケールファイルを作成する

プロジェクト直下にja.jsonとen.jsonを作成してください。

ja.json

{
  "title": "ホーム",
  "name": "{name} 様",
  "message": "こんにちは"
}

en.json

{
  "title": "HOME",
  "name": "Mr/Ms {name}"
}
  • {}で文字列を囲むと、その文字列をkeyに値を外から差し込めるようになります。
  • en.json側にmessageが存在しませんが、あとで日本語にフォールバックされることを確認したいのでこうしています。

言語設定を取得する関数を用意する

プロジェクト直下にi18n.tsxを作成して、言語設定を取得するgetLocale()を記述します。

import * as React from 'react';
import * as RNLocalize from 'react-native-localize';
import {includes} from 'lodash';

const SUPPORTED_LOCALE = ['ja', 'en'];
const DEFAULT_LOCALE = 'ja';

const getLocale = (): string => {
  const locales = RNLocalize.getLocales();
  const languageCode = locales[0].languageCode;

  if (includes(SUPPORTED_LOCALE, languageCode)) {
    return languageCode;
  }

  return DEFAULT_LOCALE;
};
  • SUPPORTED_LOCALEの値がアプリで対応する言語です。
  • RNLocalize.getLocales() から端末の言語設定を取得しています。
  • 取得した言語がサポート外ならばデフォルトロケールを返すようにしています。

翻訳情報を返す関数を用意する

まず、先ほど作成したi18n.tsxロケールファイルをインポートします。

import ja from './ja.json';
import en from './en.json';
...

次に、翻訳情報を返すgetMesseges()を追記します。

...
const getMessages = (locale: string): {[key: string]: string} => {
  switch (locale) {
    case 'ja':
      return ja;
    case 'en':
      return {
        ...getMessages('ja'),
        ...en,
      };
    default:
      throw new Error('unknown locale');
  }
};
...
  • 'ja'が引数に指定された場合、日本語の翻訳情報を返します。
  • 'en'が引数に指定された場合、英語の翻訳情報を返します。ただ、欠落している部分は日本語が入るようになっています。こうすることで英語がないとき日本語にフォールバックすることができます。

IntlProviderを設定する

react-intlを利用するため、アプリのルートコンポーネント<IntlProvider>でラップする必要があります。
また、Intlポリフィルも合わせてimportする必要があるので、i18n.tsx<IntlProvider>をラップした<IntlProviderWrapper>作成して、それをApp.tsxで呼び出すようにしたいと思います。

まず、i18n.tsxの一行目でポリフィルをインポートします。

import 'intl';
import 'intl/locale-data/jsonp/ja';
import 'intl/locale-data/jsonp/en';
...
  • import 'intl/locale-data/jsonp/ja';は、サポートする言語が増えるたび、同様に追加する必要があります。今回は日本語(ja)と英語(en)だけです。これをインポートしないと起動したときにIntlが見つからないよと怒られます。

次に、i18n.tsx<IntlProvider>をラップした<IntlProviderWrapper>を返すコンポーネントを作成します。 childrenを受け取るようにするので、指定する型(ReactNode)をインポートしておきます。

...
import {ReactNode} from 'react';
...
export const IntlProviderWrapper = ({children}: {children: ReactNode}) => {
  const locale = getLocale();
  return (
    <IntlProvider locale={locale} messages={getMessages(locale)}>
      {children}
    </IntlProvider>
  );
};
...
  • localeには翻訳するロケールを指定します
  • messagesにはロケールファイルから取得したJSONオブジェクトを渡します。こうすることで、<IntlProvider>でラップしたコンポーネントで、keyに紐づく値を取得することができるようになります。

次に、App.tsxを開いて、作成したでルートコンポーネントをラップします。

import * as React from 'react';
import {SafeAreaView, Text} from 'react-native';
import {IntlProviderWrapper} from './i18n';

const App = () => {
  return (
    <SafeAreaView>
      <Text>{'Hello'}</Text>
    </SafeAreaView>
  );
};

export default () => {
  return (
    <IntlProviderWrapper>
      <App />
    </IntlProviderWrapper>
  );
};

これで設定DONE。

実際に翻訳してみる

<FormattedMessage>を使うことで文字列の翻訳ができます。
FormattedMessageをインポートして、<SafeAreaView>の中身を以下のように変更します。

...
import {FormattedMessage} from 'react-intl';
...
const App = () => {
  return (
    <SafeAreaView>
      <Text>
        <FormattedMessage id={'title'} />
      </Text>
      <Text>
        <FormattedMessage id={'name'} values={{name: '太郎'}} />
      </Text>
      <Text>
        <FormattedMessage id={'message'} />
      </Text>
    </SafeAreaView>
  );
};
...
  • idには、keyを指定します。
  • valuesには、差し込む値を指定します。ちなみに、HTMLも差し込めます。 ※ こちらには、文字列以外の対応方法も載っています。

実行結果

端末の言語設定を英語にして実行した場合のキャプチャです。
en.jsonには、messageというkeyがないので日本語にフォールバックされてます。

f:id:kasssssy:20200429202004p:plain

メソッドの引数などに翻訳した文字列を使いたい場合

useIntl()フックを使うことで実現できます。

import {useIntl} from 'react-intl';
...
const {formatMessage} = useIntl();
const name = formatMessage({id: 'name'}, {name: '太郎'})

みたいに使えます。

最後に

最後まで読んでいただきありがとうございます。
誰かの参考になれば嬉しいです。

この記事に書いたコードは、GitHubに全て上げました。

参考

https://github.com/formatjs/react-intl