app/models i18n (#16124)

* app/models etc i18n

* delete ja.yml

* fix for PR review

* fix for spec

* delete ja.yml

* fix for spec updated
This commit is contained in:
yheuhtozr 2022-02-04 03:41:42 +09:00 committed by GitHub
parent c4778d832e
commit e45536af37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 418 additions and 122 deletions

View file

@ -14,6 +14,6 @@ class ApiSecret < ApplicationRecord
def user_api_secret_count
return if user && user.api_secrets.count < 20
errors.add(:user, "API secret limit of 20 per user has been reached")
errors.add(:user, I18n.t("models.api_secret.api_limit_reached"))
end
end

View file

@ -60,7 +60,8 @@ class ApplicationRecord < ActiveRecord::Base
return superclass.decorator_class(called_on) if superclass.respond_to?(:decorator_class)
raise UninferrableDecoratorError, "Could not infer a decorator for #{called_on.class.name}."
raise UninferrableDecoratorError,
I18n.t("models.application_record.uninferrable", class: called_on.class.name)
end
def self.statement_timeout

View file

@ -29,10 +29,12 @@ class Article < ApplicationRecord
# The date that we began limiting the number of user mentions in an article.
MAX_USER_MENTION_LIVE_AT = Time.utc(2021, 4, 7).freeze
UNIQUE_URL_ERROR = "has already been taken. " \
"Email #{ForemInstance.email} for further details.".freeze
PROHIBITED_UNICODE_CHARACTERS_REGEX = /[\u202a-\u202e]/ # BIDI embedding controls
def self.unique_url_error
I18n.t("models.article.unique_url", email: ForemInstance.email)
end
has_one :discussion_lock, dependent: :delete
has_many :mentions, as: :mentionable, inverse_of: :mentionable, dependent: :delete_all
@ -58,17 +60,20 @@ class Article < ApplicationRecord
inverse_of: :commentable,
class_name: "Comment"
validates :body_markdown, bytesize: { maximum: 800.kilobytes, too_long: "is too long." }
validates :body_markdown, bytesize: {
maximum: 800.kilobytes,
too_long: proc { I18n.t("models.article.is_too_long") }
}
validates :body_markdown, length: { minimum: 0, allow_nil: false }
validates :body_markdown, uniqueness: { scope: %i[user_id title] }
validates :cached_tag_list, length: { maximum: 126 }
validates :canonical_url,
uniqueness: { allow_nil: true, scope: :published, message: UNIQUE_URL_ERROR },
uniqueness: { allow_nil: true, scope: :published, message: unique_url_error },
if: :published?
validates :canonical_url, url: { allow_blank: true, no_local: true, schemes: %w[https http] }
validates :comments_count, presence: true
validates :feed_source_url,
uniqueness: { allow_nil: true, scope: :published, message: UNIQUE_URL_ERROR },
uniqueness: { allow_nil: true, scope: :published, message: unique_url_error },
if: :published?
validates :feed_source_url, url: { allow_blank: true, no_local: true, schemes: %w[https http] }
validates :main_image, url: { allow_blank: true, schemes: %w[https http] }
@ -379,7 +384,7 @@ class Article < ApplicationRecord
.tr("\n", " ")
.strip
else
"A post by #{user.name}"
I18n.t("models.article.a_post_by", user_name: user.name)
end
end
@ -434,18 +439,20 @@ class Article < ApplicationRecord
return unless edited?
if edited_at.year == Time.current.year
edited_at.strftime("%b %e")
I18n.l(edited_at, format: :short)
else
edited_at.strftime("%b %e '%y")
I18n.l(edited_at, format: :short_with_yy)
end
end
def readable_publish_date
relevant_date = displayable_published_at
if relevant_date && relevant_date.year == Time.current.year
relevant_date&.strftime("%b %-e")
else
relevant_date&.strftime("%b %-e '%y")
return unless relevant_date
if relevant_date.year == Time.current.year
I18n.l(relevant_date, format: :short)
elsif relevant_date
I18n.l(relevant_date, format: :short_with_yy)
end
end
@ -606,7 +613,7 @@ class Article < ApplicationRecord
end
def update_notifications
Notification.update_notifications(self, "Published")
Notification.update_notifications(self, I18n.t("models.article.published"))
end
def update_notification_subscriptions
@ -664,7 +671,7 @@ class Article < ApplicationRecord
add_tag_adjustments_to_tag_list
# check there are not too many tags
return errors.add(:tag_list, "exceed the maximum of 4 tags") if tag_list.size > 4
return errors.add(:tag_list, I18n.t("models.article.too_many_tags")) if tag_list.size > 4
# check tags names aren't too long and don't contain non alphabet characters
tag_list.each do |tag|
@ -691,48 +698,48 @@ class Article < ApplicationRecord
def validate_video
if published && video_state == "PROGRESSING"
return errors.add(:published,
"cannot be set to true if video is still processing")
I18n.t("models.article.video_processing"))
end
return unless video.present? && user.created_at > 2.weeks.ago
errors.add(:video, "cannot be added by member without permission")
errors.add(:video, I18n.t("models.article.video_unpermitted"))
end
def validate_collection_permission
return unless collection && collection.user_id != user_id
errors.add(:collection_id, "must be one you have permission to post to")
errors.add(:collection_id, I18n.t("models.article.series_unpermitted"))
end
def validate_co_authors
return if co_author_ids.exclude?(user_id)
errors.add(:co_author_ids, "must not be the same user as the author")
errors.add(:co_author_ids, I18n.t("models.article.same_author"))
end
def validate_co_authors_must_not_be_the_same
return if co_author_ids.uniq.count == co_author_ids.count
errors.add(:base, "co-author IDs must be unique")
errors.add(:base, I18n.t("models.article.unique_coauthor"))
end
def validate_co_authors_exist
return if User.where(id: co_author_ids).count == co_author_ids.count
errors.add(:co_author_ids, "must be valid user IDs")
errors.add(:co_author_ids, I18n.t("models.article.invalid_coauthor"))
end
def past_or_present_date
return unless published_at && published_at > Time.current
errors.add(:date_time, "must be entered in DD/MM/YYYY format with current or past date")
errors.add(:date_time, I18n.t("models.article.invalid_date"))
end
def canonical_url_must_not_have_spaces
return unless canonical_url.to_s.match?(/[[:space:]]/)
errors.add(:canonical_url, "must not have spaces")
errors.add(:canonical_url, I18n.t("models.article.must_not_have_spaces"))
end
def user_mentions_in_markdown
@ -742,7 +749,8 @@ class Article < ApplicationRecord
mentions_count = Nokogiri::HTML(processed_html).css(".mentioned-user").size
return if mentions_count <= Settings::RateLimit.mention_creation
errors.add(:base, "You cannot mention more than #{Settings::RateLimit.mention_creation} users in a post!")
errors.add(:base,
I18n.t("models.article.mention_too_many", count: Settings::RateLimit.mention_creation))
end
def create_slug

