レベルエンター山本大のブログ

面白いプログラミング教育を若い人たちに

BLOCKVROCKリファレンス目次はこちら

astro+Restのサービスを作る時にCORSにならない方法

Astroからaxiosやfetchやらで、RestAPIを呼ぼうとするとCORSの制限に引っかかる。

ローカルだとClientとServerをそれぞれポートを分けて立てたりするから。

仮に以下のようにするとする

Astro => http://localhost:3000/

Server => http://localhost:8080/api/

Astro側のpackage.json"proxy": "http://localhost:8080/と、サーバーのホストを知らせてやれば良い。

{

 "name": "sampleservice",
 "type": "module",
 "proxy": "http://localhost:8080/,
 "version": "0.0.1",
 ・・・
}

覚書:git である程度大きいファイルをアップする時に、pushが進まなくなる現象の対処

Git のプッシュでWriteが終わり、Total 6626 (delta 1700), reused 0 (delta 0), pack-reused 0と出たあとに止まってしまう。 ある程度大きなファイルがあるときは、以下のようにバッファを増やしてからpush する。

git config http.postBuffer 157M

大きすぎてエラーが出る場合は、LFSを使う。

A-FrameでVRを始めるときにトラブらない最低限の設定

A-Frameを使ったVRをいくつか作っていて、いつも設定で回避していることを備忘録的に記載します。

最終形

最終形を始めに示し、以降で構成要素を見ていくことにします。

<!DOCTYPE html>
<html >
  <head>
    <meta name="description" content="VR template" />
    <meta charset="utf-8" />
    <title>VR Template</title>
    <script type="text/javascript" src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras@v6.1.1/dist/aframe-extras.min.js"></script>

    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
      crossorigin="anonymous"
    />
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
      crossorigin="anonymous"
    ></script>
    <style>
    html{
      height: 100%;
    }
    body{
      font-size: 0.625%;
      overflow: hidden;
      touch-action: manipulation;
      margin: 0;
      height: 100%;
    }
    </style>
    <script type="text/babel">
    AFRAME.registerComponent("init", {
      init: async () => {
        const message:string = "init start";
        console.log(message);
      }
    });
    </script>
    <a id="myEnterARButton" href="#"></a>
  </head>
  <body>
    <a-scene init  renderer="colorManagement: true" vr-mode-ui="enterARButton: #myEnterARButton"
    device-orientation-permission-ui="enabled:true;denyButtonText:いいえ;allowButtonText:はい;cancelButtonText:キャンセル;deviceMotionMessage:向きのセンサーにアクセスしてよろしいですか;mobileDesktopMessage:モバイル用Webサイトに切り替えてこの画面をリロードしてください。"

    gltf-model="dracoDecoderPath:https://www.gstatic.com/draco/v1/decoders/" >
      <a-assets>
        <!-- <a-asset-item id="asset1" src="xxx.glb"></a-asset-item> -->
      </a-assets>

      <a-box  color="red" ></a-box>

      <a-entity id="rig" movement-controls position="0 0 4">
        <a-entity
          look-controls="pointerLockEnabled: false"
          id="main-camera"
          camera=""
        >
          <a-entity
            id="cursor"
            cursor="rayOrigin: mouse;fuse:false"
            raycaster="objects: .clickable"
          ></a-entity>
        </a-entity>
      </a-entity>
      <a-entity laser-controls raycaster="objects: .clickable; far: 10"></a-entity>
      <a-entity laser-controls raycaster="objects: .clickable; far: 10"></a-entity>
    </a-scene>
  </body>
</html>

説明のいらないところから

とりあえず、最終形から分解して説明のいらない基本セットを用意します。 A-Frameのライブラリを呼んで、a-sceneを読み込むだけです。これだけでも3Dシーンが作られるのがA-Frameの手軽なところですね。

<!DOCTYPE html>
<html >
  <head>
    <title>VR Template</title>
    <script type="text/javascript" src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
  </head>
  <body >
    <a-scene >
    </a-scene>
  </body>
</html>

スタイル

スタイル設定は主にスマホでのズームを抑制するものです。

    <style>
    html{
      height: 100%;
    }
    body{
      font-size: 0.625%;
      overflow: hidden;
      touch-action: manipulation;
      margin: 0;
      height: 100%;
    }
    </style>

viewportメタタグも拡大縮小を抑制します。

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />

シーンの設定について

a-scene要素での設定は、以下です。

