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:
parent
01e05e4ce1
commit
036d75aa6e
74 changed files with 1634 additions and 159 deletions
3
Gemfile
3
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
|
||||
|
|
|
|||
25
Gemfile.lock
25
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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? &&
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ module ApplicationHelper
|
|||
followable_name = followable.name
|
||||
|
||||
tag.button(
|
||||
"Follow",
|
||||
I18n.t("core.follow"),
|
||||
name: :button,
|
||||
type: :button,
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
" like"
|
||||
" #{I18n.t('core.like').downcase}"
|
||||
else
|
||||
" likes"
|
||||
" #{I18n.t('core.like').downcase}s"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
{`${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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
{`${totalReactions == 1 ? 'reaction' : 'reactions'}`}
|
||||
{`${totalReactions == 1 ? locale('core.reaction') : `${locale('core.reaction')}s`}`}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
10
app/javascript/utilities/locale.js
Normal file
10
app/javascript/utilities/locale.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] %>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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">•</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>
|
||||
|
|
|
|||
|
|
@ -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"> <%= story.public_reactions_count == 1 ? "reaction" : "reactions" %></span>
|
||||
<span class="hidden s:inline"> <%= 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"> <%= story.comments_count == 1 ? "comment" : "comments" %></span>
|
||||
<span class="hidden s:inline"> <%= 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 Comment</span>
|
||||
<span class="hidden s:inline">Add <%= t("core.comment") %></span>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 %>">
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
We’d 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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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") %>
|
||||
|
|
|
|||
|
|
@ -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
133
config/i18n-tasks.yml
Normal 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}']] %>
|
||||
|
|
@ -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
89
config/locales/fr.yml
Normal 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
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
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
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
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
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
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
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
BIN
vendor/cache/thread-0.2.2.gem
vendored
Normal file
Binary file not shown.
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue