KAEDE Hack blog

JavaScript 中心に ライブラリなどの使い方を解説する技術ブログ。

React react-i18next 「1つの」 json file から各言語のテキストを展開する

map を使わず読み込む場合

en.json

{
  "welcome": "Welcome to i18next",
  "hello": "Hello",
  "changeLang": "Change Language"
}

ja.json

{
  "welcome": "i18next へようこそ!",
  "hello": "こんにちわ",
  "changeLang": "言語を切り替える"
}

App.js/import

import React, { useState, useEffect } from 'react';

import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import { useTranslation } from 'react-i18next'

App.js/ヘッダー部分

i18n.use(initReactI18next).init({
  resources: {
    en: {
      translation: enJson,
    },
    ja: {
      translation: jaJson,
    },
  },
  lng: 'en',
  fallback: 'en',
  interpolation: {escapeValue: false},
});

App.js/ Function App 前半部分

function App() {
  const [t, i18n] = useTranslation();
  const [lang, setLang] = useState('en');

  useEffect( () => {
    i18n.changeLanguage(lang)
  } ,[lang, i18n]);

App.js/ Function App return 部分

  return (
    <div className="App">
      <p>{t('welcome')}</p> 
      <div>
        <button onClick={
          () => setLang('en')
        }>
          English
        </button>
        <button onClick={
          () => setLang('ja')
        }>
          Japanese
        </button>
      </div>
    </div>
  );
}

export default App;

result

f:id:kei_s_lifehack:20210309212752p:plain

これで切り替えが動く


json ファイルからまとめて map する場合

locale.json

[
  {
    "name": "en",
    "welcome": "Welcome to i18next",
    "hello": "Hello",
    "changeLang": "Change Language"
  },
  {
    "name": "zh",
    "welcome": "欢迎光临 i18next",
    "hello": "你好",
    "changeLang": "用中文"
  },
  {
    "name": "ja",
    "welcome": "i18next へようこそ!",
    "hello": "こんにちわ",
    "changeLang": "言語を切り替える"
  },
  {
    "name": "ko",
    "welcome": "i18next에 오신 것을 환영합니다",
    "hello": "안녕하세요",
    "changeLang": "한국어 변경"
  },
  {
    "name": "es",
    "welcome": "Bienvenida a i18next",
    "hello": "Hola",
    "changeLang": "usar español"
  }
]

App.js/ json の読み込み確認

import locales from './locales/lang.json'
console.log('Locales read:')
console.table(locales)

f:id:kei_s_lifehack:20210309213244p:plain

これで json ファイルの中に 一つの配列が入っていて、その中に

name: "en", welcome: "welcome to i..." と入っていることを読み取ることができた。

あとはこのデータをいかに react-i18next に渡すかになる。

init の失敗例

これで失敗して苦戦した。

i18n.use(initReactI18next).init({
  resources: {
    locales.map( (locale) => {
      console.log(locale.name);
      {locale.name}:{
        translation: {locale}
      }
    } )
  },
  lng: 'en',
  fallback: 'en',
  interpolation: {escapeValue: false},
});

これだと動かない。

i18n は object で受け取ることになってるのに、配列で渡しているからだ。


全体配列から1オブジェクト事に

object で json ファイル自体を書き直す手もあるが、ファイルはそのままでも、 取り出し方を工夫すれば object として渡せる。

array.reduce() を使って配列一つ一つに処理を施し、object として作る。

object はこれ { }

i18n.use(initReactI18next).init({
  resources: locales.reduce( (stack, locale) => {
    stack[locale.name] = { translation: locale }
    return stack
  }, {}),
  lng: 'en',
  fallback: 'en',
  interpolation: { escapeValue: false }
});

これで 前述の locale.json を当てる

[ { hoge: ua, }, { huu: gya, } , ]

の形ででかい配列にオブジェクトが5つ入ってるやつ。

stack[local.name] が en が入り

locale として今回使われている

f:id:kei_s_lifehack:20210321023445p:plain

配列の 0 こ目の object が当たる。

その object を i18next さんの translation に当ててひと言語分完了。

次に stack[local.name] に zh

locale として 1 こ目の json...

と当たっていき、全てが当てはめられる。