<a-scene init  renderer="colorManagement: true" vr-mode-ui="enterARButton: #myEnterARButton"
    device-orientation-permission-ui="enabled:true;denyButtonText:いいえ;allowButtonText:はい;cancelButtonText:キャンセル;deviceMotionMessage:向きのセンサーにアクセスしてよろしいですか;mobileDesktopMessage:モバイル用Webサイトに切り替えてこの画面をリロードしてください。"
  • init
    • registerComponentで登録したinitコンポーネントを読み込む。registerComponentのパートは後述。
  • renderer="colorManagement: true"
    • Glbの色が暗くなる問題の対応
  • vr-mode-ui="enterARButton: #myEnterARButton"

    • AndroidバイスでARボタンがVRボタンと重なって表示される。問題を回避します。 head内の<a id="myEnterARButton" href="#"></a>のコードもセットで使います。
  • device-orientation-permission-ui

    • iPhoneジャイロセンサーにアクセスする許可ダイアログを日本語化
      • enabled:true;
      • denyButtonText:いいえ;
      • allowButtonText:はい;
      • cancelButtonText:キャンセル;
      • deviceMotionMessage:向きのセンサーにアクセスしてよろしいですか;
      • mobileDesktopMessage:モバイル用Webサイトに切り替えてこの画面をリロードしてください。
  • gltf-model="dracoDecoderPath:https://www.gstatic.com/draco/v1/decoders/"
    • gltfモデルのDraco圧縮を有効にする

draco圧縮

Draco圧縮は、Googleオープンソース技術でZIP圧縮したものよりも、圧倒的に高い圧縮率を実現できます。

https://2.bp.blogspot.com/-_zGI5CHskzE/WHaG88sMbfI/AAAAAAAABAQ/2nQERexN08AzGNf3N5OYOOCURV7TCyZdgCLcB/s1600/Draco1.png

Draco圧縮するためのツールをインストールします。

npm install -g gltf-pipeline

Draco圧縮をコマンドラインで実施します。

gltf-pipeline -i xxx_from.glb -o xxx_to.glb -d

カメラはリグに入れる

カメラをリグに入れることで、カメラの方向をプログラムから変えることができます。 マウスのタップを有効にするには、カメラの配下にraycaster属性を持った要素を持ちます。 cursorのrayOriginをmouseにするだけかと思いきや、fuse:falseにしないと、マウスクリックやタップで2回クリックが発生するという罠があります

      <a-entity id="rig" movement-controls position="0 0 4">
        <a-entity
          look-controls="pointerLockEnabled: false"
          id="main-camera"
          camera=""
        >
          <a-entity
            id="cursor"
            cursor="rayOrigin: mouse;fuse:false"
            raycaster="objects: .clickable"
          ></a-entity>
        </a-entity>
      </a-entity>
      <a-entity laser-controls raycaster="objects: .clickable; far: 10"></a-entity>
      <a-entity laser-controls raycaster="objects: .clickable; far: 10"></a-entity>
    </a-scene>

この設定にあるmovement-controls属性のために以下のaframe-extrasが必要です。

<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras@v6.1.1/dist/aframe-extras.min.js"></script>

movement-controlsを設定すると、スマホで画面タップしたときに直進する機能が付加されます

AFrameのコンポーネントを作るためのregisterComponentをTypescriptで書けるようにtype="text/babel"でスクリプトを作ります。 このコンポーネントは、a-sceneで設定しています。

    <script type="text/babel">
    AFRAME.registerComponent("init", {
      init: async () => {
        const message:string = "init start";
        console.log(message);
      }
    });
    </script>

Babelのためにインポートが必要ですね。TypescriptにしないまたはWebpackなどでビルドするなら不要です。

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

決定的に重要な資質 Integrityの人

私の大好きな経営の神様ドラッカー。そのドラッカーがよく使った言葉「integrity」は、真摯さと訳されます。
 
大変難しい言葉ですが、高潔、誠実、清廉、品位、完全性を含んだ言葉で、ドラッカーが言うには「integrityこそが組織のリーダーやマネジメントを担う人材にとって決定的に重要な資質である」と。
 
私の解釈では、真面目で誠実であるだけではなく軸をもっていて推進力があり、公正であり裏表がない人物だと考えています。
 
