feat: add translatable name module for countries and regions

- Introduced `TranslatableName` module to allow for
  localized names for `Country` and `Region` models.
- Updated views to display `localized_name` instead of
  `name` for improved internationalization.
- Refactored JSON serialization for `translations` attribute.
- Enhanced localization support by adding new languages:
  Japanese and Korean, with updated locale files.
- Removed outdated English and Chinese locales for countries
  and regions to clean up the codebase.
This commit is contained in:
songtianlun 2025-02-21 23:46:25 +08:00
parent f6b9dcf187
commit bd42833953
21 changed files with 280 additions and 217 deletions

View File

@ -1 +1 @@
ruby-3.3.5 3.3.5

View File

@ -0,0 +1,22 @@
# app/models/concerns/translatable_name.rb
module TranslatableName
extend ActiveSupport::Concern
def localized_name(default_locale = "en")
return name unless translations.present?
translations_hash = translations.is_a?(String) ? JSON.parse(translations) : translations
# 尝试完全匹配当前语言设置
current_locale = I18n.locale.to_s
return translations_hash[current_locale] if translations_hash[current_locale].present?
# 尝试匹配语言的基础部分(例如 'zh-CN' => 'zh'
base_locale = current_locale.split("-").first
matching_key = translations_hash.keys.find { |k| k.start_with?(base_locale) }
return translations_hash[matching_key] if matching_key.present?
# 如果没有匹配,返回默认语言的翻译或原始名称
translations_hash[default_locale] || name
end
end

View File

@ -1,8 +1,9 @@
class Country < ApplicationRecord class Country < ApplicationRecord
include TranslatableName
extend FriendlyId extend FriendlyId
friendly_id :name, use: :slugged friendly_id :name, use: :slugged
before_save :format_timezones # before_save :format_json_attributes, :timezones, :translations
belongs_to :region, optional: true belongs_to :region, optional: true
belongs_to :subregion, optional: true belongs_to :subregion, optional: true
@ -13,14 +14,15 @@ class Country < ApplicationRecord
validates :code, presence: true, uniqueness: true validates :code, presence: true, uniqueness: true
validates :iso2, uniqueness: true, allow_blank: true validates :iso2, uniqueness: true, allow_blank: true
serialize :translations, coder: JSON
def to_s def to_s
name name
end end
def localized_name # def localized_name
I18n.t("countries.#{code}") # I18n.t("countries.#{code}")
end # end
def self.ransackable_attributes(auth_object = nil) def self.ransackable_attributes(auth_object = nil)
[ "code", "created_at", "id", "id_value", "name", "region_id", "slug", "updated_at" ] [ "code", "created_at", "id", "id_value", "name", "region_id", "slug", "updated_at" ]
@ -32,26 +34,26 @@ class Country < ApplicationRecord
private private
def format_timezones # def format_timezones
return unless timezones.is_a?(String) # return unless timezones.is_a?(String)
#
# 使用正则替换 => 为 : # # 使用正则替换 => 为 :
json_string = timezones.gsub(/=>/, ":") # json_string = timezones.gsub(/=>/, ":")
#
# 清理多余的空格 # # 清理多余的空格
json_string = json_string.gsub(/\s+/, " ").strip # json_string = json_string.gsub(/\s+/, " ").strip
#
begin # begin
# 验证是否为有效的 JSON # # 验证是否为有效的 JSON
parsed_json = JSON.parse(json_string) # parsed_json = JSON.parse(json_string)
self.timezones = parsed_json.to_json # self.timezones = parsed_json.to_json
rescue JSON::ParserError # rescue JSON::ParserError
# 如果转换失败,可以选择: # # 如果转换失败,可以选择:
# 1. 保持原值 # # 1. 保持原值
# 2. 设置为空数组 # # 2. 设置为空数组
# 3. 记录错误日志 # # 3. 记录错误日志
Rails.logger.error("Invalid JSON format for country #{id}: #{timezones}") # Rails.logger.error("Invalid JSON format for country #{id}: #{timezones}")
self.timezones = "[]" # self.timezones = "[]"
end # end
end # end
end end