5 言語のボタンも map で書く

明日やる

React で ./src/locales/ の中身を全て読み取ろうとしたが無理だった - react は複数の json を読み込めない!

why

kei-s-lifehack.hatenablog.com

前回の記事で 5言語の切り替えを実装したが、毎回

import xxJson from './locales/xx.json'

xx: {                  
  translation: xxJson, 
},                     

<button onClick={     
  () => setLang('xx') 
}>                    
  XXXXXX

これを書かないと言語を追加できないクソ設計なので直す

やったこと

react-i18next で json ファイルを map しようとした

fs で map しようとするけどどの代替ライブラリを使ってもできなかった

teratail.com

Reactでfsは使えません。 React動かすのは大抵ブラウザ上でしょ?

説明ページの中身をよく読んで、これはNode.js上で動かす事を想定しているのか、 JavaScript上で動かす事を想定しているのか推測してください。

teratail で完璧な回答があった.....

解決策

一つの json file にデータを置いて map する

medium.com

map する先の json は 1 file!

それの実行は次回の記事

記事のURL をここに入れろ!!!

失敗した取り組み

fs をまず使ってみるがダメ

path と fs のライブラリを使ってフォルダから読み込んで cll に出力してみる

両方 Node の根幹だから import は必要ない。

const localesDir = path.join(process.cwd(), 'locales')
console.log(localesDir);

これで

/locales

path が 作れてる

fs.readdirSync を...

あれ?

fsWEBPACK_IMPORTED_MODULE_3_default.a.readdirSync is not a function

ググってみると

stackoverflow.com

se brfs , https://github.com/browserify/brfs

fs.readFileSync() and fs.readFile() static asset browserify transform

ブラウザだから使えないようなことが書いてある

fs 以外で使えるものをいろいろググってみるが全滅

brfs は毎回ビルドになる

browserify というシステムの brfs

browserify.org

コンパイラで毎回ビルドするのはやってることと違うので拒否

browserify-fs を使うが同じエラーが出る

github.com

この fs 互換で行けるか?

import fs from 'browserify-fs'

const fileNames = fs.readdirSync(localesDir);

browserify_fsWEBPACK_IMPORTED_MODULE_3_default.a.readdirSync is not a function

readdirSync がない.... 同じエラーだ...

brfs の方だと bundle しないといちいちビルドしないと実行できなくなるってことになる、それは開発効率下がるからしたくない

list-react-files を試すが fs を結局使っている

stackoverflow.com

list-react-files というライブラリがあるらしい

npm i list-react-files

import listReactFiles from 'list-react-files'

listReactFiles(__dirname).then(files => console.log(files))

Promise か....展開して... あれ?

glob.js:547 Uncaught (in promise) TypeError: fs.readdir is not a function
    at Glob.push../node_modules/glob/glob.js.Glob._readdir (glob.js:547)
    at Glob.push../node_modules/glob/glob.js.Glob._processGlobStar (glob.js:625)
    at Glob.push../node_modules/glob/glob.js.Glob._process (glob.js:362)
    at new Glob (glob.js:170)
    at glob (glob.js:75)
    at index.js:29
    at new Promise (<anonymous>)
    at listReactFiles (index.js:28)

listReactFiles は glob を使っていて、glob も fs.readFiles を使っているからダメらしい。

indeed, it throws Unhandled Rejection (TypeError): fs.readdir is not a function – Mugen Jun 18 '19 at 13:34

コメントに書いてある通りやんけ

list-files を使う

リプライがないけど、最後の回答を試してみる

import find from 'list-files'

find(function(result) {
    console.log(result); 
  } ,{
    dir: localesDir,
    name: 'json'
});

index.js:19 Uncaught TypeError: exec is not a function

stackoverflow.com

exec を 使うらしい

const exec  = require('child_process').exec;

実際に使うと

dentifier 'exec' has already been declared (14:7)

import zhJson from './locales/zh.json'

を指摘されてしまう。exec がすでにされているってことらしい

www.freecodecamp.org

exec の基礎が何もわかっていない....

fs でのエラーハンドリング

いろいろ試したけどファイルからの json ファイル名の取得すらできない....

