i18nとは

Railsのプラクティスに入りました。 Railsを国際化するために必要なi18nについて調べました。

結論

i18nとは国際化(多言語化)するためのgem。

アプリケーションの文言を英語以外の 別の1つの言語に翻訳 する機能や 多言語サポート 機能を簡単かつ拡張可能な方式で導入するためのフレームワークを提供します。

国際化とLocalizationの違い

分かりにくい国際化とLocalizationの違いについて。

国際化(internationalization)

多言語に対応すること。 使われるすべての文言やロケール固有の要素 (日付や通貨フォーマットなど) の抽象化までの作業のこと。 アプリケーションを国際化するプロセスは以下の3つ。

  1. i18nをサポートする設定を追加。
  2. ロケール辞書の置き場所をRailsに指示する。
  3. ロケールの設定・保存・切替方法をRailsに指示する。

Localization

具体的な翻訳方法を提供したり、そのためのフォーマットを提供すること。 アプリケーションをローカライズするプロセスは以下の3つ。

  1. Railsのデフォルトロケールの差し替えまたはロケールの追加。(日付や時刻のフォーマット、月の呼称、Active Recordモデル名などが対象)
  2. アプリケーションで使われる文字列を抽象化し、キーで検索できる辞書に保存する。(フラッシュメッセージやビュー内の固定テキストなどが対象)
  3. 作成された辞書を別の場所に保存する。

ロケールの切り替え方法

ロケールの切り替えにはいくつかの方法がある。

ロケール設定のポイント

  • config/locales 以下にあるすべての.rbファイルと.ymlファイルは自動的に訳文読み込みパスに追加される。
  • config/application.rb でイニシャライザのデフォルトロケールを変更する。
  • config/application.rb 内のI18n.load_paths に直接追加すると外部gem由来の訳文はオーバーライドされないことに注意する。

方法1: ドメイン毎に切り替える

トップレベルドメインサブドメインドメイン名を基づいてロケールを切り替える。

例) www.example.com: 英語ロケールwww.example.es: スペイン語ロケール

Rails 国際化(i18n)API - Railsガイド

  • ロケールがURLの一部として明確に示される。
  • ApplicationControlleraround_actionロケールを設定する。

方法2: paramsで切り替え

ロケールをURL paramsに含めることでロケールを切り替える。

例) www.example.com/books?locale=jawww.example.com/ja/books

Rails 国際化(i18n)API - Railsガイド

  • ロケールをすべてのURLに含めてリクエスト経由で渡すことでロケールを設定する。
  • ApplicationController#default_url_options をオーバーライドすることでurl_for に依存するすべてのヘルパーメソッド(root_path など)にロケール情報がクエリ文字列に含まれるようになる。
  • config/routes.rbscope "/:locale" do... とすることで"/en/books" のようにロケール情報をURLの上の方に置くことができる。

方法3: クライアントの情報を元に切り替える

ユーザーが選択したロケール設定をデータベースに保存しておき、それを用いてユーザーからの認証済みリクエストごとにロケールを切り替える。

Rails 国際化(i18n)API - Railsガイド

リクエストにロケールが明示的に設定されていない場合には下記の情報を基にロケールをアプリケーション側で設定する。

  • Accept-Language: ブラウザやOSで設定した情報
  • GeoIP: IPアドレスからの地域情報(信頼性が低いためあまりおすすめできない)
  • ユーザープロファイル: 言語選択等

ロケールをセッションやcookieに保存しない

ロケールは透過的にすべきであり、かつURLの一部に含めるべきです。

全てのリソースに対してURIを通して表現できなくなってしまう。 つまり、RESTfulでなくなってしまうのでNG。

RESTful APIとは何なのか - Qiita

リソース指向アーキテクチャ(ROA)とは何なのか - Qiita

まとめ

Railsガイドを読んで自分なりにまとめてみましたが、理解度はいまいちです。 実際にアプリを国際化していくことで理解できる部分もあると思うので手を動かして理解していこうと思います。

参照

Rails 国際化(i18n)API - Railsガイド

RESTful APIとは何なのか - Qiita