View File

@ -1,4 +1,6 @@
class Region < ApplicationRecord class Region < ApplicationRecord
include TranslatableName
extend FriendlyId extend FriendlyId
friendly_id :name, use: :slugged friendly_id :name, use: :slugged
@ -9,13 +11,15 @@ class Region < ApplicationRecord
validates :name, presence: true, uniqueness: true validates :name, presence: true, uniqueness: true
validates :code, presence: true, uniqueness: true validates :code, presence: true, uniqueness: true
serialize :translations, coder: JSON
def to_s def to_s
name name
end end
def localized_name # def localized_name
I18n.t("regions.#{code}") # I18n.t("regions.#{code}")
end # end
# 模型中允许被搜索的关联 # 模型中允许被搜索的关联
def self.ransackable_associations(auth_object = nil) def self.ransackable_associations(auth_object = nil)

View File

@ -26,7 +26,7 @@
<!-- 如果有特色图片,显示其信息 --> <!-- 如果有特色图片,显示其信息 -->
<% if featured_art %> <% if featured_art %>
<div class="text-sm text-base-content/60 pt-4"> <div class="text-sm text-base-content/60 pt-4">
Latest from <%= featured_art.city.name %>, <%= featured_art.city.country.name %> <%= "#{t("text.latest_from")} #{featured_art.city.full_name}" %>
<span class="mx-2">•</span> <span class="mx-2">•</span>
<%= featured_art.formatted_time(:date) %> <%= featured_art.formatted_time(:date) %>
</div> </div>
@ -47,18 +47,18 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<%= params[:sort] == 'oldest' ? 'Oldest First' : 'Newest First' %> <%= params[:sort] == 'oldest' ? t("text.oldest_first") : t("text.newest_first") %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52"> <ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
<li> <li>
<%= link_to "Newest First", arts_path(sort: 'newest', region: params[:region]), <%= link_to t("text.newest_first"), arts_path(sort: 'newest', region: params[:region]),
class: "#{'active' if params[:sort] != 'oldest'}" %> class: "#{'active' if params[:sort] != 'oldest'}" %>
</li> </li>
<li> <li>
<%= link_to "Oldest First", arts_path(sort: 'oldest', region: params[:region]), <%= link_to t("text.oldest_first"), arts_path(sort: 'oldest', region: params[:region]),
class: "#{'active' if params[:sort] == 'oldest'}" %> class: "#{'active' if params[:sort] == 'oldest'}" %>
</li> </li>
</ul> </ul>
@ -70,7 +70,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<%= @current_region&.name || t("text.all_regions") %> <%= @current_region&.localized_name || t("text.all_regions") %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
@ -83,7 +83,7 @@
<div class="divider my-1"></div> <div class="divider my-1"></div>
<% @regions.each do |region| %> <% @regions.each do |region| %>
<li> <li>
<%= link_to region.name, arts_path(region: region.id, sort: params[:sort]), <%= link_to region.localized_name, arts_path(region: region.id, sort: params[:sort]),
class: "#{'active' if @current_region == region}" %> class: "#{'active' if @current_region == region}" %>
</li> </li>
<% end %> <% end %>
@ -95,7 +95,7 @@
<div class="text-center text-sm text-base-content/70 mt-4"> <div class="text-center text-sm text-base-content/70 mt-4">
<%= "#{t("text.showing")} #{@weather_arts.total_count} #{t("text.weather_arts")} " %> <%= "#{t("text.showing")} #{@weather_arts.total_count} #{t("text.weather_arts")} " %>
<% if @current_region %> <% if @current_region %>
from <%= @current_region.name %> from <%= @current_region.localized_name %>
<% end %> <% end %>
</div> </div>
</div> </div>
@ -118,7 +118,7 @@
<%= art.city.name %> <%= art.city.name %>
</h3> </h3>
<p class="text-sm text-white/80"> <p class="text-sm text-white/80">
<%= art.city.country.name %> <%= "#{art.city&.country&.emoji + " " || ""}#{art.city&.country&.localized_name}" %>
</p> </p>
<div class="flex items-center gap-2 text-white/90"> <div class="flex items-center gap-2 text-white/90">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">

