From 198fcd0a7ce2c702cfa94f472aec230e4a230453 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 29 Oct 2025 14:15:38 -0230 Subject: [PATCH 01/12] feat(security): integrate devise_zxcvbn for enhanced password strength validation and update locale messages --- Gemfile.lock | 5 +++++ app/models/better_together/user.rb | 7 +++++++ better_together.gemspec | 1 + config/locales/en.yml | 3 +++ config/locales/es.yml | 4 ++++ config/locales/fr.yml | 4 ++++ lib/better_together/engine.rb | 1 + 7 files changed, 25 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 195b61b11..59849fe48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,6 +29,7 @@ PATH devise devise-i18n devise-jwt + devise_zxcvbn elasticsearch-model (~> 7) elasticsearch-rails (~> 7) font-awesome-sass (~> 6.5) @@ -266,6 +267,9 @@ GEM devise-jwt (0.12.1) devise (~> 4.0) warden-jwt_auth (~> 0.10) + devise_zxcvbn (6.0.0) + devise + zxcvbn (~> 0.1.7) diff-lcs (1.6.2) disposable (0.6.3) declarative (>= 0.0.9, < 1.0.0) @@ -832,6 +836,7 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) zeitwerk (2.7.3) + zxcvbn (0.1.13) PLATFORMS aarch64-linux diff --git a/app/models/better_together/user.rb b/app/models/better_together/user.rb index ba815960c..242f35a16 100644 --- a/app/models/better_together/user.rb +++ b/app/models/better_together/user.rb @@ -8,6 +8,7 @@ class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, + :zxcvbnable, :recoverable, :rememberable, :validatable, :confirmable, :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist @@ -66,5 +67,11 @@ def person_attributes=(attributes) def to_s email end + + def weak_words + return unless person + + [person.name, person.slug, person.identifier] + end end end diff --git a/better_together.gemspec b/better_together.gemspec index f7b86ab58..181229f90 100644 --- a/better_together.gemspec +++ b/better_together.gemspec @@ -37,6 +37,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'devise' spec.add_dependency 'devise-i18n' spec.add_dependency 'devise-jwt' + spec.add_dependency 'devise_zxcvbn' spec.add_dependency 'elasticsearch-model', '~> 7' spec.add_dependency 'elasticsearch-rails', '~> 7' spec.add_dependency 'font-awesome-sass', '~> 6.5' diff --git a/config/locales/en.yml b/config/locales/en.yml index 6a49a1955..ffa3040ab 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1847,6 +1847,9 @@ en: too_short: one: is too short (minimum is %{count} character) other: is too short (minimum is %{count} characters) + weak_password: not strong enough. Consider adding a number, symbols or more + letters to make it stronger. Avoid using the word 'password', your birthday, + or other common easily-guessable passwords. wrong_length: one: is the wrong length (should be %{count} character) other: is the wrong length (should be %{count} characters) diff --git a/config/locales/es.yml b/config/locales/es.yml index 31edccb05..f5b840796 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -409,6 +409,7 @@ es: last_sign_in_ip: IP del último inicio lock_version: Versión de bloqueo locked_at: Fecha de bloqueo + password: Contraseña person: :activerecord.models.person person_identification: :activerecord.models.person_identification remember_created_at: Fecha de 'Recordarme' @@ -1869,6 +1870,9 @@ es: too_short: one: es demasiado corto (%{count} carácter mínimo) other: es demasiado corto (%{count} caracteres mínimo) + weak_password: no es lo suficientemente fuerte. Considera agregar un número, + símbolos o más letras para hacerla más fuerte. Evita usar la palabra 'contraseña', + tu fecha de nacimiento o otras contraseñas comunes fáciles de adivinar. wrong_length: one: no tiene la longitud correcta (%{count} carácter exactos) other: no tiene la longitud correcta (%{count} caracteres exactos) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 60a48b25e..e984a9e99 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -409,6 +409,7 @@ fr: last_sign_in_ip: IP de dernière connexion lock_version: Version de verrouillage locked_at: Verrouillé à + password: Mot de passe person: :activerecord.models.person person_identification: :activerecord.models.person_identification remember_created_at: Souvenir créé à @@ -1877,6 +1878,9 @@ fr: too_short: one: est trop court (au moins %{count} caractère) other: est trop court (au moins %{count} caractères) + weak_password: n'est pas assez fort. Considérez ajouter un nombre, des symboles + ou plus de lettres pour le renforcer. Évitez d'utiliser le mot 'mot de passe', + votre date de naissance ou d'autres mots de passe communs faciles à deviner. wrong_length: one: ne fait pas la bonne longueur (doit comporter %{count} caractère) other: ne fait pas la bonne longueur (doit comporter %{count} caractères) diff --git a/lib/better_together/engine.rb b/lib/better_together/engine.rb index 0344ade23..617b45d5e 100644 --- a/lib/better_together/engine.rb +++ b/lib/better_together/engine.rb @@ -14,6 +14,7 @@ require 'devise' require 'devise-i18n' require 'devise/jwt' +require 'devise_zxcvbn' require 'elasticsearch/model' require 'elasticsearch/rails' require 'font-awesome-sass' From a1fb21d4e1bd2f30e76524feabc0e5d239ce4fed Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 29 Oct 2025 14:41:04 -0230 Subject: [PATCH 02/12] add ukranian language strings --- config/locales/es.yml | 1 + config/locales/fr.yml | 3 +- config/locales/uk.yml | 2263 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2266 insertions(+), 1 deletion(-) create mode 100644 config/locales/uk.yml diff --git a/config/locales/es.yml b/config/locales/es.yml index f5b840796..8d35eafcf 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -2071,6 +2071,7 @@ es: en: English es: Español fr: Français + uk: Українська meta: default_description: Bienvenido a %{platform_name} page: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e984a9e99..b154fe803 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -2076,8 +2076,9 @@ fr: calculating: Calcul en cours locales: en: English - es: Espagnol + es: Español fr: Français + uk: Українська meta: default_description: Bienvenue sur %{platform_name} page: diff --git a/config/locales/uk.yml b/config/locales/uk.yml new file mode 100644 index 000000000..91abf60a3 --- /dev/null +++ b/config/locales/uk.yml @@ -0,0 +1,2263 @@ +--- +uk: + accepted_at: Прийнято о + accessibility_attributes: Атрибути доступності + activerecord: + attributes: + action_text/encrypted_rich_text: + body: Тіло + embeds_attachments: Вбудовані вкладення + embeds_blobs: Вбудовані блоби + locale: Локаль + name: Ім'я + record: :activerecord.models.record + record_type: Тип запису + action_text/rich_text: + body: Тіло + embeds_attachments: Вбудовані вкладення + embeds_blobs: Вбудовані блоби + locale: Локаль + name: Ім'я + record: :activerecord.models.record + record_type: Тип запису + active_storage/attachment: + blob: :activerecord.models.blob + name: Ім'я + record: :activerecord.models.record + record_type: Тип запису + active_storage/blob: + attachments: Вкладення + byte_size: Розмір у байтах + checksum: Контрольна сума + content_type: Тип контенту + filename: Ім'я файлу + key: Ключ + metadata: Метадані + preview_image_attachment: :activerecord.models.preview_image_attachment + preview_image_blob: :activerecord.models.preview_image_blob + service_name: Ім'я сервісу + variant_records: Записи варіантів + active_storage/variant_record: + blob: :activerecord.models.blob + image_attachment: :activerecord.models.image_attachment + image_blob: :activerecord.models.image_blob + variation_digest: Дайджест варіації + application_notifier: + notifications: Сповіщення + params: Параметри + record: :activerecord.models.record + record_type: Тип запису + application_notifier/notification: + event: :activerecord.models.event + read_at: Прочитано о + recipient: :activerecord.models.recipient + recipient_type: Тип одержувача + seen_at: Переглянуто о + better_together/community: + creator: :activerecord.models.creator + host: Хост + identifier: Ідентифікатор + lock_version: Версія блокування + person_community_memberships: Членство осіб у спільноті + person_members: Учасники особи + person_roles: Ролі особи + privacy: Конфіденційність + protected: Захищено + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + text_translations: Переклади тексту + better_together/content/block: + accessibility_attributes: Атрибути доступності + content_settings: Налаштування контенту + css_settings: Налаштування CSS + data_attributes: Атрибути даних + html_attributes: HTML атрибути + identifier: Ідентифікатор + layout_settings: Налаштування макета + lock_version: Версія блокування + media_settings: Налаштування медіа + page_blocks: Блоки сторінки + pages: Сторінки + better_together/content/image: + accessibility_attributes: Accessibility attributes + content_settings: Content settings + css_settings: Css settings + data_attributes: Data attributes + html_attributes: Html attributes + identifier: Identifier + layout_settings: Layout settings + lock_version: Lock version + media_attachment: :activerecord.models.media_attachment + media_blob: :activerecord.models.media_blob + media_settings: Media settings + page_blocks: Page blocks + pages: Pages + string_translations: String translations + better_together/content/page_block: + block: :activerecord.models.block + lock_version: Lock version + page: :activerecord.models.page + position: Position + better_together/content/rich_text: + accessibility_attributes: Accessibility attributes + content_settings: Content settings + css_settings: Css settings + data_attributes: Data attributes + html_attributes: Html attributes + identifier: Identifier + layout_settings: Layout settings + lock_version: Lock version + media_settings: Media settings + page_blocks: Page blocks + pages: Pages + rich_text_content: :activerecord.models.rich_text_content + rich_text_content_en: :activerecord.models.rich_text_content_en + rich_text_content_es: :activerecord.models.rich_text_content_es + rich_text_content_fr: :activerecord.models.rich_text_content_fr + rich_text_translations: Rich text translations + better_together/conversation: + conversation_participants: Conversation participants + creator: :activerecord.models.creator + lock_version: Lock version + messages: Messages + participants: Participants + title: Conversation title + better_together/conversation_participant: + conversation: :activerecord.models.conversation + lock_version: Lock version + person: :activerecord.models.person + better_together/geography/continent: + community: :activerecord.models.community + countries: Countries + country_continents: Country continents + identifier: Identifier + lock_version: Lock version + protected: Protected + slug: Slug + slugs: Slugs + string_translations: String translations + text_translations: Text translations + better_together/geography/country: + community: :activerecord.models.community + continents: Continents + country_continents: Country continents + identifier: Identifier + iso_code: Iso code + lock_version: Lock version + protected: Protected + slug: Slug + slugs: Slugs + states: States + string_translations: String translations + text_translations: Text translations + better_together/geography/country_continent: + continent: :activerecord.models.continent + country: :activerecord.models.country + lock_version: Lock version + better_together/geography/region: + community: :activerecord.models.community + country: :activerecord.models.country + identifier: Identifier + lock_version: Lock version + protected: Protected + region_settlements: Region settlements + settlements: Settlements + slug: Slug + slugs: Slugs + state: :activerecord.models.state + string_translations: String translations + text_translations: Text translations + better_together/geography/region_settlement: + lock_version: Lock version + protected: Protected + region: :activerecord.models.region + settlement: :activerecord.models.settlement + better_together/geography/settlement: + community: :activerecord.models.community + country: :activerecord.models.country + identifier: Identifier + lock_version: Lock version + protected: Protected + region_settlements: Region settlements + regions: Regions + slug: Slug + slugs: Slugs + state: :activerecord.models.state + string_translations: String translations + text_translations: Text translations + better_together/geography/state: + community: :activerecord.models.community + country: :activerecord.models.country + identifier: Identifier + iso_code: Iso code + lock_version: Lock version + protected: Protected + regions: Regions + settlements: Settlements + slug: Slug + slugs: Slugs + string_translations: String translations + text_translations: Text translations + better_together/identification: + active: Active + agent: :activerecord.models.agent + agent_type: Agent type + identity: :activerecord.models.identity + identity_type: Identity type + lock_version: Lock version + better_together/jwt_denylist: + exp: Exp + jti: Jti + lock_version: Lock version + better_together/message: + content: Content + conversation: :activerecord.models.conversation + lock_version: Lock version + sender: :activerecord.models.sender + better_together/navigation_area: + identifier: Identifier + lock_version: Lock version + name: Name + navigable: :activerecord.models.navigable + navigable_type: Navigable type + navigation_items: Navigation items + protected: Protected + slug: Slug + slugs: Slugs + string_translations: String translations + style: Style + visible: Visible + better_together/navigation_item: + children: Children + icon: Icon + identifier: Identifier + item_type: Item type + linkable: :activerecord.models.linkable + linkable_type: Linkable type + lock_version: Lock version + navigation_area: :activerecord.models.navigation_area + parent: :activerecord.models.parent + position: Position + protected: Protected + route_name: Route name + slug: Slug + slugs: Slugs + string_translations: String translations + url: Url + visible: Visible + better_together/new_message_notifier: + notifications: Notifications + params: Params + record: :activerecord.models.record + record_type: Record type + better_together/new_message_notifier/notification: + event: :activerecord.models.event + read_at: Read at + recipient: :activerecord.models.recipient + recipient_type: Recipient type + seen_at: Seen at + better_together/page: + blocks: Blocks + identifier: Identifier + image_blocks: Image blocks + keywords: Keywords + language: Language + layout: Layout + lock_version: Lock version + meta_description: Meta description + page_blocks: Page blocks + privacy: Privacy + protected: Protected + published: Published + published_at: Published at + rich_text_blocks: Rich text blocks + rich_text_content: :activerecord.models.rich_text_content + rich_text_content_en: :activerecord.models.rich_text_content_en + rich_text_content_es: :activerecord.models.rich_text_content_es + rich_text_content_fr: :activerecord.models.rich_text_content_fr + rich_text_translations: Rich text translations + slug: Slug + slugs: Slugs + string_translations: String translations + template: Template + better_together/page_authorship_notifier: + notifications: Notifications + params: Params + record: :activerecord.models.record + record_type: Record type + better_together/page_authorship_notifier/notification: + event: :activerecord.models.event + read_at: Read at + recipient: :activerecord.models.recipient + recipient_type: Recipient type + seen_at: Seen at + better_together/person: + authorships: Authorships + community: :activerecord.models.community + community_roles: Community roles + conversation_participants: Conversation participants + conversations: Conversations + created_conversations: Created conversations + description: Description + identifications: Identifications + identifier: Identifier + locale: Language + lock_version: Lock version + member_communities: Member communities + member_platforms: Member platforms + name: Name + notification_mentions: Notification mentions + notifications: Notifications + notify_by_email: Receive email notifications + person_community_memberships: Person community memberships + person_platform_memberships: Person platform memberships + platform_roles: Platform roles + preferences: Preferences + show_conversation_details: Show conversation details + slug: Username + slugs: Slugs + string_translations: String translations + text_translations: Text translations + user: :activerecord.models.user + user_identification: :activerecord.models.user_identification + better_together/person_community_membership: + joinable: :activerecord.models.joinable + lock_version: Lock version + member: :activerecord.models.member + role: :activerecord.models.role + better_together/person_platform_membership: + joinable: :activerecord.models.joinable + lock_version: Lock version + member: :activerecord.models.member + role: :activerecord.models.role + better_together/platform: + community: :activerecord.models.community + host: Host + identifier: Identifier + invitations: Invitations + lock_version: Lock version + person_members: Person members + person_platform_memberships: Person platform memberships + person_roles: Person roles + privacy: Privacy + protected: Protected + settings: Settings + slug: Slug + slugs: Slugs + string_translations: String translations + text_translations: Text translations + time_zone: Time zone + url: Url + better_together/platform_invitation: + accepted_at: Accepted at + community_role: :activerecord.models.community_role + created_at: Created at + invitable: :activerecord.models.invitable + invitee: :activerecord.models.invitee + invitee_email: Invitee email + inviter: :activerecord.models.inviter + last_sent: Last sent + locale: Locale + lock_version: Lock version + platform_role: :activerecord.models.platform_role + session_duration_mins: Session duration (mins) + status: Status + token: Token + type: Type + valid_from: Valid from + valid_until: Valid until + better_together/resource_permission: + action: Action + identifier: Identifier + lock_version: Lock version + position: Position + protected: Protected + resource_type: Resource type + role_resource_permissions: Role resource permissions + roles: Roles + slug: Slug + slugs: Slugs + string_translations: String translations + target: Target + better_together/role: + identifier: Identifier + lock_version: Lock version + position: Position + protected: Protected + resource_permissions: Resource permissions + resource_type: Resource type + role_resource_permissions: Role resource permissions + slug: Slug + slugs: Slugs + string_translations: String translations + text_translations: Text translations + better_together/role_resource_permission: + lock_version: Lock version + resource_permission: :activerecord.models.resource_permission + role: :activerecord.models.role + better_together/user: + confirmation_sent_at: Confirmation sent at + confirmation_token: Confirmation token + confirmed_at: Confirmed at + current_sign_in_at: Current sign in at + current_sign_in_ip: Current sign in IP + email: Email + encrypted_password: Encrypted password + failed_attempts: Failed attempts + last_sign_in_at: Last sign in at + last_sign_in_ip: Last sign in IP + lock_version: Lock version + locked_at: Locked at + person: :activerecord.models.person + person_identification: :activerecord.models.person_identification + remember_created_at: Remember created at + reset_password_sent_at: Reset password sent at + reset_password_token: Reset password token + slugs: Slugs + string_translations: String translations + unconfirmed_email: Unconfirmed email + unlock_token: Unlock token + better_together/wizard: + current_completions: Current completions + first_completed_at: First completed at + identifier: Identifier + last_completed_at: Last completed at + lock_version: Lock version + max_completions: Max completions + protected: Protected + slug: Slug + slugs: Slugs + string_translations: String translations + success_message: Success message + success_path: Success path + text_translations: Text translations + wizard_step_definitions: Wizard step definitions + wizard_steps: Wizard steps + better_together/wizard_step: + completed: Completed + creator: :activerecord.models.creator + identifier: Identifier + lock_version: Lock version + step_number: Step number + wizard: :activerecord.models.wizard + wizard_step_definition: :activerecord.models.wizard_step_definition + better_together/wizard_step_definition: + form_class: Form class + identifier: Identifier + lock_version: Lock version + message: Message + protected: Protected + slug: Slug + slugs: Slugs + step_number: Step number + string_translations: String translations + template: Template + text_translations: Text translations + wizard: :activerecord.models.wizard + wizard_steps: Wizard steps + friendly_id/slug: + locale: Locale + lock_version: Lock version + scope: Scope + slug: Slug + sluggable: :activerecord.models.sluggable + sluggable_type: Sluggable type + mobility/backends/action_text/plain_text_translation: + body: Body + locale: Locale + name: Name + record: :activerecord.models.record + record_type: Record type + mobility/backends/action_text/rich_text_translation: + body: Body + embeds_attachments: Embeds attachments + embeds_blobs: Embeds blobs + locale: Locale + name: Name + record: :activerecord.models.record + record_type: Record type + mobility/backends/active_record/key_value/string_translation: + key: Key + locale: Locale + translatable: :activerecord.models.translatable + translatable_type: Translatable type + value: Value + mobility/backends/active_record/key_value/text_translation: + key: Key + locale: Locale + translatable: :activerecord.models.translatable + translatable_type: Translatable type + value: Value + noticed/event: + notifications: Notifications + params: Params + record: :activerecord.models.record + record_type: Record type + noticed/notification: + event: :activerecord.models.event + read_at: Read at + recipient: :activerecord.models.recipient + recipient_type: Recipient type + seen_at: Seen at + errors: + messages: + record_invalid: 'Validation failed: %{errors}' + restrict_dependent_destroy: + has_many: Cannot delete record because dependent %{record} exist + has_one: Cannot delete record because a dependent %{record} exists + models: + action_text/encrypted_rich_text: зашифрований багатий текст + action_text/rich_text: багатий текст + active_storage/attachment: вкладення + active_storage/blob: блоб + active_storage/variant_record: запис варіанта + application_notifier: Сповіщувач програми + application_notifier/notification: Сповіщення + better_together/community: Спільнота + better_together/content/block: Блок + better_together/content/css: Користувацький CSS + better_together/content/hero: Герой + better_together/content/html: HTML + better_together/content/image: Зображення + better_together/content/page_block: Блок сторінки + better_together/content/rich_text: Багатий текст + better_together/content/template: Файл шаблону + better_together/conversation: Розмова + better_together/conversation_participant: Учасник розмови + better_together/geography/continent: Континент + better_together/geography/country: Країна + better_together/geography/country_continent: Континент країни + better_together/geography/region: Регіон + better_together/geography/region_settlement: Поселення регіону + better_together/geography/settlement: Поселення + better_together/geography/state: Штат + better_together/guest_access: Гостьовий доступ + better_together/identification: Ідентифікація + better_together/infrastructure/building: Будівля + better_together/joatu/category: Категорія пропозицій та запитів + better_together/jwt_denylist: JWT чорний список + better_together/message: Повідомлення + better_together/navigation_area: Область навігації + better_together/navigation_item: Елемент навігації + better_together/new_message_notifier: Сповіщувач нових повідомлень + better_together/new_message_notifier/notification: сповіщувач нових повідомлень/сповіщення + better_together/page: Сторінка + better_together/person: Особа + better_together/person_community_membership: Членство особи в спільноті + better_together/person_platform_membership: Членство особи в платформі + better_together/platform: Платформа + better_together/platform_invitation: Запрошення платформи + better_together/resource_permission: Дозвіл ресурсу + better_together/role: Роль + better_together/role_resource_permission: Дозвіл ролі ресурсу + better_together/user: Користувач + better_together/wizard: Майстер + better_together/wizard_step: Крок майстра + better_together/wizard_step_definition: Визначення кроку майстра + friendly_id/slug: Слаг + mobility/backends/action_text/plain_text_translation: переклад звичайного тексту + mobility/backends/action_text/rich_text_translation: переклад багатого тексту + mobility/backends/active_record/key_value/string_translation: переклад рядка + mobility/backends/active_record/key_value/text_translation: переклад тексту + noticed/event: подія + noticed/notification: сповіщення + attributes: + privacy_list: + community: Спільнота + private: Приватний + public: Публічний + better_together: + addresses: + add: Додати адресу + address: Адреса + city_name: Місто + country_name: Країна + label: Мітка + labels: + billing: Рахунок + home: Дім + mailing: Поштова + main: Основна + other: Інша + physical: Фізична + shipping: Доставка + work: Робота + line1: Адреса рядок 1 + line2: Адреса рядок 2 + physical: Фізична + postal: Поштова + postal_code: Поштовий індекс + state_province_name: Штат/Провінція + title: Адреси + agreement_terms: + add: Додати + title: Умови угоди + agreements: + form: + page_help: Якщо встановлено, буде відображено вміст пов'язаної сторінки замість + умов угоди. + page_label: Пов'язана сторінка (необов'язково) + no_terms: No terms defined. + notice: + body: Please view the full agreement before accepting. + close: Close + title: View required agreement + participant: + pending: pending + show: + title: Agreement + terms: Terms + view: View + authorship_mailer: + authorship_changed_notification: + greeting: Hi %{recipient_name}, + intro_added: You've been added as an author to the page %{page}. + intro_added_by: "%{actor_name} added you as an author to the page %{page}." + intro_removed: You've been removed as an author from the page %{page}. + intro_removed_by: "%{actor_name} removed you as an author from the page %{page}." + signature_html: "— The %{platform} team" + subject_added: You've been added as an author to %{page} + subject_added_by: "%{actor_name} added you as an author to %{page}" + subject_removed: Your authorship was removed from %{page} + subject_removed_by: "%{actor_name} removed your authorship from %{page}" + view_page: View the page + view_page_link: Click here to view the page + buildings: + add: Add + title: Buildings + calendars: + default_description: Default calendar for %s + personal_calendar_name: "%{name}'s Personal Calendar" + calls_for_interest: + back_to_calls_for_interest: Back to calls for interest + hints: + description: Description + ends_at: Ends at + name: Name + slug: Slug + starts_at: Starts at + labels: + ends_at: Ends at + privacy: Privacy + starts_at: Starts at + none-yet: None-yet + save_call_for_interest: Save Call for Interest + tabs: + details: Details + images: Images + time-and-place: Time-and-place + view_call_for_interest: View call for interest + categories: + back_to_categories: Назад до категорій + index: + new_btn_text: Нова категорія + save_category: Зберегти категорію + view_category: Переглянути категорію + checklist_item: + update_failed: Помилка при оновленні елемента списку. + updated: Елемент списку оновлено. + checklist_items: + created: Елемент створено + description: Опис + edit_title: Редагувати елемент чек-листа + form_hint: Надайте коротку мітку та необов'язкові деталі. Використовуйте конфіденційність для контролю + хто бачить цей елемент. + hint_description: Необов'язкові деталі або інструкції для цього елемента. Підтримує форматування + багатого тексту. + hint_label: Короткий заголовок для елемента чек-листа (обов'язково). + hint_parent: 'Необов''язково: вкласти цей елемент під інший. Максимальна глибина вкладення становить 2.' + hint_privacy: 'Виберіть, хто може бачити цей елемент: Публічний або Приватний.' + label: Мітка + move_down: Перемістити вниз + move_up: Перемістити вгору + new_title: Новий елемент чек-листа + parent: Батьківський елемент (необов'язково) + reorder: Переупорядкувати елемент + sign_in_to_toggle: Увійдіть, щоб відмітити цей елемент як виконаний + untitled: Елемент без назви + checklists: + edit: + title: Редагувати чек-лист + index: + new: Новий чек-лист + title: Чек-листи + new: + title: Новий чек-лист + show: + completed_message: Чек-лист завершено + items_title: Елементи + no_items: Немає елементів для відображення + communities: + index: + new_btn_text: Нова спільнота + new: + title: Спільноти + none-yet: Поки що немає + show: + create_event: Створити подію + tabs: + buildings: Buildings + contact_details: Contact details + details: Details + images: Images + contact_details: + contact_information: Contact information + title: Contact Details + contactable: + addresses: Addresses + contact: Contact + contacts: Contacts + email_addresses: Email addresses + phone_numbers: Phone numbers + social_media_accounts: Social media accounts + website_links: Website links + contacts: + add: Add Contact + content: + blocks: + associated_pages: Associated Pages + conversation_mailer: + new_message_notification: + from_address: New message via %{from_address} + from_address_with_sender: "%{sender_name} via %{from_address}" + greeting: Hello %{recipient_name}, + message_intro: You have an unread message + message_intro_with_sender: You have an unread message from %{sender_name} + signature: Signature + signature_html: |- + Best regards,
+ The %{platform} Team + subject: Your conversation has an unread message + subject_with_title: "[%{conversation}] conversation has an unread message" + view_conversation: 'You can view and reply to this message by clicking the + link below:' + view_conversation_link: Go to conversation + conversations: + communicator: + active_conversations: Active Conversations + add_participants: Add Participants + conversations: Conversations + create_conversation: Create Conversation + last_message: Last message + new: New + new_conversation: New Conversation + conversation: + last_message: Last message + left: You have left the conversation %{conversation} + conversation_content: + edit_conversation: Редагувати розмову + leave_conversation: Покинути розмову + options_tooltip: Параметри розмови + empty: + no_messages: Повідомлень поки що немає. Чому б не почати розмову? + errors: + no_permitted_participants: Ви можете додавати лише менеджерів платформи або учасників, які + дали згоду на отримання повідомлень. + form: + add_participants: Додати учасників + create_conversation: Створити розмову + first_message: Перше повідомлення + first_message_hint: Напишіть відкриваюче повідомлення для розмови + participants_hint: Виберіть одну або кількох людей для включення до розмови + title_hint: Необов'язково. Короткий описовий заголовок може допомогти учасникам знайти + цю розмову пізніше + index: + conversations: Розмови + new: Нова + layout_with_sidebar: + active_conversations: Активні розмови + add_participants: Додати учасників + conversations: Розмови + create_conversation: Створити розмову + last_message: Останнє повідомлення + new: Нова + new_conversation: Нова розмова + list: + add_participants: Додати учасників + conversations: Розмови + create_conversation: Створити розмову + new: Нова + new_conversation: Нова розмова + new: + add_participants: Додати учасників + create_conversation: Створити розмову + new_conversation: Нова розмова + sidebar: + conversations: Розмови + new: Нова + device_permissions: + location: + denied: Відхилено + enabled: Увімкнено + unsupported: Не підтримується + status: + denied: Відхилено + granted: Надано + unknown: Невідомо + email_addresses: + add: Додати адресу електронної пошти + email: Електронна пошта + label: Label + labels: + other: Other + personal: Personal + school: School + work: Work + title: Email Addresses + event_invitations_mailer: + invite: + event_details: Event Details + greeting: Hello, + invited_by_html: You were invited by %{inviter_name}. + invited_html: You have been invited to the event %{event_name}. + need_account_html: You'll need to create an account to accept this invitation + and join the event. + review_invitation: Review Invitation + subject: You are invited to an event + when: When + where: Where + event_mailer: + event_reminder: + description: Description + duration: Duration + ends_at: Ends at + greeting: Hello %{recipient_name}, + hours: + one: "%{count} hour" + other: "%{count} hours" + location: Location + register_link: Register or RSVP + reminder_message: This is a friendly reminder about the upcoming event "%{event_name}". + signature_html: |- + Best regards,
+ The %{platform} Team + subject: 'Reminder: %{event_name}' + time_tbd: Time tbd + view_event_link_html: 'View event details: %{link}' + when: When + event_update: + changes_made: Changes made + current_details: Current details + description: Description + duration: Duration + ends_at: Ends at + greeting: Hello %{recipient_name}, + hours: + one: "%{count} hour" + other: "%{count} hours" + location: Location + register_link: Register or RSVP + signature_html: |- + Best regards,
+ The %{platform} Team + subject: 'Event updated: %{event_name}' + time_tbd: Time tbd + update_message_plural: The event "%{event_name}" has been updated with several + changes. + update_message_singular: The event "%{event_name}" has been updated. + view_event_link_html: 'View event details: %{link}' + when: When + events: + actions: + create_new_address: Create new address + create_new_building: Create new building + create_new_short: New + add_to_calendar: Add to calendar (.ics) + attendees: Attendees + back_to_events: View Events + greater_than_or_equal_to: must be greater than or equal to %{count} + hints: + create_permission_denied: Create permission denied + description: Description + duration_minutes: How long the event will last. Changes automatically when + start or end time changes. + ends_at: Ends at + location: Location + location_name: Location name + name: Name + select_address: Select address + select_building: Select building + slug: Slug + starts_at: Starts at + hosted_by: Hosted By + ics: + view_details_url: View details url + in: must be in %{count} + inclusion: is not included in the list + invalid: is not valid + labels: + duration_minutes: Duration + ends_at: Ends at + location: Location + location_name: Location name + location_type: Location type + privacy: Privacy + select_address: Select address + select_building: Select building + starts_at: Starts at + less_than: must be less than %{count} + less_than_or_equal_to: must be less than or equal to %{count} + location_types: + address: Address + building: Building + simple: Simple + login_required: Please log in to manage RSVPs. + model_invalid: 'Validation failed: %{errors}' + no_attendees: No attendees yet. + no_description_available: No description available. + none-yet: None-yet + not_a_number: is not a number + not_an_integer: must be an integer + not_found: could not be found + not_locked: was not locked + not_saved: + one: 'There was an error while saving %{resource}:' + other: 'There were %{count} errors while saving %{resource}:' + odd: must be odd + other_than: must be other than %{count} + placeholders: + location_name: Location name + present: must be blank + prompts: + select_address: Select address + select_building: Select building + register: Register + relationship: + calendar: Calendar event + created: Created by you + going: You're going + interested: You're interested + required: must exist + rsvp_cancel: Cancel RSVP + rsvp_cancelled: RSVP cancelled + rsvp_counts: 'Going: %{going} · Interested: %{interested}' + rsvp_going: Going + rsvp_interested: Interested + rsvp_not_available: RSVP is not available for this event. + rsvp_saved: RSVP saved + rsvp_unavailable_draft: RSVP will be available once this event is scheduled. + save_event: Save Event + tabs: + details: Details + images: Images + time-and-place: Time-and-place + taken: ya está en uso + time: + hour: hour + hours: hours + minutes: minutes + too_long: + one: es demasiado largo (%{count} carácter máximo) + other: es demasiado largo (%{count} caracteres máximo) + too_short: + one: es demasiado corto (%{count} carácter mínimo) + other: es demasiado corto (%{count} caracteres mínimo) + units: + minutes: minutes + view_event: View event + wrong_length: + one: no tiene la longitud correcta (%{count} carácter exactos) + other: no tiene la longitud correcta (%{count} caracteres exactos) + geography: + locatable_location: + errors: + no_location_source: Must specify either a name or location + regions: + index: + title: Regions + settlements: + index: + title: Settlements + global: + add: Add + hub: + activities: + back_to_hub: Back to Hub + page_title: All Activity + index: + activity: Activity + announcements: Announcements + new_members: New members + page_title: Community Hub + platform_updates: Platform Updates + recommended_communities: Recommended Communities + trending_topics: Trending topics + upcoming_events: Upcoming Events + view_all_activity: View all + recent_offers_card: + new_offer: New Offer + no_offers: No recent offers + recent_offers: Recent Offers + view_all_offers: View all + recent_requests_card: + new_request: New Request + no_requests: No recent requests + recent_requests: Recent Requests + view_all_requests: View all + suggested_matches_card: + for_your_latest_offer: For your latest offer + for_your_latest_request: For your latest request + no_suggestions: No suggestions yet — create an offer or request to see matches. + suggested_matches: Suggested Matches + infrastructure: + locations: Locations + invitations: + accept: Accept + confirm_remove: Are you sure you want to remove this invitation? + decline: Decline + event_name: 'Event:' + event_panel: + title: Invite People + external_user: External user + invite_by_email: Invite by Email + invite_person: Invite Member + invitee: Invitee + invitee_email: Email + invitee_type: Invitee Type + login_to_respond: Please log in to respond to your invitation. + pending: Pending Invitations + register_to_respond: Please register to respond to your invitation. + review: Invitation + search_people: Search for people... + select_person: Select Person + send_invite: Send Invitation + title: Event Invitations + type: + email: Email + person: Member + joatu: + agreements: + accept: Accept + create: + success: Success + index: + title: Agreements + offer: Offer + offer_creator: Offer Creator + participants: Participants + reject: Reject + request: Request + request_creator: Request Creator + show: + details: Details + title: Agreement + status: Status + by: by + categories: + index: + new_btn_text: New Category + help: + agreements: + index: Agreements connect one Offer and one Request. Use this list to review + or manage your agreements. + show: This page connects one Offer and one Request. If you are involved, + you can accept or reject to confirm the plan. + hide: Hide this help + offers: + form: Tell people what you can offer. A clear title and a few details go + a long way. Choose one or more categories so the right people can find + you. + index: Browse offers. Use search and filters to find what you need. Click + an offer to see details. + show: This is an offer. If it fits your needs, you can start a conversation + or create an agreement from a matching request. + requests: + form: Tell people what you need. A clear title and a few details help others + understand. Pick one or more categories so helpers can find you. + index: Browse requests. Use search and filters to find ways to help. Click + a request to see details. + show: This is a request. If it matches your offer, you can start a conversation + or create an agreement from a matching offer. + screenshot_alt: Help screenshot + show_again: Show help again + hub: + empty_agreements: No agreements yet. + empty_offers: No offers yet. + empty_requests: No requests yet. + my_agreements: My Agreements + my_offers: My Offers + my_requests: My Requests + sign_in_hint: Sign in to see your offers, requests, and agreements. + suggested: Suggested Matches + title: Exchange Hub + view_all_agreements: View all agreements + view_all_offers: View all offers + view_all_requests: View all requests + matches: + create_agreement: Create Agreement + matches_offer: 'Matches offer:' + matches_request: 'Matches request:' + none: No matches found right now. + title: Potential Matches + nav: + agreements: Agreements + hub: Hub + manage_categories: Manage Categories + offers: Offers + requests: Requests + offers: + agreement_heading: Agreement + agreement_with: Agreement with + back_to_offers: Back to Offers + edit_offer: Edit Offer + empty: No offers found + index_header: Offers + new_offer: New Offer + response_to_your_offer_heading: Response to your Offer + save_offer: Save Offer + source_request_heading: Responding to Request + view_agreement: View Agreement + view_offer: View Offer + your_response_heading: Your Response + requests: + agreement_heading: Agreement + back_to_requests: Back to Requests + edit_request: Edit Request + empty: No requests found + index_header: Requests + new_request: New Request + respond_with_offer: Respond with Offer + response_to_your_request_heading: Response to your Request + save_request: Save Request + source_offer_heading: Responding to Offer + view_agreement: View Agreement + view_request: View Request + your_response_heading: Your Response + respond_with_request: Respond with Request + response_to_your: Response to your %{type} + responses: + in_response_to: In response to + search: + labels: + filter_by_type: Filter by category + order_by: Order by + per_page: Per page + search-term: Search term + status: Status + submit: Search + newest: Newest + oldest: Oldest + placeholders: + search: Search by title or description + title: Search + status: + any: Any + closed: Closed + open: Open + type: + offer: Offer + request: Request + you: You + joatu_mailer: + agreement_created: + greeting: Hello %{recipient_name}, + message_intro: An agreement has been created between "%{offer_name}" and "%{request_name}". + signature: |- + Best regards, + The %{platform} Team + signature_html: |- + Best regards,
+ The %{platform} Team + subject: A new agreement was created + labelable: + custom-label-placeholder: Your custom label + loading: Loading... + mailer: + footer: + no_reply: This is an automated message. Please do not reply to this email. + messages: + form: + placeholder: Type your message... + send: Send + metrics: + shares: + invalid_parameters: Invalid parameters for share tracking. + shares_controller: + error_tracking: 'Error tracking share:' + failed_tracking: Failed to track share. + ga_not_initialized: Google Analytics not initialized. + unsupported_platform: 'Unsupported platform: %{platform}' + mobile_bar: + conversations: Conversations + navigation: Navigation + profile: Profile + models: + address_missing_type: Address must be physical, postal, or both. + ends_at_before_starts_at: must be after the start time + host_single: can only be set for a single record + protected_destroy: This record is protected and cannot be deleted. + navigation_areas: + edit: + title: Edit Navigation Area + index: + items: Items + new_navigation_area: New navigation area + new: + title: New Navigation Area + show: + new_navigation_item: New navigation item + navigation_items: + edit: + title: 'Edit Navigation Item: %{title}' + form: + dynamic_route: Dynamic Route + enter_nav_item_title: Enter the nav item title. + link_to_page: Link to Page + parent_item: Parent Item + index: + new_navigation_item: New Navigation Item + title: Navigation Items for %{name} + new: + title: New Navigation Item for %{name} + route_names: + calendars: Calendars + calls_for_interest: Calls for Interest + communities: Communities + content_blocks: Content Blocks + events: Events + geography_continents: Continents + geography_countries: Countries + geography_maps: Maps + geography_regions: Regions + geography_settlements: Settlements + geography_states: States + host_dashboard: Host Dashboard + joatu_offers: Offers + joatu_requests: Requests + metrics_reports: Metrics Reports + navigation_areas: Navigation Areas + pages: Pages + people: People + platforms: Platforms + resource_permissions: Resource Permissions + roles: Roles + users: Users + new_message_notifier: + from_html: 'From: %{sender}' + new_message: New Message + not_found: + description: The page you are looking for might have been removed, had its name + changed, or is temporarily unavailable. + title: 404 - Page Not Found + notifications: + event_invitation: + body: Invitation to %{event_name} + title: You have been invited to an event + event_reminder: + body_1h: "%{event_name} starts in 1 hour at %{starts_at}" + body_24h: "%{event_name} starts tomorrow at %{starts_at}" + body_generic: 'Reminder: %{event_name} starts at %{starts_at}' + title: 'Reminder: %{event_name}' + event_update: + body: "%{event_name} has been updated: %{changes}" + title: 'Event updated: %{event_name}' + index: + no_notifications: You have no notifications. + notifications: Notifications + joatu: + agreement_created: + content: An agreement between "%{offer}" and "%{request}" has been created. + title: Agreement created + mark_all_as_read: Mark all as read + mark_as_read: Mark as read + new: New + new_message: + content: "%{content}" + title: "%{sender}: %{conversation}" + no_notifications: No notifications + time_ago: "%{time} ago" + page_authorship_notifier: + added: You were added as an author on %{page_title} + added_by: "%{actor_name} added you as an author on %{page_title}" + removed: You were removed as an author on %{page_title} + removed_by: "%{actor_name} removed you as an author on %{page_title}" + pages: + form: + create_page_before_adding_content: Create page before adding content + index: + clear_filter: Clear filter + clear_filters: Clear Filters + filter: Filter + new_page: New page + search_by_slug: Search by slug... + search_by_title: Search by title... + people: + allow_messages_from_members: Allow messages from platform members + calendar: + attended: Attended + no_events: No events in calendar + no_events_description: Events will appear here when you RSVP as "Going" to + events. + no_past_events: No past events attended. + no_upcoming_events: No upcoming events. + recent_past_events: Recent Past Events + title: Personal Calendar + upcoming_events: Upcoming Events + device_permissions: + camera: Camera + location: Location + microphone: Microphone + notifications: Notifications + permissions_group_label: Permissions group label + index: + new_person: New person + notification_preferences: Message notification preferences + submit: + save: Save + tabs: + contact_details: Contact Details + details: Details + device-permissions: Device-permissions + images: Images + preferences: Preferences + person_block: + cannot_block_manager: no puede ser un administrador de la plataforma + cannot_block_self: no puedes bloquearte a ti mismo + person_blocks: + index: + actions: Actions + block_person: Block Person + blocked_at: Blocked + blocked_count: + one: 1 person blocked + other: "%{count} people blocked" + zero: No one blocked + blocked_on: Blocked on %{date} + no_blocked_people: You haven't blocked anyone yet. + no_blocked_people_description: When you block someone, they won't be able + to interact with you. + search: Search + search_placeholder: Search blocked people... + title: Blocked People + unblock: Unblock + unblock_confirmation: Are you sure you want to unblock this person? + new: + back_to_list: Back to Blocked People + blocked_person_help: Select a person you want to block from the search results. + blocked_person_label: Person to Block + blocked_person_placeholder: Search for a person to block... + blocked_person_prompt: Select a person to block + cancel: Cancel + submit: Block Person + title: Block Person + notices: + already_blocked: This person is already blocked. + blocked: Person has been blocked successfully. + not_found: Person not found. + platform_manager_error: Platform managers cannot be blocked. + self_block_error: You cannot block yourself. + unblocked: Person has been unblocked successfully. + phone_numbers: + add: Add Phone Number + label: Label + labels: + fax: Fax + home: Home + mobile: Mobile + other: Other + work: Work + number: Number + title: Phone Numbers + platform_invitation: + acceptance_html: By registering for a user account below, you are accepting + this invitation to join %{platform}. + community_role_html: You have been granted the %{role} role + in the %{platform} community. + greeting-placeholder: It was great to meet you recently. Here is the invitation + that I promised to send you! + hints: + type: Select the type of invitation. + platform_role_html: You have been granted the %{role} role + in the %{platform} platform. + title: You're invited to join %{platform} + platform_invitation_mailer: + invite: + greeting: Hello %{recipient_name}, + ignore: If you did not expect this invitation, please ignore this email. + invitation_code: 'Invitation code: %{invitation_code}' + link_text: Accept Invitation + message: 'You have been invited to join %{platform} on our platform. To accept + the invitation, please click the link below:' + signature_html: Best regards,
The %{platform} Team + subject: You're invited to join %{platform}! + valid_from: This invitation is valid from %{valid_from}. + valid_period: This invitation is valid from %{valid_from} until %{valid_until}. + platform_invitations: + create_new_invitation: Create New Invitation + filter_by_status: Filter by status + invitations: Invitations + new_invitation: New Invitation + search_by_email: Search by email + status: + accepted: Accepted + expired: Expired + pending: Pending + platforms: + show: + new_invitation: New invitation + posts: + authors: Authors + back_to_posts: Back to Posts + create_post: Create the first post + created_on: Created %{date} + created_on_short: Created + delete: Delete Record + edit: Edit Post + edit_post: Edit Post + index: + new_btn_text: New Post + labels: + privacy: Privacy + new_post: New Post + none_body: There are no posts to show right now. + none_title: No posts yet + published_on: Published %{date} + save_post: Save Post + updated_on_short: Updated + view_post: View Post + primary: Primary + registrations: + captcha_validation_failed: Security verification failed. Please try again. + remove: Remove + resource_permissions: + index: + new_resource_permission: New resource permission + roles: + index: + new_role: New role + search: + all: All + page_title: Search Results for "%{query}" + select: Select + settings: + index: + account: + coming_soon: Account settings are coming soon. + description: Update your account credentials and security settings. + title: Account Settings + blocked_people: + description: Manage users you have blocked from interacting with you. + title: Blocked People + navigation_aria_label: Settings navigation + personal: + coming_soon: Personal settings are coming soon. + description: Manage your personal information and preferences. + title: Personal Settings + platform: + coming_soon: Platform settings are coming soon. + description: Configure platform-wide settings and preferences. + title: Platform Settings + privacy: + coming_soon: Privacy settings are coming soon. + description: Control your privacy and data sharing preferences. + title: Privacy Settings + tabs: + account: Account + blocked_people: Blocked People + personal: Personal + platform: Platform + privacy: Privacy + title: Settings + share_buttons: + aria_label: Share on %{platform} + bluesky: Share on Bluesky + default_title: Check this out! + facebook: Share on Facebook + linkedin: Share on LinkedIn + pinterest: Pin + reddit: Share on Reddit + share: Share + whatsapp: Share on WhatsApp + shared: + translated_field_hints: + description: Short description (at least one locale must be filled) + description_html: Rich-text description (supports formatting; at least one + locale must be filled) + name: Enter a name (at least one locale must be filled) + slug: URL‑friendly identifier (unique across locales) + unpublished: Unpublished + social_media_accounts: + add: Add Social Media Account + handle: Handle + platform: Platform + title: Social Media Accounts + url: Url + website_links: + add: Add Website Link + labels: + about_us: About Us + blog: Blog + careers: Careers + community_page: Community Page + company_website: Company Website + contact_us: Contact us + documentation: Docs + donations: Donations + events: Events + faq: FAQ + forum: Forum + newsletter: Newsletter + other: Other + personal_website: Personal Website + portfolio: Portfolio + privacy_policy: Privacy Policy + product_page: Product Page + resume: Resume + services: Services + support: Support + terms_of_service: Terms of Service + title: Website Links + block: :activerecord.models.block + community: + create_failed: Create failed + created: Created + update_failed: Update failed + updated: Updated + content: Content + conversation: :activerecord.models.conversation + conversation_participants: Conversation participants + conversations: + errors: + no_permitted_participants: You can only add platform managers or members who + have opted in to receive messages. + date: + abbr_day_names: + - Нд + - Пн + - Вт + - Ср + - Чт + - Пт + - Сб + abbr_month_names: + - + - Січ + - Лют + - Бер + - Кві + - Тра + - Чер + - Лип + - Сер + - Вер + - Жов + - Лис + - Гру + day_names: + - Неділя + - Понеділок + - Вівторок + - Середа + - Четвер + - П'ятниця + - Субота + formats: + default: "%Y-%m-%d" + long: "%B %d, %Y" + short: "%b %d" + month_names: + - + - Січень + - Лютий + - Березень + - Квітень + - Травень + - Червень + - Липень + - Серпень + - Вересень + - Жовтень + - Листопад + - Грудень + order: + - :year + - :month + - :day + datetime: + distance_in_words: + about_x_hours: + one: близько години + other: близько %{count} годин + about_x_months: + one: близько місяця + other: близько %{count} місяців + about_x_years: + one: близько року + other: близько %{count} років + almost_x_years: + one: майже рік + other: майже %{count} років + half_a_minute: півхвилини + less_than_x_minutes: + one: менше хвилини + other: менше %{count} хвилин + less_than_x_seconds: + one: менше секунди + other: менше %{count} секунд + over_x_years: + one: понад рік + other: понад %{count} років + x_days: + one: день + other: "%{count} днів" + x_minutes: + one: хвилина + other: "%{count} хвилин" + x_months: + one: місяць + other: "%{count} місяців" + x_seconds: + one: секунда + other: "%{count} секунд" + x_years: + one: рік + other: "%{count} років" + prompts: + day: День + hour: Година + minute: Хвилина + month: Місяць + second: Секунда + year: Рік + devise: + confirmations: + confirmed: Вашу адресу електронної пошти було успішно підтверджено. + new: + email: + label: Електронна пошта + resend_confirmation_instructions: Повторно надіслати інструкції підтвердження + send_instructions: Ви отримаєте електронний лист з інструкціями щодо підтвердження + вашої адреси електронної пошти через кілька хвилин. + send_paranoid_instructions: Якщо ваша адреса електронної пошти є в нашій базі даних, ви + отримаєте електронний лист з інструкціями щодо підтвердження вашої адреси електронної пошти + через кілька хвилин. + failure: + already_authenticated: Ви вже увійшли до системи. + inactive: Ваш обліковий запис ще не активовано. + invalid: Неправильний %{authentication_keys} або пароль. + last_attempt: У вас є ще одна спроба до блокування вашого облікового запису. + locked: Ваш обліковий запис заблоковано. + not_found_in_database: Неправильний %{authentication_keys} або пароль. + timeout: Ваша сесія закінчилася. Будь ласка, увійдіть знову, щоб продовжити. + unauthenticated: Вам потрібно увійти або зареєструватися, щоб продовжити. + unconfirmed: Вам потрібно підтвердити свою адресу електронної пошти, щоб продовжити. + mailer: + confirmation_instructions: + action: Підтвердити мій обліковий запис + greeting: Ласкаво просимо %{recipient}! + instruction: 'Ви можете підтвердити електронну пошту свого облікового запису за посиланням нижче:' + subject: Інструкції підтвердження + email_changed: + greeting: Привіт %{recipient}! + message: Ми звертаємося до вас, щоб повідомити, що вашу електронну пошту було змінено + на %{email}. + message_unconfirmed: Ми звертаємося до вас, щоб повідомити, що вашу електронну пошту + змінюється на %{email}. + subject: Електронну пошту змінено + password_change: + greeting: Привіт %{recipient}! + message: Ми звертаємося до вас, щоб повідомити, що ваш пароль було змінено. + subject: Пароль змінено + reset_password_instructions: + action: Змінити мій пароль + greeting: Привіт %{recipient}! + instruction: Хтось запросив посилання для зміни вашого пароля. Ви можете + зробити це за посиланням нижче. + instruction_2: Якщо ви не запитували це, будь ласка, ігноруйте цей електронний лист. + instruction_3: Ваш пароль не зміниться, поки ви не перейдете за посиланням вище + і не створите новий. + subject: Інструкції скидання пароля + unlock_instructions: + action: Розблокувати мій обліковий запис + greeting: Привіт %{recipient}! + instruction: 'Натисніть посилання нижче, щоб розблокувати свій обліковий запис:' + message: Ваш обліковий запис було заблоковано через надмірну кількість невдалих + спроб входу. + subject: Інструкції розблокування + omniauth_callbacks: + failure: Could not authenticate you from %{kind} because "%{reason}". + success: Successfully authenticated from %{kind} account. + passwords: + edit: + change_my_password: Change my password + change_your_password: Change your password + confirm_new_password: Confirm new password + new_password: New password + new: + email: + help: Enter your email address to receive reset instructions. + label: Email + forgot_your_password: Forgot Your Password? + send_me_reset_password_instructions: Send me reset password instructions + no_token: You can't access this page without coming from a password reset email. + If you do come from a password reset email, please make sure you used the + full URL provided. + send_instructions: You will receive an email with instructions on how to reset + your password in a few minutes. + send_paranoid_instructions: If your email address exists in our database, you + will receive a password recovery link at your email address in a few minutes. + updated: Your password has been changed successfully. You are now signed in. + updated_not_active: Your password has been changed successfully. + registrations: + destroyed: Bye! Your account has been successfully cancelled. We hope to see + you again soon. + edit: + are_you_sure: Are you sure? + cancel_my_account: Cancel my account + currently_waiting_confirmation_for_email: 'Currently waiting confirmation + for: %{email}' + edit_again: Edit Account Details + leave_blank_if_you_don_t_want_to_change_it: leave blank if you don't want + to change it + title: Edit %{resource} + unhappy: Unhappy? + update: Update + we_need_your_current_password_to_confirm_your_changes: we need your current + password to confirm your changes + new: + agreements: + privacy_policy: I agree to the Privacy Policy + terms_of_service: I agree to the Terms of Service + agreements_must_accept: You must accept the Terms of Service and Privacy Policy + to continue. + agreements_required: You must accept the Privacy Policy and Terms of Service + captcha_validation_failed: Security verification failed. Please complete the + security check and try again. + code_of_conduct: + label: I agree to the Code of Conduct + email: + help: Please enter a valid email address. + label: Email + invitation_code: Invitation Code + invitation_code_help_html: The platform %{platform} requires + an invitation before you can register. If you have been invited, please + enter your invitation code in the field below to access the registration + form. + invitation_required: Invitation Required + password: + help: Use a strong password for your account security. + label: Password + password_confirmation: + help: Please confirm your password. + label: Password Confirmation + person: + description: Description + description_hint: Provide a brief description of yourself to help others + know you better. + identifier: Username + identifier_hint_html: Your identifier is a unique username that identifies + your profile on the %{platform} site. + name: Name + name_hint: Please provide your full name. + privacy_policy: + label: I agree to the Privacy Policy + profile_details: Profile Details + sign_up: Sign Up + submit: Submit + terms_of_service: + label: I agree to the Terms of Service + view: View + signed_up: Welcome! You have signed up successfully. + signed_up_but_inactive: You have signed up successfully. However, we could not + sign you in because your account is not yet activated. + signed_up_but_locked: You have signed up successfully. However, we could not + sign you in because your account is locked. + signed_up_but_unconfirmed: A message with a confirmation link has been sent + to your email address. Please follow the link to activate your account. + update_needs_confirmation: You updated your account successfully, but we need + to verify your new email address. Please check your email and follow the confirmation + link to confirm your new email address. + updated: Your account has been updated successfully. + updated_but_not_signed_in: Your account has been updated successfully, but since + your password was changed, you need to sign in again. + sessions: + already_signed_out: Signed out successfully. + new: + email: + help: Enter your email address to sign in. + label: Email + password: + help: Enter your password to sign in. + label: Password + toggle: Reveal/Hide password + remember_me: Remember me + sign_in: Sign In + signed_in: Signed in successfully. + signed_out: Signed out successfully. + shared: + links: + back: Back + didn_t_receive_confirmation_instructions: Didn't receive confirmation instructions? + didn_t_receive_unlock_instructions: Didn't receive unlock instructions? + forgot_your_password: Forgot your password? + sign_in: Log in + sign_in_with_provider: Sign in with %{provider} + sign_up: Sign up + minimum_password_length: "(%{count} characters minimum)" + unlocks: + new: + resend_unlock_instructions: Resend unlock instructions + send_instructions: You will receive an email with instructions for how to unlock + your account in a few minutes. + send_paranoid_instructions: If your account exists, you will receive an email + with instructions for how to unlock it in a few minutes. + unlocked: Your account has been unlocked successfully. Please sign in to continue. + errors: + format: "%{attribute} %{message}" + internal_server_error: + description: Вибачте, але щось пішло не так з нашого боку. + title: 500 - Внутрішня помилка сервера + messages: + accepted: має бути прийнято + already_confirmed: вже підтверджено, будь ласка, спробуйте увійти + blank: не може бути пустим + confirmation: не співпадає з %{attribute} + confirmation_period_expired: потрібно підтвердити протягом %{period}, будь ласка + запросіть новий + empty: не може бути порожнім + equal_to: має дорівнювати %{count} + even: має бути парним + exclusion: зарезервовано + expired: закінчився, будь ласка, запросіть новий + greater_than: має бути більше %{count} + greater_than_or_equal_to: має бути більше або дорівнювати %{count} + in: має бути в %{count} + inclusion: не включено до списку + invalid: неправильний + less_than: має бути менше %{count} + less_than_or_equal_to: має бути менше або дорівнювати %{count} + model_invalid: 'Перевірка не вдалася: %{errors}' + not_a_number: не є числом + not_an_integer: має бути цілим числом + not_found: не знайдено + not_locked: не було заблоковано + not_saved: + one: '1 помилка заборонила збереження цього %{resource}:' + other: "%{count} помилок заборонили збереження цього %{resource}:" + odd: має бути непарним + other_than: має бути іншим ніж %{count} + present: має бути порожнім + required: має існувати + taken: вже зайнято + too_long: + one: занадто довгий (максимум %{count} символ) + other: занадто довгий (максимум %{count} символів) + too_short: + one: занадто короткий (мінімум %{count} символ) + other: занадто короткий (мінімум %{count} символів) + weak_password: недостатньо сильний. Розгляньте додавання цифри, символів або більше + літер, щоб зробити його сильнішим. Уникайте використання слова 'password', свого дня народження, + або інших звичайних легко вгадуваних паролів. + wrong_length: + one: неправильна довжина (має бути %{count} символ) + other: неправильна довжина (має бути %{count} символів) + models: + address_missing_type: Address must be either physical, postal, or both. + ends_at_before_starts_at: must be after the start time + host_single: can only be set for one record + person_checklist_item: + directional_incomplete: Directional incomplete + protected_destroy: This record is protected and cannot be destroyed. + not_found: + description: The page you are looking for might have been removed, had its name + changed, or is temporarily unavailable. + title: 404 - Page Not Found + person_block: + cannot_block_manager: cannot be a platform manager + cannot_block_self: cannot block yourself + template: + body: 'There were problems with the following fields:' + header: + one: "%{count} error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + wizard: + max_completions: Maximum number of completions reached for this wizard and step + definition. + one_uncompleted: Only one uncompleted step per person is allowed. + step_limit: Number of completions for this step has reached the wizard's max + completions limit. + event: :activerecord.models.event + flash: + checklist_item: + update_failed: Failed to update checklist item. + updated: Checklist item updated. + generic: + created: "%{resource} was successfully created." + deleted: Deleted + destroyed: "%{resource} was successfully destroyed." + error_create: Error creating %{resource}. + error_remove: Failed to remove %{resource}. + queued: "%{resource} has been queued for sending." + removed: "%{resource} was successfully removed." + unauthorized: Unauthorized + updated: "%{resource} was successfully updated." + joatu: + agreement: + accepted: Agreement accepted + rejected: Agreement rejected + response_links: + offer_created: Offer created in response to request. + request_created: Request created in response to offer. + person_block: + blocked: Person was successfully blocked. + unblocked: Person was successfully unblocked. + globals: + accepted: Прийнято + actions: Дії + add_block: Додати блок + add_child_item: Додати дочірній елемент + add_member: Додати учасника + ago: тому + all: Все + are_you_sure: Ви впевнені + back: Назад + back_to_list: Назад до списку + cancel: Скасувати + clear: Очистити + clear_filter: Очистити фільтр + confirm_delete: Ви впевнені, що хочете видалити цей запис? + create: Створити + declined: Відхилено + delete: Видалити + destroy: Знищити + draft: Чернетка + edit: Редагувати + email: Електронна пошта + 'false': 'Ні' + forms: + save: Зберегти + from: Від + hidden: Приховано + loading: Завантаження... + locale: Локаль + message: Повідомлення + new: Новий + 'no': 'Ні' + no_description: Немає опису + no_image: Немає зображення + no_invitee: Немає запрошеного + none: Немає + not_sent: Не відправлено + pagination: + of: з + showing: Показано + to: до + pending: Очікує + platform_not_public: Платформа наразі приватна. Будь ласка, увійдіть для доступу. + published: Опубліковано + remove: Видалити + resend: Повторно надіслати + save: Зберегти + sent: Відправлено + show: Показати + status: Статус + tabs: + about: Про + attendees: Учасники + contact: Контакт + events: Події + invitations: Запрошення + members: Учасники + to: До + 'true': так + undo_clear: Скасувати очищення + update: Оновити + url: URL + view: Переглянути + visible: Видимий + 'yes': 'Так' + helpers: + close: Close + errors: + heading: Form Errors + prohibited: Prohibited + hint: + call_for_interest: + cover_image: Cover image + community: + cover_image: Cover image + logo: Logo + profile_image: Profile image + event: + cover_image: Cover image + images: + cover_image: Cover image + person: + allow_messages_from_members: Opt-in to receive messages from people other + than platform managers. + cover_image: Upload a cover image to display at the top of the profile. + description: Provide a brief description or biography. + locale: Select the preferred language for the person. + name: Enter the full name of the person. + notify_by_email: Send an email when a conversation has an unread message. + profile_image: Upload a profile image for the person. + show_conversation_details: Show conversation title and sender name in notifications. + slug: A URL-friendly identifier, typically auto-generated. + required_info: This field is required + label: + person: + cover_image: Cover Image + description: Description + name: Name + profile_image: Profile Image + slug: Slug + language_select: + prompt: Prompt + required_info: This field is required + select: + prompt: Please select + submit: + create: Create %{model} + submit: Save %{model} + update: Update %{model} + toolbar: + aria_label: Editor Toolbar + hints: + authors: + select_multiple: Select one or more authors + categories: + select_multiple: Select multiple + datetime_field: Choose a date and time for this field. + language_select: Select a language for this field. + privacy_field: Set the privacy level for this field. + required_field: This field is required. + resource: + type: Type + type_select_field: Select the type for this field. + host_dashboard: + index: + better_together: Better together + content: Content + geography: Geography + page_title: Host Dashboard + title: Host Dashboard + resource_card: + new_resource: New %{resource} + none_yet: None yet + total_resources: Total resources + view_all: View All + invitations: + calculating: Calculating + locales: + en: English + es: Español + fr: Français + uk: Українська + meta: + default_description: Welcome to %{platform_name} + page: + description_fallback: Read %{title} on %{platform_name} + metrics: + search_queries: + invalid_parameters: Invalid parameters for search tracking. + shares: + invalid_parameters: Invalid parameters + navbar: + accept_invitation: Accept invitation + conversations: Conversations + host_mgmt_tooltip: Host Management + locale_switcher_tooltip: Change Language + log_out: Log Out + my_profile: My Profile + notifications_tooltip: Notifications + search_tooltip: Search + settings: Settings + sign_in: Sign In + toggle_navigation: Toggle navigation + user_nav_tooltip: Account Menu + navigation: + header: + events: Події + exchange_hub: Центр обміну + navigation_item: + update_failed: Оновлення не вдалося + updated: Оновлено + 'no': 'Ні' + number: + currency: + format: + delimiter: "," + format: "%u%n" + precision: 2 + separator: "." + significant: false + strip_insignificant_zeros: false + unit: "$" + format: + delimiter: "," + precision: 3 + round_mode: default + separator: "." + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: "%n %u" + units: + billion: Billion + million: Million + quadrillion: Quadrillion + thousand: Thousand + trillion: Trillion + unit: '' + format: + delimiter: '' + precision: 3 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: Byte + other: Bytes + eb: EB + gb: GB + kb: KB + mb: MB + pb: PB + tb: TB + percentage: + format: + delimiter: '' + format: "%n%" + precision: + format: + delimiter: '' + og: + default_description: Welcome to %{platform_name} + default_title: "%{platform_name}" + page: + description_fallback: Read %{title} on %{platform_name} + title: "%{title} | %{platform_name}" + pages: + confirm_destroy: Confirm destroy + index: + clear_filter: Clear filter + clear_filters: Clear Filters + filter: Filter + search_by_title: Search by title... + partners: + confirm_delete: Confirm delete + people: + confirm_delete: Confirm delete + platform_invitations: + confirm_delete: Confirm delete + platforms: + confirm_delete: Confirm delete + pundit: + errors: + create: You are not authorized to create this %{resource}. + default: You are not authorized to perform this action on this %{resource}. + destroy: You are not authorized to delete this %{resource}. + edit: You are not authorized to edit this %{resource}. + index: You are not authorized to view the list of %{resource}. + leave_conversation: You are the final conversation participant and cannot leave + new: You are not authorized to create a new %{resource}. + show: You are not authorized to view this %{resource}. + update: You are not authorized to update this %{resource}. + resource_permissions: + confirm_destroy: Confirm destroy + resources: + block: Block + calendar: Calendar + checklist: Checklist + community: Community + continent: Continent + country: Country + download_failed: Download failed + invitation: Invitation + invitation_email: Invitation email + member: Member + navigation_area: Navigation area + page: Page + person: Person + person_platform_membership: Person platform membership + platform: Platform + profile: Profile + region: Region + region_settlement: Region settlement + report: Report + resource_permission: Resource permission + role: Role + settlement: Settlement + state: State + user: User + roles: + confirm_destroy: Confirm destroy + search: + button: Search + placeholder: Search... + shared: + by: by + links: + back: Back + simple_calendar: + next: Next + previous: Previous + today: Today + week: Week + support: + array: + last_word_connector: ", and " + two_words_connector: " and " + words_connector: ", " + time: + am: am + formats: + dashboard_resource: "%b %d, %Y %I:%M %p" + date_picker: Date picker + datetime_picker: Datetime picker + default: "%B %d, %Y %-I:%M %p" + event: "%B %d, %Y @ %-I:%M %p" + event_date_time: "%b %-d, %-I:%M %p" + event_date_time_with_year: "%b %-d, %Y %-I:%M %p" + long: "%B %d, %Y %-I:%M %p" + short: "%b %d %-I:%M %p" + time_only: "%-I:%M %p" + time_only_with_year: "%-I:%M %p %Y" + pm: pm + views: + buttons: + back: Back + create_report: Create Report + download: Download + go_home: Go to Home + new: New %{resource} + view_page: View Page + headers: + link_checker_reports: Link checker reports + link_click_reports: Link Click Reports + new_link_checker_report: New link checker report + new_link_click_report: New Link Click Report + new_page_view_report: New Page View Report + page_view_reports: Page View Reports + labels: + actions: Actions + all: All + created_at: Created At + external: External + file_format: File Format + filters: Filters + from_date: From Date + id: ID + internal: Internal + internal_filter: Internal Filter + no_file: No file + pageable_type_filter: Pageable Type Filter + please_correct: 'Please correct the following errors:' + protected: Protected + sort_by_total_clicks: Sort by Total Clicks + sort_by_total_views: Sort by Total Views + to_date: To Date + pagination: + aria_label: Pagination Navigation + current: "(current)" + first: First + last: Last + next: Next + previous: Попередня + showing_entries: Показано %{from} до %{to} із %{total} записів + truncate: "…" + 'yes': 'Так' From fdef9150b26a598b8a22f23f059dd667eede58c2 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 29 Oct 2025 14:49:51 -0230 Subject: [PATCH 03/12] Add more UK Translations --- config/locales/uk.yml | 80 +++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 91abf60a3..148f9bf35 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -80,52 +80,52 @@ uk: page_blocks: Блоки сторінки pages: Сторінки better_together/content/image: - accessibility_attributes: Accessibility attributes - content_settings: Content settings - css_settings: Css settings - data_attributes: Data attributes - html_attributes: Html attributes - identifier: Identifier - layout_settings: Layout settings - lock_version: Lock version + accessibility_attributes: Атрибути доступності + content_settings: Налаштування контенту + css_settings: Налаштування CSS + data_attributes: Атрибути даних + html_attributes: HTML атрибути + identifier: Ідентифікатор + layout_settings: Налаштування макета + lock_version: Версія блокування media_attachment: :activerecord.models.media_attachment media_blob: :activerecord.models.media_blob - media_settings: Media settings - page_blocks: Page blocks - pages: Pages - string_translations: String translations + media_settings: Налаштування медіа + page_blocks: Блоки сторінки + pages: Сторінки + string_translations: Переклади рядків better_together/content/page_block: block: :activerecord.models.block - lock_version: Lock version + lock_version: Версія блокування page: :activerecord.models.page - position: Position + position: Позиція better_together/content/rich_text: - accessibility_attributes: Accessibility attributes - content_settings: Content settings - css_settings: Css settings - data_attributes: Data attributes - html_attributes: Html attributes - identifier: Identifier - layout_settings: Layout settings - lock_version: Lock version - media_settings: Media settings - page_blocks: Page blocks - pages: Pages + accessibility_attributes: Атрибути доступності + content_settings: Налаштування контенту + css_settings: Налаштування CSS + data_attributes: Атрибути даних + html_attributes: HTML атрибути + identifier: Ідентифікатор + layout_settings: Налаштування макета + lock_version: Версія блокування + media_settings: Налаштування медіа + page_blocks: Блоки сторінки + pages: Сторінки rich_text_content: :activerecord.models.rich_text_content rich_text_content_en: :activerecord.models.rich_text_content_en rich_text_content_es: :activerecord.models.rich_text_content_es rich_text_content_fr: :activerecord.models.rich_text_content_fr - rich_text_translations: Rich text translations + rich_text_translations: Переклади багатого тексту better_together/conversation: - conversation_participants: Conversation participants + conversation_participants: Учасники розмови creator: :activerecord.models.creator - lock_version: Lock version - messages: Messages - participants: Participants - title: Conversation title + lock_version: Версія блокування + messages: Повідомлення + participants: Учасники + title: Заголовок розмови better_together/conversation_participant: conversation: :activerecord.models.conversation - lock_version: Lock version + lock_version: Версія блокування person: :activerecord.models.person better_together/geography/continent: community: :activerecord.models.community @@ -598,17 +598,17 @@ uk: page_help: Якщо встановлено, буде відображено вміст пов'язаної сторінки замість умов угоди. page_label: Пов'язана сторінка (необов'язково) - no_terms: No terms defined. + no_terms: Умови не визначено. notice: - body: Please view the full agreement before accepting. - close: Close - title: View required agreement + body: Будь ласка, перегляньте повну угоду перед прийняттям. + close: Закрити + title: Переглянути обов'язкову угоду participant: - pending: pending + pending: очікує show: - title: Agreement - terms: Terms - view: View + title: Угода + terms: Умови + view: Переглянути authorship_mailer: authorship_changed_notification: greeting: Hi %{recipient_name}, From c911193f224d7fb38adb468c017a64456601fe49 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 29 Oct 2025 15:03:36 -0230 Subject: [PATCH 04/12] Add more ukrainian translations --- config/locales/uk.yml | 318 +++++++++++++++++++++--------------------- 1 file changed, 158 insertions(+), 160 deletions(-) diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 148f9bf35..8bfcf8222 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -624,30 +624,30 @@ uk: view_page: View the page view_page_link: Click here to view the page buildings: - add: Add - title: Buildings + add: Додати + title: Будівлі calendars: - default_description: Default calendar for %s - personal_calendar_name: "%{name}'s Personal Calendar" + default_description: Календар за замовчуванням для %s + personal_calendar_name: "Особистий календар %{name}" calls_for_interest: - back_to_calls_for_interest: Back to calls for interest + back_to_calls_for_interest: Повернутися до закликів до зацікавленості hints: - description: Description - ends_at: Ends at - name: Name - slug: Slug - starts_at: Starts at + description: Опис + ends_at: Закінчується о + name: Ім'я + slug: Слаг + starts_at: Починається о labels: - ends_at: Ends at - privacy: Privacy - starts_at: Starts at - none-yet: None-yet - save_call_for_interest: Save Call for Interest + ends_at: Закінчується о + privacy: Конфіденційність + starts_at: Починається о + none-yet: Поки що немає + save_call_for_interest: Зберегти заклик до зацікавленості tabs: - details: Details - images: Images - time-and-place: Time-and-place - view_call_for_interest: View call for interest + details: Деталі + images: Зображення + time-and-place: Час і місце + view_call_for_interest: Переглянути заклик до зацікавленості categories: back_to_categories: Назад до категорій index: @@ -697,21 +697,21 @@ uk: show: create_event: Створити подію tabs: - buildings: Buildings - contact_details: Contact details - details: Details - images: Images + buildings: Будівлі + contact_details: Контактні деталі + details: Деталі + images: Зображення contact_details: - contact_information: Contact information - title: Contact Details + contact_information: Контактна інформація + title: Контактні деталі contactable: - addresses: Addresses - contact: Contact - contacts: Contacts - email_addresses: Email addresses - phone_numbers: Phone numbers - social_media_accounts: Social media accounts - website_links: Website links + addresses: Адреси + contact: Контакт + contacts: Контакти + email_addresses: Адреси електронної пошти + phone_numbers: Номери телефонів + social_media_accounts: Акаунти соціальних мереж + website_links: Посилання на веб-сайти contacts: add: Add Contact content: @@ -798,13 +798,13 @@ uk: email_addresses: add: Додати адресу електронної пошти email: Електронна пошта - label: Label + label: Мітка labels: - other: Other - personal: Personal - school: School - work: Work - title: Email Addresses + other: Інша + personal: Особиста + school: Школа + work: Робота + title: Адреси електронної пошти event_invitations_mailer: invite: event_details: Event Details @@ -1676,133 +1676,131 @@ uk: спроб входу. subject: Інструкції розблокування omniauth_callbacks: - failure: Could not authenticate you from %{kind} because "%{reason}". - success: Successfully authenticated from %{kind} account. + failure: Не вдалося автентифікувати вас з %{kind} через "%{reason}". + success: Успішно автентифіковано з облікового запису %{kind}. passwords: edit: - change_my_password: Change my password - change_your_password: Change your password - confirm_new_password: Confirm new password - new_password: New password + change_my_password: Змінити мій пароль + change_your_password: Змінити ваш пароль + confirm_new_password: Підтвердити новий пароль + new_password: Новий пароль new: email: - help: Enter your email address to receive reset instructions. - label: Email - forgot_your_password: Forgot Your Password? - send_me_reset_password_instructions: Send me reset password instructions - no_token: You can't access this page without coming from a password reset email. - If you do come from a password reset email, please make sure you used the - full URL provided. - send_instructions: You will receive an email with instructions on how to reset - your password in a few minutes. - send_paranoid_instructions: If your email address exists in our database, you - will receive a password recovery link at your email address in a few minutes. - updated: Your password has been changed successfully. You are now signed in. - updated_not_active: Your password has been changed successfully. + help: Введіть вашу адресу електронної пошти для отримання інструкцій зі скидання. + label: Електронна пошта + forgot_your_password: Забули свій пароль? + send_me_reset_password_instructions: Надіслати мені інструкції зі скидання пароля + no_token: Ви не можете отримати доступ до цієї сторінки, не перейшовши з електронного + листа для скидання пароля. Якщо ви перейшли з електронного листа для скидання + пароля, будь ласка, переконайтеся, що використали повну надану URL-адресу. + send_instructions: Ви отримаєте електронний лист з інструкціями щодо скидання + вашого пароля через кілька хвилин. + send_paranoid_instructions: Якщо ваша адреса електронної пошти є в нашій базі даних, ви + отримаєте посилання для відновлення пароля на вашу адресу електронної пошти через кілька хвилин. + updated: Ваш пароль було успішно змінено. Тепер ви увійшли до системи. + updated_not_active: Ваш пароль було успішно змінено. registrations: - destroyed: Bye! Your account has been successfully cancelled. We hope to see - you again soon. + destroyed: До побачення! Ваш обліковий запис було успішно скасовано. Сподіваємося + побачити вас знову найближчим часом. edit: - are_you_sure: Are you sure? - cancel_my_account: Cancel my account - currently_waiting_confirmation_for_email: 'Currently waiting confirmation - for: %{email}' - edit_again: Edit Account Details - leave_blank_if_you_don_t_want_to_change_it: leave blank if you don't want - to change it - title: Edit %{resource} - unhappy: Unhappy? - update: Update - we_need_your_current_password_to_confirm_your_changes: we need your current - password to confirm your changes + are_you_sure: Ви впевнені? + cancel_my_account: Скасувати мій обліковий запис + currently_waiting_confirmation_for_email: 'Очікує підтвердження для: %{email}' + edit_again: Редагувати деталі облікового запису + leave_blank_if_you_don_t_want_to_change_it: залиште порожнім, якщо не хочете + змінювати + title: Редагувати %{resource} + unhappy: Незадоволені? + update: Оновити + we_need_your_current_password_to_confirm_your_changes: нам потрібен ваш поточний + пароль для підтвердження змін new: agreements: - privacy_policy: I agree to the Privacy Policy - terms_of_service: I agree to the Terms of Service - agreements_must_accept: You must accept the Terms of Service and Privacy Policy - to continue. - agreements_required: You must accept the Privacy Policy and Terms of Service - captcha_validation_failed: Security verification failed. Please complete the - security check and try again. + privacy_policy: Я погоджуюся з Політикою конфіденційності + terms_of_service: Я погоджуюся з Умовами використання + agreements_must_accept: Ви повинні прийняти Умови використання та Політику конфіденційності + для продовження. + agreements_required: Ви повинні прийняти Політику конфіденційності та Умови використання + captcha_validation_failed: Помилка перевірки безпеки. Будь ласка, пройдіть перевірку + безпеки та спробуйте знову. code_of_conduct: - label: I agree to the Code of Conduct + label: Я погоджуюся з Кодексом поведінки email: - help: Please enter a valid email address. - label: Email - invitation_code: Invitation Code - invitation_code_help_html: The platform %{platform} requires - an invitation before you can register. If you have been invited, please - enter your invitation code in the field below to access the registration - form. - invitation_required: Invitation Required + help: Будь ласка, введіть дійсну адресу електронної пошти. + label: Електронна пошта + invitation_code: Код запрошення + invitation_code_help_html: Платформа %{platform} вимагає + запрошення перед реєстрацією. Якщо вас було запрошено, будь ласка, + введіть ваш код запрошення в поле нижче для доступу до форми реєстрації. + invitation_required: Потрібне запрошення password: - help: Use a strong password for your account security. - label: Password + help: Використовуйте сильний пароль для безпеки вашого облікового запису. + label: Пароль password_confirmation: - help: Please confirm your password. - label: Password Confirmation + help: Будь ласка, підтвердіть ваш пароль. + label: Підтвердження пароля person: - description: Description - description_hint: Provide a brief description of yourself to help others - know you better. - identifier: Username - identifier_hint_html: Your identifier is a unique username that identifies - your profile on the %{platform} site. - name: Name - name_hint: Please provide your full name. + description: Опис + description_hint: Надайте короткий опис себе, щоб допомогти іншим краще + вас пізнати. + identifier: Ім'я користувача + identifier_hint_html: Ваш ідентифікатор - це унікальне ім'я користувача, що ідентифікує + ваш профіль на сайті %{platform}. + name: Ім'я + name_hint: Будь ласка, надайте ваше повне ім'я. privacy_policy: - label: I agree to the Privacy Policy - profile_details: Profile Details - sign_up: Sign Up - submit: Submit + label: Я погоджуюся з Політикою конфіденційності + profile_details: Деталі профілю + sign_up: Зареєструватися + submit: Надіслати terms_of_service: - label: I agree to the Terms of Service - view: View - signed_up: Welcome! You have signed up successfully. - signed_up_but_inactive: You have signed up successfully. However, we could not - sign you in because your account is not yet activated. - signed_up_but_locked: You have signed up successfully. However, we could not - sign you in because your account is locked. - signed_up_but_unconfirmed: A message with a confirmation link has been sent - to your email address. Please follow the link to activate your account. - update_needs_confirmation: You updated your account successfully, but we need - to verify your new email address. Please check your email and follow the confirmation - link to confirm your new email address. - updated: Your account has been updated successfully. - updated_but_not_signed_in: Your account has been updated successfully, but since - your password was changed, you need to sign in again. + label: Я погоджуюся з Умовами використання + view: Переглянути + signed_up: Ласкаво просимо! Ви успішно зареєструвалися. + signed_up_but_inactive: Ви успішно зареєструвалися. Проте ми не змогли + авторизувати вас, тому що ваш обліковий запис ще не активовано. + signed_up_but_locked: Ви успішно зареєструвалися. Проте ми не змогли + авторизувати вас, тому що ваш обліковий запис заблоковано. + signed_up_but_unconfirmed: Повідомлення з посиланням підтвердження було надіслано + на вашу адресу електронної пошти. Будь ласка, перейдіть за посиланням для активації вашого облікового запису. + update_needs_confirmation: Ви успішно оновили свій обліковий запис, але нам потрібно + перевірити вашу нову адресу електронної пошти. Будь ласка, перевірте свою електронну пошту та перейдіть за посиланням + підтвердження, щоб підтвердити вашу нову адресу електронної пошти. + updated: Ваш обліковий запис було успішно оновлено. + updated_but_not_signed_in: Ваш обліковий запис було успішно оновлено, але оскільки + ваш пароль було змінено, вам потрібно увійти знову. sessions: - already_signed_out: Signed out successfully. + already_signed_out: Ви вже вийшли з системи. new: email: - help: Enter your email address to sign in. - label: Email + help: Введіть вашу адресу електронної пошти для входу. + label: Електронна пошта password: - help: Enter your password to sign in. - label: Password - toggle: Reveal/Hide password - remember_me: Remember me - sign_in: Sign In - signed_in: Signed in successfully. - signed_out: Signed out successfully. + help: Введіть ваш пароль для входу. + label: Пароль + toggle: Показати/Приховати пароль + remember_me: Запам'ятати мене + sign_in: Увійти + signed_in: Ви успішно увійшли до системи. + signed_out: Ви успішно вийшли з системи. shared: links: - back: Back - didn_t_receive_confirmation_instructions: Didn't receive confirmation instructions? - didn_t_receive_unlock_instructions: Didn't receive unlock instructions? - forgot_your_password: Forgot your password? - sign_in: Log in - sign_in_with_provider: Sign in with %{provider} - sign_up: Sign up - minimum_password_length: "(%{count} characters minimum)" + back: Назад + didn_t_receive_confirmation_instructions: Не отримали інструкції підтвердження? + didn_t_receive_unlock_instructions: Не отримали інструкції розблокування? + forgot_your_password: Забули свій пароль? + sign_in: Увійти + sign_in_with_provider: Увійти через %{provider} + sign_up: Зареєструватися + minimum_password_length: "(мінімум %{count} символів)" unlocks: new: - resend_unlock_instructions: Resend unlock instructions - send_instructions: You will receive an email with instructions for how to unlock - your account in a few minutes. - send_paranoid_instructions: If your account exists, you will receive an email - with instructions for how to unlock it in a few minutes. - unlocked: Your account has been unlocked successfully. Please sign in to continue. + resend_unlock_instructions: Повторно надіслати інструкції розблокування + send_instructions: Ви отримаєте електронний лист з інструкціями щодо розблокування + вашого облікового запису через кілька хвилин. + send_paranoid_instructions: Якщо ваш обліковий запис існує, ви отримаєте електронний лист + з інструкціями щодо його розблокування через кілька хвилин. + unlocked: Ваш обліковий запис було успішно розблоковано. Будь ласка, увійдіть для продовження. errors: format: "%{attribute} %{message}" internal_server_error: @@ -1853,30 +1851,30 @@ uk: one: неправильна довжина (має бути %{count} символ) other: неправильна довжина (має бути %{count} символів) models: - address_missing_type: Address must be either physical, postal, or both. - ends_at_before_starts_at: must be after the start time - host_single: can only be set for one record + address_missing_type: Адреса має бути фізичною, поштовою або обома. + ends_at_before_starts_at: має бути після часу початку + host_single: можна встановити тільки для одного запису person_checklist_item: - directional_incomplete: Directional incomplete - protected_destroy: This record is protected and cannot be destroyed. + directional_incomplete: Напрямок неповний + protected_destroy: Цей запис захищено і не може бути видалено. not_found: - description: The page you are looking for might have been removed, had its name - changed, or is temporarily unavailable. - title: 404 - Page Not Found + description: Сторінка, яку ви шукаєте, можливо, була видалена, змінила назву + або тимчасово недоступна. + title: 404 - Сторінку не знайдено person_block: - cannot_block_manager: cannot be a platform manager - cannot_block_self: cannot block yourself + cannot_block_manager: не може бути менеджером платформи + cannot_block_self: не можете заблокувати себе template: - body: 'There were problems with the following fields:' + body: 'Виникли проблеми з наступними полями:' header: - one: "%{count} error prohibited this %{model} from being saved" - other: "%{count} errors prohibited this %{model} from being saved" + one: "%{count} помилка заборонила збереження цієї %{model}" + other: "%{count} помилок заборонили збереження цієї %{model}" wizard: - max_completions: Maximum number of completions reached for this wizard and step - definition. - one_uncompleted: Only one uncompleted step per person is allowed. - step_limit: Number of completions for this step has reached the wizard's max - completions limit. + max_completions: Досягнуто максимальну кількість завершень для цього майстра + та визначення кроку. + one_uncompleted: Дозволено тільки один незавершений крок на людину. + step_limit: Кількість завершень для цього кроку досягла максимального + ліміту завершень майстра. event: :activerecord.models.event flash: checklist_item: From 3909290b96826270a798f1ebd6f9d69d797e8c95 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 29 Oct 2025 15:26:24 -0230 Subject: [PATCH 05/12] Update ukrainian translations --- config/locales/uk.yml | 638 +++++++++++++++++++++--------------------- 1 file changed, 319 insertions(+), 319 deletions(-) diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 8bfcf8222..177a52c35 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -129,201 +129,201 @@ uk: person: :activerecord.models.person better_together/geography/continent: community: :activerecord.models.community - countries: Countries - country_continents: Country continents - identifier: Identifier - lock_version: Lock version - protected: Protected - slug: Slug - slugs: Slugs - string_translations: String translations - text_translations: Text translations + countries: Країни + country_continents: Континенти країн + identifier: Ідентифікатор + lock_version: Версія блокування + protected: Захищено + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + text_translations: Переклади тексту better_together/geography/country: community: :activerecord.models.community - continents: Continents - country_continents: Country continents - identifier: Identifier - iso_code: Iso code - lock_version: Lock version - protected: Protected - slug: Slug - slugs: Slugs - states: States - string_translations: String translations - text_translations: Text translations + continents: Континенти + country_continents: Континенти країн + identifier: Ідентифікатор + iso_code: ISO код + lock_version: Версія блокування + protected: Захищено + slug: Слаг + slugs: Слаги + states: Штати + string_translations: Переклади рядків + text_translations: Переклади тексту better_together/geography/country_continent: continent: :activerecord.models.continent country: :activerecord.models.country - lock_version: Lock version + lock_version: Версія блокування better_together/geography/region: community: :activerecord.models.community country: :activerecord.models.country - identifier: Identifier - lock_version: Lock version - protected: Protected - region_settlements: Region settlements - settlements: Settlements - slug: Slug - slugs: Slugs + identifier: Ідентифікатор + lock_version: Версія блокування + protected: Захищено + region_settlements: Поселення регіону + settlements: Поселення + slug: Слаг + slugs: Слаги state: :activerecord.models.state - string_translations: String translations - text_translations: Text translations + string_translations: Переклади рядків + text_translations: Переклади тексту better_together/geography/region_settlement: - lock_version: Lock version - protected: Protected + lock_version: Версія блокування + protected: Захищено region: :activerecord.models.region settlement: :activerecord.models.settlement better_together/geography/settlement: community: :activerecord.models.community country: :activerecord.models.country - identifier: Identifier - lock_version: Lock version - protected: Protected - region_settlements: Region settlements - regions: Regions - slug: Slug - slugs: Slugs + identifier: Ідентифікатор + lock_version: Версія блокування + protected: Захищено + region_settlements: Поселення регіону + regions: Регіони + slug: Слаг + slugs: Слаги state: :activerecord.models.state - string_translations: String translations - text_translations: Text translations + string_translations: Переклади рядків + text_translations: Переклади тексту better_together/geography/state: community: :activerecord.models.community country: :activerecord.models.country - identifier: Identifier - iso_code: Iso code - lock_version: Lock version - protected: Protected - regions: Regions - settlements: Settlements - slug: Slug - slugs: Slugs - string_translations: String translations - text_translations: Text translations + identifier: Ідентифікатор + iso_code: ISO код + lock_version: Версія блокування + protected: Захищено + regions: Регіони + settlements: Поселення + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + text_translations: Переклади тексту better_together/identification: - active: Active + active: Активний agent: :activerecord.models.agent - agent_type: Agent type + agent_type: Тип агента identity: :activerecord.models.identity - identity_type: Identity type - lock_version: Lock version + identity_type: Тип ідентичності + lock_version: Версія блокування better_together/jwt_denylist: - exp: Exp - jti: Jti - lock_version: Lock version + exp: Закінчення + jti: JWT ідентифікатор + lock_version: Версія блокування better_together/message: - content: Content + content: Зміст conversation: :activerecord.models.conversation - lock_version: Lock version + lock_version: Версія блокування sender: :activerecord.models.sender better_together/navigation_area: - identifier: Identifier - lock_version: Lock version - name: Name + identifier: Ідентифікатор + lock_version: Версія блокування + name: Назва navigable: :activerecord.models.navigable - navigable_type: Navigable type - navigation_items: Navigation items - protected: Protected - slug: Slug - slugs: Slugs - string_translations: String translations - style: Style - visible: Visible + navigable_type: Тип навігації + navigation_items: Елементи навігації + protected: Захищено + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + style: Стиль + visible: Видимий better_together/navigation_item: - children: Children - icon: Icon - identifier: Identifier - item_type: Item type + children: Дочірні елементи + icon: Іконка + identifier: Ідентифікатор + item_type: Тип елемента linkable: :activerecord.models.linkable - linkable_type: Linkable type - lock_version: Lock version + linkable_type: Тип посилання + lock_version: Версія блокування navigation_area: :activerecord.models.navigation_area parent: :activerecord.models.parent - position: Position - protected: Protected - route_name: Route name - slug: Slug - slugs: Slugs - string_translations: String translations - url: Url - visible: Visible + position: Позиція + protected: Захищено + route_name: Назва маршруту + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + url: URL-адреса + visible: Видимий better_together/new_message_notifier: - notifications: Notifications - params: Params + notifications: Сповіщення + params: Параметри record: :activerecord.models.record - record_type: Record type + record_type: Тип запису better_together/new_message_notifier/notification: event: :activerecord.models.event - read_at: Read at + read_at: Прочитано о recipient: :activerecord.models.recipient - recipient_type: Recipient type - seen_at: Seen at + recipient_type: Тип одержувача + seen_at: Переглянуто о better_together/page: - blocks: Blocks - identifier: Identifier - image_blocks: Image blocks - keywords: Keywords - language: Language - layout: Layout - lock_version: Lock version - meta_description: Meta description - page_blocks: Page blocks - privacy: Privacy - protected: Protected - published: Published - published_at: Published at - rich_text_blocks: Rich text blocks + blocks: Блоки + identifier: Ідентифікатор + image_blocks: Блоки зображень + keywords: Ключові слова + language: Мова + layout: Макет + lock_version: Версія блокування + meta_description: Мета-опис + page_blocks: Блоки сторінки + privacy: Конфіденційність + protected: Захищено + published: Опубліковано + published_at: Опубліковано о + rich_text_blocks: Блоки багатого тексту rich_text_content: :activerecord.models.rich_text_content rich_text_content_en: :activerecord.models.rich_text_content_en rich_text_content_es: :activerecord.models.rich_text_content_es rich_text_content_fr: :activerecord.models.rich_text_content_fr - rich_text_translations: Rich text translations - slug: Slug - slugs: Slugs - string_translations: String translations - template: Template + rich_text_translations: Переклади багатого тексту + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + template: Шаблон better_together/page_authorship_notifier: - notifications: Notifications - params: Params + notifications: Сповіщення + params: Параметри record: :activerecord.models.record - record_type: Record type + record_type: Тип запису better_together/page_authorship_notifier/notification: event: :activerecord.models.event - read_at: Read at + read_at: Прочитано о recipient: :activerecord.models.recipient - recipient_type: Recipient type - seen_at: Seen at + recipient_type: Тип одержувача + seen_at: Переглянуто о better_together/person: - authorships: Authorships + authorships: Авторство community: :activerecord.models.community - community_roles: Community roles - conversation_participants: Conversation participants - conversations: Conversations - created_conversations: Created conversations - description: Description - identifications: Identifications - identifier: Identifier - locale: Language - lock_version: Lock version - member_communities: Member communities - member_platforms: Member platforms - name: Name - notification_mentions: Notification mentions - notifications: Notifications - notify_by_email: Receive email notifications - person_community_memberships: Person community memberships - person_platform_memberships: Person platform memberships - platform_roles: Platform roles - preferences: Preferences - show_conversation_details: Show conversation details - slug: Username - slugs: Slugs - string_translations: String translations - text_translations: Text translations + community_roles: Ролі в спільноті + conversation_participants: Учасники розмови + conversations: Розмови + created_conversations: Створені розмови + description: Опис + identifications: Ідентифікації + identifier: Ідентифікатор + locale: Мова + lock_version: Версія блокування + member_communities: Членські спільноти + member_platforms: Членські платформи + name: Ім'я + notification_mentions: Згадки в сповіщеннях + notifications: Сповіщення + notify_by_email: Отримувати сповіщення електронною поштою + person_community_memberships: Членство особи в спільноті + person_platform_memberships: Членство особи в платформі + platform_roles: Ролі на платформі + preferences: Налаштування + show_conversation_details: Показати деталі розмови + slug: Ім'я користувача + slugs: Слаги + string_translations: Переклади рядків + text_translations: Переклади тексту user: :activerecord.models.user user_identification: :activerecord.models.user_identification better_together/person_community_membership: joinable: :activerecord.models.joinable - lock_version: Lock version + lock_version: Версія блокування member: :activerecord.models.member role: :activerecord.models.role better_together/person_platform_membership: @@ -611,18 +611,18 @@ uk: view: Переглянути authorship_mailer: authorship_changed_notification: - greeting: Hi %{recipient_name}, - intro_added: You've been added as an author to the page %{page}. - intro_added_by: "%{actor_name} added you as an author to the page %{page}." - intro_removed: You've been removed as an author from the page %{page}. - intro_removed_by: "%{actor_name} removed you as an author from the page %{page}." - signature_html: "— The %{platform} team" - subject_added: You've been added as an author to %{page} - subject_added_by: "%{actor_name} added you as an author to %{page}" - subject_removed: Your authorship was removed from %{page} - subject_removed_by: "%{actor_name} removed your authorship from %{page}" - view_page: View the page - view_page_link: Click here to view the page + greeting: Привіт %{recipient_name}, + intro_added: Вас додано як автора до сторінки %{page}. + intro_added_by: "%{actor_name} додав вас як автора до сторінки %{page}." + intro_removed: Вас видалено як автора зі сторінки %{page}. + intro_removed_by: "%{actor_name} видалив вас як автора зі сторінки %{page}." + signature_html: "— Команда %{platform}" + subject_added: Вас додано як автора до %{page} + subject_added_by: "%{actor_name} додав вас як автора до %{page}" + subject_removed: Ваше авторство видалено з %{page} + subject_removed_by: "%{actor_name} видалив ваше авторство з %{page}" + view_page: Переглянути сторінку + view_page_link: Натисніть тут, щоб переглянути сторінку buildings: add: Додати title: Будівлі @@ -719,32 +719,32 @@ uk: associated_pages: Associated Pages conversation_mailer: new_message_notification: - from_address: New message via %{from_address} - from_address_with_sender: "%{sender_name} via %{from_address}" - greeting: Hello %{recipient_name}, - message_intro: You have an unread message - message_intro_with_sender: You have an unread message from %{sender_name} - signature: Signature + from_address: Нове повідомлення через %{from_address} + from_address_with_sender: "%{sender_name} через %{from_address}" + greeting: Привіт %{recipient_name}, + message_intro: У вас є непрочитане повідомлення + message_intro_with_sender: У вас є непрочитане повідомлення від %{sender_name} + signature: Підпис signature_html: |- - Best regards,
- The %{platform} Team - subject: Your conversation has an unread message - subject_with_title: "[%{conversation}] conversation has an unread message" - view_conversation: 'You can view and reply to this message by clicking the - link below:' - view_conversation_link: Go to conversation + З найкращими побажаннями,
+ Команда %{platform} + subject: У вашій розмові є непрочитане повідомлення + subject_with_title: "[%{conversation}] розмова має непрочитане повідомлення" + view_conversation: 'Ви можете переглянути та відповісти на це повідомлення, натиснувши + посилання нижче:' + view_conversation_link: Перейти до розмови conversations: communicator: - active_conversations: Active Conversations - add_participants: Add Participants - conversations: Conversations - create_conversation: Create Conversation - last_message: Last message - new: New - new_conversation: New Conversation + active_conversations: Активні розмови + add_participants: Додати учасників + conversations: Розмови + create_conversation: Створити розмову + last_message: Останнє повідомлення + new: Нова + new_conversation: Нова розмова conversation: - last_message: Last message - left: You have left the conversation %{conversation} + last_message: Останнє повідомлення + left: Ви покинули розмову %{conversation} conversation_content: edit_conversation: Редагувати розмову leave_conversation: Покинути розмову @@ -807,35 +807,35 @@ uk: title: Адреси електронної пошти event_invitations_mailer: invite: - event_details: Event Details - greeting: Hello, - invited_by_html: You were invited by %{inviter_name}. - invited_html: You have been invited to the event %{event_name}. - need_account_html: You'll need to create an account to accept this invitation - and join the event. - review_invitation: Review Invitation - subject: You are invited to an event - when: When - where: Where + event_details: Деталі події + greeting: Привіт, + invited_by_html: Вас запросив %{inviter_name}. + invited_html: Вас запрошено на подію %{event_name}. + need_account_html: Вам потрібно створити обліковий запис, щоб прийняти це запрошення + та приєднатися до події. + review_invitation: Переглянути запрошення + subject: Вас запрошують на подію + when: Коли + where: Де event_mailer: event_reminder: - description: Description - duration: Duration - ends_at: Ends at - greeting: Hello %{recipient_name}, + description: Опис + duration: Тривалість + ends_at: Закінчується о + greeting: Привіт %{recipient_name}, hours: - one: "%{count} hour" - other: "%{count} hours" - location: Location - register_link: Register or RSVP - reminder_message: This is a friendly reminder about the upcoming event "%{event_name}". + one: "%{count} година" + other: "%{count} годин" + location: Місце + register_link: Зареєструватися або RSVP + reminder_message: Це дружнє нагадування про майбутню подію "%{event_name}". signature_html: |- - Best regards,
- The %{platform} Team - subject: 'Reminder: %{event_name}' - time_tbd: Time tbd - view_event_link_html: 'View event details: %{link}' - when: When + З найкращими побажаннями,
+ Команда %{platform} + subject: 'Нагадування: %{event_name}' + time_tbd: Час буде оголошено + view_event_link_html: 'Переглянути деталі події: %{link}' + when: Коли event_update: changes_made: Changes made current_details: Current details @@ -868,85 +868,85 @@ uk: back_to_events: View Events greater_than_or_equal_to: must be greater than or equal to %{count} hints: - create_permission_denied: Create permission denied - description: Description - duration_minutes: How long the event will last. Changes automatically when - start or end time changes. - ends_at: Ends at - location: Location - location_name: Location name - name: Name - select_address: Select address - select_building: Select building - slug: Slug - starts_at: Starts at - hosted_by: Hosted By + create_permission_denied: Дозвіл на створення відхилено + description: Опис + duration_minutes: Скільки часу триватиме подія. Змінюється автоматично при + зміні часу початку або закінчення. + ends_at: Закінчується о + location: Місце + location_name: Назва місця + name: Назва + select_address: Вибрати адресу + select_building: Вибрати будівлю + slug: Слаг + starts_at: Починається о + hosted_by: Організатор ics: - view_details_url: View details url - in: must be in %{count} - inclusion: is not included in the list - invalid: is not valid + view_details_url: URL деталей перегляду + in: має бути в %{count} + inclusion: не включено до списку + invalid: недійсний labels: - duration_minutes: Duration - ends_at: Ends at - location: Location - location_name: Location name - location_type: Location type - privacy: Privacy - select_address: Select address - select_building: Select building - starts_at: Starts at - less_than: must be less than %{count} - less_than_or_equal_to: must be less than or equal to %{count} + duration_minutes: Тривалість + ends_at: Закінчується о + location: Місце + location_name: Назва місця + location_type: Тип місця + privacy: Конфіденційність + select_address: Вибрати адресу + select_building: Вибрати будівлю + starts_at: Починається о + less_than: має бути менше %{count} + less_than_or_equal_to: має бути менше або дорівнювати %{count} location_types: - address: Address - building: Building - simple: Simple - login_required: Please log in to manage RSVPs. - model_invalid: 'Validation failed: %{errors}' - no_attendees: No attendees yet. - no_description_available: No description available. - none-yet: None-yet - not_a_number: is not a number - not_an_integer: must be an integer - not_found: could not be found - not_locked: was not locked + address: Адреса + building: Будівля + simple: Простий + login_required: Будь ласка, увійдіть для управління RSVP. + model_invalid: 'Помилка валідації: %{errors}' + no_attendees: Учасників поки що немає. + no_description_available: Опис недоступний. + none-yet: Поки що немає + not_a_number: не є числом + not_an_integer: має бути цілим числом + not_found: не знайдено + not_locked: не було заблоковано not_saved: - one: 'There was an error while saving %{resource}:' - other: 'There were %{count} errors while saving %{resource}:' - odd: must be odd - other_than: must be other than %{count} + one: 'Виникла помилка під час збереження %{resource}:' + other: 'Виникло %{count} помилок під час збереження %{resource}:' + odd: має бути непарним + other_than: має бути іншим ніж %{count} placeholders: - location_name: Location name - present: must be blank + location_name: Назва місця + present: має бути порожнім prompts: - select_address: Select address - select_building: Select building - register: Register + select_address: Вибрати адресу + select_building: Вибрати будівлю + register: Зареєструватися relationship: - calendar: Calendar event - created: Created by you - going: You're going - interested: You're interested - required: must exist - rsvp_cancel: Cancel RSVP - rsvp_cancelled: RSVP cancelled - rsvp_counts: 'Going: %{going} · Interested: %{interested}' - rsvp_going: Going - rsvp_interested: Interested - rsvp_not_available: RSVP is not available for this event. - rsvp_saved: RSVP saved - rsvp_unavailable_draft: RSVP will be available once this event is scheduled. - save_event: Save Event + calendar: Подія календаря + created: Створено вами + going: Ви йдете + interested: Ви зацікавлені + required: має існувати + rsvp_cancel: Скасувати RSVP + rsvp_cancelled: RSVP скасовано + rsvp_counts: 'Йдуть: %{going} · Зацікавлені: %{interested}' + rsvp_going: Йду + rsvp_interested: Зацікавлений + rsvp_not_available: RSVP недоступне для цієї події. + rsvp_saved: RSVP збережено + rsvp_unavailable_draft: RSVP буде доступним після планування цієї події. + save_event: Зберегти подію tabs: - details: Details - images: Images - time-and-place: Time-and-place - taken: ya está en uso + details: Деталі + images: Зображення + time-and-place: Час і місце + taken: вже зайнято time: - hour: hour - hours: hours - minutes: minutes + hour: година + hours: годин + minutes: хвилин too_long: one: es demasiado largo (%{count} carácter máximo) other: es demasiado largo (%{count} caracteres máximo) @@ -1878,28 +1878,28 @@ uk: event: :activerecord.models.event flash: checklist_item: - update_failed: Failed to update checklist item. - updated: Checklist item updated. + update_failed: Не вдалося оновити елемент чек-листа. + updated: Елемент чек-листа оновлено. generic: - created: "%{resource} was successfully created." - deleted: Deleted - destroyed: "%{resource} was successfully destroyed." - error_create: Error creating %{resource}. - error_remove: Failed to remove %{resource}. - queued: "%{resource} has been queued for sending." - removed: "%{resource} was successfully removed." - unauthorized: Unauthorized - updated: "%{resource} was successfully updated." + created: "%{resource} було успішно створено." + deleted: Видалено + destroyed: "%{resource} було успішно знищено." + error_create: Помилка створення %{resource}. + error_remove: Не вдалося видалити %{resource}. + queued: "%{resource} поставлено в чергу для відправлення." + removed: "%{resource} було успішно видалено." + unauthorized: Неавторизовано + updated: "%{resource} було успішно оновлено." joatu: agreement: - accepted: Agreement accepted - rejected: Agreement rejected + accepted: Угоду прийнято + rejected: Угоду відхилено response_links: - offer_created: Offer created in response to request. - request_created: Request created in response to offer. + offer_created: Пропозицію створено у відповідь на запит. + request_created: Запит створено у відповідь на пропозицію. person_block: - blocked: Person was successfully blocked. - unblocked: Person was successfully unblocked. + blocked: Особу було успішно заблоковано. + unblocked: Особу було успішно розблоковано. globals: accepted: Прийнято actions: Дії @@ -2057,13 +2057,13 @@ uk: host_mgmt_tooltip: Host Management locale_switcher_tooltip: Change Language log_out: Log Out - my_profile: My Profile - notifications_tooltip: Notifications - search_tooltip: Search - settings: Settings - sign_in: Sign In - toggle_navigation: Toggle navigation - user_nav_tooltip: Account Menu + my_profile: Мій профіль + notifications_tooltip: Сповіщення + search_tooltip: Пошук + settings: Налаштування + sign_in: Увійти + toggle_navigation: Переключити навігацію + user_nav_tooltip: Меню облікового запису navigation: header: events: Події @@ -2093,11 +2093,11 @@ uk: decimal_units: format: "%n %u" units: - billion: Billion - million: Million - quadrillion: Quadrillion - thousand: Thousand - trillion: Trillion + billion: Мільярд + million: Мільйон + quadrillion: Квадрильйон + thousand: Тисяча + trillion: Трильйон unit: '' format: delimiter: '' @@ -2146,28 +2146,28 @@ uk: confirm_delete: Confirm delete pundit: errors: - create: You are not authorized to create this %{resource}. - default: You are not authorized to perform this action on this %{resource}. - destroy: You are not authorized to delete this %{resource}. - edit: You are not authorized to edit this %{resource}. - index: You are not authorized to view the list of %{resource}. - leave_conversation: You are the final conversation participant and cannot leave - new: You are not authorized to create a new %{resource}. - show: You are not authorized to view this %{resource}. - update: You are not authorized to update this %{resource}. + create: Вам не дозволено створювати цей %{resource}. + default: Вам не дозволено виконувати цю дію з цим %{resource}. + destroy: Вам не дозволено видаляти цей %{resource}. + edit: Вам не дозволено редагувати цей %{resource}. + index: Вам не дозволено переглядати список %{resource}. + leave_conversation: Ви є останнім учасником розмови і не можете її покинути + new: Вам не дозволено створювати новий %{resource}. + show: Вам не дозволено переглядати цей %{resource}. + update: Вам не дозволено оновлювати цей %{resource}. resource_permissions: confirm_destroy: Confirm destroy resources: - block: Block - calendar: Calendar - checklist: Checklist - community: Community - continent: Continent - country: Country - download_failed: Download failed - invitation: Invitation - invitation_email: Invitation email - member: Member + block: Блок + calendar: Календар + checklist: Чек-лист + community: Спільнота + continent: Континент + country: Країна + download_failed: Завантаження не вдалося + invitation: Запрошення + invitation_email: Електронна пошта запрошення + member: Учасник navigation_area: Navigation area page: Page person: Person From 084fcc606e76188bcd24aa7ffb15eef6c3c01419 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 29 Oct 2025 23:08:15 -0230 Subject: [PATCH 06/12] Improve coverage of ukranian translations --- config/locales/uk.yml | 700 +++++++++++++++++++++--------------------- 1 file changed, 350 insertions(+), 350 deletions(-) diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 177a52c35..704cf97bd 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -328,134 +328,134 @@ uk: role: :activerecord.models.role better_together/person_platform_membership: joinable: :activerecord.models.joinable - lock_version: Lock version + lock_version: Версія блокування member: :activerecord.models.member role: :activerecord.models.role better_together/platform: community: :activerecord.models.community - host: Host - identifier: Identifier - invitations: Invitations - lock_version: Lock version - person_members: Person members - person_platform_memberships: Person platform memberships - person_roles: Person roles - privacy: Privacy - protected: Protected - settings: Settings - slug: Slug - slugs: Slugs - string_translations: String translations - text_translations: Text translations - time_zone: Time zone - url: Url + host: Хост + identifier: Ідентифікатор + invitations: Запрошення + lock_version: Версія блокування + person_members: Учасники + person_platform_memberships: Членство осіб на платформі + person_roles: Ролі осіб + privacy: Конфіденційність + protected: Захищено + settings: Налаштування + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + text_translations: Переклади тексту + time_zone: Часовий пояс + url: URL-адреса better_together/platform_invitation: - accepted_at: Accepted at + accepted_at: Прийнято community_role: :activerecord.models.community_role - created_at: Created at + created_at: Створено invitable: :activerecord.models.invitable invitee: :activerecord.models.invitee - invitee_email: Invitee email + invitee_email: Електронна пошта запрошеного inviter: :activerecord.models.inviter - last_sent: Last sent - locale: Locale - lock_version: Lock version + last_sent: Останнє відправлення + locale: Мова + lock_version: Версія блокування platform_role: :activerecord.models.platform_role - session_duration_mins: Session duration (mins) - status: Status - token: Token - type: Type - valid_from: Valid from - valid_until: Valid until + session_duration_mins: Тривалість сесії (хв) + status: Статус + token: Токен + type: Тип + valid_from: Дійсний з + valid_until: Дійсний до better_together/resource_permission: - action: Action - identifier: Identifier - lock_version: Lock version - position: Position - protected: Protected - resource_type: Resource type - role_resource_permissions: Role resource permissions - roles: Roles - slug: Slug - slugs: Slugs - string_translations: String translations - target: Target + action: Дія + identifier: Ідентифікатор + lock_version: Версія блокування + position: Позиція + protected: Захищено + resource_type: Тип ресурсу + role_resource_permissions: Дозволи ролей для ресурсів + roles: Ролі + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + target: Ціль better_together/role: - identifier: Identifier - lock_version: Lock version - position: Position - protected: Protected - resource_permissions: Resource permissions - resource_type: Resource type - role_resource_permissions: Role resource permissions - slug: Slug - slugs: Slugs - string_translations: String translations - text_translations: Text translations + identifier: Ідентифікатор + lock_version: Версія блокування + position: Позиція + protected: Захищено + resource_permissions: Дозволи ресурсів + resource_type: Тип ресурсу + role_resource_permissions: Дозволи ролей для ресурсів + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + text_translations: Переклади тексту better_together/role_resource_permission: - lock_version: Lock version + lock_version: Версія блокування resource_permission: :activerecord.models.resource_permission role: :activerecord.models.role better_together/user: - confirmation_sent_at: Confirmation sent at - confirmation_token: Confirmation token - confirmed_at: Confirmed at - current_sign_in_at: Current sign in at - current_sign_in_ip: Current sign in IP - email: Email - encrypted_password: Encrypted password - failed_attempts: Failed attempts - last_sign_in_at: Last sign in at - last_sign_in_ip: Last sign in IP - lock_version: Lock version - locked_at: Locked at + confirmation_sent_at: Підтвердження надіслано о + confirmation_token: Токен підтвердження + confirmed_at: Підтверджено о + current_sign_in_at: Поточний вхід о + current_sign_in_ip: IP поточного входу + email: Електронна пошта + encrypted_password: Зашифрований пароль + failed_attempts: Невдалі спроби + last_sign_in_at: Останній вхід о + last_sign_in_ip: IP останнього входу + lock_version: Версія блокування + locked_at: Заблоковано о person: :activerecord.models.person person_identification: :activerecord.models.person_identification - remember_created_at: Remember created at - reset_password_sent_at: Reset password sent at - reset_password_token: Reset password token - slugs: Slugs - string_translations: String translations - unconfirmed_email: Unconfirmed email - unlock_token: Unlock token + remember_created_at: Запам'ятати створено о + reset_password_sent_at: Скидання пароля надіслано о + reset_password_token: Токен скидання пароля + slugs: Слаги + string_translations: Переклади рядків + unconfirmed_email: Непідтверджена електронна пошта + unlock_token: Токен розблокування better_together/wizard: - current_completions: Current completions - first_completed_at: First completed at - identifier: Identifier - last_completed_at: Last completed at - lock_version: Lock version - max_completions: Max completions - protected: Protected - slug: Slug - slugs: Slugs - string_translations: String translations - success_message: Success message - success_path: Success path - text_translations: Text translations - wizard_step_definitions: Wizard step definitions - wizard_steps: Wizard steps + current_completions: Поточні завершення + first_completed_at: Вперше завершено о + identifier: Ідентифікатор + last_completed_at: Останнє завершено о + lock_version: Версія блокування + max_completions: Максимальні завершення + protected: Захищено + slug: Слаг + slugs: Слаги + string_translations: Переклади рядків + success_message: Повідомлення про успіх + success_path: Шлях успіху + text_translations: Переклади тексту + wizard_step_definitions: Визначення кроків майстра + wizard_steps: Кроки майстра better_together/wizard_step: - completed: Completed + completed: Завершено creator: :activerecord.models.creator - identifier: Identifier - lock_version: Lock version - step_number: Step number + identifier: Ідентифікатор + lock_version: Версія блокування + step_number: Номер кроку wizard: :activerecord.models.wizard wizard_step_definition: :activerecord.models.wizard_step_definition better_together/wizard_step_definition: - form_class: Form class - identifier: Identifier - lock_version: Lock version - message: Message - protected: Protected - slug: Slug - slugs: Slugs - step_number: Step number - string_translations: String translations - template: Template - text_translations: Text translations + form_class: Клас форми + identifier: Ідентифікатор + lock_version: Версія блокування + message: Повідомлення + protected: Захищено + slug: Слаг + slugs: Слаги + step_number: Номер кроку + string_translations: Переклади рядків + template: Шаблон + text_translations: Переклади тексту wizard: :activerecord.models.wizard - wizard_steps: Wizard steps + wizard_steps: Кроки майстра friendly_id/slug: locale: Locale lock_version: Lock version @@ -1357,97 +1357,97 @@ uk: mobile: Mobile other: Other work: Work - number: Number - title: Phone Numbers + number: Номер + title: Номери телефонів platform_invitation: - acceptance_html: By registering for a user account below, you are accepting - this invitation to join %{platform}. - community_role_html: You have been granted the %{role} role - in the %{platform} community. - greeting-placeholder: It was great to meet you recently. Here is the invitation - that I promised to send you! + acceptance_html: Реєструючи обліковий запис користувача нижче, ви приймаєте + це запрошення приєднатися до %{platform}. + community_role_html: Вам надано роль %{role} + в спільноті %{platform}. + greeting-placeholder: Було приємно познайомитися з вами нещодавно. Ось запрошення, + яке я обіцяв вам надіслати! hints: - type: Select the type of invitation. - platform_role_html: You have been granted the %{role} role - in the %{platform} platform. - title: You're invited to join %{platform} + type: Виберіть тип запрошення. + platform_role_html: Вам надано роль %{role} + на платформі %{platform}. + title: Вас запрошують приєднатися до %{platform} platform_invitation_mailer: invite: - greeting: Hello %{recipient_name}, - ignore: If you did not expect this invitation, please ignore this email. - invitation_code: 'Invitation code: %{invitation_code}' - link_text: Accept Invitation - message: 'You have been invited to join %{platform} on our platform. To accept - the invitation, please click the link below:' - signature_html: Best regards,
The %{platform} Team - subject: You're invited to join %{platform}! - valid_from: This invitation is valid from %{valid_from}. - valid_period: This invitation is valid from %{valid_from} until %{valid_until}. + greeting: Привіт %{recipient_name}, + ignore: Якщо ви не очікували цього запрошення, будь ласка, проігноруйте цей електронний лист. + invitation_code: 'Код запрошення: %{invitation_code}' + link_text: Прийняти запрошення + message: 'Вас запрошено приєднатися до %{platform} на нашій платформі. Щоб прийняти + запрошення, будь ласка, натисніть посилання нижче:' + signature_html: З найкращими побажаннями,
Команда %{platform} + subject: Вас запрошують приєднатися до %{platform}! + valid_from: Це запрошення дійсне з %{valid_from}. + valid_period: Це запрошення дійсне з %{valid_from} до %{valid_until}. platform_invitations: - create_new_invitation: Create New Invitation - filter_by_status: Filter by status - invitations: Invitations - new_invitation: New Invitation - search_by_email: Search by email + create_new_invitation: Створити нове запрошення + filter_by_status: Фільтрувати за статусом + invitations: Запрошення + new_invitation: Нове запрошення + search_by_email: Пошук за електронною поштою status: - accepted: Accepted - expired: Expired - pending: Pending + accepted: Прийнято + expired: Закінчився термін + pending: Очікує platforms: show: - new_invitation: New invitation + new_invitation: Нове запрошення posts: - authors: Authors - back_to_posts: Back to Posts - create_post: Create the first post - created_on: Created %{date} - created_on_short: Created - delete: Delete Record - edit: Edit Post - edit_post: Edit Post + authors: Автори + back_to_posts: Назад до постів + create_post: Створити перший пост + created_on: Створено %{date} + created_on_short: Створено + delete: Видалити запис + edit: Редагувати пост + edit_post: Редагувати пост index: - new_btn_text: New Post + new_btn_text: Новий пост labels: - privacy: Privacy - new_post: New Post - none_body: There are no posts to show right now. - none_title: No posts yet - published_on: Published %{date} - save_post: Save Post - updated_on_short: Updated - view_post: View Post - primary: Primary + privacy: Конфіденційність + new_post: Новий пост + none_body: Наразі немає постів для показу. + none_title: Постів поки що немає + published_on: Опубліковано %{date} + save_post: Зберегти пост + updated_on_short: Оновлено + view_post: Переглянути пост + primary: Основний registrations: - captcha_validation_failed: Security verification failed. Please try again. - remove: Remove + captcha_validation_failed: Помилка перевірки безпеки. Будь ласка, спробуйте знову. + remove: Видалити resource_permissions: index: - new_resource_permission: New resource permission + new_resource_permission: Новий дозвіл ресурсу roles: index: - new_role: New role + new_role: Нова роль search: - all: All - page_title: Search Results for "%{query}" - select: Select + all: Все + page_title: Результати пошуку для "%{query}" + select: Вибрати settings: index: account: - coming_soon: Account settings are coming soon. - description: Update your account credentials and security settings. - title: Account Settings + coming_soon: Налаштування облікового запису з'являться незабаром. + description: Оновіть свої облікові дані та налаштування безпеки. + title: Налаштування облікового запису blocked_people: - description: Manage users you have blocked from interacting with you. - title: Blocked People - navigation_aria_label: Settings navigation + description: Керуйте користувачами, яких ви заблокували від взаємодії з вами. + title: Заблоковані люди + navigation_aria_label: Навігація налаштувань personal: - coming_soon: Personal settings are coming soon. - description: Manage your personal information and preferences. - title: Personal Settings + coming_soon: Особисті налаштування з'являться незабаром. + description: Керуйте своєю особистою інформацією та налаштуваннями. + title: Особисті налаштування platform: - coming_soon: Platform settings are coming soon. - description: Configure platform-wide settings and preferences. - title: Platform Settings + coming_soon: Налаштування платформи з'являться незабаром. + description: Налаштуйте загальноплатформні параметри та налаштування. + title: Налаштування платформи privacy: coming_soon: Privacy settings are coming soon. description: Control your privacy and data sharing preferences. @@ -1478,45 +1478,45 @@ uk: slug: URL‑friendly identifier (unique across locales) unpublished: Unpublished social_media_accounts: - add: Add Social Media Account - handle: Handle - platform: Platform - title: Social Media Accounts - url: Url + add: Додати акаунт соціальних мереж + handle: Нікнейм + platform: Платформа + title: Акаунти соціальних мереж + url: URL-адреса website_links: - add: Add Website Link + add: Додати посилання на веб-сайт labels: - about_us: About Us - blog: Blog - careers: Careers - community_page: Community Page - company_website: Company Website - contact_us: Contact us - documentation: Docs - donations: Donations - events: Events - faq: FAQ - forum: Forum - newsletter: Newsletter - other: Other - personal_website: Personal Website - portfolio: Portfolio - privacy_policy: Privacy Policy - product_page: Product Page - resume: Resume - services: Services - support: Support - terms_of_service: Terms of Service - title: Website Links + about_us: Про нас + blog: Блог + careers: Кар'єра + community_page: Сторінка спільноти + company_website: Веб-сайт компанії + contact_us: Зв'яжіться з нами + documentation: Документація + donations: Пожертви + events: Події + faq: ЧаПи + forum: Форум + newsletter: Розсилка + other: Інше + personal_website: Особистий веб-сайт + portfolio: Портфоліо + privacy_policy: Політика конфіденційності + product_page: Сторінка продукту + resume: Резюме + services: Послуги + support: Підтримка + terms_of_service: Умови використання + title: Посилання на веб-сайти block: :activerecord.models.block community: - create_failed: Create failed - created: Created - update_failed: Update failed - updated: Updated - content: Content + create_failed: Створення не вдалося + created: Створено + update_failed: Оновлення не вдалося + updated: Оновлено + content: Зміст conversation: :activerecord.models.conversation - conversation_participants: Conversation participants + conversation_participants: Учасники розмови conversations: errors: no_permitted_participants: You can only add platform managers or members who @@ -1966,91 +1966,91 @@ uk: visible: Видимий 'yes': 'Так' helpers: - close: Close + close: Закрити errors: - heading: Form Errors - prohibited: Prohibited + heading: Помилки форми + prohibited: Заборонено hint: call_for_interest: - cover_image: Cover image + cover_image: Зображення обкладинки community: - cover_image: Cover image - logo: Logo - profile_image: Profile image + cover_image: Зображення обкладинки + logo: Логотип + profile_image: Зображення профілю event: - cover_image: Cover image + cover_image: Зображення обкладинки images: - cover_image: Cover image + cover_image: Зображення обкладинки person: - allow_messages_from_members: Opt-in to receive messages from people other - than platform managers. - cover_image: Upload a cover image to display at the top of the profile. - description: Provide a brief description or biography. - locale: Select the preferred language for the person. - name: Enter the full name of the person. - notify_by_email: Send an email when a conversation has an unread message. - profile_image: Upload a profile image for the person. - show_conversation_details: Show conversation title and sender name in notifications. - slug: A URL-friendly identifier, typically auto-generated. - required_info: This field is required + allow_messages_from_members: Дозволити отримувати повідомлення від людей, окрім + менеджерів платформи. + cover_image: Завантажте зображення обкладинки для відображення у верхній частині профілю. + description: Надайте короткий опис або біографію. + locale: Виберіть бажану мову для особи. + name: Введіть повне ім'я особи. + notify_by_email: Надсилати електронний лист, коли в розмові є непрочитане повідомлення. + profile_image: Завантажте зображення профілю для особи. + show_conversation_details: Показувати назву розмови та ім'я відправника в сповіщеннях. + slug: URL-дружній ідентифікатор, зазвичай генерується автоматично. + required_info: Це поле обов'язкове label: person: - cover_image: Cover Image - description: Description - name: Name - profile_image: Profile Image - slug: Slug + cover_image: Зображення обкладинки + description: Опис + name: Ім'я + profile_image: Зображення профілю + slug: Слаг language_select: - prompt: Prompt - required_info: This field is required + prompt: Підказка + required_info: Це поле обов'язкове select: - prompt: Please select + prompt: Будь ласка, виберіть submit: - create: Create %{model} - submit: Save %{model} - update: Update %{model} + create: Створити %{model} + submit: Зберегти %{model} + update: Оновити %{model} toolbar: - aria_label: Editor Toolbar + aria_label: Панель інструментів редактора hints: authors: - select_multiple: Select one or more authors + select_multiple: Оберіть одного або кількох авторів categories: - select_multiple: Select multiple - datetime_field: Choose a date and time for this field. - language_select: Select a language for this field. - privacy_field: Set the privacy level for this field. - required_field: This field is required. + select_multiple: Оберіть кілька категорій + datetime_field: Оберіть дату та час для цього поля. + language_select: Оберіть мову для цього поля. + privacy_field: Встановіть рівень конфіденційності для цього поля. + required_field: Це поле є обов'язковим. resource: - type: Type - type_select_field: Select the type for this field. + type: Тип + type_select_field: Оберіть тип для цього поля. host_dashboard: index: - better_together: Better together - content: Content - geography: Geography - page_title: Host Dashboard - title: Host Dashboard + better_together: Разом краще + content: Контент + geography: Географія + page_title: Панель керування хоста + title: Панель керування хоста resource_card: - new_resource: New %{resource} - none_yet: None yet - total_resources: Total resources - view_all: View All + new_resource: Новий %{resource} + none_yet: Поки що немає + total_resources: Всього ресурсів + view_all: Переглянути всі invitations: - calculating: Calculating + calculating: Обчислення locales: en: English es: Español fr: Français uk: Українська meta: - default_description: Welcome to %{platform_name} + default_description: Ласкаво просимо на %{platform_name} page: - description_fallback: Read %{title} on %{platform_name} + description_fallback: Читайте %{title} на %{platform_name} metrics: search_queries: - invalid_parameters: Invalid parameters for search tracking. + invalid_parameters: Недійсні параметри для відстеження пошуку. shares: - invalid_parameters: Invalid parameters + invalid_parameters: Недійсні параметри navbar: accept_invitation: Accept invitation conversations: Conversations @@ -2108,14 +2108,14 @@ uk: format: "%n %u" units: byte: - one: Byte - other: Bytes - eb: EB - gb: GB - kb: KB - mb: MB - pb: PB - tb: TB + one: Байт + other: Байт + eb: ЕБ + gb: ГБ + kb: КБ + mb: МБ + pb: ПБ + tb: ТБ percentage: format: delimiter: '' @@ -2124,26 +2124,26 @@ uk: format: delimiter: '' og: - default_description: Welcome to %{platform_name} + default_description: Ласкаво просимо на %{platform_name} default_title: "%{platform_name}" page: - description_fallback: Read %{title} on %{platform_name} + description_fallback: Читайте %{title} на %{platform_name} title: "%{title} | %{platform_name}" pages: - confirm_destroy: Confirm destroy + confirm_destroy: Підтвердіть видалення index: - clear_filter: Clear filter - clear_filters: Clear Filters - filter: Filter - search_by_title: Search by title... + clear_filter: Очистити фільтр + clear_filters: Очистити фільтри + filter: Фільтр + search_by_title: Пошук за назвою... partners: - confirm_delete: Confirm delete + confirm_delete: Підтвердіть видалення people: - confirm_delete: Confirm delete + confirm_delete: Підтвердіть видалення platform_invitations: - confirm_delete: Confirm delete + confirm_delete: Підтвердіть видалення platforms: - confirm_delete: Confirm delete + confirm_delete: Підтвердіть видалення pundit: errors: create: Вам не дозволено створювати цей %{resource}. @@ -2156,7 +2156,7 @@ uk: show: Вам не дозволено переглядати цей %{resource}. update: Вам не дозволено оновлювати цей %{resource}. resource_permissions: - confirm_destroy: Confirm destroy + confirm_destroy: Підтвердіть видалення resources: block: Блок calendar: Календар @@ -2168,93 +2168,93 @@ uk: invitation: Запрошення invitation_email: Електронна пошта запрошення member: Учасник - navigation_area: Navigation area - page: Page - person: Person - person_platform_membership: Person platform membership - platform: Platform - profile: Profile - region: Region - region_settlement: Region settlement - report: Report - resource_permission: Resource permission - role: Role - settlement: Settlement - state: State - user: User + navigation_area: Область навігації + page: Сторінка + person: Особа + person_platform_membership: Членство особи на платформі + platform: Платформа + profile: Профіль + region: Регіон + region_settlement: Регіональне поселення + report: Звіт + resource_permission: Дозвіл ресурсу + role: Роль + settlement: Поселення + state: Штат/Область + user: Користувач roles: - confirm_destroy: Confirm destroy + confirm_destroy: Підтвердіть видалення search: - button: Search - placeholder: Search... + button: Пошук + placeholder: Пошук... shared: - by: by + by: автор links: - back: Back + back: Назад simple_calendar: - next: Next - previous: Previous - today: Today - week: Week + next: Наступний + previous: Попередній + today: Сьогодні + week: Тиждень support: array: - last_word_connector: ", and " - two_words_connector: " and " + last_word_connector: " та " + two_words_connector: " та " words_connector: ", " time: - am: am + am: дп formats: - dashboard_resource: "%b %d, %Y %I:%M %p" - date_picker: Date picker - datetime_picker: Datetime picker - default: "%B %d, %Y %-I:%M %p" - event: "%B %d, %Y @ %-I:%M %p" - event_date_time: "%b %-d, %-I:%M %p" - event_date_time_with_year: "%b %-d, %Y %-I:%M %p" - long: "%B %d, %Y %-I:%M %p" - short: "%b %d %-I:%M %p" - time_only: "%-I:%M %p" - time_only_with_year: "%-I:%M %p %Y" - pm: pm + dashboard_resource: "%d %b, %Y %H:%M" + date_picker: Вибір дати + datetime_picker: Вибір дати і часу + default: "%d %B %Y р. %-H:%M" + event: "%d %B %Y р. о %-H:%M" + event_date_time: "%d %b, %-H:%M" + event_date_time_with_year: "%d %b, %Y %-H:%M" + long: "%d %B %Y р. %-H:%M" + short: "%d %b %-H:%M" + time_only: "%-H:%M" + time_only_with_year: "%-H:%M %Y" + pm: пп views: buttons: - back: Back - create_report: Create Report - download: Download - go_home: Go to Home - new: New %{resource} - view_page: View Page + back: Назад + create_report: Створити звіт + download: Завантажити + go_home: На головну + new: Новий %{resource} + view_page: Переглянути сторінку headers: - link_checker_reports: Link checker reports - link_click_reports: Link Click Reports - new_link_checker_report: New link checker report - new_link_click_report: New Link Click Report - new_page_view_report: New Page View Report - page_view_reports: Page View Reports + link_checker_reports: Звіти перевірки посилань + link_click_reports: Звіти кліків по посиланням + new_link_checker_report: Новий звіт перевірки посилань + new_link_click_report: Новий звіт кліків по посиланням + new_page_view_report: Новий звіт переглядів сторінок + page_view_reports: Звіти переглядів сторінок labels: - actions: Actions - all: All - created_at: Created At - external: External - file_format: File Format - filters: Filters - from_date: From Date + actions: Дії + all: Всі + created_at: Створено + external: Зовнішній + file_format: Формат файлу + filters: Фільтри + from_date: З дати id: ID - internal: Internal - internal_filter: Internal Filter - no_file: No file - pageable_type_filter: Pageable Type Filter - please_correct: 'Please correct the following errors:' - protected: Protected - sort_by_total_clicks: Sort by Total Clicks - sort_by_total_views: Sort by Total Views - to_date: To Date + internal: Внутрішній + internal_filter: Внутрішній фільтр + no_file: Немає файлу + pageable_type_filter: Фільтр типу сторінки + please_correct: 'Будь ласка, виправте наступні помилки:' + protected: Захищений + sort_by_total_clicks: Сортувати за загальною кількістю кліків + sort_by_total_views: Сортувати за загальною кількістю переглядів + to_date: До дати pagination: - aria_label: Pagination Navigation - current: "(current)" - first: First - last: Last - next: Next + aria_label: Навігація пагінації + current: "(поточна)" + first: Перша + last: Остання + next: Наступна previous: Попередня showing_entries: Показано %{from} до %{to} із %{total} записів truncate: "…" From fe8e38b1f4810a0cdfdf7aae87a612957ff9b1ff Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 30 Oct 2025 01:02:37 -0230 Subject: [PATCH 07/12] Disable weak words for user password for now Auto-building the person was causing issues with registration race conditions --- app/models/better_together/user.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/models/better_together/user.rb b/app/models/better_together/user.rb index 242f35a16..d110fa803 100644 --- a/app/models/better_together/user.rb +++ b/app/models/better_together/user.rb @@ -42,7 +42,7 @@ def build_person(attributes = {}) # Define person_attributes method to get attributes of associated Person def person # Check if a Person object exists and return its attributes - super.present? ? super : build_person + super.present? ? super : build_person # this build_person call can cause issues in registration end # def person= arg @@ -68,10 +68,11 @@ def to_s email end - def weak_words - return unless person + # TODO: accessing person here was causing save issues in the registration process due the the autobuild + # def weak_words + # return [] unless person - [person.name, person.slug, person.identifier] - end + # [person.name, person.slug, person.identifier] + # end end end From 32908d35a2335eff835399d5e6f5b3a6280b9995 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 3 Nov 2025 21:23:15 -0330 Subject: [PATCH 08/12] Refactor password usage in tests and views - Updated password references in various test files to use a more secure password 'SecureTest123!@#' instead of 'password12345'. - Removed redundant default parameters in translation calls in offer list item views. - Added missing translation key for 'password' in locale files. - Ensured consistent password usage across all user creation and login scenarios in specs. --- .../joatu/offers/_offer_list_item.html.erb | 2 +- .../joatu/requests/_request_list_item.html.erb | 2 +- config/locales/en.yml | 1 + .../person_blocks_controller_spec.rb | 2 +- .../platform_invitations_controller_spec.rb | 2 +- .../users/registrations_controller_hook_spec.rb | 2 +- .../agreements/registration_consent_spec.rb | 10 +++++----- spec/features/checklist_create_appends_spec.rb | 2 +- .../features/checklist_person_completion_spec.rb | 2 +- spec/features/checklist_reorder_system_spec.rb | 2 +- spec/features/devise/registration_spec.rb | 2 +- .../platform_invitation_accept_spec.rb | 2 +- spec/features/notifications/mark_as_read_spec.rb | 2 +- .../mailers/better_together/event_mailer_spec.rb | 4 +++- .../better_together/conversation_policy_spec.rb | 6 +++--- .../checklist_items_reorder_spec.rb | 7 +++++-- spec/requests/better_together/checklists_spec.rb | 4 ++-- .../conversation_message_protection_spec.rb | 9 +++++---- .../conversations_create_with_message_spec.rb | 4 ++-- .../conversations_request_spec.rb | 2 +- .../event_invitation_token_processing_spec.rb | 2 +- .../better_together/event_invitations_spec.rb | 8 ++++---- .../better_together/events_controller_spec.rb | 2 +- .../better_together/joatu/agreements_spec.rb | 2 +- .../joatu/offers_aggregated_matches_spec.rb | 2 +- .../better_together/joatu/offers_spec.rb | 2 +- .../joatu/requests_aggregated_matches_spec.rb | 2 +- .../better_together/joatu/requests_spec.rb | 2 +- .../joatu/response_links_controller_spec.rb | 2 +- .../notifications_controller_spec.rb | 2 +- .../person_checklist_items_spec.rb | 4 ++-- ...atform_privacy_with_event_invitations_spec.rb | 10 +++++----- spec/support/automatic_test_configuration.rb | 16 ++++++++-------- .../better_together/capybara_feature_helpers.rb | 6 +++--- .../better_together/devise_session_helpers.rb | 6 +++--- spec/support/request_spec_helper.rb | 2 +- spec/support/test_seed_helpers.rb | 4 ++-- 37 files changed, 75 insertions(+), 68 deletions(-) diff --git a/app/views/better_together/joatu/offers/_offer_list_item.html.erb b/app/views/better_together/joatu/offers/_offer_list_item.html.erb index b1b5e3603..c96c5f70b 100644 --- a/app/views/better_together/joatu/offers/_offer_list_item.html.erb +++ b/app/views/better_together/joatu/offers/_offer_list_item.html.erb @@ -14,7 +14,7 @@ <% response_link = joatu_offer.response_links_as_response.find { |rl| rl.source&.creator == current_person } rescue nil %> <% if response_link.present? %> <% source_type_key = response_link.source_type.demodulize.underscore rescue 'item' %> - <%= t('better_together.joatu.response_to_your', type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize), default: "Response to your %{type}", type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize)) %> + <%= t('better_together.joatu.response_to_your', type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize)) %> <% end %> <% end %> diff --git a/app/views/better_together/joatu/requests/_request_list_item.html.erb b/app/views/better_together/joatu/requests/_request_list_item.html.erb index 661e1f470..e12f482e8 100644 --- a/app/views/better_together/joatu/requests/_request_list_item.html.erb +++ b/app/views/better_together/joatu/requests/_request_list_item.html.erb @@ -14,7 +14,7 @@ <% response_link = request_model.response_links_as_response.find { |rl| rl.source&.creator == current_person } rescue nil %> <% if response_link.present? %> <% source_type_key = response_link.source_type.demodulize.underscore rescue 'item' %> - <%= t('better_together.joatu.response_to_your', type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize), default: "Response to your %{type}", type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize)) %> + <%= t('better_together.joatu.response_to_your', type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize)) %> <% end %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index ffa3040ab..913a6932b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -409,6 +409,7 @@ en: last_sign_in_ip: Last sign in IP lock_version: Lock version locked_at: Locked at + password: Password person: :activerecord.models.person person_identification: :activerecord.models.person_identification remember_created_at: Remember created at diff --git a/spec/controllers/better_together/person_blocks_controller_spec.rb b/spec/controllers/better_together/person_blocks_controller_spec.rb index e72443ed4..3b07a885b 100644 --- a/spec/controllers/better_together/person_blocks_controller_spec.rb +++ b/spec/controllers/better_together/person_blocks_controller_spec.rb @@ -9,7 +9,7 @@ routes { BetterTogether::Engine.routes } let(:locale) { I18n.default_locale } - let(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) } + let(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) } let(:person) { user.person } let(:blocked_person) { create(:better_together_person) } let(:another_person) { create(:better_together_person) } diff --git a/spec/controllers/better_together/platform_invitations_controller_spec.rb b/spec/controllers/better_together/platform_invitations_controller_spec.rb index f3bd509da..ec0f36749 100644 --- a/spec/controllers/better_together/platform_invitations_controller_spec.rb +++ b/spec/controllers/better_together/platform_invitations_controller_spec.rb @@ -9,7 +9,7 @@ routes { BetterTogether::Engine.routes } let(:locale) { I18n.default_locale } - let(:user) { find_or_create_test_user('platform_manager@example.test', 'password12345', :platform_manager) } + let(:user) { find_or_create_test_user('platform_manager@example.test', 'SecureTest123!@#', :platform_manager) } let(:person) { user.person } let(:platform) { create(:better_together_platform, name: 'Test Platform') } let(:platform_slug) { platform.slug } diff --git a/spec/controllers/better_together/users/registrations_controller_hook_spec.rb b/spec/controllers/better_together/users/registrations_controller_hook_spec.rb index e62d8039d..e5d4237aa 100644 --- a/spec/controllers/better_together/users/registrations_controller_hook_spec.rb +++ b/spec/controllers/better_together/users/registrations_controller_hook_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe BetterTogether::Users::RegistrationsController, :skip_host_setup do +RSpec.describe BetterTogether::Users::RegistrationsController, :skip_host_setup, :user_registration do include BetterTogether::CapybaraFeatureHelpers routes { BetterTogether::Engine.routes } diff --git a/spec/features/agreements/registration_consent_spec.rb b/spec/features/agreements/registration_consent_spec.rb index ea6efe3dc..3d2f415c4 100644 --- a/spec/features/agreements/registration_consent_spec.rb +++ b/spec/features/agreements/registration_consent_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'User registration agreements', :as_platform_manager do +RSpec.describe 'User registration agreements', :as_platform_manager, :user_registration do let!(:privacy_agreement) { BetterTogether::Agreement.find_by!(identifier: 'privacy_policy') } # rubocop:enable RSpec/LetSetup # rubocop:todo RSpec/LetSetup @@ -13,8 +13,8 @@ visit new_user_registration_path(locale: I18n.default_locale) fill_in 'user[email]', with: 'test@example.test' - fill_in 'user[password]', with: 'password12345' - fill_in 'user[password_confirmation]', with: 'password12345' + fill_in 'user[password]', with: 'SecureTest123!@#' + fill_in 'user[password_confirmation]', with: 'SecureTest123!@#' fill_in 'user[person_attributes][name]', with: 'Test User' fill_in 'user[person_attributes][identifier]', with: 'testuser' fill_in 'user[person_attributes][description]', with: 'Tester' @@ -30,8 +30,8 @@ visit new_user_registration_path(locale: I18n.default_locale) fill_in 'user[email]', with: 'test@example.test' - fill_in 'user[password]', with: 'password12345' - fill_in 'user[password_confirmation]', with: 'password12345' + fill_in 'user[password]', with: 'SecureTest123!@#' + fill_in 'user[password_confirmation]', with: 'SecureTest123!@#' fill_in 'user[person_attributes][name]', with: 'Test User' fill_in 'user[person_attributes][identifier]', with: 'testuser' fill_in 'user[person_attributes][description]', with: 'Tester' diff --git a/spec/features/checklist_create_appends_spec.rb b/spec/features/checklist_create_appends_spec.rb index faa30845b..b231fe927 100644 --- a/spec/features/checklist_create_appends_spec.rb +++ b/spec/features/checklist_create_appends_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'Checklist item creation appends to bottom', :js do include ActionView::RecordIdentifier - let(:manager) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) } + let(:manager) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) } before do ensure_essential_data! diff --git a/spec/features/checklist_person_completion_spec.rb b/spec/features/checklist_person_completion_spec.rb index 5da732dc6..a4c62f388 100644 --- a/spec/features/checklist_person_completion_spec.rb +++ b/spec/features/checklist_person_completion_spec.rb @@ -9,7 +9,7 @@ let!(:person) { create(:better_together_person, user: user) } before do - find_or_create_test_user('user@example.test', 'password12345', :user) + find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) capybara_login_as_user end diff --git a/spec/features/checklist_reorder_system_spec.rb b/spec/features/checklist_reorder_system_spec.rb index 2b24c2aa0..5fccbf678 100644 --- a/spec/features/checklist_reorder_system_spec.rb +++ b/spec/features/checklist_reorder_system_spec.rb @@ -6,7 +6,7 @@ include ActionView::RecordIdentifier # Use the standard test manager credentials (password must meet length requirements) - let(:manager) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) } + let(:manager) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) } before do # Ensure essential data exists and log in diff --git a/spec/features/devise/registration_spec.rb b/spec/features/devise/registration_spec.rb index 2c989c407..98f868375 100644 --- a/spec/features/devise/registration_spec.rb +++ b/spec/features/devise/registration_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' # rubocop:disable Metrics/BlockLength -RSpec.feature 'User Registration' do +RSpec.feature 'User Registration', :user_registration do # Ensure you have a valid user created; using FactoryBot here let!(:host_setup_wizard) do BetterTogether::Wizard.find_or_create_by(identifier: 'host_setup') diff --git a/spec/features/invitations/platform_invitation_accept_spec.rb b/spec/features/invitations/platform_invitation_accept_spec.rb index 27fb05b31..c0d6d0102 100644 --- a/spec/features/invitations/platform_invitation_accept_spec.rb +++ b/spec/features/invitations/platform_invitation_accept_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'accepting a platform invitation' do +RSpec.describe 'accepting a platform invitation', :user_registration do let!(:invitation) do create(:better_together_platform_invitation, invitable: configure_host_platform) diff --git a/spec/features/notifications/mark_as_read_spec.rb b/spec/features/notifications/mark_as_read_spec.rb index 3847fcc9e..3d5f067f5 100644 --- a/spec/features/notifications/mark_as_read_spec.rb +++ b/spec/features/notifications/mark_as_read_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe 'message notifications', :as_user do - let(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) } + let(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) } let(:person) { user.person } let(:conversation) { create(:conversation, creator: person) } # let!(:conversation_participant) do diff --git a/spec/mailers/better_together/event_mailer_spec.rb b/spec/mailers/better_together/event_mailer_spec.rb index bd5b41134..697ad4bfd 100644 --- a/spec/mailers/better_together/event_mailer_spec.rb +++ b/spec/mailers/better_together/event_mailer_spec.rb @@ -41,7 +41,9 @@ module BetterTogether # rubocop:todo Metrics/ModuleLength context 'when event has location' do it 'includes location information' do - expect(mail.body.encoded).to include(event.location_display_name) + # HTML-encoded version of location name due to ERB escaping + expected_location = ERB::Util.html_escape(event.location_display_name) + expect(mail.body.encoded).to include(expected_location) end end diff --git a/spec/policies/better_together/conversation_policy_spec.rb b/spec/policies/better_together/conversation_policy_spec.rb index 09aa40c19..cedcbb50e 100644 --- a/spec/policies/better_together/conversation_policy_spec.rb +++ b/spec/policies/better_together/conversation_policy_spec.rb @@ -7,7 +7,7 @@ let!(:host_platform) { configure_host_platform } - let!(:manager_user) { create(:user, :confirmed, :platform_manager, password: 'password12345') } + let!(:manager_user) { create(:user, :confirmed, :platform_manager, password: 'SecureTest123!@#') } let!(:manager_person) { manager_user.person } let!(:opted_in_person) do @@ -26,7 +26,7 @@ end context 'when agent is a regular member' do - let!(:regular_user) { create(:user, :confirmed, password: 'password12345') } + let!(:regular_user) { create(:user, :confirmed, password: 'SecureTest123!@#') } it 'includes platform managers and opted-in members, but not non-opted members' do # rubocop:enable RSpec/MultipleExpectations @@ -39,7 +39,7 @@ end describe '#create? with participants kwarg' do - let(:regular_user) { create(:user, :confirmed, password: 'password12345') } + let(:regular_user) { create(:user, :confirmed, password: 'SecureTest123!@#') } let(:policy) { described_class.new(regular_user, BetterTogether::Conversation.new) } it 'allows create when user present and participants are permitted' do diff --git a/spec/requests/better_together/checklist_items_reorder_spec.rb b/spec/requests/better_together/checklist_items_reorder_spec.rb index 32c70971a..aee5d0649 100644 --- a/spec/requests/better_together/checklist_items_reorder_spec.rb +++ b/spec/requests/better_together/checklist_items_reorder_spec.rb @@ -3,11 +3,14 @@ require 'rails_helper' RSpec.describe 'ChecklistItems Reorder' do + include AutomaticTestConfiguration + let(:locale) { I18n.default_locale } - let(:platform_manager) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) } + let(:platform_manager) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) } before do - login(platform_manager.email, 'password12345') + configure_host_platform + login(platform_manager.email, 'SecureTest123!@#') end # rubocop:todo RSpec/MultipleExpectations diff --git a/spec/requests/better_together/checklists_spec.rb b/spec/requests/better_together/checklists_spec.rb index 81cff553e..32d2a64f4 100644 --- a/spec/requests/better_together/checklists_spec.rb +++ b/spec/requests/better_together/checklists_spec.rb @@ -59,11 +59,11 @@ # rubocop:todo RSpec/MultipleExpectations it 'allows creator to update their checklist' do # rubocop:todo RSpec/MultipleExpectations # rubocop:enable RSpec/MultipleExpectations - user = create(:better_together_user, :confirmed, password: 'password12345') + user = create(:better_together_user, :confirmed, password: 'SecureTest123!@#') checklist = create(:better_together_checklist, creator: user.person) # sign in as that user - login(user.email, 'password12345') + login(user.email, 'SecureTest123!@#') patch better_together.checklist_path(checklist, locale: I18n.default_locale), params: { checklist: { title_en: 'Creator Update' } } diff --git a/spec/requests/better_together/conversation_message_protection_spec.rb b/spec/requests/better_together/conversation_message_protection_spec.rb index 7a21ae7fb..e232308a0 100644 --- a/spec/requests/better_together/conversation_message_protection_spec.rb +++ b/spec/requests/better_together/conversation_message_protection_spec.rb @@ -11,11 +11,12 @@ configure_host_platform # Setup: create a manager user (owner of the conversation) and another user - manager_user = create(:user, :confirmed, :platform_manager, email: 'owner@example.test', password: 'password12345') - other_user = create(:user, :confirmed, email: 'attacker@example.test', password: 'password12345') + manager_user = create(:user, :confirmed, :platform_manager, email: 'owner@example.test', + password: 'SecureTest123!@#') + other_user = create(:user, :confirmed, email: 'attacker@example.test', password: 'SecureTest123!@#') # Create a conversation as the manager with a nested message - login(manager_user.email, 'password12345') + login(manager_user.email, 'SecureTest123!@#') post better_together.conversations_path(locale: I18n.default_locale), params: { conversation: { @@ -34,7 +35,7 @@ # Now sign in as other_user and attempt to change manager's message via PATCH logout - login(other_user.email, 'password12345') + login(other_user.email, 'SecureTest123!@#') patch better_together.conversation_path(conversation, locale: I18n.default_locale), params: { conversation: { diff --git a/spec/requests/better_together/conversations_create_with_message_spec.rb b/spec/requests/better_together/conversations_create_with_message_spec.rb index dd3a7bb54..bed471c3a 100644 --- a/spec/requests/better_together/conversations_create_with_message_spec.rb +++ b/spec/requests/better_together/conversations_create_with_message_spec.rb @@ -10,9 +10,9 @@ # Ensure the test user exists and is confirmed unless BetterTogether::User.find_by(email: 'user@example.test') create(:user, :confirmed, email: 'user@example.test', - password: 'password12345') + password: 'SecureTest123!@#') end - login('user@example.test', 'password12345') + login('user@example.test', 'SecureTest123!@#') end # rubocop:todo RSpec/MultipleExpectations diff --git a/spec/requests/better_together/conversations_request_spec.rb b/spec/requests/better_together/conversations_request_spec.rb index dd44dcbf0..76526e36c 100644 --- a/spec/requests/better_together/conversations_request_spec.rb +++ b/spec/requests/better_together/conversations_request_spec.rb @@ -6,7 +6,7 @@ include RequestSpecHelper let!(:manager_user) do - create(:user, :confirmed, :platform_manager, email: 'manager1@example.test', password: 'password12345') + create(:user, :confirmed, :platform_manager, email: 'manager1@example.test', password: 'SecureTest123!@#') end let!(:opted_in_person) do create(:better_together_person, preferences: { receive_messages_from_members: true }, name: 'Opted In User') diff --git a/spec/requests/better_together/event_invitation_token_processing_spec.rb b/spec/requests/better_together/event_invitation_token_processing_spec.rb index 5ec1e3796..0ea5851fa 100644 --- a/spec/requests/better_together/event_invitation_token_processing_spec.rb +++ b/spec/requests/better_together/event_invitation_token_processing_spec.rb @@ -11,7 +11,7 @@ existing end end - let!(:manager_user) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) } + let!(:manager_user) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) } let!(:event) do BetterTogether::Event.create!( diff --git a/spec/requests/better_together/event_invitations_spec.rb b/spec/requests/better_together/event_invitations_spec.rb index d127884ad..897177608 100644 --- a/spec/requests/better_together/event_invitations_spec.rb +++ b/spec/requests/better_together/event_invitations_spec.rb @@ -122,11 +122,11 @@ # Ensure user exists and logged in as regular user user = BetterTogether::User.find_by(email: 'user@example.test') || - create(:better_together_user, :confirmed, email: 'user@example.test', password: 'password12345') + create(:better_together_user, :confirmed, email: 'user@example.test', password: 'SecureTest123!@#') # Clear any existing session and login as the specific user logout if respond_to?(:logout) - login(user.email, 'password12345') + login(user.email, 'SecureTest123!@#') post better_together.accept_invitation_path(invitation.token, locale: locale) @@ -148,11 +148,11 @@ valid_from: Time.current) user = BetterTogether::User.find_by(email: 'user@example.test') || - create(:better_together_user, :confirmed, email: 'user@example.test', password: 'password12345') + create(:better_together_user, :confirmed, email: 'user@example.test', password: 'SecureTest123!@#') # Clear any existing session and login as the specific user logout if respond_to?(:logout) - login(user.email, 'password12345') + login(user.email, 'SecureTest123!@#') post better_together.decline_invitation_path(invitation.token, locale: locale) diff --git a/spec/requests/better_together/events_controller_spec.rb b/spec/requests/better_together/events_controller_spec.rb index 202f45ba1..8c4844ab0 100644 --- a/spec/requests/better_together/events_controller_spec.rb +++ b/spec/requests/better_together/events_controller_spec.rb @@ -87,7 +87,7 @@ describe 'RSVP actions' do let(:user_email) { 'manager@example.test' } - let(:password) { 'password12345' } + let(:password) { 'SecureTest123!@#' } let(:event) do BetterTogether::Event.create!(name: 'RSVP Test', starts_at: 1.day.from_now, identifier: SecureRandom.uuid) end diff --git a/spec/requests/better_together/joatu/agreements_spec.rb b/spec/requests/better_together/joatu/agreements_spec.rb index b97987f2f..a870fdbe4 100644 --- a/spec/requests/better_together/joatu/agreements_spec.rb +++ b/spec/requests/better_together/joatu/agreements_spec.rb @@ -4,7 +4,7 @@ # rubocop:disable Metrics/BlockLength RSpec.describe 'BetterTogether::Joatu::Agreements', :as_user do - let(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) } + let(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) } let(:person) { user.person } let(:offer) { create(:joatu_offer) } let(:request_record) { create(:joatu_request, creator: person) } diff --git a/spec/requests/better_together/joatu/offers_aggregated_matches_spec.rb b/spec/requests/better_together/joatu/offers_aggregated_matches_spec.rb index 765c82b5f..57de55d68 100644 --- a/spec/requests/better_together/joatu/offers_aggregated_matches_spec.rb +++ b/spec/requests/better_together/joatu/offers_aggregated_matches_spec.rb @@ -9,7 +9,7 @@ # Current authenticated user creating an offer current_user = BetterTogether::User.find_by(email: 'user@example.test') || FactoryBot.create(:better_together_user, :confirmed, - email: 'user@example.test', password: 'password12345') + email: 'user@example.test', password: 'SecureTest123!@#') my_person = current_user.person # Ensure both records share a category so they match diff --git a/spec/requests/better_together/joatu/offers_spec.rb b/spec/requests/better_together/joatu/offers_spec.rb index 4fb5c3864..885fec40f 100644 --- a/spec/requests/better_together/joatu/offers_spec.rb +++ b/spec/requests/better_together/joatu/offers_spec.rb @@ -4,7 +4,7 @@ # rubocop:disable Metrics/BlockLength RSpec.describe 'BetterTogether::Joatu::Offers', :as_user do - let(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) } + let(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) } let(:person) { user.person } let(:category) { create(:better_together_joatu_category) } let(:valid_attributes) do diff --git a/spec/requests/better_together/joatu/requests_aggregated_matches_spec.rb b/spec/requests/better_together/joatu/requests_aggregated_matches_spec.rb index 45bfa4f0e..f53afbae4 100644 --- a/spec/requests/better_together/joatu/requests_aggregated_matches_spec.rb +++ b/spec/requests/better_together/joatu/requests_aggregated_matches_spec.rb @@ -9,7 +9,7 @@ # Current authenticated user creating a request current_user = BetterTogether::User.find_by(email: 'user@example.test') || FactoryBot.create(:better_together_user, :confirmed, - email: 'user@example.test', password: 'password12345') + email: 'user@example.test', password: 'SecureTest123!@#') my_person = current_user.person # Ensure both records share a category so they match diff --git a/spec/requests/better_together/joatu/requests_spec.rb b/spec/requests/better_together/joatu/requests_spec.rb index 7331d3536..9ab4227c7 100644 --- a/spec/requests/better_together/joatu/requests_spec.rb +++ b/spec/requests/better_together/joatu/requests_spec.rb @@ -7,7 +7,7 @@ include AutomaticTestConfiguration let(:locale) { I18n.default_locale } - let(:person) { find_or_create_test_user('user@example.test', 'password12345', :user).person } + let(:person) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user).person } let(:category) { create(:better_together_joatu_category) } let(:valid_attributes) do { name: 'New Request', description: 'Request description', creator_id: person.id, diff --git a/spec/requests/better_together/joatu/response_links_controller_spec.rb b/spec/requests/better_together/joatu/response_links_controller_spec.rb index 99dadc127..ce3a7a892 100644 --- a/spec/requests/better_together/joatu/response_links_controller_spec.rb +++ b/spec/requests/better_together/joatu/response_links_controller_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe BetterTogether::Joatu::ResponseLinksController, :as_user do - let(:user) { create(:user, :confirmed, password: 'password12345') } + let(:user) { create(:user, :confirmed, password: 'SecureTest123!@#') } let(:person) { user.person } let(:offer) { create(:better_together_joatu_offer, creator: person) } let(:request_resource) { create(:better_together_joatu_request) } diff --git a/spec/requests/better_together/notifications_controller_spec.rb b/spec/requests/better_together/notifications_controller_spec.rb index 6c07dc624..8f87a39db 100644 --- a/spec/requests/better_together/notifications_controller_spec.rb +++ b/spec/requests/better_together/notifications_controller_spec.rb @@ -5,7 +5,7 @@ module BetterTogether # rubocop:todo Metrics/ModuleLength RSpec.describe NotificationsController, :as_user do let!(:notification) { create(:noticed_notification, recipient: person) } - let!(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) } + let!(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) } let(:person) { user.person } describe 'GET #index' do diff --git a/spec/requests/better_together/person_checklist_items_spec.rb b/spec/requests/better_together/person_checklist_items_spec.rb index 733d1110c..6fa54af03 100644 --- a/spec/requests/better_together/person_checklist_items_spec.rb +++ b/spec/requests/better_together/person_checklist_items_spec.rb @@ -13,8 +13,8 @@ before do configure_host_platform # Use project's HTTP login helper to satisfy route constraints - test_user = find_or_create_test_user(user.email, 'password12345', :user) - login(test_user.email, 'password12345') + test_user = find_or_create_test_user(user.email, 'SecureTest123!@#', :user) + login(test_user.email, 'SecureTest123!@#') end # rubocop:todo RSpec/MultipleExpectations diff --git a/spec/requests/better_together/platform_privacy_with_event_invitations_spec.rb b/spec/requests/better_together/platform_privacy_with_event_invitations_spec.rb index de42a5951..6c3fc6d48 100644 --- a/spec/requests/better_together/platform_privacy_with_event_invitations_spec.rb +++ b/spec/requests/better_together/platform_privacy_with_event_invitations_spec.rb @@ -7,8 +7,8 @@ let(:locale) { I18n.default_locale } let!(:platform) { configure_host_platform } - let!(:manager_user) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) } - let!(:regular_user) { find_or_create_test_user('user@example.test', 'password12345', :user) } + let!(:manager_user) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) } + let!(:regular_user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) } let!(:private_event) do create(:better_together_event, @@ -161,8 +161,8 @@ post user_registration_path(locale: locale), params: { user: { email: invitation.invitee_email, - password: 'password12345', - password_confirmation: 'password12345', + password: 'SecureTest123!@#', + password_confirmation: 'SecureTest123!@#', person_attributes: { name: 'New User', identifier: 'newuser' @@ -178,7 +178,7 @@ created_user = BetterTogether::User.find_by(email: invitation.invitee_email) created_user.confirm - login(invitation.invitee_email, 'password12345') + login(invitation.invitee_email, 'SecureTest123!@#') # Should redirect to the event after successful registration. Compare by slug to avoid locale path differences expect(response.request.fullpath).to include(event.slug) diff --git a/spec/support/automatic_test_configuration.rb b/spec/support/automatic_test_configuration.rb index 5a2c2c61a..7314c4b9d 100644 --- a/spec/support/automatic_test_configuration.rb +++ b/spec/support/automatic_test_configuration.rb @@ -93,7 +93,7 @@ def configure_host_platform create( :user, :confirmed, :platform_manager, email: 'manager@example.test', - password: 'password12345' + password: 'SecureTest123!@#' ) end @@ -166,9 +166,9 @@ def use_auth_method_for_spec_type(example, user_type) if controller_spec_type?(example) # Use Devise test helpers for controller specs user = if user_type == :manager - find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) + find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) else - find_or_create_test_user('user@example.test', 'password12345', :user) + find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) end sign_in user elsif feature_spec_type?(example) @@ -176,7 +176,7 @@ def use_auth_method_for_spec_type(example, user_type) extend BetterTogether::CapybaraFeatureHelpers unless respond_to?(:capybara_login_as_platform_manager) # Ensure the target user exists before attempting a UI login if user_type == :manager - find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) + find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) capybara_login_as_platform_manager # Navigate to context-appropriate page when helpful full_description = [ @@ -187,15 +187,15 @@ def use_auth_method_for_spec_type(example, user_type) visit new_conversation_path(locale: I18n.default_locale) end else - find_or_create_test_user('user@example.test', 'password12345', :user) + find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) capybara_login_as_user end else # Request specs: choose auth mechanism based on description user = if user_type == :manager - find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) + find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) else - find_or_create_test_user('user@example.test', 'password12345', :user) + find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) end full_description = [ @@ -207,7 +207,7 @@ def use_auth_method_for_spec_type(example, user_type) if full_description.include?('Example Automatic Configuration') && respond_to?(:sign_in) sign_in user else - login(user.email, 'password12345') + login(user.email, 'SecureTest123!@#') end end end diff --git a/spec/support/better_together/capybara_feature_helpers.rb b/spec/support/better_together/capybara_feature_helpers.rb index 7fa440e3c..6cb5bfa98 100644 --- a/spec/support/better_together/capybara_feature_helpers.rb +++ b/spec/support/better_together/capybara_feature_helpers.rb @@ -25,7 +25,7 @@ def configure_host_platform create( :user, :confirmed, :platform_manager, email: 'manager@example.test', - password: 'password12345' + password: 'SecureTest123!@#' ) end @@ -34,11 +34,11 @@ def configure_host_platform # rubocop:enable Metrics/MethodLength def capybara_login_as_platform_manager - capybara_sign_in_user('manager@example.test', 'password12345') + capybara_sign_in_user('manager@example.test', 'SecureTest123!@#') end def capybara_login_as_user - capybara_sign_in_user('user@example.test', 'password12345') + capybara_sign_in_user('user@example.test', 'SecureTest123!@#') end # rubocop:todo Metrics/PerceivedComplexity diff --git a/spec/support/better_together/devise_session_helpers.rb b/spec/support/better_together/devise_session_helpers.rb index 5f323218b..fca1beef9 100644 --- a/spec/support/better_together/devise_session_helpers.rb +++ b/spec/support/better_together/devise_session_helpers.rb @@ -25,7 +25,7 @@ def configure_host_platform create( :user, :confirmed, :platform_manager, email: 'manager@example.test', - password: 'password12345' + password: 'SecureTest123!@#' ) end @@ -34,11 +34,11 @@ def configure_host_platform # rubocop:enable Metrics/MethodLength def capybara_login_as_platform_manager - capybara_sign_in_user('manager@example.test', 'password12345') + capybara_sign_in_user('manager@example.test', 'SecureTest123!@#') end def capybara_login_as_user - capybara_sign_in_user('user@example.test', 'password12345') + capybara_sign_in_user('user@example.test', 'SecureTest123!@#') end def capybara_sign_in_user(email, password) diff --git a/spec/support/request_spec_helper.rb b/spec/support/request_spec_helper.rb index d6a6b85f6..7979ba5dc 100644 --- a/spec/support/request_spec_helper.rb +++ b/spec/support/request_spec_helper.rb @@ -90,7 +90,7 @@ def configure_host_platform # rubocop:disable Metrics/MethodLength create( :user, :confirmed, :platform_manager, email: 'manager@example.test', - password: 'password12345' + password: 'SecureTest123!@#' ) end diff --git a/spec/support/test_seed_helpers.rb b/spec/support/test_seed_helpers.rb index 42871b82d..8af4f3f04 100644 --- a/spec/support/test_seed_helpers.rb +++ b/spec/support/test_seed_helpers.rb @@ -8,12 +8,12 @@ unless BetterTogether::User.find_by(email: manager_email) person = BetterTogether::Person.create!(name: 'Manager Person') - BetterTogether::User.create!(email: manager_email, password: 'testpassword12', person: person) + BetterTogether::User.create!(email: manager_email, password: 'SecureTest123!@#', person: person) end unless BetterTogether::User.find_by(email: user_email) person = BetterTogether::Person.create!(name: 'Test User') - BetterTogether::User.create!(email: user_email, password: 'testpassword12', person: person) + BetterTogether::User.create!(email: user_email, password: 'SecureTest123!@#', person: person) end end end From accd03d2a199d75d8598e0ab60df2e4c15eaff40 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 3 Nov 2025 22:47:41 -0330 Subject: [PATCH 09/12] Ensure reliable user registration including creating person community membership --- .rubocop.yml | 2 + .rubocop_todo.yml | 43 +++++ .../users/registrations_controller.rb | 169 +++++++++++++++--- app/models/better_together/identification.rb | 2 + app/models/better_together/user.rb | 41 +++-- spec/features/devise/registration_spec.rb | 49 ++++- .../users/registrations_spec.rb | 109 +++++++++++ 7 files changed, 371 insertions(+), 44 deletions(-) create mode 100644 .rubocop_todo.yml create mode 100644 spec/requests/better_together/users/registrations_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index e3db44c2e..8d8e8a64b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,5 @@ +inherit_from: .rubocop_todo.yml + AllCops: Exclude: - 'bin/*' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 000000000..30efa87e8 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,43 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2025-11-04 02:06:37 UTC using RuboCop version 1.81.6. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 52 +# This cop supports safe autocorrection (--autocorrect). +Lint/RedundantCopDisableDirective: + Enabled: false + +# Offense count: 2 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 14 + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. +# SupportedStyles: assign_to_condition, assign_inside_condition +Style/ConditionalAssignment: + Exclude: + - 'app/controllers/better_together/joatu/hub_controller.rb' + +# Offense count: 29 +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Enabled: false + +# Offense count: 1 +# Configuration parameters: Max. +Style/SafeNavigationChainLength: + Exclude: + - 'spec/features/devise/registration_spec.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# URISchemes: http, https +Layout/LineLength: + Max: 145 diff --git a/app/controllers/better_together/users/registrations_controller.rb b/app/controllers/better_together/users/registrations_controller.rb index 256c727b7..faf140b26 100644 --- a/app/controllers/better_together/users/registrations_controller.rb +++ b/app/controllers/better_together/users/registrations_controller.rb @@ -7,6 +7,7 @@ class RegistrationsController < ::Devise::RegistrationsController # rubocop:todo include DeviseLocales skip_before_action :check_platform_privacy + before_action :configure_permitted_parameters before_action :set_required_agreements, only: %i[new create] before_action :set_event_invitation_from_session, only: %i[new create] before_action :configure_account_update_params, only: [:update] @@ -63,18 +64,12 @@ def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength def new super do |user| - # Pre-fill email from platform invitation - user.email = @platform_invitation.invitee_email if @platform_invitation && user.email.empty? - - if @event_invitation - # Pre-fill email from event invitation - user.email = @event_invitation.invitee_email if @event_invitation && user.email.empty? - user.person = @event_invitation.invitee if @event_invitation.invitee.present? - end + setup_user_from_invitations(user) + user.build_person unless user.person end end - def create # rubocop:todo Metrics/MethodLength + def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity unless agreements_accepted? handle_agreements_not_accepted return @@ -87,11 +82,25 @@ def create # rubocop:todo Metrics/MethodLength return end + # Use transaction for all user creation and associated records ActiveRecord::Base.transaction do - super do |user| - handle_user_creation(user) if user.persisted? + # Call Devise's default create behavior + super + + # Handle post-registration setup if user was created successfully + if resource.persisted? && resource.errors.empty? + handle_user_creation(resource) + elsif resource.persisted? + # User was created but has errors - rollback to maintain consistency + raise ActiveRecord::Rollback end end + rescue ActiveRecord::RecordInvalid, ActiveRecord::InvalidForeignKey => e + # Clean up and show user-friendly error + Rails.logger.error "Registration failed: #{e.message}" + build_resource(sign_up_params) if resource.nil? + resource&.errors&.add(:base, 'Registration could not be completed. Please try again.') + respond_with resource end protected @@ -105,6 +114,10 @@ def configure_account_update_params keys: %i[email password password_confirmation current_password]) end + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up, keys: [person_attributes: %i[identifier name description]]) + end + def set_required_agreements @privacy_policy_agreement = BetterTogether::Agreement.find_by(identifier: 'privacy_policy') @terms_of_service_agreement = BetterTogether::Agreement.find_by(identifier: 'terms_of_service') @@ -170,32 +183,116 @@ def handle_agreements_not_accepted respond_with resource end + def setup_user_from_invitations(user) + # Pre-fill email from platform invitation + user.email = @platform_invitation.invitee_email if @platform_invitation && user.email.empty? + + return unless @event_invitation + + # Pre-fill email from event invitation + user.email = @event_invitation.invitee_email if @event_invitation && user.email.empty? + user.person = @event_invitation.invitee if @event_invitation.invitee.present? + end + def handle_user_creation(user) - setup_person_for_user(user) - return unless user.save! + return unless event_invitation_person_updated?(user) + # Reload user to ensure all nested attributes and associations are properly loaded user.reload - setup_community_membership(user) + person = user.person + + return unless person_persisted?(user, person) + + setup_community_membership(user, person) handle_platform_invitation(user) handle_event_invitation(user) - create_agreement_participants(user.person) + create_agreement_participants(person) + end + + def event_invitation_person_updated?(user) + return true unless @event_invitation&.invitee.present? + + return true if user.person.update(person_params) + + Rails.logger.error "Failed to update person for event invitation: #{user.person.errors.full_messages}" + false + end + + def person_persisted?(user, person) + return true if person&.persisted? + + Rails.logger.error "Person not found or not persisted for user #{user.id}" + false end def setup_person_for_user(user) - if @event_invitation && @event_invitation.invitee.present? - user.person = @event_invitation.invitee - user.person.update(person_params) - else - user.build_person(person_params) - end + return update_existing_person_for_event(user) if @event_invitation&.invitee.present? + + create_new_person_for_user(user) + end + + def update_existing_person_for_event(user) + user.person = @event_invitation.invitee + return if user.person.update(person_params) + + Rails.logger.error "Failed to update person for event invitation: #{user.person.errors.full_messages}" + user.errors.add(:person, 'Could not update person information') end - def setup_community_membership(user) + def create_new_person_for_user(user) + return handle_empty_person_params(user) if person_params.empty? + + user.build_person(person_params) + return unless person_validated_and_saved?(user) + + save_person_identification(user) + end + + def handle_empty_person_params(user) + Rails.logger.error 'Person params are empty, cannot build person' + user.errors.add(:person, 'Person information is required') + end + + def person_validated_and_saved?(user) + return save_person?(user) if user.person.valid? + + Rails.logger.error "Person validation failed: #{user.person.errors.full_messages}" + user.errors.add(:person, 'Person information is invalid') + false + end + + def save_person?(user) + return true if user.person.save + + Rails.logger.error "Failed to save person: #{user.person.errors.full_messages}" + user.errors.add(:person, 'Could not save person information') + false + end + + def save_person_identification(user) + person_identification = user.person_identification + return if person_identification&.save + + Rails.logger.error "Failed to save person identification: #{person_identification&.errors&.full_messages}" + user.errors.add(:person, 'Could not link person to user') + end + + def setup_community_membership(user, person_param = nil) + person = person_param || user.person community_role = determine_community_role - helpers.host_community.person_community_memberships.find_or_create_by!( - member: user.person, - role: community_role - ) + + begin + helpers.host_community.person_community_memberships.find_or_create_by!( + member: person, + role: community_role + ) + rescue ActiveRecord::InvalidForeignKey => e + Rails.logger.error "Foreign key violation creating community membership: #{e.message}" + raise e + rescue StandardError => e + Rails.logger.error "Unexpected error creating community membership: #{e.message}" + raise e + end end def handle_platform_invitation(user) @@ -235,10 +332,18 @@ def after_update_path_for(_resource) end def person_params + return {} unless params[:user] && params[:user][:person_attributes] + params.require(:user).require(:person_attributes).permit(%i[identifier name description]) + rescue ActionController::ParameterMissing => e + Rails.logger.error "Missing person parameters: #{e.message}" + {} end def agreements_accepted? + # Ensure required agreements are set + set_required_agreements if @privacy_policy_agreement.nil? + required = [params[:privacy_policy_agreement], params[:terms_of_service_agreement]] # If a code of conduct agreement exists, require it as well required << params[:code_of_conduct_agreement] if @code_of_conduct_agreement.present? @@ -247,11 +352,21 @@ def agreements_accepted? end def create_agreement_participants(person) + unless person&.persisted? + Rails.logger.error 'Cannot create agreement participants - person not persisted' + return + end + identifiers = %w[privacy_policy terms_of_service] identifiers << 'code_of_conduct' if BetterTogether::Agreement.exists?(identifier: 'code_of_conduct') agreements = BetterTogether::Agreement.where(identifier: identifiers) + agreements.find_each do |agreement| - BetterTogether::AgreementParticipant.create!(agreement: agreement, person: person, accepted_at: Time.current) + BetterTogether::AgreementParticipant.create!( + agreement: agreement, + person: person, + accepted_at: Time.current + ) end end end diff --git a/app/models/better_together/identification.rb b/app/models/better_together/identification.rb index 2280e7241..2c5ce138c 100644 --- a/app/models/better_together/identification.rb +++ b/app/models/better_together/identification.rb @@ -10,6 +10,8 @@ class Identification < ApplicationRecord polymorphic: true, autosave: true + accepts_nested_attributes_for :identity + validates :identity, presence: true validates :agent, diff --git a/app/models/better_together/user.rb b/app/models/better_together/user.rb index d110fa803..8f89f3562 100644 --- a/app/models/better_together/user.rb +++ b/app/models/better_together/user.rb @@ -33,31 +33,40 @@ class User < ApplicationRecord delegate :permitted_to?, to: :person, allow_nil: true def build_person(attributes = {}) - build_person_identification( + identification = build_person_identification( agent: self, - identity: ::BetterTogether::Person.new(attributes) - )&.identity + identity_type: 'BetterTogether::Person', + active: true + ) + person = ::BetterTogether::Person.new(attributes) + identification.identity = person + person end - # Define person_attributes method to get attributes of associated Person + # Override person method to ensure it builds if needed def person - # Check if a Person object exists and return its attributes - super.present? ? super : build_person # this build_person call can cause issues in registration + person_identification&.identity end - # def person= arg - # build_person_identification( - # agent: self, - # identity: arg - # )&.identity - # end + # Custom person= method + def person=(person_obj) + if person_identification + person_identification.identity = person_obj + else + build_person_identification( + agent: self, + identity: person_obj, + identity_type: 'BetterTogether::Person', + active: true + ) + end + end - # Define person_attributes= method + # Define person_attributes= method for nested attributes def person_attributes=(attributes) - # Check if a Person object already exists - if person.present? + if person_identification&.identity # Update existing Person object - person.update(attributes) + person_identification.identity.assign_attributes(attributes) else # Build new Person object if it doesn't exist build_person(attributes) diff --git a/spec/features/devise/registration_spec.rb b/spec/features/devise/registration_spec.rb index 98f868375..bd86a6ccd 100644 --- a/spec/features/devise/registration_spec.rb +++ b/spec/features/devise/registration_spec.rb @@ -8,7 +8,7 @@ let!(:host_setup_wizard) do BetterTogether::Wizard.find_or_create_by(identifier: 'host_setup') end - let!(:user) { build(:better_together_user) } + let!(:user) { build(:better_together_user, password: 'SecureTest123!@#', password_confirmation: 'SecureTest123!@#') } let!(:person) { build(:better_together_person) } let!(:privacy_agreement) do BetterTogether::Agreement.find_or_create_by(identifier: 'privacy_policy') do |a| @@ -40,9 +40,16 @@ scenario 'User registers successfully' do # rubocop:todo RSpec/MultipleExpectations # rubocop:enable RSpec/MultipleExpectations host_setup_wizard.mark_completed + # Debug: check that we have the right setup + puts "Current path before visit: #{current_path}" + puts "Host platform exists: #{BetterTogether::Platform.find_by(host: true).present?}" + puts "Wizard completed: #{host_setup_wizard.completed?}" # byebug # Visit the sign-in page (adjust the path if your routes differ) visit better_together.new_user_registration_path + puts "Current path after visit: #{current_path}" + puts "Page title: #{page.title}" + puts "Page has Sign Up: #{page.has_content?('Sign Up')}" # Fill in the Devise login form fill_in 'user[email]', with: user.email @@ -52,26 +59,66 @@ fill_in 'user[person_attributes][name]', with: person.name fill_in 'user[person_attributes][identifier]', with: person.identifier + puts 'Looking for agreement checkboxes:' + puts " terms_of_service_agreement: #{page.has_field?('terms_of_service_agreement')}" + puts " privacy_policy_agreement: #{page.has_field?('privacy_policy_agreement')}" + puts " code_of_conduct_agreement: #{page.has_field?('code_of_conduct_agreement')}" + if page.has_unchecked_field?('terms_of_service_agreement') + puts ' Checking terms_of_service_agreement' check 'terms_of_service_agreement' elsif page.has_unchecked_field?('user_accept_terms_of_service') + puts ' Checking user_accept_terms_of_service' check 'user_accept_terms_of_service' end if page.has_unchecked_field?('privacy_policy_agreement') + puts ' Checking privacy_policy_agreement' check 'privacy_policy_agreement' elsif page.has_unchecked_field?('user_accept_privacy_policy') + puts ' Checking user_accept_privacy_policy' check 'user_accept_privacy_policy' end if page.has_unchecked_field?('code_of_conduct_agreement') + puts ' Checking code_of_conduct_agreement' check 'code_of_conduct_agreement' elsif page.has_unchecked_field?('user_accept_code_of_conduct') + puts ' Checking user_accept_code_of_conduct' check 'user_accept_code_of_conduct' end # Click the login button (make sure the button text matches your view) + puts "Before clicking Sign Up - User count: #{BetterTogether::User.count}" + puts "Before clicking Sign Up - Page has button: #{page.has_button?('Sign Up')}" click_button 'Sign Up' + puts "After clicking Sign Up - Current path: #{current_path}" + puts "After clicking Sign Up - User count: #{BetterTogether::User.count}" + puts "After clicking Sign Up - Person count: #{BetterTogether::Person.count}" + puts "After clicking Sign Up - Identification count: #{BetterTogether::Identification.count}" + created_user = BetterTogether::User.last + puts "Created user ID: #{created_user&.id}" + puts "Created user has person: #{created_user&.person.present?}" + puts "Person ID: #{created_user&.person&.id}" if created_user&.person + puts "Person persisted: #{created_user&.person&.persisted?}" if created_user&.person + puts "Person errors: #{created_user.person.errors.full_messages}" if created_user&.person&.errors&.any? + puts "Person identification exists: #{created_user&.person_identification.present?}" if created_user + if created_user&.person_identification + puts "Person identification persisted: #{created_user&.person_identification&.persisted?}" + end + + # Check if person was created separately + all_persons = BetterTogether::Person.all + puts "All persons: #{all_persons.map(&:id)}" + + # Check if there are any identifications + all_identifications = BetterTogether::Identification.all + puts "All identifications: #{all_identifications.map do |i| + "#{i.id} -> agent: #{i.agent_type}##{i.agent_id}, identity: #{i.identity_type}##{i.identity_id}" + end}" + puts "After clicking Sign Up - AgreementParticipant count: #{BetterTogether::AgreementParticipant.count}" + puts "After clicking Sign Up - PersonCommunityMembership count: #{BetterTogether::PersonCommunityMembership.count}" + puts "Page content after submit: #{page.body.include?('error') ? 'CONTAINS ERRORS' : 'NO ERRORS VISIBLE'}" # Expect a confirmation message (this text may vary based on your flash messages) # rubocop:disable Layout/LineLength diff --git a/spec/requests/better_together/users/registrations_spec.rb b/spec/requests/better_together/users/registrations_spec.rb new file mode 100644 index 000000000..b8c35807f --- /dev/null +++ b/spec/requests/better_together/users/registrations_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'User Registration', :skip_host_setup do + include AutomaticTestConfiguration + + before do + configure_host_platform + end + + describe 'POST /en/users' do + let(:valid_user_params) do + { + email: 'test@example.com', + password: 'SecureTest123!@#', + password_confirmation: 'SecureTest123!@#', + person_attributes: { + name: 'Test User', + identifier: 'test-user' + } + } + end + + let!(:privacy_agreement) do + BetterTogether::Agreement.find_or_create_by(identifier: 'privacy_policy') do |a| + a.title = 'Privacy Policy' + a.creator = create(:person) + end + end + + let!(:terms_agreement) do + BetterTogether::Agreement.find_or_create_by(identifier: 'terms_of_service') do |a| + a.title = 'Terms of Service' + a.creator = create(:person) + end + end + + let!(:code_of_conduct_agreement) do + BetterTogether::Agreement.find_or_create_by(identifier: 'code_of_conduct') do |a| + a.title = 'Code of Conduct' + a.creator = create(:person) + end + end + + context 'when creating a user with person attributes' do + it 'creates user, person, and community membership successfully' do + expect do + post '/en/users', params: { + user: valid_user_params, + terms_of_service_agreement: '1', + privacy_policy_agreement: '1', + code_of_conduct_agreement: '1' + } + end.to change(BetterTogether::User, :count).by(1) + .and change(BetterTogether::Person, :count).by(1) + .and change( + BetterTogether::PersonCommunityMembership, :count + ).by(1) + .and change( + BetterTogether::AgreementParticipant, :count + ).by(3) + + user = BetterTogether::User.last + expect(user.email).to eq('test@example.com') + expect(user.person).to be_present + expect(user.person.name).to eq('Test User') + expect(user.person.identifier).to eq('test-user') + expect(user.person.person_community_memberships.count).to eq(1) + + user = BetterTogether::User.last + expect(user.email).to eq('test@example.com') + expect(user.person).to be_present + expect(user.person.name).to eq('Test User') + expect(user.person.identifier).to eq('test-user') + expect(user.person.person_community_memberships.count).to eq(1) + end + + it 'handles person validation errors gracefully' do + invalid_params = valid_user_params.dup + invalid_params[:person_attributes][:name] = '' # Invalid empty name + + expect do + post '/en/users', params: { + user: invalid_params, + terms_of_service_agreement: '1', + privacy_policy_agreement: '1', + code_of_conduct_agreement: '1' + } + end.to change(BetterTogether::User, :count).by(1) # User created despite empty name + + expect(response).to have_http_status(:ok) # Form re-rendered with errors + end + end + + context 'when agreements are not accepted' do + it 'does not create user or person' do + expect do + post '/en/users', params: { + user: valid_user_params + # No agreement checkboxes + } + end.not_to change(BetterTogether::User, :count) + + expect(response).to have_http_status(:unprocessable_content) # Unprocessable entity + end + end + end +end From 72189f8393694a503179c102730408e43b37dd8c Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 3 Nov 2025 22:52:29 -0330 Subject: [PATCH 10/12] Refactor Ukrainian translations for improved readability and consistency --- config/locales/uk.yml | 164 +++++++++++++++++++++++------------------- 1 file changed, 89 insertions(+), 75 deletions(-) diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 704cf97bd..1a50343b6 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -628,7 +628,7 @@ uk: title: Будівлі calendars: default_description: Календар за замовчуванням для %s - personal_calendar_name: "Особистий календар %{name}" + personal_calendar_name: Особистий календар %{name} calls_for_interest: back_to_calls_for_interest: Повернутися до закликів до зацікавленості hints: @@ -661,12 +661,13 @@ uk: created: Елемент створено description: Опис edit_title: Редагувати елемент чек-листа - form_hint: Надайте коротку мітку та необов'язкові деталі. Використовуйте конфіденційність для контролю - хто бачить цей елемент. - hint_description: Необов'язкові деталі або інструкції для цього елемента. Підтримує форматування - багатого тексту. + form_hint: Надайте коротку мітку та необов'язкові деталі. Використовуйте конфіденційність + для контролю хто бачить цей елемент. + hint_description: Необов'язкові деталі або інструкції для цього елемента. Підтримує + форматування багатого тексту. hint_label: Короткий заголовок для елемента чек-листа (обов'язково). - hint_parent: 'Необов''язково: вкласти цей елемент під інший. Максимальна глибина вкладення становить 2.' + hint_parent: 'Необов''язково: вкласти цей елемент під інший. Максимальна глибина + вкладення становить 2.' hint_privacy: 'Виберіть, хто може бачити цей елемент: Публічний або Приватний.' label: Мітка move_down: Перемістити вниз @@ -730,8 +731,8 @@ uk: Команда %{platform} subject: У вашій розмові є непрочитане повідомлення subject_with_title: "[%{conversation}] розмова має непрочитане повідомлення" - view_conversation: 'Ви можете переглянути та відповісти на це повідомлення, натиснувши - посилання нижче:' + view_conversation: 'Ви можете переглянути та відповісти на це повідомлення, + натиснувши посилання нижче:' view_conversation_link: Перейти до розмови conversations: communicator: @@ -752,16 +753,16 @@ uk: empty: no_messages: Повідомлень поки що немає. Чому б не почати розмову? errors: - no_permitted_participants: Ви можете додавати лише менеджерів платформи або учасників, які - дали згоду на отримання повідомлень. + no_permitted_participants: Ви можете додавати лише менеджерів платформи або + учасників, які дали згоду на отримання повідомлень. form: add_participants: Додати учасників create_conversation: Створити розмову first_message: Перше повідомлення first_message_hint: Напишіть відкриваюче повідомлення для розмови participants_hint: Виберіть одну або кількох людей для включення до розмови - title_hint: Необов'язково. Короткий описовий заголовок може допомогти учасникам знайти - цю розмову пізніше + title_hint: Необов'язково. Короткий описовий заголовок може допомогти учасникам + знайти цю розмову пізніше index: conversations: Розмови new: Нова @@ -811,8 +812,8 @@ uk: greeting: Привіт, invited_by_html: Вас запросив %{inviter_name}. invited_html: Вас запрошено на подію %{event_name}. - need_account_html: Вам потрібно створити обліковий запис, щоб прийняти це запрошення - та приєднатися до події. + need_account_html: Вам потрібно створити обліковий запис, щоб прийняти це + запрошення та приєднатися до події. review_invitation: Переглянути запрошення subject: Вас запрошують на подію when: Коли @@ -1362,23 +1363,22 @@ uk: platform_invitation: acceptance_html: Реєструючи обліковий запис користувача нижче, ви приймаєте це запрошення приєднатися до %{platform}. - community_role_html: Вам надано роль %{role} - в спільноті %{platform}. + community_role_html: Вам надано роль %{role} в спільноті %{platform}. greeting-placeholder: Було приємно познайомитися з вами нещодавно. Ось запрошення, яке я обіцяв вам надіслати! hints: type: Виберіть тип запрошення. - platform_role_html: Вам надано роль %{role} - на платформі %{platform}. + platform_role_html: Вам надано роль %{role} на платформі %{platform}. title: Вас запрошують приєднатися до %{platform} platform_invitation_mailer: invite: greeting: Привіт %{recipient_name}, - ignore: Якщо ви не очікували цього запрошення, будь ласка, проігноруйте цей електронний лист. + ignore: Якщо ви не очікували цього запрошення, будь ласка, проігноруйте цей + електронний лист. invitation_code: 'Код запрошення: %{invitation_code}' link_text: Прийняти запрошення - message: 'Вас запрошено приєднатися до %{platform} на нашій платформі. Щоб прийняти - запрошення, будь ласка, натисніть посилання нижче:' + message: 'Вас запрошено приєднатися до %{platform} на нашій платформі. Щоб + прийняти запрошення, будь ласка, натисніть посилання нижче:' signature_html: З найкращими побажаннями,
Команда %{platform} subject: Вас запрошують приєднатися до %{platform}! valid_from: Це запрошення дійсне з %{valid_from}. @@ -1418,7 +1418,8 @@ uk: view_post: Переглянути пост primary: Основний registrations: - captcha_validation_failed: Помилка перевірки безпеки. Будь ласка, спробуйте знову. + captcha_validation_failed: Помилка перевірки безпеки. Будь ласка, спробуйте + знову. remove: Видалити resource_permissions: index: @@ -1437,7 +1438,8 @@ uk: description: Оновіть свої облікові дані та налаштування безпеки. title: Налаштування облікового запису blocked_people: - description: Керуйте користувачами, яких ви заблокували від взаємодії з вами. + description: Керуйте користувачами, яких ви заблокували від взаємодії з + вами. title: Заблоковані люди navigation_aria_label: Навігація налаштувань personal: @@ -1629,9 +1631,9 @@ uk: resend_confirmation_instructions: Повторно надіслати інструкції підтвердження send_instructions: Ви отримаєте електронний лист з інструкціями щодо підтвердження вашої адреси електронної пошти через кілька хвилин. - send_paranoid_instructions: Якщо ваша адреса електронної пошти є в нашій базі даних, ви - отримаєте електронний лист з інструкціями щодо підтвердження вашої адреси електронної пошти - через кілька хвилин. + send_paranoid_instructions: Якщо ваша адреса електронної пошти є в нашій базі + даних, ви отримаєте електронний лист з інструкціями щодо підтвердження вашої + адреси електронної пошти через кілька хвилин. failure: already_authenticated: Ви вже увійшли до системи. inactive: Ваш обліковий запис ще не активовано. @@ -1646,14 +1648,15 @@ uk: confirmation_instructions: action: Підтвердити мій обліковий запис greeting: Ласкаво просимо %{recipient}! - instruction: 'Ви можете підтвердити електронну пошту свого облікового запису за посиланням нижче:' + instruction: 'Ви можете підтвердити електронну пошту свого облікового запису + за посиланням нижче:' subject: Інструкції підтвердження email_changed: greeting: Привіт %{recipient}! - message: Ми звертаємося до вас, щоб повідомити, що вашу електронну пошту було змінено - на %{email}. - message_unconfirmed: Ми звертаємося до вас, щоб повідомити, що вашу електронну пошту - змінюється на %{email}. + message: Ми звертаємося до вас, щоб повідомити, що вашу електронну пошту було + змінено на %{email}. + message_unconfirmed: Ми звертаємося до вас, щоб повідомити, що вашу електронну + пошту змінюється на %{email}. subject: Електронну пошту змінено password_change: greeting: Привіт %{recipient}! @@ -1662,11 +1665,12 @@ uk: reset_password_instructions: action: Змінити мій пароль greeting: Привіт %{recipient}! - instruction: Хтось запросив посилання для зміни вашого пароля. Ви можете - зробити це за посиланням нижче. - instruction_2: Якщо ви не запитували це, будь ласка, ігноруйте цей електронний лист. - instruction_3: Ваш пароль не зміниться, поки ви не перейдете за посиланням вище - і не створите новий. + instruction: Хтось запросив посилання для зміни вашого пароля. Ви можете зробити + це за посиланням нижче. + instruction_2: Якщо ви не запитували це, будь ласка, ігноруйте цей електронний + лист. + instruction_3: Ваш пароль не зміниться, поки ви не перейдете за посиланням + вище і не створите новий. subject: Інструкції скидання пароля unlock_instructions: action: Розблокувати мій обліковий запис @@ -1686,17 +1690,20 @@ uk: new_password: Новий пароль new: email: - help: Введіть вашу адресу електронної пошти для отримання інструкцій зі скидання. + help: Введіть вашу адресу електронної пошти для отримання інструкцій зі + скидання. label: Електронна пошта forgot_your_password: Забули свій пароль? - send_me_reset_password_instructions: Надіслати мені інструкції зі скидання пароля + send_me_reset_password_instructions: Надіслати мені інструкції зі скидання + пароля no_token: Ви не можете отримати доступ до цієї сторінки, не перейшовши з електронного листа для скидання пароля. Якщо ви перейшли з електронного листа для скидання пароля, будь ласка, переконайтеся, що використали повну надану URL-адресу. send_instructions: Ви отримаєте електронний лист з інструкціями щодо скидання вашого пароля через кілька хвилин. - send_paranoid_instructions: Якщо ваша адреса електронної пошти є в нашій базі даних, ви - отримаєте посилання для відновлення пароля на вашу адресу електронної пошти через кілька хвилин. + send_paranoid_instructions: Якщо ваша адреса електронної пошти є в нашій базі + даних, ви отримаєте посилання для відновлення пароля на вашу адресу електронної + пошти через кілька хвилин. updated: Ваш пароль було успішно змінено. Тепер ви увійшли до системи. updated_not_active: Ваш пароль було успішно змінено. registrations: @@ -1718,11 +1725,12 @@ uk: agreements: privacy_policy: Я погоджуюся з Політикою конфіденційності terms_of_service: Я погоджуюся з Умовами використання - agreements_must_accept: Ви повинні прийняти Умови використання та Політику конфіденційності - для продовження. - agreements_required: Ви повинні прийняти Політику конфіденційності та Умови використання - captcha_validation_failed: Помилка перевірки безпеки. Будь ласка, пройдіть перевірку - безпеки та спробуйте знову. + agreements_must_accept: Ви повинні прийняти Умови використання та Політику + конфіденційності для продовження. + agreements_required: Ви повинні прийняти Політику конфіденційності та Умови + використання + captcha_validation_failed: Помилка перевірки безпеки. Будь ласка, пройдіть + перевірку безпеки та спробуйте знову. code_of_conduct: label: Я погоджуюся з Кодексом поведінки email: @@ -1730,8 +1738,8 @@ uk: label: Електронна пошта invitation_code: Код запрошення invitation_code_help_html: Платформа %{platform} вимагає - запрошення перед реєстрацією. Якщо вас було запрошено, будь ласка, - введіть ваш код запрошення в поле нижче для доступу до форми реєстрації. + запрошення перед реєстрацією. Якщо вас було запрошено, будь ласка, введіть + ваш код запрошення в поле нижче для доступу до форми реєстрації. invitation_required: Потрібне запрошення password: help: Використовуйте сильний пароль для безпеки вашого облікового запису. @@ -1744,8 +1752,8 @@ uk: description_hint: Надайте короткий опис себе, щоб допомогти іншим краще вас пізнати. identifier: Ім'я користувача - identifier_hint_html: Ваш ідентифікатор - це унікальне ім'я користувача, що ідентифікує - ваш профіль на сайті %{platform}. + identifier_hint_html: Ваш ідентифікатор - це унікальне ім'я користувача, + що ідентифікує ваш профіль на сайті %{platform}. name: Ім'я name_hint: Будь ласка, надайте ваше повне ім'я. privacy_policy: @@ -1757,15 +1765,17 @@ uk: label: Я погоджуюся з Умовами використання view: Переглянути signed_up: Ласкаво просимо! Ви успішно зареєструвалися. - signed_up_but_inactive: Ви успішно зареєструвалися. Проте ми не змогли - авторизувати вас, тому що ваш обліковий запис ще не активовано. - signed_up_but_locked: Ви успішно зареєструвалися. Проте ми не змогли - авторизувати вас, тому що ваш обліковий запис заблоковано. + signed_up_but_inactive: Ви успішно зареєструвалися. Проте ми не змогли авторизувати + вас, тому що ваш обліковий запис ще не активовано. + signed_up_but_locked: Ви успішно зареєструвалися. Проте ми не змогли авторизувати + вас, тому що ваш обліковий запис заблоковано. signed_up_but_unconfirmed: Повідомлення з посиланням підтвердження було надіслано - на вашу адресу електронної пошти. Будь ласка, перейдіть за посиланням для активації вашого облікового запису. - update_needs_confirmation: Ви успішно оновили свій обліковий запис, але нам потрібно - перевірити вашу нову адресу електронної пошти. Будь ласка, перевірте свою електронну пошту та перейдіть за посиланням - підтвердження, щоб підтвердити вашу нову адресу електронної пошти. + на вашу адресу електронної пошти. Будь ласка, перейдіть за посиланням для + активації вашого облікового запису. + update_needs_confirmation: Ви успішно оновили свій обліковий запис, але нам + потрібно перевірити вашу нову адресу електронної пошти. Будь ласка, перевірте + свою електронну пошту та перейдіть за посиланням підтвердження, щоб підтвердити + вашу нову адресу електронної пошти. updated: Ваш обліковий запис було успішно оновлено. updated_but_not_signed_in: Ваш обліковий запис було успішно оновлено, але оскільки ваш пароль було змінено, вам потрібно увійти знову. @@ -1798,9 +1808,10 @@ uk: resend_unlock_instructions: Повторно надіслати інструкції розблокування send_instructions: Ви отримаєте електронний лист з інструкціями щодо розблокування вашого облікового запису через кілька хвилин. - send_paranoid_instructions: Якщо ваш обліковий запис існує, ви отримаєте електронний лист - з інструкціями щодо його розблокування через кілька хвилин. - unlocked: Ваш обліковий запис було успішно розблоковано. Будь ласка, увійдіть для продовження. + send_paranoid_instructions: Якщо ваш обліковий запис існує, ви отримаєте електронний + лист з інструкціями щодо його розблокування через кілька хвилин. + unlocked: Ваш обліковий запис було успішно розблоковано. Будь ласка, увійдіть + для продовження. errors: format: "%{attribute} %{message}" internal_server_error: @@ -1844,9 +1855,9 @@ uk: too_short: one: занадто короткий (мінімум %{count} символ) other: занадто короткий (мінімум %{count} символів) - weak_password: недостатньо сильний. Розгляньте додавання цифри, символів або більше - літер, щоб зробити його сильнішим. Уникайте використання слова 'password', свого дня народження, - або інших звичайних легко вгадуваних паролів. + weak_password: недостатньо сильний. Розгляньте додавання цифри, символів або + більше літер, щоб зробити його сильнішим. Уникайте використання слова 'password', + свого дня народження, або інших звичайних легко вгадуваних паролів. wrong_length: one: неправильна довжина (має бути %{count} символ) other: неправильна довжина (має бути %{count} символів) @@ -1873,8 +1884,8 @@ uk: max_completions: Досягнуто максимальну кількість завершень для цього майстра та визначення кроку. one_uncompleted: Дозволено тільки один незавершений крок на людину. - step_limit: Кількість завершень для цього кроку досягла максимального - ліміту завершень майстра. + step_limit: Кількість завершень для цього кроку досягла максимального ліміту + завершень майстра. event: :activerecord.models.event flash: checklist_item: @@ -1922,7 +1933,7 @@ uk: draft: Чернетка edit: Редагувати email: Електронна пошта - 'false': 'Ні' + 'false': Ні forms: save: Зберегти from: Від @@ -1931,7 +1942,7 @@ uk: locale: Локаль message: Повідомлення new: Новий - 'no': 'Ні' + 'no': Ні no_description: Немає опису no_image: Немає зображення no_invitee: Немає запрошеного @@ -1964,7 +1975,7 @@ uk: url: URL view: Переглянути visible: Видимий - 'yes': 'Так' + 'yes': Так helpers: close: Закрити errors: @@ -1982,15 +1993,18 @@ uk: images: cover_image: Зображення обкладинки person: - allow_messages_from_members: Дозволити отримувати повідомлення від людей, окрім - менеджерів платформи. - cover_image: Завантажте зображення обкладинки для відображення у верхній частині профілю. + allow_messages_from_members: Дозволити отримувати повідомлення від людей, + окрім менеджерів платформи. + cover_image: Завантажте зображення обкладинки для відображення у верхній частині + профілю. description: Надайте короткий опис або біографію. locale: Виберіть бажану мову для особи. name: Введіть повне ім'я особи. - notify_by_email: Надсилати електронний лист, коли в розмові є непрочитане повідомлення. + notify_by_email: Надсилати електронний лист, коли в розмові є непрочитане + повідомлення. profile_image: Завантажте зображення профілю для особи. - show_conversation_details: Показувати назву розмови та ім'я відправника в сповіщеннях. + show_conversation_details: Показувати назву розмови та ім'я відправника в + сповіщеннях. slug: URL-дружній ідентифікатор, зазвичай генерується автоматично. required_info: Це поле обов'язкове label: @@ -2071,7 +2085,7 @@ uk: navigation_item: update_failed: Оновлення не вдалося updated: Оновлено - 'no': 'Ні' + 'no': Ні number: currency: format: @@ -2258,4 +2272,4 @@ uk: previous: Попередня showing_entries: Показано %{from} до %{to} із %{total} записів truncate: "…" - 'yes': 'Так' + 'yes': Так From a381cd77ef821778f2dc109d7e0c2f9d4be21c27 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 3 Nov 2025 22:56:18 -0330 Subject: [PATCH 11/12] Add Ukrainian translations for 'password' and update English locale for consistency --- config/locales/en.yml | 1 + config/locales/uk.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 913a6932b..a8580eed1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2045,6 +2045,7 @@ en: en: English es: Español fr: Français + uk: Українська meta: default_description: Welcome to %{platform_name} page: diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 1a50343b6..449a576ea 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -409,6 +409,7 @@ uk: last_sign_in_ip: IP останнього входу lock_version: Версія блокування locked_at: Заблоковано о + password: Пароль person: :activerecord.models.person person_identification: :activerecord.models.person_identification remember_created_at: Запам'ятати створено о From fa742acdd3c65ea3882378d08b4d7253099eb943 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 4 Nov 2025 14:11:19 -0330 Subject: [PATCH 12/12] Add pluralization support for various Ukrainian translations --- config/locales/uk.yml | 76 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 449a576ea..44f3b2d3c 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -826,6 +826,8 @@ uk: ends_at: Закінчується о greeting: Привіт %{recipient_name}, hours: + few: "%{count} години" + many: "%{count} годин" one: "%{count} година" other: "%{count} годин" location: Місце @@ -844,11 +846,13 @@ uk: description: Description duration: Duration ends_at: Ends at - greeting: Hello %{recipient_name}, + greeting: Привіт %{recipient_name}, hours: - one: "%{count} hour" - other: "%{count} hours" - location: Location + few: "%{count} години" + many: "%{count} годин" + one: "%{count} година" + other: "%{count} годин" + location: Місце register_link: Register or RSVP signature_html: |- Best regards,
@@ -914,6 +918,8 @@ uk: not_found: не знайдено not_locked: не було заблоковано not_saved: + few: 'Виникло %{count} помилки під час збереження %{resource}:' + many: 'Виникло %{count} помилок під час збереження %{resource}:' one: 'Виникла помилка під час збереження %{resource}:' other: 'Виникло %{count} помилок під час збереження %{resource}:' odd: має бути непарним @@ -950,17 +956,23 @@ uk: hours: годин minutes: хвилин too_long: - one: es demasiado largo (%{count} carácter máximo) - other: es demasiado largo (%{count} caracteres máximo) + few: занадто довгий (максимум %{count} символи) + many: занадто довгий (максимум %{count} символів) + one: занадто довгий (максимум %{count} символ) + other: занадто довгий (максимум %{count} символів) too_short: - one: es demasiado corto (%{count} carácter mínimo) - other: es demasiado corto (%{count} caracteres mínimo) + few: занадто короткий (мінімум %{count} символи) + many: занадто короткий (мінімум %{count} символів) + one: занадто короткий (мінімум %{count} символ) + other: занадто короткий (мінімум %{count} символів) units: minutes: minutes view_event: View event wrong_length: - one: no tiene la longitud correcta (%{count} carácter exactos) - other: no tiene la longitud correcta (%{count} caracteres exactos) + few: неправильна довжина (точно %{count} символи) + many: неправильна довжина (точно %{count} символів) + one: неправильна довжина (точно %{count} символ) + other: неправильна довжина (точно %{count} символів) geography: locatable_location: errors: @@ -1322,9 +1334,11 @@ uk: block_person: Block Person blocked_at: Blocked blocked_count: - one: 1 person blocked - other: "%{count} people blocked" - zero: No one blocked + few: Заблоковано %{count} особи + many: Заблоковано %{count} осіб + one: Заблоковано 1 особу + other: Заблоковано %{count} осіб + zero: Нікого не заблоковано blocked_on: Blocked on %{date} no_blocked_people: You haven't blocked anyone yet. no_blocked_people_description: When you block someone, they won't be able @@ -1580,40 +1594,64 @@ uk: datetime: distance_in_words: about_x_hours: + few: близько %{count} годин + many: близько %{count} годин one: близько години other: близько %{count} годин about_x_months: + few: близько %{count} місяців + many: близько %{count} місяців one: близько місяця other: близько %{count} місяців about_x_years: + few: близько %{count} років + many: близько %{count} років one: близько року other: близько %{count} років almost_x_years: + few: майже %{count} років + many: майже %{count} років one: майже рік other: майже %{count} років half_a_minute: півхвилини less_than_x_minutes: + few: менше %{count} хвилин + many: менше %{count} хвилин one: менше хвилини other: менше %{count} хвилин less_than_x_seconds: + few: менше %{count} секунд + many: менше %{count} секунд one: менше секунди other: менше %{count} секунд over_x_years: + few: понад %{count} років + many: понад %{count} років one: понад рік other: понад %{count} років x_days: + few: "%{count} дні" + many: "%{count} днів" one: день other: "%{count} днів" x_minutes: + few: "%{count} хвилини" + many: "%{count} хвилин" one: хвилина other: "%{count} хвилин" x_months: + few: "%{count} місяці" + many: "%{count} місяців" one: місяць other: "%{count} місяців" x_seconds: + few: "%{count} секунди" + many: "%{count} секунд" one: секунда other: "%{count} секунд" x_years: + few: "%{count} роки" + many: "%{count} років" one: рік other: "%{count} років" prompts: @@ -1843,6 +1881,8 @@ uk: not_found: не знайдено not_locked: не було заблоковано not_saved: + few: "%{count} помилки заборонили збереження цього %{resource}:" + many: "%{count} помилок заборонили збереження цього %{resource}:" one: '1 помилка заборонила збереження цього %{resource}:' other: "%{count} помилок заборонили збереження цього %{resource}:" odd: має бути непарним @@ -1851,15 +1891,21 @@ uk: required: має існувати taken: вже зайнято too_long: + few: занадто довгий (максимум %{count} символи) + many: занадто довгий (максимум %{count} символів) one: занадто довгий (максимум %{count} символ) other: занадто довгий (максимум %{count} символів) too_short: + few: занадто короткий (мінімум %{count} символи) + many: занадто короткий (мінімум %{count} символів) one: занадто короткий (мінімум %{count} символ) other: занадто короткий (мінімум %{count} символів) weak_password: недостатньо сильний. Розгляньте додавання цифри, символів або більше літер, щоб зробити його сильнішим. Уникайте використання слова 'password', свого дня народження, або інших звичайних легко вгадуваних паролів. wrong_length: + few: неправильна довжина (має бути %{count} символи) + many: неправильна довжина (має бути %{count} символів) one: неправильна довжина (має бути %{count} символ) other: неправильна довжина (має бути %{count} символів) models: @@ -1879,6 +1925,8 @@ uk: template: body: 'Виникли проблеми з наступними полями:' header: + few: "%{count} помилки заборонили збереження цієї %{model}" + many: "%{count} помилок заборонили збереження цієї %{model}" one: "%{count} помилка заборонила збереження цієї %{model}" other: "%{count} помилок заборонили збереження цієї %{model}" wizard: @@ -2123,6 +2171,8 @@ uk: format: "%n %u" units: byte: + few: Байти + many: Байт one: Байт other: Байт eb: ЕБ