リソース指向アーキテクチャ(ROA)とは何なのか - Qiita

ディレクトリ・トラバーサルとは

Sinatraのメモアプリのプルリクでディレクトリ・トラバーサルの危険性があると指摘されました。 初めて聞く言葉だったのでディレクトリ・トラバーサルについて調べました。

結論

ウェブアプリケーションの中には、外部からのパラメータにウェブサーバ内のファイル名を直接指定しているものがあります。このようなウェブアプリケーションでは、ファイル名指定の実装に問題がある場合、攻撃者に任意のファイルを指定され、ウェブアプリケーションが意図しない処理を行ってしまう可能性があります。このような問題の一種を「ディレクトリ・トラバーサルの脆弱性」と呼び、この問題を悪用した攻撃手法の一つに、「ディレクトリ・トラバーサル攻撃」があります。

パス名パラメータを悪用してファイル操作を行う攻撃のようです。

ディレクトリ・トラバーサルが発生しやすいwebアプリの特徴

外部パラメータにwebサーバ内のファイル名を直接指定している。

対策

  • 外部パラメータにwebサーバ内のファイル名を直接指定する仕様を避ける。
  • ファイルを開く際は、固定のディレクトリを指定し、かつファイル名にディレクトリ名が含まれないようにする。

保険的な対策

  • ウェブサーバ内のファイルへのアクセス権限の設定を正しく管理する。
  • ファイル名の指定時にディレクトリを指定できる文字列が含まれていないかチェックする。

ディレクトリ・トラバーサルをやってみる

悲しいことに今回作成したアプリの仕様はまさにディレクトリ・トラバーサルが起こりうる仕様になっていました... せっかくなので作成したアプリで実際にディレクトリ・トラバーサルを試してみます。

リポジトリ: karlley/sinatra-memo: メモを管理できる簡易的なアプリケーション

# 対象のコード

patch '/memos/:id' do |id|
  update_data = { id: id.to_s, title: title_with_default_text, content: params[:content] }
  File.open("data/#{id}.json", 'w') { |file| JSON.dump(update_data, file) }
  redirect "/memos/#{id}"
  erb :show
end
  1. メモの編集画面でDevToolsを開く。
  2. formタグのaction要素のパスを任意の文字列に変更します。(今回は分かりやすくmemos/directory_traversal に変更)
  3. submitボタンをクリックすると任意の文字列を元にした新たなファイルが作成されてしまいます。(今回の場合はmomos/directory_traversal.json )

このように簡単に攻撃を受けてしまいました。

今回の場合のディレクトリ・トラバーサルの対策

本来であれば「外部パラメータにwebサーバ内のファイル名を直接指定する仕様を避ける」という対策が一番良さそうでしたが、ほぼ作り直しになりそうだったので簡易的に対策を行ってみました。対策後のレビューと確認はまだ終わっていないので間違っているかもしれません。

helperにmemo_exists? メソッドを作成し、作成済みのファイルかどうか判断する処理を追加しました。

# helpers/crud_helper.rb

helpers do
  def memo_exists?(id)
    all_ids = Dir.glob('data/*.json').map do |file|
      JSON.parse(File.read(file), symbolize_names: true)[:id]
    end
    all_ids.include?(id)
  end
end

作成したhelperをrequire_relative で読み込み、作成済みのファイルの場合のみupdateの処理が走るように修正しました。作成済みのファイルでない場合は事前に作成しておいたnot_found.erb を表示するように分岐しています。

require_relative 'helpers/crud_helper'

patch '/memos/:id' do |id|
  if memo_exists?(id)
    update_data = { id: id.to_s, title: title_with_default_text, content: params[:content] }
    File.open("data/#{id}.json", 'w') { |file| JSON.dump(update_data, file) }
    redirect "/memos/#{id}"
    erb :show
  else
    erb :not_found
  end
end

上記の修正を追加したことで未作成のファイルへのupdateの処理を防止することができました。

まとめ

ディレクトリ・トラバーサルという攻撃を知る良い機会になって良かったです。 やはりセキュリティは奥が深くとても難しい。 とにかく場数を踏んで色々と覚えていくしかなさそうです。