View File

@ -27,7 +27,7 @@
<div class="inline-block mt-6 px-4 py-2 bg-base-100/80 backdrop-blur-sm rounded-full text-sm"> <div class="inline-block mt-6 px-4 py-2 bg-base-100/80 backdrop-blur-sm rounded-full text-sm">
<%= t("text.latest_from") %> <%= t("text.latest_from") %>
<span class="font-semibold"><%= featured_art.city.name %></span>, <span class="font-semibold"><%= featured_art.city.name %></span>,
<%= featured_art.city.country.name %> <%= featured_art.city.country.localized_name %>
<span class="mx-2">•</span> <span class="mx-2">•</span>
<%= featured_art.formatted_time(:date) %> <%= featured_art.formatted_time(:date) %>
</div> </div>
@ -50,7 +50,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<%= @current_region&.name || t("text.all_regions") %> <%= @current_region&.localized_name || t("text.all_regions") %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
@ -65,7 +65,7 @@
<div class="divider my-1"></div> <div class="divider my-1"></div>
<% @regions.each do |region| %> <% @regions.each do |region| %>
<li> <li>
<%= link_to region.name, <%= link_to region.localized_name,
cities_path(region: region.slug), cities_path(region: region.slug),
class: "#{@current_region == region ? 'active' : ''}" %> class: "#{@current_region == region ? 'active' : ''}" %>
</li> </li>
@ -79,21 +79,21 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg> </svg>
<%= @current_country&.name || "All Countries" %> <%= @current_country&.localized_name || t("text.all_countries") %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52 max-h-80 overflow-y-auto flex-nowrap"> <ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52 max-h-80 overflow-y-auto flex-nowrap">
<li> <li>
<%= link_to "All in #{@current_region.name}", <%= link_to "#{t("text.all_in")} #{@current_region.localized_name}",
cities_path(region: @current_region.slug), cities_path(region: @current_region.slug),
class: "#{@current_country ? '' : 'active'}" %> class: "#{@current_country ? '' : 'active'}" %>
</li> </li>
<div class="divider my-1"></div> <div class="divider my-1"></div>
<% @current_region.countries.order(:name).each do |country| %> <% @current_region.countries.order(:name).each do |country| %>
<li> <li>
<%= link_to "#{country&.emoji + " " || ""}#{country.name}", <%= link_to "#{country&.emoji + " " || ""}#{country.localized_name}",
cities_path(region: @current_region.slug, country: country.slug), cities_path(region: @current_region.slug, country: country.slug),
class: "#{@current_country == country ? 'active' : ''}" %> class: "#{@current_country == country ? 'active' : ''}" %>
</li> </li>
@ -106,9 +106,9 @@
<div class="text-sm text-base-content/70 hidden"> <div class="text-sm text-base-content/70 hidden">
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %> <%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
<% if @current_country %> <% if @current_country %>
in <%= @current_country.name %> in <%= @current_country.localized_name %>
<% elsif @current_region %> <% elsif @current_region %>
in <%= @current_region.name %> in <%= @current_region.localized_name %>
<% end %> <% end %>
</div> </div>
</div> </div>

View File

@ -35,9 +35,10 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</svg> </svg>
<span><%= t("title.sign_in") %></span> <span><%= t("title.sign_in") %></span>
<% end %>
<% end %>
<%= render 'shared/language_switcher' %> <%= render 'shared/language_switcher' %>
<% end %>
<% end %>
</div> </div>
<!-- Mobile Menu --> <!-- Mobile Menu -->

View File