この真摯さについて考えると時に私がイメージする人物が、先日公開したレベルエンターのWebサイトでインタビューの特集記事に協力いただいた、デジタルハリウッド大学池谷 和浩 執行役員です。池谷さんは真摯さ(integrity)という言葉がぴったりだなと思います。
そんな池谷さんから、大変高く評価していただいていることをとても嬉しく思います。
ということで、既にご覧いただいた方もいらっしゃると思いますが、ぜひ御覧ください。
 
 
レベルエンターは沢山の人のお世話になってここまで歩んできておりますが、VR/ARという分野で、池谷さんに出会えたことは大変大きな変化をいただくきっかけになりました。
池谷さんを紹介してくださったのはハコスコの 藤井 直敬 先生、その藤井先生とのきっかけをいただいたのが、カヤック柳澤 大輔CEOVR部の原さんでビッグな繋がりの先に頂いたご縁です。
レベルエンターとしてはまだまだ沢山の人にお世話になっており、機会を見つけて、お世話になった方々にインタビューのご協力をいただきたいなと思っています。

Webサイトリニューアルと夏休みのイベント

株式会社レベルエンターの新しいWebサイトができました。

levelenter.com


たくさんの皆様に協力いただき、オープンすることができました。
WebARになるのでちょっと遊んでみてください。

 

また、7月1日よりレベルエンターは8期目が始まっています。

7年間もフラフラとしておりまして、これからもフラフラする予定です。

 

また、BLOCKVROCKをプラットフォームにして、夏祭りメタバースを0から作り込みむワークショップを夏休みに開催します。


GREE VR Studio Laboratoryさんのプロデュースで、デジハリの教授の皆様の協力もいただきます。
森ビルさんの会場をかりて、やります。豪華布陣!

スペシャリストたちのチームで、中々すごいのができそうです。

vr.gree.net

NFTとXMLの類似性

NFTについてひとこと言えるほどの力量はないけれど、やっぱり最近の以下の議論がとても面白かったので書きたくなってしまった。

kumagi.hatenablog.com

NFTとXMLはアルファベット3文字の略称という以外にも、「実現できることは比較的シンプルな技術でありながら、壮大な未来が語られる」あたりが似ているように思えています。

XML

2000年前半、当時IT業界新卒だった僕は、XML(Extensible Markup Language)は画期的な技術で、ソフトウェア開発技術者としては絶対に知っておくべき技術だと教えられました。

当時XMLについて勉強し、記憶しているのは以下のような文句です。

  • 企業や銀行のデータ連携(EDI)は全てXMLベースに置き換わる
  • 企業はXMLインターフェイスにして業務ノウハウ(ビジネスロジック)を売り買いする時代になる
  • 画面の定義もできる
  • 今後全ての設定ファイルがXMLに置き換わる
  • 医療カルテや定型外のデータが全てXMLになりどんなソフトでも交換可能になる
  • データベースに革命が起きXMLデータベースが覇権を握る
  • HTMLの親戚である。それどころか、HTMLもXMLの1つにすぎないのである

今振り返って聞くと、顧客が欲しかったこととエンジニアの実装の差がよくわかりますが、当時の僕にはまったくわからん技術でした。

さらに、これだけのことができる技術でありながら、勉強してみるとXMLというのは、実の所データ記述するルールぐらいのことでしかなく、さらに謎が深まるわけです。

XMLをわかりたくて仕方なかった僕は、本当によくわからんので勉強しようと思ってXMLマスターという試験を受け、それでもよくわからなくてXMLマスタープロフェッショナルの資格本に執筆者の一人として参加するという斜め上の展開にたどり着いたという思い出があります。

XMLマスター教科書プロフェッショナル | 株式会社クロノス 志賀 澄人, 株式会社クロノス 山本 大, XML技術者育成推進委員会 |本 | 通販 | Amazon

この本の評価は残念ながら当時からビミョーでした。

当時の、XML技術によって生み出される未来という喧伝には、いくつもの論理の飛躍がありました。

たとえば、電子カルテが全てXMLになってどんなソフトでも交換可能になるという未来像は

電子カルテXMLフォーマットが業界で統一されたらの話」という論理の飛躍があり

電子カルテを読み込むソフトがその統一フォーマットに対応したらの話」という飛躍があり

「多くの医師がその統一フォーマット準拠のソフトに切り替えたらの話」という飛躍がある

これだけの困難な変化が起こってはじめてXMLで、全ての電子カルテが交換可能になるのです。

というわけで、2000年当時にはSF(サイエンスフィクション)の域を超えない話でした。

NFT

