From 036d75aa6e4ca2629d46128eaeeb897260d6cea5 Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Tue, 28 Sep 2021 11:04:35 -0400 Subject: [PATCH] Admin-configurable display locale (#14620) * Admin-configurable display locale * Add i18n-js and namespacing * Basic tests and clean up * A few test adjustments * Update vendor cache * Fix a few tests * Fix a few tests * Update app/views/articles/_actions.html.erb Co-authored-by: Michael Kohl * Update app/views/articles/_comments_actions.html.erb Co-authored-by: Michael Kohl * Update app/views/articles/_single_story.html.erb Co-authored-by: Michael Kohl * Update app/views/articles/_single_story.html.erb Co-authored-by: Michael Kohl * Update app/views/comments/_comment_header.html.erb Co-authored-by: Michael Kohl * Update app/views/layouts/_sidebar_tags.html.erb Co-authored-by: Michael Kohl * Update app/views/listings/index.html.erb Co-authored-by: Michael Kohl * Update spec/system/homepage/user_visits_homepage_articles_spec.rb Co-authored-by: Michael Kohl * Update spec/system/user/view_user_index_spec.rb Co-authored-by: Michael Kohl * Alphabetical locale page * Add activerecord custom validation error translations * Add i18n to webpacker * Fix a few tests * Adjust error messages * Add i18n-tasks * Adjust JS to get working with jest * Adjust the way translations are pulled in * Adjust jest tests * Remove time localization * Remove superfluous public js * Add basic tests for i18n application controller * Remove unnecessary content Co-authored-by: Michael Kohl --- Gemfile | 3 + Gemfile.lock | 25 + app/assets/javascripts/base.js.erb | 4 + .../initializeCommentsPage.js.erb | 4 +- .../utilities/buildCommentHTML.js.erb | 4 +- app/controllers/application_controller.rb | 9 + app/helpers/application_helper.rb | 2 +- app/helpers/comments_helper.rb | 6 +- app/javascript/Search/SearchForm.jsx | 4 +- .../Search/__tests__/Search.test.jsx | 3 +- .../articles/__tests__/Article.test.jsx | 6 +- .../__snapshots__/Article.test.jsx.snap | 4 +- .../articles/components/CommentsCount.jsx | 6 +- .../articles/components/ReactionsCount.jsx | 3 +- .../listings/singleListing/DropdownMenu.jsx | 3 +- app/javascript/packs/commentDropdowns.js | 3 +- app/javascript/packs/followButtons.js | 11 +- app/javascript/utilities/locale.js | 10 + app/lib/constants/settings/user_experience.rb | 3 + app/models/settings/user_experience.rb | 1 + .../settings/forms/_user_experience.html.erb | 11 + app/views/articles/_actions.html.erb | 4 +- app/views/articles/_bottom_content.html.erb | 2 +- app/views/articles/_comments_actions.html.erb | 2 +- .../_conduct_and_abuse_actions.html.erb | 2 +- app/views/articles/_single_story.html.erb | 8 +- app/views/articles/_sticky_nav.html.erb | 4 +- app/views/articles/index.html.erb | 12 +- app/views/comments/_comment_footer.html.erb | 2 +- app/views/comments/_comment_header.html.erb | 2 +- app/views/comments/index.html.erb | 3 +- .../registrations/_registration_form.html.erb | 16 - app/views/layouts/_sidebar_tags.html.erb | 6 +- app/views/layouts/_signup_modal.html.erb | 47 +- app/views/layouts/_top_bar.html.erb | 8 +- app/views/layouts/application.html.erb | 2 + app/views/listings/index.html.erb | 4 +- app/views/organizations/_header.html.erb | 2 +- app/views/pages/report_abuse.html.erb | 10 +- app/views/shared/_auth_widget.html.erb | 6 +- .../shared/_authentication_actions.html.erb | 4 +- app/views/sidebars/_homepage_content.html.erb | 4 +- .../stories/tagged_articles/index.html.erb | 2 +- app/views/tags/index.html.erb | 2 +- app/views/users/show.html.erb | 2 +- config/application.rb | 2 + config/i18n-tasks.yml | 133 +++++ config/locales/en.yml | 78 ++- config/locales/fr.yml | 89 +++ .../articleFlows/postSidebarActions.spec.js | 8 +- package.json | 1 + spec/requests/comments_spec.rb | 5 - spec/requests/stories_index_spec.rb | 32 + spec/requests/tags_spec.rb | 2 +- ...accounts_if_feed_mark_canonical_false_.yml | 555 ++++++++++++++++++ ..._accounts_if_feed_mark_canonical_true_.yml | 555 ++++++++++++++++++ .../user_visits_articles_by_tag_spec.rb | 6 +- .../dashboards/user_followers_display_spec.rb | 4 +- .../user_visits_homepage_articles_spec.rb | 4 +- .../homepage/user_visits_homepage_spec.rb | 5 +- .../notifications/notifications_page_spec.rb | 4 +- .../user_views_an_organization_spec.rb | 4 +- .../search/display_users_search_spec.rb | 2 +- .../system/search/user_searches_users_spec.rb | 8 +- spec/system/user/view_user_index_spec.rb | 4 +- .../acts_as_follower.gemspec | 6 +- vendor/cache/easy_translate-0.5.1.gem | Bin 0 -> 8704 bytes vendor/cache/highline-2.0.3.gem | Bin 0 -> 216576 bytes vendor/cache/i18n-js-3.9.0.gem | Bin 0 -> 117248 bytes vendor/cache/i18n-tasks-0.9.34.gem | Bin 0 -> 62464 bytes vendor/cache/rails-i18n-6.0.0.gem | Bin 0 -> 109568 bytes vendor/cache/terminal-table-3.0.1.gem | Bin 0 -> 22528 bytes vendor/cache/thread-0.2.2.gem | Bin 0 -> 14336 bytes yarn.lock | 5 + 74 files changed, 1634 insertions(+), 159 deletions(-) create mode 100644 app/javascript/utilities/locale.js create mode 100644 config/i18n-tasks.yml create mode 100644 config/locales/fr.yml create mode 100644 spec/support/fixtures/vcr_cassettes/Feeds_Import/when_multiple_users_fetch_from_the_same_feed_url/fetches_the_articles_in_both_accounts_if_feed_mark_canonical_false_.yml create mode 100644 spec/support/fixtures/vcr_cassettes/Feeds_Import/when_multiple_users_fetch_from_the_same_feed_url/fetches_the_articles_in_both_accounts_if_feed_mark_canonical_true_.yml create mode 100644 vendor/cache/easy_translate-0.5.1.gem create mode 100644 vendor/cache/highline-2.0.3.gem create mode 100644 vendor/cache/i18n-js-3.9.0.gem create mode 100644 vendor/cache/i18n-tasks-0.9.34.gem create mode 100644 vendor/cache/rails-i18n-6.0.0.gem create mode 100644 vendor/cache/terminal-table-3.0.1.gem create mode 100644 vendor/cache/thread-0.2.2.gem diff --git a/Gemfile b/Gemfile index 49e172ee1..af8bf1163 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,7 @@ gem "honeybadger", "~> 4.9" # Used for tracking application errors gem "honeycomb-beeline", "~> 2.7.0" # Monitoring and Observability gem gem "html_truncator", "~> 0.4" # Truncate an HTML string properly gem "htmlentities", "~> 4.3", ">= 4.3.4" # A module for encoding and decoding (X)HTML entities +gem "i18n-js", "~> 3.9.0" # Helps with internationalization in Rails. gem "httparty", "~> 0.19" # Makes http fun! Also, makes consuming restful web services dead easy gem "imgproxy", "~> 2.0" # A gem that easily generates imgproxy URLs for your images gem "inline_svg", "~> 1.7" # Embed SVG documents in your Rails views and style them with CSS @@ -122,9 +123,11 @@ group :development do gem "brakeman", "~> 5.1", require: false # Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis gem "bundler-audit", "~> 0.9" # bundler-audit provides patch-level verification for Bundled apps gem "derailed_benchmarks", "~> 2.1", require: false # A series of things you can use to benchmark a Rails or Ruby app + gem "easy_translate", "~> 0.5.1" # Google translate tie-in to be used with i18n tasks gem "erb_lint", "~> 0.0.37", require: false # ERB Linter tool gem "guard", "~> 2.18", require: false # Guard is a command line tool to easily handle events on file system modifications gem "guard-livereload", "~> 2.5", require: false # Guard::LiveReload automatically reloads your browser when 'view' files are modified + gem "i18n-tasks", "~> 0.9.34" # Helpers to find and manage missing and unused translations gem "listen", "~> 3.7", require: false # Helps 'listen' to file system modifications events (also used by other gems like guard) gem "memory_profiler", "~> 1.0", require: false # Memory profiling routines for Ruby 2.3+ gem "web-console", "~> 4.1" # Rails Console on the Browser diff --git a/Gemfile.lock b/Gemfile.lock index e4c125f97..ff7359890 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -218,6 +218,9 @@ GEM dotenv-rails (2.7.6) dotenv (= 2.7.6) railties (>= 3.2) + easy_translate (0.5.1) + thread + thread_safe em-websocket (0.5.2) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) @@ -347,6 +350,7 @@ GEM hashie (4.1.0) heapy (0.2.0) thor + highline (2.0.3) hkdf (0.3.0) honeybadger (4.9.0) honeycomb-beeline (2.7.0) @@ -376,6 +380,18 @@ GEM activerecord (>= 5) i18n (1.8.10) concurrent-ruby (~> 1.0) + i18n-js (3.9.0) + i18n (>= 0.6.6) + i18n-tasks (0.9.34) + activesupport (>= 4.0.2) + ast (>= 2.1.0) + erubi + highline (>= 2.0.0) + i18n + parser (>= 2.2.3.0) + rails-i18n + rainbow (>= 2.2.2, < 4.0) + terminal-table (>= 1.5.1) ice_nine (0.11.2) image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) @@ -569,6 +585,9 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) + rails-i18n (6.0.0) + i18n (>= 0.7, < 2) + railties (>= 6.0.0, < 7) railties (6.1.4.1) actionpack (= 6.1.4.1) activesupport (= 6.1.4.1) @@ -769,8 +788,11 @@ GEM stripe (> 5, < 6) strong_migrations (0.7.8) activerecord (>= 5) + terminal-table (3.0.1) + unicode-display_width (>= 1.1.1, < 3) test-prof (1.0.7) thor (1.1.0) + thread (0.2.2) thread_safe (0.3.6) tilt (2.0.10) timecop (0.9.4) @@ -878,6 +900,7 @@ DEPENDENCIES dogstatsd-ruby (~> 4.8) doorkeeper (~> 5.5) dotenv-rails (~> 2.7.6) + easy_translate (~> 0.5.1) email_validator (~> 2.2) emoji_regex (~> 3.2) erb_lint (~> 0.0.37) @@ -906,6 +929,8 @@ DEPENDENCIES htmlentities (~> 4.3, >= 4.3.4) httparty (~> 0.19) hypershield (~> 0.2.2) + i18n-js (~> 3.9.0) + i18n-tasks (~> 0.9.34) imgproxy (~> 2.0) inline_svg (~> 1.7) jbuilder (~> 2.11) diff --git a/app/assets/javascripts/base.js.erb b/app/assets/javascripts/base.js.erb index a4a56931e..96e06ba51 100644 --- a/app/assets/javascripts/base.js.erb +++ b/app/assets/javascripts/base.js.erb @@ -4,8 +4,12 @@ //= require initializePage //= require utilities/getImageForLink //= require honeybadger-js/dist/honeybadger.js +//= require i18n //= require ahoy.js/dist/ahoy.js +I18n.defaultLocale = 'en'; +I18n.locale = document.body.dataset.locale; +I18n.translations = JSON.parse(document.getElementById('i18n-translations').dataset.translations); var instantClick , InstantClick = instantClick = function(document, location, $userAgent) { // Internal variables diff --git a/app/assets/javascripts/initializers/initializeCommentsPage.js.erb b/app/assets/javascripts/initializers/initializeCommentsPage.js.erb index 1024dec15..5f4295778 100644 --- a/app/assets/javascripts/initializers/initializeCommentsPage.js.erb +++ b/app/assets/javascripts/initializers/initializeCommentsPage.js.erb @@ -54,9 +54,9 @@ function initializeCommentsPage() { if (publicReactionCounts[i].count > 0) { if (publicReactionCounts[i].count > 1) { - reactionsLabelWrapper.innerHTML = " likes"; + reactionsLabelWrapper.innerHTML = ` ${I18n.t('core.like').toLowerCase()}s`; } else { - reactionsLabelWrapper.innerHTML = " like"; + reactionsLabelWrapper.innerHTML = ` ${I18n.t('core.like').toLowerCase()}`; } reactionsCountWrapper.id = 'reactions-count-' + publicReactionCounts[i].id; diff --git a/app/assets/javascripts/utilities/buildCommentHTML.js.erb b/app/assets/javascripts/utilities/buildCommentHTML.js.erb index 0619469a3..3e9aba1c5 100644 --- a/app/assets/javascripts/utilities/buildCommentHTML.js.erb +++ b/app/assets/javascripts/utilities/buildCommentHTML.js.erb @@ -68,9 +68,9 @@ function buildCommentHTML(comment) { @@ -471,7 +471,7 @@ Object { diff --git a/app/javascript/articles/components/CommentsCount.jsx b/app/javascript/articles/components/CommentsCount.jsx index 967dc2003..641b1f539 100644 --- a/app/javascript/articles/components/CommentsCount.jsx +++ b/app/javascript/articles/components/CommentsCount.jsx @@ -1,6 +1,8 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; import { Button } from '../../crayons/Button'; +import { locale } from '../../utilities/locale'; + export const CommentsCount = ({ count, articlePath }) => { const commentsSVG = () => ( @@ -28,7 +30,7 @@ export const CommentsCount = ({ count, articlePath }) => { {count}   - {`${count > 1 ? 'comments' : 'comment'}`} + {`${count > 1 ? `${locale('core.comment')}s` : locale('core.comment') }`} @@ -46,7 +48,7 @@ export const CommentsCount = ({ count, articlePath }) => { data-testid="add-a-comment" > 0 - Add comment + {locale('core.add_comment')} ); } diff --git a/app/javascript/articles/components/ReactionsCount.jsx b/app/javascript/articles/components/ReactionsCount.jsx index fb0c3dbe0..6da4c139e 100644 --- a/app/javascript/articles/components/ReactionsCount.jsx +++ b/app/javascript/articles/components/ReactionsCount.jsx @@ -1,6 +1,7 @@ import { h } from 'preact'; import { articlePropTypes } from '../../common-prop-types'; import { Button } from '../../crayons/Button'; +import { locale } from '../../utilities/locale'; export const ReactionsCount = ({ article }) => { const totalReactions = article.public_reactions_count || 0; @@ -32,7 +33,7 @@ export const ReactionsCount = ({ article }) => { {totalReactions}   - {`${totalReactions == 1 ? 'reaction' : 'reactions'}`} + {`${totalReactions == 1 ? locale('core.reaction') : `${locale('core.reaction')}s`}`} diff --git a/app/javascript/listings/singleListing/DropdownMenu.jsx b/app/javascript/listings/singleListing/DropdownMenu.jsx index 7fe9ca0e4..60a9b60f2 100644 --- a/app/javascript/listings/singleListing/DropdownMenu.jsx +++ b/app/javascript/listings/singleListing/DropdownMenu.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import { h, Component, createRef } from 'preact'; import { listingPropTypes } from './listingPropTypes'; import { Button, Dropdown } from '@crayons'; +import { locale } from '@utilities/locale'; const Icon = () => ( ) : ( - Report Abuse + {locale('core.report_abuse')} )} diff --git a/app/javascript/packs/commentDropdowns.js b/app/javascript/packs/commentDropdowns.js index e05338a8c..a68d2f384 100644 --- a/app/javascript/packs/commentDropdowns.js +++ b/app/javascript/packs/commentDropdowns.js @@ -3,6 +3,7 @@ import { initializeDropdown, getDropdownRepositionListener, } from '@utilities/dropdownUtils'; +import { locale } from '@utilities/locale'; /* global Runtime */ @@ -57,7 +58,7 @@ const initializeArticlePageDropdowns = () => { '.report-abuse-link-wrapper', ); if (reportAbuseWrapper) { - reportAbuseWrapper.innerHTML = `Report abuse`; + reportAbuseWrapper.innerHTML = `${locale('core.report_abuse')}`; } // Initialize the "Copy link" functionality diff --git a/app/javascript/packs/followButtons.js b/app/javascript/packs/followButtons.js index cb5670b72..d0cd74f24 100644 --- a/app/javascript/packs/followButtons.js +++ b/app/javascript/packs/followButtons.js @@ -1,4 +1,5 @@ import { getInstantClick } from '../topNavigation/utilities'; +import { locale } from '@utilities/locale'; /* global showLoginModal userData showModalAfterError*/ @@ -8,6 +9,8 @@ import { getInstantClick } from '../topNavigation/utilities'; * @param {HTMLElement} button The Follow button to update * @param {string} style The style of the button from its "info" data attribute */ + + function addButtonFollowText(button, style) { const { name, className } = JSON.parse(button.dataset.info); @@ -28,7 +31,7 @@ function addButtonFollowText(button, style) { followType: className, style: 'follow-back', }); - button.textContent = 'Follow back'; + button.textContent = locale('core.follow_back'); break; default: addAriaLabelToButton({ @@ -37,7 +40,7 @@ function addButtonFollowText(button, style) { followType: className, style: 'follow', }); - button.textContent = 'Follow'; + button.textContent = locale('core.follow'); } } @@ -85,7 +88,7 @@ function addAriaLabelToButton({ button, followType, followName, style = '' }) { * @param {string} style The style of the button from its "info" data attribute */ function addButtonFollowingText(button, style) { - button.textContent = style === 'small' ? '✓' : 'Following'; + button.textContent = style === 'small' ? '✓' : locale('core.following'); } /** @@ -159,7 +162,7 @@ function updateFollowingButton(button, style) { */ function updateUserOwnFollowButton(button) { button.dataset.verb = 'self'; - button.textContent = 'Edit profile'; + button.textContent = locale('core.edit_profile'); addAriaLabelToButton({ button, followName: '', diff --git a/app/javascript/utilities/locale.js b/app/javascript/utilities/locale.js new file mode 100644 index 000000000..10efee1b6 --- /dev/null +++ b/app/javascript/utilities/locale.js @@ -0,0 +1,10 @@ +import I18n from "i18n-js" +const translationsDiv = document.getElementById('i18n-translations') +if (translationsDiv) { + I18n.translations = JSON.parse(translationsDiv.dataset.translations); +} +I18n.defaultLocale = 'en'; +I18n.locale = document.body.dataset.locale; +export function locale(term) { + return I18n.t(term); +} \ No newline at end of file diff --git a/app/lib/constants/settings/user_experience.rb b/app/lib/constants/settings/user_experience.rb index f8ddc12f9..f89b8a8b7 100644 --- a/app/lib/constants/settings/user_experience.rb +++ b/app/lib/constants/settings/user_experience.rb @@ -5,6 +5,9 @@ module Constants default_font: { description: "Determines the default reading font (registered users can change this in their UX settings)" }, + default_locale: { + description: "Determines the default language and lozalization for the Forem (currently in experimental beta)" + }, feed_strategy: { description: "Determines the main feed algorithm approach the app takes: basic or large_forem_experimental " \ "(which should only be used for 10k+ member communities)", diff --git a/app/models/settings/user_experience.rb b/app/models/settings/user_experience.rb index 736c8ce23..b801821db 100644 --- a/app/models/settings/user_experience.rb +++ b/app/models/settings/user_experience.rb @@ -22,6 +22,7 @@ module Settings # a public forem could have more fine-grained authentication (listings ar private etc.) in future setting :public, type: :boolean, default: 0 setting :tag_feed_minimum_score, type: :integer, default: 0 + setting :default_locale, type: :string, default: "en" setting :display_in_directory, type: :boolean, default: true end end diff --git a/app/views/admin/settings/forms/_user_experience.html.erb b/app/views/admin/settings/forms/_user_experience.html.erb index de7c1050e..3b761cc05 100644 --- a/app/views/admin/settings/forms/_user_experience.html.erb +++ b/app/views/admin/settings/forms/_user_experience.html.erb @@ -54,6 +54,17 @@ multiple: false, class: "selectpicker" %> +
+ <%= admin_config_label :default_locale, model: Settings::UserExperience %> + <%= admin_config_description Constants::Settings::UserExperience::DETAILS[:default_locale][:description] %> + <%= select_tag "settings_user_experience[default_locale]", + options_for_select( + [%w[English en], %w[Français fr]], + Settings::UserExperience.default_locale, + ), + multiple: false, + class: "selectpicker" %> +
<%= admin_config_label :primary_brand_color_hex, model: Settings::UserExperience %> <%= admin_config_description Constants::Settings::UserExperience::DETAILS[:primary_brand_color_hex][:description] %> diff --git a/app/views/articles/_actions.html.erb b/app/views/articles/_actions.html.erb index 3c42c1dc9..bcafe9584 100644 --- a/app/views/articles/_actions.html.erb +++ b/app/views/articles/_actions.html.erb @@ -39,7 +39,7 @@ id="copy-post-url-button" class="flex justify-between crayons-link crayons-link--block w-100 bg-transparent border-0" data-postUrl="<%= article_url(@article) %>"> - Copy Post URL + <%= t("core.copy_link") %> <%= inline_svg_tag("copy.svg", aria_hidden: true, id: "article-copy-icon", class: "crayons-icon mx-2 shrink-0", title: "Copy article link to the clipboard") %> @@ -89,7 +89,7 @@ Share Post via... - Report Abuse + <%= t("core.report_abuse") %>
diff --git a/app/views/articles/_bottom_content.html.erb b/app/views/articles/_bottom_content.html.erb index 8d8db2f03..62ed2f61e 100644 --- a/app/views/articles/_bottom_content.html.erb +++ b/app/views/articles/_bottom_content.html.erb @@ -1,6 +1,6 @@ <% if articles && articles.any? %> diff --git a/app/views/layouts/_sidebar_tags.html.erb b/app/views/layouts/_sidebar_tags.html.erb index 21445bac7..bb5f467f9 100644 --- a/app/views/layouts/_sidebar_tags.html.erb +++ b/app/views/layouts/_sidebar_tags.html.erb @@ -1,14 +1,14 @@