@ -14,8 +14,10 @@
url: request.original_url url: request.original_url
}, },
alternate: { alternate: {
"en" => url_for(locale: 'en'),
"zh-CN" => url_for(locale: 'zh-CN'), "zh-CN" => url_for(locale: 'zh-CN'),
"en" => url_for(locale: 'en') "ja" => url_for(locale: 'ja'),
"ko" => url_for(locale: 'ko')
} }
) %> ) %>
<%= csrf_meta_tags %> <%= csrf_meta_tags %>

View File

@ -1,17 +1,26 @@
<%# app/views/shared/_language_switcher.html.erb %>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle"> <label tabindex="0" class="btn btn-ghost btn-sm">
<% if I18n.locale.to_s == 'en' %> <%= t("language.#{I18n.locale}") %>
🇺🇸 <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor">
<% else %> <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
🇨🇳 </svg>
</label>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-32">
<%= link_to url_for(locale: :en), class: "px-4 py-2 hover:bg-base-200 rounded-lg #{I18n.locale == :en ? 'bg-base-200' : ''}" do %>
<%= t("language.en") %>
<% end %> <% end %>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> <%= link_to url_for(locale: :'zh-CN'), class: "px-4 py-2 hover:bg-base-200 rounded-lg #{I18n.locale == :'zh-CN' ? 'bg-base-200' : ''}" do %>
<%= link_to url_for(locale: :en), class: "#{I18n.locale == :en ? 'active' : ''}" do %> <%= t("language.zh-CN") %>
<li><span class="flex items-center gap-2">🇺🇸 English</span></li>
<% end %> <% end %>
<%= link_to url_for(locale: :'zh-CN'), class: "#{I18n.locale == :'zh-CN' ? 'active' : ''}" do %>
<li><span class="flex items-center gap-2">🇨🇳 简体中文</span></li> <%= link_to url_for(locale: :ja), class: "px-4 py-2 hover:bg-base-200 rounded-lg #{I18n.locale == :ja ? 'bg-base-200' : ''}" do %>
<%= t("language.ja") %>
<% end %>
<%= link_to url_for(locale: :ko), class: "px-4 py-2 hover:bg-base-200 rounded-lg #{I18n.locale == :ko ? 'bg-base-200' : ''}" do %>
<%= t("language.ko") %>
<% end %> <% end %>
</ul> </ul>
</div> </div>

View File

