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 <citizen428@dev.to>

* Update app/views/articles/_comments_actions.html.erb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update app/views/articles/_single_story.html.erb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update app/views/articles/_single_story.html.erb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update app/views/comments/_comment_header.html.erb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update app/views/layouts/_sidebar_tags.html.erb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update app/views/listings/index.html.erb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update spec/system/homepage/user_visits_homepage_articles_spec.rb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update spec/system/user/view_user_index_spec.rb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* 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 <citizen428@dev.to>
This commit is contained in:
Ben Halpern 2021-09-28 11:04:35 -04:00 committed by GitHub
parent 01e05e4ce1
commit 036d75aa6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 1634 additions and 159 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -54,9 +54,9 @@ function initializeCommentsPage() {
if (publicReactionCounts[i].count > 0) {
if (publicReactionCounts[i].count > 1) {
reactionsLabelWrapper.innerHTML = "&nbsp;likes";
reactionsLabelWrapper.innerHTML = `&nbsp;${I18n.t('core.like').toLowerCase()}s`;
} else {
reactionsLabelWrapper.innerHTML = "&nbsp;like";
reactionsLabelWrapper.innerHTML = `&nbsp;${I18n.t('core.like').toLowerCase()}`;
}
reactionsCountWrapper.id = 'reactions-count-' + publicReactionCounts[i].id;

View file

@ -68,9 +68,9 @@ function buildCommentHTML(comment) {
</button>
<div id="comment-dropdown-${comment.id}" class="crayons-dropdown right-1 s:right-0 s:left-auto fs-base dropdown">
<ul class="m-0">
<li><a href="${ comment.url }" class="crayons-link crayons-link--block permalink-copybtn" aria-label="Copy link to ${ comment.user.name }'s comment" data-no-instant>Copy link</a></li>
<li><a href="${ comment.url }" class="crayons-link crayons-link--block permalink-copybtn" aria-label="Copy link to ${ comment.user.name }'s comment" data-no-instant>${ I18n.t('core.copy_link') }</a></li>
<li><a href="${ comment.url }/settings" class="crayons-link crayons-link--block" aria-label="Go to ${ comment.user.name }'s comment settings">Settings</a></li>
<li><a href="/report-abuse?url=${ comment.url }" class="crayons-link crayons-link--block" aria-label="Report ${ comment.user.name }'s comment as abusive or violating our code of conduct and/or terms and conditions">Report abuse</a></li>
<li><a href="/report-abuse?url=${ comment.url }" class="crayons-link crayons-link--block" aria-label="Report ${ comment.user.name }'s comment as abusive or violating our code of conduct and/or terms and conditions">${ I18n.t('core.report_abuse') }</a></li>
<li class="${ comment.newly_created ? '' : 'hidden' }"><a href="${ comment.url }/edit" class="crayons-link crayons-link--block" rel="nofollow" aria-label="Edit this comment">Edit</a></li>
<li class="${ comment.newly_created ? '' : 'hidden' }"><a data-no-instant="" href="${ comment.url }/delete_confirm" class="crayons-link crayons-link--block" rel="nofollow" aria-label="Delete this comment">Delete</a></li>
</ul>

View file

@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
before_action :remember_cookie_sync
before_action :forward_to_app_config_domain
before_action :determine_locale
include SessionCurrentUser
include ValidRequest
@ -178,6 +179,14 @@ class ApplicationController < ActionController::Base
Stripe.log_level = Stripe::LEVEL_INFO
end
def determine_locale
I18n.locale = if %w[en fr].include?(params[:locale])
params[:locale]
else
Settings::UserExperience.default_locale
end
end
def remember_cookie_sync
# Set remember cookie token in case not properly set.
if user_signed_in? &&

View file

@ -132,7 +132,7 @@ module ApplicationHelper
followable_name = followable.name
tag.button(
"Follow",
I18n.t("core.follow"),
name: :button,
type: :button,
data: {

View file

@ -53,11 +53,11 @@ module CommentsHelper
def like_button_text(comment)
case comment.public_reactions_count
when 0
"Like"
I18n.t("core.like")
when 1
"&nbsp;like"
"&nbsp;#{I18n.t('core.like').downcase}"
else
"&nbsp;likes"
"&nbsp;#{I18n.t('core.like').downcase}s"
end
end

View file

@ -1,8 +1,10 @@
import PropTypes from 'prop-types';
import { h } from 'preact';
import { forwardRef } from 'preact/compat';
import { locale } from '../utilities/locale';
import { Button } from '@crayons';
const SearchIcon = () => (
<svg
width="24"
@ -31,7 +33,7 @@ export const SearchForm = forwardRef(({ searchTerm, onSubmitSearch }, ref) => (
className="crayons-header--search-input crayons-textfield"
type="text"
name="q"
placeholder="Search..."
placeholder={`${locale('core.search')}...`}
autoComplete="off"
aria-label="Search term"
value={searchTerm}

View file

@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';
import { Search } from '../Search';
import { locale } from '../../utilities/locale';
describe('<Search />', () => {
beforeEach(() => {
@ -40,7 +41,7 @@ describe('<Search />', () => {
const searchInput = getByRole('textbox', { name: /search/i });
expect(searchInput.value).toEqual('fish');
expect(searchInput.getAttribute('placeholder')).toEqual('Search...');
expect(searchInput.getAttribute('placeholder')).toEqual(`${locale('core.search')}...`);
expect(searchInput.getAttribute('autocomplete')).toEqual('off');
});

View file

@ -1,8 +1,10 @@
/* eslint-disable no-irregular-whitespace */
import { h } from 'preact';
import { render } from '@testing-library/preact';
import { axe } from 'jest-axe';
import '@testing-library/jest-dom';
import { Article } from '..';
import { locale } from '../../utilities/locale';
import {
article,
articleWithOrganization,
@ -181,7 +183,7 @@ describe('<Article /> component', () => {
const reactions = getByTitle('Number of reactions');
expect(reactions.textContent).toEqual('232 reactions');
expect(reactions.textContent).toEqual(`232 ${locale('core.reaction')}s`);
});
it('should render with comments', () => {
@ -195,7 +197,7 @@ describe('<Article /> component', () => {
const comments = getByTitle('Number of comments');
expect(comments.textContent).toEqual('213 comments');
expect(comments.textContent).toEqual(`213 ${locale('core.comment')}s`);
});
it('should render with an add comment button when there are no comments', () => {

View file

@ -223,7 +223,7 @@ Object {
<span
class="hidden s:inline"
>
Add comment
[missing "en.core.add_comment" translation]
</span>
</a>
</div>
@ -471,7 +471,7 @@ Object {
<span
class="hidden s:inline"
>
Add comment
[missing "en.core.add_comment" translation]
</span>
</a>
</div>

View file

@ -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}
<span className="hidden s:inline">
&nbsp;
{`${count > 1 ? 'comments' : 'comment'}`}
{`${count > 1 ? `${locale('core.comment')}s` : locale('core.comment') }`}
</span>
</span>
</Button>
@ -46,7 +48,7 @@ export const CommentsCount = ({ count, articlePath }) => {
data-testid="add-a-comment"
>
<span className="inline s:hidden">0</span>
<span className="hidden s:inline">Add comment</span>
<span className="hidden s:inline">{locale('core.add_comment')}</span>
</Button>
);
}

View file

@ -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}
<span className="hidden s:inline">
&nbsp;
{`${totalReactions == 1 ? 'reaction' : 'reactions'}`}
{`${totalReactions == 1 ? locale('core.reaction') : `${locale('core.reaction')}s`}`}
</span>
</span>
</Button>

View file

@ -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 = () => (
<svg
@ -62,7 +63,7 @@ export class DropdownMenu extends Component {
</a>
) : (
<a href={reportUrl} className="crayons-link crayons-link--block">
Report Abuse
{locale('core.report_abuse')}
</a>
)}
</div>

View file

@ -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 = `<a href="${reportAbuseWrapper.dataset.path}" class="crayons-link crayons-link--block">Report abuse</a>`;
reportAbuseWrapper.innerHTML = `<a href="${reportAbuseWrapper.dataset.path}" class="crayons-link crayons-link--block">${locale('core.report_abuse')}</a>`;
}
// Initialize the "Copy link" functionality

View file

@ -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: '',

View file

@ -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);
}

View file

@ -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)",

View file

@ -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

View file

@ -54,6 +54,17 @@
multiple: false,
class: "selectpicker" %>
</div>
<div class="crayons-field">
<%= 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" %>
</div>
<div class="crayons-field">
<%= admin_config_label :primary_brand_color_hex, model: Settings::UserExperience %>
<%= admin_config_description Constants::Settings::UserExperience::DETAILS[:primary_brand_color_hex][:description] %>

View file

@ -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) %>">
<span class="fw-bold">Copy Post URL</span>
<span class="fw-bold"><%= t("core.copy_link") %></span>
<%= 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") %>
</button>
<div id="article-copy-link-announcer" aria-live="polite" class="crayons-notice crayons-notice--success my-2 p-1" aria-live="polite" hidden>Copied to Clipboard</div>
@ -89,7 +89,7 @@
<a href="#" class="dropdown-link-row crayons-link crayons-link--block">Share Post via...</a>
</template>
<a href="/report-abuse" class="crayons-link crayons-link--block">Report Abuse</a>
<a href="/report-abuse" class="crayons-link crayons-link--block"><%= t("core.report_abuse") %></a>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<% if articles && articles.any? %>
<section class="crayons-card crayons-card--secondary text-padding mb-4 print-hidden" id="bottom-content-read-next">
<h2 class="crayons-subtitle-1">Read next</h2>
<h2 class="crayons-subtitle-1"><%= t("core.read_next") %></h2>
<% articles.each do |article| %>
<a href="<%= article.path %>" data-preload-image="<%= cloud_cover_url(article.main_image) %>" class="crayons-link mt-6 block">
<div class="flex items-center">

View file

@ -2,7 +2,7 @@
<% if @comments_to_show_count && @article.comments_count > @comments_to_show_count %>
<div class="mb-4">
<a class="crayons-btn crayons-btn--secondary w-100" href="<%= @article.path %>/comments">
View full discussion (<%= @article.comments_count %> comments)
<%= t("core.view") %> <%= t("core.full") %> <%= t("core.discussion") %> (<%= @article.comments_count %> comments)
</a>
</div>
<% end %>

View file

@ -1,5 +1,5 @@
<nav class="fs-s align-center block" aria-label="Conduct controls">
<a href="/code-of-conduct" class="crayons-link crayons-link--secondary">Code of Conduct</a>
<span class="opacity-25 px-2" role="presentation">&bull;</span>
<a href="/report-abuse" class="crayons-link crayons-link--secondary">Report abuse</a>
<a href="/report-abuse" class="crayons-link crayons-link--secondary"><%= t("core.report_abuse") %></a>
</nav>

View file

@ -65,7 +65,7 @@
<button
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
data-info='{"id": <%= story.user_id %>, "className": "User", "style": "full", "name": "<%= story.cached_user.name %>"}'>
Follow
<%= t("core.follow") %>
</button>
</div>
<div
@ -120,19 +120,19 @@
<a href="<%= story.path %>" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left" data-reaction-count data-reactable-id="<%= story.id %>">
<%= inline_svg_tag("small-heart.svg", aria: true, width: 24, height: 24, class: "crayons-icon", title: "Reactions") %>
<%= story.public_reactions_count %>
<span class="hidden s:inline">&nbsp;<%= story.public_reactions_count == 1 ? "reaction" : "reactions" %></span>
<span class="hidden s:inline">&nbsp;<%= story.public_reactions_count == 1 ? t("core.reaction").downcase : t("core.reaction").downcase.pluralize %></span>
</a>
<% end %>
<% if story.comments_count > 0 %>
<a href="<%= story.path %>#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left">
<%= inline_svg_tag("small-comment.svg", aria: true, width: 24, height: 24, class: "crayons-icon", title: "Comments") %>
<%= story.comments_count %>
<span class="hidden s:inline">&nbsp;<%= story.comments_count == 1 ? "comment" : "comments" %></span>
<span class="hidden s:inline">&nbsp;<%= story.comments_count == 1 ? t("core.comment").downcase : t("core.comment").downcase.pluralize %></span>
</a>
<% else %>
<a href="<%= story.path %>#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left">
<%= inline_svg_tag("small-comment.svg", aria: true, width: 24, height: 24, class: "crayons-icon", title: "Comments") %>
<span class="hidden s:inline">Add&nbsp;Comment</span>
<span class="hidden s:inline">Add&nbsp;<%= t("core.comment") %></span>
</a>
<% end %>
</div>

View file

@ -10,7 +10,7 @@
<div class="crayons-card crayons-card--secondary">
<header class="crayons-card__header">
<h3 class="crayons-subtitle-2">
More from <a href="<%= @actor.path %>"><%= @actor.name %></a>
<%= t("core.more_from") %> <a href="<%= @actor.path %>"><%= @actor.name %></a>
</h3>
</header>
<div>
@ -30,7 +30,7 @@
<div class="crayons-card crayons-card--secondary">
<header class="crayons-card__header">
<h3 class="crayons-subtitle-2">
Trending on <a href="<%= app_url %>"><%= community_name %></a>
<%= t("core.trending_on") %> <a href="<%= app_url %>"><%= community_name %></a>
<%= inline_svg_tag("twemoji/fire.svg", aria: true, class: "crayons-icon crayons-icon--default", title: "Hot") %>
</h3>
</header>

View file

@ -44,10 +44,10 @@
<ul class="crayons-navigation crayons-navigation--horizontal">
<li>
<a data-text="Feed" href="<%= list_path %>/" class="crayons-navigation__item <%= "crayons-navigation__item--current" if %w[week month year infinity latest].exclude?(params[:timeframe]) %>"<%= %w[week month year infinity
latest].exclude?(params[:timeframe]) ? ' aria-current="page"'.html_safe : "" %>>Feed</a>
latest].exclude?(params[:timeframe]) ? ' aria-current="page"'.html_safe : "" %>><%= t("core.feed") %></a>
</li>
<li>
<a data-text="Latest" href="<%= list_path %>/latest" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("latest") %>"<%= timeframe_check("latest") ? ' aria-current="page"'.html_safe : "" %>>Latest</a>
<a data-text="Latest" href="<%= list_path %>/latest" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("latest") %>"<%= timeframe_check("latest") ? ' aria-current="page"'.html_safe : "" %>><%= t("core.latest") %></a>
</li>
<li>
<a data-text="Top" href="<%= list_path %>/top/week"
@ -60,16 +60,16 @@
<% if timeframe_check("week") || timeframe_check("month") || timeframe_check("year") || timeframe_check("infinity") %>
<ul class="crayons-navigation crayons-navigation--horizontal fs-base">
<li>
<a data-text="Week" href="<%= list_path %>/top/week" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("week") %>"<%= timeframe_check("week") ? ' aria-current="page"'.html_safe : "" %>>Week</a>
<a data-text="Week" href="<%= list_path %>/top/week" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("week") %>"<%= timeframe_check("week") ? ' aria-current="page"'.html_safe : "" %>><%= t("core.week") %></a>
</li>
<li>
<a data-text="Month" href="<%= list_path %>/top/month" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("month") %>"<%= timeframe_check("month") ? ' aria-current="page"'.html_safe : "" %>>Month</a>
<a data-text="Month" href="<%= list_path %>/top/month" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("month") %>"<%= timeframe_check("month") ? ' aria-current="page"'.html_safe : "" %>><%= t("core.month") %></a>
</li>
<li>
<a data-text="Year" href="<%= list_path %>/top/year" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("year") %>"<%= timeframe_check("year") ? ' aria-current="page"'.html_safe : "" %>>Year</a>
<a data-text="Year" href="<%= list_path %>/top/year" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("year") %>"<%= timeframe_check("year") ? ' aria-current="page"'.html_safe : "" %>><%= t("core.year") %></a>
</li>
<li>
<a data-text="Infinity" href="<%= list_path %>/top/infinity" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("infinity") %>"<%= timeframe_check("infinity") ? ' aria-current="page"'.html_safe : "" %>>Infinity</a>
<a data-text="Infinity" href="<%= list_path %>/top/infinity" class="crayons-navigation__item <%= "crayons-navigation__item--current" if timeframe_check("infinity") %>"<%= timeframe_check("infinity") ? ' aria-current="page"'.html_safe : "" %>><%= t("core.infinity") %></a>
</li>
</ul>
<% end %>

View file

@ -26,7 +26,7 @@
data-path="<%= commentable.path %>/comments/<%= comment.id_code_generated %>"
rel="nofollow">
<%= inline_svg_tag("small-comment.svg", aria: true, class: "crayons-icon reaction-icon not-reacted", title: "Comment button") %>
<span class="hidden m:inline-block">Reply</span>
<span class="hidden m:inline-block"><%= t("core.reply") %></span>
</a>
<% else %>
<span class="fs-s inline-flex items-center fs-italic color-base-50 pl-1">

View file

@ -27,7 +27,7 @@
</button>
<div id="comment-dropdown-<%= comment.id %>" class="crayons-dropdown right-1 s:right-0 s:left-auto fs-base dropdown">
<ul class="m-0">
<li><a href="<%= URL.comment(comment) %>" class="crayons-link crayons-link--block permalink-copybtn" aria-label="Copy link to <%= comment.user.name %>'s comment" data-no-instant>Copy link</a></li>
<li><a href="<%= URL.comment(comment) %>" class="crayons-link crayons-link--block permalink-copybtn" aria-label="Copy link to <%= comment.user.name %>'s comment" data-no-instant><%= t("core.copy_link") %></a></li>
<li class="comment-actions hidden" data-user-id="<%= comment.user_id %>" data-action="settings-button" data-path="<%= URL.comment(comment) %>/settings" aria-label="Go to <%= comment.user.name %>'s comment settings"></li>
<% action = comment.hidden_by_commentable_user ? "Unhide" : "Hide" %>
<li class="comment-actions hidden" data-action="hide-button" data-commentable-user-id="<%= commentable.user_id %>" data-user-id="<%= comment.user_id %>">

View file

@ -94,8 +94,7 @@
<%= @commentable.title %>
</h1>
<div class="flex">
<a class="crayons-btn crayons-btn--outlined mr-2" href="<%= @commentable.path %>">View post</a>
<a class="view-discussion crayons-btn crayons-btn--outlined mr-2" href="<%= @commentable.path %><%= user_signed_in? ? "/" : "#" %>comments">Full discussion</a>
<a class="crayons-btn crayons-btn--outlined mr-2" href="<%= @commentable.path %>"><%= t("core.view") %> <%= t("core.post").downcase %></a>
</div>
</header>
<% end %>

View file

@ -41,20 +41,4 @@
<% end %>
</div>
</div>
<footer class="registration__footer">
<p class="registration__footer-content">
Open Source
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44" width="24" height="24">
<path fill="#FFCC4D" d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18" /><path fill-rule="evenodd" clip-rule="evenodd" fill="#292F33" d="M1.24 11.018c.24.239 1.438.957 1.677 1.675.24.717.72 4.784 2.158 5.981 1.483 1.232 7.077.774 8.148.24 2.397-1.195 2.691-4.531 3.115-6.221.239-.957 1.677-.957 1.677-.957s1.438 0 1.678.956c.424 1.691.72 5.027 3.115 6.221 1.072.535 6.666.994 8.151-.238 1.436-1.197 1.915-5.264 2.155-5.982.238-.717 1.438-1.435 1.677-1.674.241-.239.241-1.196 0-1.436-.479-.478-6.134-.904-12.223-.239-1.215.133-1.677.478-4.554.478-2.875 0-3.339-.346-4.553-.478-6.085-.666-11.741-.24-12.221.238-.239.239-.239 1.197 0 1.436z" /><path fill="#664500" d="M27.335 23.629c-.178-.161-.444-.171-.635-.029-.039.029-3.922 2.9-8.7 2.9-4.766 0-8.662-2.871-8.7-2.9-.191-.142-.457-.13-.635.029-.177.16-.217.424-.094.628C8.7 24.472 11.788 29.5 18 29.5s9.301-5.028 9.429-5.243c.123-.205.084-.468-.094-.628z" />
</svg>
· Free Forever
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44" width="24" height="24">
<path fill="#DD2E44" d="M39.885 15.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C5.751 26.587 15.216 35.568 22 38.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z" />
</svg>
</p>
<p>
We strive for transparency and don't collect excess data.
</p>
</footer>
</section>

View file

@ -1,14 +1,14 @@
<nav class="mb-6" aria-label="Secondary sidebar nav">
<% if user_signed_in? %>
<header class="p-2 pr-0 flex items-center justify-between">
<h3 class="crayons-subtitle-3">My Tags</h3>
<h3 class="crayons-subtitle-3"><%= t("core.my_tags") %></h3>
<a id="tag-priority-link" href="/dashboard/following_tags" class="crayons-btn crayons-btn--icon crayons-btn--ghost-dimmed" aria-label="Customize tag priority" title="Customize tag priority">
<%= inline_svg_tag("cog.svg", class: "crayons-icon") %>
</a>
</header>
<div id="sidebar-nav-followed-tags" class="overflow-y-auto mb-2 sidebar-nav-followed-tags" style="max-height: 42vh;"></div>
<% else %>
<h3 class="crayons-subtitle-3 p-2">Popular Tags</h3>
<h3 class="crayons-subtitle-3 p-2"><%= t("core.popular_tags") %></h3>
<div id="sidebar-nav-default-tags" class="overflow-y-auto" style="max-height: 42vh">
<% Tag.where(supported: true).order(hotness_score: :desc).limit(30).pluck(:id, :name).each do |tag_array| %>
<div class="sidebar-nav-element" id="default-sidebar-element-<%= tag_array.second %>">
@ -18,7 +18,7 @@
<a class="follow-action-button sidebar-nav-link-follow crayons-btn crayons-btn--s"
href="#" id="sidebar-nav-link-follow-<%= tag_array.second %>"
data-info='{"id":<%= tag_array.first %>,"className":"Tag", "name": "<%= tag_array.second %>"}'>
Follow
<%= t("core.follow") %>
</a>
</div>
<% end %>

View file

@ -1,31 +1,24 @@
<div id="global-signup-modal" class="authentication-modal hidden">
<div class="authentication-modal__container">
<figure class="authentication-modal__image-container">
<img class="authentication-modal__image" src="<%= optimized_image_url(Settings::General.logo_png, width: 190) %>"
alt="<%= community_name %>" loading="lazy" />
</figure>
<div class="authentication-modal__content">
<p class="authentication-modal__description">
<% if Settings::Community.tagline.present? %>
<%= Settings::Community.tagline %>
<% else %>
Wed love for you to be a part of this community.
<% end %>
</p>
</div>
<div class="authentication-modal__actions">
<a href="<%= sign_up_path %>" class="crayons-btn" aria-label="Log in" data-no-instant>
Log in
</a>
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn crayons-btn--ghost-brand" aria-label="Create new account" data-no-instant>
Create new account
</a>
</div>
<footer class="authentication-modal__footer">
<p class="authentication-modal__footer-label">
We strive for transparency and don't collect excess data.
</p>
</footer>
<div class="authentication-modal__container">
<figure class="authentication-modal__image-container">
<img class="authentication-modal__image" src="<%= optimized_image_url(Settings::General.logo_png, width: 190) %>"
alt="<%= community_name %>" loading="lazy" />
</figure>
<div class="authentication-modal__content">
<p class="authentication-modal__description">
<% if Settings::Community.tagline.present? %>
<%= Settings::Community.tagline %>
<% end %>
</p>
</div>
<div class="authentication-modal__actions">
<a href="<%= sign_up_path %>" class="crayons-btn" aria-label="Log in" data-no-instant>
<%= t("core.log_in") %>
</a>
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn crayons-btn--ghost-brand" aria-label="Create new account" data-no-instant>
<%= t("core.create_account") %>
</a>
</div>
</div>
</div>

View file

@ -23,7 +23,7 @@
<form accept-charset="UTF-8" method="get" action="<%= search_path %>" role="search">
<div class="crayons-fields crayons-fields--horizontal">
<div class="crayons-field flex-1 relative">
<input class="crayons-header--search-input crayons-textfield js-search-input" type="text" id="nav-search" name="q" placeholder="Search..." autocomplete="off" />
<input class="crayons-header--search-input crayons-textfield js-search-input" type="text" id="nav-search" name="q" placeholder="<%= t("core.search") %>..." autocomplete="off" />
<button type="submit" aria-label="Search" class="crayons-btn crayons-btn--ghost crayons-btn--s crayons-btn--icon-rounded absolute right-2 bottom-0 top-0 m-1">
<%= inline_svg_tag("search.svg", aria_hidden: true, width: 24, height: 24, class: "crayons-icon", title: "Search") %>
</button>
@ -37,7 +37,7 @@
<%= inline_svg_tag("search.svg", aria: true, class: "crayons-icon", title: "Search") %>
</a>
<% if user_signed_in? %>
<a href="<%= new_path %>" class="crayons-btn hidden mr-2 whitespace-nowrap m:block ml-auto">Create Post</a>
<a href="<%= new_path %>" class="crayons-btn hidden mr-2 whitespace-nowrap m:block ml-auto"><%= t("core.create_post") %></a>
<span class="trusted-visible-block">
<a id="moderation-link" class="crayons-header__link crayons-btn crayons-btn--ghost crayons-btn--icon-rounded m:flex hidden" aria-label="Moderation" href="<%= mod_path %>">
@ -65,11 +65,11 @@
</div>
<% else %>
<a href="<%= sign_up_path %>" class="crayons-btn crayons-btn--ghost-brand mr-2 hidden m:block whitespace-nowrap ml-auto" data-no-instant>
Log in
<%= t("core.log_in") %>
</a>
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn mr-2 whitespace-nowrap fs-s s:fs-base" data-no-instant>
Create account
<%= t("core.create_account") %>
</a>
<% end %>
</div>

View file

@ -49,6 +49,7 @@
<body
data-user-status="<%= user_logged_in_status %>"
class="<%= Settings::UserExperience.default_font.tr("_", "-") %>-article-body default-header"
data-locale="<%= I18n.locale %>"
data-pusher-key="<%= ApplicationConfig["PUSHER_KEY"] %>"
data-app-domain="<%= ChatChannel.urlsafe_encoded_app_domain %>"
data-honeybadger-key="<%= ApplicationConfig["HONEYBADGER_JS_API_KEY"] %>"
@ -120,6 +121,7 @@
}
</script>
<% end %>
<div id="i18n-translations" data-translations="<%= { I18n.locale.to_sym => { core: I18n::JS.translations[I18n.locale.to_sym][:core] } }.to_json %>"></div>
</body>
</html>
<% end %>

View file

@ -1,5 +1,5 @@
<%= content_for :page_meta do %>
<% title "Listings" %>
<% title "#{t('core.listings')}" %>
<link rel="canonical" href="<%= app_url(request.path) %>" />
<meta name="description" content="<%= Settings::Community.community_description %>">
@ -30,7 +30,7 @@
<main id="main-content">
<div class="crayons-layout flex items-center justify-between p-2 l:p-4 l:pb-0">
<h1 class="crayons-title">Listings</h1>
<h1 class="crayons-title"><%= t("core.listings") %></h1>
<nav class="flex items-center" aria-label="Listings">
<ul class="list-none flex">

View file

@ -21,7 +21,7 @@
id="user-follow-butt"
class="crayons-btn whitespace-nowrap follow-action-button"
data-info='{"id":<%= @user.id %>,"className":"<%= @user.class.name %>", "name": "<%= @user.name %>"}'>
Follow
<%= t("core.follow") %>
</button>
</div>
</div>

View file

@ -1,21 +1,21 @@
<% title "Report abuse" %>
<% title t("core.report_abuse") %>
<%= content_for :page_meta do %>
<link rel="canonical" href="<%= app_url("/report-abuse") %>" />
<meta name="description" content="Report Abuse">
<meta name="description" content="<%= t("core.report_abuse") %>">
<%= meta_keywords_default %>
<meta property="og:type" content="article" />
<meta property="og:url" content="<%= app_url("/report-abuse") %>" />
<meta property="og:title" content="Report Abuse" />
<meta property="og:title" content="<%= t("core.report_abuse") %>" />
<meta property="og:site_name" content="<%= community_name %>" />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@<%= Settings::General.social_media_handles["twitter"] %>">
<meta name="twitter:title" content="Report Abuse">
<meta name="twitter:title" content="<%= t("core.report_abuse") %>">
<% end %>
<div class="crayons-layout crayons-layout--limited-l">
<div class="crayons-card text-styles text-padding">
<h1 class="fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight mb-4 mt-0">Report Abuse</h1>
<h1 class="fs-3xl s:fs-4xl l:fs-5xl fw-bold s:fw-heavy lh-tight mb-4 mt-0"><%= t("core.report_abuse") %></h1>
<p>
Thank you for reporting any abuse that violates our <a href="/code-of-conduct">code of conduct</a> or

View file

@ -5,12 +5,10 @@
<p class="color-base-70 mb-4">
<% if Settings::Community.tagline.present? %>
<%= Settings::Community.tagline %>
<% else %>
Log in to customize your experience and get involved.
<% end %>
</p>
<div>
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn w-100 mb-2" aria-label="Create new account">Create new account</a>
<a href="<%= sign_up_path %>" class="crayons-btn crayons-btn--ghost-brand w-100" aria-label="Log in">Log in</a>
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn w-100 mb-2" aria-label="Create new account"><%= t("core.create_account") %></a>
<a href="<%= sign_up_path %>" class="crayons-btn crayons-btn--ghost-brand w-100" aria-label="Log in"><%= t("core.log_in") %></a>
</div>
</div>

View file

@ -1,6 +1,6 @@
<a href="<%= sign_up_path(state: "new-user") %>" class="crayons-btn" aria-label="Create new account" data-no-instant>
Create new account
<%= t("core.create_account") %>
</a>
<a href="<%= sign_up_path %>" class="crayons-btn crayons-btn--ghost-brand" aria-label="Log in">
Log in
<%= t("core.log_in") %>
</a>

View file

@ -12,9 +12,9 @@
<% if params[:timeframe].blank? && @listings.any? %>
<section class="crayons-card crayons-card--secondary">
<header class="crayons-card__header">
<h3 class="crayons-subtitle-2">Listings</h3>
<h3 class="crayons-subtitle-2"><%= t("core.listings") %></h3>
<div class="crayons-card__actions">
<a href="/listings" class="crayons-link--branded fw-medium fs-s">See all</a>
<a href="/listings" class="crayons-link--branded fw-medium fs-s"><%= t("core.see_all") %></a>
</div>
</header>

View file

@ -25,7 +25,7 @@
id="user-follow-butt"
class="crayons-btn follow-action-button"
data-info='{"id":<%= @tag_model.id %>,"className":"Tag", "name": "<%= @tag_model.pretty_name || @tag %>"}'>
Follow
<%= t("core.follow") %>
</button>
<% end %>
</div>

View file

@ -49,7 +49,7 @@
<button
class="crayons-btn crayons-btn--secondary follow-action-button"
data-info='{"id":<%= tag.id %>,"className":"Tag", "followStyle":"secondary", "name": "<%= tag.name %>"}'>
Follow
<%= t("core.follow") %>
</button>
</div>
<% end %>

View file

@ -35,7 +35,7 @@
</a>
<% end %>
<% end %>
<button id="user-follow-butt" class="crayons-btn whitespace-nowrap follow-action-button follow-user" data-info='{"id":<%= @user.id %>,"className":"<%= @user.class.name %>", "name": "<%= @user.name %>"}'>Follow</button>
<button id="user-follow-butt" class="crayons-btn whitespace-nowrap follow-action-button follow-user" data-info='{"id":<%= @user.id %>,"className":"<%= @user.class.name %>", "name": "<%= @user.name %>"}'><%= t("core.follow") %></button>
<div class="profile-dropdown ml-2 s:relative hidden" data-username="<%= @user.username %>">
<button id="user-profile-dropdown" aria-expanded="false" aria-controls="user-profile-dropdownmenu" aria-haspopup="true" class="crayons-btn crayons-btn--ghost-dimmed crayons-btn--icon">
<%= inline_svg_tag("overflow-horizontal.svg", class: "dropdown-icon crayons-icon", aria: true, title: "User actions") %>

View file

@ -55,6 +55,8 @@ module PracticalDeveloper
config.middleware.use Rack::Deflater
config.i18n.fallbacks = [:en]
# Globally handle Pundit::NotAuthorizedError by serving 404
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :not_found

133
config/i18n-tasks.yml Normal file
View file

@ -0,0 +1,133 @@
# i18n-tasks finds and manages missing and unused translations: https://github.com/glebm/i18n-tasks
# The "main" locale.
base_locale: en
## All available locales are inferred from the data by default. Alternatively, specify them explicitly:
# locales: [es, fr]
## Reporting locale, default: en. Available: en, ru.
# internal_locale: en
# Read and write translations.
data:
## Translations are read from the file system. Supported format: YAML, JSON.
## Provide a custom adapter:
# adapter: I18n::Tasks::Data::FileSystem
# Locale files or `File.find` patterns where translations are read from:
read:
## Default:
# - config/locales/%{locale}.yml
## More files:
# - config/locales/**/*.%{locale}.yml
# Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom:
# `i18n-tasks normalize -p` will force move the keys according to these rules
write:
## For example, write devise and simple form keys to their respective files:
# - ['{devise, simple_form}.*', 'config/locales/\1.%{locale}.yml']
## Catch-all default:
# - config/locales/%{locale}.yml
# External locale data (e.g. gems).
# This data is not considered unused and is never written to.
external:
## Example (replace %#= with %=):
# - "<%#= %x[bundle info vagrant --path].chomp %>/templates/locales/%{locale}.yml"
## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, or a custom class.
# router: conservative_router
yaml:
write:
# do not wrap lines at 80 characters
line_width: -1
## Pretty-print JSON:
# json:
# write:
# indent: ' '
# space: ' '
# object_nl: "\n"
# array_nl: "\n"
# Find translate calls
search:
## Paths or `File.find` patterns to search in:
# paths:
# - app/
## Root directories for relative keys resolution.
# relative_roots:
# - app/controllers
# - app/helpers
# - app/mailers
# - app/presenters
# - app/views
## Files or `File.fnmatch` patterns to exclude from search. Some files are always excluded regardless of this setting:
## %w(*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less *.yml *.json)
exclude:
- app/assets/images
- app/assets/fonts
- app/assets/videos
## Alternatively, the only files or `File.fnmatch patterns` to search in `paths`:
## If specified, this settings takes priority over `exclude`, but `exclude` still applies.
# only: ["*.rb", "*.html.slim"]
## If `strict` is `false`, guess usages such as t("categories.#{category}.title"). The default is `true`.
# strict: true
## Multiple scanners can be used. Their results are merged.
## The options specified above are passed down to each scanner. Per-scanner options can be specified as well.
## See this example of a custom scanner: https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example
## Translation Services
# translation:
# # Google Translate
# # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate
# google_translate_api_key: "AbC-dEf5"
# # DeepL Pro Translate
# # Get an API key and subscription at https://www.deepl.com/pro to use DeepL Pro
# deepl_api_key: "48E92789-57A3-466A-9959-1A1A1A1A1A1A"
## Do not consider these keys missing:
# ignore_missing:
# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'
# - '{devise,simple_form}.*'
## Consider these keys used:
# ignore_unused:
# - 'activerecord.attributes.*'
# - '{devise,kaminari,will_paginate}.*'
# - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*'
# - 'simple_form.{error_notification,required}.:'
## Exclude these keys from the `i18n-tasks eq-base' report:
# ignore_eq_base:
# all:
# - common.ok
# fr,es:
# - common.brand
## Exclude these keys from the `i18n-tasks check-consistent-interpolations` report:
# ignore_inconsistent_interpolations:
# - 'activerecord.attributes.*'
## Ignore these keys completely:
# ignore:
# - kaminari.*
## Sometimes, it isn't possible for i18n-tasks to match the key correctly,
## e.g. in case of a relative key defined in a helper method.
## In these cases you can use the built-in PatternMapper to map patterns to keys, e.g.:
#
# <%# I18n::Tasks.add_scanner 'I18n::Tasks::Scanners::PatternMapper',
# only: %w(*.html.haml *.html.slim),
# patterns: [['= title\b', '.page_title']] %>
#
# The PatternMapper can also match key literals via a special %{key} interpolation, e.g.:
#
# <%# I18n::Tasks.add_scanner 'I18n::Tasks::Scanners::PatternMapper',
# patterns: [['\bSpree\.t[( ]\s*%{key}', 'spree.%{key}']] %>

View file

@ -30,20 +30,72 @@
# available at https://guides.rubyonrails.org/i18n.html.
en:
hello: "Hello world"
errors: &errors
core:
add_comment: "Add comment"
comment: "Comment"
copy_link: "Copy link"
create_account: "Create account"
create_post: "Create Post"
discussion: "Discussion"
edit_profile: "Edit profile"
feed: "Feed"
follow: "Follow"
follow_back: "Follow back"
following: "Following"
full: "Full"
infinity: "Infinity"
latest: "Latest"
like: "Like"
listings: "Listings"
log_in: "Log in"
month: "Month"
more_from: "More from"
my_tags: "My Tags"
post: "Post"
reaction: "Reaction"
read_next: "Read next"
reply: "Reply"
report_abuse: "Report abuse"
search: "Search"
see_all: "See all"
trending_on: "Trending on"
view: "View"
week: "Week"
year: "Year"
errors:
messages:
bad_uri: is an invalid url
bad_protocol: must start with %{protocols}
activemodel:
errors:
<<: *errors
activerecord:
errors:
<<: *errors
administrate:
actions:
new: 'New'
accepted: must be accepted
bad_uri: is an invalid url
bad_protocol: must start with %{protocols}
blank: can't be blank
confirmation: doesn't match %{attribute}
empty: can't be empty
equal_to: must be equal to %{count}
even: must be even
exclusion: is reserved
greater_than: must be greater than %{count}
greater_than_or_equal_to: must be greater than or equal to %{count}
inclusion: is not included in the list
invalid: is invalid
less_than: must be less than %{count}
less_than_or_equal_to: must be less than or equal to %{count}
model_invalid: 'Validation failed: %{errors}'
not_a_number: is not a number
not_an_integer: must be an integer
odd: must be odd
other_than: must be other than %{count}
present: must be blank
required: must exist
taken: has already been taken
too_long:
one: is too long (maximum is 1 character)
other: is too long (maximum is %{count} characters)
too_short:
one: is too short (minimum is 1 character)
other: is too short (minimum is %{count} characters)
wrong_length:
one: is the wrong length (should be 1 character)
other: is the wrong length (should be %{count} characters)
podcasts:
statuses:
unreachable: Podcast's feed_url is not reachable

89
config/locales/fr.yml Normal file
View file

@ -0,0 +1,89 @@
---
fr:
core:
add_comment: Ajouter un commentaire
comment: Commentaire
copy_link: Copier le lien
create_account: Créer un profil
create_post: Créer Publication
discussion: Discussion
edit_profile: Editer le profil
feed: Fil
follow: Suivre
follow_back: Suivre aussi
following: Abonné
full: Complet
infinity: Infini
latest: Récent
like: Aime
listings: Annonces
log_in: Entrer
month: Mois
more_from: Plus de
my_tags: Mes Sujets
popular_tags: Sujets Populaire
post: Publication
reaction: Réaction
read_next: Lire ensuite
reply: Répondre
report_abuse: Signaler un abus
search: Recherche
see_all: Voir tout
trending_on: Populaire sur
view: Vue
week: Semain
year: Anée
date:
abbr_month_names:
- Janv
- Fevr
- Mars
- Avril
- Mai
- Juin
- Juil
- Aout
- Sept
- Oct
- Nov
- Dec
errors:
messages:
accepted: doit être accepté
bad_protocol: doit commencer par
bad_uri: est une URL invalide
blank: ne peut pas être vide
confirmation: ne correspond pas à
empty: ne peut pas être vide
equal_to: doit être égal à
even: doit être pair
exclusion: est réservé
greater_than: doit être supérieur à
greater_than_or_equal_to: doit être supérieur ou égal à
inclusion: n'est pas inclus dans la liste
invalid: est invalide
less_than: doit être inférieur à
less_than_or_equal_to: doit être inférieur ou égal à
model_invalid: 'Échec de la validation :'
not_a_number: n'est pas un nombre
not_an_integer: Doit être un entier
odd: doit être étrange
other_than: doit être différent de
present: doit être vide
required: doit exister
taken: a déjà été pris
too_long:
one: est trop long (le maximum est de 1 caractère)
other: est trop long (le maximum est de caractères)
too_short:
one: est trop court (au moins 1 caractère)
other: est trop court (le minimum est de caractères)
wrong_length:
one: est la mauvaise longueur (devrait être 1 caractère)
other: est la mauvaise longueur (devrait être %{count} caractères)
podcasts:
statuses:
ssl_failed: Échec de la vérification du certificat SSL lors de la récupération de feed_url du podcast
unparsable: Le rss du podcast n'a pas pu être analysé
unplayable: Ce podcast peut ne pas être lisible dans le navigateur
unreachable: Le feed_url du podcast n'est pas accessible

View file

@ -19,13 +19,13 @@ describe('Post sidebar actions', () => {
it('should open and close the share menu for the post', () => {
// Check dropdown is closed by asserting the first option isn't visible
cy.findByRole('button', { name: /^Copy Post URL$/i }).should('not.exist');
cy.findByRole('button', { name: /^Copy link$/i }).should('not.exist');
cy.findByRole('button', { name: /^Share post options$/i }).as(
'dropdownButton',
);
cy.get('@dropdownButton').click();
cy.findByRole('button', { name: /^Copy Post URL$/i }).as(
cy.findByRole('button', { name: /^Copy link$/i }).as(
'copyPostUrlButton',
);
cy.get('@copyPostUrlButton').should('have.focus');
@ -51,7 +51,7 @@ describe('Post sidebar actions', () => {
'dropdownButton',
);
cy.get('@dropdownButton').click();
cy.findByRole('button', { name: /^Copy Post URL$/i }).as(
cy.findByRole('button', { name: /^Copy link$/i }).as(
'copyPostUrlButton',
);
cy.get('@copyPostUrlButton').should('have.focus');
@ -65,7 +65,7 @@ describe('Post sidebar actions', () => {
'dropdownButton',
);
cy.get('@dropdownButton').click();
cy.findByRole('button', { name: /^Copy Post URL$/i }).as(
cy.findByRole('button', { name: /^Copy link$/i }).as(
'copyPostUrlButton',
);
cy.findByText('Copied to Clipboard').should('not.be.visible');

View file

@ -156,6 +156,7 @@
"focus-visible": "^5.2.0",
"he": "^1.2.0",
"honeybadger-js": "2.3.0",
"i18n-js": "^3.8.0",
"intersection-observer": "^0.12.0",
"linkstate": "^2.0.1",
"lodash.debounce": "4.0.8",

View file

@ -19,11 +19,6 @@ RSpec.describe "Comments", type: :request do
expect(response.body).to include(comment.processed_html)
end
it "displays full discussion text" do
get comment.path
expect(response.body).to include("Full discussion")
end
it "renders user payment pointer if set" do
article.user.update_column(:payment_pointer, "test-pointer-for-comments")
get "#{article.path}/comments"

View file

@ -280,6 +280,38 @@ RSpec.describe "StoriesIndex", type: :request do
expect(response.body).not_to include('<a href="https://campaign-lander.com"')
end
end
context "with default_locale configured to fr" do
before do
allow(Settings::UserExperience).to receive(:default_locale).and_return("fr")
get "/"
end
it "names proper locale" do
expect(I18n.locale).to eq(:fr)
end
it "has proper locale content on page" do
expect(response.body).to include("Recherche")
end
end
end
describe "GET locale index" do
it "names proper locale" do
get "/locale/fr"
expect(I18n.locale).to eq(:fr)
end
it "has proper locale content on page" do
get "/locale/fr"
expect(response.body).to include("Recherche")
end
it "uses fallback locale if invalid locale passed" do
get "/locale/fake"
expect(I18n.locale).to eq(:en)
end
end
describe "GET podcast index" do

View file

@ -156,7 +156,7 @@ RSpec.describe "Tags", type: :request, proper_status: true do
expect(response_tag["name"]).to eq(tag.name)
expect(response_tag["bg_color_hex"]).to eq(tag.bg_color_hex)
expect(response_tag["text_color_hex"]).to eq(tag.text_color_hex)
expect(response_tag["following"]).to be_nil
expect(response_tag[I18n.t("core.following")]).to be_nil
end
it "returns only suggested tags" do

View file

@ -23,13 +23,13 @@ RSpec.describe "User visits articles by tag", type: :system do
end
it "shows the follow button", js: true do
within("header.tag-header") { expect(page).to have_button("Follow") }
within("header.tag-header") { expect(page).to have_button(I18n.t("core.follow")) }
end
# Regression test for https://github.com/forem/forem/pull/12724
it "does not display a comment count of 0", js: true do
expect(page).to have_text("Add Comment")
expect(page).not_to have_text("0 comments")
expect(page).not_to have_text("0 #{I18n.t('core.comment').downcase}s")
end
it "shows correct articles count" do
@ -65,7 +65,7 @@ RSpec.describe "User visits articles by tag", type: :system do
it "shows the following button", js: true do
wait_for_javascript
within("header.tag-header") { expect(page).to have_button("Following") }
within("header.tag-header") { expect(page).to have_button(I18n.t("core.following")) }
end
it "shows time buttons" do

View file

@ -17,8 +17,8 @@ RSpec.describe "Followers Dashboard", type: :system, js: true do
user.follow(followed_user)
visit "/dashboard/user_followers"
expect(JSON.parse(find_button("Following")["data-info"])["id"]).to eq(followed_user.id)
expect(JSON.parse(find_button("Follow back")["data-info"])["id"]).to eq(following_user.id)
expect(JSON.parse(find_button(I18n.t("core.following"))["data-info"])["id"]).to eq(followed_user.id)
expect(JSON.parse(find_button(I18n.t("core.follow_back"))["data-info"])["id"]).to eq(following_user.id)
end
end
end

View file

@ -22,10 +22,10 @@ RSpec.describe "User visits a homepage", type: :system do
# Regression test for https://github.com/forem/forem/pull/12724
it "does not display a comment count of 0", js: true do
expect(page).to have_text("Add Comment")
expect(page).not_to have_text("0 comments")
expect(page).not_to have_text("0 #{I18n.t('core.comment').downcase}s")
article.update_column(:comments_count, 50)
visit "/"
expect(page).to have_text(/50\s*comments/)
expect(page).to have_text(/50\s*#{I18n.t("core.comment").downcase}s/)
end
it "shows the main article readable date and time", js: true do

View file

@ -9,8 +9,9 @@ RSpec.describe "User visits a homepage", type: :system do
it "shows the sign-in block" do
visit "/"
within "#sidebar-wrapper-left" do
expect(page).to have_text("Log in")
expect(page).to have_text("Create new account")
p I18n.t("create_account")
expect(page).to have_text(I18n.t("core.log_in"))
expect(page).to have_text(I18n.t("core.create_account"))
end
end

View file

@ -50,8 +50,8 @@ RSpec.describe "Notifications page", type: :system, js: true do
Notification.send_new_follower_notification_without_delay(follow, is_read: true)
visit "/notifications"
expect(page).to have_css("div.spec-notification")
click_button("Follow back")
expect(page).to have_text("Following")
click_button(I18n.t("core.follow_back"))
expect(page).to have_text(I18n.t("core.following"))
end
context "when user is trusted" do

View file

@ -15,7 +15,7 @@ RSpec.describe "Organization index", type: :system do
it "shows the header", js: true do
within("h1") { expect(page).to have_content(organization.name) }
within("div.profile-header__actions") do
expect(page).to have_button("Follow")
expect(page).to have_button(I18n.t("core.follow"))
end
end
@ -55,7 +55,7 @@ RSpec.describe "Organization index", type: :system do
visit "/#{organization.slug}"
within(".profile-header__actions") do
expect(page).to have_button("Following")
expect(page).to have_button(I18n.t("core.following"))
end
end
end

View file

@ -28,6 +28,6 @@ RSpec.describe "Display users search spec", type: :system, js: true do
expect(page).to have_content(found_user.name)
expect(find("span.crayons-story__flare-tag").text).to have_content("person")
expect(find(:xpath, "//img[@alt='#{found_user.username} profile']")["src"]).to include(found_user.profile_image_90)
expect(JSON.parse(find_button("Follow")["data-info"])["id"]).to eq(found_user.id)
expect(JSON.parse(find_button(I18n.t("core.follow"))["data-info"])["id"]).to eq(found_user.id)
end
end

View file

@ -16,9 +16,9 @@ RSpec.describe "User searches users", type: :system do
it "shows the correct follow buttons", js: true do
visit "/search?q=&filters=class_name:User"
expect(JSON.parse(find_button("Edit profile")["data-info"])["id"]).to eq(current_user.id)
expect(JSON.parse(find_button("Following")["data-info"])["id"]).to eq(followed_user.id)
expect(JSON.parse(find_button("Follow")["data-info"])["id"]).to eq(not_followed_user.id)
expect(JSON.parse(find_button("Follow back")["data-info"])["id"]).to eq(follow_back_user.id)
expect(JSON.parse(find_button(I18n.t("core.edit_profile"))["data-info"])["id"]).to eq(current_user.id)
expect(JSON.parse(find_button(I18n.t("core.following"))["data-info"])["id"]).to eq(followed_user.id)
expect(JSON.parse(find_button(I18n.t("core.follow"))["data-info"])["id"]).to eq(not_followed_user.id)
expect(JSON.parse(find_button(I18n.t("core.follow_back"))["data-info"])["id"]).to eq(follow_back_user.id)
end
end

View file

@ -29,7 +29,7 @@ RSpec.describe "User index", type: :system do
def shows_header
within("h1") { expect(page).to have_content(user.name) }
within(".profile-header__actions") do
expect(page).to have_button("Follow")
expect(page).to have_button(I18n.t("core.follow"))
end
end
@ -99,7 +99,7 @@ RSpec.describe "User index", type: :system do
def shows_header
within("h1") { expect(page).to have_content(user.name) }
within(".profile-header__actions") do
expect(page).to have_button("Edit profile")
expect(page).to have_button(I18n.t("core.edit_profile"))
end
end

View file

@ -8,16 +8,16 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
s.require_paths = ["lib".freeze]
s.authors = ["Tom Cocca".freeze]
s.date = "2021-07-08"
s.date = "2021-04-09"
s.description = "acts_as_follower is a Rubygem to allow any model to follow any other model. This is accomplished through a double polymorphic relationship on the Follow model. There is also built in support for blocking/un-blocking follow records. Main uses would be for Users to follow other Users or for Users to follow Books, etc\u2026 (Basically, to develop the type of follow system that GitHub has)".freeze
s.email = ["tom dot cocca at gmail dot com".freeze]
s.files = [".gitignore".freeze, ".travis.yml".freeze, "Gemfile".freeze, "MIT-LICENSE".freeze, "README.rdoc".freeze, "Rakefile".freeze, "acts_as_follower.gemspec".freeze, "init.rb".freeze, "lib/acts_as_follower.rb".freeze, "lib/acts_as_follower/follow_scopes.rb".freeze, "lib/acts_as_follower/followable.rb".freeze, "lib/acts_as_follower/follower.rb".freeze, "lib/acts_as_follower/follower_lib.rb".freeze, "lib/acts_as_follower/railtie.rb".freeze, "lib/acts_as_follower/version.rb".freeze, "lib/generators/USAGE".freeze, "lib/generators/acts_as_follower_generator.rb".freeze, "lib/generators/templates/migration.rb".freeze, "lib/generators/templates/model.rb".freeze, "test/README".freeze, "test/acts_as_followable_test.rb".freeze, "test/acts_as_follower_test.rb".freeze, "test/dummy30/Gemfile".freeze, "test/dummy30/Rakefile".freeze, "test/dummy30/app/models/application_record.rb".freeze, "test/dummy30/app/models/band.rb".freeze, "test/dummy30/app/models/band/punk.rb".freeze, "test/dummy30/app/models/band/punk/pop_punk.rb".freeze, "test/dummy30/app/models/custom_record.rb".freeze, "test/dummy30/app/models/some.rb".freeze, "test/dummy30/app/models/user.rb".freeze, "test/dummy30/config.ru".freeze, "test/dummy30/config/application.rb".freeze, "test/dummy30/config/boot.rb".freeze, "test/dummy30/config/database.yml".freeze, "test/dummy30/config/environment.rb".freeze, "test/dummy30/config/environments/development.rb".freeze, "test/dummy30/config/environments/test.rb".freeze, "test/dummy30/config/initializers/backtrace_silencers.rb".freeze, "test/dummy30/config/initializers/inflections.rb".freeze, "test/dummy30/config/initializers/secret_token.rb".freeze, "test/dummy30/config/initializers/session_store.rb".freeze, "test/dummy30/config/locales/en.yml".freeze, "test/dummy30/config/routes.rb".freeze, "test/factories/bands.rb".freeze, "test/factories/somes.rb".freeze, "test/factories/users.rb".freeze, "test/follow_test.rb".freeze, "test/schema.rb".freeze, "test/test_helper.rb".freeze]
s.homepage = "https://github.com/tcocca/acts_as_follower".freeze
s.licenses = ["MIT".freeze]
s.rubygems_version = "3.2.22".freeze
s.rubygems_version = "3.1.4".freeze
s.summary = "A Rubygem to add Follow functionality for ActiveRecord models".freeze
s.installed_by_version = "3.2.22" if s.respond_to? :installed_by_version
s.installed_by_version = "3.1.4" if s.respond_to? :installed_by_version
if s.respond_to? :specification_version then
s.specification_version = 4

BIN
vendor/cache/easy_translate-0.5.1.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/highline-2.0.3.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/i18n-js-3.9.0.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/i18n-tasks-0.9.34.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/rails-i18n-6.0.0.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/terminal-table-3.0.1.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/thread-0.2.2.gem vendored Normal file

Binary file not shown.

View file

@ -8046,6 +8046,11 @@ husky@^7.0.2:
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.2.tgz#21900da0f30199acca43a46c043c4ad84ae88dff"
integrity sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==
i18n-js@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/i18n-js/-/i18n-js-3.8.0.tgz#b8fd6b12e1d88cb71f9806c29bca7c31c012e504"
integrity sha512-hDsGgPuvw/2P+lXSbOafAwspK8Ste8YrwuuUg17W3wEcO1JkQxBlPgsN1t2+852nTnz4YSYTjZc/1nAA2PC/nw==
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"