NFT(Non-Fungible Token)も、紐解いていくと要するに取引が記録されて消えにくい証明書トークンであるというもの。

できることは割とシンプルでそれ以上のことはないんだけれど、NFTは大きな期待を持って次のように語られます。

  • NFTでアートやツイートが高額で売れる
  • NFTでデジタルの土地が売り買いできる
  • NFTでメタバースで商売して生きる人類がうまれる
  • NFTをベースとした組織が新しい組織の形態となる

現時点では、NFTをビジネスと紐づけて熱く語る人の中には、高額で取引される投機的な部分だけに興味があるという人が多いようです。

高額な取引が実際に行われているので、いつものバズワードよりも熱を帯びている気がします。

ただ、上記のNFTが生み出す未来の中には、多大な論理の飛躍やいくつかの技術的・社会的なブレイクスルーがあって初めて成り立つことが、すぐにでも起こる未来のように語られている部分もあるように思います。

その期待値によってNFTの現在の価値が釣り上がるのがバブル的です。

論理の飛躍とともに語られる姿は、XMLのそれと近いと感じました。

NFTとメタバースの文脈での、論理の飛躍を含んだシナリオは例えば以下のような感じでしょうか。

「多くの人がメタバースに参加する必然性がある状態になったらの話」

「多くのメタバースを名乗る仮想空間プラットフォームが業界標準を作ったらの話」

「各社のプラットフォームが喧嘩をせずに標準に参画したらの話」

「次にアイテムや土地などの希少性が皆が認める状態になったらの話」

というようなSFが現時点では残っています。

個別の仮想空間プラットフォームが、標準化ないしは統合されて一つの大きなメタバースになるところが特に不透明で、上述した「標準化」というようなこれまで人類が何度も挑戦して失敗したプロセスが発生するシナリオをとるぐらいなら、巨大な1つの仮想空間プラットフォームが他を飲み込んで大きくなるシナリオの方がしっくりくるようにも思います。

ただ、そんな単一企業が独占する仮想空間プラットフォームだとしたら、NFTで土地やアイテムのやりとりを管理するよりも、単なるDBで管理した方がよっぽど理にかなっていてNFTの出番は無くなってしまいます。

ややこしいのは、いままででもソーシャルゲームの世界でNFTを使わなくても十分に成り立っていた土地の売買やアイテムの売買が、NFTによって実現されている出来事のように語られていることです。

NFTとXMLとSF

NFTにおけるSFも、XMLのその後に近くなるのではないでしょうか。

つまりNFTが作り出す未来として語られているSFのいくつかは別の方法で実現され、NFTは本質的に合っている用途(転売防止やデジタルデータの持ち主証明)などで進化する未来です。

XMLはデータフォーマットとしての地位は、JSONにとってかわられましたが言語やOSを問わないデータフォーマットのニーズはまやかしではなかった。

XMLによるEDIはできたところもあったができなかったところもあった。やっぱり標準化が安易にできるとは思わない方がいいですね。

SOAPという通信は今では見かけなくなったけれど、RESTやGraphQLにその遺伝子を残し、概ねXMLでやりたかったことは実現したように思います。

ビジネスロジックは売り買いできませんでしたね。

XMLを勉強していた当時、これは絶対無理だろうと思っていたUDDIというアイデアがありました。

SOAP通信のインターフェイス定義を全世界統一のUDDIというディレクトリで管理してシステム連携先を選ぶようなプロトコルで、データ連携の接続先を自動で切り替えることまで視野にいれていました。

流石にそれは無理があったのですが、システム開発を楽にする方法としてRESTful APIで様々なサービスを接続先に選ぶことはできるようになったり、

npmなどのパッケージマネージャが参照するグローバルなパッケージのリポジトリがあったり、当時思い描いたものとは異なるけれども便利な世界が実現されています。

NFTも課題が残る部分(よく言われるガス代とか環境負担とか)をそのままに発展するとは考えにくく、代替技術が出てきてからが本番というように感じています。

標準化みたいな時間のかかる論理の飛躍が含まれている未来像は、20年スパンぐらいでコモディティな感じになってから現実的になってくるような感じではないでしょうか。

そういう感じで、実現することとしないことがありつつ、一部別の形で実現し進むのでしょうね。

テクノロジーや人類の発展にはSFの力も必要だと僕は思います。

人間には想像できることしか実現できないので、XMLが出た時に夢のような未来を描くことがなければ、実現に向けて現実感をもって投資したり熱意を持って開発したりできなかったと思います。