そもそもエラーハンドリングをしないと使えない?

https://www.codegrepper.com/code-examples/javascript/list+all+files+in+dir+react

fs.readdir(localesDir, function (err, files) {
    //handling error
    if (err) {
        return console.log('Unable to scan directory: ' + err);
    } 
    //listing all files using forEach
    files.forEach(function (file) {
        // Do whatever you want to do with the file
        console.log(file); 
    });
});

fsWEBPACK_IMPORTED_MODULE_3_default.a.readdir is not a function

まず readdir が fs にないって言われてしまってどうしようもない...


React react-i18next 挨拶サイト 5言語の切り替えをできるようにした

why

kei-s-lifehack.hatenablog.com

前回の記事で

日英の切り替えだけ作って満足していたら

ブラッシュアップに 対応言語に、中国語(簡体字)、中国語(繁体字)、スペイン語、韓国語、ポルトガル語を追加しろ それができれば、設計の最小限のレベルだ

と言われたので。

問題点

import xxJson from './locales/xx.json'

xx: {                  
  translation: xxJson, 
},                     

<button onClick={     
  () => setLang('xx') 
}>                    
  XXXXXX

言語ひとつ追加するたびにこれ毎回全箇所書くのシステムと呼べないだろ

言語追加するために button を何個も作らないといけない。map すれば楽になりそう。

button が多い。select にすればコンパクトになるけど、select で発火するのを実装する必要がある。クリックの回数が増える。

基礎機能

react-i18next-try の Repository を流用

繁体字どうするの

言語コードと国コード

繁体字は中国語のコードの zh 以外に何があるのかわからなかったが、

書体の問題なので、国-書体 として

zh-hans, zh-hant, にするべきらしい。

言語コードまとめ

www.science.co.il

これを参考にして

zh-hans, zh-hant, の簡体字繁体字

es, ko, pt, のスペイン、コリア、ポルトガル、を使用する。


とりあえず中国語を追加する

前提として前回の記事で日英切り替えができる状態になっている

locales/zh.json を作成

Google Translate で翻訳する

{
  "welcome": "欢迎光临 i18next",
  "changeLang": "用中文"
}

作成

import zhJson from './locales/zh.json'

    zh: {
      translation: zhJson,
    },

App.js で import して zh に当てると

f:id:kei_s_lifehack:20210306212213p:plain

ボタン固定で作成できた。しかし現在の言語をハイライトするか、もしくは非表示にしないと違和感があるな...

参考に厚労省のサイト見たら現在の言語が一番上に出ている設計になっていた。

f:id:kei_s_lifehack:20210320182959p:plain

切り替えると警告が出た

The following pages are translated by a machine translation system. Note that the machine translation system doesn't guarantee 100% correctness. Some proper nouns might not be translated correctly

機械翻訳機能、すごい!

挨拶一覧

Japanese, ja, こんにちわ, ようこそ,
English, en, Hello, Welcome to,
Chinese
  "welcome": "欢迎光临 i18next",
  "hello": "你好",
  "changeLang": "用中文"
Spanish
  "welcome": "Bienvenida a i18next",
  "hello": "Hola",
  "changeLang": "usar español"
Korean
  "welcome": "i18next에 오신 것을 환영합니다",
  "hello": "안녕하세요",
  "changeLang": "한국어 변경"

韓国とスペインと

f:id:kei_s_lifehack:20210306231827p:plain

f:id:kei_s_lifehack:20210306231903p:plain

とりあえずこれで 5言語挨拶できた

まとめ

前述の通り、言語ごとのテキストをまとめて 展開して読み込めるようにする必要がある

次回以降の記事でやる。

React react-i18next で日英切り替え Ark 生物紹介サイトを作る

why

kei-s-lifehack.hatenablog.com

前回の記事で、react-i18next で二言語の切り替えができるようになった

これを使って何かを作りたい!

Hatena ではなく、自分で攻略サイトを作りたい

Next.js で react-i18next が使えそうにないことからガッツリはつくれないが、

生物紹介の2言語版ならできるだろう

1 ページ構成で bootstrap で右上に切り替えボタンを表示する予定

Result

f:id:kei_s_lifehack:20210306100800p:plain