@ -1,37 +1,37 @@
<%# Partial _weather_stats.html.erb %> <%# Partial _weather_stats.html.erb %>
<div class="stat bg-gradient-to-br from-primary/10 to-primary/20 hover:from-primary hover:to-primary/30 p-4 rounded-lg"> <div class="stat bg-gradient-to-br from-primary/10 to-primary/20 hover:from-primary hover:to-primary/30 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Temperature</div> <div class="stat-title font-medium text-base"><%= t("card.temperature") %></div>
<div class="stat-value text-3xl"><%= weather_art.temperature %>°C</div> <div class="stat-value text-3xl"><%= weather_art.temperature %>°C</div>
<div class="stat-desc">Feels like <%= weather_art.feeling_temp %>°C</div> <div class="stat-desc"><%= t("card.feel_like") %> <%= weather_art.feeling_temp %>°C</div>
</div> </div>
<div class="stat bg-gradient-to-br from-secondary/10 to-secondary/20 hover:from-secondary hover:to-secondary/30 p-4 rounded-lg"> <div class="stat bg-gradient-to-br from-secondary/10 to-secondary/20 hover:from-secondary hover:to-secondary/30 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Wind</div> <div class="stat-title font-medium text-base"><%= t("card.wind") %></div>
<div class="stat-value text-3xl"><%= weather_art.wind_scale %></div> <div class="stat-value text-3xl"><%= weather_art.wind_scale %></div>
<div class="stat-desc"><%= weather_art.wind_speed %> km/h</div> <div class="stat-desc"><%= weather_art.wind_speed %> km/h</div>
</div> </div>
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg"> <div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Humidity</div> <div class="stat-title font-medium text-base"><%= t("card.humidity") %></div>
<div class="stat-value text-3xl"><%= weather_art.humidity %>%</div> <div class="stat-value text-3xl"><%= weather_art.humidity %>%</div>
<div class="stat-desc">Relative humidity</div> <div class="stat-desc"><%= t("card.relative_humidity") %></div>
</div> </div>
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg"> <div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Visibility</div> <div class="stat-title font-medium text-base"><%= t("card.visibility") %></div>
<div class="stat-value text-3xl"><%= weather_art.visibility %> km</div> <div class="stat-value text-3xl"><%= weather_art.visibility %> km</div>
<div class="stat-desc">Clear view distance</div> <div class="stat-desc"><%= t("card.clear_view_distance") %></div>
</div> </div>
<div class="stat bg-accent/10 hover:bg-accent p-4 rounded-lg"> <div class="stat bg-accent/10 hover:bg-accent p-4 rounded-lg">
<div class="stat-title font-medium text-base">Pressure</div> <div class="stat-title font-medium text-base"><%= t("card.pressure") %></div>
<div class="stat-value text-3xl"><%= weather_art.pressure %> hPa</div> <div class="stat-value text-3xl"><%= weather_art.pressure %> hPa</div>
<div class="stat-desc">Atmospheric pressure</div> <div class="stat-desc"><%= t("card.atmospheric_pressure") %></div>
</div> </div>
<div class="stat bg-base-200 hover:bg-base-100 p-4 rounded-lg"> <div class="stat bg-base-200 hover:bg-base-100 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Cloud Cover</div> <div class="stat-title font-medium text-base"><%= t("card.cloud_cover") %></div>
<div class="stat-value text-3xl"><%= weather_art.cloud %>%</div> <div class="stat-value text-3xl"><%= weather_art.cloud %>%</div>
<div class="stat-desc">Sky coverage</div> <div class="stat-desc"><%= t("card.sky_coverage") %></div>
</div> </div>

View File

@ -23,7 +23,7 @@
<div class="flex flex-wrap gap-4 mb-6"> <div class="flex flex-wrap gap-4 mb-6">
<div class="badge badge-lg badge-primary"> <div class="badge badge-lg badge-primary">
<%= "#{@weather_art&.city&.country&.emoji + " " || ""}#{@city&.country&.name}" %> <%= "#{@weather_art&.city&.country&.emoji + " " || ""}#{@city&.country&.localized_name}" %>
</div> </div>
<div class="badge badge-lg badge-secondary"> <div class="badge badge-lg badge-secondary">
<%= @weather_art&.city&.state&.name %> <%= @weather_art&.city&.state&.name %>
@ -82,7 +82,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<h3 class="font-display font-bold text-lg">AI Prompt</h3> <h3 class="font-display font-bold text-lg"><%= t("title.ai_prompt") %></h3>
</div> </div>
<p class="text-base-content/80 leading-relaxed"> <p class="text-base-content/80 leading-relaxed">
<%= @weather_art.prompt %> <%= @weather_art.prompt %>

View File

@ -5,7 +5,7 @@ require "i18n/backend/fallbacks"
I18n.load_path += Dir[Rails.root.join("config", "locales", "*.{rb,yml}")] I18n.load_path += Dir[Rails.root.join("config", "locales", "*.{rb,yml}")]
# Permitted locales available for the application # Permitted locales available for the application
I18n.available_locales = [ :en, :"zh-CN" ] I18n.available_locales = [ :en, :"zh-CN", :ja, :ko ]
I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
# I18n::Backend::Simple.include I18n::Backend::Fallbacks # I18n::Backend::Simple.include I18n::Backend::Fallbacks

View File