過去の多くのSFでも予想ができなかった未来に我々は生きています。

ドラえもんの道具や、BTTFの2015年など答え合わせがされていっていて、こらから先の未来を作るためには、もっとSFが必要なんだろうなって思います。

Typescriptで国際化対応する方法を試行錯誤したので、viteつかってハンズオンできるように残す

f:id:iad_otomamay:20220207221801g:plain
国際化対応の完成図

Typescriptを使った開発で、国際化対応(多国語へのコンテンツ変換)に対応し、できるだけ開発体験をよくするための方法を考えてみました。

この記事では、typescriptの環境を作るためにviteでバニラtypescript(フレームワークを使わないシンプルなtypescript)のプロジェクトを作ってみることもやってみます。

僕は、自分のサービスで国際化対応するのは初めてではなく、なんどか試みたことがあるのですが開発体験がものすごく下がることが辛くて、一度挫折しました。

その課題を解決する方法を模索していて、ある程度納得のいく環境ができたので記事にします。

国際化対応のつらさ(課題)

当初はi18nextというパッケージでen.jsonとかja.jsonなどのリソースファイルを作ってやってましたが、以下のような辛さがありました。

  • ソース中を文字列などでgrepした時にリソースファイルに飛んでしまう
  • リソースファイルと本体のソースコードを行ったり来たりする際に迷子になる
  • 別言語での書き方を参考にしたい時に、リソースファイルが別れているのでいちいち探すのがつらい

どんな形が自分には理想的か?

開発の規模や国際化対応の規模によって、全然話が変わってくるのですが、僕の場合とりあえず日・ひらがな・英ぐらいで対応できたらいいだけなので小規模前提です。

  • 国際化の必要な文字リソースはプロジェクトのあらゆる場所に頻繁に出てくるので、1文字でも記述量を減らしたいし補完で書きたい。
  • リソースファイルの定義では、各言語が別ファイルにあるのではなく同一語句はまとめて定義したい。
  • リソースファイルは、機能群毎に分割したい。
  • IDEの機能でリソースファイルにすぐに飛びたい。
  • Typescriptの補完が効くのがベストだけれど、そのために書くべきことが増えるのはだめ。

ということで試行錯誤した結果、以下のような書き方に落ち着きました。

  • 利用する側
const title = translate.to("こんにちは世界", "title");

app.innerHTML = `<h1>${title}</h1>`;

#to()の第1引数に、日本語でのリソースを記述することによって、ソースの可読性やgrepした時の課題を解決します。

#to()の第2引数は、リソースのキーを記述します。これは定義されたリソースファイルのプロパティ名で補完されます。(後述しますがtypescript のkeyofがミソです)

translateオブジェクトの参照を辿れば、リソースファイルへはすぐに辿り着けるようにしました。

書き方を変えて以下のようにHTMLやVueテンプレートなどに埋め込むことも想定します。

translateを短くtに置き換えておくとテンプレートの中でも邪魔にならないですね。

  • テンプレートに文字列を埋め込む
const t = translate;

app.innerHTML = `<h1>${t.to("こんにちは世界", "title")}</h1>`;

そしてリソース定義側は以下のように同じリソース名に対する翻訳は、並べて記述します。

  • リソース定義
import { Translation } from "./Translation";

class TopResource extends Translation {
  title = {
    en: "Hello world!",
    cn: "你好世界!",
  };
 foo = {
   en: "Bar",
   cn: "婆",
 }
}
export const topResource = new TopResource();

translateオブジェクトは機能単位で分割可能で、Translationクラスを継承すればOKです。

リソースが増えても、toの第2引数はリソースのプロパティ(titleやfoo)で補完でき、型チェックも効いてくれます!

ではそんな環境を作っていきましょう

ハンズオン開始!viteプロジェクトを作成

vite は最近出てきたフロントエンドのツールで、ややこしかったビルド環境の構築を瞬時にやってくれるすごいやつです。 React、VueといったフレームワークだけではなくバニラなTypescriptの環境も作ってくれます(ちゃんとimportなどを解決してくれます)

以下のコマンドを実行します。

npm init vite@latest

そうすると対話式で、プロジェクトのScaffoldをつくてくれます。 まずはプロジェクト名、今回は「localization」と名づけました。

Project name: › localization

次に利用するフレームワークですが、ここは「vanilla」つまりフレームワークなしを選択しました。選択肢が広くていいですね。