f:id:kei_s_lifehack:20210306100827p:plain

問題点

map してないから全部 return のとこに書かないといけない。構造化して map できるか試してない

json から 引っ張って画像を出せるか試してない

json の string に書くのがめっちゃめんどくさい

Markdown の読み取りのライブラリと matter での配列変換はあるんだから、そこから持ってくれば json なんかを試すより楽にテキストが書けるか...

と思うけどそもそも fs でファイルを読み取るのがフロントではできない、Next がいる。

json ではなく markdown から引っ張ってきて当てられるかいろいろ組まないと、テキストを入れる手間がやばい、もしくは cms と組み合わせるか、

それをするには http ちゃんと使えないと無理。

git init

久しぶりに React のみで立ち上げたので 再び npm と git で立ち上げるやり方を思い出す

npm create-react-app ark-ab-dinos

CRA で React App 作成

cd ark-ab-dinos

そのプロジェクトに移動

GitHub で Repository を作成

git@github.com:kaede0902/ark-ab-dinos.git

ssh url 発行して copy

git init

git remote add origin git@github.com:kaede0902/ark-ab-dinos.git

git add .gitignore

git add .

初期化して git のリモートとローカルの接続

テキスト、辞書ファイルとして locale/ を作る

src/locales/en.json

と ja.json を作る

i18n react-i18next をセット

i18n, initReactI18next, useTransition,

切り替え用の

useEffect, useState,

をセットする

前回の記事と同様。てか App.js を copy

i18next, react-i18next を npm i

中身を書き換える

locales を書き換えた

f:id:kei_s_lifehack:20210306081222p:plain

f:id:kei_s_lifehack:20210306081240p:plain

タイトルを変えていない

title を toggle できるようにする

  useEffect( () => {
    i18n.changeLanguage(lang)
    document.title = lang
  } ,[lang, i18n]);

これで ja, en にタブの名前がなったが、これではおかしい

  useEffect( () => {
    i18n.changeLanguage(lang)
    document.title = t('welcome')
  } ,[lang, i18n]);

useEffect で doc.title に t 関数で welcome のテキストを表示するようにできたが

react_devtools_backend.js:2430 src/App.js Line 31:6: React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array react-hooks/exhaustive-deps

警告が出てしまう。

直前で t を定義してるんだが...

動いているのでとりあえずこれでよしとする。

問題が発生したらシンプルに

document.title = `Ark Abberation Dinos (${lang})`

こうする。

f:id:kei_s_lifehack:20210306082328p:plain