@ -1,55 +0,0 @@
en:
countries:
# East Asia
CN: 'China'
JP: 'Japan'
KR: 'South Korea'
TW: 'Taiwan'
HK: 'Hong Kong'
# South Asia
IN: 'India'
PK: 'Pakistan'
BD: 'Bangladesh'
# Southeast Asia
ID: 'Indonesia'
VN: 'Vietnam'
TH: 'Thailand'
MM: 'Myanmar'
SG: 'Singapore'
# Middle East
TR: 'Turkey'
IR: 'Iran'
SA: 'Saudi Arabia'
IQ: 'Iraq'
# Africa
NG: 'Nigeria'
EG: 'Egypt'
CD: 'Democratic Republic of the Congo'
TZ: 'Tanzania'
ZA: 'South Africa'
KE: 'Kenya'
AO: 'Angola'
ML: 'Mali'
CI: 'Ivory Coast'
# Europe
RU: 'Russia'
GB: 'United Kingdom'
DE: 'Germany'
# North America
US: 'United States'
MX: 'Mexico'
# South America
BR: 'Brazil'
PE: 'Peru'
CO: 'Colombia'
CL: 'Chile'
# Oceania
AU: 'Australia'

View File

@ -1,55 +0,0 @@
zh-CN:
countries:
# East Asia
CN: '中国'
JP: '日本'
KR: '韩国'
TW: '台湾'
HK: '香港'
# South Asia
IN: '印度'
PK: '巴基斯坦'
BD: '孟加拉国'
# Southeast Asia
ID: '印度尼西亚'
VN: '越南'
TH: '泰国'
MM: '缅甸'
SG: '新加坡'
# Middle East
TR: '土耳其'
IR: '伊朗'
SA: '沙特阿拉伯'
IQ: '伊拉克'
# Africa
NG: '尼日利亚'
EG: '埃及'
CD: '刚果民主共和国'
TZ: '坦桑尼亚'
ZA: '南非'
KE: '肯尼亚'
AO: '安哥拉'
ML: '马里'
CI: '科特迪瓦'
# Europe
RU: '俄罗斯'
GB: '英国'
DE: '德国'
# North America
US: '美国'
MX: '墨西哥'
# South America
BR: '巴西'
PE: '秘鲁'
CO: '哥伦比亚'
CL: '智利'
# Oceania
AU: '澳大利亚'

View File

@ -28,6 +28,11 @@
# enabled: "ON" # enabled: "ON"
en: en:
language:
en: "English"
zh-CN: "简体中文"
ja: "日本語"
ko: "한국어"
hello: "Hello world" hello: "Hello world"
brand: brand:
name: "Today AI Weather" name: "Today AI Weather"
@ -40,12 +45,17 @@ en:
admin_dashboard: "Admin Dashboard" admin_dashboard: "Admin Dashboard"
latest_weather_art: "Latest Weather Art" latest_weather_art: "Latest Weather Art"
popular_weather_art: "Popular Weather Art" popular_weather_art: "Popular Weather Art"
ai_prompt: "AI Prompt"
text: text:
latest_from: "Latest from" latest_from: "Latest from"
search_cities: "Search cities..." search_cities: "Search cities..."
all_regions: "All Regions" all_regions: "All Regions"
all_countries: "All Countries"
all_in: "All in"
showing: "Showing" showing: "Showing"
weather_arts: "Weather Arts" weather_arts: "Weather Arts"
newest_first: "Newest First"
oldest_first: "Oldest First"
cities: cities:
title: "Explore Cities" title: "Explore Cities"
arts: arts:
@ -62,6 +72,18 @@ en:
view_all_weather_arts: "View All Weather Arts" view_all_weather_arts: "View All Weather Arts"
back_to_cities: "Back to Cities" back_to_cities: "Back to Cities"
back_to: "Back to" back_to: "Back to"
card:
temperature: "Temperature"
wind: "Wind"
humidity: "Humidity"
visibility: "Visibility"
pressure: "Pressure"
cloud_cover: "Cloud Cover"
feel_like: "Feels like"
relative_humidity: "Relative humidity"
clear_view_distance: "Clear view distance"
atmospheric_pressure: "Atmospheric pressure"
sky_coverage: "Sky coverage"
pagination: pagination:
showing_items: "Showing %{from} to %{to} of %{total} %{items}" showing_items: "Showing %{from} to %{to} of %{total} %{items}"
items: items:

62
config/locales/ja.yml Normal file
View File

@ -0,0 +1,62 @@
ja:
hello: "こんにちは世界"
brand:
name: "今日のAI天気"
title:
cities: "都市"
arts: "アート"
sign_in: "サインイン"
sign_out: "サインアウト"
settings: "設定"
admin_dashboard: "管理者ダッシュボード"
latest_weather_art: "最新の天気アート"
popular_weather_art: "人気の天気アート"
ai_prompt: "AIプロンプト"
text:
latest_from: "最新情報"
search_cities: "都市を検索..."
all_regions: "すべての地域"
all_countries: "すべての国"
all_in: "すべて含む"
showing: "表示中"
weather_arts: "天気アート"
newest_first: "最新順"
oldest_first: "古い順"
cities:
title: "都市を探る"
arts:
title: "天気アートギャラリー"
subtitle: "世界中の都市から生成されたAI天気アートを発見"
home:
headline_html: 天気が出会う場所<br>人工知能
subtitle:
AI生成アートのレンズを通して天気を体験し、
日常の気象現象に新しい視点をもたらします。
button:
explore_cities: "都市を探る"
view_detail: "詳細を見る"
view_all_weather_arts: "すべての天気アートを見る"
back_to_cities: "都市に戻る"
back_to: "戻る"
card:
temperature: "温度"
wind: "風"
humidity: "湿度"
visibility: "視界"
pressure: "圧力"
cloud_cover: "雲の覆い"
feel_like: "体感温度"
relative_humidity: "相対湿度"
clear_view_distance: "クリアビュー距離"
atmospheric_pressure: "大気圧"
sky_coverage: "空の覆い"
pagination:
showing_items: "合計 %{total} %{items} のうち %{from} から %{to} まで表示"
items:
weather: "天気記録"
default: "アイテム"
time:
formats:
time_only: "%H:%M"
with_zone: "%{time} %{zone}"
date_and_time: "%{date} %{time}"

62
config/locales/ko.yml Normal file
View File

@ -0,0 +1,62 @@
ko:
hello: "안녕하세요 세계"
brand:
name: "오늘의 AI 날씨"
title:
cities: "도시"
arts: "예술"
sign_in: "로그인"
sign_out: "로그아웃"
settings: "설정"
admin_dashboard: "관리자 대시보드"
latest_weather_art: "최신 날씨 예술"
popular_weather_art: "인기 있는 날씨 예술"
ai_prompt: "AI 프롬프트"
text:
latest_from: "최신 소식"
search_cities: "도시 검색..."
all_regions: "모든 지역"
all_countries: "모든 국가"
all_in: "모두 포함"
showing: "표시 중"
weather_arts: "날씨 예술"
newest_first: "최신순"
oldest_first: "오래된 순"
cities:
title: "도시 탐험"
arts:
title: "날씨 예술 갤러리"
subtitle: "전 세계 도시에서 생성된 AI 날씨 예술 발견하기"
home:
headline_html: 날씨가 만나는 곳<br>인공지능
subtitle:
AI 생성 예술의 렌즈를 통해 날씨를 경험하세요,
일상적인 기상 현상에 대한 새로운 관점을 제공합니다.
button:
explore_cities: "도시 탐험"
view_detail: "상세 보기"
view_all_weather_arts: "모든 날씨 예술 보기"
back_to_cities: "도시로 돌아가기"
back_to: "돌아가기"
card:
temperature: "온도"
wind: "바람"
humidity: "습도"
visibility: "가시성"
pressure: "압력"
cloud_cover: "구름 덮개"
feel_like: "체감 온도"
relative_humidity: "상대 습도"
clear_view_distance: "맑은 시야 거리"
atmospheric_pressure: "대기압"
sky_coverage: "하늘 덮개"
pagination:
showing_items: "총 %{total} %{items} 중 %{from}에서 %{to}까지 표시"
items:
weather: "날씨 기록"
default: "항목"
time:
formats:
time_only: "%H:%M"
with_zone: "%{time} %{zone}"
date_and_time: "%{date} %{time}"