参照

安全なウェブサイトの作り方 - 1.3 パス名パラメータの未チェック/ディレクトリ・トラバーサル:IPA 独立行政法人 情報処理推進機構

hawksnowlog: Sinatra の Rack::Protection で有効になっているパストラバーサルの機能を無効にしてどのような影響があるのか確認してみた

メソッド名は動詞で始めなくても良い

Sinatraのメモアプリのレビューの中でhelperに定義してメソッド名に対してアドバイスを頂きました。 「メソッド名は動詞で始める」と思い込んでいましたが、間違いだったと気付くことができたので良かったです。

結論

次のような処理内容のメソッド名は動詞で始める必要はない。

  • 毎回同じ値を返すだけのプロパティ系のメソッド: 例)full_name
  • 真偽値を返すだけのメソッド(Rubyの場合): 例)action_required?
  • データ型を変換するだけのメソッド: 例)to_date
  • 検証用のメソッド(英文そのものをメソッド名にする): 例)member_count_should_not_exceed

今回の場合

helperにメモのタイトルが未入力の場合にUntitled が設定されるようなメソッドを定義しました。

def set_title
  params[:title].empty? ? 'Untitled' : params[:title]
end

PR内で以下のようなコメントをいただきました。

setはしていない気がするのでtitle_with_default_text とかですかね?

確かにsetしている訳ではないし、「メソッド名は動詞で始める」と決めつけてメソッド名を考えたような記憶がありました。 コメント中のtitle_with_default_text というメソッド名を見た際に「動詞で始まっていないけど大丈夫なのかな?」という疑問が浮かびましたが、伊藤さんの記事にあるように「毎回同じ値を返すだけのプロパティ系のメソッド」に該当するので動詞で始める必要はないということに気付くことができました。

盲目的に「メソッドは必ず動詞、メソッドは必ず動詞……」と思い込まないように注意してください。

まさにその通りだなと思いました。反省。

まとめ

Rubyのプラクティス後、久しぶりに命名で指摘されましたがRailsのプラクティスに入った今のタイミングで指摘してもらって良かったと思います。 命名は難しいですが、ちょっとでも分かりやすい命名ができるように語彙力を付けていきたいです。

参照

モデルやメソッドに名前を付けるときは英語の品詞に気をつけよう - Qiita

rails s でPlease run rails webpacker:install と表示されて起動しない

Railsの教科書の手順で進めていましたがrails sでアプリが起動しなかったので調べてみました。

結論

node.jsをインストールしてwebpaderをインストールする。

環境

Railsの必要rubyバージョンの確認

下記ページでrails 6.1.6に必要なrubyのバージョンを確認

rails | RubyGems.org | コミュニティのGemホスティングサービス

ruby 2.5.0以降が対応している

状況

rails s でエラー。ログから以下が必要だと読み取れる。

  • rails webpacker:install が必要。
  • RuntimeError が発生している。