[f:id:kei_s_lifehack:20210306082354p:plain

タイトルを動的に一応反映できた。ja en で三項演算子使って適切なタイトルのテキスト仕込めば行けそう。

  "rava": "ラヴェジャー",
  "rava_desc": "野生では出血攻撃を持つ。ラプターやオオカミと同様にパックボーナスで攻防が倍増する仲間にしても単体ではオオカミと比べると火力が低く、ラプターの倍の硬さの強さしかない。しかしこのサイズで 500kg 以上も掲載量があり、 Zipline を昇り降りできるのがかなり有用。"

これと

  "rava": "Ravager",
  "rava_desc": "Only wild one has bleed attack."

これを json に仕込むことで

f:id:kei_s_lifehack:20210306094119p:plain

f:id:kei_s_lifehack:20210306094144p:plain

生物紹介を出せた!!!

React react-i18next での言語切り替えを試した

why

kei-s-lifehack.hatenablog.com

Next.js と react-intl での言語切り替えがうまくいかなかった、react-18next があると聞いた

suzukalight.com

t, translate 関数での二言語の切り替えが react-intl より簡単そうに見えた

結果

f:id:kei_s_lifehack:20210306045346p:plain

f:id:kei_s_lifehack:20210306045359p:plain

Next.js ではこのコードは使えなかった

React.js 単体では動いた

実装

とりあえず1 からトップだけ作ってみることにする。

ボタンでの言語切り替え。

suzukalight.com

この「素振り」を参考にする

問題点

TS の React.FC の型がどうして参考記事で使われているのか理解できていない

 () => setLang(lang === 'en' ? 'ja' : 'en')      

のロジックは理解できたが、これを自分で思いつける自信がない

github.com

公式の GitHub の サンプルを見たが、どうして class component も使うのか理解できない

install

npm i  i18next react-i18next

いつも通り npm i

t('trans_key')と setLang でのトグル切り替えを仕込む

<p>{t('welcome')}</p> 
<div>                                               
  <button onClick={                                 
    () => setLang(lang === 'en' ? 'ja' : 'en')      
  }>                                                
    {t('changeLang')}                          
  </button>                                         
</div>                                              

最初に jsx に

{ t('translation_key') }

と t 関数と引数の key となる string で t 関数を用いた変換された key を書く

これはあくまで 変換の際に辞書ファイルを検索するための key であり、実際のメッセージではない。

次に btn で クリックされたときに

() => setLang(lang === 'en' ? 'ja' : 'en')

setLang が即時関数で発動し、

引数には lang という変数の中身が 初期値の ja から en になっていた場合であれば

en からの変更なので ja に

en じゃない場合は ja になるので、ja からの変更なので en に

変更する。


i18n の import と辞書の配列の作成

先ほどの index.js の前半 で i18n の import と 変換用の辞書のリソースを入れる

先ほど作った t(welcome), t(changLang) はここからとってくる。

json だし その技術があれば CMS で外部からも持って来れそうだな...

import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'

i18n と initReactI18next を import して初期値を入れる。

i18n.use(initReactI18next).init({
  resources: {
    en: {
      translation: {
        'welcome': 'Welcom to i18next',
        'changeLang': 'Change Language',
      },
    },
    ja: {
      translation: {
        'welcome': 'i18next にようこそ。'
        'changeLang': '言語を切り替える',
      },
    },
  },
  lng: 'en',
  fallback: 'en',
  interpolation: {escapeValue: false},
});

検索先のリソースとして

welcome, changeLang をそれぞれの言語で入れる。

lng で最初の言語、fallback が何かあったときの言語だろう

inter polation と escape value は不明

Hook を使って言語の切り替え関数を作る

index のさらに上部に 本命の useTranslation での 作成した辞書から引っ張ってくるライブラリの import と、react-hooks での state と副作用での切り替え機能を作る

記事では ts のため React.FC を使っているが、function のみで実装できた。

import { useTranslation } from 'react-i18next'
import React, { useState, useEffect } from 'react';

useTranslation と useState, useEffect を import

function App() {
  const [t, i18n] = useTranslation();
  const [lang, setLang] = useState('en');

traslate, i18n, lang, setLang の hook を作成

  useEffect( () => {
    i18n.changeLanguage(lang)
  } ,[lang, i18n]);

lang か i18n が変化したときに、 i18n の changeLanguage を使って 現状の lang の値にする。

JS で type を使っていてエラーが出る

f:id:kei_s_lifehack:20210305115049p:plain

key を間違えていたが、React.FC がないと思って js に React.FC を使ったところ

react_devtools_backend.js:2430 ./pages/index.js:27:10
Syntax error: Const declarations require an initialization value

  25 | });
  26 | 
> 27 | const Home: React.FC = () => {
     |           ^
  28 |   const [t, i18n] = useTranslation();
  29 |   const [lang, setLang] = useState('en');
  30 | 

const Home: React.FC だと初期値がない?からだめとエラーが出た

react_devtools_backend.js:2430 ./pages/index.js:27:8
Syntax error: Unexpected token, expected ";"

  25 | });
  26 | 
> 27 | let Home: React.FC = () => {
     |         ^
  28 |   const [t, i18n] = useTranslation();
  29 |   const [lang, setLang] = useState('en');
  30 | 

let にしても : React.FC が受け入れられない

Next との食い合わせが悪いのか...???

Syntax error: Const declarations require an initialization value

  25 | });
  26 | 