View File

@ -1,15 +0,0 @@
en:
regions:
AS: 'Asia'
SA: 'South Asia'
SEA: 'Southeast Asia'
EA: 'East Asia'
ME: 'Middle East'
AF: 'Africa'
NA: 'North Africa'
SSA: 'Sub-Saharan Africa'
EU: 'Europe'
NAM: 'North America'
SAM: 'South America'
CAM: 'Central America'
OC: 'Oceania'

View File

@ -1,15 +0,0 @@
zh-CN:
regions:
AS: '亚洲'
SA: '南亚'
SEA: '东南亚'
EA: '东亚'
ME: '中东'
AF: '非洲'
NA: '北非'
SSA: '撒哈拉以南非洲'
EU: '欧洲'
NAM: '北美洲'
SAM: '南美洲'
CAM: '中美洲'
OC: '大洋洲'

View File

@ -11,12 +11,17 @@ zh-CN:
admin_dashboard: "管理中枢" admin_dashboard: "管理中枢"
latest_weather_art: "气象绘卷·新作" latest_weather_art: "气象绘卷·新作"
popular_weather_art: "气象绘卷·佳作" popular_weather_art: "气象绘卷·佳作"
ai_prompt: "天气描述"
text: text:
latest_from: "新至之城" latest_from: "新至之城"
search_cities: "寻城觅境…" search_cities: "寻城觅境…"
all_regions: "寰宇之境" all_regions: "寰宇之境"
all_countries: "所有国家"
all_in: "全部"
showing: "映现" showing: "映现"
weather_arts: "气象艺境" weather_arts: "气象艺境"
newest_first: "最新优先"
oldest_first: "最早优先"
cities: cities:
title: "云游四海" title: "云游四海"
arts: arts:
@ -32,6 +37,18 @@ zh-CN:
view_all_weather_arts: "尽览天工" view_all_weather_arts: "尽览天工"
back_to_cities: "继续探索城市" back_to_cities: "继续探索城市"
back_to: "回到" back_to: "回到"
card:
temperature: "温度"
wind: "风力"
humidity: "湿度"
visibility: "能见度"
pressure: "气压"
cloud_cover: "云量"
feel_like: "体感温度"
relative_humidity: "相对湿度"
clear_view_distance: "清晰视距"
atmospheric_pressure: "大气压力"
sky_coverage: "天空覆盖"
pagination: pagination:
showing_items: "显示第 %{from} 到第 %{to} 条,共 %{total} 条%{items}" showing_items: "显示第 %{from} 到第 %{to} 条,共 %{total} 条%{items}"
items: items:

View File

@ -32,7 +32,7 @@ namespace :geo do
region.update!( region.update!(
name: data["name"], name: data["name"],
code: data["name"], code: data["name"],
translations: data["translations"], translations: data["translations"].to_json,
flag: data["flag"] || true, flag: data["flag"] || true,
wiki_data_id: data["wikiDataId"] wiki_data_id: data["wikiDataId"]
) )
@ -55,7 +55,7 @@ namespace :geo do
count += 1 count += 1
subregion.update!( subregion.update!(
translations: data["translations"], translations: data["translations"].to_json,
flag: data["flag"] || true, flag: data["flag"] || true,
wiki_data_id: data["wikiDataId"] wiki_data_id: data["wikiDataId"]
) )
@ -106,8 +106,8 @@ namespace :geo do
tld: data["tld"], tld: data["tld"],
native: data["native"], native: data["native"],
nationality: data["nationality"], nationality: data["nationality"],
timezones: data["timezones"], timezones: data["timezones"].to_json,
translations: data["translations"], translations: data["translations"].to_json,
latitude: data["latitude"], latitude: data["latitude"],
longitude: data["longitude"], longitude: data["longitude"],
emoji: data["emoji"], emoji: data["emoji"],