? Select a framework: › - Use arrow-keys. Return to submit.
❯   vanilla
  vue
  react
  preact
  lit
  svelte

次は、JSかTSか。「vanilla-ts」を選びました。

? Select a variant: › - Use arrow-keys. Return to submit.
vanilla
❯   vanilla-ts

「Done. 」

Scaffolding project in /Users/dai/projects/2022/localization...

Done. Now run:

出来上がったプロジェクトのtreeを見てみると以下のような感じです。

.
├── favicon.svg
├── index.html
├── package.json
├── src
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
└── tsconfig.json

node_modulesがないのでnpm installを実行しておきます。

これだけで、VanillaなTypescript環境が出来上がりました。

package.jsonにあるdev のscriptをnpm runで実行しましょう。

npm run dev

ブラウザで「http://localhost:3000」にアクセスします。

f:id:iad_otomamay:20220207214947p:plain
Vite初期画面

VanillaなTypescriptをまともに作ろうと思うと、これまではどうしてもWebPackやらが必要になっていたのでありがたいです。

以降では、国際化対応のコードを記載していきます。

国際化対応のコードを記載

まずはi18next関連のライブラリをインストールします。

npm install i18next i18next-browser-languagedetector

次にリソースファイルです。

  • TopResource.ts(新規)
import { Translation } from "./Translation";

class TopResource extends Translation {
  title = {
    en: "Hello world!",
    cn: "你好世界!",
  };
 foo = {
   en: "Bar",
   cn: "婆",
 }
}
export const topResource = new TopResource();

次のクラスが翻訳を実施するクラスです。

  • Translation.ts(新規)
import i18next from "i18next";
import i18nextBrowserLanguageDetector from "i18next-browser-languagedetector";

/**
 * 対応言語を設定
 */
const Languages = ["en", "ja", "cn"] as const;
type LanguageTypes = typeof Languages[number];

/**
 * リソースファイルの親クラス
 */
export class Translation {
  constructor() {
    i18next.use(i18nextBrowserLanguageDetector).init();
  }

  // 対応する言語かを判定
  isAvailable = (name: string): name is LanguageTypes => {
    return Languages.some((value) => value === name);
  };

  /**
   * 現在選択されている言語を返す、リストになければenを返す
   */
  get language(): LanguageTypes {
    return this.isAvailable(i18next.language) ? i18next.language : "en";
  }

  set language(lang: LanguageTypes) {
    i18next.changeLanguage(lang).catch();
  }

  /**
   * リソースの翻訳を実施する
   * @param defaultJp 日本語のリソース文字列
   * @param key リソースを探すキーワード(リソースファイルのプロパティ名に一致)
   * @returns 翻訳された文字列
   */
  to(defaultJp: string, key: keyof this) {
    if (this.language === "ja") return defaultJp;
    const keyObject = this[key] as any;
    return keyObject[this.language] as string;
  }
}

ポイントは、toのメソッドです。

to(defaultJp: string, key: keyof this) 

引数keyの型がkeyof thisとなっており、継承サブクラスであるResourceクラスのプロパティ名をstring literal union 型にしています。

初めて使ったキーワードですが、なかなか感動!

これでkeyの指定をするために""などの文字列のクオーテーションを書くだけでリソースファイルからキーの一覧が出てきます。

そのほかのテクニックとしては、対応言語をstring literal union typeで指定しつつ、その内容も値として使いたい(string literal unionに含まれるかをチェックしたい)のでas constとtypeofをつかってstring literal unionを作っています。

画面を操作するmain.tsでは、各言語に変換できるボタンを実装しておきます。 宣言的にはかけないので頑張ります。

  • main.ts (修正)
import "./style.css";
import { topResource } from "./TopResource";

const app = document.querySelector<HTMLDivElement>("#app")!;
const t = topResource;

app.innerHTML = `
  <h1 id="title">${t.to("こんにちは世界", "title")}</h1>
  <button id="ja" class="lang"  >Japanese</button>
  <button id="en" class="lang"  >English</button>
  <button id="cn"  class="lang"  >Chinese</button>
`;

document.querySelectorAll(".lang").forEach((b) => {
  b.addEventListener("click", () => {
    t.language = b.id as "en" | "cn" | "ja";
    const title = t.to("こんにちは世界", "title");
    document.getElementById("title")!.innerText = title;
  });
});

できました。

f:id:iad_otomamay:20220207221801g:plain
国際化対応の完成図