> 27 | const Home:React.FC = (lang) => {
     |           ^
  28 |   const [t, i18n] = useTranslation();
  29 |   const [lang, setLang] = useState('en');

中身を入れてもダメ、どうすればいいのか詰まったが、

js なのに tsの記述があるのは不自然

とご指摘をいただいた。

確かに元のコードは tsx だ。

調べなおしたらそもそも React.FC は React の type だった。

React で無事に動かす

f:id:kei_s_lifehack:20210306045359p:plain

f:id:kei_s_lifehack:20210306045346p:plain

key を統一して 無事にトグルすることができた。

次やるべきこと

これを Next.js でも結局できるのか試す。

React.js のみでは 複数ページのルーティングが手間だったり meta 周りの実装に苦労するのでやはり最終的には Next で動かさなければしっかりとしたものはつくれないだろう

TS の型を入れると何が嬉しいのかわかるようにする。

ここからやりたいこと

レスポンシブにして json ファイルにデータを分けて、かねてより作りたかった Ark Aberration 生物図鑑のサイトを公開する

json ファイルに辞書配列を分ける

suzukalight.com

i18n.use(initReactI18next).init({
  resources: {
    en: {
      translation: {
        "welcome": "Welcom to i18next",
        "changeLang': "Change Language"
      },
    },
    ja: {
      translation: {
        "welcome": "i18next にようこそ。",
        "changeLang": "言語を切り替える"
      },
    },
  },
  lng: 'en',
  fallback: 'en',
  interpolation: {escapeValue: false},
});

この App.js に書いている resources の鍵と翻訳を別のファイルに分ける。

src/locales/ を作成

en.json を作って...

f:id:kei_s_lifehack:20210306054311p:plain

json は single quote がダメらしい。Shift 押したくないんだがな...

f:id:kei_s_lifehack:20210306054505p:plain

これでよし

{
  "welcome": "i18next にようこそ。",
  "changeLang": "言語を切り替える"
}

ja.json も作成。

json ファイルを読み込む

import enJson from './locales/en.json'
import jaJson from './locales/ja.json'

enJson, jaJson, という形で App.js で読み込み

resources: {             
  en: {                  
    translation: enJson, 
  },                     
  ja: {                  
    translation: jaJson, 
  },                     
},                       

初期化のリソースの英語の訳を enJson, 日本語を jaJson, に当てる

これで無事動いた。

あとは key で t() ちゃんが検索してくれるので react-intl みたいに id をつける必要もなし!!!やったね!t ちゃん!!!

これで攻略ブログ作るぞ〜〜〜!!

Next.js getInitialProps で http で UserAgent を取得する

why

en/ja での切り替えができるサイトを作ろうとしたら、header に 301 を書き込んで 移動させたり、現在の url の場所を見たり、req/res の https 周りの知識が全然足りなかった

getInitialProps もよくわかってなかった

Next docs data-fetch の UserAgent を見てみる

nextjs.org

現在のページの情報を取るメソッドがあるので、それを使ってみる

header に 現在のユーザーのロケールやブラウザなどが記載されていて、それをひろえると推測して動かす

jsx 三項演算子の分岐でさらに条件式を使う方法

why

サーバーサイドでは動かないコードを render 部分で処理させる必要があったので使うことになった

三項演算子 での 処理

三項演算子での 条件が true の場合の処理で js を使いたい場合、

    {
      (true)
      ?
        (
        {
          console.log(hoge)
        }
          <div>
          true
          </div>
        )
      :
        (
          <div>no window</div>
        )
    }

このように

{ console.log() }
<div> someTextHere <div>

と書くと

internal server error 505 で

実行されないが

    {
      (true)
      ?
        (
          <div>
        {
          console.log(hoge)
        }
          true
          </div>
        )
      :
        (
          <div>no window</div>
        )
    }

このように

<div> { console.log() }  messageTextHere <div>

と html タグの内部に入れると実行される。

しかし、html タグの内部に書いても

    {
      (true)
      ?
        (
          <div>
        {
          const hoge = 'hogemsg'
          console.log(hoge)
        }
          true
          </div>
        )
      :
        (
          <div>no window</div>
        )
    }

変数定義してそれを出力する処理はできない。

なのでここで処理しなくてはならない場合は、変数定義というクッションをおかずに直接処理を console か html に書き出せば行ける

<div>     
{ 1 * 3 } 
</div>    

このように。内部計算ならできるようだ。

しかし jsx では if は使えないので

<div>               
{ (false)?'y':'n' } 
</div>              

三項演算子ならできた。