マイペースなRailsおじさん

Ruby、Ruby on Rails、オブジェクト指向設計を主なテーマとして扱います。だんだん大きくなっていくRuby on Rails製プロダクトのメンテナンス性を損なわない方法を考えたり考えなかったりしている人のブログです。

Railsで、ユーザーの環境にあった言語を表示する

やりたいこと

  • 言語設定が英語の人には英語、日本語の人には日本語を表示したい
  • HTTPのAccept-Languageヘッダに応じて切り替えたい

手順

  1. Rack::Localeミドルウェアを導入する
  2. ローカライズする

Rack::Localeミドルウェアを使うと、Accept-Languageヘッダに応じてI18n.localeを設定してくれる。

1. Rack::Localeミドルウェアを導入する

 bundle add rack-contrib

config/application.rb(任意environment.rbや、initializerの中でも良いです)

config.middleware.use Rack::Locale

2. 訳文を設定する

config/locales配下に、言語ごとのファイルを作り、コードに訳文のキーを設定します。 詳しくは下記を参照

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

Use rails application configuration to read credentials instead of reading it directly.

When we use secret values, we can utilize credentials. Explained at below page.
Securing Rails Applications — Ruby on Rails Guides

For example, to use secret information for omniauth configuration, we can read from credentials.

  config.omniauth(
    :twitter,
    Rails.application.credentials.twitter[:api_key],
    Rails.application.credentials.twitter[:api_secret]
  )

But I don't prefer user credentials directly. Because it is just a configuration. In staging or testing environment, it may not be secret.

I think mediate Rails.configuration to read values.

config/twitter.yml

shared:
  api_key: <%= Rails.application.credentials.twitter[:api_key] %>
  api_secret: <%= Rails.application.credentials.twitter[:api_secret] %>
test:
  api_key: dummy
  api_secret: dummy

config/application.rb

config.twitter = config_for(:twitter)

initializers/devise.rb

  config.omniauth(
    :twitter,
    Rails.configuration.twitter[:api_key],
    Rails.configuration.twitter[:api_secret]
  )

In this case, there are 2 benefits.

  1. Encrypt only truly secret values.
  2. Works correctly in environments do not have master.key.

So, I prefer use credentials only in yaml configuration file or enviroment/*.rb file.

Set placeholder for 'contenteditable' element.

We can add placeholder to contenteditable element.

HTML

[contenteditable="true"]:empty::before {
    content: attr(data-placeholder);
    color: #ccc;
}

CSS

<p contenteditable="true" data-placeholder="Please input something."></p>

And then we placeholder appers. f:id:ytnk531:20210217054622p:plain

Sample

https://jsfiddle.net/ytnk531/tgLw1fv5/5/

We user empty selector to match empty input element. And use attr(data-placeholder) to write placeholder content.

Notice: Element should be empty

Definition of empty is shown in here. We can not have whitespaces inside contenteditable element.

<!- Empty ->
<p contenteditable="true" data-placeholder="Please input something."></p>
<p contenteditable="true" data-placeholder="Please input something."><!- Comments are OK. -></p>
<!- Not Empty ->
<p contenteditable="true" data-placeholder="Please input something.">
</p>
<p contenteditable="true" data-placeholder="Please input something."> </p>

When using some editors, be careful to automatically inserted whitespaces.

tribute.js supports `contenteditable`

tribute.js is used to implement mention feature.

GitHub - zurb/tribute: ES6 Native @mentions

By the way, in Slack (and some chat applications) highlights inputted mentions as illustrated below.

f:id:ytnk531:20210217042734p:plain

tribute.js can implement such decoration. I implemented very simple example in jsfiddle.

https://jsfiddle.net/ntxmrc5y/3/

This can be realized by contenteditable specification. If we use textarea, or text input field, we can not decorate input text. But we can, by using contenteditable element.

sidekiq does not support local time zone.

sidekiq logger prints messages with occurrence time. This time is UTC.

f:id:ytnk531:20210216084008p:plain

I wanted to use JST. After investigating the source code and github of sideq, I retired to do that. Because I realized sidekiq uses only UTC arbitrarily.

In sidekiq logger source code, time is formatted with UTC. It is not configurable.

      class Pretty < Base
        def call(severity, time, program_name, message)
          "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
        end
      end

sidekiq/logger.rb at master · mperham/sidekiq · GitHub

It means if we wanted to use our time zone, we need to override class definitions with monkey patching.

Additionally, the author of sidekiq expressed he prefer use UTC only. Sidekiq time · Issue #2052 · mperham/sidekiq · GitHub

Conclusion

UTC is good, if the framework not support other time zones.

`break` terminates smallest loop and overrides return value.

As you know break terminates loop. Below code execute the block only once. And return nil.

[1, 2, 3].each { |n| break } # => nil

break can have 1 argument. If the argument was given, the value will be used as the return value of evaluation of the method which use the block.

It is possible to return not Array value as the return value of map or select.

each = [1, 2, 3].each { |n| break n }
map = [1, 2, 3].map { |n| break n }
select = [1, 2, 3].select { |n| break n }

pp each # => 1
pp map # => 1
pp select # => 1

Authenticate user by twitter with devise and omniauth

We can authenticate an user with twitter.

Set route as below. This activates callback url from twitter.

  devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
  devise_scope :user do
    get 'sign_in', :to => 'devise/sessions#new', :as => :new_user_session
    get 'sign_out', :to => 'devise/sessions#destroy', :as => :destroy_user_session
  end

Then we need to implement registration sequence.

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: "Twitter") if is_navigational_format?
    else
      @user.save!
      redirect_to root_path
    end
  end

  def failure
    redirect_to root_path
  end
end