View file

@ -32,7 +32,7 @@ class Broadcast < ApplicationRecord
type_of == "Announcement" &&
[nil, id].exclude?(first_broadcast.pick(:id))
errors.add(:base, "You can only have one active announcement broadcast")
errors.add(:base, I18n.t("models.broadcast.single_active"))
end
def update_active_status_updated_at

View file

@ -9,9 +9,6 @@ class Comment < ApplicationRecord
COMMENTABLE_TYPES = %w[Article PodcastEpisode].freeze
TITLE_DELETED = "[deleted]".freeze
TITLE_HIDDEN = "[hidden by post author]".freeze
URI_REGEXP = %r{
\A
(?:https?://)? # optional scheme
@ -59,7 +56,8 @@ class Comment < ApplicationRecord
validates :reactions_count, presence: true
validates :commentable, on: :create, presence: {
message: lambda do |object, _data|
"#{object.commentable_type.presence || 'item'} has been deleted."
I18n.t("models.comment.has_been_deleted",
type: I18n.t("models.comment.type.#{object.commentable_type.presence || 'item'}"))
end
}
@ -97,6 +95,14 @@ class Comment < ApplicationRecord
.to_h
end
def self.title_deleted
I18n.t("models.comment.deleted")
end
def self.title_hidden
I18n.t("models.comment.hidden")
end
def search_id
"comment_#{id}"
end
@ -134,8 +140,8 @@ class Comment < ApplicationRecord
end
def title(length = 80)
return TITLE_DELETED if deleted
return TITLE_HIDDEN if hidden_by_commentable_user
return self.class.title_deleted if deleted
return self.class.title_hidden if hidden_by_commentable_user
text = ActionController::Base.helpers.strip_tags(processed_html).strip
truncated_text = ActionController::Base.helpers.truncate(text, length: length).gsub("&#39;", "'").gsub("&amp;", "&")
@ -148,9 +154,9 @@ class Comment < ApplicationRecord
def readable_publish_date
if created_at.year == Time.current.year
created_at.strftime("%b %-e")
I18n.l(created_at, format: :short)
else
created_at.strftime("%b %-e '%y")
I18n.l(created_at, format: :short_with_yy)
end
end
@ -315,11 +321,13 @@ class Comment < ApplicationRecord
def discussion_not_locked
return unless commentable_type == "Article" && commentable.discussion_lock
errors.add(:commentable_id, "the discussion is locked on this Post")
errors.add(:commentable_id, I18n.t("models.comment.locked"))
end
def published_article
errors.add(:commentable_id, "is not valid.") if commentable_type == "Article" && !commentable.published
return unless commentable_type == "Article" && !commentable.published
errors.add(:commentable_id, I18n.t("models.comment.is_not_valid"))
end
def user_mentions_in_markdown
@ -329,7 +337,9 @@ class Comment < ApplicationRecord
mentions_count = Nokogiri::HTML(processed_html).css(".mentioned-user").size
return if mentions_count <= Settings::RateLimit.mention_creation
errors.add(:base, "You cannot mention more than #{Settings::RateLimit.mention_creation} users in a comment!")
errors.add(:base,
I18n.t("models.comment.mention_too_many",
count: Settings::RateLimit.mention_creation))
end
def record_field_test_event

View file

@ -9,10 +9,13 @@ class FeedbackMessage < ApplicationRecord
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy
REPORTER_UNIQUENESS_SCOPE = %i[reported_url feedback_type].freeze
REPORTER_UNIQUENESS_MSG = "(you) previously reported this URL.".freeze
CATEGORIES = ["spam", "other", "rude or vulgar", "harassment", "bug", "listings"].freeze
STATUSES = %w[Open Invalid Resolved].freeze
def self.reporter_uniqueness_msg
I18n.t("models.feedback_message.reported")
end
scope :open_abuse_reports, -> { where(status: "Open", feedback_type: "abuse-reports") }
scope :all_user_reports, lambda { |user|
user.reporter_feedback_messages
@ -31,7 +34,7 @@ class FeedbackMessage < ApplicationRecord
inclusion: {
in: STATUSES
}
validates :reporter_id, uniqueness: { scope: REPORTER_UNIQUENESS_SCOPE, message: REPORTER_UNIQUENESS_MSG },
validates :reporter_id, uniqueness: { scope: REPORTER_UNIQUENESS_SCOPE, message: reporter_uniqueness_msg },
if: :abuse_report? && :reporter_id
def abuse_report?

View file

@ -72,10 +72,10 @@ class GithubIssue < ApplicationRecord
def error_message(url)
if PATH_COMMENT_REGEXP.match?(url)
_, issue_id = comment_repo_and_issue_id(url)
"Issue comment #{issue_id} not found"
I18n.t("models.github_issue.issue_comment_not_found", issue_id: issue_id)
else
_, issue_id = issue_or_pull_repo_and_issue_id(url)
"Issue #{issue_id} not found"
I18n.t("models.github_issue.issue_not_found", issue_id: issue_id)
end
end
end

View file

@ -56,7 +56,7 @@ class HtmlVariant < ApplicationRecord
return if group == "campaign"
published_and_approved = (approved && (html_changed? || name_changed? || group_changed?)) && persisted?
errors.add(:base, "cannot change once published and approved") if published_and_approved
errors.add(:base, I18n.t("models.html_variant.no_edits")) if published_and_approved
end
def prefix_all_images

View file

@ -2,9 +2,6 @@
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
class Identity < ApplicationRecord
NO_EMAIL_MSG = "No email found. Please relink your %<provider>s " \
"account to avoid errors.".freeze
belongs_to :user
scope :enabled, -> { where(provider: Authentication::Providers.enabled) }
@ -44,6 +41,6 @@ class Identity < ApplicationRecord
end
def email
auth_data_dump&.info&.email || format(NO_EMAIL_MSG, provider: provider)
auth_data_dump&.info&.email || I18n.t("models.identity.no_email_msg", provider: provider)
end
end

View file

@ -115,15 +115,14 @@ class Listing < ApplicationRecord
def restrict_markdown_input
markdown_string = body_markdown.to_s
if markdown_string.scan(/(?=\n)/).count > 12
errors.add(:body_markdown,
"has too many linebreaks. No more than 12 allowed.")
errors.add(:body_markdown, I18n.t("models.listing.too_many_linebreaks"))
end
errors.add(:body_markdown, "is not allowed to include images.") if markdown_string.include?("![")
errors.add(:body_markdown, "is not allowed to include liquid tags.") if markdown_string.include?("{% ")
errors.add(:body_markdown, I18n.t("models.listing.image_not_allowed")) if markdown_string.include?("![")
errors.add(:body_markdown, I18n.t("models.listing.liquid_not_allowed")) if markdown_string.include?("{% ")
end
def validate_tags
errors.add(:tag_list, "exceed the maximum of 8 tags") if tag_list.length > 8
errors.add(:tag_list, I18n.t("models.listing.too_many_tags")) if tag_list.length > 8
end
def create_slug

View file

@ -29,6 +29,6 @@ class Mention < ApplicationRecord
end
def permission
errors.add(:mentionable_id, "is not valid.") unless mentionable&.valid?
errors.add(:mentionable_id, I18n.t("models.mention.is_not_valid")) unless mentionable&.valid?
end
end

View file

@ -42,7 +42,7 @@ class NavigationLink < ApplicationRecord
# @see NavigationLink.create_or_update_by_identity
# @see NavigationLink#strip_local_hostname
def self.normalize_url(url)
parsed_url = URI.parse(url)
parsed_url = Addressable::URI.parse(url)
return url unless url.match?(/^#{URL.url}/i)
parsed_url.path
@ -53,10 +53,10 @@ class NavigationLink < ApplicationRecord
# We want to allow relative URLs (e.g. /contact) for navigation links while
# still going through the normal validation process.
def allow_relative_url
parsed_url = URI.parse(url)
parsed_url = Addressable::URI.parse(url)
return unless parsed_url.relative? && url.starts_with?("/")
self.url = URI.parse(URL.url).merge(parsed_url).to_s
self.url = Addressable::URI.parse(URL.url).join(parsed_url).to_s
end
# When persisting to the database we store local links as relative URLs which

View file

@ -6,10 +6,6 @@ class Organization < ApplicationRecord
COLOR_HEX_REGEXP = /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
INTEGER_REGEXP = /\A\d+\z/
SLUG_REGEXP = /\A[a-zA-Z0-9\-_]+\z/
MESSAGES = {
integer_only: "Integer only. No sign allowed.",
reserved_word: "%<value>s is a reserved word. Contact site admins for help registering your organization."
}.freeze
acts_as_followable
@ -44,7 +40,7 @@ class Organization < ApplicationRecord
validates :articles_count, presence: true
validates :bg_color_hex, format: COLOR_HEX_REGEXP, allow_blank: true
validates :company_size, format: { with: INTEGER_REGEXP, message: MESSAGES[:integer_only], allow_blank: true }
validates :company_size, format: { with: INTEGER_REGEXP, message: :integer_only, allow_blank: true }
validates :company_size, length: { maximum: 7 }, allow_nil: true
validates :credits_count, presence: true
validates :cta_body_markdown, length: { maximum: 256 }
@ -57,7 +53,7 @@ class Organization < ApplicationRecord
validates :proof, length: { maximum: 1500 }
validates :secret, length: { is: 100 }, allow_nil: true
validates :secret, uniqueness: true
validates :slug, exclusion: { in: ReservedWords.all, message: MESSAGES[:reserved_word] }
validates :slug, exclusion: { in: ReservedWords.all, message: :reserved_word }
validates :slug, format: { with: SLUG_REGEXP }, length: { in: 2..18 }
validates :slug, presence: true, uniqueness: { case_sensitive: false }
validates :spent_credits_count, presence: true
@ -80,6 +76,14 @@ class Organization < ApplicationRecord
alias_attribute :old_old_username, :old_old_slug
alias_attribute :website_url, :url
def self.integer_only
I18n.t("models.organization.integer_only")
end
def self.reserved_word
I18n.t("models.organization.reserved_word")
end
def check_for_slug_change
return unless slug_changed?

View file

@ -75,7 +75,7 @@ class Page < ApplicationRecord
def body_present
return unless body_markdown.blank? && body_html.blank? && body_json.blank?
errors.add(:body_markdown, "must exist if body_html or body_json doesn't exist.")
errors.add(:body_markdown, I18n.t("models.page.body_must_exist"))
end
# As there can only be one global landing page, we want to ensure that

View file

@ -21,7 +21,7 @@ class Podcast < ApplicationRecord
presence: true,
uniqueness: true,
format: { with: /\A[a-zA-Z0-9\-_]+\Z/ },
exclusion: { in: ReservedWords.all, message: "slug is reserved" }
exclusion: { in: ReservedWords.all, message: I18n.t("models.podcast.slug_is_reserved") }
validates :slug, unique_cross_model_slug: true, if: :slug_changed?

View file

@ -6,5 +6,7 @@ class PodcastEpisodeAppearance < ApplicationRecord
belongs_to :podcast_episode
validates :podcast_episode_id, uniqueness: { scope: :user_id }
validates :role, presence: true
validates :role, inclusion: { in: %w[host guest], message: "provided role is not valid" }
validates :role,
inclusion: { in: %w[host guest],
message: I18n.t("models.podcast_episode_appearance.provided_role_is_not_valid") }
end

View file

@ -17,6 +17,6 @@ class PollSkip < ApplicationRecord
return false unless poll
return false unless poll.vote_previously_recorded_for?(user_id: user_id)
errors.add(:base, "cannot vote more than once in one poll")
errors.add(:base, I18n.t("models.poll_skip.cannot_vote_more_than_once"))
end
end

View file

@ -30,7 +30,7 @@ class PollVote < ApplicationRecord
return false unless poll
return false unless poll.vote_previously_recorded_for?(user_id: user_id)
errors.add(:base, "cannot vote more than once in one poll")
errors.add(:base, I18n.t("models.poll_vote.cannot_vote_more_than_once"))
end
def touch_poll_votes_count

View file

@ -14,10 +14,10 @@ class ProfilePin < ApplicationRecord
private
def only_five_pins_per_profile
errors.add(:base, "cannot have more than five total pinned posts") if profile.profile_pins.size > 4
errors.add(:base, I18n.t("models.profile_pin.only_five")) if profile.profile_pins.size > 4
end
def pinnable_belongs_to_profile
errors.add(:pinnable_id, "must have proper permissions for pin") if pinnable.user_id != profile_id
errors.add(:pinnable_id, I18n.t("models.profile_pin.pin_unpermitted")) if pinnable.user_id != profile_id
end
end

View file

@ -22,8 +22,8 @@ class RatingVote < ApplicationRecord
end
def permissions
return if user == article&.user || user&.trusted? || context != "explicit"
return unless context == "explicit" && !user&.trusted && user_id != article&.user_id
errors.add(:user_id, "is not permitted to take this action.")
errors.add(:user_id, I18n.t("models.rating_vote.not_permitted"))
end
end

View file

@ -178,9 +178,10 @@ class Reaction < ApplicationRecord
end
def permissions
errors.add(:category, "is not valid.") if negative_reaction_from_untrusted_user?
errors.add(:category, I18n.t("models.reaction.is_not_valid")) if negative_reaction_from_untrusted_user?
return unless reactable_type == "Article" && !reactable&.published
errors.add(:reactable_id, "is not valid.") if reactable_type == "Article" && !reactable&.published
errors.add(:reactable_id, I18n.t("models.reaction.is_not_valid"))
end
def negative_reaction_from_untrusted_user?

View file

@ -11,19 +11,18 @@ class ResponseTemplate < ApplicationRecord
CONTENT_TYPES = %w[plain_text html body_markdown].freeze
COMMENT_CONTENT_TYPE = %w[body_markdown].freeze
EMAIL_CONTENT_TYPES = %w[plain_text html].freeze
COMMENT_VALIDATION_MSG = "Comment templates must use Markdown as its content type.".freeze
EMAIL_VALIDATION_MSG = "Email templates must use plain text or HTML as its content type.".freeze
USER_NIL_TYPE_OF_MSG = "cannot have a user ID associated.".freeze
validates :type_of, :content_type, :content, :title, presence: true
validates :content, uniqueness: { scope: %i[user_id type_of content_type] }
validates :type_of, inclusion: { in: TYPE_OF_TYPES }
validates :content_type, inclusion: { in: CONTENT_TYPES }
validates :content_type,
inclusion: { in: COMMENT_CONTENT_TYPE, message: COMMENT_VALIDATION_MSG },
inclusion: { in: COMMENT_CONTENT_TYPE,
message: proc { I18n.t("models.response_template.comment_markdown") } },
if: -> { type_of&.include?("comment") }
validates :content_type,
inclusion: { in: EMAIL_CONTENT_TYPES, message: EMAIL_VALIDATION_MSG },
inclusion: { in: EMAIL_CONTENT_TYPES,
message: proc { I18n.t("models.response_template.email_text") } },
if: -> { type_of&.include?("email") }
validate :user_nil_only_for_user_nil_types
validate :template_count
@ -33,13 +32,13 @@ class ResponseTemplate < ApplicationRecord
def user_nil_only_for_user_nil_types
return unless user_id.present? && USER_NIL_TYPE_OF_TYPES.include?(type_of)
errors.add(:type_of, USER_NIL_TYPE_OF_MSG)
errors.add(:type_of, I18n.t("models.response_template.user_nil_only"))
end
def template_count
return unless user
return if user.trusted? || user.response_templates.count <= 30
return if user.trusted || user.response_templates.count <= 30
errors.add(:user, "Response template limit of 30 per user has been reached")
errors.add(:user, I18n.t("models.response_template.limit_reached"))
end
end

View file

@ -5,7 +5,7 @@ module Settings
# Define your settings
setting :articles_expiry_time, type: :integer, default: 4
setting :articles_require_approval, type: :boolean, default: 0
setting :call_to_action, type: :string, default: "Share your project"
setting :call_to_action, type: :string, default: -> { I18n.t("models.settings.campaign.share_your_project") }
setting :featured_tags, type: :array, default: %w[]
setting :hero_html_variant_name, type: :string, default: ""
setting :sidebar_enabled, type: :boolean, default: 0

View file

@ -10,11 +10,11 @@ module Settings
setting(
:community_name,
type: :string,
default: ApplicationConfig["COMMUNITY_NAME"] || "New Forem",
default: ApplicationConfig["COMMUNITY_NAME"] || I18n.t("models.settings.community.new_forem"),
validates: {
format: {
with: /\A[^[<|>]]+\Z/,
message: "may not include the \"<\" nor \">\" character"
message: I18n.t("models.settings.community.message")
}
},
)

View file

@ -48,7 +48,9 @@ module Settings
type: :string,
default: proc { URL.local_image("mascot.png") },
validates: { url: true }
setting :mascot_image_description, type: :string, default: "The community mascot"
setting :mascot_image_description, type: :string, default: lambda {
I18n.t("models.settings.general.the_community_mascot")
}
setting :mascot_footer_image_url, type: :string, validates: { url: true }
setting :mascot_footer_image_width, type: :integer, default: 52
setting :mascot_footer_image_height, type: :integer, default: 120
@ -93,7 +95,7 @@ module Settings
setting :twitter_hashtag, type: :string
# Sponsors
setting :sponsor_headline, default: "Community Sponsors"
setting :sponsor_headline, default: -> { I18n.t("models.settings.general.community_sponsors") }
# Tags
setting :sidebar_tags, type: :array, default: %w[]

View file

@ -21,7 +21,7 @@ module Settings
setting :primary_brand_color_hex, type: :string, default: "#3b49df", validates: {
format: {
with: HEX_COLOR_REGEX,
message: "must be be a 3 or 6 character hex (starting with #)"
message: proc { I18n.t("models.settings.user_experience.message") }
},
color_contrast: true
}

View file

@ -23,7 +23,7 @@ class Sponsorship < ApplicationRecord
validates :sponsorable_type, inclusion: {
in: SPONSORABLE_TYPES,
allow_blank: true,
message: "is not a sponsorable type"
message: I18n.t("models.sponsorship.invalid_type")
}
validate :validate_tag_uniqueness, if: proc { level.to_s == "tag" }
@ -44,13 +44,14 @@ class Sponsorship < ApplicationRecord
return unless self.class.where(sponsorable: sponsorable, level: :tag)
.exists?(["expires_at > ? AND id != ?", Time.current, id.to_i])
errors.add(:level, "The tag is already sponsored")
errors.add(:level, I18n.t("models.sponsorship.already_sponsored"))
end
def validate_level_uniqueness
return unless self.class.where(organization: organization)
.exists?(["level IN (?) AND expires_at > ? AND id != ?", METAL_LEVELS, Time.current, id.to_i])
errors.add(:level, "You can have only one sponsorship of #{METAL_LEVELS.join(', ')}")
levels = METAL_LEVELS.map { |l| I18n.t("models.sponsorship.level.#{l}") }.to_sentence(locale: I18n.locale)
errors.add(:level, I18n.t("models.sponsorship.only_one_level", levels: levels))
end
end

View file

@ -119,7 +119,7 @@ class Tag < ActsAsTaggableOn::Tag
end
def validate_name
errors.add(:name, "is too long (maximum is 30 characters)") if name.length > 30
errors.add(:name, I18n.t("errors.messages.too_long", count: 30)) if name.length > 30
# [:alnum:] is not used here because it supports diacritical characters.
# If we decide to allow diacritics in the future, we should replace the
# following regex with [:alnum:].
@ -258,7 +258,7 @@ class Tag < ActsAsTaggableOn::Tag
def validate_alias_for
return if Tag.exists?(name: alias_for)
errors.add(:tag, "alias_for must refer to an existing tag")
errors.add(:tag, I18n.t("models.tag.alias_for"))
end
def pound_it

View file

@ -1,5 +1,6 @@
class TagAdjustment < ApplicationRecord
validates :tag_name, presence: true, uniqueness: { scope: :article_id, message: "can't be an already adjusted tag" }
validates :tag_name, presence: true,
uniqueness: { scope: :article_id, message: I18n.t("models.tag_adjustment.unique") }
validates :reason_for_adjustment, presence: true
validates :adjustment_type, inclusion: { in: %w[removal addition] }, presence: true
validates :status, inclusion: { in: %w[committed pending committed_and_resolvable resolved] }, presence: true
@ -14,7 +15,7 @@ class TagAdjustment < ApplicationRecord
private
def user_permissions
errors.add(:user_id, "does not have privilege to adjust these tags") unless has_privilege_to_adjust?
errors.add(:user_id, I18n.t("models.tag_adjustment.unpermitted")) unless has_privilege_to_adjust?
end
def has_privilege_to_adjust?
@ -28,8 +29,10 @@ class TagAdjustment < ApplicationRecord
tag.casecmp(tag_name).zero?
end
errors.add(:tag_id,
"selected for removal is not a current live tag.")
I18n.t("models.tag_adjustment.not_live"))
end
errors.add(:base, "4 tags max per article.") if adjustment_type == "addition" && article.tag_list.count > 3
return unless adjustment_type == "addition" && article.tag_list.count > 3
errors.add(:base, I18n.t("models.tag_adjustment.too_many_tags"))
end
end

View file

@ -47,7 +47,7 @@ class Tweet < ApplicationRecord
def fetch(status_id)
retrieve_and_save_tweet(status_id)
rescue TwitterClient::Errors::NotFound => e
raise e, "Tweet not found"
raise e, I18n.t("models.tweet.tweet_not_found")
end
def retrieve_and_save_tweet(status_id)

View file

@ -26,9 +26,6 @@ class User < ApplicationRecord
USERNAME_MAX_LENGTH = 30
USERNAME_REGEXP = /\A[a-zA-Z0-9_]+\z/
MESSAGES = {
reserved_username: "username is reserved"
}.freeze
# follow the syntax in https://interledger.org/rfcs/0026-payment-pointers/#payment-pointer-syntax
PAYMENT_POINTER_REGEXP = %r{
\A # start
@ -141,9 +138,12 @@ class User < ApplicationRecord
validates :subscribed_to_user_subscriptions_count, presence: true
validates :unspent_credits_count, presence: true
validates :username, length: { in: 2..USERNAME_MAX_LENGTH }, format: USERNAME_REGEXP
validates :username, presence: true, exclusion: { in: ReservedWords.all, message: MESSAGES[:invalid_username] }
validates :username, presence: true, exclusion: {
in: ReservedWords.all,
message: proc { I18n.t("models.user.username_is_reserved") }
}
validates :username, uniqueness: { case_sensitive: false, message: lambda do |_obj, data|
"#{data[:value]} is taken."
I18n.t("models.user.is_taken", username: (data[:value]))
end }, if: :username_changed?
# add validators for provider related usernames
@ -483,7 +483,7 @@ class User < ApplicationRecord
end
def non_banished_username
errors.add(:username, "has been banished.") if BanishedUser.exists?(username: username)
errors.add(:username, I18n.t("models.user.has_been_banished")) if BanishedUser.exists?(username: username)
end
def subscribe_to_mailchimp_newsletter
@ -662,7 +662,7 @@ class User < ApplicationRecord
rate_limiter.track_limit_by_action(:send_email_confirmation)
rate_limiter.check_limit!(:send_email_confirmation)
rescue RateLimitChecker::LimitReached => e
errors.add(:email, "confirmation could not be sent. #{e.message}")
errors.add(:email, I18n.t("models.user.could_not_send", e_message: e.message))
end
def update_rate_limit
@ -671,13 +671,13 @@ class User < ApplicationRecord
rate_limiter.track_limit_by_action(:user_update)
rate_limiter.check_limit!(:user_update)
rescue RateLimitChecker::LimitReached => e
errors.add(:base, "User could not be saved. #{e.message}")
errors.add(:base, I18n.t("models.user.user_could_not_be_saved", e_message: e.message))
end
def password_matches_confirmation
return true if password == password_confirmation
errors.add(:password, "doesn't match password confirmation")
errors.add(:password, I18n.t("models.user.password_not_matched"))
end
def strip_payment_pointer

View file

@ -30,7 +30,7 @@ class UserBlock < ApplicationRecord
private
def blocker_cannot_be_same_as_blocked
errors.add(:blocker_id, "can't be the same as the blocked_id") if blocker_id == blocked_id
errors.add(:blocker_id, I18n.t("models.user_block.cant_be_the_same")) if blocker_id == blocked_id
end
def bust_blocker_cache

View file

@ -52,7 +52,7 @@ class UserSubscription < ApplicationRecord
return unless user_subscription_sourceable
return if liquid_tags_used.include?(UserSubscriptionTag)
errors.add(:base, "User subscriptions are not enabled for the source.")
errors.add(:base, I18n.t("models.user_subscription.not_enabled"))
end
def liquid_tags_used
@ -68,7 +68,7 @@ class UserSubscription < ApplicationRecord
def non_apple_auth_subscriber
return unless subscriber_email&.end_with?("@privaterelay.appleid.com")
errors.add(:subscriber_email, "Can't subscribe with an Apple private relay. Please update email.")
errors.add(:subscriber_email, I18n.t("models.user_subscription.non_apple"))
end
def active_user_subscription_source
@ -88,6 +88,6 @@ class UserSubscription < ApplicationRecord
return if source_active
errors.add(:base, "Source not found. Please make sure your #{user_subscription_sourceable_type} is active!")
errors.add(:base, I18n.t("models.user_subscription.source_not_found", source: user_subscription_sourceable_type))
end
end

View file

@ -16,7 +16,7 @@ module Users
def self.id() = nil
def self.darker_color() = Color::CompareHex.new(USER_COLORS).brightness
def self.username() = "[deleted user]"
def self.name() = "[Deleted User]"
def self.name() = I18n.t("models.users.deleted_user.name")
def self.summary() = nil
def self.twitter_username() = nil
def self.github_username() = nil

View file

@ -22,7 +22,8 @@ module Users
validates :brand_color1,
:brand_color2,
format: { with: HEX_COLOR_REGEXP, message: "is not a valid hex color" },
format: { with: HEX_COLOR_REGEXP,
message: I18n.t("models.users.setting.invalid_hex") },
allow_nil: true
validates :experience_level, numericality: { less_than_or_equal_to: 10 }, allow_blank: true
validates :feed_referential_link, inclusion: { in: [true, false] }
@ -42,7 +43,7 @@ module Users
valid = Feeds::ValidateUrl.call(feed_url)
errors.add(:feed_url, "is not a valid RSS/Atom feed") unless valid
errors.add(:feed_url, I18n.t("models.users.setting.invalid_rss")) unless valid
rescue StandardError => e
errors.add(:feed_url, e.message)
end

View file

@ -69,7 +69,7 @@ module Spam
author_id: Settings::General.mascot_user_id,
noteable: user,
reason: "automatic_suspend",
content: "User suspended for too many spammy interactions, triggered by autovomit.",
content: I18n.t("models.comment.suspended_too_many"),
)
end
private_class_method :suspend!

View file

@ -3,10 +3,10 @@ json.id_code comment.id_code_generated
json.created_at utc_iso_timestamp(comment.created_at)
if comment.deleted?
json.body_html "<p>#{Comment::TITLE_DELETED}</p>"
json.body_html "<p>#{Comment.title_deleted}</p>"
json.set! :user, {}
elsif comment.hidden_by_commentable_user?
json.body_html "<p>#{Comment::TITLE_HIDDEN}</p>"
json.body_html "<p>#{Comment.title_hidden}</p>"
json.set! :user, {}
else
json.body_html comment.processed_html

View file

@ -0,0 +1,131 @@
---
en:
models:
api_secret:
api_limit_reached: API secret limit of 20 per user has been reached
application_record:
uninferrable: Could not infer a decorator for %{class}.
article:
a_post_by: A post by %{user_name}
video_unpermitted: cannot be added by member without permission
video_processing: cannot be set to true if video is still processing
cannot_search_tags_for: 'Cannot search tags for: %{tag}'
cannot_search_tags_for_tags: 'Cannot search tags for: %{tags}'
unique_coauthor: co-author IDs must be unique
too_many_tags: exceed the maximum of 4 tags
is_too_long: is too long.
invalid_date: must be entered in DD/MM/YYYY format with current or past date
series_unpermitted: must be one you have permission to post to
invalid_coauthor: must be valid user IDs
same_author: must not be the same user as the author
must_not_have_spaces: must not have spaces
published: Published
unique_url: has already been taken. Email %{email} for further details.
mention_too_many:
one: You cannot mention more than %{count} users in a post!
other: You cannot mention more than %{count} users in a post!
broadcast:
single_active: You can only have one active announcement broadcast
comment:
deleted: "[deleted]"
has_been_deleted: "%{type} has been deleted."
hidden: "[hidden by post author]"
is_not_valid: is not valid.
locked: the discussion is locked on this Post
type:
Article: Article
item: item
PodcastEpisode: Podcast episode
suspended_too_many: User suspended for too many spammy interactions, triggered by autovomit.
mention_too_many:
one: You cannot mention more than %{count} users in a comment!
other: You cannot mention more than %{count} users in a comment!
feedback_message:
reported: "(you) previously reported this URL."
github_issue:
issue_comment_not_found: Issue comment %{issue_id} not found
issue_not_found: Issue %{issue_id} not found
html_variant:
no_edits: cannot change once published and approved
identity:
no_email_msg: No email found. Please relink your %{provider} account to avoid errors.
listing:
too_many_tags: exceed the maximum of 8 tags
too_many_linebreaks: has too many linebreaks. No more than 12 allowed.
image_not_allowed: is not allowed to include images.
liquid_not_allowed: is not allowed to include liquid tags.
mention:
is_not_valid: is not valid.
organization:
integer_only: Integer only. No sign allowed.
reserved_word: "%<value>s is a reserved word. Contact site admins for help registering your organization."
page:
body_must_exist: must exist if body_html or body_json doesn't exist.
podcast:
slug_is_reserved: slug is reserved
podcast_episode_appearance:
provided_role_is_not_valid: provided role is not valid
poll_skip:
cannot_vote_more_than_once: cannot vote more than once in one poll
poll_vote:
cannot_vote_more_than_once: cannot vote more than once in one poll
profile_pin:
only_five: cannot have more than five total pinned posts
pin_unpermitted: must have proper permissions for pin
rating_vote:
not_permitted: is not permitted to take this action.
reaction:
is_not_valid: is not valid.
response_template:
user_nil_only: cannot have a user ID associated.
comment_markdown: Comment templates must use Markdown as its content type.
email_text: Email templates must use plain text or HTML as its content type.
limit_reached: Response template limit of 30 per user has been reached
settings:
campaign:
share_your_project: Share your project
community:
message: may not include the "<" nor ">" character
new_forem: New Forem
user: user
general:
community_sponsors: Community Sponsors
the_community_mascot: The community mascot
user_experience:
message: 'must be be a 3 or 6 character hex (starting with #)'
sponsorship:
invalid_type: is not a sponsorable type
already_sponsored: The tag is already sponsored
only_one_level: You can have only one sponsorship of %{levels}
level:
gold: gold
silver: silver
bronze: bronze
tag:
alias_for: alias_for must refer to an existing tag
tag_adjustment:
too_many_tags: 4 tags max per article.
unpermitted: does not have privilege to adjust these tags
not_live: selected for removal is not a current live tag.
unique: can't be an already adjusted tag
tweet:
tweet_not_found: Tweet not found
user:
could_not_send: confirmation could not be sent. %{e_message}
password_not_matched: doesn't match password confirmation
has_been_banished: has been banished.
is_taken: "%{username} is taken."
user_could_not_be_saved: User could not be saved. %{e_message}
username_is_reserved: username is reserved
user_block:
cant_be_the_same: can't be the same as the blocked_id
user_subscription:
non_apple: Can't subscribe with an Apple private relay. Please update email.
source_not_found: Source not found. Please make sure your %{source} is active!
not_enabled: User subscriptions are not enabled for the source.
users:
deleted_user:
name: "[Deleted User]"
setting:
invalid_hex: is not a valid hex color
invalid_rss: is not a valid RSS/Atom feed

View file

@ -0,0 +1,131 @@
---
fr:
models:
api_secret:
api_limit_reached: API secret limit of 20 per user has been reached
application_record:
uninferrable: Could not infer a decorator for %{class}.
article:
a_post_by: A post by %{user_name}
video_unpermitted: cannot be added by member without permission
video_processing: cannot be set to true if video is still processing
cannot_search_tags_for: 'Cannot search tags for: %{tag}'
cannot_search_tags_for_tags: 'Cannot search tags for: %{tags}'
unique_coauthor: co-author IDs must be unique
too_many_tags: exceed the maximum of 4 tags
is_too_long: is too long.
invalid_date: must be entered in DD/MM/YYYY format with current or past date
series_unpermitted: must be one you have permission to post to
invalid_coauthor: must be valid user IDs
same_author: must not be the same user as the author
must_not_have_spaces: must not have spaces
published: Published
unique_url: has already been taken. Email %{email} for further details.
mention_too_many:
one: You cannot mention more than %{count} users in a post!
other: You cannot mention more than %{count} users in a post!
broadcast:
single_active: You can only have one active announcement broadcast
comment:
deleted: "[deleted]"
has_been_deleted: "%{type} has been deleted."
hidden: "[hidden by post author]"
is_not_valid: is not valid.
locked: the discussion is locked on this Post
type:
Article: Article
item: item
PodcastEpisode: Podcast episode
suspended_too_many: User suspended for too many spammy interactions, triggered by autovomit.
mention_too_many:
one: You cannot mention more than %{count} users in a comment!
other: You cannot mention more than %{count} users in a comment!
feedback_message:
reported: "(you) previously reported this URL."
github_issue:
issue_comment_not_found: Issue comment %{issue_id} not found
issue_not_found: Issue %{issue_id} not found
html_variant:
no_edits: cannot change once published and approved
identity:
no_email_msg: No email found. Please relink your %{provider} account to avoid errors.
listing:
too_many_tags: exceed the maximum of 8 tags
too_many_linebreaks: has too many linebreaks. No more than 12 allowed.
image_not_allowed: is not allowed to include images.
liquid_not_allowed: is not allowed to include liquid tags.
mention:
is_not_valid: is not valid.
organization:
integer_only: Integer only. No sign allowed.
reserved_word: "%<value>s is a reserved word. Contact site admins for help registering your organization."
page:
body_must_exist: must exist if body_html or body_json doesn't exist.
podcast:
slug_is_reserved: slug is reserved
podcast_episode_appearance:
provided_role_is_not_valid: provided role is not valid
poll_skip:
cannot_vote_more_than_once: cannot vote more than once in one poll
poll_vote:
cannot_vote_more_than_once: cannot vote more than once in one poll
profile_pin:
only_five: cannot have more than five total pinned posts
pin_unpermitted: must have proper permissions for pin
rating_vote:
not_permitted: is not permitted to take this action.
reaction:
is_not_valid: is not valid.
response_template:
user_nil_only: cannot have a user ID associated.
comment_markdown: Comment templates must use Markdown as its content type.
email_text: Email templates must use plain text or HTML as its content type.
limit_reached: Response template limit of 30 per user has been reached
settings:
campaign:
share_your_project: Share your project
community:
message: may not include the "<" nor ">" character
new_forem: New Forem
user: user
general:
community_sponsors: Community Sponsors
the_community_mascot: The community mascot
user_experience:
message: 'must be be a 3 or 6 character hex (starting with #)'
sponsorship:
invalid_type: is not a sponsorable type
already_sponsored: The tag is already sponsored
only_one_level: You can have only one sponsorship of %{levels}
level:
gold: gold
silver: silver
bronze: bronze
tag:
alias_for: alias_for must refer to an existing tag
tag_adjustment:
too_many_tags: 4 tags max per article.
unpermitted: does not have privilege to adjust these tags
not_live: selected for removal is not a current live tag.
unique: can't be an already adjusted tag
tweet:
tweet_not_found: Tweet not found
user:
could_not_send: confirmation could not be sent. %{e_message}
password_not_matched: doesn't match password confirmation
has_been_banished: has been banished.
is_taken: "%{username} is taken."
user_could_not_be_saved: User could not be saved. %{e_message}
username_is_reserved: username is reserved
user_block:
cant_be_the_same: can't be the same as the blocked_id
user_subscription:
non_apple: Can't subscribe with an Apple private relay. Please update email.
source_not_found: Source not found. Please make sure your %{source} is active!
not_enabled: User subscriptions are not enabled for the source.
users:
deleted_user:
name: "[Deleted User]"
setting:
invalid_hex: is not a valid hex color
invalid_rss: is not a valid RSS/Atom feed

View file

@ -39,7 +39,7 @@ RSpec.describe FeedbackMessage, type: :model do
it do
expect(feedback_message).to validate_uniqueness_of(:reporter_id)
.scoped_to(described_class::REPORTER_UNIQUENESS_SCOPE)
.with_message(described_class::REPORTER_UNIQUENESS_MSG)
.with_message(described_class.reporter_uniqueness_msg)
end
it { is_expected.to validate_length_of(:reported_url).is_at_most(250) }

View file

@ -1,7 +1,7 @@
require "rails_helper"
RSpec.describe ResponseTemplate, type: :model do
let(:comment_validation_message) { ResponseTemplate::COMMENT_VALIDATION_MSG }
let(:comment_validation_message) { I18n.t("models.response_template.comment_markdown") }
it { is_expected.to validate_inclusion_of(:type_of).in_array(ResponseTemplate::TYPE_OF_TYPES) }
it { is_expected.to validate_inclusion_of(:content_type).in_array(ResponseTemplate::CONTENT_TYPES) }
@ -25,7 +25,9 @@ RSpec.describe ResponseTemplate, type: :model do
it "validates that there is no user ID associated" do
response_template = build(:response_template, type_of: "mod_comment", content_type: "body_markdown", user_id: 1)
expect(response_template.valid?).to eq false
expect(response_template.errors.messages[:type_of].to_sentence).to eq(ResponseTemplate::USER_NIL_TYPE_OF_MSG)
expect(response_template.errors.messages[:type_of].to_sentence).to eq(
I18n.t("models.response_template.user_nil_only"),
)
end
end
end

View file

@ -59,7 +59,7 @@ RSpec.describe "/admin/advanced/response_templates", type: :request do
title: "something"
}
}
expect(response.body).to include(ResponseTemplate::COMMENT_VALIDATION_MSG)
expect(response.body).to include(I18n.t("models.response_template.comment_markdown"))
end
end
@ -102,7 +102,7 @@ RSpec.describe "/admin/advanced/response_templates", type: :request do
content_type: "html"
}
}
expect(response.body).to include(ResponseTemplate::COMMENT_VALIDATION_MSG)
expect(response.body).to include(I18n.t("models.response_template.comment_markdown"))
end
end