/Users/karlley/.rbenv/versions/3.0.3/lib/ruby/3.0.0/digest.rb:6: warning: already initialized constant Digest::REQUIRE_MUTEX
/Users/karlley/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/digest-3.1.0/lib/digest.rb:20: warning: previous definition of REQUIRE_MUTEX was here
=> Booting Puma
=> Rails 6.1.6 application starting in development
=> Run `bin/rails server --help` for more startup options
Exiting
/Users/karlley/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/webpacker-5.4.3/lib/webpacker/configuration.rb:103:in `rescue in load': Webpacker configuration file not found /Users/karlley/Workspace/my_web_apps/helloworld/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /Users/karlley/Workspace/my_web_apps/helloworld/config/webpacker.yml (RuntimeError)

原因

以下の2つがインストールされていないことが原因。

  • webpacker: フロントエンドの開発に必要なアセットを使えるようにするツール
  • node.js: Railsの一部をコンパイルする際に必要なJavaScriptランタイムとして使用する

rails webpacker:install を実行するとnode.jsが必要と表示される。

rails webpacker:install
sh: node: command not found
sh: nodejs: command not found
Node.js not installed. Please download and install Node.js https://nodejs.org/en/download/
Exiting!

対策

  1. webpackerをインストールする為にnode.jsをインストールする。
  2. webpackerをインストールする。

node.jsをhomebrew経由でインストール。

$ brew install node
$ node --version
v18.4.0

再度rails webpacker:install を実行。

$ rails webpacker:install
...
Webpacker successfully installed 🎉 🍰

rails s で正常に起動しました。

$ rails s
=> Booting Puma
=> Rails 6.1.6 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.4 (ruby 3.0.3-p157) ("Birdie's Version")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 77606
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

http://localhost:3000 にアクセスするとwelcome railsが表示されます。

まとめ

やっぱり環境構築は難しい。手順を覚えるのではなく上手く解決できるようにエラーログをしっかり読んで問題を切り分け、対処していくことを心掛けようと思います。

参照

Rails6が立ち上がらない!Node.js, webpacker, yarnのインストール 【開発日記】薬価検索アプリno.5-2 - Qiita

バージョン指定してrails new

最新のrails7系ではなくrails6系でrails new したかったのでバージョン指定する手順を調べました。

結論

  1. 指定したいRailsをgem経由でインストール
  2. rails _バージョン_ new アプリ名 でバージョンを指定してnewする

インストールされているrails gemのバージョンを確認

gem info コマンドでインストール済みのrails gemの情報を確認

$ gem info -e rails

*** LOCAL GEMS ***

rails (7.0.3)
    Author: David Heinemeier Hansson
    Homepage: https://rubyonrails.org
    License: MIT
    Installed at (7.0.3): /Users/karlley/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0

    Full-stack web application framework.

rails の全バージョンの確認

以下のページでrails gemの全バージョンが確認できます。

railsの全バージョン履歴 | RubyGems.org | コミュニティのGemホスティングサービス

コマンドラインから確認する場合はgem search コマンドに以下のオプションを追加します。

  • -e: 文字列で検索
  • -a: 全バージョンを表示
$ gem search -ea rails

指定したいバージョンのrails gem をインストール

インストールされているrails gemのバージョンを確認

  • -l: ローカルのgemを検索
$ gem list rails -l

*** LOCAL GEMS ***

importmap-rails (1.1.2)
rails (7.0.3)
rails-dom-testing (2.0.3)
rails-html-sanitizer (1.4.3)
sprockets-rails (3.4.2)
stimulus-rails (1.0.4)
turbo-rails (1.1.1)

指定したいバージョンのrails gemをインストール

$ gem install rails -v 6.1.6
$ gem list rails -l

*** LOCAL GEMS ***

importmap-rails (1.1.2)
rails (7.0.3, 6.1.6) # 6.1.6が追加されている
rails-dom-testing (2.0.3)
rails-html-sanitizer (1.4.3)
sprockets-rails (3.4.2)
stimulus-rails (1.0.4)
turbo-rails (1.1.1)

バージョンを指定してrails new

バージョン指定でrails new

$ rails _バージョン_ new アプリ名
$ rails _6.1.6_ new helloworld

バージョン確認

$ cd helloworld
$ rails -v
Rails 6.1.6
$ gem info -e rails

*** LOCAL GEMS ***

rails (7.0.3, 6.1.6)
    Author: David Heinemeier Hansson
    Homepage: https://rubyonrails.org
    License: MIT
    Installed at (7.0.3): /Users/karlley/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0
                 (6.1.6): /Users/karlley/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0

    Full-stack web application framework.

バージョン指定してrails newする場合の注意点

  • Rubyのバージョンに付随してrails gemのバージョンは変化する
  • カレントディレクトリが変わるとrails gemのバージョンが変化する

まとめ

バージョン指定してnewするだけですごく時間が掛かってしまった。 学ぶことは無限にあるので必要になったタイミングで深堀りするような行動に変えていこう。

参照

【Ruby on Rails】Railsをバージョン指定してインストールする方法

Railsチュートリアルでrails newするときはバージョンを指定しよう - Qiita