ElixirDesktopで作るスマホアプリ Part 10 端末の言語設定を取得 ネイティブとの双方向データ送信

# はじめに

この記事はElixirアドベントカレンダー2024のシリーズ２、13日目の記事です

https://qiita.com/advent-calendar/2024/elixir

端末の言語設定をアプリ起動時に取得して、Phoenix側の言語設定に反映させます

# 流れ

1. [ios]起動
2. [ph]page_live.exにアクセス
3. [ph]phx-hookでネイティブブリッジ実行
4. [ios]言語設定取得
5. [ios]コールバック実行
6. [ph]言語設定の値をセッションに追加
7. [ph]ページを開く際に設定した言語で表示


# [ph]page_live.exにアクセス

```diff_elixir:lib/trarecord_web/router.ex
  scope "/", TrarecordWeb do
    pipe_through :browser

-   get "/", PageController, :index
+   get "/home", PageController, :index
+   live "/", PageLive
  end
```

div要素だけを表示して、マウント時にjs hookのget_localeを実行します
コールバックとして `receive`をキーとしてhandle_eventで待ち受けます

receiveイベントはコールバックの値を付けて /homeにリダイレクトしています

```lib/trarecord_web/live/page_live.ex
defmodule TrarecordWeb.PageLive do
  use TrarecordWeb, :live_view

  def render(assigns) do
    ~H"""
    <div id="main" phx-hook="NativeBridge"></div>
    """
  end

  def mount(_params, _session, socket) do
    socket
    |> push_event("get_locale", %{})
    |> then(&{:ok, &1})
  end

  def handle_event("receive", %{"message" => locale}, socket) do
    {:noreply, redirect(socket, to: ~p"/home?locale=#{String.slice(locale, 0..1)}")}
  end
end
```

# [ph]phx-hookでネイティブブリッジ実行

UAをチェックいて処理を分けていますが、最近ごちゃごちゃしているので確認が必要です
callbackのトリガーは関数をグローバルオブジェクトとして設定しています

```diff_javascript:assets/js/hooks/NativeBridge.js
const NativeBridge = {
  mounted() {
    this.handleEvent("open_safari", ({ url }) => {
      try {
        window.webkit.messageHandlers.openSafari.postMessage(url);
      } catch {}
    });
// 以下追加
    this.handleEvent("get_locale", () => {
      if (window.navigator.userAgent.match(/Mobile/) === null) {
        // PC
        this.pushEvent("receive", { message: "ja" });
      } else if (window.navigator.userAgent.match(/Android/) !== null) {
        // Android
        this.pushEvent("receive", { message: "ja" });
      } else {
        // iOS
        window.webkit.messageHandlers.getLocale.postMessage("get_locale");
      }
    });
    
    const callFromNative = (value) => {
      this.pushEvent("receive", { message: value });
    };
    window.callFromNative = callFromNative;

  },
};
export default NativeBridge;
```

# [ios]言語設定取得

```diff_swift:native/ios/Trarecord/WebView.swift:L38
        super.init()
        
        configuration.userContentController.add(self, name: "openSafari")
+       configuration.userContentController.add(self, name: "getLocale")
```

`Locale(identifier: Locale.preferredLanguages.first!))`
で優先度が一番上の言語を取得しています

```diff_swift:native/ios/Trarecord/WebView.swift:L90
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        switch message.name {
        case "openSafari":
            print(message.body)
            let url = URL(string:message.body as! String)
            if( UIApplication.shared.canOpenURL(url!) ) {
              UIApplication.shared.open(url!)
            }
+       case "getLocale":
+           evalJavaScript(message: Locale(identifier: Locale.preferredLanguages.first!))

        case "error":
            // You should actually handle the error :)
            let error = (message.body as? [String: Any])?["message"] as? String ?? "unknown"
            assertionFailure("JavaScript error: \(error)")
        default:
            assertionFailure("Received invalid message: \(message.name)")
        }
    }
```

# [ios]コールバック実行

グローバルオブジェクトに追加した関数を取得した言語設定の値を付けて実行します
void in以下はデバッグ用にifを付けてるだけなのでコールバック自体は不要です

```diff_swift:native/ios/Trarecord/WebView.swift:L128
    // Native -> JS の呼び出し
    func evalJavaScript(message: Any) {
        let executeScript: String = "window.callFromNative(\"\(message)\");"
        webview.evaluateJavaScript(executeScript, completionHandler: { (object, error) -> Void in
            if let object = object {
                print("ok")
                print(object)
            }
            if let error = error {
                print("error")
                print(error)
            }
        })
    }
```


# [ph]言語設定の値をセッションに追加

日本語だったらセッションにjaを設定、それ以外は英語を設定してトップページにリダイレクトを行います



```diff_elixir:lib/trarecord_web/controllers/page_controller.ex
defmodule TrarecordWeb.PageController do
  use TrarecordWeb, :controller
  alias Trarecord.Accounts.User

- def index(conn, _params) do
-   redirect_to(conn, conn.assigns.current_user)
- end
+ def index(conn, %{"locale" => "ja"}) do
+   conn
+   |> put_session(:locale, "ja")
+   |> redirect_to(conn.assigns.current_user)
+ end

+ def index(conn, %{"locale" => _locale}) do
+   conn
+   |> put_session(:locale, "en")
+   |> redirect_to(conn.assigns.current_user)
+ end

  defp redirect_to(conn, %User{}) do
    redirect(conn, to: ~p"/folders")
  end

  defp redirect_to(conn, nil) do
    render(conn, :home, layout: false)
  end
end
```


# [ph]ページを開く際に設定した言語で表示

言語設定がセッションに入っているのでmount時にロケールをセットします
共通処理なので、on_mountで行うと良いかもしれません

```diff_elixir:lib/trarecord_web/live/onboarding_live/index.ex:L34
  def mount(_params, session, socket) do
+   Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))

    socket
    |> assign(:page, 1)
    |> assign(:page_data, Enum.find(intro(), &(&1.page == 1)))
    |> then(&{:ok, &1})
  end
```

# 最後に

JS hook -> WKUserScript
WKUserScript -> global object function
で相互に通信できることがわかりました

ログアウト、ユーザー削除、利用規約などのアプリ全体の設定画面を実装します
本記事は以上になりますありがとうございました