View file

@ -118,7 +118,7 @@ RSpec.describe "Api::V0::Comments", type: :request do
it "replaces the body_html" do
get api_comments_path(a_id: article.id)
expect(find_child_comment(response)["body_html"]).to eq("<p>#{Comment::TITLE_DELETED}</p>")
expect(find_child_comment(response)["body_html"]).to eq("<p>#{Comment.title_deleted}</p>")
end
it "does not render the user information" do
@ -148,7 +148,7 @@ RSpec.describe "Api::V0::Comments", type: :request do
it "replaces the body_html" do
get api_comments_path(a_id: article.id)
expect(find_child_comment(response)["body_html"]).to eq("<p>#{Comment::TITLE_HIDDEN}</p>")
expect(find_child_comment(response)["body_html"]).to eq("<p>#{Comment.title_hidden}</p>")
end
it "does not render the user information" do

View file

@ -52,6 +52,7 @@ RSpec.describe Notifications::NewMention::Send, type: :service do
user.notification_setting.update(mobile_mention_notifications: true)
allow(PushNotifications::Send).to receive(:call)
allow(I18n).to receive(:t).with("services.notifications.new_mention.new")
allow(I18n).to receive(:l)
allow(I18n).to receive(:t).with("views.notifications.mention.article_mobile",
user: mention.mentionable.user.username,
title: anything).and_call_original