diff --git a/Gemfile b/Gemfile index 9165970713..ec21017139 100644 --- a/Gemfile +++ b/Gemfile @@ -128,6 +128,7 @@ gem 'faraday' gem 'faraday_middleware' gem 'rack-cors' +gem 'rails-i18n' group :production do gem 'bonsai-elasticsearch-rails' # Integration with Bonsa-Elasticsearch on heroku diff --git a/Gemfile.lock b/Gemfile.lock index e0abee70ab..27e16731c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -721,6 +721,7 @@ DEPENDENCIES rails (~> 7.0.4) rails-assets-leaflet.markercluster! rails-controller-testing + rails-i18n rails_12factor rake (>= 10.0.0) responders diff --git a/app/views/admin/index.html.haml b/app/views/admin/index.html.haml index 9a60bb3247..75dda73271 100644 --- a/app/views/admin/index.html.haml +++ b/app/views/admin/index.html.haml @@ -17,7 +17,7 @@ %li= link_to "Roles", admin_roles_path, class: 'nav-link' %li= link_to "Forums", forums_path, class: 'nav-link' %li= link_to "CMS", comfy_admin_cms_path, class: 'nav-link' - %li= link_to t('.garden_types'), garden_types_path, class: 'nav-link' + %li= link_to t('layouts.header.garden_types'), garden_types_path, class: 'nav-link' .col-md-4 .card diff --git a/app/views/admin/roles/index.html.haml b/app/views/admin/roles/index.html.haml index eced7a10d4..a8d2f1abd4 100644 --- a/app/views/admin/roles/index.html.haml +++ b/app/views/admin/roles/index.html.haml @@ -23,8 +23,8 @@ - if can? :edit, role = link_to edit_admin_role_path(role), class: 'btn btn-default btn-xs' do = edit_icon - = t('.edit') + = t('buttons.edit') - if can?(:destroy, role) && ! role.members.any? = link_to admin_role_path(role), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-default btn-xs text-danger' do = delete_icon - = t('.delete') + = t('buttons.delete') diff --git a/app/views/crops/_alternate_names.html.haml b/app/views/crops/_alternate_names.html.haml index 04c4314fc8..d4e69e46f9 100644 --- a/app/views/crops/_alternate_names.html.haml +++ b/app/views/crops/_alternate_names.html.haml @@ -10,11 +10,11 @@ - if can? :edit, an = link_to edit_alternate_name_path(an), class: 'dropdown-item' do = edit_icon - = t('.edit') + = t('buttons.edit') - if can? :destroy, an = link_to an, method: :delete, data: { confirm: 'Are you sure?' }, class: 'dropdown-item' do = delete_icon - = t('.delete') + = t('buttons.delete') - else .badge= an.name diff --git a/app/views/crops/_scientific_names.html.haml b/app/views/crops/_scientific_names.html.haml index bf6ace2503..f0895005c3 100644 --- a/app/views/crops/_scientific_names.html.haml +++ b/app/views/crops/_scientific_names.html.haml @@ -10,11 +10,11 @@ .dropdown-menu.dropdown-menu-xs{"aria-labelledby" => "planting-actions-button"} = link_to edit_scientific_name_path(sn), class: 'dropdown-item' do = edit_icon - = t('.edit') + = t('buttons.edit') .dropdown-divider = link_to sn, method: :delete, data: { confirm: 'Are you sure?' }, class: 'dropdown-item text-danger' do = delete_icon - = t('.delete') + = t('buttons.delete') - else .badge= sn.name diff --git a/app/views/crops/_wrangle.html.haml b/app/views/crops/_wrangle.html.haml index 6c93c734d2..e8777213df 100644 --- a/app/views/crops/_wrangle.html.haml +++ b/app/views/crops/_wrangle.html.haml @@ -8,7 +8,7 @@ .dropdown-menu.dropdown-menu-xs{"aria-labelledby" => "crop-actions-button"} = link_to edit_crop_path(crop), class: 'dropdown-item' do = edit_icon - = t('.edit') + = t('buttons.edit') = link_to crop_openfarm_path(crop), method: :post, class: 'dropdown-item' do = icon 'far', 'update' diff --git a/app/views/layouts/_menu.haml b/app/views/layouts/_menu.haml index fba2e4aeaa..172697ffd7 100644 --- a/app/views/layouts/_menu.haml +++ b/app/views/layouts/_menu.haml @@ -10,7 +10,7 @@ %li.nav-item.dropdown %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"} = image_tag "icons/gardener.svg", class: 'img img-icon' - = t('.record') + = t('layouts.header.record') .dropdown-menu = link_to new_planting_path, class: 'dropdown-item' do = image_icon('planting-add') @@ -27,36 +27,36 @@ - cache("everyone-menu", expires_in: 1.week) do %li.nav-item.dropdown - %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"}= t('.crops') + %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"}= Crop.model_name.human(count: 2).titleize .dropdown-menu = link_to crops_path, class: 'dropdown-item' do - = t('.browse_crops') + = t('layouts.header.browse_crops') = link_to seeds_path, class: 'dropdown-item' do = seed_icon - = t('.seeds') + = t('layouts.header.seeds') = link_to plantings_path, class: 'dropdown-item' do = planting_icon - = t('.plantings') + = t('layouts.header.plantings') = link_to harvests_path, class: 'dropdown-item' do = harvest_icon - = t('.harvests') + = t('layouts.header.harvests') %li.nav-item.dropdown - %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"}= t('.community') + %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"}= t('layouts.header.community') .dropdown-menu{"aria-labelledby" => "navbarDropdown"} - = link_to t('.community_map'), places_path, class: 'dropdown-item' - = link_to t('.browse_members'), members_path, class: 'dropdown-item' - = link_to t('.posts'), posts_path, class: 'dropdown-item' - = link_to t('.forums'), forums_path, class: 'dropdown-item' + = link_to t('layouts.header.community_map'), places_path, class: 'dropdown-item' + = link_to t('layouts.header.browse_members'), members_path, class: 'dropdown-item' + = link_to Post.model_name.human(count: 2).titleize, posts_path, class: 'dropdown-item' + = link_to t('layouts.header.forums'), forums_path, class: 'dropdown-item' - if member_signed_in? - if current_member.role?(:crop_wrangler) || current_member.role?(:admin) %li.nav-item.dropdown - %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"}= t('.admin') + %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"}= t('layouts.header.admin') .dropdown-menu{"aria-labelledby" => "navbarDropdown"} - if current_member.role?(:crop_wrangler) - = link_to t('.crop_wrangling'), wrangle_crops_path, class: 'dropdown-item' + = link_to t('layouts.header.crop_wrangling'), wrangle_crops_path, class: 'dropdown-item' - if current_member.role?(:admin) - = link_to t('.admin'), admin_path, class: 'dropdown-item' + = link_to t('layouts.header.admin'), admin_path, class: 'dropdown-item' %li.nav-item.dropdown %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"} @@ -66,25 +66,25 @@ %span.badge.badge-info= current_member.unread_count .dropdown-menu{"aria-labelledby" => "navbarDropdown"} = link_to member_path(current_member), class: 'dropdown-item' do - = t('.profile') + = t('layouts.header.profile') = link_to member_gardens_path(current_member), class: 'dropdown-item' do - = t('.gardens') + = Garden.model_name.human(count: 2).titleize = link_to member_plantings_path(current_member), class: 'dropdown-item' do - = t('.plantings') + = Planting.model_name.human(count: 2).titleize = link_to member_harvests_path(current_member), class: 'dropdown-item' do - = t('.harvest') + = Harvest.model_name.human(count: 2).titleize = link_to member_seeds_path(current_member), class: 'dropdown-item' do - = t('.seeds') - = link_to t('.posts'), member_posts_path(current_member), class: 'dropdown-item' + = Seed.model_name.human(count: 2).titleize + = link_to Post.model_name.human(count: 2).titleize, member_posts_path(current_member), class: 'dropdown-item' - if current_member.unread_count.positive? .dropdown-divider %strong = link_to(conversations_path, class: 'dropdown-item') do - = t('.inbox') + = t('layouts.header.inbox') %span.badge.badge-info= current_member.unread_count - else - = link_to t('.inbox'), conversations_path, class: 'dropdown-item' + = link_to t('layouts.header.inbox'), conversations_path, class: 'dropdown-item' .dropdown-divider = link_to t('.sign_out'), destroy_member_session_path, method: :delete, class: 'dropdown-item' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index e1a1c35d95..afa4d7720b 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -47,7 +47,8 @@ search: ## Paths to search in, passed to File.find paths: - - app/ + - app/views/ + - app/controllers/ ## Root for resolving relative keys (default) # relative_roots: diff --git a/config/locales/en.yml b/config/locales/en.yml index 0027f36980..8fe9e7df96 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,108 +1,38 @@ --- -# Files in the config/locales directory are used for internationalization -# and are automatically loaded by Rails. If you want to use locales other -# than English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t 'hello' -# -# In views, this is aliased to just `t`: -# -# <%= t('hello') %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# The following keys must be escaped otherwise they will not be retrieved by -# the default I18n backend: -# -# true, false, on, off, yes, no -# -# Instead, surround them with single quotes. -# -# en: -# 'true': 'foo' -# -# To learn more, please read the Rails Internationalization guide -# available at http://guides.rubyonrails.org/i18n.html. - en: - activerecord: - models: - comment: - one: comment - other: comments - crop: - one: crop - other: crops - follow: - one: follow - other: follows - garden: - one: garden - other: gardens - harvest: - one: harvest - other: harvests - member: - one: member - other: members - photo: - one: photo - other: photos - planting: - one: planting - other: plantings - post: - one: post - other: posts - seed: - one: seed - other: seeds - application_helper: - title: - title: - default: Default are_you_sure: Are you sure? buttons: - add: Add add_photo: Add photo - add_seed_to_stash: Add %{crop_name} seeds to stash delete: Delete edit: Edit - harvest: Harvest - harvest_crop: Harvest %{crop_name} - mark_as_active: Mark as active - mark_as_finished: Mark as finished - mark_as_inactive: Mark as inactive - my_gardens: My Gardens - new_seeds: New saved seed + new_harvest: New harvest new_planting: New planting new_post: Write new post - new_harvest: New harvest - plant: Plant - plant_crop: Plant %{crop_name} - plant_something_here: Plant something here - record_harvest: Record Harvest + new_seeds: New saved seed save_seeds: Save seeds - timeline: Timeline - write_blog_post: Write blog post crops: - search: Search crops index: - subtitle: "%{crops_size} total" title: Browse Crops requested: - link: You have %{number_crops} crops awaiting approval subtitle: Pending approval title: Requested crops - edit_crop: Edit crop - forms: - optional: "(Optional)" + date: + abbr_month_names: + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec + errors: + messages: + not_saved: Not saved forums: index: title: Forums @@ -111,23 +41,9 @@ en: deleted: Garden was successfully deleted. form: location_helper: If you have a location set in your profile, it will be used when you create a new garden. - location: "%{owner}'s %{garden}" - overview: - gardensactions: gardens/actions - gardensphoto: gardens/photo - no_plantings: no plantings - plantingsthumbnail: plantings/thumbnail updated: Garden was successfully updated. harvests: - created: Harvest was successfully created. harvest_something: Harvest something - index: - title: - crop_harvests: Everyone's %{crop} harvests - default: Everyone's harvests - owner_harvests: "%{owner}'s harvests" - planting_harvests: Harvests from %{planting} - updated: Harvest was successfully updated. home: blurb: already_html: Or %{sign_in} if you already have an account @@ -139,64 +55,23 @@ en: sign_up: Sign up crops: our_crops: Some of our crops - recently_added: Recently added crops - recently_planted: Recently Planted view_all: View all crops discuss: - discussion: Discussion - forums: Forums view_all: View all posts + harvests: + recently_harvested: Recently Harvested + view_all: View all harvests index: - add_seeds: Add seeds - edit_profile: Edit profile - harvest: Harvest - plant: Plant - post: Post recently_added: Recently Added welcome: Welcome to %{site_name}, %{member_name} - harvests: - view_all: View all harvests members: title: Some of our members view_all: View all members - open: - ad_free_linktext: ad-free - api_docs_linktext: API documentation - creative_commons_linktext: Creative Commons license - get_involved_body_html: > - We believe in collaboration, and work closely with our members and the wider food-growing community. - Our team includes volunteers from all walks of life and all skill levels. To get involved, - visit %{talk_link} or find more information on the %{wiki_link}. - get_involved_title: Get Involved - github_linktext: Github - open_data_body_html: > - We're building a database of crops, planting advice, seed sources, and other information that anyone - can use for free, under a %{creative_commons_link}. You can use this data for research, to build apps, - or for any purpose at all. Read more about our %{wiki_link} and %{api_docs_link}. - open_data_title: Open Data and APIs - open_source_body_html: > - %{site_name} is open source software, which means that we share this website's code for free with our - community and the world. We believe that openness, sustainability, and social good go hand in hand. - You can read more about %{why} or check out our code on %{github}. - open_source_title: Open Source - - support_body_html: Growstuff is independent, %{ad_free} and we have no outside investment. You can support our work by %{buy_account}. - support_title: Support Growstuff - talk_linktext: Growstuff Talk - why_linktext: why Growstuff is open source - wiki_linktext: Growstuff Wiki plantings: recently_planted: Recently Planted view_all: View all plantings seeds: - crop: Crop - description: Description - details: Details - from: From location - owner: Owner title: Seeds available to trade - trade_to: Will trade to - unspecified: unspecified view_all: View all seeds stats: member_linktext: "%{count} members" @@ -205,47 +80,31 @@ en: number_gardens_linktext: "%{count} gardens" number_plantings_linktext: "%{count} times" label: - days_until_harvest: "%{number} days" - weeks_until_harvest: "%{number} weeks until harvest" - days_until_finished: "%{number} days" - weeks_until_finished: "%{number} weeks" - harvesting_now: harvesting now data: 'The data on this page is available in the following formats:' + harvesting_now: harvesting now + weeks_until_harvest: "%{number} weeks until harvest" layouts: + application: + skip: Skip header: - account: Account admin: Admin browse_crops: Browse Crops browse_members: Browse Members community: Community community_map: Community Map - garden_type: Garden Type - garden_types: Garden Types crop_wrangling: Crop Wrangling - crops: Crops - current_memberlogin_name: "%{current_memberlogin_name}" forums: Forums - gardens: Gardens - harvest: Harvest + garden_types: Garden Types harvests: Harvests - record: Record inbox: Inbox - inbox_unread: Inbox (%{unread_count}) plantings: Plantings - posts: Posts profile: Profile + record: Record seeds: Seeds - skip: Skip navigation menu - support_growstuff: Support Growstuff - toggle_navigation: Toggle Navigation - your_stuff: Your Stuff menu: sign_in: Sign in sign_out: Sign out sign_up: Sign up - links: - my_gardens: My gardens - members: edit_profile: Edit profile index: @@ -262,72 +121,31 @@ en: harvest: "%{crop} harvest by %{owner}" planting: "%{planting}" seed: "%{seed}" - thing_by: A %{thing} by %{owner} places: index: title: "%{site_name} Community Map" - planting: - status: - finished: Finished - growing: Growing - harvesting: Harvesting - late: Late - not planted: Not planted - not_enough_data: Not enough data - perennial: Perennial - unknown: Unknown plantings: badges: - days_until_finished: days until finished - days_until_harvest: days until harvest - harvesting_now: harvesting now late_finishing: late finishing - sharedbuttonsfinish_planting: shared/buttons/finish_planting super_late: super late - plant_something: Plant something form: finish_helper: > - A planting is finished when you've harvested all of the crop, or it dies, or it's otherwise - no longer growing in your garden. - index: - title: - crop_plantings: Everyone's %{crop} plantings - default: Everyone's plantings - owner_plantings: "%{owner}'s plantings" - view_owners_profile: View %{owner}'s profile >> - string: "%{crop} planting in %{garden} by %{owner}" - progress: - progress_0_not_planted_yet: 'Progress: 0% - not planted yet' + A planting is finished when you've harvested all of the crop, or it dies, + or it's otherwise no longer growing in your garden. + plant_something: Plant something posts: - write_blog_post: Write blog post index: title: author_posts: "%{author} posts" default: Everyone's posts + write_blog_post: Write blog post seeds: form: - trade_help: > - Are you interested in trading or swapping seeds with other %{site_name} members? If you list - your seeds as available for trade, other members can contact you to request seeds. You can - list any conditions or other information in the description, above. finish_helper: > Seeds are finished when you've planted them all, or you've traded them all away. - index: - title: - crop_seeds: Everyone's %{crop} seeds - default: Everyone's seeds - owner_seeds: "%{owner} seeds" + trade_help: > + Are you interested in trading or swapping seeds with other %{site_name} members? + If you list your seeds as available for trade, other members can contact you to request seeds. + You can list any conditions or other information in the description, above. save_seeds: Save seeds - string: "%{crop} seeds belonging to %{owner}" - unauthorized: - create: - all: Please sign in or sign up to create a %{subject}. - notification: Please sign in to send a message. - planting: Please sign in or sign up to plant something. - post: Please sign in or sign up to post. - seed: Please sign in or sign up to add seeds. - garden_type: Not authorized. Only admins can create garden types. - manage: - all: Not authorized to %{action} %{subject}. - read: - notification: You must be signed in to view notifications. + view: View diff --git a/config/locales/ja.yml b/config/locales/ja.yml deleted file mode 100644 index 2a51c32c5a..0000000000 --- a/config/locales/ja.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -ja: - home: - blurb: - intro: "%{site_name}はガーデナーのコミュニティです。" diff --git a/spec/views/i18n_spec.rb b/spec/views/i18n_spec.rb new file mode 100644 index 0000000000..dd2f0c088b --- /dev/null +++ b/spec/views/i18n_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'i18n/tasks' + +RSpec.describe I18n do + let(:i18n) { I18n::Tasks::BaseTask.new } + let(:missing_keys) { i18n.missing_keys } + let(:unused_keys) { i18n.unused_keys } + let(:inconsistent_interpolations) { i18n.inconsistent_interpolations } + + it 'does not have missing keys' do + expect(missing_keys).to be_empty, + "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" + end + + it 'does not have unused keys' do + expect(unused_keys).to be_empty, + "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" + end + + xit 'files are normalized' do + non_normalized = i18n.non_normalized_paths + error_message = "The following files need to be normalized:\n" \ + "#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \ + "Please run `i18n-tasks normalize' to fix" + expect(non_normalized).to be_empty, error_message + end + + it 'does not have inconsistent interpolations' do + error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \ + "Run `i18n-tasks check-consistent-interpolations' to show them" + expect(inconsistent_interpolations).to be_empty, error_message + end +end