diff --git a/.mailmap b/.mailmap index 16feb97c9d99e..8e36002c6c679 100644 --- a/.mailmap +++ b/.mailmap @@ -97,6 +97,7 @@ Rishi Gupta Rixant Rokaha Rixant Rokaha Rixant Rokaha +Rohan Gudimetla Sahil Batra <35494118+sahil839@users.noreply.github.com> Sahil Batra Satyam Bansal @@ -120,6 +121,7 @@ Tim Abbott Ujjawal Modi <99073049+Ujjawal3@users.noreply.github.com> umkay umkay +Viktor Illmer <1476338+v-ji@users.noreply.github.com> Vishnu KS Vishnu KS Waseem Daher diff --git a/analytics/management/commands/populate_analytics_db.py b/analytics/management/commands/populate_analytics_db.py index 7167196280079..0826217bce614 100644 --- a/analytics/management/commands/populate_analytics_db.py +++ b/analytics/management/commands/populate_analytics_db.py @@ -32,10 +32,10 @@ Recipient, Stream, Subscription, - SystemGroups, UserGroup, UserProfile, ) +from zerver.models.groups import SystemGroups class Command(BaseCommand): diff --git a/analytics/tests/test_activity_views.py b/analytics/tests/test_activity_views.py index d93f82dc071f2..999afdf0d03b8 100644 --- a/analytics/tests/test_activity_views.py +++ b/analytics/tests/test_activity_views.py @@ -1,11 +1,18 @@ +import uuid from datetime import timedelta from unittest import mock from django.utils.timezone import now as timezone_now +from corporate.lib.stripe import add_months +from corporate.models import Customer, CustomerPlan, LicenseLedger from zerver.lib.test_classes import ZulipTestCase from zerver.models import Client, UserActivity, UserProfile -from zilencer.models import RemoteRealmAuditLog, get_remote_server_guest_and_non_guest_count +from zilencer.models import ( + RemoteRealmAuditLog, + RemoteZulipServer, + get_remote_server_guest_and_non_guest_count, +) event_time = timezone_now() - timedelta(days=3) data_list = [ @@ -99,8 +106,32 @@ def test_activity(self, unused_mock: mock.Mock) -> None: result = self.client_get("/activity") self.assertEqual(result.status_code, 200) + # Add data for remote activity page RemoteRealmAuditLog.objects.bulk_create([RemoteRealmAuditLog(**data) for data in data_list]) - with self.assert_database_query_count(6): + remote_server = RemoteZulipServer.objects.get(id=1) + customer = Customer.objects.create(remote_server=remote_server) + plan = CustomerPlan.objects.create( + customer=customer, + billing_cycle_anchor=timezone_now(), + billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL, + tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS, + price_per_license=8000, + next_invoice_date=add_months(timezone_now(), 12), + ) + LicenseLedger.objects.create( + licenses=10, + licenses_at_next_renewal=10, + event_time=timezone_now(), + is_renewal=True, + plan=plan, + ) + RemoteZulipServer.objects.create( + uuid=str(uuid.uuid4()), + api_key="magic_secret_api_key", + hostname="demo.example.com", + contact_email="email@example.com", + ) + with self.assert_database_query_count(10): result = self.client_get("/activity/remote") self.assertEqual(result.status_code, 200) diff --git a/analytics/tests/test_counts.py b/analytics/tests/test_counts.py index 2a947a32d8aac..b30e9df0b100c 100644 --- a/analytics/tests/test_counts.py +++ b/analytics/tests/test_counts.py @@ -68,20 +68,19 @@ Client, Huddle, Message, - NotificationTriggers, PreregistrationUser, Realm, RealmAuditLog, Recipient, Stream, - SystemGroups, UserActivityInterval, UserGroup, UserProfile, - get_client, - get_user, - is_cross_realm_bot_email, ) +from zerver.models.clients import get_client +from zerver.models.groups import SystemGroups +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.users import get_user, is_cross_realm_bot_email from zilencer.models import ( RemoteInstallationCount, RemotePushDeviceToken, @@ -1459,8 +1458,9 @@ def test_mobile_pushes_received_count(self) -> None: now = timezone_now() with time_machine.travel(now, tick=False), mock.patch( "zilencer.views.send_android_push_notification", return_value=1 - ), mock.patch( - "zilencer.views.send_apple_push_notification", return_value=1 + ), mock.patch("zilencer.views.send_apple_push_notification", return_value=1), mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=10, ), self.assertLogs( "zilencer.views", level="INFO" ): @@ -1519,8 +1519,9 @@ def test_mobile_pushes_received_count(self) -> None: } with time_machine.travel(now, tick=False), mock.patch( "zilencer.views.send_android_push_notification", return_value=1 - ), mock.patch( - "zilencer.views.send_apple_push_notification", return_value=1 + ), mock.patch("zilencer.views.send_apple_push_notification", return_value=1), mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=10, ), self.assertLogs( "zilencer.views", level="INFO" ): @@ -1578,8 +1579,9 @@ def test_mobile_pushes_received_count(self) -> None: with time_machine.travel(now, tick=False), mock.patch( "zilencer.views.send_android_push_notification", return_value=1 - ), mock.patch( - "zilencer.views.send_apple_push_notification", return_value=1 + ), mock.patch("zilencer.views.send_apple_push_notification", return_value=1), mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=10, ), self.assertLogs( "zilencer.views", level="INFO" ): diff --git a/analytics/tests/test_stats_views.py b/analytics/tests/test_stats_views.py index 2591a4004465c..97959a0fc0533 100644 --- a/analytics/tests/test_stats_views.py +++ b/analytics/tests/test_stats_views.py @@ -10,7 +10,8 @@ from analytics.views.stats import rewrite_client_arrays, sort_by_totals, sort_client_labels from zerver.lib.test_classes import ZulipTestCase from zerver.lib.timestamp import ceiling_to_day, ceiling_to_hour, datetime_to_timestamp -from zerver.models import Client, get_realm +from zerver.models import Client +from zerver.models.realms import get_realm class TestStatsEndpoint(ZulipTestCase): diff --git a/analytics/tests/test_support_views.py b/analytics/tests/test_support_views.py index 1c31d3f057023..922b1bd79ab32 100644 --- a/analytics/tests/test_support_views.py +++ b/analytics/tests/test_support_views.py @@ -13,6 +13,8 @@ Customer, CustomerPlan, LicenseLedger, + SponsoredPlanTypes, + ZulipSponsorshipRequest, get_current_plan_by_realm, get_customer_by_realm, ) @@ -21,15 +23,8 @@ from zerver.actions.user_settings import do_change_user_setting from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import reset_email_visibility_to_everyone_in_zulip_realm -from zerver.models import ( - MultiuseInvite, - PreregistrationUser, - Realm, - UserMessage, - UserProfile, - get_org_type_display_name, - get_realm, -) +from zerver.models import MultiuseInvite, PreregistrationUser, Realm, UserMessage, UserProfile +from zerver.models.realms import OrgTypeEnum, get_org_type_display_name, get_realm from zilencer.lib.remote_counts import MissingDataError if TYPE_CHECKING: @@ -43,6 +38,24 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase): @override def setUp(self) -> None: + def add_sponsorship_request( + hostname: str, org_type: int, website: str, paid_users: str, plan: str + ) -> None: + remote_server = RemoteZulipServer.objects.get(hostname=hostname) + customer = Customer.objects.create( + remote_server=remote_server, sponsorship_pending=True + ) + ZulipSponsorshipRequest.objects.create( + customer=customer, + org_type=org_type, + org_website=website, + org_description="We help people.", + expected_total_users="20-35", + paid_users_count=paid_users, + paid_users_description="", + requested_plan=plan, + ) + super().setUp() # Set up some initial example data. @@ -52,6 +65,23 @@ def setUp(self) -> None: hostname=hostname, contact_email=f"admin@{hostname}", plan_type=1, uuid=uuid.uuid4() ) + # Add example sponsorship request data + add_sponsorship_request( + hostname="zulip-1.example.com", + org_type=OrgTypeEnum.Community.value, + website="", + paid_users="None", + plan=SponsoredPlanTypes.BUSINESS.value, + ) + + add_sponsorship_request( + hostname="zulip-2.example.com", + org_type=OrgTypeEnum.OpenSource.value, + website="example.org", + paid_users="", + plan=SponsoredPlanTypes.COMMUNITY.value, + ) + def test_search(self) -> None: self.login("cordelia") @@ -77,6 +107,19 @@ def test_search(self) -> None: self.assert_in_success_response(["Max monthly messages: 1000"], result) self.assert_not_in_success_response(["

zulip-2.example.com

"], result) + # Sponsorship request information + self.assert_in_success_response(["
  • Organization type: Community
  • "], result) + self.assert_in_success_response( + ["
  • Organization website: No website submitted
  • "], result + ) + self.assert_in_success_response(["
  • Paid users: None
  • "], result) + self.assert_in_success_response(["
  • Requested plan: Business
  • "], result) + self.assert_in_success_response( + ["
  • Organization description: We help people.
  • "], result + ) + self.assert_in_success_response(["
  • Estimated total users: 20-35
  • "], result) + self.assert_in_success_response(["
  • Description of paid users:
  • "], result) + with mock.patch( "analytics.views.support.compute_max_monthly_messages", side_effect=MissingDataError ): @@ -96,6 +139,35 @@ def test_search(self) -> None: self.assert_in_success_response(["Contact email: admin@zulip-2.example.com"], result) self.assert_not_in_success_response(["

    zulip-1.example.com

    "], result) + # Sponsorship request information + self.assert_in_success_response( + ["
  • Organization type: Open-source project
  • "], result + ) + self.assert_in_success_response( + ["
  • Organization website: example.org
  • "], result + ) + self.assert_in_success_response(["
  • Paid users:
  • "], result) + self.assert_in_success_response(["
  • Requested plan: Community
  • "], result) + self.assert_in_success_response( + ["
  • Organization description: We help people.
  • "], result + ) + self.assert_in_success_response(["
  • Estimated total users: 20-35
  • "], result) + self.assert_in_success_response(["
  • Description of paid users:
  • "], result) + + result = self.client_get("/activity/remote/support", {"q": "admin@zulip-3.example.com"}) + self.assert_in_success_response(["

    zulip-3.example.com

    "], result) + self.assert_in_success_response(["Contact email: admin@zulip-3.example.com"], result) + self.assert_not_in_success_response(["

    zulip-1.example.com

    "], result) + + # Sponsorship request information + self.assert_not_in_success_response( + ["
  • Organization description: We help people.
  • "], result + ) + self.assert_not_in_success_response( + ["
  • Estimated total users: 20-35
  • "], result + ) + self.assert_not_in_success_response(["
  • Description of paid users:
  • "], result) + class TestSupportEndpoint(ZulipTestCase): def test_search(self) -> None: diff --git a/analytics/views/installation_activity.py b/analytics/views/installation_activity.py index 5144e969c85dc..58f4aabc36085 100644 --- a/analytics/views/installation_activity.py +++ b/analytics/views/installation_activity.py @@ -25,7 +25,8 @@ from analytics.views.support import get_plan_type_string from zerver.decorator import require_server_admin from zerver.lib.request import has_request_variables -from zerver.models import Realm, get_org_type_display_name +from zerver.models import Realm +from zerver.models.realms import get_org_type_display_name if settings.BILLING_ENABLED: from corporate.lib.analytics import ( diff --git a/analytics/views/remote_activity.py b/analytics/views/remote_activity.py index 36d9af8a3d755..099e28267001e 100644 --- a/analytics/views/remote_activity.py +++ b/analytics/views/remote_activity.py @@ -10,6 +10,7 @@ remote_installation_stats_link, remote_installation_support_link, ) +from corporate.lib.analytics import get_plan_data_by_remote_server from zerver.decorator import require_server_admin from zilencer.models import get_remote_server_guest_and_non_guest_count @@ -69,6 +70,7 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse: left join icount on icount.server_id = rserver.id left join mobile_push_forwarded_count on mobile_push_forwarded_count.server_id = rserver.id left join remote_push_devices on remote_push_devices.server_id = rserver.id + where not deactivated order by latest_value DESC NULLS LAST, push_user_count DESC NULLS LAST """ ) @@ -82,6 +84,9 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse: "Mobile users", "Last update time", "Mobile pushes forwarded", + "Plan name", + "Plan status", + "ARR", "Non guest users", "Guest users", "Links", @@ -90,13 +95,27 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse: rows = get_query_data(query) total_row = [] totals_columns = [4, 5] + plan_data_by_remote_server = get_plan_data_by_remote_server() + for row in rows: - stats = remote_installation_stats_link(row[0]) - support = remote_installation_support_link(row[1]) - links = stats + " " + support + # Add estimated revenue for server + server_plan_data = plan_data_by_remote_server.get(row[0]) + if server_plan_data is None: + row.append("---") + row.append("---") + row.append("---") + else: + row.append(server_plan_data.current_plan_name) + row.append(server_plan_data.current_status) + row.append(server_plan_data.annual_revenue) + # Add user counts remote_server_counts = get_remote_server_guest_and_non_guest_count(row[0]) row.append(remote_server_counts.non_guest_user_count) row.append(remote_server_counts.guest_user_count) + # Add links + stats = remote_installation_stats_link(row[0]) + support = remote_installation_support_link(row[1]) + links = stats + " " + support row.append(links) for i, col in enumerate(cols): if col == "Last update time": diff --git a/analytics/views/stats.py b/analytics/views/stats.py index 9366945151db3..a12873bf55779 100644 --- a/analytics/views/stats.py +++ b/analytics/views/stats.py @@ -36,7 +36,8 @@ from zerver.lib.streams import access_stream_by_id from zerver.lib.timestamp import convert_to_UTC from zerver.lib.validator import to_non_negative_int -from zerver.models import Client, Realm, Stream, UserProfile, get_realm +from zerver.models import Client, Realm, Stream, UserProfile +from zerver.models.realms import get_realm if settings.ZILENCER_ENABLED: from zilencer.models import RemoteInstallationCount, RemoteRealmCount, RemoteZulipServer diff --git a/analytics/views/support.py b/analytics/views/support.py index b4920e198b4f8..d16ef66e48331 100644 --- a/analytics/views/support.py +++ b/analytics/views/support.py @@ -40,10 +40,9 @@ Realm, RealmReactivationStatus, UserProfile, - get_org_type_display_name, - get_realm, - get_user_profile_by_id, ) +from zerver.models.realms import get_org_type_display_name, get_realm +from zerver.models.users import get_user_profile_by_id from zerver.views.invite import get_invitee_emails_set if settings.ZILENCER_ENABLED: @@ -60,8 +59,10 @@ ) from corporate.lib.support import ( PlanData, + SupportData, get_current_plan_data_for_support_view, get_customer_discount_for_support_view, + get_data_for_support_view, ) from corporate.models import CustomerPlan @@ -73,7 +74,10 @@ def get_plan_type_string(plan_type: int) -> str: Realm.PLAN_TYPE_STANDARD: "Standard", Realm.PLAN_TYPE_STANDARD_FREE: "Standard free", Realm.PLAN_TYPE_PLUS: "Plus", - RemoteZulipServer.PLAN_TYPE_SELF_HOSTED: "Self-hosted", + RemoteZulipServer.PLAN_TYPE_SELF_MANAGED: "Self-managed", + RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY: CustomerPlan.name_from_tier( + CustomerPlan.TIER_SELF_HOSTED_LEGACY + ), RemoteZulipServer.PLAN_TYPE_COMMUNITY: "Community", RemoteZulipServer.PLAN_TYPE_BUSINESS: "Business", RemoteZulipServer.PLAN_TYPE_ENTERPRISE: "Enterprise", @@ -476,8 +480,8 @@ def remote_servers_support( email_to_search=email_to_search, hostname_to_search=hostname_to_search ) remote_server_to_max_monthly_messages: Dict[int, Union[int, str]] = dict() - server_plan_data: Dict[int, PlanData] = {} - realm_plan_data: Dict[int, PlanData] = {} + server_support_data: Dict[int, SupportData] = {} + realm_support_data: Dict[int, SupportData] = {} remote_realms: Dict[int, List[RemoteRealm]] = {} for remote_server in remote_servers: # Get remote realms attached to remote server @@ -488,12 +492,12 @@ def remote_servers_support( # Get plan data for remote realms for remote_realm in remote_realms[remote_server.id]: realm_billing_session = RemoteRealmBillingSession(remote_realm=remote_realm) - remote_realm_plan_data = get_current_plan_data_for_support_view(realm_billing_session) - realm_plan_data[remote_realm.id] = remote_realm_plan_data + remote_realm_data = get_data_for_support_view(realm_billing_session) + realm_support_data[remote_realm.id] = remote_realm_data # Get plan data for remote server server_billing_session = RemoteServerBillingSession(remote_server=remote_server) - remote_server_plan_data = get_current_plan_data_for_support_view(server_billing_session) - server_plan_data[remote_server.id] = remote_server_plan_data + remote_server_data = get_data_for_support_view(server_billing_session) + server_support_data[remote_server.id] = remote_server_data # Get max monthly messages try: remote_server_to_max_monthly_messages[remote_server.id] = compute_max_monthly_messages( @@ -503,11 +507,10 @@ def remote_servers_support( remote_server_to_max_monthly_messages[remote_server.id] = "Recent data missing" context["remote_servers"] = remote_servers - context["remote_servers_plan_data"] = server_plan_data + context["remote_servers_support_data"] = server_support_data context["remote_server_to_max_monthly_messages"] = remote_server_to_max_monthly_messages context["remote_realms"] = remote_realms - context["remote_realms_plan_data"] = realm_plan_data - context["get_discount"] = get_customer_discount_for_support_view + context["remote_realms_support_data"] = realm_support_data context["get_plan_type_name"] = get_plan_type_string context["get_org_type_display_name"] = get_org_type_display_name context["SPONSORED_PLAN_TYPE"] = RemoteZulipServer.PLAN_TYPE_COMMUNITY diff --git a/analytics/views/user_activity.py b/analytics/views/user_activity.py index 5143f58c8f972..1b1778efb3af1 100644 --- a/analytics/views/user_activity.py +++ b/analytics/views/user_activity.py @@ -11,7 +11,8 @@ make_table, ) from zerver.decorator import require_server_admin -from zerver.models import UserActivity, UserProfile, get_user_profile_by_id +from zerver.models import UserActivity, UserProfile +from zerver.models.users import get_user_profile_by_id if settings.BILLING_ENABLED: pass diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 2f5d63d08b457..c01622b33b7ae 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -18,14 +18,31 @@ clients should check the `zulip_feature_level` field, present in the /register`](/api/register-queue) responses, to determine the API format used by the Zulip server that they are interacting with. +## Changes in Zulip 9.0 + +Feature levels 238-239 are reserved for future use in 8.x maintenance +releases. + ## Changes in Zulip 8.0 +**Feature level 237** + +No changes; feature level used for Zulip 8.0 release. + +**Feature level 236** + +* [`POST /messages`](/api/send-message), [`POST + /scheduled_messages`](/api/create-scheduled-message): The new + `read_by_sender` parameter lets the client override the heuristic + that determines whether the new message will be initially marked + read by its sender. + **Feature level 235** * [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults), [`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings): - Added a new user setting,`automatically_follow_topics_where_mentioned` - that allows user to automatically follow topics where the user is mentioned. + Added a new user setting, `automatically_follow_topics_where_mentioned`, + that allows the user to automatically follow topics where the user is mentioned. **Feature level 234** diff --git a/corporate/lib/analytics.py b/corporate/lib/analytics.py index 0739f4ba78e24..d2e2d40e527cb 100644 --- a/corporate/lib/analytics.py +++ b/corporate/lib/analytics.py @@ -1,13 +1,25 @@ +from dataclasses import dataclass from decimal import Decimal from typing import Any, Dict from django.utils.timezone import now as timezone_now -from corporate.lib.stripe import renewal_amount +from corporate.lib.stripe import ( + RealmBillingSession, + RemoteRealmBillingSession, + RemoteServerBillingSession, +) from corporate.models import Customer, CustomerPlan from zerver.lib.utils import assert_is_not_none +@dataclass +class RemoteActivityPlanData: + current_status: str + current_plan_name: str + annual_revenue: int + + def get_realms_with_default_discount_dict() -> Dict[str, Decimal]: realms_with_default_discount: Dict[str, Any] = {} customers = ( @@ -31,9 +43,52 @@ def estimate_annual_recurring_revenue_by_realm() -> Dict[str, int]: # nocoverag if plan.customer.realm is not None: # TODO: figure out what to do for plans that don't automatically # renew, but which probably will renew - renewal_cents = renewal_amount(plan, timezone_now()) + renewal_cents = RealmBillingSession( + realm=plan.customer.realm + ).get_customer_plan_renewal_amount(plan, timezone_now()) if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY: renewal_cents *= 12 # TODO: Decimal stuff annual_revenue[plan.customer.realm.string_id] = int(renewal_cents / 100) return annual_revenue + + +def get_plan_data_by_remote_server() -> Dict[int, RemoteActivityPlanData]: # nocoverage + remote_server_plan_data: Dict[int, RemoteActivityPlanData] = {} + for plan in CustomerPlan.objects.filter( + status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD, customer__realm__isnull=True + ).select_related("customer__remote_server", "customer__remote_realm"): + renewal_cents = 0 + server_id = None + + if plan.customer.remote_server is not None: + server_id = plan.customer.remote_server.id + renewal_cents = RemoteServerBillingSession( + remote_server=plan.customer.remote_server + ).get_customer_plan_renewal_amount(plan, timezone_now()) + elif plan.customer.remote_realm is not None: + server_id = plan.customer.remote_realm.server.id + renewal_cents = RemoteRealmBillingSession( + remote_realm=plan.customer.remote_realm + ).get_customer_plan_renewal_amount(plan, timezone_now()) + + assert server_id is not None + + if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY: + renewal_cents *= 12 + + current_data = remote_server_plan_data.get(server_id) + if current_data is not None: + current_revenue = remote_server_plan_data[server_id].annual_revenue + remote_server_plan_data[server_id] = RemoteActivityPlanData( + current_status="Multiple plans", + current_plan_name="See support view", + annual_revenue=current_revenue + int(renewal_cents / 100), + ) + else: + remote_server_plan_data[server_id] = RemoteActivityPlanData( + current_status=plan.get_plan_status_as_text(), + current_plan_name=plan.name, + annual_revenue=int(renewal_cents / 100), + ) + return remote_server_plan_data diff --git a/corporate/lib/decorator.py b/corporate/lib/decorator.py index 2325361f5d5d9..dde25911a3f90 100644 --- a/corporate/lib/decorator.py +++ b/corporate/lib/decorator.py @@ -24,7 +24,7 @@ def is_self_hosting_management_subdomain(request: HttpRequest) -> bool: subdomain = get_subdomain(request) - return settings.DEVELOPMENT and subdomain == settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN + return subdomain == settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN def self_hosting_management_endpoint( diff --git a/corporate/lib/registration.py b/corporate/lib/registration.py index 65d50b5d5e5a3..3bfc9c6a787d1 100644 --- a/corporate/lib/registration.py +++ b/corporate/lib/registration.py @@ -7,7 +7,8 @@ from corporate.models import get_current_plan_by_realm from zerver.actions.create_user import send_message_to_signup_notification_stream from zerver.lib.exceptions import InvitationError -from zerver.models import Realm, UserProfile, get_system_bot +from zerver.models import Realm, UserProfile +from zerver.models.users import get_system_bot def generate_licenses_low_warning_message_if_required(realm: Realm) -> Optional[str]: diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index d03d0862ef674..221d2e3ba3939 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -17,6 +17,8 @@ from django.core import signing from django.core.signing import Signer from django.db import transaction +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render from django.urls import reverse from django.utils.timezone import now as timezone_now from django.utils.translation import gettext as _ @@ -46,17 +48,11 @@ send_email_to_billing_admins_and_realm_owners, ) from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime -from zerver.lib.types import RemoteRealmDictValue from zerver.lib.url_encoding import append_url_query_string from zerver.lib.utils import assert_is_not_none -from zerver.models import ( - Realm, - RealmAuditLog, - UserProfile, - get_org_type_display_name, - get_realm, - get_system_bot, -) +from zerver.models import Realm, RealmAuditLog, UserProfile +from zerver.models.realms import get_org_type_display_name, get_realm +from zerver.models.users import get_system_bot from zilencer.lib.remote_counts import MissingDataError from zilencer.models import ( RemoteRealm, @@ -84,6 +80,8 @@ ParamT = ParamSpec("ParamT") ReturnT = TypeVar("ReturnT") +BILLING_SUPPORT_EMAIL = "sales@zulip.com" + MIN_INVOICED_LICENSES = 30 MAX_INVOICED_LICENSES = 1000 DEFAULT_INVOICE_DAYS_UNTIL_DUE = 30 @@ -308,28 +306,6 @@ def next_invoice_date(plan: CustomerPlan) -> Optional[datetime]: return dt -def renewal_amount( - plan: CustomerPlan, event_time: datetime, last_ledger_entry: Optional[LicenseLedger] = None -) -> int: # nocoverage: TODO - if plan.fixed_price is not None: - return plan.fixed_price - new_plan = None - if last_ledger_entry is None: - realm = plan.customer.realm - billing_session = RealmBillingSession(user=None, realm=realm) - new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed( - plan, event_time - ) - if last_ledger_entry is None: - return 0 - if last_ledger_entry.licenses_at_next_renewal is None: - return 0 - if new_plan is not None: - plan = new_plan - assert plan.price_per_license is not None # for mypy - return plan.price_per_license * last_ledger_entry.licenses_at_next_renewal - - def get_amount_to_credit_for_plan_tier_change( current_plan: CustomerPlan, plan_change_date: datetime ) -> int: @@ -422,6 +398,14 @@ class StripeConnectionError(BillingError): pass +class ServerDeactivateWithExistingPlanError(BillingError): # nocoverage + def __init__(self) -> None: + super().__init__( + "server deactivation with existing plan", + "", + ) + + class UpgradeWithExistingPlanError(BillingError): def __init__(self) -> None: super().__init__( @@ -1149,6 +1133,20 @@ def ensure_current_plan_is_upgradable( f"Cannot upgrade from {plan.name} to {CustomerPlan.name_from_tier(new_plan_tier)}" ) + def check_customer_not_on_paid_plan(self, customer: Customer) -> str: # nocoverage + current_plan = get_current_plan_by_customer(customer) + if current_plan is not None: + # Check if the customer is scheduled for an upgrade. + next_plan = self.get_next_plan(current_plan) + if next_plan is not None: + return f"Customer scheduled for upgrade to {next_plan.name}. Please cancel upgrade before approving sponsorship!" + + # It is fine to end legacy plan not scheduled for an upgrade. + if current_plan.tier != CustomerPlan.TIER_SELF_HOSTED_LEGACY: + return f"Customer on plan {current_plan.name}. Please end current plan before approving sponsorship!" + + return "" + @catch_stripe_errors def process_initial_upgrade( self, @@ -1659,6 +1657,26 @@ def get_next_plan(self, plan: CustomerPlan) -> Optional[CustomerPlan]: # nocove ).first() return None + def get_customer_plan_renewal_amount( + self, + plan: CustomerPlan, + event_time: datetime, + last_ledger_entry: Optional[LicenseLedger] = None, + ) -> int: + if plan.fixed_price is not None: + return plan.fixed_price + new_plan = None + if last_ledger_entry is None: + new_plan, last_ledger_entry = self.make_end_of_cycle_updates_if_needed(plan, event_time) + if last_ledger_entry is None: + return 0 # nocoverage + if last_ledger_entry.licenses_at_next_renewal is None: + return 0 # nocoverage + if new_plan is not None: + plan = new_plan # nocoverage + assert plan.price_per_license is not None # for mypy + return plan.price_per_license * last_ledger_entry.licenses_at_next_renewal + def get_billing_context_from_plan( self, customer: Customer, @@ -1708,7 +1726,7 @@ def get_billing_context_from_plan( renewal_cents = monthly_price_per_license * licenses_at_next_renewal price_per_license = format_money(monthly_price_per_license) else: - renewal_cents = renewal_amount(plan, now, last_ledger_entry) + renewal_cents = self.get_customer_plan_renewal_amount(plan, now, last_ledger_entry) if plan.price_per_license is None: price_per_license = "" @@ -2344,7 +2362,7 @@ def get_sponsorship_plan_name( ): # nocoverage return sponsorship_request.requested_plan - # We only support sponsorships for these plans. + # Default name for sponsorship plan. sponsored_plan_name = CustomerPlan.name_from_tier(CustomerPlan.TIER_CLOUD_STANDARD) if is_remotely_hosted: sponsored_plan_name = CustomerPlan.name_from_tier( @@ -2384,6 +2402,9 @@ def get_sponsorship_request_context(self) -> Optional[Dict[str, Any]]: if plan is not None: context["plan_name"] = plan.name context["free_trial"] = plan.is_free_trial() + context["is_server_on_legacy_plan"] = ( + plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY + ) self.add_sponsorship_info_to_context(context) return context @@ -2439,10 +2460,11 @@ def request_sponsorship(self, form: SponsorshipRequestForm) -> None: "paid_users_count": sponsorship_request.paid_users_count, "paid_users_description": sponsorship_request.paid_users_description, "requested_plan": sponsorship_request.requested_plan, + "is_cloud_organization": isinstance(self, RealmBillingSession), } send_email( "zerver/emails/sponsorship_request", - to_emails=[FromAddress.SUPPORT], + to_emails=[BILLING_SUPPORT_EMAIL], # Sent to the server's support team, so this email is not user-facing. from_name="Zulip sponsorship request", from_address=FromAddress.tokenized_no_reply_address(), @@ -2524,12 +2546,16 @@ def update_license_ledger_for_manual_plan( raise AssertionError("Pass licenses or licenses_at_next_renewal") def get_billable_licenses_for_customer( - self, customer: Customer, tier: int, licenses: Optional[int] = None + self, + customer: Customer, + tier: int, + licenses: Optional[int] = None, + event_time: datetime = timezone_now(), ) -> int: if licenses is not None and customer.exempt_from_license_number_check: return licenses - current_licenses_count = self.current_count_for_billed_licenses() + current_licenses_count = self.current_count_for_billed_licenses(event_time) min_licenses_for_plan = self.min_licenses_for_plan(tier) if customer.exempt_from_license_number_check: # nocoverage billed_licenses = current_licenses_count @@ -2550,16 +2576,22 @@ def update_license_ledger_for_automanaged_plan( next_plan = self.get_next_plan(plan) assert next_plan is not None licenses_at_next_renewal = self.get_billable_licenses_for_customer( - plan.customer, next_plan.tier + plan.customer, + next_plan.tier, + event_time=event_time, ) # Current licenses stay as per the limits of current plan. current_plan_licenses_at_next_renewal = self.get_billable_licenses_for_customer( - plan.customer, plan.tier + plan.customer, + plan.tier, + event_time=event_time, ) licenses = max(current_plan_licenses_at_next_renewal, last_ledger_entry.licenses) else: licenses_at_next_renewal = self.get_billable_licenses_for_customer( - plan.customer, plan.tier + plan.customer, + plan.tier, + event_time=event_time, ) licenses = max(licenses_at_next_renewal, last_ledger_entry.licenses) @@ -2610,7 +2642,11 @@ def migrate_customer_to_legacy_plan( **legacy_plan_params, ) - billed_licenses = self.get_billable_licenses_for_customer(customer, legacy_plan.tier) + try: + billed_licenses = self.get_billable_licenses_for_customer(customer, legacy_plan.tier) + except MissingDataError: + billed_licenses = 0 + # Create a ledger entry for the legacy plan for tracking purposes. ledger_entry = LicenseLedger.objects.create( plan=legacy_plan, @@ -2627,6 +2663,8 @@ def migrate_customer_to_legacy_plan( extra_data=legacy_plan_params, ) + self.do_change_plan_type(tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY, is_sponsored=False) + def get_last_ledger_for_automanaged_plan_if_exists( self, ) -> Optional[LicenseLedger]: # nocoverage @@ -2835,10 +2873,15 @@ def approve_sponsorship(self) -> str: # Sponsorship approval is only a support admin action. assert self.support_session + customer = self.get_customer() + if customer is not None: + error_message = self.check_customer_not_on_paid_plan(customer) + if error_message != "": # nocoverage + return error_message + from zerver.actions.message_send import internal_send_private_message self.do_change_plan_type(tier=None, is_sponsored=True) - customer = self.get_customer() if customer is not None and customer.sponsorship_pending: customer.sponsorship_pending = False customer.save(update_fields=["sponsorship_pending"]) @@ -2977,7 +3020,7 @@ def sync_license_ledger_if_needed(self) -> None: # nocoverage pass -class RemoteRealmBillingSession(BillingSession): # nocoverage +class RemoteRealmBillingSession(BillingSession): def __init__( self, remote_realm: RemoteRealm, @@ -2986,7 +3029,8 @@ def __init__( ) -> None: self.remote_realm = remote_realm self.remote_billing_user = remote_billing_user - if support_staff is not None: + self.support_staff = support_staff + if support_staff is not None: # nocoverage assert support_staff.is_staff self.support_session = True else: @@ -2994,12 +3038,12 @@ def __init__( @override @property - def billing_entity_display_name(self) -> str: + def billing_entity_display_name(self) -> str: # nocoverage return self.remote_realm.name @override @property - def billing_session_url(self) -> str: + def billing_session_url(self) -> str: # nocoverage return f"{settings.EXTERNAL_URI_SCHEME}{settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN}.{settings.EXTERNAL_HOST}/realm/{self.remote_realm.uuid}" @override @@ -3008,7 +3052,7 @@ def billing_base_url(self) -> str: return f"/realm/{self.remote_realm.uuid}" @override - def support_url(self) -> str: + def support_url(self) -> str: # nocoverage return build_support_url("remote_servers_support", self.remote_realm.server.hostname) @override @@ -3029,8 +3073,21 @@ def current_count_for_billed_licenses(self, event_time: datetime = timezone_now( ) return remote_realm_counts.non_guest_user_count + remote_realm_counts.guest_user_count + def missing_data_error_page(self, request: HttpRequest) -> HttpResponse: # nocoverage + # The RemoteRealm error page code path should not really be + # possible, in that the self-hosted server will have uploaded + # current audit log data as needed as part of logging the user + # in. + missing_data_context: Dict[str, Any] = { + "remote_realm_session": True, + "supports_remote_realms": self.remote_realm.server.last_api_feature_level is not None, + } + return render( + request, "corporate/server_not_uploading_data.html", context=missing_data_context + ) + @override - def get_audit_log_event(self, event_type: AuditLogEventType) -> int: + def get_audit_log_event(self, event_type: AuditLogEventType) -> int: # nocoverage if event_type is AuditLogEventType.STRIPE_CUSTOMER_CREATED: return RemoteRealmAuditLog.STRIPE_CUSTOMER_CREATED elif event_type is AuditLogEventType.STRIPE_CARD_CHANGED: @@ -3061,7 +3118,7 @@ def write_to_audit_log( event_time: datetime, *, extra_data: Optional[Dict[str, Any]] = None, - ) -> None: + ) -> None: # nocoverage # These audit logs don't use all the fields of `RemoteRealmAuditLog`: # # * remote_id is None because this is not synced from a remote table. @@ -3073,6 +3130,10 @@ def write_to_audit_log( "remote_realm": self.remote_realm, "event_type": audit_log_event, "event_time": event_time, + # At most one of these should be set, but we may + # not want an assert for that yet: + "acting_support_user": self.support_staff, + "acting_remote_user": self.remote_billing_user, } if extra_data: @@ -3081,7 +3142,7 @@ def write_to_audit_log( RemoteRealmAuditLog.objects.create(**log_data) @override - def get_data_for_stripe_customer(self) -> StripeCustomerData: + def get_data_for_stripe_customer(self) -> StripeCustomerData: # nocoverage # Support requests do not set any stripe billing information. assert self.support_session is False metadata: Dict[str, Any] = {} @@ -3097,9 +3158,12 @@ def get_data_for_stripe_customer(self) -> StripeCustomerData: @override def update_data_for_checkout_session_and_payment_intent( self, metadata: Dict[str, Any] - ) -> Dict[str, Any]: - # TODO: Figure out what this should do. + ) -> Dict[str, Any]: # nocoverage + assert self.remote_billing_user is not None updated_metadata = dict( + remote_realm_user_id=self.remote_billing_user.id, + remote_realm_user_email=self.get_email(), + remote_realm_host=self.remote_realm.host, **metadata, ) return updated_metadata @@ -3107,7 +3171,7 @@ def update_data_for_checkout_session_and_payment_intent( @override def update_or_create_customer( self, stripe_customer_id: Optional[str] = None, *, defaults: Optional[Dict[str, Any]] = None - ) -> Customer: + ) -> Customer: # nocoverage if stripe_customer_id is not None: # Support requests do not set any stripe billing information. assert self.support_session is False @@ -3123,21 +3187,18 @@ def update_or_create_customer( return customer @override - def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None: + def do_change_plan_type( + self, *, tier: Optional[int], is_sponsored: bool = False + ) -> None: # nocoverage if is_sponsored: plan_type = RemoteRealm.PLAN_TYPE_COMMUNITY elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: plan_type = RemoteRealm.PLAN_TYPE_BUSINESS - elif ( - tier == CustomerPlan.TIER_SELF_HOSTED_PLUS - ): # nocoverage # Plus plan doesn't use this code path yet. - plan_type = RemoteRealm.PLAN_TYPE_ENTERPRISE + elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: + plan_type = RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY else: raise AssertionError("Unexpected tier") - # TODO: Set usage limits. - # TODO: Set the usage limit in handle_customer_migration_from_server_to_realms. - old_plan_type = self.remote_realm.plan_type self.remote_realm.plan_type = plan_type self.remote_realm.save(update_fields=["plan_type"]) @@ -3148,12 +3209,26 @@ def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False ) @override - def approve_sponsorship(self) -> str: + def approve_sponsorship(self) -> str: # nocoverage # Sponsorship approval is only a support admin action. assert self.support_session - self.do_change_plan_type(tier=None, is_sponsored=True) customer = self.get_customer() + if customer is not None: + error_message = self.check_customer_not_on_paid_plan(customer) + if error_message != "": + return error_message + + if self.remote_realm.plan_type == RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY: + plan = get_current_plan_by_customer(customer) + # Ideally we should have always have a plan here but since this is support page, we can be lenient about it. + if plan is not None: + assert self.get_next_plan(plan) is None + assert plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY + plan.status = CustomerPlan.ENDED + plan.save(update_fields=["status"]) + + self.do_change_plan_type(tier=None, is_sponsored=True) if customer is not None and customer.sponsorship_pending: customer.sponsorship_pending = False customer.save(update_fields=["sponsorship_pending"]) @@ -3170,7 +3245,7 @@ def approve_sponsorship(self) -> str: send_email( "zerver/emails/sponsorship_approved_community_plan", to_emails=billing_emails, - from_address="sales@zulip.com", + from_address=BILLING_SUPPORT_EMAIL, context={ "billing_entity": self.billing_entity_display_name, "plans_link": "https://zulip.com/plans/#self-hosted", @@ -3188,7 +3263,7 @@ def is_sponsored(self) -> bool: return self.remote_realm.plan_type == self.remote_realm.PLAN_TYPE_COMMUNITY @override - def get_metadata_for_stripe_update_card(self) -> Dict[str, Any]: + def get_metadata_for_stripe_update_card(self) -> Dict[str, Any]: # nocoverage assert self.remote_billing_user is not None return {"type": "card_update", "remote_realm_user_id": str(self.remote_billing_user.id)} @@ -3205,10 +3280,10 @@ def get_upgrade_page_session_type_specific_context( ) @override - def process_downgrade(self, plan: CustomerPlan) -> None: + def process_downgrade(self, plan: CustomerPlan) -> None: # nocoverage with transaction.atomic(): old_plan_type = self.remote_realm.plan_type - new_plan_type = RemoteRealm.PLAN_TYPE_SELF_HOSTED + new_plan_type = RemoteRealm.PLAN_TYPE_SELF_MANAGED self.remote_realm.plan_type = new_plan_type self.remote_realm.save(update_fields=["plan_type"]) self.write_to_audit_log( @@ -3223,7 +3298,7 @@ def process_downgrade(self, plan: CustomerPlan) -> None: @override def get_type_of_plan_tier_change( self, current_plan_tier: int, new_plan_tier: int - ) -> PlanTierChangeType: + ) -> PlanTierChangeType: # nocoverage valid_plan_tiers = [ CustomerPlan.TIER_SELF_HOSTED_LEGACY, CustomerPlan.TIER_SELF_HOSTED_BUSINESS, @@ -3251,7 +3326,7 @@ def get_type_of_plan_tier_change( return PlanTierChangeType.DOWNGRADE @override - def has_billing_access(self) -> bool: + def has_billing_access(self) -> bool: # nocoverage # We don't currently have a way to authenticate a remote # session that isn't authorized for billing access. return True @@ -3262,7 +3337,7 @@ def has_billing_access(self) -> bool: ] @override - def on_paid_plan(self) -> bool: + def on_paid_plan(self) -> bool: # nocoverage return self.remote_realm.plan_type in self.PAID_PLANS @override @@ -3283,7 +3358,7 @@ def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None: @override def get_sponsorship_request_session_specific_context( self, - ) -> SponsorshipRequestSessionSpecificContext: + ) -> SponsorshipRequestSessionSpecificContext: # nocoverage assert self.remote_billing_user is not None return SponsorshipRequestSessionSpecificContext( realm_user=None, @@ -3298,7 +3373,7 @@ def get_sponsorship_request_session_specific_context( ) @override - def save_org_type_from_request_sponsorship_session(self, org_type: int) -> None: + def save_org_type_from_request_sponsorship_session(self, org_type: int) -> None: # nocoverage if self.remote_realm.org_type != org_type: self.remote_realm.org_type = org_type self.remote_realm.save(update_fields=["org_type"]) @@ -3326,33 +3401,11 @@ def sync_license_ledger_if_needed(self) -> None: current_plan, audit_log.event_time ) if end_of_cycle_plan is None: - return + return # nocoverage current_plan = end_of_cycle_plan - def get_push_service_validity_dict(self) -> RemoteRealmDictValue: - customer = self.get_customer() - if customer is None: - return {"can_push": True, "expected_end_timestamp": None} - - current_plan = get_current_plan_by_customer(customer) - if current_plan is None: - return {"can_push": True, "expected_end_timestamp": None} - - expected_end_timestamp = None - if current_plan.status in [ - CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, - CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL, - ]: - expected_end_timestamp = datetime_to_timestamp( - self.get_next_billing_cycle(current_plan) - ) - return { - "can_push": True, - "expected_end_timestamp": expected_end_timestamp, - } - -class RemoteServerBillingSession(BillingSession): # nocoverage +class RemoteServerBillingSession(BillingSession): """Billing session for pre-8.0 servers that do not yet support creating RemoteRealm objects.""" @@ -3364,7 +3417,8 @@ def __init__( ) -> None: self.remote_server = remote_server self.remote_billing_user = remote_billing_user - if support_staff is not None: + self.support_staff = support_staff + if support_staff is not None: # nocoverage assert support_staff.is_staff self.support_session = True else: @@ -3372,12 +3426,12 @@ def __init__( @override @property - def billing_entity_display_name(self) -> str: + def billing_entity_display_name(self) -> str: # nocoverage return self.remote_server.hostname @override @property - def billing_session_url(self) -> str: + def billing_session_url(self) -> str: # nocoverage return f"{settings.EXTERNAL_URI_SCHEME}{settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN}.{settings.EXTERNAL_HOST}/server/{self.remote_server.uuid}" @override @@ -3386,7 +3440,7 @@ def billing_base_url(self) -> str: return f"/server/{self.remote_server.uuid}" @override - def support_url(self) -> str: + def support_url(self) -> str: # nocoverage return build_support_url("remote_servers_support", self.remote_server.hostname) @override @@ -3399,7 +3453,9 @@ def get_email(self) -> str: return self.remote_billing_user.email @override - def current_count_for_billed_licenses(self, event_time: datetime = timezone_now()) -> int: + def current_count_for_billed_licenses( + self, event_time: datetime = timezone_now() + ) -> int: # nocoverage if has_stale_audit_log(self.remote_server): raise MissingDataError remote_server_counts = get_remote_server_guest_and_non_guest_count( @@ -3407,8 +3463,19 @@ def current_count_for_billed_licenses(self, event_time: datetime = timezone_now( ) return remote_server_counts.non_guest_user_count + remote_server_counts.guest_user_count + def missing_data_error_page(self, request: HttpRequest) -> HttpResponse: # nocoverage + # The remedy for a RemoteZulipServer login is usually + # upgrading to Zulip 8.0 or enabling SUBMIT_USAGE_STATISTICS. + missing_data_context = { + "remote_realm_session": False, + "supports_remote_realms": self.remote_server.last_api_feature_level is not None, + } + return render( + request, "corporate/server_not_uploading_data.html", context=missing_data_context + ) + @override - def get_audit_log_event(self, event_type: AuditLogEventType) -> int: + def get_audit_log_event(self, event_type: AuditLogEventType) -> int: # nocoverage if event_type is AuditLogEventType.STRIPE_CUSTOMER_CREATED: return RemoteZulipServerAuditLog.STRIPE_CUSTOMER_CREATED elif event_type is AuditLogEventType.STRIPE_CARD_CHANGED: @@ -3439,12 +3506,16 @@ def write_to_audit_log( event_time: datetime, *, extra_data: Optional[Dict[str, Any]] = None, - ) -> None: + ) -> None: # nocoverage audit_log_event = self.get_audit_log_event(event_type) log_data = { "server": self.remote_server, "event_type": audit_log_event, "event_time": event_time, + # At most one of these should be set, but we may + # not want an assert for that yet: + "acting_support_user": self.support_staff, + "acting_remote_user": self.remote_billing_user, } if extra_data: @@ -3453,7 +3524,7 @@ def write_to_audit_log( RemoteZulipServerAuditLog.objects.create(**log_data) @override - def get_data_for_stripe_customer(self) -> StripeCustomerData: + def get_data_for_stripe_customer(self) -> StripeCustomerData: # nocoverage # Support requests do not set any stripe billing information. assert self.support_session is False metadata: Dict[str, Any] = {} @@ -3469,10 +3540,12 @@ def get_data_for_stripe_customer(self) -> StripeCustomerData: @override def update_data_for_checkout_session_and_payment_intent( self, metadata: Dict[str, Any] - ) -> Dict[str, Any]: + ) -> Dict[str, Any]: # nocoverage + assert self.remote_billing_user is not None updated_metadata = dict( - server=self.remote_server, - email=self.get_email(), + remote_server_user_id=self.remote_billing_user.id, + remote_server_user_email=self.get_email(), + remote_server_host=self.remote_server.hostname, **metadata, ) return updated_metadata @@ -3480,7 +3553,7 @@ def update_data_for_checkout_session_and_payment_intent( @override def update_or_create_customer( self, stripe_customer_id: Optional[str] = None, *, defaults: Optional[Dict[str, Any]] = None - ) -> Customer: + ) -> Customer: # nocoverage if stripe_customer_id is not None: # Support requests do not set any stripe billing information. assert self.support_session is False @@ -3496,24 +3569,20 @@ def update_or_create_customer( return customer @override - def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None: - # TODO: Create actual plan types. - + def do_change_plan_type( + self, *, tier: Optional[int], is_sponsored: bool = False + ) -> None: # nocoverage # This function needs to translate between the different # formats of CustomerPlan.tier and RealmZulipServer.plan_type. if is_sponsored: plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS - elif ( - tier == CustomerPlan.TIER_SELF_HOSTED_PLUS - ): # nocoverage # Plus plan doesn't use this code path yet. - plan_type = RemoteZulipServer.PLAN_TYPE_ENTERPRISE + elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: + plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY else: raise AssertionError("Unexpected tier") - # TODO: Set usage limits. - old_plan_type = self.remote_server.plan_type self.remote_server.plan_type = plan_type self.remote_server.save(update_fields=["plan_type"]) @@ -3524,12 +3593,26 @@ def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False ) @override - def approve_sponsorship(self) -> str: + def approve_sponsorship(self) -> str: # nocoverage # Sponsorship approval is only a support admin action. assert self.support_session - self.do_change_plan_type(tier=None, is_sponsored=True) customer = self.get_customer() + if customer is not None: + error_message = self.check_customer_not_on_paid_plan(customer) + if error_message != "": + return error_message + + if self.remote_server.plan_type == RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY: + plan = get_current_plan_by_customer(customer) + # Ideally we should have always have a plan here but since this is support page, we can be lenient about it. + if plan is not None: + assert self.get_next_plan(plan) is None + assert plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY + plan.status = CustomerPlan.ENDED + plan.save(update_fields=["status"]) + + self.do_change_plan_type(tier=None, is_sponsored=True) if customer is not None and customer.sponsorship_pending: customer.sponsorship_pending = False customer.save(update_fields=["sponsorship_pending"]) @@ -3544,7 +3627,7 @@ def approve_sponsorship(self) -> str: send_email( "zerver/emails/sponsorship_approved_community_plan", to_emails=billing_emails, - from_address="sales@zulip.com", + from_address=BILLING_SUPPORT_EMAIL, context={ "billing_entity": self.billing_entity_display_name, "plans_link": "https://zulip.com/plans/#self-hosted", @@ -3554,10 +3637,10 @@ def approve_sponsorship(self) -> str: return f"Sponsorship approved for {self.billing_entity_display_name}" @override - def process_downgrade(self, plan: CustomerPlan) -> None: + def process_downgrade(self, plan: CustomerPlan) -> None: # nocoverage with transaction.atomic(): old_plan_type = self.remote_server.plan_type - new_plan_type = RemoteZulipServer.PLAN_TYPE_SELF_HOSTED + new_plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED self.remote_server.plan_type = new_plan_type self.remote_server.save(update_fields=["plan_type"]) self.write_to_audit_log( @@ -3574,7 +3657,7 @@ def is_sponsored(self) -> bool: return self.remote_server.plan_type == self.remote_server.PLAN_TYPE_COMMUNITY @override - def get_metadata_for_stripe_update_card(self) -> Dict[str, Any]: + def get_metadata_for_stripe_update_card(self) -> Dict[str, Any]: # nocoverage assert self.remote_billing_user is not None return {"type": "card_update", "remote_server_user_id": str(self.remote_billing_user.id)} @@ -3593,7 +3676,7 @@ def get_upgrade_page_session_type_specific_context( @override def get_type_of_plan_tier_change( self, current_plan_tier: int, new_plan_tier: int - ) -> PlanTierChangeType: + ) -> PlanTierChangeType: # nocoverage valid_plan_tiers = [ CustomerPlan.TIER_SELF_HOSTED_LEGACY, CustomerPlan.TIER_SELF_HOSTED_BUSINESS, @@ -3638,11 +3721,11 @@ def has_billing_access(self) -> bool: ] @override - def on_paid_plan(self) -> bool: + def on_paid_plan(self) -> bool: # nocoverage return self.remote_server.plan_type in self.PAID_PLANS @override - def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None: + def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None: # nocoverage context.update( realm_org_type=self.remote_server.org_type, sorted_org_types=sorted( @@ -3659,7 +3742,7 @@ def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None: @override def get_sponsorship_request_session_specific_context( self, - ) -> SponsorshipRequestSessionSpecificContext: + ) -> SponsorshipRequestSessionSpecificContext: # nocoverage assert self.remote_billing_user is not None return SponsorshipRequestSessionSpecificContext( realm_user=None, @@ -3674,13 +3757,13 @@ def get_sponsorship_request_session_specific_context( ) @override - def save_org_type_from_request_sponsorship_session(self, org_type: int) -> None: + def save_org_type_from_request_sponsorship_session(self, org_type: int) -> None: # nocoverage if self.remote_server.org_type != org_type: self.remote_server.org_type = org_type self.remote_server.save(update_fields=["org_type"]) @override - def sync_license_ledger_if_needed(self) -> None: + def sync_license_ledger_if_needed(self) -> None: # nocoverage last_ledger = self.get_last_ledger_for_automanaged_plan_if_exists() if last_ledger is None: return @@ -3827,10 +3910,9 @@ def do_change_remote_server_plan_type(remote_server: RemoteZulipServer, plan_typ @transaction.atomic -def do_deactivate_remote_server(remote_server: RemoteZulipServer) -> None: - # TODO: This should also ensure that the server doesn't have an active plan, - # and deactivate it otherwise. (Like do_deactivate_realm does.) - +def do_deactivate_remote_server( + remote_server: RemoteZulipServer, billing_session: RemoteServerBillingSession +) -> None: if remote_server.deactivated: billing_logger.warning( "Cannot deactivate remote server with ID %d, server has already been deactivated.", @@ -3838,6 +3920,36 @@ def do_deactivate_remote_server(remote_server: RemoteZulipServer) -> None: ) return + server_plans_to_consider = CustomerPlan.objects.filter( + customer__remote_server=remote_server + ).exclude(status=CustomerPlan.ENDED) + realm_plans_to_consider = CustomerPlan.objects.filter( + customer__remote_realm__server=remote_server + ).exclude(status=CustomerPlan.ENDED) + + for possible_plan in list(server_plans_to_consider) + list(realm_plans_to_consider): + if possible_plan.tier in [ + CustomerPlan.TIER_SELF_HOSTED_BASE, + CustomerPlan.TIER_SELF_HOSTED_LEGACY, + CustomerPlan.TIER_SELF_HOSTED_COMMUNITY, + ]: # nocoverage + # No action required for free plans. + continue + + if possible_plan.status in [ + CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL, + CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, + ]: # nocoverage + # No action required for plans scheduled to downgrade + # automatically. + continue + + # This customer has some sort of paid plan; ask the customer + # to downgrade their paid plan so that they get the + # communication in that flow, and then they can come back and + # deactivate their server. + raise ServerDeactivateWithExistingPlanError # nocoverage + remote_server.deactivated = True remote_server.save(update_fields=["deactivated"]) RemoteZulipServerAuditLog.objects.create( @@ -3944,3 +4056,88 @@ def downgrade_small_realms_behind_on_payments_as_needed() -> None: # the last invoice open, void the open invoices. billing_session = RealmBillingSession(user=None, realm=realm) billing_session.void_all_open_invoices() + + +@dataclass +class PushNotificationsEnabledStatus: + can_push: bool + expected_end_timestamp: Optional[int] + + # Not sent to clients, just for debugging + message: str + + +MAX_USERS_WITHOUT_PLAN = 10 + + +def get_push_status_for_remote_request( + remote_server: RemoteZulipServer, remote_realm: Optional[RemoteRealm] +) -> PushNotificationsEnabledStatus: + # First, get the operative Customer object for this + # installation. If there's a `RemoteRealm` customer, that + # takes precedence. + customer = None + + if remote_realm is not None: + billing_session: BillingSession = RemoteRealmBillingSession(remote_realm) + customer = billing_session.get_customer() + + if customer is None: + billing_session = RemoteServerBillingSession(remote_server) + customer = billing_session.get_customer() + + if customer is not None: + current_plan = get_current_plan_by_customer(customer) + else: + current_plan = None + + if billing_session.is_sponsored(): + return PushNotificationsEnabledStatus( + can_push=True, + expected_end_timestamp=None, + message="Community plan", + ) + + if current_plan is not None: + if current_plan.status in [ + CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, + CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL, + ]: + # Plans scheduled to end + expected_end_timestamp = datetime_to_timestamp( + billing_session.get_next_billing_cycle(current_plan) + ) + return PushNotificationsEnabledStatus( + can_push=True, + expected_end_timestamp=expected_end_timestamp, + message="Scheduled end", + ) + + # Current plan, no expected end. + return PushNotificationsEnabledStatus( + can_push=True, + expected_end_timestamp=None, + message="Active plan", + ) + + try: + user_count = billing_session.current_count_for_billed_licenses() + except MissingDataError: + return PushNotificationsEnabledStatus( + can_push=False, + expected_end_timestamp=None, + message="Missing data", + ) + + if user_count > MAX_USERS_WITHOUT_PLAN: + return PushNotificationsEnabledStatus( + can_push=False, + expected_end_timestamp=None, + message="No plan many users", + ) + + return PushNotificationsEnabledStatus( + can_push=True, + expected_end_timestamp=None, + message="No plan", + ) diff --git a/corporate/lib/stripe_event_handler.py b/corporate/lib/stripe_event_handler.py index da90cbe849e77..1cd7d005212c4 100644 --- a/corporate/lib/stripe_event_handler.py +++ b/corporate/lib/stripe_event_handler.py @@ -13,7 +13,7 @@ UpgradeWithExistingPlanError, ) from corporate.models import Customer, CustomerPlan, Event, PaymentIntent, Session -from zerver.models import get_active_user_profile_by_id_in_realm +from zerver.models.users import get_active_user_profile_by_id_in_realm billing_logger = logging.getLogger("corporate.stripe") diff --git a/corporate/lib/support.py b/corporate/lib/support.py index 02a9a6db08bc5..40762459938ef 100644 --- a/corporate/lib/support.py +++ b/corporate/lib/support.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from decimal import Decimal -from typing import Optional +from typing import Optional, TypedDict from urllib.parse import urlencode, urljoin, urlunsplit from django.conf import settings @@ -8,11 +8,34 @@ from django.utils.timezone import now as timezone_now from corporate.lib.stripe import BillingSession -from corporate.models import Customer, CustomerPlan, get_current_plan_by_customer -from zerver.models import Realm, get_realm +from corporate.models import ( + Customer, + CustomerPlan, + ZulipSponsorshipRequest, + get_current_plan_by_customer, +) +from zerver.models import Realm +from zerver.models.realms import get_org_type_display_name, get_realm from zilencer.lib.remote_counts import MissingDataError +class SponsorshipRequestDict(TypedDict): + org_type: str + org_website: str + org_description: str + total_users: str + paid_users: str + paid_users_description: str + requested_plan: str + + +@dataclass +class SponsorshipData: + sponsorship_pending: bool = False + default_discount: Optional[Decimal] = None + latest_sponsorship_request: Optional[SponsorshipRequestDict] = None + + @dataclass class PlanData: customer: Optional["Customer"] = None @@ -24,6 +47,12 @@ class PlanData: warning: Optional[str] = None +@dataclass +class SupportData: + plan_data: PlanData + sponsorship_data: SponsorshipData + + def get_realm_support_url(realm: Realm) -> str: support_realm_uri = get_realm(settings.STAFF_SUBDOMAIN).uri support_url = urljoin( @@ -41,6 +70,40 @@ def get_customer_discount_for_support_view( return customer.default_discount +def get_customer_sponsorship_data(customer: Customer) -> SponsorshipData: + pending = customer.sponsorship_pending + discount = customer.default_discount + sponsorship_request = None + if pending: + last_sponsorship_request = ( + ZulipSponsorshipRequest.objects.filter(customer=customer).order_by("id").last() + ) + if last_sponsorship_request is not None: + org_type_name = get_org_type_display_name(last_sponsorship_request.org_type) + if ( + last_sponsorship_request.org_website is None + or last_sponsorship_request.org_website == "" + ): + website = "No website submitted" + else: + website = last_sponsorship_request.org_website + sponsorship_request = SponsorshipRequestDict( + org_type=org_type_name, + org_website=website, + org_description=last_sponsorship_request.org_description, + total_users=last_sponsorship_request.expected_total_users, + paid_users=last_sponsorship_request.paid_users_count, + paid_users_description=last_sponsorship_request.paid_users_description, + requested_plan=last_sponsorship_request.requested_plan, + ) + + return SponsorshipData( + sponsorship_pending=pending, + default_discount=discount, + latest_sponsorship_request=sponsorship_request, + ) + + def get_current_plan_data_for_support_view(billing_session: BillingSession) -> PlanData: customer = billing_session.get_customer() plan = None @@ -69,3 +132,17 @@ def get_current_plan_data_for_support_view(billing_session: BillingSession) -> P plan_data.has_fixed_price = plan_data.current_plan.fixed_price is not None return plan_data + + +def get_data_for_support_view(billing_session: BillingSession) -> SupportData: + plan_data = get_current_plan_data_for_support_view(billing_session) + customer = billing_session.get_customer() + if customer is not None: + sponsorship_data = get_customer_sponsorship_data(customer) + else: + sponsorship_data = SponsorshipData() + + return SupportData( + plan_data=plan_data, + sponsorship_data=sponsorship_data, + ) diff --git a/corporate/models.py b/corporate/models.py index 3b46b2269736f..57e918997d88e 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -289,6 +289,10 @@ class CustomerPlan(models.Model): # TODO maybe override setattr to ensure billing_cycle_anchor, etc # are immutable. + @override + def __str__(self) -> str: + return f"{self.name} (status: {self.get_plan_status_as_text()})" + @staticmethod def name_from_tier(tier: int) -> str: # NOTE: Check `statement_descriptor` values after updating this. @@ -298,7 +302,7 @@ def name_from_tier(tier: int) -> str: CustomerPlan.TIER_CLOUD_STANDARD: "Zulip Cloud Standard", CustomerPlan.TIER_CLOUD_PLUS: "Zulip Cloud Plus", CustomerPlan.TIER_CLOUD_ENTERPRISE: "Zulip Enterprise", - CustomerPlan.TIER_SELF_HOSTED_LEGACY: "Self-managed", + CustomerPlan.TIER_SELF_HOSTED_LEGACY: "Self-managed (legacy plan)", CustomerPlan.TIER_SELF_HOSTED_BUSINESS: "Zulip Business", CustomerPlan.TIER_SELF_HOSTED_COMMUNITY: "Community", }[tier] diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.create.1.json new file mode 100644 index 0000000000000..a9f571a450f3b --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.create.1.json @@ -0,0 +1,33 @@ +{ + "address": null, + "balance": 0, + "created": 1702576334, + "currency": null, + "default_currency": null, + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": null, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.modify.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.modify.1.json new file mode 100644 index 0000000000000..cd9f838240fe3 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.modify.1.json @@ -0,0 +1,33 @@ +{ + "address": null, + "balance": 0, + "created": 1702576334, + "currency": null, + "default_currency": null, + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.1.json new file mode 100644 index 0000000000000..ca0780edce37c --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.1.json @@ -0,0 +1,79 @@ +{ + "address": null, + "balance": 0, + "created": 1702576334, + "currency": null, + "default_currency": null, + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": { + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2024, + "fingerprint": "9XKsMixKBi6kIIzd", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1702576335, + "customer": "cus_PBgRA4C5R3pi4D", + "id": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "livemode": false, + "metadata": {}, + "object": "payment_method", + "type": "card" + }, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.2.json new file mode 100644 index 0000000000000..ca0780edce37c --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.2.json @@ -0,0 +1,79 @@ +{ + "address": null, + "balance": 0, + "created": 1702576334, + "currency": null, + "default_currency": null, + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": { + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2024, + "fingerprint": "9XKsMixKBi6kIIzd", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1702576335, + "customer": "cus_PBgRA4C5R3pi4D", + "id": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "livemode": false, + "metadata": {}, + "object": "payment_method", + "type": "card" + }, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.3.json new file mode 100644 index 0000000000000..ca0780edce37c --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.3.json @@ -0,0 +1,79 @@ +{ + "address": null, + "balance": 0, + "created": 1702576334, + "currency": null, + "default_currency": null, + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": { + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2024, + "fingerprint": "9XKsMixKBi6kIIzd", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1702576335, + "customer": "cus_PBgRA4C5R3pi4D", + "id": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "livemode": false, + "metadata": {}, + "object": "payment_method", + "type": "card" + }, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.4.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.4.json new file mode 100644 index 0000000000000..ca0780edce37c --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.4.json @@ -0,0 +1,79 @@ +{ + "address": null, + "balance": 0, + "created": 1702576334, + "currency": null, + "default_currency": null, + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": { + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2024, + "fingerprint": "9XKsMixKBi6kIIzd", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1702576335, + "customer": "cus_PBgRA4C5R3pi4D", + "id": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "livemode": false, + "metadata": {}, + "object": "payment_method", + "type": "card" + }, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.5.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.5.json new file mode 100644 index 0000000000000..e62154affaf82 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.5.json @@ -0,0 +1,79 @@ +{ + "address": null, + "balance": 0, + "created": 1702576334, + "currency": "usd", + "default_currency": "usd", + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": { + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2024, + "fingerprint": "9XKsMixKBi6kIIzd", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1702576335, + "customer": "cus_PBgRA4C5R3pi4D", + "id": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "livemode": false, + "metadata": {}, + "object": "payment_method", + "type": "card" + }, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 2, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.1.json new file mode 100644 index 0000000000000..631a36eb0ba04 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.1.json @@ -0,0 +1,60 @@ +{ + "data": [ + { + "api_version": "2020-08-27", + "created": 1702576337, + "data": { + "object": { + "address": null, + "balance": 0, + "created": 1702576334, + "currency": null, + "default_currency": null, + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null + }, + "previous_attributes": { + "invoice_settings": { + "default_payment_method": null + } + } + }, + "id": "evt_1ONJ53DEQaroqDjs6X8RdGoG", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_wwsRfdUsURvz0x", + "idempotency_key": "a708c9c9-d37a-45a2-a538-c68f7bab7c4f" + }, + "type": "customer.updated" + } + ], + "has_more": true, + "object": "list", + "url": "/v1/events" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.2.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.2.json new file mode 100644 index 0000000000000..e2ff6da703f39 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.2.json @@ -0,0 +1,445 @@ +{ + "data": [ + { + "api_version": "2020-08-27", + "created": 1702576341, + "data": { + "object": { + "amount": 80000, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 80000, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "data": [ + { + "amount": 80000, + "amount_captured": 80000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3ONJ56DEQaroqDjs0M2f9IAT", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "ZULIP BUSINESS", + "captured": true, + "created": 1702576341, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "description": "Upgrade to Zulip Business, $80.0 x 10", + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "id": "ch_3ONJ56DEQaroqDjs0BpPKMXi", + "invoice": null, + "livemode": false, + "metadata": { + "billing_modality": "charge_automatically", + "billing_schedule": "1", + "license_management": "automatic", + "licenses": "10", + "plan_tier": "103", + "price_per_license": "8000", + "remote_realm_host": "zulip.testserver", + "remote_realm_user_email": "hamlet@zulip.com", + "remote_realm_user_id": "1", + "seat_count": "10", + "type": "upgrade" + }, + "object": "charge", + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 42, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3ONJ56DEQaroqDjs0GqTybKR", + "payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "payment_method_details": { + "card": { + "amount_authorized": 80000, + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2024, + "extended_authorization": { + "status": "disabled" + }, + "fingerprint": "9XKsMixKBi6kIIzd", + "funding": "credit", + "incremental_authorization": { + "status": "unavailable" + }, + "installments": null, + "last4": "4242", + "mandate": null, + "multicapture": { + "status": "unavailable" + }, + "network": "visa", + "network_token": { + "used": false + }, + "overcapture": { + "maximum_amount_capturable": 80000, + "status": "unavailable" + }, + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": "hamlet@zulip.com", + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKNaB7asGMgb_lG0CwDA6LBZCJioPOi6vvS1_5Jah85xVXTVBIwa3Q40b6QRKTkL8y6vv7b6n9LHk73Xx", + "refunded": false, + "refunds": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/charges/ch_3ONJ56DEQaroqDjs0BpPKMXi/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": "Zulip Business", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3ONJ56DEQaroqDjs0GqTybKR" + }, + "client_secret": "pi_3ONJ56DEQaroqDjs0GqTybKR_secret_fPt0ZuASHDpKHmov3pUovneYg", + "confirmation_method": "automatic", + "created": 1702576340, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "description": "Upgrade to Zulip Business, $80.0 x 10", + "id": "pi_3ONJ56DEQaroqDjs0GqTybKR", + "invoice": null, + "last_payment_error": null, + "latest_charge": "ch_3ONJ56DEQaroqDjs0BpPKMXi", + "livemode": false, + "metadata": { + "billing_modality": "charge_automatically", + "billing_schedule": "1", + "license_management": "automatic", + "licenses": "10", + "plan_tier": "103", + "price_per_license": "8000", + "remote_realm_host": "zulip.testserver", + "remote_realm_user_email": "hamlet@zulip.com", + "remote_realm_user_id": "1", + "seat_count": "10", + "type": "upgrade" + }, + "next_action": null, + "object": "payment_intent", + "on_behalf_of": null, + "payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "payment_method_configuration_details": null, + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": "hamlet@zulip.com", + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": "Zulip Business", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + }, + "id": "evt_3ONJ56DEQaroqDjs0F79v5g4", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_8V6izeXwNoDWNV", + "idempotency_key": "1cdc58e7-dec7-4356-9b72-61b61a9ed3ed" + }, + "type": "payment_intent.succeeded" + }, + { + "api_version": "2020-08-27", + "created": 1702576341, + "data": { + "object": { + "amount": 80000, + "amount_captured": 80000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3ONJ56DEQaroqDjs0M2f9IAT", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "ZULIP BUSINESS", + "captured": true, + "created": 1702576341, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "description": "Upgrade to Zulip Business, $80.0 x 10", + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "id": "ch_3ONJ56DEQaroqDjs0BpPKMXi", + "invoice": null, + "livemode": false, + "metadata": { + "billing_modality": "charge_automatically", + "billing_schedule": "1", + "license_management": "automatic", + "licenses": "10", + "plan_tier": "103", + "price_per_license": "8000", + "remote_realm_host": "zulip.testserver", + "remote_realm_user_email": "hamlet@zulip.com", + "remote_realm_user_id": "1", + "seat_count": "10", + "type": "upgrade" + }, + "object": "charge", + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 42, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3ONJ56DEQaroqDjs0GqTybKR", + "payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "payment_method_details": { + "card": { + "amount_authorized": 80000, + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2024, + "extended_authorization": { + "status": "disabled" + }, + "fingerprint": "9XKsMixKBi6kIIzd", + "funding": "credit", + "incremental_authorization": { + "status": "unavailable" + }, + "installments": null, + "last4": "4242", + "mandate": null, + "multicapture": { + "status": "unavailable" + }, + "network": "visa", + "network_token": { + "used": false + }, + "overcapture": { + "maximum_amount_capturable": 80000, + "status": "unavailable" + }, + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": "hamlet@zulip.com", + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKNWB7asGMgZoVpKVI9Y6LBYFiEbfwSeeDRJVYhm1dXIiKeS-mvibLUviHCmDavwS0pBbUflfN5Ei_gW4", + "refunded": false, + "refunds": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/charges/ch_3ONJ56DEQaroqDjs0BpPKMXi/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": "Zulip Business", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + }, + "id": "evt_3ONJ56DEQaroqDjs0h4RMitN", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_8V6izeXwNoDWNV", + "idempotency_key": "1cdc58e7-dec7-4356-9b72-61b61a9ed3ed" + }, + "type": "charge.succeeded" + }, + { + "api_version": "2020-08-27", + "created": 1702576340, + "data": { + "object": { + "amount": 80000, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/charges?payment_intent=pi_3ONJ56DEQaroqDjs0GqTybKR" + }, + "client_secret": "pi_3ONJ56DEQaroqDjs0GqTybKR_secret_fPt0ZuASHDpKHmov3pUovneYg", + "confirmation_method": "automatic", + "created": 1702576340, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "description": "Upgrade to Zulip Business, $80.0 x 10", + "id": "pi_3ONJ56DEQaroqDjs0GqTybKR", + "invoice": null, + "last_payment_error": null, + "latest_charge": null, + "livemode": false, + "metadata": { + "billing_modality": "charge_automatically", + "billing_schedule": "1", + "license_management": "automatic", + "licenses": "10", + "plan_tier": "103", + "price_per_license": "8000", + "remote_realm_host": "zulip.testserver", + "remote_realm_user_email": "hamlet@zulip.com", + "remote_realm_user_id": "1", + "seat_count": "10", + "type": "upgrade" + }, + "next_action": null, + "object": "payment_intent", + "on_behalf_of": null, + "payment_method": null, + "payment_method_configuration_details": null, + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": "hamlet@zulip.com", + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": "Zulip Business", + "statement_descriptor_suffix": null, + "status": "requires_payment_method", + "transfer_data": null, + "transfer_group": null + } + }, + "id": "evt_3ONJ56DEQaroqDjs07vmQzRX", + "livemode": false, + "object": "event", + "pending_webhooks": 2, + "request": { + "id": "req_8V6izeXwNoDWNV", + "idempotency_key": "1cdc58e7-dec7-4356-9b72-61b61a9ed3ed" + }, + "type": "payment_intent.created" + } + ], + "has_more": false, + "object": "list", + "url": "/v1/events" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.3.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.3.json new file mode 100644 index 0000000000000..15e823a6104b8 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.3.json @@ -0,0 +1,650 @@ +{ + "data": [ + { + "api_version": "2020-08-27", + "created": 1702576344, + "data": { + "object": { + "account_country": "US", + "account_name": "Kandra Labs, Inc.", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "manual", + "charge": null, + "collection_method": "charge_automatically", + "created": 1702576343, + "currency": "usd", + "custom_fields": null, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_address": null, + "customer_email": "hamlet@zulip.com", + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "effective_at": 1702576344, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ102007SAk5Ult?s=ap", + "id": "in_1ONJ59DEQaroqDjsUzP2pYN4", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ102007SAk5Ult/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "data": [ + { + "amount": 80000, + "amount_excluding_tax": 80000, + "currency": "usd", + "description": "Zulip Business", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjssw1k4VTb", + "invoice_item": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 10, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "8000" + }, + { + "amount": -80000, + "amount_excluding_tax": -80000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjsM5QX68m5", + "invoice_item": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "-80000" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1ONJ59DEQaroqDjsUzP2pYN4/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3F94EF3A-0001", + "object": "invoice", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1702576343, + "period_start": 1702576343, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering": { + "amount_tax_display": null, + "pdf": { + "page_size": "letter" + } + }, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Business", + "status": "paid", + "status_transitions": { + "finalized_at": 1702576344, + "marked_uncollectible_at": null, + "paid_at": 1702576344, + "voided_at": null + }, + "subscription": null, + "subscription_details": { + "metadata": null + }, + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": 1702576344 + }, + "previous_attributes": { + "attempted": false, + "auto_advance": true, + "effective_at": null, + "ending_balance": null, + "hosted_invoice_url": null, + "invoice_pdf": null, + "next_payment_attempt": 1702579943, + "number": null, + "paid": false, + "rendering": { + "pdf": { + "page_size": "auto" + } + }, + "status": "draft", + "status_transitions": { + "finalized_at": null, + "paid_at": null + } + } + }, + "id": "evt_1ONJ5BDEQaroqDjsYQhfAK8o", + "livemode": false, + "object": "event", + "pending_webhooks": 2, + "request": { + "id": "req_C4QsvtqC0MULT7", + "idempotency_key": "7e929cfa-04e8-4d96-859e-c862f9d8aa0c" + }, + "type": "invoice.updated" + }, + { + "api_version": "2020-08-27", + "created": 1702576343, + "data": { + "object": { + "account_country": "US", + "account_name": "Kandra Labs, Inc.", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "manual", + "charge": null, + "collection_method": "charge_automatically", + "created": 1702576343, + "currency": "usd", + "custom_fields": null, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_address": null, + "customer_email": "hamlet@zulip.com", + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "effective_at": null, + "ending_balance": null, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": null, + "id": "in_1ONJ59DEQaroqDjsUzP2pYN4", + "invoice_pdf": null, + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "data": [ + { + "amount": 80000, + "amount_excluding_tax": 80000, + "currency": "usd", + "description": "Zulip Business", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjssw1k4VTb", + "invoice_item": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 10, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "8000" + }, + { + "amount": -80000, + "amount_excluding_tax": -80000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjsM5QX68m5", + "invoice_item": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "-80000" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1ONJ59DEQaroqDjsUzP2pYN4/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1702579943, + "number": null, + "object": "invoice", + "on_behalf_of": null, + "paid": false, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1702576343, + "period_start": 1702576343, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering": { + "amount_tax_display": null, + "pdf": { + "page_size": "auto" + } + }, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Business", + "status": "draft", + "status_transitions": { + "finalized_at": null, + "marked_uncollectible_at": null, + "paid_at": null, + "voided_at": null + }, + "subscription": null, + "subscription_details": { + "metadata": null + }, + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": null + } + }, + "id": "evt_1ONJ5ADEQaroqDjsZ0lhyuIF", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_bEUGBI20bpmfLA", + "idempotency_key": "b0b89be4-7c1f-448f-8e47-d16aea228cfa" + }, + "type": "invoice.created" + }, + { + "api_version": "2020-08-27", + "created": 1702576343, + "data": { + "object": { + "amount": 80000, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "date": 1702576343, + "description": "Zulip Business", + "discountable": false, + "discounts": [], + "id": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "quantity": 10, + "subscription": null, + "tax_rates": [], + "test_clock": null, + "unit_amount": 8000, + "unit_amount_decimal": "8000" + } + }, + "id": "evt_1ONJ59DEQaroqDjsXRolg24c", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_qHCiOfgWnK4ggM", + "idempotency_key": "d10ca38c-6487-45b4-837a-044cf8e40e20" + }, + "type": "invoiceitem.created" + }, + { + "api_version": "2020-08-27", + "created": 1702576342, + "data": { + "object": { + "amount": -80000, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "date": 1702576342, + "description": "Payment (Card ending in 4242)", + "discountable": false, + "discounts": [], + "id": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "quantity": 1, + "subscription": null, + "tax_rates": [], + "test_clock": null, + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + } + }, + "id": "evt_1ONJ59DEQaroqDjsQM6q8WPT", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_s6u52P6HI1FqZO", + "idempotency_key": "65868faa-9cb7-4c6f-a6d0-244c36f96ed4" + }, + "type": "invoiceitem.created" + }, + { + "api_version": "2020-08-27", + "created": 1702576342, + "data": { + "object": { + "address": null, + "balance": 0, + "created": 1702576334, + "currency": "usd", + "default_currency": "usd", + "default_source": null, + "delinquent": false, + "description": "zulip.testserver 8bb4095c-58a", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_PBgRA4C5R3pi4D", + "invoice_prefix": "3F94EF3A", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": { + "remote_realm_host": "zulip.testserver", + "remote_realm_uuid": "8bb4095c-58a8-42b8-afd4-9d92e91691e5" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null + }, + "previous_attributes": { + "currency": null, + "default_currency": null + } + }, + "id": "evt_1ONJ59DEQaroqDjsSwL1yXix", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_s6u52P6HI1FqZO", + "idempotency_key": "65868faa-9cb7-4c6f-a6d0-244c36f96ed4" + }, + "type": "customer.updated" + } + ], + "has_more": false, + "object": "list", + "url": "/v1/events" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.4.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.4.json new file mode 100644 index 0000000000000..42236941d150b --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.4.json @@ -0,0 +1,676 @@ +{ + "data": [ + { + "api_version": "2020-08-27", + "created": 1702576344, + "data": { + "object": { + "account_country": "US", + "account_name": "Kandra Labs, Inc.", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "manual", + "charge": null, + "collection_method": "charge_automatically", + "created": 1702576343, + "currency": "usd", + "custom_fields": null, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_address": null, + "customer_email": "hamlet@zulip.com", + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "effective_at": 1702576344, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ102007SAk5Ult?s=ap", + "id": "in_1ONJ59DEQaroqDjsUzP2pYN4", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ102007SAk5Ult/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "data": [ + { + "amount": 80000, + "amount_excluding_tax": 80000, + "currency": "usd", + "description": "Zulip Business", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjssw1k4VTb", + "invoice_item": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 10, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "8000" + }, + { + "amount": -80000, + "amount_excluding_tax": -80000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjsM5QX68m5", + "invoice_item": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "-80000" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1ONJ59DEQaroqDjsUzP2pYN4/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3F94EF3A-0001", + "object": "invoice", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1702576343, + "period_start": 1702576343, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering": { + "amount_tax_display": null, + "pdf": { + "page_size": "letter" + } + }, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Business", + "status": "paid", + "status_transitions": { + "finalized_at": 1702576344, + "marked_uncollectible_at": null, + "paid_at": 1702576344, + "voided_at": null + }, + "subscription": null, + "subscription_details": { + "metadata": null + }, + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": 1702576344 + } + }, + "id": "evt_1ONJ5BDEQaroqDjsqVWjxUDI", + "livemode": false, + "object": "event", + "pending_webhooks": 2, + "request": { + "id": "req_C4QsvtqC0MULT7", + "idempotency_key": "7e929cfa-04e8-4d96-859e-c862f9d8aa0c" + }, + "type": "invoice.payment_succeeded" + }, + { + "api_version": "2020-08-27", + "created": 1702576344, + "data": { + "object": { + "account_country": "US", + "account_name": "Kandra Labs, Inc.", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "manual", + "charge": null, + "collection_method": "charge_automatically", + "created": 1702576343, + "currency": "usd", + "custom_fields": null, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_address": null, + "customer_email": "hamlet@zulip.com", + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "effective_at": 1702576344, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ102007SAk5Ult?s=ap", + "id": "in_1ONJ59DEQaroqDjsUzP2pYN4", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ102007SAk5Ult/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "data": [ + { + "amount": 80000, + "amount_excluding_tax": 80000, + "currency": "usd", + "description": "Zulip Business", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjssw1k4VTb", + "invoice_item": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 10, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "8000" + }, + { + "amount": -80000, + "amount_excluding_tax": -80000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjsM5QX68m5", + "invoice_item": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "-80000" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1ONJ59DEQaroqDjsUzP2pYN4/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3F94EF3A-0001", + "object": "invoice", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1702576343, + "period_start": 1702576343, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering": { + "amount_tax_display": null, + "pdf": { + "page_size": "letter" + } + }, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Business", + "status": "paid", + "status_transitions": { + "finalized_at": 1702576344, + "marked_uncollectible_at": null, + "paid_at": 1702576344, + "voided_at": null + }, + "subscription": null, + "subscription_details": { + "metadata": null + }, + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": 1702576344 + } + }, + "id": "evt_1ONJ5BDEQaroqDjs5cCSUBTH", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_C4QsvtqC0MULT7", + "idempotency_key": "7e929cfa-04e8-4d96-859e-c862f9d8aa0c" + }, + "type": "invoice.paid" + }, + { + "api_version": "2020-08-27", + "created": 1702576344, + "data": { + "object": { + "account_country": "US", + "account_name": "Kandra Labs, Inc.", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "manual", + "charge": null, + "collection_method": "charge_automatically", + "created": 1702576343, + "currency": "usd", + "custom_fields": null, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_address": null, + "customer_email": "hamlet@zulip.com", + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "effective_at": 1702576344, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ102007SAk5Ult?s=ap", + "id": "in_1ONJ59DEQaroqDjsUzP2pYN4", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ102007SAk5Ult/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "data": [ + { + "amount": 80000, + "amount_excluding_tax": 80000, + "currency": "usd", + "description": "Zulip Business", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjssw1k4VTb", + "invoice_item": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 10, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "8000" + }, + { + "amount": -80000, + "amount_excluding_tax": -80000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjsM5QX68m5", + "invoice_item": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "-80000" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1ONJ59DEQaroqDjsUzP2pYN4/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3F94EF3A-0001", + "object": "invoice", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1702576343, + "period_start": 1702576343, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering": { + "amount_tax_display": null, + "pdf": { + "page_size": "letter" + } + }, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Business", + "status": "paid", + "status_transitions": { + "finalized_at": 1702576344, + "marked_uncollectible_at": null, + "paid_at": 1702576344, + "voided_at": null + }, + "subscription": null, + "subscription_details": { + "metadata": null + }, + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": 1702576344 + } + }, + "id": "evt_1ONJ5BDEQaroqDjsw0VZxItF", + "livemode": false, + "object": "event", + "pending_webhooks": 0, + "request": { + "id": "req_C4QsvtqC0MULT7", + "idempotency_key": "7e929cfa-04e8-4d96-859e-c862f9d8aa0c" + }, + "type": "invoice.finalized" + } + ], + "has_more": false, + "object": "list", + "url": "/v1/events" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.5.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.5.json new file mode 100644 index 0000000000000..6d922067afeef --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.5.json @@ -0,0 +1,6 @@ +{ + "data": [], + "has_more": false, + "object": "list", + "url": "/v1/events" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.create.1.json new file mode 100644 index 0000000000000..effea70934fea --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.create.1.json @@ -0,0 +1,208 @@ +{ + "account_country": "US", + "account_name": "Kandra Labs, Inc.", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": true, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "manual", + "charge": null, + "collection_method": "charge_automatically", + "created": 1702576343, + "currency": "usd", + "custom_fields": null, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_address": null, + "customer_email": "hamlet@zulip.com", + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "effective_at": null, + "ending_balance": null, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": null, + "id": "in_1ONJ59DEQaroqDjsUzP2pYN4", + "invoice_pdf": null, + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "data": [ + { + "amount": 80000, + "amount_excluding_tax": 80000, + "currency": "usd", + "description": "Zulip Business", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjssw1k4VTb", + "invoice_item": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 10, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "8000" + }, + { + "amount": -80000, + "amount_excluding_tax": -80000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjsM5QX68m5", + "invoice_item": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "-80000" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1ONJ59DEQaroqDjsUzP2pYN4/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1702579943, + "number": null, + "object": "invoice", + "on_behalf_of": null, + "paid": false, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1702576343, + "period_start": 1702576343, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering": { + "amount_tax_display": null, + "pdf": { + "page_size": "auto" + } + }, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Business", + "status": "draft", + "status_transitions": { + "finalized_at": null, + "marked_uncollectible_at": null, + "paid_at": null, + "voided_at": null + }, + "subscription": null, + "subscription_details": { + "metadata": null + }, + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000000..b1d0b1333982d --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.finalize_invoice.1.json @@ -0,0 +1,208 @@ +{ + "account_country": "US", + "account_name": "Kandra Labs, Inc.", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "manual", + "charge": null, + "collection_method": "charge_automatically", + "created": 1702576343, + "currency": "usd", + "custom_fields": null, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_address": null, + "customer_email": "hamlet@zulip.com", + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "effective_at": 1702576344, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ00200ynvW9wzF?s=ap", + "id": "in_1ONJ59DEQaroqDjsUzP2pYN4", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_17vTkwDEQaroqDjs/test_YWNjdF8xN3ZUa3dERVFhcm9xRGpzLF9QQmdTSkNoaFdYRHpYT0E1QVJ1Y09WQ1lTQU9Lclk4LDkzMTE3MTQ00200ynvW9wzF/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "data": [ + { + "amount": 80000, + "amount_excluding_tax": 80000, + "currency": "usd", + "description": "Zulip Business", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjssw1k4VTb", + "invoice_item": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 10, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "8000" + }, + { + "amount": -80000, + "amount_excluding_tax": -80000, + "currency": "usd", + "description": "Payment (Card ending in 4242)", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "id": "il_1ONJ59DEQaroqDjsM5QX68m5", + "invoice_item": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "livemode": false, + "metadata": {}, + "object": "line_item", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "-80000" + } + ], + "has_more": false, + "object": "list", + "total_count": 2, + "url": "/v1/invoices/in_1ONJ59DEQaroqDjsUzP2pYN4/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3F94EF3A-0001", + "object": "invoice", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1702576343, + "period_start": 1702576343, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering": { + "amount_tax_display": null, + "pdf": { + "page_size": "letter" + } + }, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": "Zulip Business", + "status": "paid", + "status_transitions": { + "finalized_at": 1702576344, + "marked_uncollectible_at": null, + "paid_at": 1702576344, + "voided_at": null + }, + "subscription": null, + "subscription_details": { + "metadata": null + }, + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": 1702576344 +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.list.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.list.1.json new file mode 100644 index 0000000000000..e39960ab7254f --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.list.1.json @@ -0,0 +1,6 @@ +{ + "data": [], + "has_more": false, + "object": "list", + "url": "/v1/invoices" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.1.json new file mode 100644 index 0000000000000..ddb9d9e39e4b2 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.1.json @@ -0,0 +1,47 @@ +{ + "amount": -80000, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "date": 1702576342, + "description": "Payment (Card ending in 4242)", + "discountable": false, + "discounts": [], + "id": "ii_1ONJ58DEQaroqDjs8GcLTLsv", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1702576342, + "start": 1702576342 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1700565834, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OEs3aDEQaroqDjszVnZ9owE", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_OvY3hKgN66WIeP", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": -80000, + "unit_amount_decimal": "-80000" + }, + "proration": false, + "quantity": 1, + "subscription": null, + "tax_rates": [], + "test_clock": null, + "unit_amount": -80000, + "unit_amount_decimal": "-80000" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.2.json new file mode 100644 index 0000000000000..a5dbe96af2c25 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.2.json @@ -0,0 +1,47 @@ +{ + "amount": 80000, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "date": 1702576343, + "description": "Zulip Business", + "discountable": false, + "discounts": [], + "id": "ii_1ONJ59DEQaroqDjsWrf7ze7y", + "invoice": null, + "livemode": false, + "metadata": {}, + "object": "invoiceitem", + "period": { + "end": 1357095845, + "start": 1325473445 + }, + "plan": null, + "price": { + "active": false, + "billing_scheme": "per_unit", + "created": 1701482271, + "currency": "usd", + "custom_unit_amount": null, + "id": "price_1OIiSpDEQaroqDjsgWEnTxtK", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "object": "price", + "product": "prod_P6wL91VQYef5Zp", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 8000, + "unit_amount_decimal": "8000" + }, + "proration": false, + "quantity": 10, + "subscription": null, + "tax_rates": [], + "test_clock": null, + "unit_amount": 8000, + "unit_amount_decimal": "8000" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--PaymentIntent.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--PaymentIntent.create.1.json new file mode 100644 index 0000000000000..4e7ba87775621 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--PaymentIntent.create.1.json @@ -0,0 +1,197 @@ +{ + "amount": 80000, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 80000, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "data": [ + { + "amount": 80000, + "amount_captured": 80000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3ONJ56DEQaroqDjs0M2f9IAT", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "ZULIP BUSINESS", + "captured": true, + "created": 1702576341, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "description": "Upgrade to Zulip Business, $80.0 x 10", + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "id": "ch_3ONJ56DEQaroqDjs0BpPKMXi", + "invoice": null, + "livemode": false, + "metadata": { + "billing_modality": "charge_automatically", + "billing_schedule": "1", + "license_management": "automatic", + "licenses": "10", + "plan_tier": "103", + "price_per_license": "8000", + "remote_realm_host": "zulip.testserver", + "remote_realm_user_email": "hamlet@zulip.com", + "remote_realm_user_id": "1", + "seat_count": "10", + "type": "upgrade" + }, + "object": "charge", + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 42, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3ONJ56DEQaroqDjs0GqTybKR", + "payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "payment_method_details": { + "card": { + "amount_authorized": 80000, + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2024, + "extended_authorization": { + "status": "disabled" + }, + "fingerprint": "9XKsMixKBi6kIIzd", + "funding": "credit", + "incremental_authorization": { + "status": "unavailable" + }, + "installments": null, + "last4": "4242", + "mandate": null, + "multicapture": { + "status": "unavailable" + }, + "network": "visa", + "network_token": { + "used": false + }, + "overcapture": { + "maximum_amount_capturable": 80000, + "status": "unavailable" + }, + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": "hamlet@zulip.com", + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKNWB7asGMgaQgUMl0uk6LBaI3EDlvb9GkU1rXPQWvth9c8efRC6OUx8tpZgICrb4ST6zwYP4VmG6ZRqq", + "refunded": false, + "refunds": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/charges/ch_3ONJ56DEQaroqDjs0BpPKMXi/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": "Zulip Business", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3ONJ56DEQaroqDjs0GqTybKR" + }, + "client_secret": "pi_3ONJ56DEQaroqDjs0GqTybKR_secret_fPt0ZuASHDpKHmov3pUovneYg", + "confirmation_method": "automatic", + "created": 1702576340, + "currency": "usd", + "customer": "cus_PBgRA4C5R3pi4D", + "description": "Upgrade to Zulip Business, $80.0 x 10", + "id": "pi_3ONJ56DEQaroqDjs0GqTybKR", + "invoice": null, + "last_payment_error": null, + "latest_charge": "ch_3ONJ56DEQaroqDjs0BpPKMXi", + "livemode": false, + "metadata": { + "billing_modality": "charge_automatically", + "billing_schedule": "1", + "license_management": "automatic", + "licenses": "10", + "plan_tier": "103", + "price_per_license": "8000", + "remote_realm_host": "zulip.testserver", + "remote_realm_user_email": "hamlet@zulip.com", + "remote_realm_user_id": "1", + "seat_count": "10", + "type": "upgrade" + }, + "next_action": null, + "object": "payment_intent", + "on_behalf_of": null, + "payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "payment_method_configuration_details": null, + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": "hamlet@zulip.com", + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": "Zulip Business", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.create.1.json new file mode 100644 index 0000000000000..bbed0c4ce859b --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.create.1.json @@ -0,0 +1,34 @@ +{ + "application": null, + "automatic_payment_methods": null, + "cancellation_reason": null, + "client_secret": "seti_1ONJ51DEQaroqDjsZNLHHtQn_secret_PBgRmbfqmvy01Qdp2OcqIiVep8ZYauJ", + "created": 1702576335, + "customer": "cus_PBgRA4C5R3pi4D", + "description": null, + "flow_directions": null, + "id": "seti_1ONJ51DEQaroqDjsZNLHHtQn", + "last_setup_error": null, + "latest_attempt": "setatt_1ONJ51DEQaroqDjsi7EH4DD8", + "livemode": false, + "mandate": null, + "metadata": {}, + "next_action": null, + "object": "setup_intent", + "on_behalf_of": null, + "payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "payment_method_configuration_details": null, + "payment_method_options": { + "card": { + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "single_use_mandate": null, + "status": "succeeded", + "usage": "off_session" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.list.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.list.1.json new file mode 100644 index 0000000000000..81f6813884508 --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.list.1.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "application": null, + "automatic_payment_methods": null, + "cancellation_reason": null, + "client_secret": "seti_1ONJ50DEQaroqDjs4pap3aTQ_secret_PBgRHYUx24GJSskzKKwSuQLrKhYNeay", + "created": 1702576334, + "customer": "cus_PBgRA4C5R3pi4D", + "description": null, + "flow_directions": null, + "id": "seti_1ONJ50DEQaroqDjs4pap3aTQ", + "last_setup_error": null, + "latest_attempt": null, + "livemode": false, + "mandate": null, + "metadata": {}, + "next_action": null, + "object": "setup_intent", + "on_behalf_of": null, + "payment_method": null, + "payment_method_configuration_details": null, + "payment_method_options": { + "card": { + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "single_use_mandate": null, + "status": "requires_payment_method", + "usage": "off_session" + } + ], + "has_more": true, + "object": "list", + "url": "/v1/setup_intents" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.retrieve.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.retrieve.1.json new file mode 100644 index 0000000000000..bbed0c4ce859b --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.retrieve.1.json @@ -0,0 +1,34 @@ +{ + "application": null, + "automatic_payment_methods": null, + "cancellation_reason": null, + "client_secret": "seti_1ONJ51DEQaroqDjsZNLHHtQn_secret_PBgRmbfqmvy01Qdp2OcqIiVep8ZYauJ", + "created": 1702576335, + "customer": "cus_PBgRA4C5R3pi4D", + "description": null, + "flow_directions": null, + "id": "seti_1ONJ51DEQaroqDjsZNLHHtQn", + "last_setup_error": null, + "latest_attempt": "setatt_1ONJ51DEQaroqDjsi7EH4DD8", + "livemode": false, + "mandate": null, + "metadata": {}, + "next_action": null, + "object": "setup_intent", + "on_behalf_of": null, + "payment_method": "pm_1ONJ51DEQaroqDjs55u0UX9G", + "payment_method_configuration_details": null, + "payment_method_options": { + "card": { + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "single_use_mandate": null, + "status": "succeeded", + "usage": "off_session" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.create.1.json new file mode 100644 index 0000000000000..13f92597f0fcb --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.create.1.json @@ -0,0 +1,74 @@ +{ + "after_expiration": null, + "allow_promotion_codes": null, + "amount_subtotal": null, + "amount_total": null, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_address_collection": null, + "cancel_url": "http://selfhosting.testserver/realm/8bb4095c-58a8-42b8-afd4-9d92e91691e5/upgrade/", + "client_reference_id": null, + "client_secret": null, + "consent": null, + "consent_collection": null, + "created": 1702576334, + "currency": null, + "currency_conversion": null, + "custom_fields": [], + "custom_text": { + "after_submit": null, + "shipping_address": null, + "submit": null, + "terms_of_service_acceptance": null + }, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_creation": null, + "customer_details": { + "address": null, + "email": "hamlet@zulip.com", + "name": null, + "phone": null, + "tax_exempt": null, + "tax_ids": null + }, + "customer_email": null, + "expires_at": 1702662734, + "id": "cs_test_c1qc1I3QN6VOVyLmmPYRt748pBxWTIgXj1XJRwQN5cD8HCNx5X6vq036GI", + "invoice": null, + "invoice_creation": null, + "livemode": false, + "locale": null, + "metadata": { + "remote_realm_user_id": "1", + "type": "card_update" + }, + "mode": "setup", + "object": "checkout.session", + "payment_intent": null, + "payment_link": null, + "payment_method_collection": "always", + "payment_method_configuration_details": null, + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "payment_status": "no_payment_required", + "phone_number_collection": { + "enabled": false + }, + "recovered_from": null, + "setup_intent": "seti_1ONJ50DEQaroqDjs4pap3aTQ", + "shipping": null, + "shipping_address_collection": null, + "shipping_options": [], + "shipping_rate": null, + "status": "open", + "submit_type": null, + "subscription": null, + "success_url": "http://selfhosting.testserver/realm/8bb4095c-58a8-42b8-afd4-9d92e91691e5/billing/event_status?stripe_session_id={CHECKOUT_SESSION_ID}", + "total_details": null, + "ui_mode": "hosted", + "url": "https://checkout.stripe.com/c/pay/cs_test_c1qc1I3QN6VOVyLmmPYRt748pBxWTIgXj1XJRwQN5cD8HCNx5X6vq036GI#fidkdWxOYHwnPyd1blpxYHZxWl1UZjFOczZJXUE2PUpzUWNPV2ZpdlFzUCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBaZmppcGhrJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl" +} diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.list.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.list.1.json new file mode 100644 index 0000000000000..85feaa1cb212d --- /dev/null +++ b/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.list.1.json @@ -0,0 +1,81 @@ +{ + "data": [ + { + "after_expiration": null, + "allow_promotion_codes": null, + "amount_subtotal": null, + "amount_total": null, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_address_collection": null, + "cancel_url": "http://selfhosting.testserver/realm/8bb4095c-58a8-42b8-afd4-9d92e91691e5/upgrade/", + "client_reference_id": null, + "client_secret": null, + "consent": null, + "consent_collection": null, + "created": 1702576334, + "currency": null, + "currency_conversion": null, + "custom_fields": [], + "custom_text": { + "after_submit": null, + "shipping_address": null, + "submit": null, + "terms_of_service_acceptance": null + }, + "customer": "cus_PBgRA4C5R3pi4D", + "customer_creation": null, + "customer_details": { + "address": null, + "email": "hamlet@zulip.com", + "name": null, + "phone": null, + "tax_exempt": null, + "tax_ids": null + }, + "customer_email": null, + "expires_at": 1702662734, + "id": "cs_test_c1qc1I3QN6VOVyLmmPYRt748pBxWTIgXj1XJRwQN5cD8HCNx5X6vq036GI", + "invoice": null, + "invoice_creation": null, + "livemode": false, + "locale": null, + "metadata": { + "remote_realm_user_id": "1", + "type": "card_update" + }, + "mode": "setup", + "object": "checkout.session", + "payment_intent": null, + "payment_link": null, + "payment_method_collection": "always", + "payment_method_configuration_details": null, + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "payment_status": "no_payment_required", + "phone_number_collection": { + "enabled": false + }, + "recovered_from": null, + "setup_intent": "seti_1ONJ50DEQaroqDjs4pap3aTQ", + "shipping": null, + "shipping_address_collection": null, + "shipping_options": [], + "shipping_rate": null, + "status": "open", + "submit_type": null, + "subscription": null, + "success_url": "http://selfhosting.testserver/realm/8bb4095c-58a8-42b8-afd4-9d92e91691e5/billing/event_status?stripe_session_id={CHECKOUT_SESSION_ID}", + "total_details": null, + "ui_mode": "hosted", + "url": "https://checkout.stripe.com/c/pay/cs_test_c1qc1I3QN6VOVyLmmPYRt748pBxWTIgXj1XJRwQN5cD8HCNx5X6vq036GI#fidkdWxOYHwnPyd1blpxYHZxWl1UZjFOczZJXUE2PUpzUWNPV2ZpdlFzUCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBaZmppcGhrJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl" + } + ], + "has_more": true, + "object": "list", + "url": "/v1/checkout/sessions" +} diff --git a/corporate/tests/test_remote_billing.py b/corporate/tests/test_remote_billing.py index da024a8ffbed2..e681bf8bece6b 100644 --- a/corporate/tests/test_remote_billing.py +++ b/corporate/tests/test_remote_billing.py @@ -15,25 +15,32 @@ RemoteBillingIdentityDict, RemoteBillingUserDict, ) +from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession, add_months +from corporate.models import ( + CustomerPlan, + get_current_plan_by_customer, + get_customer_by_remote_realm, + get_customer_by_remote_server, +) from corporate.views.remote_billing_page import generate_confirmation_link_for_server_deactivation from zerver.lib.remote_server import send_server_data_to_push_bouncer from zerver.lib.test_classes import BouncerTestCase from zerver.lib.timestamp import datetime_to_timestamp -from zerver.models import UserProfile +from zerver.models import Realm, UserProfile from zilencer.models import ( PreregistrationRemoteRealmBillingUser, PreregistrationRemoteServerBillingUser, RemoteRealm, RemoteRealmBillingUser, RemoteServerBillingUser, + RemoteZulipServer, ) if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse -@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") -class RemoteBillingAuthenticationTest(BouncerTestCase): +class RemoteRealmBillingTestCase(BouncerTestCase): def execute_remote_billing_authentication_flow( self, user: UserProfile, @@ -66,7 +73,7 @@ def execute_remote_billing_authentication_flow( # When logging in for the first time some extra steps are needed # to confirm and verify the email address. self.assertEqual(result.status_code, 200) - self.assert_in_success_response(["Enter log in email"], result) + self.assert_in_success_response(["Enter email"], result) self.assert_in_success_response([user.realm.host], result) self.assert_in_success_response( [f'action="/remote-billing-login/{signed_access_token}/confirm/"'], result @@ -80,7 +87,11 @@ def execute_remote_billing_authentication_flow( ) self.assertEqual(result.status_code, 200) self.assert_in_success_response( - ["We have sent a log in link", "link will expire in", user.delivery_email], + [ + "To finish logging in, check your email account (", + ") for a confirmation email from Zulip.", + user.delivery_email, + ], result, ) confirmation_url = self.get_confirmation_url_from_outbox( @@ -157,6 +168,9 @@ def execute_remote_billing_authentication_flow( # depending on the set up and intent of the test. return result + +@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") +class RemoteBillingAuthenticationTest(RemoteRealmBillingTestCase): @responses.activate def test_remote_billing_authentication_flow(self) -> None: self.login("desdemona") @@ -447,8 +461,304 @@ def test_remote_billing_authentication_flow_cant_access_billing_without_finishin # we can't do a nice redirect back to their original server. self.assertEqual(result.status_code, 401) + @responses.activate + def test_transfer_legacy_plan_from_server_to_all_realms(self) -> None: + # Assert current server is not on any plan. + self.assertIsNone(get_customer_by_remote_server(self.server)) + + start_date = timezone_now() + end_date = add_months(timezone_now(), 10) + + # Migrate server to legacy to plan. + server_billing_session = RemoteServerBillingSession(self.server) + server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) + + server_customer = server_billing_session.get_customer() + assert server_customer is not None + server_plan = get_current_plan_by_customer(server_customer) + assert server_plan is not None + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY) + self.assertEqual(server_plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) + self.assertEqual(server_plan.status, CustomerPlan.ACTIVE) + + # There are four test realms on this server: + # , , , + self.assert_length(Realm.objects.all(), 4) + + # Delete any existing remote realms. + RemoteRealm.objects.all().delete() + + # Send server data to push bouncer. + self.add_mock_response() + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # RemoteRealm objects should be created for all realms on the server. + self.assert_length(RemoteRealm.objects.all(), 4) + + # Server plan status was reset + self.server.refresh_from_db() + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) + # Check no CustomerPlan exists for server. + self.assertIsNone(get_current_plan_by_customer(server_customer)) + + # Check legacy CustomerPlan exists for all realms except bot realm. + for remote_realm in RemoteRealm.objects.all(): + if remote_realm.is_system_bot_realm: + self.assertIsNone(get_customer_by_remote_realm(remote_realm)) + continue + + customer = get_customer_by_remote_realm(remote_realm) + assert customer is not None + plan = get_current_plan_by_customer(customer) + assert plan is not None + self.assertEqual(remote_realm.plan_type, RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY) + self.assertEqual(plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) + self.assertEqual(plan.status, CustomerPlan.ACTIVE) + self.assertEqual(plan.billing_cycle_anchor, start_date) + self.assertEqual(plan.end_date, end_date) + + @responses.activate + def test_transfer_legacy_plan_scheduled_for_upgrade_from_server_to_realm( + self, + ) -> None: + # Assert current server is not on any plan. + self.assertIsNone(get_customer_by_remote_server(self.server)) + + start_date = timezone_now() + end_date = add_months(timezone_now(), 10) + + # Migrate server to legacy to plan. + server_billing_session = RemoteServerBillingSession(self.server) + server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) + + server_customer = server_billing_session.get_customer() + assert server_customer is not None + server_plan = get_current_plan_by_customer(server_customer) + assert server_plan is not None + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY) + self.assertEqual(server_plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) + self.assertEqual(server_plan.status, CustomerPlan.ACTIVE) + + # Schedule upgrade for plan. + server_plan.status = CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END + server_plan.save(update_fields=["status"]) + + # Just create a temporary plan and check if gets transferred or not. + server_next_plan = CustomerPlan.objects.create( + customer=server_customer, + billing_cycle_anchor=end_date, + billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL, + tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS, + status=CustomerPlan.NEVER_STARTED, + ) + + # There are four test realms on this server: + # , , , + self.assert_length(Realm.objects.all(), 4) + + # Delete any existing remote realms. + RemoteRealm.objects.all().delete() + + # Send server data to push bouncer. + self.add_mock_response() + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # Server plan status stayed the same. + self.server.refresh_from_db() + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY) + + # RemoteRealm objects should be created for all realms on the server but no customer plans. + self.assert_length(RemoteRealm.objects.all(), 4) + for remote_realm in RemoteRealm.objects.all(): + self.assertIsNone(get_customer_by_remote_realm(remote_realm)) + + # Same customer plan exists for server since there are multiple realms to manage here. + server_plan.refresh_from_db() + self.assertEqual(get_current_plan_by_customer(server_customer), server_plan) + self.assertEqual(server_plan.customer, server_customer) + + # Deactivate realms other than bot realm and zulip realm then try the migration again. + Realm.objects.exclude(string_id__in=["zulip", "zulipinternal"]).update(deactivated=True) + + # Send server data to push bouncer. + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # Server plan status was reset + self.server.refresh_from_db() + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) + # Check if zephyr and lear were deactivated + self.assertEqual( + list(RemoteRealm.objects.filter(realm_deactivated=True).values_list("host", flat=True)), + ["zephyr.testserver", "lear.testserver"], + ) + + # Check legacy CustomerPlan exists for all realms except bot realm. + for remote_realm in RemoteRealm.objects.filter(realm_deactivated=False): + if remote_realm.is_system_bot_realm: + self.assertIsNone(get_customer_by_remote_realm(remote_realm)) + continue + + self.assertEqual(remote_realm.host, "zulip.testserver") + customer = get_customer_by_remote_realm(remote_realm) + assert customer is not None + # Customer got transferred from server to realm. + self.assertEqual(customer, server_customer) + plan = get_current_plan_by_customer(customer) + assert plan is not None + self.assertEqual(remote_realm.plan_type, RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY) + self.assertEqual(plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) + self.assertEqual(plan.status, CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END) + self.assertEqual(plan.billing_cycle_anchor, start_date) + self.assertEqual(plan.end_date, end_date) + self.assertEqual( + RemoteRealmBillingSession(remote_realm).get_next_plan(plan), server_next_plan + ) + + @responses.activate + def test_transfer_business_plan_from_server_to_realm( + self, + ) -> None: + # Assert current server is not on any plan. + self.assertIsNone(get_customer_by_remote_server(self.server)) + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) + + # Add server to business plan. + server_billing_session = RemoteServerBillingSession(self.server) + server_customer = server_billing_session.update_or_create_customer(stripe_customer_id=None) + assert server_customer is not None + + # Just create a temporary plan and check if gets transferred or not. + server_plan = CustomerPlan.objects.create( + customer=server_customer, + billing_cycle_anchor=timezone_now(), + billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL, + tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS, + status=CustomerPlan.ACTIVE, + ) + self.server.plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS + self.server.save(update_fields=["plan_type"]) + + # There are four test realms on this server: + # , , , + self.assert_length(Realm.objects.all(), 4) -class LegacyServerLoginTest(BouncerTestCase): + # Delete any existing remote realms. + RemoteRealm.objects.all().delete() + + # Send server data to push bouncer. + self.add_mock_response() + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # Server plan status stayed the same. + self.server.refresh_from_db() + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_BUSINESS) + + # RemoteRealm objects should be created for all realms on the server but no customer plans. + self.assert_length(RemoteRealm.objects.all(), 4) + for remote_realm in RemoteRealm.objects.all(): + self.assertIsNone(get_customer_by_remote_realm(remote_realm)) + + # Same customer plan exists for server since there are multiple realms to manage here. + server_plan.refresh_from_db() + self.assertEqual(get_current_plan_by_customer(server_customer), server_plan) + self.assertEqual(server_plan.customer, server_customer) + + # Deactivate realms other than bot realm and zulip realm then try the migration again. + Realm.objects.exclude(string_id__in=["zulip", "zulipinternal"]).update(deactivated=True) + + # Send server data to push bouncer. + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # Server plan status was reset + self.server.refresh_from_db() + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) + + # Check business CustomerPlan exists for all realms except bot realm. + for remote_realm in RemoteRealm.objects.filter(realm_deactivated=False): + if remote_realm.is_system_bot_realm: + self.assertIsNone(get_customer_by_remote_realm(remote_realm)) + continue + + self.assertEqual(remote_realm.host, "zulip.testserver") + customer = get_customer_by_remote_realm(remote_realm) + assert customer is not None + # Customer got transferred from server to realm. + self.assertEqual(customer, server_customer) + plan = get_current_plan_by_customer(customer) + assert plan is not None + self.assertEqual(remote_realm.plan_type, RemoteRealm.PLAN_TYPE_BUSINESS) + self.assertEqual(plan.tier, CustomerPlan.TIER_SELF_HOSTED_BUSINESS) + self.assertEqual(plan.status, CustomerPlan.ACTIVE) + + @responses.activate + def test_transfer_plan_from_server_to_realm_edge_cases(self) -> None: + # CASE: Server has no customer + self.assertIsNone(get_customer_by_remote_server(self.server)) + + # Send server data to push bouncer. + self.add_mock_response() + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # Still no customer. + self.assertIsNone(get_customer_by_remote_server(self.server)) + + # CASE: Server has customer but no plan. + server_billing_session = RemoteServerBillingSession(self.server) + server_customer = server_billing_session.update_or_create_customer(stripe_customer_id=None) + + # Send server data to push bouncer. + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # Server still has no plan. + self.assertIsNone(get_current_plan_by_customer(server_customer)) + + # CASE: Server has legacy plan but all realms are deactivated. + start_date = timezone_now() + end_date = add_months(timezone_now(), 10) + server_billing_session = RemoteServerBillingSession(self.server) + server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) + # All realms are deactivated. + Realm.objects.all().update(deactivated=True) + + # Send server data to push bouncer. + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # Server stays on the same plan. + server_plan = get_current_plan_by_customer(server_customer) + assert server_plan is not None + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY) + self.assertEqual(server_plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) + self.assertEqual(server_plan.status, CustomerPlan.ACTIVE) + + # CASE: Server has business plan but all realms are deactivated. + server_plan.tier = CustomerPlan.TIER_SELF_HOSTED_BUSINESS + server_plan.save(update_fields=["tier"]) + self.server.plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS + self.server.save(update_fields=["plan_type"]) + + # Send server data to push bouncer. + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + # Server stays on the same plan. + server_customer.refresh_from_db() + server_plan.refresh_from_db() + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_BUSINESS) + self.assertEqual(server_plan.tier, CustomerPlan.TIER_SELF_HOSTED_BUSINESS) + + # CASE: Server has business plan but there are no realms. + Realm.objects.all().delete() + + # Send server data to push bouncer. + send_server_data_to_push_bouncer(consider_usage_statistics=False) + server_customer.refresh_from_db() + server_plan.refresh_from_db() + # Server stays on same plan. + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_BUSINESS) + self.assertEqual(server_plan.tier, CustomerPlan.TIER_SELF_HOSTED_BUSINESS) + self.assertEqual(server_plan.status, CustomerPlan.ACTIVE) + + +class RemoteServerTestCase(BouncerTestCase): @override def setUp(self) -> None: super().setUp() @@ -507,7 +817,7 @@ def execute_remote_billing_authentication_flow( ) self.assertEqual(result.status_code, 200) self.assert_in_success_response( - ["We have sent a log in link", "link will expire in", email], + ["We have sent", "a log in", "link will expire in", email], result, ) @@ -562,12 +872,12 @@ def execute_remote_billing_authentication_flow( return result + +class LegacyServerLoginTest(RemoteServerTestCase): def test_server_login_get(self) -> None: result = self.client_get("/serverlogin/", subdomain="selfhosting") self.assertEqual(result.status_code, 200) - self.assert_in_success_response( - ["Authenticate server for Zulip billing management"], result - ) + self.assert_in_success_response(["Authenticate server for Zulip plan management"], result) def test_server_login_invalid_zulip_org_id(self) -> None: result = self.client_post( @@ -767,7 +1077,7 @@ def test_generate_deactivation_link(self) -> None: result = self.client_get(confirmation_url, subdomain="selfhosting") self.assert_in_success_response( - ["Log in to Zulip plan management", server.contact_email], result + ["Log in to deactivate registration for", server.contact_email], result ) payload = {"full_name": "test", "tos_consent": "true"} result = self.client_post(confirmation_url, payload, subdomain="selfhosting") @@ -786,7 +1096,9 @@ def test_generate_deactivation_link(self) -> None: result = self.client_post( f"/server/{server.uuid!s}/deactivate/", {"confirmed": "true"}, subdomain="selfhosting" ) - self.assert_in_success_response([f"Registration deactivated for {server.hostname}"], result) + self.assert_in_success_response( + [f"Registration deactivated for
    {server.hostname}"], result + ) server.refresh_from_db() self.assertEqual(server.deactivated, True) diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index be73803a66e53..35f98e12cc953 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -19,7 +19,10 @@ Optional, Sequence, Tuple, + Type, TypeVar, + Union, + cast, ) from unittest import mock from unittest.mock import Mock, patch @@ -31,6 +34,7 @@ import time_machine from django.conf import settings from django.core import signing +from django.test import override_settings from django.urls.resolvers import get_resolver from django.utils.crypto import get_random_string from django.utils.timezone import now as timezone_now @@ -85,6 +89,7 @@ get_current_plan_by_realm, get_customer_by_realm, ) +from corporate.tests.test_remote_billing import RemoteRealmBillingTestCase, RemoteServerTestCase from zerver.actions.create_realm import do_create_realm from zerver.actions.create_user import ( do_activate_mirror_dummy_user, @@ -93,22 +98,19 @@ ) from zerver.actions.realm_settings import do_deactivate_realm, do_reactivate_realm from zerver.actions.users import do_deactivate_user +from zerver.lib.remote_server import send_server_data_to_push_bouncer from zerver.lib.test_classes import ZulipTestCase from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.utils import assert_is_not_none -from zerver.models import ( - Message, - Realm, - RealmAuditLog, - Recipient, - UserProfile, - get_realm, - get_system_bot, -) +from zerver.models import Message, Realm, RealmAuditLog, Recipient, UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_system_bot from zilencer.lib.remote_counts import MissingDataError from zilencer.models import ( RemoteRealm, RemoteRealmAuditLog, + RemoteRealmBillingUser, + RemoteServerBillingUser, RemoteZulipServer, RemoteZulipServerAuditLog, ) @@ -427,6 +429,10 @@ def setUp(self) -> None: hamlet.is_billing_admin = True hamlet.save(update_fields=["is_billing_admin"]) + self.billing_session: Union[ + RealmBillingSession, RemoteRealmBillingSession, RemoteServerBillingSession + ] = RealmBillingSession(user=hamlet, realm=realm) + def get_signed_seat_count_from_response(self, response: "TestHttpResponse") -> Optional[str]: match = re.search(r"name=\"signed_seat_count\" value=\"(.+)\"", response.content.decode()) return match.group(1) if match else None @@ -457,8 +463,8 @@ def get_test_card_string( def assert_details_of_valid_session_from_event_status_endpoint( self, stripe_session_id: str, expected_details: Dict[str, Any] ) -> None: - json_response = self.client_get( - "/json/billing/event/status", + json_response = self.client_billing_get( + "/billing/event/status", { "stripe_session_id": stripe_session_id, }, @@ -471,8 +477,8 @@ def assert_details_of_valid_payment_intent_from_event_status_endpoint( stripe_payment_intent_id: str, expected_details: Dict[str, Any], ) -> None: - json_response = self.client_get( - "/json/billing/event/status", + json_response = self.client_billing_get( + "/billing/event/status", { "stripe_payment_intent_id": stripe_payment_intent_id, }, @@ -528,8 +534,8 @@ def send_stripe_webhook_events(self, most_recent_event: stripe.Event) -> None: most_recent_event = events_old_to_new[-1] def add_card_to_customer_for_upgrade(self, charge_succeeds: bool = True) -> None: - start_session_json_response = self.client_post( - "/json/upgrade/session/start_card_update_session" + start_session_json_response = self.client_billing_post( + "/upgrade/session/start_card_update_session" ) response_dict = self.assert_json_success(start_session_json_response) self.assert_details_of_valid_session_from_event_status_endpoint( @@ -568,7 +574,14 @@ def upgrade( **kwargs: Any, ) -> "TestHttpResponse": if upgrade_page_response is None: - upgrade_page_response = self.client_get("/upgrade/", {}) + if self.billing_session.billing_base_url: + upgrade_page_response = self.client_get( + f"{self.billing_session.billing_base_url}/upgrade/", {}, subdomain="selfhosting" + ) + else: + upgrade_page_response = self.client_get( + f"{self.billing_session.billing_base_url}/upgrade/", {} + ) params: Dict[str, Any] = { "schedule": "annual", "signed_seat_count": self.get_signed_seat_count_from_response(upgrade_page_response), @@ -593,7 +606,7 @@ def upgrade( if talk_to_stripe: [last_event] = iter(stripe.Event.list(limit=1)) - upgrade_json_response = self.client_post("/json/billing/upgrade", params) + upgrade_json_response = self.client_billing_post("/billing/upgrade", params) if upgrade_json_response.status_code != 200 or dont_confirm_payment: # Return early if the upgrade request failed. @@ -622,15 +635,22 @@ def upgrade( self.send_stripe_webhook_events(last_event) return upgrade_json_response - def add_card_and_upgrade(self, user: UserProfile, **kwargs: Any) -> stripe.Customer: + def add_card_and_upgrade( + self, user: Optional[UserProfile] = None, **kwargs: Any + ) -> stripe.Customer: # Add card with time_machine.travel(self.now, tick=False): self.add_card_to_customer_for_upgrade() # Check that we correctly created a Customer object in Stripe - stripe_customer = stripe_get_customer( - assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id) - ) + if user is not None: + stripe_customer = stripe_get_customer( + assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id) + ) + else: + customer = self.billing_session.get_customer() + assert customer is not None + stripe_customer = stripe_get_customer(assert_is_not_none(customer.stripe_customer_id)) self.assertTrue(stripe_customer_has_credit_card_as_default_payment_method(stripe_customer)) with time_machine.travel(self.now, tick=False): @@ -688,6 +708,30 @@ def setup_mocked_stripe(self, callback: Callable[..., Any], *args: Any, **kwargs callback(*args, **kwargs) return mocked + def client_billing_get(self, url_suffix: str, info: Mapping[str, Any] = {}) -> Any: + url = f"/json{self.billing_session.billing_base_url}" + url_suffix + if self.billing_session.billing_base_url: + response = self.client_get(url, info, subdomain="selfhosting") + else: + response = self.client_get(url, info) + return response + + def client_billing_post(self, url_suffix: str, info: Mapping[str, Any] = {}) -> Any: + url = f"/json{self.billing_session.billing_base_url}" + url_suffix + if self.billing_session.billing_base_url: + response = self.client_post(url, info, subdomain="selfhosting") + else: + response = self.client_post(url, info) + return response + + def client_billing_patch(self, url_suffix: str, info: Mapping[str, Any] = {}) -> Any: + url = f"/json{self.billing_session.billing_base_url}" + url_suffix + if self.billing_session.billing_base_url: + response = self.client_patch(url, info, subdomain="selfhosting") # nocoverage + else: + response = self.client_patch(url, info) + return response + class StripeTest(StripeTestCase): def test_catch_stripe_errors(self) -> None: @@ -1456,8 +1500,8 @@ def test_upgrade_race_condition_during_card_upgrade(self, *mocks: Mock) -> None: self.login_user(hamlet) hamlet_upgrade_page_response = self.client_get("/upgrade/") self.add_card_to_customer_for_upgrade() - self.client_post( - "/json/billing/upgrade", + self.client_billing_post( + "/billing/upgrade", { "billing_modality": "charge_automatically", "schedule": "annual", @@ -1741,7 +1785,7 @@ def test_request_sponsorship_form_with_invalid_url(self) -> None: "paid_users_description": "We have 1 paid user.", } - response = self.client_post("/json/billing/sponsorship", data) + response = self.client_billing_post("/billing/sponsorship", data) self.assert_json_error(response, "Enter a valid URL.") @@ -1757,7 +1801,7 @@ def test_request_sponsorship_form_with_blank_url(self) -> None: "paid_users_description": "We have 1 paid user.", } - response = self.client_post("/json/billing/sponsorship", data) + response = self.client_billing_post("/billing/sponsorship", data) self.assert_json_success(response) @@ -1823,7 +1867,7 @@ def test_request_sponsorship(self) -> None: "paid_users_count": "1 user", "paid_users_description": "We have 1 paid user.", } - response = self.client_post("/json/billing/sponsorship", data) + response = self.client_billing_post("/billing/sponsorship", data) self.assert_json_success(response) customer = get_customer_by_realm(user.realm) @@ -1848,7 +1892,7 @@ def test_request_sponsorship(self) -> None: for message in outbox: self.assert_length(message.to, 1) - self.assertEqual(message.to[0], "desdemona+admin@zulip.com") + self.assertEqual(message.to[0], "sales@zulip.com") self.assertEqual(message.subject, "Sponsorship request (Open-source project) for zulip") self.assertEqual(message.reply_to, ["hamlet@zulip.com"]) self.assertEqual(self.email_envelope_from(message), settings.NOREPLY_EMAIL_ADDRESS) @@ -2094,8 +2138,8 @@ def test_replace_payment_method(self, *mocks: Mock) -> None: stripe.Invoice.finalize_invoice(stripe_invoice) RealmAuditLog.objects.filter(event_type=RealmAuditLog.STRIPE_CARD_CHANGED).delete() - start_session_json_response = self.client_post( - "/json/billing/session/start_card_update_session" + start_session_json_response = self.client_billing_post( + "/billing/session/start_card_update_session" ) response_dict = self.assert_json_success(start_session_json_response) self.assert_details_of_valid_session_from_event_status_endpoint( @@ -2115,8 +2159,8 @@ def test_replace_payment_method(self, *mocks: Mock) -> None: self.get_test_card_string(attaches_to_customer=False) ) - start_session_json_response = self.client_post( - "/json/billing/session/start_card_update_session" + start_session_json_response = self.client_billing_post( + "/billing/session/start_card_update_session" ) response_dict = self.assert_json_success(start_session_json_response) self.assert_details_of_valid_session_from_event_status_endpoint( @@ -2160,8 +2204,8 @@ def test_replace_payment_method(self, *mocks: Mock) -> None: response = self.client_get("/billing/") self.assert_in_success_response(["No payment method on file."], response) - start_session_json_response = self.client_post( - "/json/billing/session/start_card_update_session" + start_session_json_response = self.client_billing_post( + "/billing/session/start_card_update_session" ) self.assert_json_success(start_session_json_response) self.trigger_stripe_checkout_session_completed_webhook( @@ -2181,8 +2225,8 @@ def test_replace_payment_method(self, *mocks: Mock) -> None: ) self.login_user(self.example_user("iago")) - response = self.client_get( - "/json/billing/event/status", + response = self.client_billing_get( + "/billing/event/status", {"stripe_session_id": response_dict["stripe_session_id"]}, ) self.assert_json_error_contains( @@ -2201,8 +2245,9 @@ def test_replace_payment_method(self, *mocks: Mock) -> None: ) # Test if manual license management upgrade session is created and is successfully recovered. - start_session_json_response = self.client_post( - "/json/upgrade/session/start_card_update_session", {"manual_license_management": "true"} + start_session_json_response = self.client_billing_post( + "/upgrade/session/start_card_update_session", + {"manual_license_management": "true"}, ) response_dict = self.assert_json_success(start_session_json_response) self.assert_details_of_valid_session_from_event_status_endpoint( @@ -2227,8 +2272,9 @@ def test_downgrade(self) -> None: self.assertEqual(plan.licenses_at_next_renewal(), self.seat_count) with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch( - "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} + response = self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}, ) stripe_customer_id = Customer.objects.get(realm=user.realm).id new_plan = get_current_plan_by_realm(user.realm) @@ -2346,8 +2392,8 @@ def test_switch_from_monthly_plan_to_annual_plan_for_automatic_license_managemen with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch( - "/json/billing/plan", + response = self.client_billing_patch( + "/billing/plan", {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}, ) expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {stripe_customer_id}, CustomerPlan.id: {new_plan.id}, status: {CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}" @@ -2538,8 +2584,8 @@ def test_switch_from_monthly_plan_to_annual_plan_for_manual_license_management( assert new_plan is not None with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch( - "/json/billing/plan", + response = self.client_billing_patch( + "/billing/plan", {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}, ) self.assertEqual( @@ -2655,8 +2701,8 @@ def test_switch_from_annual_plan_to_monthly_plan_for_automatic_license_managemen assert self.now is not None with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch( - "/json/billing/plan", + response = self.client_billing_patch( + "/billing/plan", {"status": CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE}, ) expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {stripe_customer_id}, CustomerPlan.id: {new_plan.id}, status: {CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE}" @@ -2825,8 +2871,9 @@ def test_reupgrade_after_plan_status_changed_to_downgrade_at_end_of_cycle(self) ) with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch( - "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} + response = self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}, ) stripe_customer_id = Customer.objects.get(realm=user.realm).id new_plan = get_current_plan_by_realm(user.realm) @@ -2839,7 +2886,10 @@ def test_reupgrade_after_plan_status_changed_to_downgrade_at_end_of_cycle(self) self.assertEqual(plan.status, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE) with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch("/json/billing/plan", {"status": CustomerPlan.ACTIVE}) + response = self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.ACTIVE}, + ) expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {stripe_customer_id}, CustomerPlan.id: {new_plan.id}, status: {CustomerPlan.ACTIVE}" self.assertEqual(m.output[0], expected_log) self.assert_json_success(response) @@ -2867,8 +2917,9 @@ def test_downgrade_during_invoicing(self, *mocks: Mock) -> None: new_plan = get_current_plan_by_realm(user.realm) assert new_plan is not None with time_machine.travel(self.now, tick=False): - self.client_patch( - "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} + self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}, ) expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {stripe_customer_id}, CustomerPlan.id: {new_plan.id}, status: {CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}" self.assertEqual(m.output[0], expected_log) @@ -2899,8 +2950,8 @@ def test_switch_now_free_trial_from_monthly_to_annual(self, *mocks: Mock) -> Non customer = get_customer_by_realm(user.realm) assert customer is not None - result = self.client_patch( - "/json/billing/plan", + result = self.client_billing_patch( + "/billing/plan", { "status": CustomerPlan.FREE_TRIAL, "schedule": CustomerPlan.BILLING_SCHEDULE_ANNUAL, @@ -2954,8 +3005,8 @@ def test_switch_now_free_trial_from_annual_to_monthly(self, *mocks: Mock) -> Non customer = get_customer_by_realm(user.realm) assert customer is not None - result = self.client_patch( - "/json/billing/plan", + result = self.client_billing_patch( + "/billing/plan", { "status": CustomerPlan.FREE_TRIAL, "schedule": CustomerPlan.BILLING_SCHEDULE_MONTHLY, @@ -3020,7 +3071,10 @@ def test_end_free_trial(self) -> None: self.login_user(user) with time_machine.travel(self.now, tick=False): - self.client_patch("/json/billing/plan", {"status": CustomerPlan.ENDED}) + self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.ENDED}, + ) plan.refresh_from_db() self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_LIMITED) @@ -3064,8 +3118,8 @@ def test_downgrade_at_end_of_free_trial(self) -> None: # Schedule downgrade with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch( - "/json/billing/plan", + response = self.client_billing_patch( + "/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL}, ) stripe_customer_id = Customer.objects.get(realm=user.realm).id @@ -3175,8 +3229,8 @@ def test_cancel_downgrade_at_end_of_free_trial(self) -> None: # Schedule downgrade with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch( - "/json/billing/plan", + response = self.client_billing_patch( + "/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL}, ) stripe_customer_id = Customer.objects.get(realm=user.realm).id @@ -3195,8 +3249,9 @@ def test_cancel_downgrade_at_end_of_free_trial(self) -> None: # Cancel downgrade with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - response = self.client_patch( - "/json/billing/plan", {"status": CustomerPlan.FREE_TRIAL} + response = self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.FREE_TRIAL}, ) stripe_customer_id = Customer.objects.get(realm=user.realm).id new_plan = get_current_plan_by_realm(user.realm) @@ -3222,8 +3277,9 @@ def test_reupgrade_by_billing_admin_after_downgrade(self) -> None: self.login_user(user) with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - self.client_patch( - "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} + self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}, ) stripe_customer_id = Customer.objects.get(realm=user.realm).id new_plan = get_current_plan_by_realm(user.realm) @@ -3281,38 +3337,44 @@ def test_update_licenses_of_manual_plan_from_billing_page(self, *mocks: Mock) -> self.upgrade(invoice=True, licenses=100) with time_machine.travel(self.now, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses": 100}) + result = self.client_billing_patch("/billing/plan", {"licenses": 100}) self.assert_json_error_contains( result, "Your plan is already on 100 licenses in the current billing period." ) with time_machine.travel(self.now, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 100}) + result = self.client_billing_patch( + "/billing/plan", + {"licenses_at_next_renewal": 100}, + ) self.assert_json_error_contains( result, "Your plan is already scheduled to renew with 100 licenses." ) with time_machine.travel(self.now, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses": 50}) + result = self.client_billing_patch("/billing/plan", {"licenses": 50}) self.assert_json_error_contains( result, "You cannot decrease the licenses in the current billing period." ) with time_machine.travel(self.now, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 25}) + result = self.client_billing_patch( + "/billing/plan", + {"licenses_at_next_renewal": 25}, + ) self.assert_json_error_contains( result, "You must purchase licenses for all active users in your organization (minimum 30).", ) with time_machine.travel(self.now, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses": 2000}) + result = self.client_billing_patch("/billing/plan", {"licenses": 2000}) self.assert_json_error_contains( result, "Invoices with more than 1000 licenses can't be processed from this page." ) with time_machine.travel(self.now, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses": 150}) + result = self.client_billing_patch("/billing/plan", {"licenses": 150}) self.assert_json_success(result) invoice_plans_as_needed(self.next_year) stripe_customer = stripe_get_customer( @@ -3362,7 +3424,10 @@ def test_update_licenses_of_manual_plan_from_billing_page(self, *mocks: Mock) -> self.assertEqual(extra_license_item.get(key), value) with time_machine.travel(self.next_year, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 120}) + result = self.client_billing_patch( + "/billing/plan", + {"licenses_at_next_renewal": 120}, + ) self.assert_json_success(result) invoice_plans_as_needed(self.next_year + timedelta(days=365)) stripe_customer = stripe_get_customer( @@ -3415,8 +3480,8 @@ def test_update_licenses_of_manual_plan_from_billing_page_exempt_from_license_nu self.local_upgrade(100, False, CustomerPlan.BILLING_SCHEDULE_ANNUAL, True, False) with time_machine.travel(self.now, tick=False): - result = self.client_patch( - "/json/billing/plan", + result = self.client_billing_patch( + "/billing/plan", {"licenses_at_next_renewal": get_latest_seat_count(user.realm) - 2}, ) @@ -3465,11 +3530,14 @@ def test_update_licenses_of_automatic_plan_from_billing_page(self) -> None: ) with time_machine.travel(self.now, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses": 100}) + result = self.client_billing_patch("/billing/plan", {"licenses": 100}) self.assert_json_error_contains(result, "Your plan is on automatic license management.") with time_machine.travel(self.now, tick=False): - result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 100}) + result = self.client_billing_patch( + "/billing/plan", + {"licenses_at_next_renewal": 100}, + ) self.assert_json_error_contains(result, "Your plan is on automatic license management.") def test_update_plan_with_invalid_status(self) -> None: @@ -3479,8 +3547,8 @@ def test_update_plan_with_invalid_status(self) -> None: ) self.login_user(self.example_user("hamlet")) - response = self.client_patch( - "/json/billing/plan", + response = self.client_billing_patch( + "/billing/plan", {"status": CustomerPlan.NEVER_STARTED}, ) self.assert_json_error_contains(response, "Invalid status") @@ -3493,7 +3561,7 @@ def test_update_plan_without_any_params(self) -> None: self.login_user(self.example_user("hamlet")) with time_machine.travel(self.now, tick=False): - response = self.client_patch("/json/billing/plan", {}) + response = self.client_billing_patch("/billing/plan", {}) self.assert_json_error_contains(response, "Nothing to change") def test_update_plan_that_which_is_due_for_expiry(self) -> None: @@ -3505,8 +3573,9 @@ def test_update_plan_that_which_is_due_for_expiry(self) -> None: self.login_user(self.example_user("hamlet")) with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - result = self.client_patch( - "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} + result = self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}, ) self.assert_json_success(result) self.assertRegex( @@ -3515,7 +3584,10 @@ def test_update_plan_that_which_is_due_for_expiry(self) -> None: ) with time_machine.travel(self.next_year, tick=False): - result = self.client_patch("/json/billing/plan", {"status": CustomerPlan.ACTIVE}) + result = self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.ACTIVE}, + ) self.assert_json_error_contains( result, "Unable to update the plan. The plan has ended." ) @@ -3529,8 +3601,9 @@ def test_update_plan_that_which_is_due_for_replacement(self) -> None: self.login_user(self.example_user("hamlet")) with self.assertLogs("corporate.stripe", "INFO") as m: with time_machine.travel(self.now, tick=False): - result = self.client_patch( - "/json/billing/plan", {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE} + result = self.client_billing_patch( + "/billing/plan", + {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}, ) self.assert_json_success(result) self.assertRegex( @@ -3539,7 +3612,7 @@ def test_update_plan_that_which_is_due_for_replacement(self) -> None: ) with time_machine.travel(self.next_month, tick=False): - result = self.client_patch("/json/billing/plan", {}) + result = self.client_billing_patch("/billing/plan", {}) self.assert_json_error_contains( result, "Unable to update the plan. The plan has been expired and replaced with a new plan.", @@ -4578,7 +4651,7 @@ def test_change_remote_server_plan_type(self) -> None: hostname="demo.example.com", contact_email="email@example.com", ) - self.assertEqual(remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_HOSTED) + self.assertEqual(remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) do_change_remote_server_plan_type(remote_server, RemoteZulipServer.PLAN_TYPE_BUSINESS) @@ -4588,7 +4661,7 @@ def test_change_remote_server_plan_type(self) -> None: ).last() assert remote_realm_audit_log is not None expected_extra_data = { - "old_value": RemoteZulipServer.PLAN_TYPE_SELF_HOSTED, + "old_value": RemoteZulipServer.PLAN_TYPE_SELF_MANAGED, "new_value": RemoteZulipServer.PLAN_TYPE_BUSINESS, } self.assertEqual(remote_realm_audit_log.extra_data, expected_extra_data) @@ -4604,7 +4677,8 @@ def test_deactivate_remote_server(self) -> None: ) self.assertFalse(remote_server.deactivated) - do_deactivate_remote_server(remote_server) + billing_session = RemoteServerBillingSession(remote_server) + do_deactivate_remote_server(remote_server, billing_session) remote_server = RemoteZulipServer.objects.get(uuid=server_uuid) remote_realm_audit_log = RemoteZulipServerAuditLog.objects.filter( @@ -4615,7 +4689,7 @@ def test_deactivate_remote_server(self) -> None: # Try to deactivate a remote server that is already deactivated with self.assertLogs("corporate.stripe", "WARN") as warning_log: - do_deactivate_remote_server(remote_server) + do_deactivate_remote_server(remote_server, billing_session) self.assertEqual( warning_log.output, [ @@ -5431,3 +5505,425 @@ def test_downgrade_realm_and_void_open_invoices(self, *mocks: Mock) -> None: self.assertEqual(success_message, "zulip downgraded and voided 1 open invoices") original_plan.refresh_from_db() self.assertEqual(original_plan.status, CustomerPlan.ENDED) + + +class TestRemoteBillingWriteAuditLog(StripeTestCase): + def test_write_audit_log(self) -> None: + support_admin = self.example_user("iago") + server_uuid = str(uuid.uuid4()) + remote_server = RemoteZulipServer.objects.create( + uuid=server_uuid, + api_key="magic_secret_api_key", + hostname="demo.example.com", + contact_email="email@example.com", + ) + realm_uuid = str(uuid.uuid4()) + remote_realm = RemoteRealm.objects.create( + server=remote_server, + uuid=realm_uuid, + uuid_owner_secret="dummy-owner-secret", + host="dummy-hostname", + realm_date_created=timezone_now(), + ) + remote_realm_billing_user = RemoteRealmBillingUser.objects.create( + remote_realm=remote_realm, email="admin@example.com", user_uuid=uuid.uuid4() + ) + remote_server_billing_user = RemoteServerBillingUser.objects.create( + remote_server=remote_server, email="admin@example.com" + ) + event_time = timezone_now() + + def assert_audit_log( + audit_log: Union[RemoteRealmAuditLog, RemoteZulipServerAuditLog], + acting_remote_user: Optional[Union[RemoteRealmBillingUser, RemoteServerBillingUser]], + acting_support_user: Optional[UserProfile], + event_type: int, + event_time: datetime, + ) -> None: + self.assertEqual(audit_log.event_type, event_type) + self.assertEqual(audit_log.event_time, event_time) + self.assertEqual(audit_log.acting_remote_user, acting_remote_user) + self.assertEqual(audit_log.acting_support_user, acting_support_user) + + for session_class, audit_log_class, remote_object, remote_user in [ + ( + RemoteRealmBillingSession, + RemoteRealmAuditLog, + remote_realm, + remote_realm_billing_user, + ), + ( + RemoteServerBillingSession, + RemoteZulipServerAuditLog, + remote_server, + remote_server_billing_user, + ), + ]: + # Necessary cast or mypy doesn't understand that we can use Django's + # model .objects. style queries on this. + audit_log_model = cast( + Union[Type[RemoteRealmAuditLog], Type[RemoteZulipServerAuditLog]], audit_log_class + ) + assert isinstance(remote_user, (RemoteRealmBillingUser, RemoteServerBillingUser)) + # No acting user: + session = session_class(remote_object) + session.write_to_audit_log( + # This "ordinary billing" event type value gets translated by write_to_audit_log + # into a RemoteRealmBillingSession.CUSTOMER_PLAN_CREATED or + # RemoteServerBillingSession.CUSTOMER_PLAN_CREATED value. + event_type=AuditLogEventType.CUSTOMER_PLAN_CREATED, + event_time=event_time, + ) + audit_log = audit_log_model.objects.latest("id") + assert_audit_log( + audit_log, None, None, audit_log_class.CUSTOMER_PLAN_CREATED, event_time + ) + + session = session_class(remote_object, remote_billing_user=remote_user) + session.write_to_audit_log( + event_type=AuditLogEventType.CUSTOMER_PLAN_CREATED, + event_time=event_time, + ) + audit_log = audit_log_model.objects.latest("id") + assert_audit_log( + audit_log, remote_user, None, audit_log_class.CUSTOMER_PLAN_CREATED, event_time + ) + + session = session_class( + remote_object, remote_billing_user=None, support_staff=support_admin + ) + session.write_to_audit_log( + event_type=AuditLogEventType.CUSTOMER_PLAN_CREATED, + event_time=event_time, + ) + audit_log = audit_log_model.objects.latest("id") + assert_audit_log( + audit_log, None, support_admin, audit_log_class.CUSTOMER_PLAN_CREATED, event_time + ) + + +@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") +class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): + @override + def setUp(self) -> None: + # We need to time travel to 2012-1-2 because super().setUp() + # creates users and changes roles with event_time=timezone_now(). + # That affects the LicenseLedger queries as their event_time would + # be more recent than other operations we perform in this test. + with time_machine.travel(datetime(2012, 1, 2, 3, 4, 5, tzinfo=timezone.utc), tick=False): + super().setUp() + + hamlet = self.example_user("hamlet") + remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid) + self.billing_session = RemoteRealmBillingSession(remote_realm=remote_realm) + + @responses.activate + @mock_stripe() + def test_non_sponsorship_billing(self, *mocks: Mock) -> None: + self.add_mock_response() + with time_machine.travel(self.now, tick=False): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + self.login("hamlet") + hamlet = self.example_user("hamlet") + + result = self.execute_remote_billing_authentication_flow(hamlet) + self.assertEqual(result.status_code, 302) + self.assertEqual(result["Location"], f"{self.billing_session.billing_base_url}/plans/") + + # upgrade to business plan + with time_machine.travel(self.now, tick=False): + result = self.client_get( + f"{self.billing_session.billing_base_url}/upgrade/", subdomain="selfhosting" + ) + self.assertEqual(result.status_code, 200) + self.assert_in_success_response(["Add card", "Purchase Zulip Business"], result) + + self.assertFalse(Customer.objects.exists()) + self.assertFalse(CustomerPlan.objects.exists()) + self.assertFalse(LicenseLedger.objects.exists()) + + with time_machine.travel(self.now, tick=False): + stripe_customer = self.add_card_and_upgrade() + + customer = Customer.objects.get(stripe_customer_id=stripe_customer.id) + plan = CustomerPlan.objects.get(customer=customer) + LicenseLedger.objects.get(plan=plan) + + with time_machine.travel(self.now + timedelta(days=1), tick=False): + response = self.client_get( + f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting" + ) + for substring in [ + "Zulip Business", + "Number of licenses", + "10 (managed automatically)", + "Your plan will automatically renew on", + "Visa ending in 4242", + "Update card", + ]: + self.assert_in_response(substring, response) + + # Verify that change in user count updates LicenseLedger. + audit_log_count = RemoteRealmAuditLog.objects.count() + self.assertEqual(LicenseLedger.objects.count(), 1) + + with time_machine.travel(self.now + timedelta(days=2), tick=False): + user_count = self.billing_session.current_count_for_billed_licenses( + self.now + timedelta(days=2) + ) + for count in range(10): + do_create_user( + f"email {count}", + f"password {count}", + hamlet.realm, + "name", + role=UserProfile.ROLE_MEMBER, + acting_user=None, + ) + + with time_machine.travel(self.now + timedelta(days=3), tick=False): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + self.assertEqual( + RemoteRealmAuditLog.objects.count(), + audit_log_count + 10, + ) + latest_ledger = LicenseLedger.objects.last() + assert latest_ledger is not None + self.assertEqual(latest_ledger.licenses, user_count + 10) + + @responses.activate + def test_request_sponsorship(self) -> None: + self.login("hamlet") + hamlet = self.example_user("hamlet") + realm = hamlet.realm + + self.add_mock_response() + send_server_data_to_push_bouncer(consider_usage_statistics=False) + remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid) + billing_base_url = self.billing_session.billing_base_url + + self.assertEqual(remote_realm.plan_type, RemoteRealm.PLAN_TYPE_SELF_MANAGED) + self.assertIsNone(self.billing_session.get_customer()) + result = self.execute_remote_billing_authentication_flow(hamlet) + + # User has no plan, so we redirect to /plans by default. + self.assertEqual(result["Location"], f"/realm/{realm.uuid!s}/plans/") + + # Check strings on plans page. + result = self.client_get(result["Location"], subdomain="selfhosting") + self.assert_in_success_response(["Request sponsorship"], result) + self.assert_not_in_success_response(["Sponsorship pending"], result) + + # Navigate to request sponsorship page. + result = self.client_get(f"{billing_base_url}/sponsorship/", subdomain="selfhosting") + self.assert_in_success_response( + ["Description of your organization", "Requested plan"], result + ) + + # Submit form data. + data = { + "organization_type": Realm.ORG_TYPES["opensource"]["id"], + "website": "https://infinispan.org/", + "description": "Infinispan is a distributed in-memory key/value data store with optional schema.", + "expected_total_users": "10 users", + "paid_users_count": "1 user", + "paid_users_description": "We have 1 paid user.", + "requested_plan": "Community", + } + response = self.client_billing_post("/billing/sponsorship", data) + self.assert_json_success(response) + + customer = self.billing_session.get_customer() + assert customer is not None + + sponsorship_request = ZulipSponsorshipRequest.objects.get(customer=customer) + self.assertEqual(sponsorship_request.requested_plan, data["requested_plan"]) + self.assertEqual(sponsorship_request.org_website, data["website"]) + self.assertEqual(sponsorship_request.org_description, data["description"]) + self.assertEqual( + sponsorship_request.org_type, + Realm.ORG_TYPES["opensource"]["id"], + ) + + from django.core.mail import outbox + + # First email is remote user email confirmation, second email is for sponsorship + message = outbox[1] + self.assert_length(outbox, 2) + self.assert_length(message.to, 1) + self.assertEqual(message.to[0], "sales@zulip.com") + self.assertEqual(message.subject, "Sponsorship request (Open-source project) for Zulip Dev") + self.assertEqual(message.reply_to, ["hamlet@zulip.com"]) + self.assertEqual(self.email_envelope_from(message), settings.NOREPLY_EMAIL_ADDRESS) + self.assertIn("Zulip sponsorship request None: + super().setUp() + + self.remote_server = RemoteZulipServer.objects.get(hostname="demo.example.com") + self.billing_session = RemoteServerBillingSession(remote_server=self.remote_server) + + @responses.activate + def test_request_sponsorship(self) -> None: + hamlet = self.example_user("hamlet") + now = timezone_now() + with time_machine.travel(now, tick=False): + result = self.execute_remote_billing_authentication_flow( + hamlet.delivery_email, hamlet.full_name, expect_tos=True, confirm_tos=True + ) + + self.add_mock_response() + send_server_data_to_push_bouncer(consider_usage_statistics=False) + billing_base_url = self.billing_session.billing_base_url + + self.assertEqual(self.remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) + self.assertIsNone(self.billing_session.get_customer()) + + # User has no plan, so we redirect to /plans by default. + self.assertEqual(result["Location"], f"/server/{self.remote_server.uuid!s}/plans/") + + # Check strings on plans page. + result = self.client_get(result["Location"], subdomain="selfhosting") + self.assert_in_success_response(["Request sponsorship"], result) + self.assert_not_in_success_response(["Sponsorship pending"], result) + + # Navigate to request sponsorship page. + result = self.client_get(f"{billing_base_url}/sponsorship/", subdomain="selfhosting") + self.assert_in_success_response( + ["Description of your organization", "Requested plan"], result + ) + + # Submit form data. + data = { + "organization_type": Realm.ORG_TYPES["opensource"]["id"], + "website": "https://infinispan.org/", + "description": "Infinispan is a distributed in-memory key/value data store with optional schema.", + "expected_total_users": "10 users", + "paid_users_count": "1 user", + "paid_users_description": "We have 1 paid user.", + "requested_plan": "Community", + } + response = self.client_billing_post("/billing/sponsorship", data) + self.assert_json_success(response) + + customer = self.billing_session.get_customer() + assert customer is not None + + sponsorship_request = ZulipSponsorshipRequest.objects.get(customer=customer) + self.assertEqual(sponsorship_request.requested_plan, data["requested_plan"]) + self.assertEqual(sponsorship_request.org_website, data["website"]) + self.assertEqual(sponsorship_request.org_description, data["description"]) + self.assertEqual( + sponsorship_request.org_type, + Realm.ORG_TYPES["opensource"]["id"], + ) + + from django.core.mail import outbox + + # First email is remote user email confirmation, second email is for sponsorship + message = outbox[1] + self.assert_length(outbox, 2) + self.assert_length(message.to, 1) + self.assertEqual(message.to[0], "sales@zulip.com") + self.assertEqual( + message.subject, "Sponsorship request (Open-source project) for demo.example.com" + ) + self.assertEqual(message.reply_to, ["hamlet@zulip.com"]) + self.assertEqual(self.email_envelope_from(message), settings.NOREPLY_EMAIL_ADDRESS) + self.assertIn("Zulip sponsorship request HttpResponse: # nocoverage +) -> HttpResponse: realm_uuid = billing_session.remote_realm.uuid context: Dict[str, Any] = { # We wouldn't be here if user didn't have access. @@ -107,13 +109,16 @@ def remote_realm_billing_page( "billing_base_url": billing_session.billing_base_url, } - if billing_session.remote_realm.plan_type == RemoteRealm.PLAN_TYPE_COMMUNITY: + if billing_session.remote_realm.plan_type == RemoteRealm.PLAN_TYPE_COMMUNITY: # nocoverage return HttpResponseRedirect(reverse("remote_realm_sponsorship_page", args=(realm_uuid,))) customer = billing_session.get_customer() - if customer is not None and customer.sponsorship_pending: - # Don't redirect to sponsorship page if the remote realm is on a paid plan - if not billing_session.on_paid_plan(): + if customer is not None and customer.sponsorship_pending: # nocoverage + # Don't redirect to sponsorship page if the remote realm is on a paid plan or scheduled for an upgrade. + if ( + not billing_session.on_paid_plan() + and billing_session.get_legacy_remote_server_next_plan_name(customer) is None + ): return HttpResponseRedirect( reverse("remote_realm_sponsorship_page", args=(realm_uuid,)) ) @@ -125,12 +130,20 @@ def remote_realm_billing_page( or get_current_plan_by_customer(customer) is None or ( billing_session.get_legacy_remote_server_next_plan_name(customer) is None - and billing_session.remote_realm.plan_type == RemoteRealm.PLAN_TYPE_SELF_HOSTED + and billing_session.remote_realm.plan_type + in [ + RemoteRealm.PLAN_TYPE_SELF_MANAGED, + RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY, + ] ) - ): + ): # nocoverage return HttpResponseRedirect(reverse("remote_realm_plans_page", args=(realm_uuid,))) - main_context = billing_session.get_billing_page_context() + try: + main_context = billing_session.get_billing_page_context() + except MissingDataError: # nocoverage + return billing_session.missing_data_error_page(request) + if main_context: context.update(main_context) context["success_message"] = success_message @@ -164,8 +177,11 @@ def remote_server_billing_page( customer = billing_session.get_customer() if customer is not None and customer.sponsorship_pending: - # Don't redirect to sponsorship page if the remote realm is on a paid plan - if not billing_session.on_paid_plan(): + # Don't redirect to sponsorship page if the remote realm is on a paid plan or scheduled for an upgrade. + if ( + not billing_session.on_paid_plan() + and billing_session.get_legacy_remote_server_next_plan_name(customer) is None + ): return HttpResponseRedirect( reverse( "remote_server_sponsorship_page", @@ -180,7 +196,11 @@ def remote_server_billing_page( or get_current_plan_by_customer(customer) is None or ( billing_session.get_legacy_remote_server_next_plan_name(customer) is None - and billing_session.remote_server.plan_type == RemoteZulipServer.PLAN_TYPE_SELF_HOSTED + and billing_session.remote_server.plan_type + in [ + RemoteZulipServer.PLAN_TYPE_SELF_MANAGED, + RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY, + ] ) ): return HttpResponseRedirect( @@ -190,7 +210,11 @@ def remote_server_billing_page( ) ) - main_context = billing_session.get_billing_page_context() + try: + main_context = billing_session.get_billing_page_context() + except MissingDataError: + return billing_session.missing_data_error_page(request) + if main_context: context.update(main_context) context["success_message"] = success_message @@ -291,11 +315,11 @@ def remote_server_deactivate_page( return HttpResponseNotAllowed(["GET", "POST"]) remote_server = billing_session.remote_server + context = { + "server_hostname": remote_server.hostname, + "action_url": reverse(remote_server_deactivate_page, args=[str(remote_server.uuid)]), + } if request.method == "GET": - context = { - "server_hostname": remote_server.hostname, - "action_url": reverse(remote_server_deactivate_page, args=[str(remote_server.uuid)]), - } return render(request, "corporate/remote_billing_server_deactivate.html", context=context) assert request.method == "POST" @@ -303,7 +327,12 @@ def remote_server_deactivate_page( # Should be impossible if the user is using the UI. raise JsonableError(_("Parameter 'confirmed' is required")) - do_deactivate_remote_server(remote_server) + try: + do_deactivate_remote_server(remote_server, billing_session) + except ServerDeactivateWithExistingPlanError: # nocoverage + context["show_existing_plan_error"] = "true" + return render(request, "corporate/remote_billing_server_deactivate.html", context=context) + return render( request, "corporate/remote_billing_server_deactivated_success.html", diff --git a/corporate/views/event_status.py b/corporate/views/event_status.py index 54d871544500c..16542bc8d7e95 100644 --- a/corporate/views/event_status.py +++ b/corporate/views/event_status.py @@ -48,7 +48,7 @@ def remote_realm_event_status( *, stripe_session_id: Optional[str] = None, stripe_payment_intent_id: Optional[str] = None, -) -> HttpResponse: # nocoverage +) -> HttpResponse: event_status_request = EventStatusRequest( stripe_session_id=stripe_session_id, stripe_payment_intent_id=stripe_payment_intent_id ) diff --git a/corporate/views/portico.py b/corporate/views/portico.py index 473cb1a743741..cae5e64417c80 100644 --- a/corporate/views/portico.py +++ b/corporate/views/portico.py @@ -80,6 +80,7 @@ class PlansPageContext: customer_plan: Optional[CustomerPlan] = None is_legacy_server_with_scheduled_upgrade: bool = False legacy_server_new_plan: Optional[CustomerPlan] = None + requested_sponsorship_plan: Optional[str] = None billing_base_url: str = "" @@ -140,6 +141,7 @@ def remote_realm_plans_page( free_trial_days=get_free_trial_days(True), billing_base_url=billing_session.billing_base_url, is_sponsored=billing_session.is_sponsored(), + requested_sponsorship_plan=billing_session.get_sponsorship_plan_name(customer, True), ) context.on_free_tier = customer is None and not context.is_sponsored @@ -152,9 +154,13 @@ def remote_realm_plans_page( if context.customer_plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: # Free trial is disabled for legacy customers. context.free_trial_days = None - context.on_free_tier = context.customer_plan.tier in ( - CustomerPlan.TIER_SELF_HOSTED_LEGACY, - CustomerPlan.TIER_SELF_HOSTED_BASE, + context.on_free_tier = ( + context.customer_plan.tier + in ( + CustomerPlan.TIER_SELF_HOSTED_LEGACY, + CustomerPlan.TIER_SELF_HOSTED_BASE, + ) + and not context.is_sponsored ) context.on_free_trial = is_customer_on_free_trial(context.customer_plan) context.is_legacy_server_with_scheduled_upgrade = ( @@ -192,6 +198,7 @@ def remote_server_plans_page( free_trial_days=get_free_trial_days(True), billing_base_url=billing_session.billing_base_url, is_sponsored=billing_session.is_sponsored(), + requested_sponsorship_plan=billing_session.get_sponsorship_plan_name(customer, True), ) context.on_free_tier = customer is None and not context.is_sponsored @@ -255,7 +262,9 @@ def team_view(request: HttpRequest) -> HttpResponse: @add_google_analytics def landing_view(request: HttpRequest, template_name: str) -> HttpResponse: - return TemplateResponse(request, template_name, latest_info_context()) + context = latest_info_context() + context["billing_base_url"] = "" + return TemplateResponse(request, template_name, context) @add_google_analytics diff --git a/corporate/views/remote_billing_page.py b/corporate/views/remote_billing_page.py index 25e6e66c4cdfb..84874c36e8fea 100644 --- a/corporate/views/remote_billing_page.py +++ b/corporate/views/remote_billing_page.py @@ -5,6 +5,7 @@ from django.conf import settings from django.core import signing from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Exists, OuterRef from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse @@ -30,6 +31,16 @@ RemoteBillingUserDict, get_remote_server_and_user_from_session, ) +from corporate.lib.stripe import ( + BILLING_SUPPORT_EMAIL, + RemoteRealmBillingSession, + RemoteServerBillingSession, +) +from corporate.models import ( + CustomerPlan, + get_current_plan_by_customer, + get_customer_by_remote_server, +) from zerver.lib.exceptions import ( JsonableError, MissingRemoteRealmError, @@ -74,8 +85,6 @@ def remote_realm_billing_entry( uri_scheme: Literal["http://", "https://"] = "https://", next_page: VALID_NEXT_PAGES_TYPE = None, ) -> HttpResponse: - if not settings.DEVELOPMENT: - return render(request, "404.html", status=404) try: remote_realm = RemoteRealm.objects.get(uuid=realm.uuid, server=remote_server) except RemoteRealm.DoesNotExist: @@ -128,6 +137,15 @@ def get_identity_dict_from_signed_access_token( return identity_dict +def is_tos_consent_needed_for_user( + remote_user: Union[RemoteRealmBillingUser, RemoteServerBillingUser] +) -> bool: + assert settings.TERMS_OF_SERVICE_VERSION is not None + return int(settings.TERMS_OF_SERVICE_VERSION.split(".")[0]) > int( + remote_user.tos_version.split(".")[0] + ) + + @self_hosting_management_endpoint @typed_endpoint def remote_realm_billing_finalize_login( @@ -136,6 +154,8 @@ def remote_realm_billing_finalize_login( signed_billing_access_token: PathOnly[str], full_name: Optional[str] = None, tos_consent: Literal[None, "true"] = None, + enable_major_release_emails: Literal[None, "true", "false"] = None, + enable_maintenance_release_emails: Literal[None, "true", "false"] = None, ) -> HttpResponse: """ This is the endpoint accessed via the billing_access_url, generated by @@ -174,6 +194,19 @@ def remote_realm_billing_finalize_login( # pretty recently. (And we generally don't delete these at all.) raise AssertionError + # Redirect to error page if server is on an active plan + server_customer = get_customer_by_remote_server(remote_server) + if server_customer is not None: + server_plan = get_current_plan_by_customer(server_customer) + if server_plan is not None: + return render( + request, + "corporate/remote_realm_login_error_for_server_on_active_plan.html", + context={ + "server_plan_name": server_plan.name, + }, + ) + user_dict = identity_dict["user"] user_email = user_dict["user_email"] @@ -188,9 +221,7 @@ def remote_realm_billing_finalize_login( remote_realm=remote_realm, user_uuid=user_uuid, ) - tos_consent_needed = int(settings.TERMS_OF_SERVICE_VERSION.split(".")[0]) > int( - remote_user.tos_version.split(".")[0] - ) + tos_consent_needed = is_tos_consent_needed_for_user(remote_user) except RemoteRealmBillingUser.DoesNotExist: # This is the first time this user is logging in. remote_user = None @@ -238,7 +269,7 @@ def remote_realm_billing_finalize_login( # Users logging in for the first time need to be created and follow # a different path - they should not be POSTing here. It should be impossible # to get here with a remote_user that is None without tampering with the form - # or manualling crafting a POST request. + # or manually crafting a POST request. raise JsonableError(_("User account doesn't exist yet.")) if tos_consent_needed and not tos_consent_given: @@ -246,14 +277,25 @@ def remote_realm_billing_finalize_login( # don't need a pretty error. raise JsonableError(_("You must accept the Terms of Service to proceed.")) - # The current approach is to update the full_name - # based on what the user entered in the login confirmation form. - # Usually they'll presumably just use the name already set for this object. + # The current approach is to update the full_name and email preferences + # only when the user first logs in. if full_name is not None: remote_user.full_name = full_name + remote_user.enable_major_release_emails = enable_major_release_emails == "true" + remote_user.enable_maintenance_release_emails = enable_maintenance_release_emails == "true" + remote_user.tos_version = settings.TERMS_OF_SERVICE_VERSION remote_user.last_login = timezone_now() - remote_user.save(update_fields=["full_name", "tos_version", "last_login"]) + + remote_user.save( + update_fields=[ + "full_name", + "tos_version", + "last_login", + "enable_maintenance_release_emails", + "enable_major_release_emails", + ] + ) identity_dict["remote_billing_user_id"] = remote_user.id request.session["remote_billing_identities"] = {} @@ -261,18 +303,33 @@ def remote_realm_billing_finalize_login( f"remote_realm:{remote_realm_uuid}" ] = identity_dict - # TODO: Figure out redirects based on whether the realm/server already has a plan - # and should be taken to /billing or doesn't have and should be taken to /plans. - # For now we're only implemented the case where we have the RemoteRealm, and we take - # to /plans. - - assert identity_dict["next_page"] in VALID_NEXT_PAGES - if identity_dict["next_page"] is None: + next_page = identity_dict["next_page"] + assert next_page in VALID_NEXT_PAGES + if next_page is not None: + return HttpResponseRedirect( + reverse(f"remote_realm_{next_page}_page", args=(remote_realm_uuid,)) + ) + elif remote_realm.plan_type in [ + RemoteRealm.PLAN_TYPE_SELF_MANAGED, + RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY, + ]: + # If they have a scheduled upgrade, redirect to billing page. + billing_session = RemoteRealmBillingSession(remote_realm) + customer = billing_session.get_customer() + if ( + customer is not None + and billing_session.get_legacy_remote_server_next_plan_name(customer) is not None + ): + return HttpResponseRedirect( + reverse("remote_realm_billing_page", args=(remote_realm_uuid,)) + ) return HttpResponseRedirect(reverse("remote_realm_plans_page", args=(remote_realm_uuid,))) - else: + elif remote_realm.plan_type == RemoteRealm.PLAN_TYPE_COMMUNITY: return HttpResponseRedirect( - reverse(f"remote_realm_{identity_dict['next_page']}_page", args=(remote_realm_uuid,)) + reverse("remote_realm_sponsorship_page", args=(remote_realm_uuid,)) ) + else: + return HttpResponseRedirect(reverse("remote_realm_billing_page", args=(remote_realm_uuid,))) @self_hosting_management_endpoint @@ -318,7 +375,7 @@ def remote_realm_billing_confirm_email( "remote_realm_host": remote_realm.host, "confirmation_url": url, "billing_help_link": "https://zulip.com/help/self-hosted-billing", - "billing_contact_email": "sales@zulip.com", + "billing_contact_email": BILLING_SUPPORT_EMAIL, "validity_in_hours": LOGIN_CONFIRMATION_EMAIL_DURATION_HOURS, } send_email( @@ -535,7 +592,7 @@ def remote_billing_legacy_server_confirm_login( "remote_server_hostname": remote_server.hostname, "confirmation_url": url, "billing_help_link": "https://zulip.com/help/self-hosted-billing", - "billing_contact_email": "sales@zulip.com", + "billing_contact_email": BILLING_SUPPORT_EMAIL, "validity_in_hours": LOGIN_CONFIRMATION_EMAIL_DURATION_HOURS, } send_email( @@ -548,7 +605,22 @@ def remote_billing_legacy_server_confirm_login( return render( request, "corporate/remote_billing_email_confirmation_sent.html", - context={"email": email}, + context={"email": email, "remote_server_hostname": remote_server.hostname}, + ) + + +def has_live_plan_for_any_remote_realm_on_server(server: RemoteZulipServer) -> bool: + has_plan_with_status_lt_live_threshold = CustomerPlan.objects.filter( + customer__remote_realm__server=server, + status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD, + customer__remote_realm=OuterRef("pk"), + ) + + return ( + RemoteRealm.objects.filter(server=server) + .annotate(has_plan=Exists(has_plan_with_status_lt_live_threshold)) + .filter(has_plan=True) + .exists() ) @@ -560,6 +632,8 @@ def remote_billing_legacy_server_from_login_confirmation_link( confirmation_key: PathOnly[str], full_name: Optional[str] = None, tos_consent: Literal[None, "true"] = None, + enable_major_release_emails: Literal[None, "true", "false"] = None, + enable_maintenance_release_emails: Literal[None, "true", "false"] = None, ) -> HttpResponse: """ The user comes here via the confirmation link they received via email. @@ -582,14 +656,18 @@ def remote_billing_legacy_server_from_login_confirmation_link( # If this user (identified by email) already did this flow, meaning the have a RemoteServerBillingUser, # then we don't re-do the ToS consent again. - tos_consent_needed = not RemoteServerBillingUser.objects.filter( + remote_billing_user = RemoteServerBillingUser.objects.filter( remote_server=remote_server, email=prereg_object.email - ).exists() + ).first() + tos_consent_needed = remote_billing_user is None or is_tos_consent_needed_for_user( + remote_billing_user + ) if request.method == "GET": context = { "remote_server_uuid": remote_server_uuid, "host": remote_server.hostname, + "user_full_name": getattr(remote_billing_user, "full_name", None), "user_email": prereg_object.email, "tos_consent_needed": tos_consent_needed, "action_url": reverse( @@ -597,6 +675,7 @@ def remote_billing_legacy_server_from_login_confirmation_link( args=(confirmation_key,), ), "legacy_server_confirmation_flow": True, + "next_page": prereg_object.next_page, } return render( request, @@ -611,14 +690,26 @@ def remote_billing_legacy_server_from_login_confirmation_link( # don't need a pretty error. raise JsonableError(_("You must accept the Terms of Service to proceed.")) - next_page = prereg_object.next_page + if ( + has_live_plan_for_any_remote_realm_on_server(remote_server) + and prereg_object.next_page != "deactivate" + ): + return render( + request, + "corporate/remote_server_login_error_for_any_realm_on_active_plan.html", + ) - remote_billing_user, created = RemoteServerBillingUser.objects.update_or_create( - defaults={"full_name": full_name}, - email=prereg_object.email, - remote_server=remote_server, - ) - if created: + if remote_billing_user is None: + assert full_name is not None + assert settings.TERMS_OF_SERVICE_VERSION is not None + remote_billing_user = RemoteServerBillingUser.objects.create( + full_name=full_name, + email=prereg_object.email, + remote_server=remote_server, + tos_version=settings.TERMS_OF_SERVICE_VERSION, + enable_major_release_emails=enable_major_release_emails == "true", + enable_maintenance_release_emails=enable_maintenance_release_emails == "true", + ) prereg_object.created_user = remote_billing_user prereg_object.save(update_fields=["created_user"]) @@ -639,12 +730,26 @@ def remote_billing_legacy_server_from_login_confirmation_link( remote_billing_user_id=remote_billing_user.id, ) + next_page = prereg_object.next_page assert next_page in VALID_NEXT_PAGES if next_page is not None: return HttpResponseRedirect( reverse(f"remote_server_{next_page}_page", args=(remote_server_uuid,)) ) - elif remote_server.plan_type == RemoteZulipServer.PLAN_TYPE_SELF_HOSTED: + elif remote_server.plan_type in [ + RemoteZulipServer.PLAN_TYPE_SELF_MANAGED, + RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY, + ]: + # If they have a scheduled upgrade, redirect to billing page. + billing_session = RemoteServerBillingSession(remote_server) + customer = billing_session.get_customer() + if ( + customer is not None + and billing_session.get_legacy_remote_server_next_plan_name(customer) is not None + ): + return HttpResponseRedirect( + reverse("remote_server_billing_page", args=(remote_server_uuid,)) + ) return HttpResponseRedirect(reverse("remote_server_plans_page", args=(remote_server_uuid,))) elif remote_server.plan_type == RemoteZulipServer.PLAN_TYPE_COMMUNITY: return HttpResponseRedirect( diff --git a/corporate/views/session.py b/corporate/views/session.py index f5ec1dcaa9f42..0371ac07a2cec 100644 --- a/corporate/views/session.py +++ b/corporate/views/session.py @@ -75,7 +75,7 @@ def start_card_update_stripe_session_for_remote_realm_upgrade( billing_session: RemoteRealmBillingSession, *, manual_license_management: Json[bool] = False, -) -> HttpResponse: # nocoverage +) -> HttpResponse: session_data = billing_session.create_card_update_session_for_upgrade(manual_license_management) return json_success( request, diff --git a/corporate/views/upgrade.py b/corporate/views/upgrade.py index 60e19b5af29f1..74fc9cba79a2d 100644 --- a/corporate/views/upgrade.py +++ b/corporate/views/upgrade.py @@ -28,6 +28,7 @@ from zerver.lib.typed_endpoint import typed_endpoint from zerver.lib.validator import check_bool, check_int, check_string_in from zerver.models import UserProfile +from zilencer.lib.remote_counts import MissingDataError billing_logger = logging.getLogger("corporate.stripe") @@ -96,7 +97,7 @@ def remote_realm_upgrade( ), licenses: Optional[int] = REQ(json_validator=check_int, default=None), remote_server_plan_start_date: Optional[str] = REQ(default=None), -) -> HttpResponse: # nocoverage +) -> HttpResponse: try: upgrade_request = UpgradeRequest( billing_modality=billing_modality, @@ -111,7 +112,7 @@ def remote_realm_upgrade( ) data = billing_session.do_upgrade(upgrade_request) return json_success(request, data) - except BillingError as e: + except BillingError as e: # nocoverage billing_logger.warning( "BillingError during upgrade: %s. remote_realm=%s (%s), billing_modality=%s, " "schedule=%s, license_management=%s, licenses=%s", @@ -124,7 +125,7 @@ def remote_realm_upgrade( licenses, ) raise e - except Exception: + except Exception: # nocoverage billing_logger.exception("Uncaught exception in billing:", stack_info=True) error_message = BillingError.CONTACT_SUPPORT.format(email=settings.ZULIP_ADMINISTRATOR) error_description = "uncaught exception during upgrade" @@ -214,15 +215,18 @@ def remote_realm_upgrade_page( *, manual_license_management: Json[bool] = False, success_message: str = "", -) -> HttpResponse: # nocoverage +) -> HttpResponse: initial_upgrade_request = InitialUpgradeRequest( manual_license_management=manual_license_management, tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS, success_message=success_message, ) - redirect_url, context = billing_session.get_initial_upgrade_context(initial_upgrade_request) + try: + redirect_url, context = billing_session.get_initial_upgrade_context(initial_upgrade_request) + except MissingDataError: # nocoverage + return billing_session.missing_data_error_page(request) - if redirect_url: + if redirect_url: # nocoverage return HttpResponseRedirect(redirect_url) response = render(request, "corporate/upgrade.html", context=context) @@ -243,7 +247,10 @@ def remote_server_upgrade_page( tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS, success_message=success_message, ) - redirect_url, context = billing_session.get_initial_upgrade_context(initial_upgrade_request) + try: + redirect_url, context = billing_session.get_initial_upgrade_context(initial_upgrade_request) + except MissingDataError: + return billing_session.missing_data_error_page(request) if redirect_url: return HttpResponseRedirect(redirect_url) diff --git a/docs/development/setup-recommended.md b/docs/development/setup-recommended.md index b5ce2f9d898e8..c99be173ec209 100644 --- a/docs/development/setup-recommended.md +++ b/docs/development/setup-recommended.md @@ -373,34 +373,24 @@ Next, start the Zulip server: $ ./tools/run-dev ``` -You will see several lines of output starting with something like: +You will see something like: ```console -2016-05-04 22:20:33,895 INFO: process_fts_updates starting -Recompiling templates -2016-05-04 18:20:34,804 INFO: Not in recovery; listening for FTS updates -done -Validating Django models.py... -System check identified no issues (0 silenced). - -Django version 1.8 -Tornado server is running at http://localhost:9993/ -Quit the server with CTRL-C. -2016-05-04 18:20:40,716 INFO Tornado loaded 0 event queues in 0.001s -2016-05-04 18:20:40,722 INFO Tornado 95.5% busy over the past 0.0 seconds -Performing system checks... -``` +Starting Zulip on: -And ending with something similar to: + http://localhost:9991/ -```console -http://localhost:9994/webpack-dev-server/ -webpack result is served from http://localhost:9991/webpack/ -content is served from /srv/zulip +Internal ports: + 9991: Development server proxy (connect here) + 9992: Django + 9993: Tornado + 9994: webpack + +Tornado server (re)started on port 9993 -webpack: bundle is now VALID. -2016-05-06 21:43:29,553 INFO Tornado 31.6% busy over the past 10.6 seconds -2016-05-06 21:43:35,007 INFO Tornado 23.9% busy over the past 16.0 seconds +2023-12-15 20:57:14.206 INFO [process_queue] 13 queue worker threads were launched +frontend: + frontend (webpack 5.89.0) compiled successfully in 8054 ms ``` Now the Zulip server should be running and accessible. Verify this by diff --git a/docs/development/using.md b/docs/development/using.md index d05a1ea991408..acb8b629b6f98 100644 --- a/docs/development/using.md +++ b/docs/development/using.md @@ -47,7 +47,7 @@ the development environment][authentication-dev-server]. - The Python queue workers will also automatically restart when you save changes, as long as they haven't crashed (which can happen if they reloaded into a version with a syntax error). -- If you change the database schema (`zerver/models.py`), you'll need +- If you change the database schema (`zerver/models/*.py`), you'll need to use the [Django migrations process](../subsystems/schema-migrations.md); see also the [new feature tutorial][new-feature-tutorial] for an example. diff --git a/docs/overview/changelog.md b/docs/overview/changelog.md index 5ea5c2e302b5d..7c5f9bb4c5293 100644 --- a/docs/overview/changelog.md +++ b/docs/overview/changelog.md @@ -3,11 +3,23 @@ This page contains the release history for the Zulip server. See also the [Zulip release lifecycle](../overview/release-lifecycle.md). +## Zulip 9.x series + +### 9.0 -- unreleased + +This section is an incomplete draft of the release notes for the next +major release, and is only updated occasionally. See the [commit +log][commit-log] for an up-to-date list of all changes. + +#### Upgrade notes for 9.0 + +- None yet. + ## Zulip Server 8.x series -### Zulip Server 8.0-beta2 +### Zulip Server 8.0 -_Released 2023-12-12_ +_Released 2023-12-15_ #### Highlights @@ -3704,7 +3716,8 @@ _Released 2015-10-19_ This section links to the upgrade notes from past releases, so you can easily read them all when upgrading across multiple releases. -- [Draft upgrade notes for 8.0](#upgrade-notes-for-80) +- [Draft upgrade notes for 9.0](#upgrade-notes-for-90) +- [Upgrade notes for 8.0](#upgrade-notes-for-80) - [Upgrade notes for 7.0](#upgrade-notes-for-70) - [Upgrade notes for 6.0](#upgrade-notes-for-60) - [Upgrade notes for 5.0](#upgrade-notes-for-50) diff --git a/docs/overview/directory-structure.md b/docs/overview/directory-structure.md index e146a8277264f..65f78d40c7813 100644 --- a/docs/overview/directory-structure.md +++ b/docs/overview/directory-structure.md @@ -17,9 +17,9 @@ paths will be familiar to Django developers. [Django routes file](https://docs.djangoproject.com/en/3.2/topics/http/urls/). Defines which URLs are handled by which view functions or templates. -- `zerver/models.py` Main +- `zerver/models/*.py` [Django models](https://docs.djangoproject.com/en/3.2/topics/db/models/) - file. Defines Zulip's database tables. + files. Defines Zulip's database tables. - `zerver/lib/*.py` Most library code. diff --git a/docs/production/authentication-methods.md b/docs/production/authentication-methods.md index a1fc75b371f47..2e2262ffbf7b8 100644 --- a/docs/production/authentication-methods.md +++ b/docs/production/authentication-methods.md @@ -365,7 +365,7 @@ You can look at the [full list of fields][models-py] in the Zulip user model; search for `class UserProfile`, but the above should cover all the fields that would be useful to sync from your LDAP databases. -[models-py]: https://github.com/zulip/zulip/blob/main/zerver/models.py +[models-py]: https://github.com/zulip/zulip/blob/main/zerver/models/users.py [django-auth-booleans]: https://django-auth-ldap.readthedocs.io/en/latest/users.html#easy-attributes ### Multiple LDAP searches @@ -747,7 +747,7 @@ integration](../production/scim.md). `https://authentik.company/application/saml//sso/binding/redirect/` where `` is the application slug you've assigned to this application in Authentik settings (e.g `zulip`). 1. Update the attribute mapping in your new entry in `SOCIAL_AUTH_SAML_ENABLED_IDPS` to match how - Authentik specifies attributes in its`SAMLResponse`: + Authentik specifies attributes in its `SAMLResponse`: ``` "attr_user_permanent_id": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", @@ -791,7 +791,7 @@ this with another IdP. Disable `Force POST Binding`, as Zulip only supports the Redirect binding. 1. In `Fine Grain SAML Endpoint Configuration`, set `Logout Service Redirect Binding URL` to the same value you provided for `SSO URL` above. -1. Add the IdP's `Redirect Binding URL`for `SingleLogoutService` to +1. Add the IdP's `Redirect Binding URL` for `SingleLogoutService` to your IdP configuration dict in `SOCIAL_AUTH_SAML_ENABLED_IDPS` in `/etc/zulip/settings.py` as `slo_url`. For example it may look like this: diff --git a/docs/production/deployment.md b/docs/production/deployment.md index 3cfcb2f18c0f1..5d36f45ca508a 100644 --- a/docs/production/deployment.md +++ b/docs/production/deployment.md @@ -568,7 +568,7 @@ Apache requires you use the hostname, not the IP address; see ### HAProxy configuration Below is a working example of a HAProxy configuration. It assumes that -your Zulip server sits at `https://10.10.10.10:443`see +your Zulip server sits at `https://10.10.10.10:443`; see [above](#configuring-zulip-to-allow-http) to switch to HTTP. 1. Follow the instructions to [configure Zulip to trust diff --git a/docs/production/management-commands.md b/docs/production/management-commands.md index b777501ae6f0a..f7259a39946ff 100644 --- a/docs/production/management-commands.md +++ b/docs/production/management-commands.md @@ -95,7 +95,7 @@ is already a function in `zerver.actions` with a name like `do_change_full_name` that updates that field and notifies clients correctly. -For convenience, Zulip automatically imports `zerver/models.py` +For convenience, Zulip automatically imports `zerver.models` into every management shell; if you need to access other functions, you'll need to import them yourself. diff --git a/docs/production/mobile-push-notifications.md b/docs/production/mobile-push-notifications.md index c8a567eebca85..6112f1c9919b3 100644 --- a/docs/production/mobile-push-notifications.md +++ b/docs/production/mobile-push-notifications.md @@ -64,6 +64,53 @@ Congratulations! You've successfully set up the service. You can now test mobile push notifications by following [these instructions](https://zulip.com/help/mobile-notifications#testing-mobile-notifications). +## Plan management for a Zulip organization + +On a self-hosted Zulip server running Zulip 8.0+, [organization +owners](https://zulip.com/help/roles-and-permissions) and billing administrators +can conveniently access plan management from the Zulip app. See [help center +documentation](https://zulip.com/help/self-hosted-billing) for detailed +instructions. + +You can add billing administrators using the `change_user_role` [management +command][management-commands], passing [the organization's +`string_id`][accessing-string-id], and the email address of the Zulip user who +should be added as a billing administrator. + +``` +/home/zulip/deployments/current/manage.py change_user_role -r '' username@example.com is_billing_admin +``` + +You can remove a user's billing administrator permissions with the `--revoke` +option: + +``` +/home/zulip/deployments/current/manage.py change_user_role --revoke -r '' username@example.com is_billing_admin +``` + +[management-commands]: ../production/management-commands.md +[accessing-string-id]: https://zulip.readthedocs.io/en/stable/production/management-commands.html#accessing-an-organization-s-string-id + +## Plan management for an entire Zulip server + +Servers running Zulip releases older than Zulip 8.0 can start the plan +management log in process at +. This option is also +available for Zulip 8.0+ servers, and makes it possible to use a +single plan for multiple organizations on one installation. See [help +center documentation](https://zulip.com/help/self-hosted-billing) for +detailed log in instructions. + +You will use your server's `zulip_org_id` and `zulip_org_key` as the username +and password to access plan management. You can obtain these from +`/etc/zulip/zulip-secrets.conf` on your Zulip server, or via the following +commands: + +``` +/home/zulip/deployments/current/scripts/get-django-setting ZULIP_ORG_ID +/home/zulip/deployments/current/scripts/get-django-setting ZULIP_ORG_KEY +``` + ## Why a push notification service is necessary Both Google's and Apple's push notification services have a security @@ -198,6 +245,15 @@ Some of the graphs on your server's [usage statistics page](https://zulip.com/help/analytics) can be generated from these statistics. +When enabled, usage statistics are submitted via an hourly cron +job. If you'd like to access plan management immediately after +enabling `SUBMIT_USAGE_STATISTICS=True` on a pre-8.0 Zulip server, you +can run the analytics job manually via: + +``` +/home/zulip/deployments/current/manage.py update_analytics_counts +``` + Our use of uploaded usage statistics is governed by the same [Terms of Service](https://zulip.com/policies/terms) and [Privacy Policy](https://zulip.com/policies/privacy) that covers the Mobile diff --git a/docs/subsystems/caching.md b/docs/subsystems/caching.md index d1742d9c40603..9228681eb6fc3 100644 --- a/docs/subsystems/caching.md +++ b/docs/subsystems/caching.md @@ -131,7 +131,7 @@ you configure some code to run every time Django does something (for `post_save`, right after any write to the database using Django's `.save()`). -There's a handful of lines in `zerver/models.py` like these that +There's a handful of lines in `zerver/models/*.py` like these that configure this: ```python diff --git a/docs/subsystems/schema-migrations.md b/docs/subsystems/schema-migrations.md index 3499e3858e5f0..ebde9eff1aaf5 100644 --- a/docs/subsystems/schema-migrations.md +++ b/docs/subsystems/schema-migrations.md @@ -9,9 +9,9 @@ This page documents some important issues related to writing schema migrations. - If your database migration is just to reflect new fields in - `models.py`, you'll typically want to just: + `models/*.py`, you'll typically want to just: - Rebase your branch before you start (this may save work later). - - Update the model class definitions in `zerver/models.py`. + - Update the model class definitions in `zerver/models/*.py`. - Run `./manage.py makemigrations` to generate a migration file - Rename the migration file to have a descriptive name if Django generated used a date-based name like `0089_auto_20170710_1353.py` @@ -33,7 +33,7 @@ migrations. - If your migrations were automatically generated using `manage.py makemigrations`, a good option is to just remove your migration and rerun the command after rebasing. Remember to - `git rebase` to do this in the the commit that changed `models.py` + `git rebase` to do this in the the commit that changed `models/*.py` if you have a multi-commit branch. - If you wrote code as part of preparing your migrations, or prefer this workflow, you can use run `./tools/renumber-migrations`, diff --git a/docs/subsystems/typing-indicators.md b/docs/subsystems/typing-indicators.md index 75e29cf4bc928..b69c5570e4ced 100644 --- a/docs/subsystems/typing-indicators.md +++ b/docs/subsystems/typing-indicators.md @@ -37,7 +37,7 @@ On a high level the typing indicators system works like this: Note that there is a user-level privacy setting to disable sending typing notifications that a client should check when implementing the "writing user" protocol below. See `send_private_typing_notifications` -in the `UserBaseSettings` model in `zerver/models.py` and in the +in the `UserBaseSettings` model in `zerver/models/users.py` and in the `user_settings` object in the `POST /register` response. ## Writing user diff --git a/docs/testing/testing.md b/docs/testing/testing.md index 146cb5cfdc19d..06040c817b930 100644 --- a/docs/testing/testing.md +++ b/docs/testing/testing.md @@ -32,7 +32,7 @@ because it takes a long time. Instead, your edit/refresh cycle will typically involve running subsets of the tests with commands like these: ```bash -./tools/lint zerver/models.py # Lint the file you just changed +./tools/lint zerver/models/__init__.py # Lint the file you just changed ./tools/test-backend zerver.tests.test_markdown.MarkdownTest.test_inline_youtube ./tools/test-backend MarkdownTest # Run `test-backend --help` for more options ./tools/test-js-with-node util @@ -60,7 +60,7 @@ eventually work with, each with its own page detailing how it works: Additionally, Zulip also has about a dozen smaller tests suites: - `tools/test-migrations`: Checks whether the `zerver/migrations` - migration content the models defined in `zerver/models.py`. See our + migration content the models defined in `zerver/models/*.py`. See our [schema migration documentation](../subsystems/schema-migrations.md) for details on how to do database migrations correctly. - `tools/test-documentation`: Checks for broken links in this diff --git a/docs/tutorials/new-feature-tutorial.md b/docs/tutorials/new-feature-tutorial.md index e871f2a5526d6..0e34ee02fa5f2 100644 --- a/docs/tutorials/new-feature-tutorial.md +++ b/docs/tutorials/new-feature-tutorial.md @@ -35,7 +35,7 @@ organization in Zulip). The following files are involved in the process: **Backend** -- `zerver/models.py`: Defines the database model. +- `zerver/models/realms.py`: Defines the database model. - `zerver/views/realm.py`: The view function that implements the API endpoint for editing realm objects. - `zerver/actions/realm_settings.py`: Contains code for updating and interacting with the database. @@ -73,7 +73,7 @@ organization in Zulip). The following files are involved in the process: ### Adding a field to the database **Update the model:** The server accesses the underlying database in -`zerver/models.py`. Add a new field in the appropriate class. +`zerver/models/realms.py`. Add a new field in the appropriate class. **Create and run the migration:** To create and apply a migration, run the following commands: @@ -185,10 +185,10 @@ task of requiring messages to have a topic, you can [view this commit](https://g First, update the database and model to store the new setting. Add a new boolean field, `mandatory_topics`, to the Realm model in -`zerver/models.py`. +`zerver/models/realms.py`. ```diff - # zerver/models.py + # zerver/models/realms.py class Realm(models.Model): # ... @@ -205,7 +205,7 @@ is the field's type. Add the new field to the `property_types` dictionary. ```diff - # zerver/models.py + # zerver/models/realms.py class Realm(models.Model) # ... diff --git a/help/contact-support.md b/help/contact-support.md index fedf78b99a99c..b8d99820344af 100644 --- a/help/contact-support.md +++ b/help/contact-support.md @@ -17,13 +17,25 @@ feedback. ## Support requests -For support requests regarding your Zulip Cloud organization or self-hosted -server, you can request interactive chat support in the [Zulip development -community][development-community], or [email Zulip -support](mailto:support@zulip.com). Response time: Usually within 1-3 business days, or - within one business day for paid customers. +* For support requests regarding your Zulip Cloud organization, you can request + interactive chat support in the [Zulip development + community](#zulip-community), or [email Zulip + support](mailto:support@zulip.com). + * Response time: Usually within 1-3 business days, or within one business + day for paid customers. -Phone support is available only for **Zulip Enterprise** customers. +* For support requests regarding your **self-hosted server**: + + * **Business** and **Enterprise** plan customers can request interactive + chat support in the [Zulip development community](#zulip-community), + or [email Zulip support](mailto:support@zulip.com). Phone support is + available for Enterprise customers upon request. + + * Response time: Usually within one business day. + + * **Self-managed** and **Community** plan customers can ask for help in the + [Zulip development community](#zulip-community). You will usually get a + friendly reply within 1-3 business days. ## Sales, billing, and partnerships diff --git a/help/getting-your-organization-started-with-zulip.md b/help/getting-your-organization-started-with-zulip.md index 33179114245f0..75666ce6fb3c0 100644 --- a/help/getting-your-organization-started-with-zulip.md +++ b/help/getting-your-organization-started-with-zulip.md @@ -19,16 +19,7 @@ us know! ## Choosing between Zulip Cloud and self-hosting -Whether [signing up for Zulip Cloud](/new/) or [self-hosting -Zulip](/self-hosting/) is the right choice for you depends on the -needs of your organization. - -If you aren’t sure what you need, our high quality export and import -tools ([cloud][export-cloud], [self-hosted][export-self-hosted]) -ensure you can always move from our hosting to yours (and back). - -[export-cloud]: /help/export-your-organization -[export-self-hosted]: https://zulip.readthedocs.io/en/stable/production/export-and-import.html +{!cloud-vs-self-hosting-intro.md!} ### Advantages of Zulip Cloud diff --git a/help/include/advantages-of-self-hosting-zulip.md b/help/include/advantages-of-self-hosting-zulip.md index ed052fd54d0df..c83e00cfb846e 100644 --- a/help/include/advantages-of-self-hosting-zulip.md +++ b/help/include/advantages-of-self-hosting-zulip.md @@ -1,6 +1,7 @@ -* Zulip is [100% open-source software][zulip-github], with no "open core" catch. - When you self-host Zulip, you get the same software as [Zulip Cloud - Standard](https://zulip.com/plans/) customers. +* All [self-hosted plans](https://zulip.com/plans/#self-hosted) offer the same + [100% open-source software] [zulip-github]. Organizations that do not require + support with their installation can always use Zulip for free with no + limitations. * Retain full control over your data and simplify compliance by self-hosting Zulip behind your firewall. diff --git a/help/include/advantages-of-zulip-cloud.md b/help/include/advantages-of-zulip-cloud.md index c08af3cc00144..4095c3af20a7d 100644 --- a/help/include/advantages-of-zulip-cloud.md +++ b/help/include/advantages-of-zulip-cloud.md @@ -1,6 +1,4 @@ * Simple managed solution, with no setup or maintenance overhead. [Sign up](https://zulip.com/new/) with just a few clicks. * Always updated to the latest version of Zulip. -* Anyone can [start with Zulip Cloud Free](https://zulip.com/new/). [Free or heavily - discounted Zulip Cloud Standard](https://zulip.com/plans/) pricing is available for - most non-business uses. +* Anyone can [start with Zulip Cloud Free](https://zulip.com/new/). diff --git a/help/include/cloud-vs-self-hosting-intro.md b/help/include/cloud-vs-self-hosting-intro.md new file mode 100644 index 0000000000000..9d241e0357e2b --- /dev/null +++ b/help/include/cloud-vs-self-hosting-intro.md @@ -0,0 +1,11 @@ +Whether [signing up for Zulip Cloud](https://zulip.com/new/) or [self-hosting +Zulip](https://zulip.com/self-hosting/) is the right choice for you depends on +the needs of your organization. + +If you aren’t sure what you need, our high quality export and import tools +([cloud][export-cloud], [self-hosted][export-self-hosted]) ensure you can always +move from our hosting to yours (and back). No matter which option you go with, +free or heavily discounted pricing is available for most non-business uses. + +[export-cloud]: /help/export-your-organization +[export-self-hosted]: https://zulip.readthedocs.io/en/stable/production/export-and-import.html diff --git a/help/include/legacy-log-in-intro.md b/help/include/legacy-log-in-intro.md new file mode 100644 index 0000000000000..08f685c32c821 --- /dev/null +++ b/help/include/legacy-log-in-intro.md @@ -0,0 +1,7 @@ +!!! tip "" + + A **server administrator** is anyone who sets up and manages your Zulip + installation. A **billing administrator** is anyone responsible for managing + your Zulip plan. + +**Server administrator steps:** diff --git a/help/include/legacy-log-in.md b/help/include/legacy-log-in.md new file mode 100644 index 0000000000000..e3ff5e3aab0ce --- /dev/null +++ b/help/include/legacy-log-in.md @@ -0,0 +1,15 @@ +1. Go to . + +1. Fill out the requested server information, and click **Continue**. + +1. Enter the e-mail address of the billing contact for your organization, + and click **Confirm email**. + +**Billing administrator steps:** + +1. In your e-mail account, open the e-mail you received + (Subject: Log in to Zulip plan management), and click **Log in**. + +1. Verify your information, and click **Continue**. If you are logging in for + the first time, you will need to enter your name and accept the [Terms of + Service](https://zulip.com/policies/terms). diff --git a/help/include/manual-billing-intro.md b/help/include/manual-billing-intro.md new file mode 100644 index 0000000000000..4beffbd2fb107 --- /dev/null +++ b/help/include/manual-billing-intro.md @@ -0,0 +1,11 @@ +With automatic billing, you automatically purchase a Zulip license for each user +in your organization at the start of each billing period (month or year). +[Deactivating a user][deactivate-user] frees up their license for reuse. +Additional licenses are purchased automatically as needed. + +With manual billing, you choose and pay for a preset user limit. If +he limit is reached, no more users can join until licenses are manually +added. + +In general, selecting automatic billing is highly recommended unless you +have a specific reason to do otherwise. diff --git a/help/include/register-server-legacy.md b/help/include/register-server-legacy.md new file mode 100644 index 0000000000000..88d36eaa8e07c --- /dev/null +++ b/help/include/register-server-legacy.md @@ -0,0 +1,3 @@ +1. Register the server with Zulip's Mobile Push Notification Service, following + [these + instructions](https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html). diff --git a/help/include/register-server.md b/help/include/register-server.md new file mode 100644 index 0000000000000..734a133acd04e --- /dev/null +++ b/help/include/register-server.md @@ -0,0 +1,3 @@ +1. Your Zulip server administrator should register the server with Zulip's + Mobile Push Notification Service, following [these + instructions](https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html). diff --git a/help/include/self-hosted-billing-admin-only.md b/help/include/self-hosted-billing-admin-only.md new file mode 100644 index 0000000000000..31524aa439c08 --- /dev/null +++ b/help/include/self-hosted-billing-admin-only.md @@ -0,0 +1,3 @@ +!!! warn "" + + This feature is only available to organization [owners](/help/roles-and-permissions) and billing administrators. diff --git a/help/include/self-hosted-billing-multiple-organizations.md b/help/include/self-hosted-billing-multiple-organizations.md new file mode 100644 index 0000000000000..b83f3bdf3117f --- /dev/null +++ b/help/include/self-hosted-billing-multiple-organizations.md @@ -0,0 +1,5 @@ +!!! warn "" + + If your server has more than one organization, upgrade to Zulip Server 8.0+ + manage billing and upgrades separately for each organization. Older servers + only support server-wide plan management. diff --git a/help/include/self-hosted-log-in.md b/help/include/self-hosted-log-in.md new file mode 100644 index 0000000000000..db07e21e0a5c6 --- /dev/null +++ b/help/include/self-hosted-log-in.md @@ -0,0 +1,16 @@ +1. Click on the **gear** () icon in + the upper right corner of the web or desktop app. + +1. Select **Plan management**. + +1. *(first-time log in)* Enter the e-mail address you want to use for plan + management, and click **Continue**. + +1. *(first-time log in)* In your e-mail account, open the e-mail you received + (Subject: Confirm email for Zulip plan management), and click **Confirm and + log in**. + +1. *(first-time log in)* Enter your name, configure your email preferences, and + accept the [Terms of Service](https://zulip.com/policies/terms). + +1. Verify your information, and click **Continue**. diff --git a/help/include/sidebar_index.md b/help/include/sidebar_index.md index 6008344f5972d..a2ece81ab5639 100644 --- a/help/include/sidebar_index.md +++ b/help/include/sidebar_index.md @@ -241,6 +241,7 @@ ## Support * [View Zulip version](/help/view-zulip-version) * [Zulip Cloud billing](/help/zulip-cloud-billing) +* [Self-hosted billing](/help/self-hosted-billing) * [Support the Zulip project](/help/support-zulip-project) * [Linking to the Zulip website](/help/linking-to-zulip-website) * [Contact support](/help/contact-support) diff --git a/help/self-hosted-billing.md b/help/self-hosted-billing.md index 393c5ee58abde..2a051ed4246fc 100644 --- a/help/self-hosted-billing.md +++ b/help/self-hosted-billing.md @@ -6,16 +6,410 @@ questions about plans and billing for self-hosted organizations. Please refer to details. If you have any questions not answered here, please don't hesitate to reach out at [sales@zulip.com](mailto:sales@zulip.com). +## Business plan details and upgrades + +The Business plan is appropriate for most business organizations. It includes +unlimited access to the Mobile Push Notification Service and commercial support +for dozens of features and integrations that help businesses take full advantage +of their Zulip implementation. + +For businesses with up to 10 Zulip users, the Self-managed plan is a good +option, and includes free access to the Mobile Push Notification service. For +commercial support with your installation, sign up for the Business plan, with a +minimum purchase of 10 licenses. + +If you organization requires hands-on support, such as real-time support during +installation and upgrades, support for advanced deployment options, custom +feature development or integrations, etc., should contact +[sales@zulip.com](mailto:sales@zulip.com) to discuss pricing. + +Business plan discounts are available in a variety of situations; see +[below](#business-plan-discounts) for details. + +### Upgrades for legacy customers + +Any Zulip server that registered for Zulip's [Mobile Push Notification +Service](https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html) +prior to December 12, 2023 is considered to be a **legacy customer**. Legacy +customers can continue using the notification service for free (no action +required) until February 15, 2024. + +To continue using the service after that date, organizations with more than 10 +users must upgrade to the Business, Community or Enterprise plan. When you +upgrade to the Business plan, you can start the plan right away (if you‘d like +your technical support to start immediately), or schedule a February 15 start date. + +#### Do I have to upgrade my server first? + +While upgrading your Zulip server to version 8.0+ makes it more convenient to +manage your plan, you do not have to upgrade your Zulip installation in order to +sign up for a plan. **The same plans are offered for all Zulip versions.** + +In addition to hundreds of other improvements, upgrading to Zulip Server 8.0+ lets +you: + +- Easily log in to Zulip plan management, without an additional server + authentication step. + +- Separately manage plans for all the organizations on your server. + +- Upload only the [basic + metadata](https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html#uploading-basic-metadata) + required for the service, without also [uploading usage + statistics](https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html#uploading-usage-statistics). + +If you upgrade your server after signing up for a plan, you will be able to +transfer your plan to an organization on your server. If your server has one +organization on it, this will happen automatically. Otherwise, contact +[support@zulip.com](mailto:support@zulip.com) for help. + +#### Upgrading to Zulip Business + +{!self-hosted-billing-multiple-organizations.md!} + +{start_tabs} + +{tab|v8} + +{!self-hosted-billing-admin-only.md!} + +{!self-hosted-log-in.md!} + +1. You will be logged in to Zulip's [Plans and pricing + page](https://zulip.com/plans/). Under the **Business** pricing plan on the + **Self-hosted** tab, click **Upgrade to Business**. + +1. Select your preferred option from the **Payment schedule** dropdown. + +1. Under **Plan start date**, select **February 15, 2024** or **Today**. + +1. Click **Add card** to enter your payment details. + +1. Click **Purchase Zulip Business** to upgrade immediately, or **Schedule + upgrade to Zulip Business** to schedule an upgrade for February 15. + +!!! warn "" + + If your server hosts more than one organization, commercial + support for server-wide configurations requires upgrading the + organization with the largest number of users. + +{tab|older-versions} + +{!legacy-log-in-intro.md!} + +{!legacy-log-in.md!} + +1. Select your preferred option from the **Payment schedule** dropdown. + +1. Under **Plan start date**, select **February 15, 2024** or **Today**. + +1. Click **Add card** to enter your payment details. + +1. Click **Purchase Zulip Business** to upgrade immediately, or **Schedule + upgrade to Zulip Business** to schedule an upgrade for February 15. + +{end_tabs} + +### Upgrades for new customers + +**New customers** are eligible for a free 30-day trial of Zulip Business. An +organization is considered to be a new customer if: + +- It was not registered for Zulip's [Mobile Push Notification + Service](https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html) + prior to December 12, 2023, and +- It has never previously signed up for a self-hosted Zulip plan (Business, + Community or Enterprise). + +{!self-hosted-billing-multiple-organizations.md!} + +{start_tabs} + +{tab|v8} + +{!register-server.md!} + +{!self-hosted-log-in.md!} + +1. Under the **Business** pricing plan on the **Self-hosted** tab, click + **Start 30-day trial**. + +2. Click **Add card** to enter your payment details. + +3. Click **Start 30-day trial** to start your free trial. + +!!! tip "" + + Once you start the trial, you can switch between monthly and annual billing + on your organization's billing page. + +!!! warn "" + + If your server hosts more than one organization, commercial + support for server-wide configurations requires upgrading the + organization with the largest number of users. + +{tab|older-versions} + +{!legacy-log-in-intro.md!} + +{!register-server-legacy.md!} + +{!legacy-log-in.md!} + +1. Under the **Business** pricing plan on the **Self-hosted** tab, click + **Start 30-day trial**. + +1. Click **Add card** to enter your payment details. + +1. Click **Start 30-day trial** to start your free trial. + +!!! tip "" + + Once you start the trial, you can switch between monthly and annual billing + on your organization's billing page. + +{end_tabs} + +## Manage billing + +{!self-hosted-billing-multiple-organizations.md!} + +{start_tabs} + +{tab|v8} + +{!self-hosted-log-in.md!} + +{tab|older-versions} + +{!legacy-log-in-intro.md!} + +{!legacy-log-in.md!} + +{end_tabs} + +## Cancel paid plan + +{!self-hosted-billing-multiple-organizations.md!} + +If you cancel your plan, your organization will be downgraded to the +**Self-managed** plan at the end of the current billing period. + +{start_tabs} + +{tab|v8} + +{!self-hosted-log-in.md!} + +1. At the bottom of the page, click **Cancel plan**. + +2. Click **Downgrade** to confirm. + +{tab|older-versions} + +{!legacy-log-in-intro.md!} + +{!legacy-log-in.md!} + +1. At the bottom of the page, click **Cancel plan**. + +1. Click **Downgrade** to confirm. + +{end_tabs} + +## Free Community plan + +Zulip sponsors free plans for over 1000 worthy organizations. The following +types of organizations are generally eligible for the free Community plan. + +- Open-source projects, including projects with a small paid team. +- Research organizations, such as research groups, cross-institutional + collaborations, etc. +- Education and non-profit organizations with up to 100 users. +- Communities and personal organizations (clubs, groups of + friends, volunteer groups, etc.). + +Organizations that have up to 10 users, or do not require mobile push +notifications, will likely find the Self-managed plan to be the most convenient +option. Larger organizations are encouraged to apply for the free Community +plan, which includes unlimited push notifications and support for many Zulip +features. + +If you aren't sure whether your organization qualifies, submitting a sponsorship +form describing your situation is a great starting point. Many organizations +that don't qualify for the Community plan can still receive discounted Business +plan pricing. + +### Apply for Community plan + +These instructions describe the Community plan application process for an +existing Zulip server. If you would like to inquire about Community plan +eligibility prior to setting up a server, contact +[sales@zulip.com](mailto:sales@zulip.com). + +!!! tip "" + + Organizations that do not qualify for a Community plan may be offered a + discount on the Business plan. + +{start_tabs} + +{tab|v8} + +{!register-server.md!} + +{!self-hosted-log-in.md!} + +1. Under the **Community** pricing plan on the **Self-hosted** tab, click + **Apply to upgrade**. + +1. Fill out the requested information, and click **Submit**. Your application + will be reviewed for Community plan eligibility. + +{tab|older-versions} + +{!legacy-log-in-intro.md!} + +{!register-server-legacy.md!} + +{!legacy-log-in.md!} + +1. Under the **Community** pricing plan on the **Self-hosted** tab, click + **Apply to upgrade**. + +1. Fill out the requested information, and click **Submit**. Your application + will be reviewed for Community plan eligibility. + +!!! tip "" + + Organizations that do not qualify for a Community plan may be offered a + discount on the Business plan. + +{end_tabs} + +## Business plan discounts + +The following types of organizations are generally eligible for significant +discounts on the Zulip Business plan. You can also contact +[sales@zulip.com](mailto:sales@zulip.com) to discuss bulk discount pricing for a +large organization. + +- **Education pricing** is available with a minimum purchase of 100 licenses. + Organizations with up to 100 users are eligible for free Community plan + sponsorship. + + - **For-profit education pricing**: $1 per user per month with annual billing + ($1.20/month billed monthly). + + - **Non-profit education pricing**: $0.67 per user per month with annual billing + ($0.80/month billed monthly). The non-profit discount applies to + online purchases only (no additional legal agreements) for use at registered + non-profit institutions (e.g. colleges and universities). + +- **Non-profit** discounts of 85+% are available with a minimum purchase of 100 + licenses. Organizations with up to 100 users are eligible for free Community plan + sponsorship. + +- Discounts are available for organizations based in the **developing world**. + +- Any organization where many users are **not paid staff** is likely eligible for a discount. + +### Apply for Business plan discount + +These instructions describe the Business plan discount application process for an +existing Zulip server. If you would like to inquire about Business plan discount +eligibility prior to setting up a server, contact +[sales@zulip.com](mailto:sales@zulip.com). + +{start_tabs} + +{tab|v8} + +{!register-server.md!} + +{!self-hosted-log-in.md!} + +1. Under **Sponsorship and discounts** on the **Self-hosted** tab, click + **Request sponsorship**. + +1. Under **Plan**, select **Business**. + +1. Fill out the requested information, and click **Submit**. Your application + will be reviewed for discount eligibility. + +{tab|older-versions} + +{!legacy-log-in-intro.md!} + +{!register-server-legacy.md!} + +{!legacy-log-in.md!} + +1. Under **Sponsorship and discounts** on the **Self-hosted** tab, click + **Request sponsorship**. + +1. Under **Plan**, select **Business**. + +1. Fill out the requested information, and click **Submit**. Your application + will be reviewed for discount eligibility. + +{end_tabs} + +## Payment methods + +### Can I pay by credit card and/or invoice? + +You can always use a credit card to pay. If you would like to pay by invoice, +you will need to sign up for an annual plan. + +### What is the difference between automatic and manual billing? + +{!manual-billing-intro.md!} + +#### Manually manage licenses + +{start_tabs} + +{tab|v8} + +{!self-hosted-log-in.md!} + +1. Modify **Number of licenses for current billing period** or **Number of + licenses for next billing period**, and click **Update**. + +!!! tip "" + + You can only increase the number of licenses for the current billing period. + +{tab|older-versions} + +{!legacy-log-in-intro.md!} + +{!legacy-log-in.md!} + +1. Modify **Number of licenses for current billing period** or **Number of + licenses for next billing period**, and click **Update**. + +!!! tip "" + + You can only increase the number of licenses for the current billing period. + +{end_tabs} + +## Self-managed installations + Zulip is 100% open-source. Organizations that do not require support with their -installation can always use Zulip for free with no limitations. +installation can always use Zulip for free with no limitations. Additionally, +the [Mobile Push Notification +Service](https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html) +is provided free of charge for organizations with up to 10 users. You can self-manage your Zulip installation without signing up for a plan. Get started with the [installation guide](https://zulip.readthedocs.io/en/stable/production/install.html). -To inquire about an Enterprise plan, contact -[sales@zulip.com](mailto:sales@zulip.com). - ## Related articles * [Trying out Zulip](/help/trying-out-zulip) diff --git a/help/zulip-cloud-billing.md b/help/zulip-cloud-billing.md index e56dc198f5485..331c3bb6b21e2 100644 --- a/help/zulip-cloud-billing.md +++ b/help/zulip-cloud-billing.md @@ -152,6 +152,7 @@ your regular per-user pricing for each additional guest. * [Trying out Zulip](/help/trying-out-zulip) * [Zulip Cloud or self-hosting?](/help/zulip-cloud-or-self-hosting) +* [Self-hosted billing](/help/self-hosted-billing) * [Migrating from other chat tools](/help/migrating-from-other-chat-tools) * [Contact support](/help/contact-support) diff --git a/help/zulip-cloud-or-self-hosting.md b/help/zulip-cloud-or-self-hosting.md index f5d1882adaf7b..4868601774061 100644 --- a/help/zulip-cloud-or-self-hosting.md +++ b/help/zulip-cloud-or-self-hosting.md @@ -1,12 +1,6 @@ # Choosing between Zulip Cloud and self-hosting -Whether [signing up for Zulip Cloud](https://zulip.com/new/) or [self-hosting -Zulip](https://zulip.com/self-hosting/) is the right choice for you depends on -the needs of your organization. - -If you aren’t sure what you need, our high quality export and import -tools ([cloud][export-cloud], [self-hosted][export-self-hosted]) -ensure you can always move from our hosting to yours (and back). +{!cloud-vs-self-hosting-intro.md!} ## Advantages of Zulip Cloud @@ -21,9 +15,6 @@ ensure you can always move from our hosting to yours (and back). * [Sign up for Zulip Cloud](https://zulip.com/new/) * [Self-hosting Zulip](https://zulip.com/self-hosting/) * [Trying out Zulip](/help/trying-out-zulip) -* [Setting up your organization](/help/getting-your-organization-started-with-zulip) -* [Migrating from other chat tools](/help/migrating-from-other-chat-tools) -* [Getting started with Zulip](/help/getting-started-with-zulip) - -[export-cloud]: /help/export-your-organization -[export-self-hosted]: https://zulip.readthedocs.io/en/stable/production/export-and-import.html +* [Plans and pricing](https://zulip.com/plans/) +* [Zulip Cloud billing](/help/zulip-cloud-billing) +* [Self-hosted billing](/help/self-hosted-billing) diff --git a/locale/ar/LC_MESSAGES/django.po b/locale/ar/LC_MESSAGES/django.po index 7292824ada39f..e0481dd241ed4 100644 --- a/locale/ar/LC_MESSAGES/django.po +++ b/locale/ar/LC_MESSAGES/django.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: RanaJT, 2023\n" "Language-Team: Arabic (http://app.transifex.com/zulip/zulip/language/ar/)\n" @@ -66,7 +66,7 @@ msgstr "وقت البدء متأخر عن وقت الانتهاء. البدء: { msgid "No analytics data available. Please contact your server administrator." msgstr "No analytics data available. Please contact your server administrator." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -137,124 +137,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} تنتهي في {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Unknown payment method. Please contact {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "هناك خطأ ما. الرجاء التواصل {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "هناك خطأ ما. الرجاء إعادة تحميل الصفحة." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "هناك خطأ ما. الرجاء الإنتظار لبضع دقائق و إعادة المحاولة." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "غير قادر على تحديث الخطة. انتهت صلاحية الخطة واستبدلت بخطة جديدة." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "غير قادر على تحديث الخطة. انتهت الخطة." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "غير قادر على تحديث التراخيص يدويًا. خطتك على إدارة الترخيص التلقائي." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "خطتك موجودة بالفعل على تراخيص {licenses} في فترة الفوترة الحالية." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "لا يمكنك تقليل التراخيص في فترة الفوترة الحالية." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "تمت جدولة خطتك بالفعل للتجديد مع تراخيص {licenses_at_next_renewal}." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "لا شيء لتغييره." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "لا يوجد زبون لهذه المنظمة!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "الجلسة غير موجودة" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "يجب أن يكون مسؤول الفوترة أو مالك منظمة" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "لم يتم العثور على القصد من الدفع" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "مرر stripe_session_id أو stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -262,33 +262,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -315,7 +319,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "خطأ في الخادم الداخلي" @@ -564,31 +568,31 @@ msgstr "تأكد من نسخ الرابط بشكل صحيح في متصفحك. msgid "Billing" msgstr "الفواتير" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "إلغاء" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "تأكيد" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -687,6 +691,10 @@ msgstr "تسعير التعليم" msgid "View pricing" msgstr "عرض السعر" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "\"زوليب\" للأعمال" @@ -828,8 +836,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2251,7 +2257,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2260,7 +2265,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2268,7 +2273,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2284,7 +2288,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2301,15 +2304,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3354,65 +3366,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "الرسالة(الرسائل) غير صالحة" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "تعذر تقديم الرسالة" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "مُتوقع غرفة واحدة بالضبط" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "نوع البيانات غير صالح للغرفة" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "نوع البيانات غير صالح للمستلمين" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "قد تحتوي قوائم المستلمين على رسائل بريد إلكتروني أو معرفات ID للمستخدم، ولكن ليس كليهما." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "حاول الروبوت الخاص بك {bot_identity} إرسال رسالة إلى الغرفة ذات الـ ID {stream_id} ، ولكن لا يوجد غرفة بهذا الـ ID." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "حاول الروبوت الخاص بك {bot_identity} إرسال رسالة إلى الغرفة {stream_name}، لكن هذه الغرفة غير موجودة. انقر [هنا]({new_stream_link}) لإنشائها." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "حاول الروبوت الخاص بك {bot_identity} إرسال رسالة إلى الغرفة {stream_name}. الغرفة موجودة ولكن ليس فيها أي مشتركين." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "مواضيع مطلوبة في هذه المنظمة" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "الأدوات: أرسل مبرمج الـ API محتوى JSON غير صالح" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "الأدوات: {error_msg}" @@ -3429,28 +3441,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3727,7 +3739,7 @@ msgstr "حدث خطأ أثناء حذف المرفق. رجاءً حاول مرة msgid "Message must have recipients!" msgstr "يجب أن تحتوي الرسالة على مستلمين!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3906,7 +3918,7 @@ msgid "API usage exceeded rate limit" msgstr "تجاوز استخدام API حد المعدل" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON تالف" @@ -4361,7 +4373,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "الرمز غير موجود" @@ -4414,7 +4426,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "المستخدم غير مخول لهذا الاستعلام" @@ -4461,7 +4473,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4574,7 +4586,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} ليس تاريخًا" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} ليس dict" @@ -4612,7 +4624,7 @@ msgid "{var_name} is too large" msgstr "{var_name} كبير جدًا" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} ليست قائمة" @@ -4776,7 +4788,7 @@ msgstr "نوع الروبوت غير صالح" msgid "Invalid interface type" msgstr "نوع الواجهة غير صالح" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4839,46 +4851,46 @@ msgstr "{var_name} ليس allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} خطأ)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} ليس عنوان URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' لا يمكن أن يكون فارغًا." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' ليس اختيارًا صالحًا لـ '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} ليس نص أو قائمة أعداد صحيحة" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} ليس نص أو عددًا صحيحًا" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} لا يحتوي على طول" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} مفقود" @@ -5067,57 +5079,57 @@ msgstr "يمكن فقط لمسؤولي المنظمة والوسطاء النش msgid "Only organization full members can post" msgstr "فقط أعضاء المنظمة الكاملين يمكنهم النشر" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "رموز Unicode التعبيرية" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "رموز تعبيرية مخصصة" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "رموز \"زوليب\" التعبيرية الإضافية" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "قائمة الخيارات" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "منتقي الأشخاص" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "نص قصير" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "نص طويل" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "منتقي التاريخ" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "رابط" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "حساب خارجي" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "الضمائر" @@ -5133,20 +5145,20 @@ msgstr "نظام تشغيل غير معروف" msgid "An unknown browser" msgstr "متصفح غير معروف" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "وسيط 'queue_id' مفقود" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "وسيط 'last_event_id' مفقود" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "تم بالفعل إزالة حدث أحدث من {event_id}!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "الحدث {event_id} لم يكن في قائمة الانتظار هذه" @@ -5318,19 +5330,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "المرسل مفقود" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "النسخ المتطابق غير مسموح به مع IDs المستخدم المستلم" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "رسالة معكوسة غير صالحة" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "لا يسمح بالنسخ المتطابق لـ Zephyr في هذه المنظمة" @@ -5396,26 +5408,26 @@ msgstr "يجب أن يكون أحد الوسطاء التاليين على ال msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "يجب تمكين طريقة مصادقة واحدة على الأقل." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "يجب أن تكون منظمة تجريبية." @@ -5893,11 +5905,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "المجال الفرعي غير صالح لحارس إرسال الإشعارات" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "يجب التحقق من الصحة باستخدام مفتاح API لخادم \"زوليب\" صالح" @@ -5911,43 +5923,43 @@ msgstr "UUID غير صالح" msgid "Invalid token type" msgstr "نوع الرمز غير صالح" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "user_id أو user_uuid مفقود" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "البيانات خارج الترتيب." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/be/LC_MESSAGES/django.po b/locale/be/LC_MESSAGES/django.po index b93ed79465bf5..c04311c4e4cb6 100644 --- a/locale/be/LC_MESSAGES/django.po +++ b/locale/be/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:58+0000\n" +"POT-Creation-Date: 2023-12-15 17:44+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -63,7 +63,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -131,124 +131,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this " "page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new " "plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2901 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You " @@ -258,33 +258,37 @@ msgid "" "we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:136 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:138 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:275 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:280 +#: corporate/views/remote_billing_page.py:693 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:506 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:513 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:517 msgid "Your server registration has been deactivated." msgstr "" @@ -312,7 +316,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -569,31 +573,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:313 templates/corporate/billing.html:339 +#: templates/corporate/billing.html:368 templates/corporate/billing.html:397 +#: templates/corporate/billing.html:423 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:324 templates/corporate/billing.html:353 +#: templates/corporate/billing.html:382 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:326 templates/corporate/billing.html:410 +#: templates/corporate/billing.html:433 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:355 templates/corporate/billing.html:384 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:408 templates/corporate/billing.html:431 msgid "Never mind" msgstr "" @@ -692,6 +696,62 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:4 +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:16 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:4 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:16 +msgid "Plan management not available" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:18 +#, python-format +msgid "" +" Plan management is not available for this\n" +" organization, because your Zulip server is already " +"on a\n" +" %(server_plan_name)s plan, which covers all\n" +" organizations on this server. Follow the log\n" +" in instructions\n" +" for All older versions\n" +" of the Zulip server to manage your plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:29 +msgid "" +" To move the plan from the server to this\n" +" organization, or for other questions, contact support.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:18 +msgid "" +"\n" +" Plan management for this server is not available " +"because at least one organization\n" +" hosted on this server already has an active plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:24 +#, python-format +msgid "" +"\n" +" Log in to plan management for your\n" +" organization instead, or contact support with any questions.\n" +" " +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -834,8 +894,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2279,7 +2337,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2288,7 +2345,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2296,7 +2353,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2312,7 +2368,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2329,16 +2384,25 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for " -"%(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2369,7 +2433,7 @@ msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " -"appreciate it!." +"appreciate it!" msgstr "" #: templates/zerver/find_account.html:4 @@ -3398,65 +3462,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but " "that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The " "stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3473,27 +3537,27 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 -#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:128 +#: zerver/actions/scheduled_messages.py:155 +#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3772,7 +3836,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3951,7 +4015,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4404,45 +4468,45 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:739 zilencer/views.py:241 msgid "Token does not exist" msgstr "" -#: zerver/lib/push_notifications.py:902 +#: zerver/lib/push_notifications.py:910 msgid "" "This organization has disabled including message content in mobile push " "notifications" msgstr "" -#: zerver/lib/push_notifications.py:1015 +#: zerver/lib/push_notifications.py:1023 #, python-brace-format msgid "{full_name} mentioned @{user_group_name}:" msgstr "" -#: zerver/lib/push_notifications.py:1019 +#: zerver/lib/push_notifications.py:1027 #, python-brace-format msgid "{full_name} mentioned you:" msgstr "" -#: zerver/lib/push_notifications.py:1026 +#: zerver/lib/push_notifications.py:1034 #, python-brace-format msgid "{full_name} mentioned everyone:" msgstr "" -#: zerver/lib/push_notifications.py:1436 +#: zerver/lib/push_notifications.py:1438 msgid "Test notification" msgstr "" -#: zerver/lib/push_notifications.py:1437 +#: zerver/lib/push_notifications.py:1439 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." msgstr "" -#: zerver/lib/push_notifications.py:1488 +#: zerver/lib/push_notifications.py:1490 msgid "Device not recognized" msgstr "" -#: zerver/lib/push_notifications.py:1500 +#: zerver/lib/push_notifications.py:1502 msgid "Device not recognized by the push bouncer" msgstr "" @@ -4457,7 +4521,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4504,7 +4568,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4617,7 +4681,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4655,7 +4719,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4882,46 +4946,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5110,15 +5174,15 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" @@ -5176,20 +5240,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5361,19 +5425,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5439,26 +5503,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5936,61 +6000,61 @@ msgid "" "({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" #. error -#: zilencer/views.py:77 zilencer/views.py:79 +#: zilencer/views.py:81 zilencer/views.py:83 msgid "Invalid UUID" msgstr "" #. error -#: zilencer/views.py:84 +#: zilencer/views.py:88 msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:126 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:182 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:185 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:610 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:613 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:620 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:696 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:957 msgid "" "Failed to migrate customer from server to realms. Please contact support for " "assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:1009 msgid "Malformed audit log data" msgstr "" diff --git a/locale/bg/LC_MESSAGES/django.po b/locale/bg/LC_MESSAGES/django.po index ef07ac079d4f0..b7c0549a15f8d 100644 --- a/locale/bg/LC_MESSAGES/django.po +++ b/locale/bg/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Veselin Georgiev , 2018\n" "Language-Team: Bulgarian (http://app.transifex.com/zulip/zulip/language/bg/)\n" @@ -63,7 +63,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "Няма налични данни за анализ. Моля свържете се с вашия администратор на сървъра." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -134,124 +134,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -259,33 +259,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -312,7 +316,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -561,31 +565,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Откажи" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -684,6 +688,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -825,8 +833,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2248,7 +2254,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2257,7 +2262,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2265,7 +2270,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2281,7 +2285,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2298,15 +2301,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3351,65 +3363,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3426,28 +3438,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3724,7 +3736,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3903,7 +3915,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4358,7 +4370,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4411,7 +4423,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4458,7 +4470,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4571,7 +4583,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4609,7 +4621,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4773,7 +4785,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4836,46 +4848,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5064,57 +5076,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5130,20 +5142,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5315,19 +5327,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5393,26 +5405,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5890,11 +5902,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5908,43 +5920,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/bqi/LC_MESSAGES/django.po b/locale/bqi/LC_MESSAGES/django.po index cf6053b7ea09f..5fa6e10a9f92c 100644 --- a/locale/bqi/LC_MESSAGES/django.po +++ b/locale/bqi/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: hosêyň abāspanā , 2023\n" "Language-Team: Luri (Bakhtiari) (http://app.transifex.com/zulip/zulip/language/bqi/)\n" @@ -62,7 +62,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -133,124 +133,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -258,33 +258,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -311,7 +315,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "xatā mêni,mên sêrvêr" @@ -560,31 +564,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "raď kerdên" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -683,6 +687,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -824,8 +832,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2247,7 +2253,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2256,7 +2261,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2264,7 +2269,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2280,7 +2284,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2297,15 +2300,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3350,65 +3362,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "payom (ā) nazêbāl" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3425,28 +3437,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3723,7 +3735,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3902,7 +3914,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON êštêvā" @@ -4357,7 +4369,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "nêšovêi nî" @@ -4410,7 +4422,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4457,7 +4469,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4570,7 +4582,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4608,7 +4620,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4772,7 +4784,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4835,46 +4847,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5063,57 +5075,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "êmuji sêfârêši" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "ling" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5129,20 +5141,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5314,19 +5326,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "fêšnāko pati hêď" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5392,26 +5404,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5889,11 +5901,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5907,43 +5919,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index 0aedeb8e91385..4f95a0f28dafb 100644 --- a/locale/ca/LC_MESSAGES/django.po +++ b/locale/ca/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Gonçal Garcés, 2020-2021\n" "Language-Team: Catalan (http://app.transifex.com/zulip/zulip/language/ca/)\n" @@ -64,7 +64,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "Sense dades d'analytics. Parleu amb l'administrador." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -135,124 +135,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Alguna cosa ha fallat. Recarregueu la pàgina." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Alguna cosa ha fallat. Espereu uns segons i torneu a provar-ho." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -260,33 +260,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -313,7 +317,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Error intern del servidor" @@ -562,31 +566,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Cancel·la" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -685,6 +689,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -826,8 +834,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2249,7 +2255,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2258,7 +2263,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2266,7 +2271,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2282,7 +2286,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2299,15 +2302,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3352,65 +3364,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3427,28 +3439,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3725,7 +3737,7 @@ msgstr "S'ha produït un error a l'adjuntar el fitxer. Torneu a intentar-ho més msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3904,7 +3916,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4359,7 +4371,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4412,7 +4424,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4459,7 +4471,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4572,7 +4584,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4610,7 +4622,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4774,7 +4786,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4837,46 +4849,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5065,57 +5077,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emojis personalitzats" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Enllaç" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5131,20 +5143,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5316,19 +5328,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5394,26 +5406,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5891,11 +5903,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5909,43 +5921,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/cs/LC_MESSAGES/django.po b/locale/cs/LC_MESSAGES/django.po index 3e5b70628fecf..f0f7b50e58b0c 100644 --- a/locale/cs/LC_MESSAGES/django.po +++ b/locale/cs/LC_MESSAGES/django.po @@ -14,7 +14,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: fri, 2017-2023\n" "Language-Team: Czech (http://app.transifex.com/zulip/zulip/language/cs/)\n" @@ -68,7 +68,7 @@ msgstr "Čas začátku je později než čas konce. Začátek: {start}, konec: { msgid "No analytics data available. Please contact your server administrator." msgstr "Žádná analytická data nejsou k dispozici. Spojte se, prosím, se správcem serveru." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -139,124 +139,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "Musíte zakoupit licence pro všechny činné uživatele ve vaší organizaci (minimum {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Účty s více než {max_licenses} licencemi nelze zpracovat z této stránky. Pro dokončení povýšení se, prosím, spojte s {email}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "Žádná platební metoda v záznamu." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} končí za {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Neznámá platební metoda. Spojte se, prosím, s {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Nastala chyba. Spojte se, prosím, s {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Něco se nepodařilo. Načtěte prosím stránku znovu." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Něco se nepodařilo. Počkejte prosím několik sekund a zkuste to znovu." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Aktualizace platebního plánu se nezdařila. Plán již vypršel a byl nahrazen novým." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Aktualizace platebního plánu se nezdařila. Plán již byl ukončen." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Ruční aktualizace licencí se nezdařila. Váš platební plán má nastavenu automatickou správu licencí." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Váš aktuální platební plán již využívá {licenses} licencí v aktuálním zúčtovacím období." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Nelze snížit počet licencí v aktuálním zúčtovacím období." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Váš platební plán je již nastaven aby v dalším zúčtovacím období obnovil {licenses_at_next_renewal} licencí." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Není co měnit." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Pro tuto organizaci neexistuje žádný zákazník!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Relace nebyla nalezena" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Musí být správcem plateb nebo vlastníkem organizace" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Záměr platby nenalezen" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Vložte stripe_session_id nebo stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -264,33 +264,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "Žádost vaší organizace o sponzorovaný hosting byla schválena! Byli jste bezplatně převedeni na {plan_name}. {emoji}\n\nPokud byste mohli {begin_link} uvést společnost Zulip jako sponzora na svých webových stránkách{end_link}, budeme vám velmi vděčni!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -317,7 +321,7 @@ msgid "" msgstr "\n Pokud je tato chyba neočekávaná, můžete\n se spojit s podporou.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Vnitřní chyba serveru" @@ -566,31 +570,31 @@ msgstr "Ujistěte se, že jste odkaz správně zkopírovali do svého prohlíže msgid "Billing" msgstr "Fakturace" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Zavřít okno" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Zrušit" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "Ponížit" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Potvrdit" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "Nevadí." @@ -689,6 +693,10 @@ msgstr "Ceny za vzdělávání" msgid "View pricing" msgstr "Zobrazit ceny" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip pro podnikání" @@ -830,8 +838,6 @@ msgstr "Již máte účet." #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2096,7 +2102,7 @@ msgstr "Jít na Zulip" #: templates/zerver/emails/onboarding_zulip_topics.subject.txt:1 msgid "Keep your conversations organized with topics" -msgstr "Udržujte konverzace uspořádané pomocí témat" +msgstr "Udržujte rozhovory uspořádány pomocí témat" #: templates/zerver/emails/onboarding_zulip_topics.txt:3 msgid "" @@ -2253,7 +2259,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2262,7 +2267,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2270,7 +2275,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2286,7 +2290,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2303,15 +2306,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2403,7 +2415,7 @@ msgstr "Bezpečnost" #: templates/zerver/footer.html:14 msgid "Integrations" -msgstr "Integrace" +msgstr "Doplňky" #: templates/zerver/footer.html:15 msgid "Desktop & mobile apps" @@ -2585,7 +2597,7 @@ msgstr "Stáhnout nejnovější vydání." #: templates/zerver/integrations/index.html:23 #, python-format msgid "Over %(integrations_count_display)s native integrations." -msgstr "Více než %(integrations_count_display)s nativních integrací." +msgstr "Více než %(integrations_count_display)s nativních doplňků." #: templates/zerver/integrations/index.html:27 msgid "" @@ -2599,12 +2611,12 @@ msgstr "\n A stovky dalších od\n #: templates/zerver/integrations/index.html:40 msgid "Search integrations" -msgstr "Hledat integrace" +msgstr "Hledat doplňky" #: templates/zerver/integrations/index.html:61 #: templates/zerver/integrations/index.html:88 msgid "Custom integrations" -msgstr "Vlastní začlenění" +msgstr "Vlastní doplňky" #: templates/zerver/integrations/index.html:63 #: templates/zerver/integrations/index.html:90 @@ -3356,65 +3368,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Neplatná zpráva/y" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Nelze zpracovat zprávu" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Očekáván přesně jeden kanál" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Neplatný datový typ pro kanál" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Neplatný datový typ pro příjemce" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Seznamy příjemců mohou obsahovat adresy nebo uživatelská ID, ale ne obojí." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Robot {bot_identity} se pokusil poslat zprávu kanálu s ID {stream_id}, ale žádný takový kanál neexistuje." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Robot {bot_identity} se pokusil poslat zprávu kanálu {stream_name}, ale tento kanál neexistuje. Klepněte [zde]({new_stream_link}) pro jeho vytvoření." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Robot {bot_identity} se pokusil poslat zprávu kanálu {stream_name}. ento kanál existuje. Nemá však žádné odběratele." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." -msgstr "" +msgstr "Přímé zprávy jsou v této organizaci zakázány." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Témata jsou v této organizaci povinná" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "\"Widgety\": programátor API odeslal neplatný JSON" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Součástky: {error_msg}" @@ -3431,28 +3443,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3729,7 +3741,7 @@ msgstr "Při mazání přílohy se vyskytla chyba. Zkuste to, prosím, znovu." msgid "Message must have recipients!" msgstr "Zpráva musí mít příjemce!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3908,7 +3920,7 @@ msgid "API usage exceeded rate limit" msgstr "Používání API překročilo rychlostní omezení" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Nevalidní JSON" @@ -4075,11 +4087,11 @@ msgstr "Klepněte zde pro zahájení nové konverzace. Vyberte si téma (nejlep #: zerver/lib/integrations.py:38 msgid "Integration frameworks" -msgstr "Frameworky integrací" +msgstr "Rámce doplňků" #: zerver/lib/integrations.py:44 msgid "Continuous integration" -msgstr "Nepřetržitá integrace" +msgstr "Průběžná integrace" #: zerver/lib/integrations.py:45 msgid "Customer support" @@ -4363,7 +4375,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Elektronický klíč neexistuje" @@ -4416,7 +4428,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Uživatel není oprávněn položit tento dotaz" @@ -4463,7 +4475,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4576,7 +4588,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} není datum" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} není slovník" @@ -4614,7 +4626,7 @@ msgid "{var_name} is too large" msgstr "{var_name} je moc velký" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} není seznam" @@ -4754,7 +4766,7 @@ msgstr "Špatný název nebo uživatelské jméno" #: zerver/lib/users.py:105 #, python-brace-format msgid "Invalid integration '{integration_name}'." -msgstr "" +msgstr "Neplatný doplněk '{integration_name}'." #: zerver/lib/users.py:111 #, python-brace-format @@ -4778,7 +4790,7 @@ msgstr "Neplatný typ robota" msgid "Invalid interface type" msgstr "Neplatný typ rozhraní" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4841,46 +4853,46 @@ msgstr "{var_name} není allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} je chybná)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} není adresa (URL)" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' nemůže být prázdné." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' je neplatná možnost pro pole '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} není řetězec nebo seznam celých čísel" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} není řetězec nebo celé číslo" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} nemá délku" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} chybí" @@ -4928,7 +4940,7 @@ msgstr "" #: zerver/models.py:758 msgid "GIPHY integration disabled" -msgstr "" +msgstr "Doplněk GIPHY zakázán" #: zerver/models.py:763 msgid "Allow GIFs rated Y (Very young audience)" @@ -5011,7 +5023,7 @@ msgstr "Nikdo" #: zerver/models.py:1961 msgid "Unknown user" -msgstr "" +msgstr "Neznámý uživatel" #: zerver/models.py:2117 msgid "Organization owner" @@ -5069,57 +5081,57 @@ msgstr "Zprávy mohou přidávat pouze správci a moderátoři organizace." msgid "Only organization full members can post" msgstr "Pouze členové organizace mohou přidávat zprávy." -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode obrázeček" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Vlastní obrázeček" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Další obrázeček Zulipu" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Seznam možností" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Výběr osoby" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Krátký text" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Dlouhý text" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Výběr data" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Odkaz" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Vnější účet" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Zájmena" @@ -5135,20 +5147,20 @@ msgstr "neznámý operační systém" msgid "An unknown browser" msgstr "Neznámý prohlížeč" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Chybí argument 'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Chybí argument 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Událost novější než {event_id} již byla ořezána!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Událost {event_id} nebyla v této řadě" @@ -5320,19 +5332,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Chybí odesílatel" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Zrcadlení není povoleno společně s uživatelskými ID příjemců" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Neplatná zrcadlená zpráva" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Zrcadlení Zephyru není v této organizaci povoleno." @@ -5398,26 +5410,26 @@ msgstr "Alespoň jedno z následujících musí být uvedeno: emoji_name, emoji_ msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Alespoň jedna metoda autentizace musí být povolena." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Musí to být ukázková organizace." @@ -5895,11 +5907,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Neplatná subdoména pro \"bouncer push\" oznámení" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Musí se prokázat platným Zulip server API klíčem" @@ -5913,43 +5925,43 @@ msgstr "Neplatné UUID" msgid "Invalid token type" msgstr "Neplatný typ elektronického klíče" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "Chybí uživatelské_id nebo user_uuid" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Data nemají správné pořadí." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/cs/translations.json b/locale/cs/translations.json index 01d9486612a9c..7d4ca739b8712 100644 --- a/locale/cs/translations.json +++ b/locale/cs/translations.json @@ -5,7 +5,7 @@ "(hidden)": "(skryto)", "(no description)": "(žádný popis)", "(no topic)": "(žádné téma)", - "(or )": "", + "(or )": "(nebo )", "(you)": "(vy)", "({message_retention_days} days)": "({message_retention_days} dní)", "/dark (Switch to the dark theme)": "/dark (přepnout do tmavého vzhledu)", @@ -32,24 +32,24 @@ "

    You are searching for messages that are sent by more than one person, which is not possible.

    ": "

    Hledáte zprávy odeslané více než jednou osobou, což není možné.

    ", "

    You are searching for messages that belong to more than one stream, which is not possible.

    ": "

    Hledáte zprávy patřící k více než jednomu kanálu, což není možné.

    ", "

    You are searching for messages that belong to more than one topic, which is not possible.

    ": "

    Hledáte zprávy patřící k více než jednomu tématu, což není možné.

    ", - "{name} (guest) is not subscribed to this stream. They will not be notified if you mention them.": "", - "{name} (guest) is not subscribed to this stream. They will not be notified unless you subscribe them.": "", + "{name} (guest) is not subscribed to this stream. They will not be notified if you mention them.": "{name} (host) neodebírá tento kanál. Pokud je zmíníte, nebudou upozorněni.", + "{name} (guest) is not subscribed to this stream. They will not be notified unless you subscribe them.": "{name} (host) neodebírá tento kanál. Nebudou upozorněni, pokud odběr nenastavíte.", "{name} is not subscribed to this stream. They will not be notified if you mention them.": "{name} neodebírá tento kanál. Pokud jej zmíníte, nebude upozorněn.", "{name} is not subscribed to this stream. They will not be notified unless you subscribe them.": "{name} neodebírá tento kanál. Nebude upozorněn pokud odběr nenastavíte.", "{username} has {number_of_invites_by_user} unexpired invitations.": "{username} má {number_of_invites_by_user} nevypršených pozvánek.", "Subscribe": "Odebírat", - "Click here to learn about exporting private streams and direct messages.": "", + "Click here to learn about exporting private streams and direct messages.": "Klepněte sem a naučte se, jak ukládat soukromé kanály a přímé zprávy.", "Upgrade for more space.": "Povýšit kvůli více místa.", " to add a new line": " pro přidání nového řádku", " to send": " pro odeslání", " will have the same properties as it did prior to deactivation, including role, owner and stream subscriptions.": " bude mít stejné vlastnosti jako před vypnutím, včetně role, vlastníka a odběru kanálů.", " will have the same role, stream subscriptions, user group memberships, and other settings and permissions as they did prior to deactivation.": " bude mít stejnou roli, odebírání kanálů, členství ve skupinách uživatelů a další nastavení a oprávnění jako před vypnutím.", - "@{name} (guest)": "", + "@{name} (guest)": "@{name} (host)", "A Topic Move already in progress.": "Přesun tématu již probíhá", "A deactivated bot cannot send messages, access data, or take any other action.": "Vypnutý robot nemůže odesílat zprávy, přistupovat k datům ani provádět žádné jiné činnosti.", "A deactivated emoji will remain visible in existing messages and emoji reactions, but cannot be used on new messages.": "Vypnutý obrázeček zůstane viditelný ve stávajících zprávách a obrázečkových odpovědích, ale nelze jej použít v nových zprávách.", "A language is marked as 100% translated only if every string in the web, desktop, and mobile apps is translated, including administrative UI and error messages.": "Jazyk je označen jako 100% přeložený, pouze pokud je přeložen každý řetězec ve webové, stolní a mobilní aplikaci, včetně uživatelského rozhraní pro správu a včetně chybových zpráv.", - "A poll must be an entire message.": "", + "A poll must be an entire message.": "Hlasování musí být celá zpráva.", "A stream needs to have a name": "Kanál musí mít název", "A stream with this name already exists": "Kanál s tímto názvem již existuje", "A user group needs to have a name": "Uživatelská skupina musí mít název", @@ -87,18 +87,18 @@ "Add all users": "Přidat všechny uživatelé", "Add another user...": "Přidat dalšího uživatele...", "Add code playground": "Přidat hřiště s kódem", - "Add default streams": "", - "Add email": "", + "Add default streams": "Přidat výchozí kanály", + "Add email": "Přidat e-mail", "Add emoji": "Přidat obrázeček", "Add emoji reaction": "Přidat odpověď pomocí obrázečku", "Add extra emoji for members of the {realm_name} organization.": "Přidat další obrázeček pro členy organizace {realm_name}.", "Add global time": "Přidat celosvětový čas", "Add linkifier": "Přidat generátor odkazů", "Add members": "Přidat členy", - "Add members. Use usergroup or #streamname to bulk add members.": "", + "Add members. Use usergroup or #streamname to bulk add members.": "Přidat členy. K hromadnému přidávání odběratelů použijte uživatelskou skupinu nebo #streamname.", "Add one or more users": "Přidat jednoho nebo více uživatelů", "Add option": "Přidat volbu", - "Add poll": "", + "Add poll": "Přidat hlasování", "Add question": "Přidat otázku", "Add stream": "Přidat kanál", "Add streams": "Přidat kanály", @@ -106,8 +106,8 @@ "Add subscribers. Use usergroup or #streamname to bulk add subscribers.": "Přidat odběratele. K hromadnému přidávání odběratelů použijte uživatelskou skupinu nebo #streamname.", "Add task": "Přidat úkol", "Add video call": "Přidat obrazový hovor", - "Add voice call": "", - "Add your email to invite other users or convert to a permanent Zulip organization.": "", + "Add voice call": "Přidat hlasový hovor", + "Add your email to invite other users or convert to a permanent Zulip organization.": "Přidejte svůj e-mail pro pozvání dalších uživatelů nebo přejití na stálou organizaci Zulip.", "Added successfully!": "Úspěšně přidáno!", "Added successfully.": "Úspěšně přidáno.", "Administrator": "Správce", @@ -130,15 +130,15 @@ "Alert words allow you to be notified as if you were @-mentioned when certain words or phrases are used in Zulip. Alert words are not case sensitive.": "Sledovaná slova umožňují, abyste byl uvědoměn, kdykoliv někdo použije určitá slova nebo fráze, podobně jako kdybyste byl zmíněn pomocí @. U sledovaných slov se nerozlišuje velikost písmen.", "Alerted messages": "Zprávy s upozorněním", "All direct messages": "Všechny přímé zprávy", - "All groups": "", + "All groups": "Všechny skupiny", "All messages": "Všechny zprávy", "All messages including muted streams": "Všechny zprávy včetně ztlumených kanálů", "All streams": "Všechny kanály", "All time": "Za celou dobu", - "All topics": "", - "All unmuted topics": "", - "All unread messages": "", - "All users will need to log in again at your new organization URL.": "", + "All topics": "Všechna témata", + "All unmuted topics": "Všechna neztlumená témata", + "All unread messages": "Všechny nepřečtené zprávy", + "All users will need to log in again at your new organization URL.": "Všichni uživatelé se budou muset znovu přihlásit na nové adrese URL organizace.", "Allow creating web-public streams (visible to anyone on the Internet)": "Povolit vytváření internetových veřejných kanálů (viditelných pro kohokoli na internetu)", "Allow message content in message notification emails": "Povolit obsah zpráv v e-mailech s oznámeními o zprávách", "Allow message editing": "Povolit úpravy zpráv", @@ -146,11 +146,11 @@ "Allow subdomains": "Povolit subdomény", "Allowed domains": "Povolené domény", "Allowed domains: {domains}": "Povolené domény: {domains}", - "Already members:": "", + "Already members:": "Již členy:", "Already not subscribed.": "Odběr již vypnut.", "Already subscribed to {stream}": "Již odebírá {stream}", "Already subscribed users:": "Již k odběru přihlášení uživatelé:", - "Already subscribed.": "", + "Already subscribed.": "Již přihlášeno k odběru.", "Always": "Vždy", "An API key can be used to programmatically access a Zulip account. Anyone with access to your API key has the ability to read your messages, send messages on your behalf, and otherwise impersonate you on Zulip, so you should guard your API key as carefully as you guard your password.
    We recommend creating bots and using the bots' accounts and API keys to access the Zulip API, unless the task requires access to your account.": "API klíč lze použít pro progamový přístup k Zulip účtu. Kdokoliv, kdo má přístup k vašemu API klíči, může číst vaše zprávy, odesílat zprávy za vás a v zásadě vystupovat na Zulipu vaším jménem. To znamená, že byste měli svůj API klíč držet v bezpečí stejně jako vaše heslo.
    Doporučujeme využít roboty a robotické účty (resp. jejich API klíče) pro přístup k API Zulipu pokud úkol vyloženě nevyžaduje přístup k vašemu účtu.", "An hour ago": "Před hodinou", @@ -158,17 +158,17 @@ "Announce new stream in": "Oznámit nový kanál v", "Any organization administrator can conduct an export.": "Všichni správci organizace mohou provést uložení dat.", "Any time": "Kdykoli", - "Anyone can add more options after the poll is posted.": "", + "Anyone can add more options after the poll is posted.": "Po zveřejnění hlasování může kdokoli přidat další možnosti.", "April": "Duben", "Archive ?": "Archivovat ?", "Archive stream": "Archivovat kanál", "Archiving stream will immediately unsubscribe everyone. This action cannot be undone.": "Archivací kanálu bude všem okamžitě zrušen odběr. Tento krok nepůjde vrátit zpět.", - "Archiving this stream will also disable settings that were configured to use this stream:": "", + "Archiving this stream will also disable settings that were configured to use this stream:": "Archivací tohoto kanálu se také vypnou nastavení, která byla nastavena pro použití tohoto kanálu:", "Are you sure you want to continue?": "Určitě chcete pokračovat?", "Are you sure you want to create stream ''''{stream_name}'''' and subscribe {count} users to it?": "Jste si jistý, že chcete vytvořit kanál '''{stream_name}'''' a přihlásit k jeho odběru {count} uživatelů?", "Are you sure you want to deactivate this organization?": "Opravdu chcete vypnout tuto organizaci?", "Are you sure you want to deactivate your account?": "Opravdu chcete vypnout svůj účet?", - "Are you sure you want to delete all drafts? This action cannot be undone.": "Určitě chcete smazat všechny koncepty? Tento krok nelze vrátit zpět.", + "Are you sure you want to delete all drafts? This action cannot be undone.": "Určitě chcete smazat všechny návrhy? Tento krok nelze vrátit zpět.", "Are you sure you want to delete your profile picture?": "Opravdu chcete smazat obrázek svého profilu?", "Are you sure you want to mark all messages as read? This action cannot be undone.": "Určitě chcete označit všechny zprávy jako přečtené? Tento krok nelze vzít zpět.", "Are you sure you want to mute {user_name}? Messages sent by muted users will never trigger notifications, will be marked as read, and will be hidden.": "Opravdu chcete ztlumit {user_name}? Zprávy odeslané ztlumenými uživateli nikdy nespustí oznámení, budou označeny jako přečtené a budou skryty.", @@ -176,8 +176,8 @@ "Are you sure you want to resend the invitation to ?": "Opravdu chcete znovu poslat pozvánku ?", "Are you sure you want to revoke the invitation to {email}?": "Opravdu chcete zrušit pozvánku {email}?", "Are you sure you want to revoke this invitation link created by {referred_by}?": "Opravdu chcete zrušit tento zvací odkaz vytvořený {referred_by}?", - "Are you sure you want to revoke this invitation link?": "", - "Are you sure you want to send @-mention notifications to the {subscriber_count} users subscribed to #{stream_name}? If not, please edit your message to remove the @{stream_wildcard_mention} mention.": "", + "Are you sure you want to revoke this invitation link?": "Opravdu chcete zrušit tento odkaz na pozvánku?", + "Are you sure you want to send @-mention notifications to the {subscriber_count} users subscribed to #{stream_name}? If not, please edit your message to remove the @{stream_wildcard_mention} mention.": "Jste si jisti, že chcete zasílat oznámení o @-zmínkách {subscriber_count} uživatelům odebírajícím #{stream_name}? Pokud ne, upravte prosím svou zprávu tak, abyste odstranili zmínku @{stream_wildcard_mention}.", "Are you sure you want to unstar all messages in ? This action cannot be undone.": "Chcete odhvězdičkovat všechny ohvězdičkované zprávy v ? Tento krok nelze vrátit zpět.", "Are you sure you want to unstar all starred messages? This action cannot be undone.": "Chcete odhvězdičkovat všechny ohvězdičkované zprávy? Tento krok nelze vrátit zpět.", "Ask me later": "Zeptat se později", @@ -190,18 +190,18 @@ "Automated messages and emails": "Automatické zprávy a e-maily", "Automatic": "Automaticky", "Automatic (follows system settings)": "Automaticky (podle nastavení systému)", - "Automatically follow topics": "", - "Automatically follow topics where I'm mentioned": "", + "Automatically follow topics": "Automaticky sledovat témata", + "Automatically follow topics where I'm mentioned": "Automaticky sledovat témata, kde jsem zmíněn", "Automatically mark messages as read": "Automatické označování zpráv jako přečtených", - "Automatically unmute topics in muted streams": "", + "Automatically unmute topics in muted streams": "Automaticky zrušit ztlumení témat ve ztlumených kanálech", "Available on Zulip Cloud Standard. Upgrade or request sponsorship to access.": "Dostupné na Zulip Cloud Standard. Pro přístup povýšit nebo požádat o sponzorství.", "Avatar changes are disabled in this organization": "Změny avatarů jsou v této organizaci zakázány", "Avatar from Gravatar": "Avatar z Gravataru", "Back to streams": "Zpět na kanály", - "Because the original owner of this bot is deactivated, you will become the owner for this bot.": "", - "Because you are removing the last subscriber from a private stream, it will be automatically archived.": "", + "Because the original owner of this bot is deactivated, you will become the owner for this bot.": "Protože původní majitel tohoto robota je vypnut, stanete se vlastníkem tohoto robota.", + "Because you are removing the last subscriber from a private stream, it will be automatically archived.": "Protože ze soukromého kanálu odebíráte posledního účastníka, bude automaticky. archivován.", "Because you are the only organization owner, you cannot deactivate your account.": "Protože jste jediným vlastníkem organizace, nemůžete svůj účet vypnout.", - "Because you are the only subscriber, this stream will be automatically archived.": "", + "Because you are the only subscriber, this stream will be automatically archived.": "Protože jste jediný odběratel, bude tento kanál automaticky archivován.", "Billing": "Fakturace", "Bold": "Tučné", "Bot": "Robot", @@ -210,24 +210,24 @@ "Bot owner": "Vlastník robota", "Bot type": "Druh robota", "Bots": "Roboti", - "Browse 1 more stream": "", + "Browse 1 more stream": "Procházet 1 další kanál", "Browse recent conversations": "Procházet nedávné konverzace", "Browse streams": "Procházet kanály", - "Browse {can_subscribe_stream_count} more streams": "", - "Bulleted list": "", + "Browse {can_subscribe_stream_count} more streams": "Procházet {can_subscribe_stream_count} další kanály", + "Bulleted list": "Odrážkový seznam", "Business": "Podnikání", "Busy": "Zaneprázdněn", "By deactivating your account, you will be logged out immediately.": "Deaktivací vašeho účtu budete okamžitě odhlášen/a.", - "Call provider": "", + "Call provider": "Poskytovatel hovoru", "Cancel": "Zrušit", "Cancel compose": "Zahodit koncept", "Cancel compose and save draft": "Zrušit sestavení a uložit návrh", - "Cannot join group {name}": "", - "Cannot leave group {name}": "", - "Cannot send message while files are being uploaded.": "", - "Cannot subscribe to ": "", - "Cannot subscribe to private stream ": "", - "Cannot view stream": "", + "Cannot join group {name}": "Nelze se připojit ke skupině {name}", + "Cannot leave group {name}": "Nelze opustit skupinu {name}", + "Cannot send message while files are being uploaded.": "Během nahrávání souborů nelze odeslat zprávu.", + "Cannot subscribe to ": "Nelze odebírat ", + "Cannot subscribe to private stream ": "Nelze odebírat soukromý kanál ", + "Cannot view stream": "Nelze zobrazit kanál", "Card": "Karta", "Center the view around message ID .": "Vystředit zobrazení okolo zprávy s ID .", "Change": "Změnit", @@ -236,7 +236,7 @@ "Change email": "Změnit e-mail", "Change group info": "Změnit informace o skupině", "Change password": "Změnit heslo", - "Change setting": "", + "Change setting": "Změnit nastavení", "Check all": "Označit vše", "Check your email ({email}) to confirm the new address.": "Zkontrolujte svoji e-mailovou schránku ({email}) pro potvrzení nové adresy.", "Choose avatar": "Vybrat avatara", @@ -245,12 +245,12 @@ "Clear avatar": "Smazat avatar", "Clear image": "Vymazat obrázek", "Clear profile picture": "Vymazat obrázek profilu", - "Clear status": "", - "Clear topic": "", - "Clear your status": "", + "Clear status": "Smazat stav", + "Clear topic": "Smazat téma", + "Clear your status": "Smazat váš stav", "Click here to reveal.": "Klepněte zde pro odhalení.", "Click on the pencil () icon to edit and reschedule a message.": "Klepněte na ikonu tužky () pro upravení a přeplánování zprávy.", - "Click to change notifications for this topic.": "", + "Click to change notifications for this topic.": "Klepněte pro změnu oznámení pro toto téma.", "Click to view or download.": "Klepněte pro zobrazení nebo stáhnutí.", "Close": "Zavřít", "Close modal": "Zavřít okno", @@ -261,18 +261,18 @@ "Collapse compose": "Sbalit koncept", "Collapse direct messages": "Sbalit přímé zprávy", "Collapse message": "Sbalit zprávu", - "Collapse views": "", + "Collapse views": "Sbalit pohledy", "Collapse/show selected message": "Sbalit/Rozbalit vybranou zprávu", "Community": "Společenství", "Commuting": "Dojíždění", "Compact": "Kompaktní", "Complete": "Dokončeno", - "Complete your organization profile, which is displayed on your organization's registration and login pages.": "", + "Complete your organization profile, which is displayed on your organization's registration and login pages.": "Dokončete svůj profil organizace, který se zobrazuje na registračních a přihlašovacích stránkách vaší organizace.", "Compose message": "Vytvořit zprávu", "Compose your message here": "Svoji zprávu sepište zde", "Compose your message here...": "Svoji zprávu sepište zde...", "Composing messages": "Vytváření zpráv", - "Configure how Zulip notifies you about new messages. In muted streams, stream notification settings apply only to unmuted topics.": "", + "Configure how Zulip notifies you about new messages. In muted streams, stream notification settings apply only to unmuted topics.": "Nastavte způsob, jakým vás Zulip upozorňuje na nové zprávy. U ztlumených kanálů se nastavení upozornění na kanál vztahuje pouze na neztlumená témata.", "Configure regular expression patterns that will be used to automatically transform any matching text in Zulip messages and topics into links.": "Nastavte vzory regulárních výrazů, které se použijí k automatické přeměně odpovídajícího textu ve zprávách a tématech Zulip na odkazy.", "Configure the default personal preference settings for new users joining your organization.": "Nastavte výchozí nastavení osobních nastavení pro nové uživatele, kteří se připojí k vaší organizaci.", "Configure the authentication methods for your organization.": "Nastavit metody autentizace pro vaší organizaci.", @@ -282,11 +282,11 @@ "Contact a moderator to resolve this topic.": "Pro vyřešení tohoto tématu se spojte s moderátorem.", "Contact a moderator to unresolve this topic.": "Pro zrušení tohoto tématu se spojte s moderátorem.", "Contact support": "Spojit se s podporou", - "Convert": "", + "Convert": "Převést", "Convert emoticons before sending (:) becomes 😃)": "Převést emotikony před odesláním (z :) bude 😃)", "Cookie Bot": "Cookie robot", "Copied!": "Zkopírováno!", - "Copy URL": "", + "Copy URL": "Kopírovat adresu", "Copy address": "Kopírovat adresu", "Copy and close": "Kopírovat a zavřít", "Copy code": "Kopírovat kód", @@ -299,9 +299,9 @@ "Could not resolve topic": "Nepodařilo se vyřešit téma", "Could not unresolve topic": "Nepodařilo se zrušit řešení tématu", "Create": "Vytvořit", - "Create a poll": "", + "Create a poll": "Vytvořit hlasování", "Create a stream": "Vytvořit kanál", - "Create a user group": "", + "Create a user group": "Vytvořit uživatelskou skupinu", "Create new stream": "Vytvořit nový kanál", "Create new user group": "Vytvořit novou uživatelskou skupinu", "Create stream": "Vytvořit kanál", @@ -312,7 +312,7 @@ "Currently viewing all messages.": "V současné době se zobrazují všechny zprávy.", "Currently viewing the entire stream.": "V současné době se zobrazuje celý kanál.", "Custom": "Vlastní", - "Custom URL": "", + "Custom URL": "Vlastní adresa", "Custom emoji": "Vlastní obrázeček", "Custom language: {query}": "Vlastní jazyk: {query}", "Custom linkifier added!": "Přidán vlastní odkazovač!", @@ -322,9 +322,9 @@ "Cycle between stream narrows": "Procházet mezi jednotlivými zúženími kanálů", "DIRECT MESSAGES": "PŘÍMÉ ZPRÁVY", "DM": "PZ", - "DMs and mentions": "", + "DMs and mentions": "Přímé zprávy a zmínky", "DMs, mentions, and alerts": "Přímé zprávy, zmínky a sledovaná slova", - "DMs, mentions, and followed topics": "", + "DMs, mentions, and followed topics": "Přímé zprávy, zmínky a sledovaná témata", "Dark": "Tmavé", "Dark theme": "Tmavý vzhled", "Dark theme logo": "Logo tmavého vzhledu", @@ -343,35 +343,35 @@ "Deactivate {name}?": "Vypnout {name}?", "Deactivated users": "Vypnutí uživatelé", "December": "Prosinec", - "Default": "", + "Default": "Výchozí", "Default for stream": "Výchozí pro kanál", "Default is {language}. Use 'text' to disable highlighting.": "Výchozí je {language}. Použijte 'text' pro zakázání zvýrazňování.", "Default language for code blocks": "Výchozí jazyk pro bloky kódu", "Default streams": "Výchozí kanály", - "Default streams for new users cannot be made private.": "", + "Default streams for new users cannot be made private.": "Výchozí kanály pro nové uživatele nelze nastavit jako soukromé.", "Default streams for this organization": "Výchozí kanály pro tuto organizaci", "Default user settings": "Výchozí uživatelská nastavení", "Delay before sending message notification emails": "Zpoždění před odesláním e-mailů s oznámeními o zprávách", "Delay period (minutes)": "Doba zpoždění (minutes)", "Delete": "Smazat", "Delete alert word": "Smazat sledované slovo", - "Delete all drafts": "Smazat všechny koncepty", - "Delete all selected drafts": "", + "Delete all drafts": "Smazat všechny návrhy", + "Delete all selected drafts": "Smazat všechny vybrané návrhy", "Delete code playground?": "Smazat hřiště s kódem?", "Delete custom profile field?": "Smazat vlastní pole profilu?", "Delete data export?": "Smazat uložení dat?", - "Delete draft": "Smazat koncept", + "Delete draft": "Smazat návrh", "Delete file": "Smazat soubor", "Delete file?": "Smazat soubor?", - "Delete group": "", + "Delete group": "Smazat skupinu", "Delete icon": "Smazat ikonu", "Delete linkifier?": "Smazat generátor odkazů?", "Delete logo": "Smazat logo", "Delete message": "Smazat zprávu", "Delete message?": "Smazat zprávu?", "Delete profile picture": "Smazat obrázek profilu", - "Delete scheduled message": "", - "Delete selected draft": "Smazat vybraný koncept", + "Delete scheduled message": "Smazat zařazenou zprávu", + "Delete selected draft": "Smazat vybraný návrh", "Delete topic": "Smazat téma", "Delete {user_group_name}?": "Smazat {user_group_name}?", "Deleted": "Smazáno", @@ -385,31 +385,31 @@ "Depending on the size of your organization, an export can take anywhere from seconds to an hour.": "V závislosti na velikosti organizace může uložení dat trvat několik sekund až hodinu.", "Deprecation notice": "Oznámení o zastarání", "Description": "Popis", - "Deselect draft": "", + "Deselect draft": "Zrušit výběr návrhu", "Desktop": "Pracovní plocha", "Desktop & mobile apps": "Aplikace pro počítače a telefony", "Desktop message notifications": "Oznámení o zprávách na ploše", "Detailed keyboard shortcuts documentation": "Podrobný popis klávesových zkratek", "Detailed message formatting documentation": "Podrobná dokumentace k formátování zpráv", - "Detailed search filters documentation": "", - "Direct message": "", - "Direct message to me": "", + "Detailed search filters documentation": "Podrobná dokumentace k vyhledávacím filtrům", + "Direct message": "Přímá zpráva", + "Direct message to me": "Přímá zpráva pro mě", "Direct messages": "Přímé zprávy", - "Direct messages are disabled in this organization.": "", - "Direct messages disabled": "", - "Disable": "", - "Disable notifications?": "", + "Direct messages are disabled in this organization.": "Přímé zprávy jsou v této organizaci zakázány.", + "Direct messages disabled": "Přímé zprávy zakázány", + "Disable": "Zakázat", + "Disable notifications?": "Vypnout oznámení?", "Disabled": "Zakázáno", "Discard": "Zahodit", "Dismiss for a week": "Pustit z hlavy na týden", "Display availability to other users": "Zobrazit dostupnost ostatním uživatelům.", "Display my availability to other users": "Zobrazit moji dostupnost ostatním uživatelům", "Display names of reacting users when few users have reacted to a message": "Zobrazit jména odpovídajících uživatelů, pokud na zprávu odpovědělo několik uživatelů", - "Display on user card": "", - "Display “(guest)” after names of guest users": "", - "Do you still want to move the latest {total_messages_allowed_to_move, plural, one {message} other {# messages}}?": "", + "Display on user card": "Zobrazit na kartě uživatele", + "Display “(guest)” after names of guest users": "Zobrazit \"(host)\" za jmény hostujících uživatelů", + "Do you still want to move the latest {total_messages_allowed_to_move, plural, one {message} other {# messages}}?": "Chcete ještě přesunout poslední {total_messages_allowed_to_move, množné číslo, jednu {message} další {# messages}}?", "Do you want to add everyone?": "Chcete přidat všechny?", - "Do you want to mark them all as read?": "", + "Do you want to mark them all as read?": "Chcete je všechny označit jako přečtené?", "Domain": "Doména", "Don’t allow disposable email addresses": "Zakázat jednorázové e-mailové adresy", "Download": "Stáhnout", @@ -421,25 +421,25 @@ "Download zuliprc": "Stáhnout zuliprc", "Download {filename}": "Stáhnout {filename}", "Drafts": "Koncepty", - "Drafts are not synced to other devices and browsers.": "Koncepty nejsou synchronizovány s ostatními zařízeními a prohlížeči.", + "Drafts are not synced to other devices and browsers.": "Návrhy nejsou synchronizovány s ostatními zařízeními a prohlížeči.", "Drafts from conversation with {recipient}": "Návrhy z rozhovoru s {recipient}", "Drafts from {recipient}": "Návrhy od {recipient}", - "Drafts older than {draft_lifetime} days are automatically removed.": "Koncepty starší než {draft_lifetime} dnů budou automaticky odstraněny.", + "Drafts older than {draft_lifetime} days are automatically removed.": "Návrhy starší než {draft_lifetime} dnů budou automaticky odstraněny.", "Duration deletion is allowed after posting (minutes)": "Smazání doby trvání je povoleno po odeslání (minuty)", "Duration editing is allowed after posting (minutes)": "Upravení doby trvání je povoleno po odeslání (minuty)", "EDITED": "UPRAVENO", "Edit": "Upravit", "Edit #{stream_name}": "Upravit #{stream_name}", - "Edit and reschedule message": "", + "Edit and reschedule message": "Upravit a přeřadit zprávu", "Edit bot": "Upravit robota", "Edit custom profile field": "Upravit vlastní pole profilu", "Edit linkfiers": "Upravit generátory odkazů", "Edit message": "Upravit zprávu", - "Edit profile": "", - "Edit selected draft": "Upravit vybraný koncept", - "Edit selected message or view source": "", + "Edit profile": "Upravit profil", + "Edit selected draft": "Upravit vybraný návrh", + "Edit selected message or view source": "Upravit vybranou zprávu nebo zobrazit zdroj", "Edit status": "Upravit stav", - "Edit stream name and description": "", + "Edit stream name and description": "Upravit název a popis kanálu", "Edit topic": "Upravit téma", "Edit user": "Upravit uživatele", "Edit your last message": "Upravit poslední zprávu", @@ -453,7 +453,7 @@ "Email address": "E-mailová adresa", "Email address changes are disabled in this organization.": "Změny e-mailu jsou v této organizaci zakázány.", "Email copied": "E-mail zkopírován", - "Email footers (e.g., signature)": "", + "Email footers (e.g., signature)": "Zápatí elektronického dopisu (např. podpis)", "Email message notifications": "E-mailová oznámení o zprávách", "Email notifications": "Oznámení elektronickou poštou", "Emails (one on each line or comma-separated)": "E-maily (jeden na každý řádek nebo oddělené čárkou)", @@ -470,7 +470,7 @@ "Enter sends when composing a message": "Klávesa Enter odešle rozepsanou zprávu", "Error": "Chyba", "Error adding subscription": "Chyba při přidávání odběru", - "Error checking subscription.": "", + "Error checking subscription.": "Chyba při kontrole odběru.", "Error creating stream": "Chyba při vytváření kanálu", "Error creating user group.": "Chyba při vytváření uživatelské skupiny.", "Error deleting message": "Chyba při mazání zprávy", @@ -487,21 +487,21 @@ "Error saving edit": "Chyba při ukládání úpravy", "Error: Cannot deactivate the only organization owner.": "Chyba: Nelze vypnout jediného vlastníka organizace.", "Error: Cannot deactivate the only user. You can deactivate the whole organization though in your organization profile settings.": "Chyba: Nelze vypnout jediného uživatele. Celou organizaci můžete vypnout ve svém nastavení profilu organizace.", - "Escape key navigates to home view": "", + "Escape key navigates to home view": "Po stisknutí klávesy Esc se přejde do domácího zobrazení", "Estimated messages per week": "Odhadovaný počet zpráv za týden", "Event or conference": "Událost nebo konference", "Everyone": "Každý", - "Everyone on the internet": "", - "Everyone sees global times in their own time zone.": "", + "Everyone on the internet": "Všichni na internetu", + "Everyone sees global times in their own time zone.": "Každý vidí světový čas ve svém časovém pásmu.", "Everyone sees this in their own time zone.": "Každý to vidí ve svém časovém pásmu.", "Exclude messages with topic .": "Vyloučit zprávy s tématem .", "Exit search": "Ukončit hledání", "Expand compose": "Rozbalit koncept", - "Expand direct messages": "", - "Expand message": "", - "Expand views": "", + "Expand direct messages": "Rozbalit přímé zprávy", + "Expand message": "Rozbalit zprávu", + "Expand views": "Rozbalit pohledy", "Expires at": "Vyprší v", - "Expires on {date} at {time}": "", + "Expires on {date} at {time}": "Platnost končí {date} v {time}.", "Export failed": "Uložení dat v jiném formátu selhalo", "Export organization": "Vyvést organizaci", "Export started. Check back in a few minutes.": "Ukládání dat do jiného formátu bylo zahájeno. Zkontrolujte průběh za pár minut.", @@ -509,14 +509,14 @@ "External account type": "Vnější typ účtu", "External link": "Externí odkaz", "Failed": "Nepodařilo se", - "Failed adding one or more streams.": "", + "Failed adding one or more streams.": "Nepodařilo se přidat jeden nebo více kanálů.", "Failed to create video call.": "Nepodařilo se vytvořit obrazový hovor.", "Failed to generate preview": "Nepodařilo se vytvořit náhled", "Failed to upload %'{file}'": "Nepodařilo se nahrát %'{file}'", "Failed!": "Nepodařilo se!", "Failed: A custom emoji with this name already exists.": "Selhalo: Vlastní obrázeček s tímto názvem již existuje.", "Failed: Emoji name is required.": "Selhalo: je požadován název obrázečku.", - "Failed: There is a default emoji with this name. Only administrators can override default emoji.": "", + "Failed: There is a default emoji with this name. Only administrators can override default emoji.": "Selhalo: Tento název má výchozí obrázeček. Výchozí obrázeček mohou přepsat pouze správci.", "February": "Únor", "Field choices": "Možnosti", "File": "Soubor", @@ -547,84 +547,84 @@ "First message": "První zpráva", "First time? Read our guidelines for creating and naming streams.": "Poprvé? Přečtěte si naše pokyny, jak tvořit a pojmenovávat kanály.", "First time? Read our guidelines for creating user groups.": "Poprvé? Přečtěte si naše pokyny, jak tvořit uživatelské skupiny.", - "Follow": "", - "Followed": "", - "Followed topics": "", - "For example, to configure a code playground for code blocks tagged as Python, you can set:": "", - "For more examples and technical details, see the help center documentation on adding code playgrounds.": "", - "For more examples, see the help center documentation on adding linkifiers.": "", - "Forgot it?": "", + "Follow": "Sledovat", + "Followed": "Sledováno", + "Followed topics": "Sledovaná témata", + "For example, to configure a code playground for code blocks tagged as Python, you can set:": "Chcete-li například nastavit hřiště kódu pro bloky kódu označené jako Python, můžete nastavit:", + "For more examples and technical details, see the help center documentation on adding code playgrounds.": "Další příklady a technické podrobnosti naleznete v dokumentaci centra nápovědy o přidávání kódových hřišť.", + "For more examples, see the help center documentation on adding linkifiers.": "Další příklady naleznete v dokumentaci centra nápovědy o přidávání odkazovačů.", + "Forgot it?": "Zapomněl jste?", "Forked from upstream at {zulip_merge_base}": "Odvozeno z hlavní vývojové větve ma {zulip_merge_base}", "Friday": "Pátek", "Full name": "Celé jméno", "GIPHY attribution": "Přisuzování GIPHY", "GIPHY integration": "Integrace GIPHY", "General": "Obecné", - "Generate URL for an integration": "", - "Generate email address": "", + "Generate URL for an integration": "Vygenerovat adresu URL pro doplněk", + "Generate email address": "Vygenerovat e-mailovou adresu", "Generate invite link": "Vytvořit zvací odkaz", "Generate new API key": "Vytvořit nový API klíč", - "Generate stream email address": "", + "Generate stream email address": "Vygenerovat e-mailovou adresu kanálu", "Generating link...": "Vytvoří se odkaz...", "Generic": "Obecný", "Get API key": "Získat API klíč", "Go back through viewing history": "Vrátit se zpět do historie prohlížení", "Go forward through viewing history": "Pokračovat vpřed v historii prohlížení", "Go invisible": "Jít neviditelný", - "Go to #{display_recipient}": "", - "Go to #{display_recipient} > {topic}": "", + "Go to #{display_recipient}": "Jít do #{display_recipient}", + "Go to #{display_recipient} > {topic}": "Jít do #{display_recipient} > {topic}", "Go to conversation": "Jít do rozhovoru", - "Go to direct messages with {display_reply_to_for_tooltip}": "", - "Go to home view": "", - "Go to stream settings": "", + "Go to direct messages with {display_reply_to_for_tooltip}": "Jít do přímých zpráv pomocí {display_reply_to_for_tooltip}", + "Go to home view": "Jít do domácího pohledu", + "Go to stream settings": "Jít do nastavení kanálu", "Got it": "Rozumím", "Got it!": "Chápu!", "Government": "Vláda", "Grant Zulip the Kerberos tickets needed to run your Zephyr mirror via Webathena": "Dejte Zulipu lístky Kerberos, které jsou potřeba ke spuštění zrcadla Zephyr prostřednictvím Webatheny", - "Group permissions": "", - "Group settings": "", + "Group permissions": "Oprávnění pro skupiny", + "Group settings": "Nastavení skupiny", "Guest": "Host", "Guests": "Hosté", "Guests cannot edit custom emoji.": "Hosté nemohou upravovat vlastní obrázečky.", "Header": "Záhlaví", "Help center": "Centrum nápovědy", - "Help menu": "", + "Help menu": "Nabídka nápovědy", "Hide muted message again": "Znovu skrýt ztlumenou zprávu", "Hide password": "Skrýt heslo", "Hide starred message count": "Skrýt počet zpráv s hvězdičkami", "High contrast mode": "Režim vysokého kontrastu", "Hint": "Nápověda", "Hint (up to 80 characters)": "Nápověda (až 80 znaků)", - "Home view": "", - "How would you like to invite users?": "", - "However, it will no longer be subscribed to the private streams that you are not subscribed to.": "", + "Home view": "Domácí pohled", + "How would you like to invite users?": "Jak chcete pozvat uživatele?", + "However, it will no longer be subscribed to the private streams that you are not subscribed to.": "Nebude se však již přihlašovat k soukromým kanálům, ke kterým nejste přihlášeni.", "Humans": "Lidé", "Idle": "Nečinný", - "If you don't know your password, you can reset it.": "", - "If you haven't updated your name, it's a good idea to do so before inviting other users to join you!": "", + "If you don't know your password, you can reset it.": "Pokud neznáte své heslo, můžete je obnovit.", + "If you haven't updated your name, it's a good idea to do so before inviting other users to join you!": "Pokud jste své jméno ještě neaktualizovali, je dobré tak učinit dříve, než pozvete další uživatele, aby se k vám připojili!", "Ignored deactivated users:": "Přehlížet vypnuté uživatele:", "Image": "Obrázek", "In a meeting": "Na schůzce", - "In muted streams, stream notification settings apply only to unmuted topics.": "", + "In muted streams, stream notification settings apply only to unmuted topics.": "U ztlumených kanálů se nastavení upozornění na kanál vztahuje pouze na neztlumená témata.", "Inactive": "Nečinný", "Inactive bots": "Nečinní roboti", - "Inbox": "", - "Include DMs": "", - "Include content of direct messages in desktop notifications": "", + "Inbox": "Došlá pošta", + "Include DMs": "Zahrnout přímé zprávy", + "Include content of direct messages in desktop notifications": "Zahrnout obsah přímých zpráv do oznámení na ploše", "Include message content in message notification emails": "Zahrnout obsah zpráv v e-mailech s oznámeními o zprávách", "Include organization name in subject of message notification emails": "Zahrnout název organizace do předmětu e-mailů s oznámeními o zprávách", - "Includes muted streams and topics": "", + "Includes muted streams and topics": "Zahrnuje ztlumené kanály a témata", "Initiate a search": "Vyhledávání", "Insert new line": "Vložit nový řádek", - "Integration": "", - "Integration URL will appear here.": "", - "Integrations": "Integrace", + "Integration": "Doplněk", + "Integration URL will appear here.": "Zde se zobrazí adresa URL doplňku.", + "Integrations": "Doplněk", "Interface": "Rozhraní", "Invalid URL": "Neplatná adresa (URL)", "Invalid stream ID": "Neplatné ID kanálu", "Invalid time format: {timestamp}": "Neplatný formát času: {timestamp}", - "Invalid user": "", - "Invalid users": "", + "Invalid user": "Neplatný uživatel", + "Invalid users": "Neplatní uživatelé", "Invitation expires after": "Pozvánka vyprší za", "Invitations": "Pozvánky", "Invitations are required for joining this organization": "Pro připojení do této organizace je nutná pozvánka", @@ -641,11 +641,11 @@ "Inviting...": "Odesílání pozvánek...", "Italic": "Kurzíva", "January": "Leden", - "Jitsi server URL": "", - "Join group": "", - "Join group {name}": "", - "Join video call.": "", - "Join voice call.": "", + "Jitsi server URL": "Adresa URL serveru Jitsi", + "Join group": "Připojit se ke skupině", + "Join group {name}": "Připojit se ke skupině {name}", + "Join video call.": "Připojit se k obrazovému hovoru.", + "Join voice call.": "Připojit se ke hlasovému hovoru.", "Join {realm_name}": "Připojit se k {realm_name}", "Joined": "Přidal se", "Joined {date_joined}": "Připojen {date_joined}", @@ -654,7 +654,7 @@ "June": "Červen", "Just now": "Právě teď", "Keyboard shortcuts": "Klávesové zkratky", - "LaTeX": "", + "LaTeX": "LaTeX", "Label": "Štítek", "Language": "Jazyk", "Language for automated messages and invitation emails": "Jazyk automatických zpráv a zvacích e-mailů", @@ -664,49 +664,49 @@ "Last 30 days": "Posledních 30 dnů", "Last 6 months": "Posledních 6 měsíců", "Last active": "Naposledy činný", - "Last edited {last_edit_timestr}.": "", + "Last edited {last_edit_timestr}.": "Naposledy upraveno {last_edit_timestr}.", "Last message": "Poslední zpráva", "Last modified": "Naposledy změněno", - "Last moved {last_edit_timestr}.": "", + "Last moved {last_edit_timestr}.": "Naposledy přesunuto {last_edit_timestr}.", "Learn more": "Dozvědět se více", "Learn more about mentions here.": "O zmínkách se můžete dozvědět víc zde.", "Learn more about starring messages here.": "Další informace o označování zpráv hvězdičkou zde.", - "Leave group": "", - "Leave group {name}": "", + "Leave group": "Opustit skupinu", + "Leave group {name}": "Opustit skupinu {name}", "Leave {group_name}": "Opustit {group_name}", "Let others see when I've read messages": "Umožnit ostatním vidět, kdy jsem si přečetl zprávy", - "Let recipients see when I'm typing direct messages": "", - "Let recipients see when I'm typing messages in streams": "", - "Let recipients see when a user is typing direct messages": "", - "Let recipients see when a user is typing stream messages": "", - "Light": "", + "Let recipients see when I'm typing direct messages": "Umožnit příjemcům vidět, když píšu přímé zprávy", + "Let recipients see when I'm typing messages in streams": "Umožnit příjemcům vidět, když píšu zprávy v kanálech", + "Let recipients see when a user is typing direct messages": "Umožnit příjemcům vidět, když uživatel píše přímé zprávy", + "Let recipients see when a user is typing stream messages": "Umožnit příjemcům vidět, když uživatel píše zprávy v kanálech", + "Light": "Světlé", "Light theme": "Světlý vzhled", - "Light theme logo": "", + "Light theme logo": "Logo světlého vzhledu", "Link": "Odkaz", "Link with Webathena": "Propojit s Webathena", "Link:": "Odkaz:", "Linkifiers": "Generátory odkazů", - "Linkifiers make it easy to refer to issues or tickets in third party issue trackers, like GitHub, Salesforce, Zendesk, and others. For instance, you can add a linkifier that automatically turns #2468 into a link to the GitHub issue in the Zulip repository with:": "", - "Load more": "", - "Loading…": "", + "Linkifiers make it easy to refer to issues or tickets in third party issue trackers, like GitHub, Salesforce, Zendesk, and others. For instance, you can add a linkifier that automatically turns #2468 into a link to the GitHub issue in the Zulip repository with:": "Odkazovače usnadňují odkazování na věci nebo lístky v systémech pro sledování problémů třetích stran, jako je GitHub, Salesforce, Zendesk a další. Například můžete přidat odkazovač, který automaticky změní #2468 na odkaz na věc na GitHub v úložišti Zulip pomocí:", + "Load more": "Nahrát další", + "Loading…": "Nahrává se...", "Local time": "Místní čas", "Log in": "Přihlásit se", - "Log in to browse more streams": "", + "Log in to browse more streams": "Přihlaste se a procházejte další kanály", "Log out": "Odhlásit se", - "Looking for our integrations or API documentation?": "Hledáte naše začlenění nebo dokumentaci k API?", + "Looking for our integrations or API documentation?": "Hledáte naše doplňky nebo dokumentaci k API?", "MOVED": "PŘESUNUTO", - "Main menu": "", - "Make all messages my home view": "", - "Make inbox my home view": "", - "Make recent conversation my home view": "", - "Make organization permanent": "", + "Main menu": "Hlavní nabídka", + "Make all messages my home view": "Udělat ze všech zpráv můj domácí pohled", + "Make inbox my home view": "Udělat z došlé pošty můj domácí pohled", + "Make recent conversation my home view": "Udělat z nedávných rozhovorů můj domácí pohled", + "Make organization permanent": "Udělat organizaci trvalou", "Manage bot": "Spravovat robota", "Manage this bot": "Spravovat tohoto robota", "Manage this user": "Spravovat tohoto uživatele", "Manage user": "Spravovat uživatele", "March": "Březen", "Mark all messages as read": "Označit všechny zprávy za přečtené", - "Mark all messages as read?": "", + "Mark all messages as read?": "Označit všechny zprávy za přečtené?", "Mark as read": "Označit jako přečtené", "Mark as resolved": "Označit jako vyřešené", "Mark as unread from here": "Označit jako nepřečtené odtud", @@ -729,57 +729,57 @@ "Message edit history": "Historie úprav zprávy", "Message editing": "Upravování zpráv", "Message formatting": "Formátování zprávy", - "Message length shouldn't be greater than 10000 characters.": "", + "Message length shouldn't be greater than 10000 characters.": "Délka zprávy by neměla být větší než 10000 znaků.", "Message length shouldn't be greater than {max_length} characters.": "Délka zprávy by neměla být větší než {max_length} znaků.", "Message retention": "Zadržování zpráv", "Message retention period": "Doba zadržování zprávy", "Message {recipient_label}": "Zpráva {recipient_label}", "Message {recipient_names}": "Zpráva {recipient_names}", "Message {recipient_name} ({recipient_status})": "Zpráva {recipient_name} ({recipient_status})", - "Messages in all public streams": "", - "Messages sent by you": "", - "Messages sent by {sender}": "", - "Messages will not be automatically marked as read because this is not a conversation view. Change setting": "", - "Messages will not be automatically marked as read. Change setting": "", + "Messages in all public streams": "Zprávy ve všech veřejných kanálech", + "Messages sent by you": "Zprávy odeslané vámi", + "Messages sent by {sender}": "Zprávy odeslané {sender}", + "Messages will not be automatically marked as read because this is not a conversation view. Change setting": "Zprávy nebudou automaticky označeny jako přečtené, protože se nejedná o zobrazení rozhovoru. Změnit nastavení", + "Messages will not be automatically marked as read. Change setting": "Zprávy nebudou automaticky označeny jako přečtené. Změnit nastavení", "Mobile": "Telefon", "Mobile message notifications": "Mobilní oznámení o zprávách", "Mobile notifications": "Telefonní oznámení", - "Mobile push notifications are not enabled on this server.": "", - "Mobile push notifications are not enabled on this server. Learn more": "", + "Mobile push notifications are not enabled on this server.": "Telefonní krátká oznámení nejsou na tomto serveru povolena.", + "Mobile push notifications are not enabled on this server. Learn more": "Telefonní krátká oznámení nejsou na tomto serveru povolena. Zjistit více", "Moderator": "Moderátor", "Moderators": "Moderátoři", "Monday": "Pondělí", - "Monday at {time}": "", + "Monday at {time}": "Pondělí v {time}", "Move all messages in {topic_name}": "Přesunout všechny zprávy v {topic_name}", "Move all messages in this topic": "Přesunout všechny zprávy v tomto tématu", "Move message": "Přesunout zprávu", - "Move messages": "", - "Move messages or topic": "", + "Move messages": "Přesunout zprávy", + "Move messages or topic": "Přesunout zprávy nebo témata", "Move only this message": "Přesunout pouze tuto zprávu", - "Move some messages?": "", + "Move some messages?": "Přesunout některé zprávy?", "Move this and all following messages in this topic": "Přesunout tuto a všechny následující zprávy v tomto tématu", "Move topic": "Přesunout téma", "Moved by {full_name}": "Přesunuto {full_name}", - "Moving messages": "", + "Moving messages": "Přesunují se zprávy", "Must be invited by a subscriber; new subscribers can only see messages sent after they join; hidden from non-administrator users": "Musí být pozván odběratelem; noví odběratelé mohou vidět pouze zprávy poslané až poté, co se připojili; skryto uživatelům, kteří nejsou správci", "Must be invited by a subscriber; new subscribers can view complete message history; hidden from non-administrator users": "Musí být pozván odběratelem; noví odběratelé mohou vidět celou historii zpráv; skryto uživatelům, kteří nejsou správci", - "Mute": "", + "Mute": "Ztlumit", "Mute stream": "Ztlumit kanál", "Mute this user": "Ztlumit tohoto uživatele", "Mute topic": "Ztlumit téma", "Mute user": "Ztlumit uživatele", "Muted": "Utlumeno", "Muted user": "Ztlumený uživatel", - "Muted user (guest)": "", + "Muted user (guest)": "Ztlumený uživatel (host)", "Muted users": "Ztlumení uživatelé", "Name": "Jméno", "Name changes are disabled in this organization. Contact an administrator to change your name.": "Měnění jména bylo v této organizaci zakázáno. Pro změnu jména se spojte se správcem.", - "Narrow to all direct messages": "", + "Narrow to all direct messages": "Zúžit na všechny přímé zprávy", "Narrow to all unmuted messages": "Zúžit na všechny neutlumené zprávy", "Narrow to current compose box recipient": "Zúžit na příjemce nyní psané zprávy", - "Narrow to direct messages that include .": "", - "Narrow to direct messages with .": "", - "Narrow to direct messages.": "", + "Narrow to direct messages that include .": "Zúžit na přímé zprávy, které zahrnují .", + "Narrow to direct messages with .": "Zúžit na přímé zprávy s .", + "Narrow to direct messages.": "Zúžit na přímé zprávy.", "Narrow to just message ID .": "Zúžit na pouhé ID zprávy .", "Narrow to messages containing images.": "Zúžit na zprávy obsahující obrázky.", "Narrow to messages containing links.": "Zúžit na zprávy obsahující odkazy.", @@ -791,11 +791,11 @@ "Narrow to messages that mention you.": "Zúžit na zprávy, ve kterých jste zmíněni.", "Narrow to messages with alert words.": "Zúžit na zprávy se sledovanými slovy.", "Narrow to messages with topic .": "Zúžit na zprávy s tématem .", - "Narrow to next unread direct message": "", + "Narrow to next unread direct message": "Zúžit na další nepřečtenou přímou zprávu", "Narrow to next unread topic": "Zúžit na další nepřečtené téma", "Narrow to starred messages.": "Zúžit na zprávy označené hvězdičkami.", - "Narrow to stream from topic view": "", - "Narrow to topic or DM conversation": "", + "Narrow to stream from topic view": "Zúžit na kanál ze zobrazení tématu", + "Narrow to topic or DM conversation": "Zúžit na téma nebo rozhovor vedený přímými zprávami", "Narrow to unread messages.": "Zúžit na nepřečtené zprávy.", "Narrow to {message_recipient}": "Zúžit na {message_recipient}", "Narrowing": "Zúžení", @@ -804,7 +804,7 @@ "Never ask on this computer": "Nikdy se neptat na tomto počítači", "Never expires": "Nikdy nevyprší", "New": "Nový", - "New direct message": "", + "New direct message": "Nová přímá zpráva", "New email": "Nový e-mail", "New message": "Nová zpráva", "New option": "Nová volba", @@ -818,53 +818,53 @@ "New user announcements": "Oznámení nového uživatele", "New user notifications": "Upozornění na nové uživatele", "Next message": "Další zpráva", - "Next unread direct message": "", - "Next unread followed topic": "", + "Next unread direct message": "Další nepřečtená přímá zpráva", + "Next unread followed topic": "Další nepřečtené sledované téma", "Next unread topic": "Další nepřečtené téma", - "No bots found.": "", + "No bots found.": "Nebyli nalezeni žádní roboti.", "No bots match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádní roboti.", - "No conversations match your filters.": "", - "No custom emojis found.": "", - "No custom emojis match your current filter.": "", - "No custom profile field configured for this organization.": "", - "No deactivated users found.": "", - "No default streams found.": "", - "No default streams match your current filter.": "", + "No conversations match your filters.": "Vašim filtrům neodpovídají žádné rozhovory.", + "No custom emojis found.": "Nebyly nalezeny žádné vlastní obrázečky.", + "No custom emojis match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádné vlastní obrázečky.", + "No custom profile field configured for this organization.": "Pro tuto organizaci není nastaveno žádné vlastní profilové pole.", + "No deactivated users found.": "Nebyli nalezeni žádní vypnutí uživatelé.", + "No default streams found.": "Nebyly nalezeny žádné výchozí kanály.", + "No default streams match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádné výchozí kanály.", "No description.": "Žádný popis.", - "No drafts selected": "", + "No drafts selected": "Žádné vybrané návrhy", "No drafts.": "Žádné návrhy.", - "No exports found.": "", - "No group members match your current filter.": "", - "No invites found.": "", + "No exports found.": "Nebyla nalezena žádná uložená data.", + "No group members match your current filter.": "Nynějšímu filtru neodpovídají žádní členové skupiny.", + "No invites found.": "Nebyly nalezeny žádné pozvánky.", "No invites match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádné pozvánky.", "No language set": "Nenastaven žádný jazyk", - "No linkifiers match your current filter.": "", + "No linkifiers match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádné odkazovače.", "No linkifiers set.": "Nenastaven žádný generátor odkazů.", - "No matching results": "", - "No matching streams": "", + "No matching results": "Žádné odpovídající výsledky", + "No matching streams": "Žádné odpovídající kanály", "No matching users.": "Žádní odpovídající uživatelé.", "No one has read this message yet.": "Tuto zprávu si zatím nikdo nepřečetl.", "No owner": "Žádný vlastník", "No playgrounds configured.": "Nenastavena žádná hřiště.", - "No playgrounds match your current filter.": "", + "No playgrounds match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádná hřiště.", "No restrictions": "Žádná omezení", - "No scheduled messages.": "", - "No search results.": "", - "No status text": "", - "No stream subscribers match your current filter.": "", + "No scheduled messages.": "Žádné zařazené zprávy.", + "No search results.": "Žádné výsledky hledání.", + "No status text": "Žádný stavový text", + "No stream subscribers match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádní odběratelé kanálů.", "No stream subscriptions.": "Žádné odběry kanálů.", "No streams": "Žádné kanály", "No topics are marked as resolved.": "Žádná témata nejsou označena jako vyřešená.", "No topics match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádná témata.", - "No uploaded files match your current filter.": "", + "No uploaded files match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádné nahrané soubory.", "No user group subscriptions.": "Žádné odběry skupin uživatelů.", - "No user groups": "", + "No user groups": "Žádné uživatelské skupiny", "No user to subscribe.": "Žádný uživatel k odběru.", "No users match your current filter.": "Vašemu nynějšímu filtru neodpovídají žádní uživatelé.", - "No users to add.": "", + "No users to add.": "Žádní uživatelé k přidání.", "No, I'll catch up.": "Ne, později.", "Nobody": "Nikdo", - "Nobody in this Zulip organization will be able to see this email address.": "", + "Nobody in this Zulip organization will be able to see this email address.": "Nikdo v této organizaci Zulip tuto e-mailovou adresu neuvidí.", "Non-profit (registered)": "Neziskové (registrováno)", "None": "Žádný", "Note that any bots that you maintain will be disabled.": "Nezapomeňte, že všichni vámi udržovaní roboti budou vypnuti.", @@ -881,35 +881,35 @@ "Notify this user by email?": "Upozornit tohoto uživatele e-mailem?", "Notify topic": "Oznámit téma", "November": "Listopad", - "Now following {stream_topic}.": "", - "Numbered list": "", + "Now following {stream_topic}.": "Nyní sleduje {stream_topic}.", + "Numbered list": "Číslovaný seznam", "October": "Říjen", "Old password": "Staré heslo", "Once you leave this group, you will not be able to rejoin.": "Jakmile tuto skupinu opustíte, nebudete se moci znovu připojit.", "Once you leave this stream, you will not be able to rejoin.": "Jakmile tento kanál opustíte, nebudete se moci znovu připojit.", "One or more email addresses...": "Jeden nebo více e-mailů...", "One or more of these users do not exist!": "Jeden nebo více z těchto uživatelů neexistuje!", - "Only 2 custom profile fields can be displayed on the user card.": "", + "Only 2 custom profile fields can be displayed on the user card.": "Na kartě uživatele lze zobrazit pouze 2 vlastní profilová pole.", "Only group members can add users to a group.": "Do skupiny mohou uživatele přidávat pouze členové skupiny.", "Only in conversation views": "Pouze při zobrazení konverzace", "Only organization administrators can edit these settings": "Tato nastavení mohou upravovat pouze správci organizace", "Only organization administrators can edit these settings.": "Pouze správci organizace mohou upravovat tato nastavení.", "Only organization owners can edit these settings.": "Pouze vlastníci organizace mohou upravovat tato nastavení.", - "Only organization owners may deactivate an organization.": "", + "Only organization owners may deactivate an organization.": "Pouze vlastníci organizace mohou vypnout organizaci.", "Only owners can change these settings.": "Tato nastavení mohou měnit pouze majitelé.", - "Only stream members can add users to a private stream.": "", - "Only subscribers can access or join private streams, so you will lose access to this stream if you convert it to a private stream while not subscribed to it.": "", + "Only stream members can add users to a private stream.": "Do soukromého kanálu mohou další uživatele přidávat pouze jeho členové.", + "Only subscribers can access or join private streams, so you will lose access to this stream if you convert it to a private stream while not subscribed to it.": "K soukromým kanálům mají přístup nebo se k nim mohou připojit pouze odběratelé, takže pokud tento kanál převedete na soukromý a nebudete přihlášeni k jeho odběru, ztratíte k němu přístup.", "Only subscribers to this stream can edit stream permissions.": "Oprávnění k tomuto kanálu mohou upravovat pouze jeho odběratelé.", - "Only topics you follow": "", + "Only topics you follow": "Pouze témata, která sledujete", "Open": "Otevřít", - "Open help menu": "", + "Open help menu": "Otevřít nabídku s nápovědou", "Open message menu": "Otevřít nabídku ke zprávě", - "Open personal menu": "", + "Open personal menu": "Otevřít osobní nabídku", "Open reactions menu": "Otevřít nabídku odpovědí", "Open-source project": "Projekt s otevřeným zdrojovým kódem", "Option already present.": "Volba již existuje.", "Optional": "Volitelné", - "Options": "", + "Options": "Volby", "Organization": "Organizace", "Organization URL": "Adresa organizace", "Organization administrators": "Správci organizace", @@ -931,7 +931,7 @@ "Other emails": "Další e-maily", "Other permissions": "Jiná oprávnění", "Other settings": "Jiná nastavení", - "Other users in this Zulip organization will be able to see this email address.": "", + "Other users in this Zulip organization will be able to see this email address.": "Tuto e-mailovou adresu uvidí i ostatní uživatelé v této organizaci Zulip.", "Out sick": "Nemocný", "Outgoing webhook message format": "Formát odchozí webhook zprávy", "Override default emoji?": "Přepsat výchozí obrázeček?", @@ -946,27 +946,27 @@ "Password should be at least {length} characters long": "Heslo by mělo být {length} alespoň znaků", "Pattern": "Předpis", "Personal": "Osobní", - "Personal menu": "", + "Personal menu": "Osobní nabídka", "Personal settings": "Osobní nastavení", "Pin stream to top": "Přišpendlit kanál nahoru", "Pin stream to top of left sidebar": "Přišpendlit kanál v levém postranním panelu nahoru", "Pinned": "Přišpendleno", - "Plan management": "", + "Plan management": "Správa plánu", "Plans and pricing": "Ceník a plány", "Play sound": "Přehrát zvuk", "Please contact support for an exception or add users with a reusable invite link.": "Prosím, spojte se s podporou kvůli výjimce nebo přidání uživatelů pomocí opakovaně použitelného odkazu s pozvánkou.", "Please ask a billing administrator to increase the number of licenses or deactivate inactive users, and try again.": "Požádejte správce fakturace pro navýšení počtu licencí nebo vypnutí nečinných uživatelů, a zkuste to znovu.", "Please choose a new password": "Prosím, zadejte nové heslo", - "Please enter a question.": "", + "Please enter a question.": "Zadejte, prosím, otázku.", "Please enter your password": "Prosím, zadejte své heslo", "Please just upload one file.": "Nahrejte prosím pouze jeden soubor.", "Please only use characters that are valid in an email address": "Používejte prosím pouze znaky, které jsou platné pro e-mailovou adresu.", "Please re-enter your password to confirm your identity.": "Zadejte prosím znovu své heslo pro ověření vaší totožnosti.", "Please specify a stream.": "Zadejte, prosím, kanál.", - "Please specify at least one valid recipient.": "", + "Please specify at least one valid recipient.": "Zadejte, prosím, alespoň jednoho platného příjemce.", "Political group": "Politická skupina", "Posted by {full_name}": "Zveřejnil {full_name}", - "Preferences": "", + "Preferences": "Nastavení", "Press > for list of topics": "Stiskněte > pro zobrazení seznamu témat", "Prevent users from changing their avatar": "Zabránit uživatelům v úpravách jejich avatarů", "Prevent users from changing their email address": "Zabránit uživatelům v úpravách jejich e-mailů", @@ -977,42 +977,42 @@ "Previous message": "Předchozí zpráva", "Privacy": "Soukromí", "Privacy settings": "Nastavení soukromí", - "Private streams cannot be default streams for new users.": "", + "Private streams cannot be default streams for new users.": "Soukromé kanály nemohou být výchozími kanály pro nové uživatele.", "Private, protected history": "Soukromá, chráněná historie", "Private, shared history": "Soukromá, sdílená historie", "Profile": "Profil", "Pronouns": "Zájmena", "Public": "Veřejné", "Question": "Otázka", - "Quote": "", + "Quote": "Citovat", "Quote and reply": "Citovat a odpovědět", "Quote and reply to message": "Ocitovat a odpovědět na zprávu", - "Quoted original email (in replies)": "", + "Quoted original email (in replies)": "Citace původního e-mailu (v odpovědích)", "React to selected message with": "Odpovědět na vybranou zprávu s", "Reactivate": "Znovu zapnout", "Reactivate bot": "Znovu zapnout robota", "Reactivate this bot": "Zapnout znovu tohoto robota", "Reactivate this user": "Zapnout znovu tohoto uživatele", - "Reactivate user": "", + "Reactivate user": "Znovu zapnout uživatele", "Reactivate {name}": "Zapnout znovu {name}", "Read receipts": "Potvrzení o přečtení", "Read receipts are currently disabled in this organization.": "Potvrzení o přečtení jsou v současnosti v této organizaci zakázány.", "Read receipts are not available for Notification Bot messages.": "Potvrzení o přečtení nejsou dostupná pro zprávy oznamovacího robota.", "Receives new stream announcements": "Přijímá oznámení nového kanálu", - "Recent conversations": "", + "Recent conversations": "Nedávné rozhovory", "Remove": "Odstranit", "Remove from default": "Odstranit z výchozích", "Removed successfully.": "Úspěšně odstraněno.", - "Rename topic": "", - "Rename topic to:": "", - "Reply @-mentioning sender": "", - "Reply directly to sender": "", + "Rename topic": "Přejmenovat téma", + "Rename topic to:": "Přejmenovat téma na:", + "Reply @-mentioning sender": "Odpovědět a zmínit odesílatele", + "Reply directly to sender": "Odpovědět přímo odesílateli", "Reply mentioning bot": "Odpovědět a zmínit robota", "Reply mentioning user": "Odpovědět a zmínit uživatele", "Reply to message": "Odpovědět na zprávu", - "Reply to selected conversation": "", + "Reply to selected conversation": "Odpovědět na vybraný rozhovor", "Reply to selected message": "Odpovědět na vybranou zprávu", - "Request education pricing": "", + "Request education pricing": "Vyžádat si ceny za vzdělávání", "Request sponsorship": "Požádejte o sponzorství", "Requesting user": "Žádající uživatel", "Require topics in stream messages": "Vyžadovat využití témat ve zprávách kanálu", @@ -1021,11 +1021,11 @@ "Resend invitation": "Znovu odeslat pozvánku", "Resending encountered an error. Please reload and try again.": "Při pokusu o opětovné poslání došlo k chybě. Načtěte stránku znovu a zkuste to ještě jednou.", "Reset zoom": "Obnovit přiblížení", - "Restore draft": "Obnovit koncept", + "Restore draft": "Obnovit návrh", "Restrict email domains of new users?": "Omezit domény e-mailů, ze kterých je umožněna registrace nových uživatelů?", "Restrict to a list of domains": "Omezit na seznam domén", "Retain forever": "Podržet navždy", - "Retention period (days)": "", + "Retention period (days)": "Doba uchování (ve dnech)", "Retry": "Zkusit znovu", "Revoke": "Odvolat", "Revoke invitation link": "Odvolat zvací odkaz", @@ -1038,50 +1038,50 @@ "Save changes": "Uložit změny", "Save failed": "Ukládání selhalo", "Saved": "Uloženo", - "Saved as draft": "Uloženo jako koncept", + "Saved as draft": "Uloženo jako návrh", "Saved. Please reload for the change to take effect.": "Uloženo. Aby se tyto změny projevily, obnovte prosím stránku.", "Saving": "Ukládání", - "Schedule for {deliver_at}": "", - "Schedule for {formatted_send_later_time}": "", - "Schedule message": "", - "Scheduled messages": "", + "Schedule for {deliver_at}": "Zařadit na {deliver_at}", + "Schedule for {formatted_send_later_time}": "Zařadit na {formatted_send_later_time}", + "Schedule message": "Zařadit zprávu", + "Scheduled messages": "Zařazené zprávy", "Scroll down": "Posunout dolů", - "Scroll down to view your message.": "", + "Scroll down to view your message.": "Posuňte se dolů a zobrazte svou zprávu.", "Scroll through streams": "Procházet kanály", "Scroll to bottom": "Přejít dolů", "Scroll up": "Posunout nahoru", "Search": "Hledat", "Search GIFs": "Hledat GIFy", "Search all public streams in the organization.": "Prohledat všechny veřejné kanály v organizaci.", - "Search filters": "", + "Search filters": "Vyhledávací filtry", "Search for in the topic or message content.": "Hledat v tématu nebo obsahu zprávy.", "Search people": "Hledat osoby", "Search results": "Hledat výsledky", "See how to configure email.": "Podívejte se jak nastavit e-mail.", "Select a stream": "Vybrat kanál", "Select a stream below or change topic name.": "Vyberte kanál níže nebo změňte název tématu.", - "Select a stream to subscribe": "", - "Select all drafts": "", - "Select an integration": "", - "Select draft": "", + "Select a stream to subscribe": "Vyberte kanál, který chcete odebírat", + "Select all drafts": "Vybrat všechny návrhy", + "Select an integration": "Vyberte doplňek", + "Select draft": "Vybrat návrh", "Select emoji": "Vybrat obrázeček", "Select language": "Vybrat jazyk", - "Select stream": "", + "Select stream": "Vybrat kanál", "Send": "Odeslat", - "Send all notifications to a single topic": "", - "Send an email": "", + "Send all notifications to a single topic": "Poslat všechna oznámení do jednoho tématu", + "Send an email": "Poslat e-mail", "Send automated notice to new topic": "Poslat automatické upozornění na nové téma", "Send automated notice to old topic": "Poslat automatické upozornění na staré téma", "Send digest emails when I'm away": "Posílat zprávy s přehledy, když jsem pryč", "Send digest emails when user is away": "Odesílat elektronické dopisy s přehledem, když je uživatel pryč", - "Send direct message": "", + "Send direct message": "Poslat přímou zprávu", "Send email notifications for new logins to my account": "Posílat elektronickou poštou oznámení o nových přihlášeních do mého účtu", "Send emails introducing Zulip to new users": "Posílat e-maily s představením Zulipu novým uživatelům", "Send me Zulip's low-traffic newsletter (a few emails a year)": "Pošlete mi občas Zulipův zpravodaj (několik elektronických dopisů ročně)", "Send message": "Poslat zprávu", "Send mobile notifications even if I'm online": "Posílat telefonní oznámení, i když jsem připojen k internetu", "Send mobile notifications even if user is online": "Posílat telefonní oznámení, i když je uživatel připojen k internetu", - "Send options": "", + "Send options": "Volby pro posílání", "Send weekly digest emails to inactive users": "Posílat zprávy s týdenními přehledy nečinným uživatelům", "Sent!": "Odesláno!", "Sent! Your message is outside your current narrow.": "Odesláno! Vaše zpráva je mimo vaše aktuální zúžení zobrazení.", @@ -1099,16 +1099,16 @@ "Show images in thread": "Ukázat obrázky ve vláknu", "Show keyboard shortcuts": "Zobrazit klávesové zkratky", "Show less": "Zobrazit méně", - "Show message sender's user card": "", + "Show message sender's user card": "Ukázat kartu uživatele odesílatele zprávy", "Show more": "Ukázat více", "Show password": "Ukázat heslo", "Show previews of linked websites": "Ukázat náhledy odkazovaných webů", - "Show previews of uploaded and linked images and videos": "", + "Show previews of uploaded and linked images and videos": "Ukázat náhledy nahraných a odkazovaných obrázků a obrazových záznamů", "Show starred message count": "Ukázat počet zpráv s hvězdičkami", "Show status text": "Zobrazit stavový text", - "Show unread counts for": "", + "Show unread counts for": "Ukázat počty nepřečtených pro", "Show/change your API key": "Ukázat/změnit váš API klíč", - "Showing messages since {time_string}.": "", + "Showing messages since {time_string}.": "Zobrazení zpráv od {time_string}.", "Sign up": "Zaregistrovat se", "Silent mentions do not trigger notifications.": "Tiché zmínky nespouštějí oznámení.", "Size": "Velikost", @@ -1119,43 +1119,43 @@ "Sort by estimated weekly traffic": "Řadit podle odhadovaného týdenního provozu", "Sort by name": "Řadit podle názvu", "Sort by number of subscribers": "Řadit podle počtu odběratelů", - "Sort by unread message count": "", + "Sort by unread message count": "Řadit podle počtu nepřečtených zpráv", "Spoiler": "Vyzrazení", - "Sponsorship request pending": "", - "Standard view": "", + "Sponsorship request pending": "Žádost o sponzorství čeká na vyřízení", + "Standard view": "Běžné zobrazení", "Star": "Hvězdička", "Star selected message": "Přidat hvězdičku k označené zprávě", - "Star this message": "", + "Star this message": "Označit tuto zprávu hvězdičkou", "Starred messages": "Zprávy s hvězdičkami", - "Start a new topic or select one from the list.": "", + "Start a new topic or select one from the list.": "Založte nové téma nebo vyberte jedno ze seznamu.", "Start export of public data": "Začít s ukládáním veřejných dat", - "Start new conversation": "", + "Start new conversation": "Začněte nový rozhovor", "Status": "Stav", "Stream": "Kanál", "Stream color": "Barva kanálu", "Stream created recently": "Kanál vytvořen nedávno", "Stream creation": "Vytvoření kanálu", "Stream description": "Popis kanálu", - "Stream email address:": "", + "Stream email address:": "E-mailová adresa kanálu:", "Stream name": "Název kanálu", "Stream permissions": "Oprávnění ke kanálu", "Stream settings": "Nastavení kanálu", "Stream successfully created!": "Kanál úspěšně vytvořen!", "Streams": "Kanály", "Streams they should join": "Kanály, ke kterým se mají přidat", - "Strikethrough": "", + "Strikethrough": "Přeškrtnutí", "Subject": "Předmět", "Subscribe": "Odebírat", "Subscribe them": "Odebírat je", - "Subscribe to ": "", + "Subscribe to ": "Odebírat ", "Subscribe to/unsubscribe from selected stream": "Odebírat/Zrušit odběr vybraných kanálů", - "Subscribe {full_name} to streams": "", + "Subscribe {full_name} to streams": "Přihlásit {full_name} k odběru kanálů", "Subscribed": "Odebíráno", "Subscribed streams": "Odebírané kanály", "Subscribed successfully!": "Odběr úspěšně nastaven!", "Subscriber count": "Počet odběratelů", "Subscribers": "Odběratelé", - "Successfully subscribed user:": "", + "Successfully subscribed user:": "Úspěšně přihlášený uživatel:", "Successfully subscribed users:": "Úspěšně přihlášení uživatelé:", "Sunday": "Neděle", "Support Zulip": "Podpořte Zulip", @@ -1169,58 +1169,58 @@ "The administrators provided the following comment:": "Správci poskytli následující vyjádření:", "The basics": "Základy", "The group description cannot contain newline characters.": "Popis skupiny nemůže obsahovat znaky pro nový řádek.", - "The recipient {recipient} is not valid.": "", - "The recipients {recipients} are not valid.": "", - "The sender's email address": "", - "The stream #{stream_name} does not exist. Manage your subscriptions on your Streams page.": "", + "The recipient {recipient} is not valid.": "Příjemce {recipient} není platný.", + "The recipients {recipients} are not valid.": "Příjemci {recipients} nejsou platní.", + "The sender's email address": "E-mailová adresa odesílatele", + "The stream #{stream_name} does not exist. Manage your subscriptions on your Streams page.": "Kanál #{stream_name} neexistuje. Spravujte své odběry na stránce vašich kanálů.", "The stream description cannot contain newline characters.": "Popis kanálu nemůže obsahovat znaky pro nový řádek.", "Their password will be cleared from our systems, and any bots they maintain will be disabled.": "Jejich heslo bude vymazáno z našich systémů a všichni jimi spravovaní roboti budou vypnuti.", "Theme": "Vzhled", "There are no current alert words.": "Nyní nejsou dostupná žádná sledovaná slova.", - "There are no messages here.": "", - "There are no streams you can view in this organization.": "", - "There are no unread messages in your inbox.": "", - "There are no user groups you can view in this organization.": "", - "There is a default emoji with this name. Do you want to override it with a custom emoji? The name :{emoji_name}: will no longer work to access the default emoji.": "", + "There are no messages here.": "Nejsou zde žádné zprávy.", + "There are no streams you can view in this organization.": "V této organizaci nelze zobrazit žádné kanály.", + "There are no unread messages in your inbox.": "Ve složce s došlou poštou nejsou žádné nepřečtené zprávy.", + "There are no user groups you can view in this organization.": "V této organizaci nelze zobrazit žádné skupiny uživatelů.", + "There is a default emoji with this name. Do you want to override it with a custom emoji? The name :{emoji_name}: will no longer work to access the default emoji.": "Existuje výchozí obrázeček s tímto názvem. Chcete jej přepsat vlastním obrázečkem? Název :{emoji_name}: již nebude pracovat jako přístup k výchozímu obrázečku.", "They administer the following bots:": "Spravují následující roboty:", "This Zulip server is running an old version and should be upgraded.": "Server Zulip běží ve staré verzi a měl by být povýšen.", "This action cannot be undone.": "Tento úkon nelze vrátit zpět.", "This action is permanent and cannot be undone. All users will permanently lose access to their Zulip accounts.": "Tento krok je trvalý a nelze jej vrátit zpět. Všichni uživatelé natrvalo ztratí přístup ke svým účtům v Zulipu.", - "This bot cannot be deactivated.": "", - "This bot cannot be edited.": "", + "This bot cannot be deactivated.": "Tohoto robota nelze vypnout.", + "This bot cannot be edited.": "Tohoto robota nelze upravit.", "This bot has been deactivated.": "Tento robot byl vypnut.", - "This conversation may have additional messages not shown in this view.": "", - "This demo organization will be automatically deleted in {days_remaining} days, unless it's converted into a permanent organization.": "", - "This feature is available on Zulip Cloud Plus. Upgrade to access.": "", - "This group has no members.": "", - "This is a demo organization and will be automatically deleted in {days_remaining} days, unless it's converted into a permanent organization.": "", + "This conversation may have additional messages not shown in this view.": "Tento rozhovor může obsahovat další zprávy, které se v tomto zobrazení nezobrazují.", + "This demo organization will be automatically deleted in {days_remaining} days, unless it's converted into a permanent organization.": "Tato ukázková organizace bude automaticky smazána za {days_remaining} dní, pokud není přeměněna na stálou organizaci.", + "This feature is available on Zulip Cloud Plus. Upgrade to access.": "Tato funkce je dostupná v aplikaci Zulip Cloud Plus. Pro přístup k ní proveďte povýšení verze programu.", + "This group has no members.": "Tato skupina nemá žádné členy.", + "This is a demo organization and will be automatically deleted in {days_remaining} days, unless it's converted into a permanent organization.": "Toto je ukázková organizace a bude automaticky smazán za {days_remaining} dní, pokud nebude přeměněna na stálou organizaci.", "This is not a publicly accessible conversation.": "Toto není veřejně přístupný rozhovor.", "This is what a Zulip notification looks like.": "Takto vypadá oznámení Zulipu.", - "This is your home view.": "", - "This message could not be sent at the scheduled time.": "", - "This message is no longer scheduled to be sent.": "", + "This is your home view.": "Toto je váš domácí pohled.", + "This message could not be sent at the scheduled time.": "Tuto zprávu se nepodařilo odeslat v plánovaném čase.", + "This message is no longer scheduled to be sent.": "Tato zpráva již není zařazena k odeslání.", "This message was hidden because you have muted the sender.": "Tato zpráva byla skrytá, protože jste ztlumil odesílatele.", - "This organization is configured so that administrators and moderators can add custom emoji.": "", - "This organization is configured so that admins and moderators can invite users to this organization.": "", - "This organization is configured so that admins can invite users to this organization.": "", - "This organization is configured so that admins, moderators and full members can invite users to this organization.": "", - "This organization is configured so that admins, moderators and members can invite users to this organization.": "", - "This organization is configured so that any member of this organization can add custom emoji.": "", - "This organization is configured so that anyone can add bots.": "", - "This organization is configured so that full members can add custom emoji.": "", - "This organization is configured so that nobody can invite users to this organization.": "", - "This organization is configured so that only administrators can add bots.": "", - "This organization is configured so that only administrators can add custom emoji.": "", - "This organization is configured so that only administrators can add generic bots.": "", + "This organization is configured so that administrators and moderators can add custom emoji.": "Tato organizace je nastavena tak, aby správci a moderátoři mohli přidávat vlastní obrázečky.", + "This organization is configured so that admins and moderators can invite users to this organization.": "Tato organizace je nastavena tak, aby správci a moderátoři mohli do této organizace zvát uživatele.", + "This organization is configured so that admins can invite users to this organization.": "Tato organizace je nastavena tak, aby správci mohli do této organizace zvát uživatele.", + "This organization is configured so that admins, moderators and full members can invite users to this organization.": "Tato organizace je nastavena tak, aby správci, moderátoři a řádní členové mohli do této organizace zvát uživatele.", + "This organization is configured so that admins, moderators and members can invite users to this organization.": "Tato organizace je nastavena tak, aby správci, moderátoři a členové mohli do této organizace zvát uživatele.", + "This organization is configured so that any member of this organization can add custom emoji.": "Tato organizace je nastavena tak, aby každý její člen mohl přidávat vlastní obrázeček.", + "This organization is configured so that anyone can add bots.": "Tato organizace je nastavena tak, aby roboty mohl přidávat kdokoli.", + "This organization is configured so that full members can add custom emoji.": "Tato organizace je nastavena tak, aby řádní členové mohli přidávat vlastní obrázečky.", + "This organization is configured so that nobody can invite users to this organization.": "Tato organizace je nastavena tak, aby nikdo nemohl do této organizace zvát uživatele.", + "This organization is configured so that only administrators can add bots.": "Tato organizace je nastavena tak, že přidávat roboty mohou pouze správci.", + "This organization is configured so that only administrators can add custom emoji.": "Tato organizace je nastavena tak, že přidávat vlastní obrázečky mohou pouze správci.", + "This organization is configured so that only administrators can add generic bots.": "Tato organizace je nastavena tak, že přidávat obecné roboty mohou pouze správci.", "This organization is configured to restrict editing of message content to {minutes_to_edit} minutes after it is sent.": "Tato organizace je nastavena tak, aby byly úpravy obsahu zprávy omezeny na {minutes_to_edit} minut, poté co byla odeslána.", "This stream does not exist or is private.": "Tento kanál neexistuje nebo je soukromý.", "This stream does not yet have a description.": "Tento kanál zatím nemá popis.", "This stream has been deactivated": "Tento kanál byl vypnut", - "This stream has no subscribers.": "", - "This stream has {sub_count, plural, =0 {no subscribers} one {# subscriber} other {# subscribers}}.": "", + "This stream has no subscribers.": "Tento kanál nemá žádné odběratele.", + "This stream has {sub_count, plural, =0 {no subscribers} one {# subscriber} other {# subscribers}}.": "Tento kanál má {sub_count, plural, =0 {no subscribers} jednoho {# subscriber} další {# subscribers}}.", "This user does not exist!": "Tento uživatel neexistuje!", "This user has been deactivated.": "Tento uživatel byl vypnut.", - "This view is still loading messages.": "", + "This view is still loading messages.": "Toto zobrazení stále ještě nahrává zprávy.", "This will clear the profile field for 1 user.": "Tím se vymaže pole profilu pro 1 uživatele.", "This will clear the profile field for users.": "Tím se vymaže pole profilu pro uživatele.", "This will delete the profile field for 1 user.": "Tím se smaže pole profilu pro 1 uživatele.", @@ -1230,73 +1230,73 @@ "Time format": "Formát času", "Time limit for deleting messages": "Časová lhůta pro mazání zpráv", "Time limit for editing messages": "Časová lhůta pro upravování zpráv", - "Time limit for editing topics": "", - "Time limit for moving messages between streams": "", + "Time limit for editing topics": "Časová lhůta pro upravování témat", + "Time limit for moving messages between streams": "Časová lhůta pro přesunování zpráv mezi kanály", "Time zone": "Časové pásmo", "Time's up!": "Čas vypršel!", - "Tip: You can also send \"/poll Some question\"": "Tip: Také můžete poslat \"/poll Nějaká otázka?\"", + "Tip: You can also send \"/poll Some question\"": "Rada: Také můžete poslat \"/poll Nějaká otázka?\"", "To invite users, please increase the number of licenses or deactivate inactive users.": "Chcete-li pozvat uživatele, prosím, navyšte počet licencí nebo vypněte nečinné uživatele.", "To preserve your reading state, this view does not mark messages as read.": "Pro zachování vašeho stavu čtení toto zobrazení neoznačuje zprávy jako přečtené.", "Today": "Dnes", - "Today at {time}": "", - "Toggle first emoji reaction on selected message": "", + "Today at {time}": "Dnes v {time}", + "Toggle first emoji reaction on selected message": "Přepnout první odpověď obrázečkem na vybrané zprávě", "Toggle subscription": "Přepnout odběr", "Toggle the gear menu": "Otevřít nastavení Zulipu", "Toggle topic mute": "Přepnout ztlumení tématu", - "Tomorrow at {time}": "", + "Tomorrow at {time}": "Zítra v {time}", "Topic": "Téma", "Topic muted": "Téma ztlumeno", - "Topic notifications": "", - "Topic settings": "", + "Topic notifications": "Oznámení témat", + "Topic settings": "Nastavení témat", "Topics": "Témata", - "Topics I participate in": "", - "Topics I send a message to": "", - "Topics I start": "", - "Topics are required in this organization.": "", + "Topics I participate in": "Témata, kterých se účastním", + "Topics I send a message to": "Témata, kterým posílám zprávu", + "Topics I start": "Témata, která začínám", + "Topics are required in this organization.": "Témata jsou v této organizaci povinná.", "Topics marked as resolved": "Témata označena jako vyřešená", "Tuesday": "Úterý", "Turn off invisible mode": "Vypnout neviditelný režim", "Two factor authentication": "2FA (dvoufaktorové ověření)", "Type": "Typ", "URL": "Adresa (URL)", - "URL for your integration": "", + "URL for your integration": "Adresa URL pro váš doplněk", "URL pattern": "Vzor adresy (URL) ", - "URL template": "", + "URL template": "Předloha URL", "USERS": "UŽIVATELÉ", "Uncheck all": "Odznačit vše", "Undo": "Zpět", - "Undo mute": "", + "Undo mute": "Zrušit ztlumení", "Unknown": "Neznámý", "Unknown stream": "Neznámý kanál", - "Unknown stream #{search_text}": "", - "Unknown user": "", + "Unknown stream #{search_text}": "Neznámý kanál #{search_text}", + "Unknown user": "Neznámý uživatel", "Unmute": "Zrušit ztlumení", "Unmute stream": "Zrušit ztlumení kanálu", "Unmute this user": "Zrušit ztlumení tohoto uživatele", "Unmute topic": "Zrušit ztlumení tématu", - "Unmuted": "", - "Unmuted {stream_topic}.": "", - "Unmuted streams and topics": "", + "Unmuted": "Zrušeno ztlumení", + "Unmuted {stream_topic}.": "Neztlumené {stream_topic}.", + "Unmuted streams and topics": "Neztlumené kanály a témata", "Unpin stream from top": "Odepnout kanál odshora", "Unread": "Nepřečtené", "Unread count badge (appears in desktop sidebar and browser tab)": "Znamení počtu nepřečtených (zobrazuje se na postranním panelu plochy a na kartě prohlížeče)", - "Unread messages": "", + "Unread messages": "Nepřečtené zprávy", "Unresolve topic": "Zrušit vyřešení tématu", "Unstar": "Odebrat hvězdičku", "Unstar all messages": "Odebrat všem zprávám hvězdičky", "Unstar all messages in topic": "Odhvězdičkovat všechny zprávy v tématu", "Unstar messages in topic": "Odhvězdičkovat zprávy v tématu", - "Unstar this message": "", + "Unstar this message": "Odhvězdičkovat tuto zprávu", "Unsubscribe": "Zrušit odběr", - "Unsubscribe from ?": "", - "Unsubscribe from ": "", - "Unsubscribe {full_name} from ?": "", + "Unsubscribe from ?": "Odhlásit se z odběru ?", + "Unsubscribe from ": "Odhlásit se z odběru ", + "Unsubscribe {full_name} from ?": "Odhlásit {full_name} z odběru ?", "Unsubscribed successfully!": "Odběr úspěšně zrušen!", "Up to {time_limit} after posting": "Až do {time_limit} po odeslání", "Update successful: Subdomains allowed for {domain}": "Aktualizace úspěšná: subdomény povoleny pro {domain}", "Update successful: Subdomains no longer allowed for {domain}": "Aktualizace úspěšná: Poddomény už více pro {domain} nepovoleny", - "Upgrade to the latest release": "", - "Upgrade to {standard_plan_name}": "", + "Upgrade to the latest release": "Povýšit na nejnovější vydání.", + "Upgrade to {standard_plan_name}": "Povýšit na {standard_plan_name}", "Upload avatar": "Nahrát avatar", "Upload files": "Nahrát soubory", "Upload icon": "Nahrát ikonu", @@ -1309,10 +1309,10 @@ "Uploading {filename}…": "Nahrává se {filename}…", "Usage statistics": "Statistiky využití", "Use full width on wide screens": "Na širokých obrazovkách použít celou šířku", - "Use html encoding (not recommended)": "", + "Use html encoding (not recommended)": "Použít kódování html (nedoporučuje se)", "Use organization level settings {org_level_message_retention_setting}": "Použít nastavení na úrovni organizace {org_level_message_retention_setting}", "Use stream settings to unsubscribe from private streams.": "K odhlášení odběru soukromých kanálů použijte nastavení kanálu.", - "Use stream settings to unsubscribe the last user from a private stream.": "", + "Use stream settings to unsubscribe the last user from a private stream.": "K odhlášení posledního uživatele z odběru soukromého kanálu použijte nastavení kanálu.", "User": "Uživatel", "User ID": "ID uživatele", "User group creation": "Vytvoření uživatelské skupiny", @@ -1329,15 +1329,15 @@ "User role": "Role uživatele", "Users": "Uživatelé", "Users can always disable their personal read receipts.": "Uživatelé mohou své osobní potvrzení o přečtení vždy zakázat.", - "Users join as": "", - "VIEWS": "", + "Users join as": "Uživatelé budou přidáni jako", + "VIEWS": "POHLEDY", "Vacationing": "Na dovolené", "Version {zulip_version}": "Verze {zulip_version}", - "View all streams": "", - "View all user groups": "", - "View direct messages": "", - "View drafts": "Zobrazit koncepty", - "View edit and move history": "", + "View all streams": "Zobrazit všechny kanály", + "View all user groups": "Zobrazit všechny uživatelské skupiny", + "View direct messages": "Zobrazit přímé zprávy", + "View drafts": "Zobrazit návrhy", + "View edit and move history": "Zobrazit historii úprav a přesunů", "View edit history": "Zobrazit historii úprav", "View file": "Zobrazit soubor", "View in playground": "Zobrazit na hřišti", @@ -1345,35 +1345,35 @@ "View in {playground_name}": "Zobrazit v {playground_name}", "View message source": "Zobrazit zdroj zprávy", "View messages sent": "Zobrazit odeslané zprávy", - "View messages with yourself": "", - "View profile": "", + "View messages with yourself": "Zobrazit zprávy se sebou samým", + "View profile": "Zobrazit profil", "View read receipts": "Zobrazit potvrzení o přečtení", - "View recent conversations": "", - "View scheduled messages": "", + "View recent conversations": "Zobrazit nedávné rozhovory", + "View scheduled messages": "Zobrazit zařazené zprávy", "View stream": "Zobrazit kanál", "View stream messages": "Zobrazit zprávy z kanálu", - "View streams": "", - "View user card": "", + "View streams": "Zobrazit kanály", + "View user card": "Zobrazit kartu uživatele", "View your profile": "Zobrazit váš profil", "Visual": "Viditelný", "Visual desktop notifications": "Viditelné oznámení na ploše", "Waiting period (days)": "Doba čekání (ve dnech)", "Waiting period before new members turn into full members": "Doba čekání, předtím než se z nových členů stanou plnoprávní členové", - "Warning: #{stream_name} is a private stream.": "", - "We are about to have a poll. Please wait for the question.": "Proběhne anketa. Počkejte na otázku.", + "Warning: #{stream_name} is a private stream.": "Upozornění: kanál #{stream_name} je soukromý.", + "We are about to have a poll. Please wait for the question.": "Proběhne hlasování. Počkejte na otázku.", "We strongly recommend enabling desktop notifications. They help Zulip keep your team connected.": "Velmi doporučujeme povolit oznámení na ploše. Pomohou Zulipu udržet váš tým propojený.", "We've replaced the \"{originalHotkey}\" hotkey with \"{replacementHotkey}\" to make this common shortcut easier to trigger.": "Abychom usnadnili spuštění této běžné zkratky, nahradili jsme klávesovou zkratku „{originalHotkey}“ klávesovou zkratkou „{replacementHotkey}“.", "Web-public": "Internetový veřejný", "Wednesday": "Středa", "Week of {date}": "Týden {date}", - "Welcome back!": "", + "Welcome back!": "Vítejte zpět", "What pronouns should people use to refer to you?": "Jaká zájmena by měli lidé používat, když o vás mluví?", "When you deactivate , they will be immediately logged out.": "Když vypnete , budou okamžitě odhlášeni..", - "Where to send notifications": "", + "Where to send notifications": "Kam zasílat oznámení", "Whether wildcard mentions like @all are treated as mentions for the purpose of notifications.": "Zda je se zmínkami (zástupný symbol) jako @all zacházeno jako se zmínkami pro účel oznámení.", - "Which parts of the email should be included in the Zulip message sent to this stream?": "", + "Which parts of the email should be included in the Zulip message sent to this stream?": "Které části elektronického dopisu mají být zahrnuty do zprávy Zulipu odeslané do tohoto kanálu?", "Who can access the stream?": "Kdo může přistupovat ke kanálu?", - "Who can access user's email address": "", + "Who can access user's email address": "Kdo může přistupovat k adresám elektronické pošty uživatelů", "Who can access your email address": "Kdo může přistupovat k vaší adrese elektronické pošty", "Who can add bots": "Kdo může přidávat roboty", "Who can add custom emoji": "Kdo může přidat vlastní emoji", @@ -1381,18 +1381,18 @@ "Who can create and manage user groups": "Kdo může vytvářet a spravovat uživatelské skupiny", "Who can create private streams": "Kdo může vytvářet soukromé kanály", "Who can create public streams": "Kdo může vytvářet veřejné kanály", - "Who can create reusable invitation links": "", + "Who can create reusable invitation links": "Kdo může vytvářet opakovaně použitelné odkazy na pozvánky", "Who can create web-public streams": "Kdo může vytvářet internetové veřejné kanály", "Who can delete their own messages": "Kdo může mazat své vlastní zprávy", - "Who can mention this group?": "", - "Who can move messages to another stream": "", - "Who can move messages to another topic": "", - "Who can notify a large number of users with a wildcard mention": "", + "Who can mention this group?": "Kdo může zmínit tuto skupinu?", + "Who can move messages to another stream": "Kdo může přesunout zprávy do jiného kanálu", + "Who can move messages to another topic": "Kdo může přesunout zprávy do jiného tématu", + "Who can notify a large number of users with a wildcard mention": "Kdo může upozornit velký počet uživatelů pomocí zmínky vyjádřené zástupným symbolem", "Who can post to the stream?": "Kdo může přidat zprávu do kanálu?", - "Who can send email invitations to new users": "", - "Who can unsubscribe others from this stream?": "", - "Who can use direct messages": "", - "Who can view all other users in the organization": "", + "Who can send email invitations to new users": "Kdo může zasílat e-mailové pozvánky novým uživatelům", + "Who can unsubscribe others from this stream?": "Kdo může ostatní odhlásit z tohoto kanálu?", + "Who can use direct messages": "Kdo může používat přímé zprávy", + "Who can view all other users in the organization": "Kdo může zobrazit všechny ostatní uživatele v organizaci", "Why not start a conversation with yourself?": "Proč nezačít rozhovor sám se sebou?", "Why not start the conversation?": "Proč nezačít rozhovor?", "Word": "Slovo", @@ -1400,8 +1400,8 @@ "Working…": "Pracuji...", "Write": "Psát", "Yes, please!": "Ano, prosím!", - "Yes, save": "", - "Yes, schedule": "", + "Yes, save": "Ano, uložit", + "Yes, schedule": "Ano, zařadit", "Yes, send": "Ano, odeslat", "Yesterday": "Včera", "You": "Vy", @@ -1409,156 +1409,156 @@ "You (click to remove) reacted with {emoji_name}": "Odpověděl jste (klepněte pro odstranění) {emoji_name}", "You (click to remove), {comma_separated_usernames} and {last_username} reacted with {emoji_name}": "Vy (klepněte pro odstranění), {comma_separated_usernames} a {last_username} jste odpověděl {emoji_name}", "You and": "Vy a", - "You and ": "", + "You and ": "Vy a ", "You and {recipients}": "Vy a {recipients}", - "You are about to disable all notifications for direct messages, @‑mentions and alerts, which may cause you to miss messages that require your timely attention. If you would like to temporarily disable all desktop notifications, consider turning on \"Do not disturb\" instead.": "", - "You are not a member of any user groups.": "", - "You are not allowed to send direct messages in this organization.": "", + "You are about to disable all notifications for direct messages, @‑mentions and alerts, which may cause you to miss messages that require your timely attention. If you would like to temporarily disable all desktop notifications, consider turning on \"Do not disturb\" instead.": "Chystáte se vypnout všechna oznámení pro přímé zprávy, @‑zmínky a upozornění - sledovaní slov, což může způsobit, že vám uniknou zprávy, které vyžadují vaši včasnou pozornost. Pokud chcete dočasně vypnout všechna oznámení na ploše, zvažte místo toho možnost zapnutí funkce \"Nerušit\".", + "You are not a member of any user groups.": "Nejste členem žádné skupiny uživatelů.", + "You are not allowed to send direct messages in this organization.": "V této organizaci není povoleno posílat přímé zprávy.", "You are not currently subscribed to this stream.": "Momentálně tento kanál neodebíráte.", - "You are not subscribed to any streams.": "", - "You are not subscribed to stream ": "", + "You are not subscribed to any streams.": "Nejste přihlášeni k odběru žádných kanálů.", + "You are not subscribed to stream ": "Nemáte nastaven odběr kanálu ", "You are sending a message to a resolved topic. You can send as-is or unresolve the topic first.": "Odesíláte zprávu do vyřešeného tématu. Můžete odeslat tak, jak je, nebo nejprve zrušit vyřešení tématu.", "You are using an old version of the Zulip desktop app with known security bugs.": "Používáte starou verzi aplikace pracovní plochy Zulip se známými chybami zabezpečení.", - "You are viewing all the bots in this organization.": "", + "You are viewing all the bots in this organization.": "Zobrazujete všechny roboty v této organizaci.", "You aren't subscribed to this stream and nobody has talked about that yet!": "Neodebíráte tento kanál a nikdo o tomto ještě nemluvil!", - "You can add a new bot or manage your own bots.": "", - "You can manage your own bots.": "", + "You can add a new bot or manage your own bots.": "Můžete přidat nového robota nebo spravovat své vlastní roboty.", + "You can manage your own bots.": "Můžete spravovat své vlastní roboty.", "You can also make tables with this Markdown-ish table syntax.": "Také můžete dělat tabulky s toutoMarkdown tabulkovou syntaxí.", - "You can combine search filters as needed.": "", - "You can convert this demo organization to a permanent Zulip organization. All users and message history will be preserved.": "", + "You can combine search filters as needed.": "Vyhledávací filtry můžete podle potřeby spojovat.", + "You can convert this demo organization to a permanent Zulip organization. All users and message history will be preserved.": "Tuto ukázkovou organizaci můžete převést na trvalou organizaci Zulip. Všichni uživatelé a historie zpráv zůstanou zachovány.", "You can fully access this community and participate in conversations by creating a Zulip account in this organization.": "Vytvořením účtu Zulip v této organizaci získáte plný přístup k tomuto společenství a můžete se účastnit rozhovorů.", - "You can no longer save changes to this message.": "", - "You can only view or manage invitations that you sent.": "", + "You can no longer save changes to this message.": "Změny této zprávy již nelze uložit.", + "You can only view or manage invitations that you sent.": "Můžete zobrazit nebo spravovat pouze pozvánky, které jste odeslali.", "You can reactivate deactivated users from organization settings.": "Vypnuté uživatele můžete znovu zapnout v nastavení organizace.", - "You can use email to send messages to Zulip streams.": "", + "You can use email to send messages to Zulip streams.": "K odesílání zpráv do kanálů Zulipu můžete použít e-mail.", "You cannot create a stream with no subscribers!": "Nelze vytvořit kanál, který nemá žádné odběratele!", - "You cannot create a user group with no members!": "", + "You cannot create a user group with no members!": "Nelze vytvořit skupinu uživatelů bez členů!", "You cannot send messages to deactivated users.": "Vypnutým uživatelům nelze poslat zprávy.", - "You do not have permission to add custom emoji.": "", - "You do not have permission to add other users to streams in this organization.": "", - "You do not have permission to join this group.": "", - "You do not have permission to leave this group.": "", - "You do not have permission to move some of the messages in this topic. Contact a moderator to move all messages.": "", + "You do not have permission to add custom emoji.": "Nemáte oprávnění přidat vlastní obrázeček.", + "You do not have permission to add other users to streams in this organization.": "Nemáte oprávnění přidávat další uživatele do kanálů v této organizaci.", + "You do not have permission to join this group.": "Nemáte oprávnění připojit se k této skupině.", + "You do not have permission to leave this group.": "Nemáte povolení opustit tuto skupinu.", + "You do not have permission to move some of the messages in this topic. Contact a moderator to move all messages.": "Nemáte oprávnění přesouvat některé zprávy v tomto tématu. Pro přesunutí všech zpráv se spojte s moderátorem.", "You do not have permission to post in this stream.": "Nemáte oprávnění vkládat příspěvky do tohoto kanálu.", - "You do not have permission to resolve topics with messages older than {N, plural, one {# day} other {# days}} in this organization.": "", - "You do not have permission to resolve topics with messages older than {N, plural, one {# hour} other {# hours}} in this organization.": "", - "You do not have permission to resolve topics with messages older than {N, plural, one {# minute} other {# minutes}} in this organization.": "", - "You do not have permission to unresolve topics with messages older than {N, plural, one {# day} other {# days}} in this organization.": "", - "You do not have permission to unresolve topics with messages older than {N, plural, one {# hour} other {# hours}} in this organization.": "", - "You do not have permission to unresolve topics with messages older than {N, plural, one {# minute} other {# minutes}} in this organization.": "", - "You do not have permission to use @topic mentions in this topic.": "", - "You do not have permission to use @{stream_wildcard_mention} mentions in this stream.": "", - "You do not have permissions to generate invite links in this organization.": "", - "You do not have permissions to send email invitations in this organization.": "", - "You don't have any direct message conversations yet.": "", - "You follow this topic": "", + "You do not have permission to resolve topics with messages older than {N, plural, one {# day} other {# days}} in this organization.": "V této organizaci nemáte oprávnění řešit témata se zprávami staršími než {N, plural, one {# day} other {# days}}.", + "You do not have permission to resolve topics with messages older than {N, plural, one {# hour} other {# hours}} in this organization.": "V této organizaci nemáte oprávnění řešit témata se zprávami staršími než {N, plural, one {# hour} other {# hours}}.", + "You do not have permission to resolve topics with messages older than {N, plural, one {# minute} other {# minutes}} in this organization.": "V této organizaci nemáte oprávnění řešit témata se zprávami staršími než {N, plural, one {# minute} other {# minutes}}.", + "You do not have permission to unresolve topics with messages older than {N, plural, one {# day} other {# days}} in this organization.": "V této organizaci nemáte oprávnění rušit témata se zprávami staršími než {N, plural, one {# day} other {# days}}.", + "You do not have permission to unresolve topics with messages older than {N, plural, one {# hour} other {# hours}} in this organization.": "V této organizaci nemáte oprávnění rušit témata se zprávami staršími než {N, plural, one {# hour} other {# hours}}.", + "You do not have permission to unresolve topics with messages older than {N, plural, one {# minute} other {# minutes}} in this organization.": "V této organizaci nemáte oprávnění rušit témata se zprávami staršími než {N, plural, one {# minute} other {# minutes}}.", + "You do not have permission to use @topic mentions in this topic.": "Nemáte povolení v tomto tématu používat zmínky @topic.", + "You do not have permission to use @{stream_wildcard_mention} mentions in this stream.": "Nemáte oprávnění používat v tomto kanálu zmínky @{stream_wildcard_mention}.", + "You do not have permissions to generate invite links in this organization.": "V této organizaci nemáte oprávnění k vytváření odkazů na pozvánky.", + "You do not have permissions to send email invitations in this organization.": "V této organizaci nemáte oprávnění k odesílání e-mailových pozvánek.", + "You don't have any direct message conversations yet.": "Zatím nemáte žádné rozhovory s přímými zprávami.", + "You follow this topic": "Sledujete toto téma", "You get": "Dostanete", - "You have at least {unread_msgs_count} unread messages.": "", - "You have {scheduled_message_count, plural, =1 {1 scheduled message} other {# scheduled messages}} for this conversation.": "", - "You have {unread_msgs_count} unread messages.": "", - "You have mentions": "", + "You have at least {unread_msgs_count} unread messages.": "Máte alespoň {unread_msgs_count} nepřečtených zpráv.", + "You have {scheduled_message_count, plural, =1 {1 scheduled message} other {# scheduled messages}} for this conversation.": "Máte {scheduled_message_count, plural, =1 {1 scheduled message} other {# scheduled messages}} pro tento rozhovor.", + "You have {unread_msgs_count} unread messages.": "Máte {unread_msgs_count} nepřečtených zpráv.", + "You have mentions": "Máte zmínky", "You have muted .": "Ztlumil jste .", - "You have muted this topic": "", + "You have muted this topic": "Ztlumil jste toto téma", "You have no active bots.": "Nemáte žádné činné roboty.", - "You have no direct messages including {person} yet.": "", - "You have no direct messages with these users yet.": "", - "You have no direct messages with {person} yet.": "", - "You have no direct messages yet!": "", + "You have no direct messages including {person} yet.": "Zatím nemáte žádné přímé zprávy zahrnující {person}.", + "You have no direct messages with these users yet.": "S těmito uživateli zatím nemáte žádné přímé zprávy.", + "You have no direct messages with {person} yet.": "Zatím nemáte žádné přímé zprávy s {person}.", + "You have no direct messages yet!": "Zatím nemáte žádné přímé zprávy!", "You have no inactive bots.": "Nemáte žádné nečinné roboty.", - "You have no more unread direct messages.": "", - "You have no more unread topics.": "", - "You have no starred messages.": "", - "You have no unread messages in followed topics.": "", + "You have no more unread direct messages.": "Nemáte žádné další nepřečtené přímé zprávy.", + "You have no more unread topics.": "Nemáte žádná další nepřečtená témata.", + "You have no starred messages.": "Nemáte žádné zprávy označené hvězdičkou.", + "You have no unread messages in followed topics.": "Ve sledovaných tématech nemáte žádné nepřečtené zprávy.", "You have no unread messages!": "Nemáte žádné nepřečtené zprávy!", - "You have not configured any topics yet.": "", + "You have not configured any topics yet.": "Zatím jste nenastavil žádná témata.", "You have not muted any users yet.": "Ještě jste neztlumil/a žádné uživatele.", - "You have not sent any direct messages to yourself yet!": "", + "You have not sent any direct messages to yourself yet!": "Ještě jste si neposlali žádnou přímou zprávu!", "You have not uploaded any files.": "Ještě jste nenahrál/a žádné soubory.", - "You have unmuted this topic": "", + "You have unmuted this topic": "Zrušil jste ztlumení tohoto tématu", "You haven't been mentioned yet!": "Ještě vás nikdo nikde nezmínil!", - "You haven't received any messages sent by {person} yet.": "", - "You may want to configure your organization's login page prior to inviting users.": "", - "You may want to upload a profile picture for your organization prior to inviting users.": "", - "You may want to configure default new user settings and custom profile fields prior to inviting users.": "", - "You might be interested in recent conversations.": "", - "You must configure your email to access this feature.": "", + "You haven't received any messages sent by {person} yet.": "Zatím jste neobdrželi žádné zprávy odeslané {person}.", + "You may want to configure your organization's login page prior to inviting users.": "Možná budete chtít nastavit přihlašovací stránku vaší organizace před pozváním uživatelů.", + "You may want to upload a profile picture for your organization prior to inviting users.": "Možná budete chtít nahrát profilový obrázek přihlašovací stránku vaší organizace před pozváním uživatelů.", + "You may want to configure default new user settings and custom profile fields prior to inviting users.": "Možná budete chtít nastavit výchozí nastavení nového uživatele a vlastní profilová pole před pozváním uživatelů.", + "You might be interested in recent conversations.": "Mohly by vás zajímat nedávné rozhovory.", + "You must configure your email to access this feature.": "Pro přístup k této funkci je nutné nastavit e-mail..", "You must be an organization administrator to create a stream without subscribing.": "Abyste mohl/a vytvořit kanál bez toho, aniž byste ho také odebíral/a, musíte být správcem organizace.", - "You must configure your email to access this feature.": "", + "You must configure your email to access this feature.": "Pro přístup k této funkci je nutné nastavit e-mail.", "You need to be running Zephyr mirroring in order to send messages!": "Abyste mohl/a odesílat zprávy, musíte zajistit funkční zrcadlení Zephyr!", "You searched for:": "Hledal jste:", - "You subscribed to stream ": "", + "You subscribed to stream ": "Přihlásil jste se k odběru kanálu ", "You type": "Napíšete", - "You unsubscribed from stream ": "", - "You will automatically follow topics that you have configured to both follow and unmute.": "", - "You will get default notifications for this topic": "", - "You will not receive notifications about new messages.": "", - "You're done!": "", + "You unsubscribed from stream ": "Odhlásil jste se z odběru kanálu ", + "You will automatically follow topics that you have configured to both follow and unmute.": "Automaticky budete sledovat témata, která jste si nastavili pro jak sleodvání, tak zrušení ztlumení.", + "You will get default notifications for this topic": "Budete dostávat výchozí oznámení pro toto téma", + "You will not receive notifications about new messages.": "Nebudete dostávat oznámení o nových zprávách.", + "You're done!": "Hotovo!", "You're not subscribed to this stream. You will not be notified if other users reply to your message.": "Tento kanál neodebíráte. Pokud vám někdo odpoví na vaši zprávu, nedostanete o tom žádné upozornění.", "Your API key:": "Váš API kíč:", "Your Zulip account on has been deactivated, and you will no longer be able to log in.": "Váš účet Zulip v byl vypnut a vy se již nebudete moci přihlásit.", - "Your groups": "", - "Your message has been scheduled for {deliver_at}.": "", - "Your message is taking longer than expected to be sent. Sending…": "", - "Your message was sent to a stream you have muted.": "", - "Your message was sent to a topic you have muted.": "", - "Your password": "", - "Your question": "", + "Your groups": "Vaše skupiny", + "Your message has been scheduled for {deliver_at}.": "Vaše zpráva byla zařazena na {deliver_at}.", + "Your message is taking longer than expected to be sent. Sending…": "Odeslání vaší zprávy trvá déle, než se očekávalo. Posílá se...", + "Your message was sent to a stream you have muted.": "Vaše zpráva byla odeslána do kanálu, který máte utlumen.", + "Your message was sent to a topic you have muted.": "Vaše zpráva byla odeslána do tématu, který máte utlumen.", + "Your password": "Vaše heslo", + "Your question": "Vaše otázka", "Your status": "Váš stav", "Your time zone:": "Vaše časové pásmo:", - "Zoom to message in conversation context": "", + "Zoom to message in conversation context": "Přiblížit zprávu v prostředí rozhovoru", "Zulip": "Zulip", - "Zulip Server dev environment": "", - "Zulip Server {display_version}": "", - "Zulip Server {display_version} (modified)": "", - "Zulip Server {display_version} (patched)": "", - "Zulip lets you configure notifications for each topic. You can also automatically follow or unmute topics you start or participate in.": "", - "Zulip lets you mute topics and streams to avoid receiving notifications messages you are not interested in. Muting a stream effectively mutes all topics in that stream. You can also manually mute a topic in an unmuted stream, or unmute a topic in a muted stream. Learn more.": "", + "Zulip Server dev environment": "Vývojové prostředí serveru Zulip", + "Zulip Server {display_version}": "Server Zulip {display_version}", + "Zulip Server {display_version} (modified)": "Server Zulip {display_version} (modified)", + "Zulip Server {display_version} (patched)": "Server Zulip {display_version} (patched)", + "Zulip lets you configure notifications for each topic. You can also automatically follow or unmute topics you start or participate in.": "Zulip umožňuje nastavovat oznámení pro každé téma. Můžete také automaticky sledovat témata, která jste založili nebo se jich účastníte, nebo je zrušit.", + "Zulip lets you mute topics and streams to avoid receiving notifications messages you are not interested in. Muting a stream effectively mutes all topics in that stream. You can also manually mute a topic in an unmuted stream, or unmute a topic in a muted stream. Learn more.": "Zulip umožňuje ztlumit témata a kanály, abyste nedostávali oznámení o zprávách, které vás nezajímají. Ztlumením kanálu se účinně ztlumí všechna témata v daném kanálu. Můžete také ručně ztlumit téma v neztlumeném kanálu nebo zrušit ztlumení tématu v ztlumeném kanálu. Zjistit více.", "Zulip needs to send email to confirm users' addresses and send notifications.": "Zulip potřebuje odeslat e-mail pro ověření adresy uživatele a posílání oznámení.", "Zulip needs your permission to enable desktop notifications.": "Zulip potřebuje vaše dovolení pro povolení oznámení na ploše.", "Zulip's translations are contributed by our amazing community of volunteer translators. If you'd like to help, see the Zulip translation guidelines.": "Překlady Zulipu přispívá naše úžasné společenství dobrovolných překladatelů. Pokud chcete být nápomocni, podívejte se na Pokyny pro překlad Zulipu.", "[Configure]": "[Nastavit]", "[Quoting…]": "[Citování...]", - "acme": "", + "acme": "acme", "and {remaining_senders, plural, one {1 other} other {# others}}.": "a {remaining_senders, plural, one {1 other} other {# others}}.", - "back to streams": "", + "back to streams": "zpět na kanály", "beta": "beta", "clear": "Smazat", "cookie": "cookie", - "deactivated": "", + "deactivated": "vypnuto", "deprecated": "vyřazeno", - "direct messages with yourself": "", - "direct messages with {recipient}": "", - "does not apply to administrators": "", - "does not apply to moderators and administrators": "", - "group direct messages with {recipient}": "", - "guest": "", + "direct messages with yourself": "přímé zprávy se sebou samým", + "direct messages with {recipient}": "přímé zprávy s {recipient}", + "does not apply to administrators": "se nevztahuje na správce", + "does not apply to moderators and administrators": "se nevztahuje na moderátory a správce", + "group direct messages with {recipient}": "seskupovat přímé zprávy s {recipient}", + "guest": "host", "he/him": "on/jemu", "invisible mode off": "neviditelný režim vypnut", - "is …": "", + "is …": "je …", "leafy green vegetable": "listnatá zelená zelenina", "marketing": "Marketing", "more conversations": "další konverzace", "more topics": "další témata", "she/her": "ona/jí", "they/them": "oni/jim", - "{N, plural, one {Done! {N} message marked as read.} other {Done! {N} messages marked as read.}}": "{N, plural, one {} few {} many {} other {}}", - "{N, plural, one {Done! {N} message marked as unread.} other {Done! {N} messages marked as unread.}}": "{N, plural, one {} few {} many {} other {}}", - "{N, plural, one {User invited successfully.} other {Users invited successfully.}}": "{N, plural, one {} few {} many {} other {}}", - "{N, plural, one {Working… {N} message marked as read so far.} other {Working… {N} messages marked as read so far.}}": "{N, plural, one {} few {} many {} other {}}", - "{N, plural, one {Working… {N} message marked as unread so far.} other {Working… {N} messages marked as unread so far.}}": "{N, plural, one {} few {} many {} other {}}", + "{N, plural, one {Done! {N} message marked as read.} other {Done! {N} messages marked as read.}}": "{N, plural, one {Hotovo! {N} zpráva označena jako přečtená.} few {Hotovo! {N} zprávy označeny jako přečtené.} many {Hotovo! {N} zpráv označeno jako přečtené.} other {Hotovo! {N} zpráv označeno jako přečtené.}}", + "{N, plural, one {Done! {N} message marked as unread.} other {Done! {N} messages marked as unread.}}": "{N, plural, one {Hotovo! {N} zpráva označena jako nepřečtená.} few {Hotovo! {N} zprávy označeny jako nepřečtené.} many {Hotovo! {N} zpráv označeno jako nepřečtené.} other {Hotovo! {N} zpráv označeno jako nepřečtené.}}", + "{N, plural, one {User invited successfully.} other {Users invited successfully.}}": "{N, plural, one {Uživatel úspěšně pozván.} few {Uživatelé úspěšně pozváni.} many {Uživatelé úspěšně pozváni.} other {Uživatelé úspěšně pozváni.}}", + "{N, plural, one {Working… {N} message marked as read so far.} other {Working… {N} messages marked as read so far.}}": "{N, plural, one {Pracuje se... {N} zpráva zatím označena jako přečtená.} few {Pracuje se... {N} zprávy zatím označeny jako přečtené.} many {Pracuje se... {N} zpráv zatím označených jako přečtené.} other {Pracuje se... {N} zpráv zatím označených jako přečtené.}}", + "{N, plural, one {Working… {N} message marked as unread so far.} other {Working… {N} messages marked as unread so far.}}": "{N, plural, one {Pracuje se... {N} zpráva zatím označena jako nepřečtená.} few {Pracuje se... {N} zprávy zatím označeny jako nepřečtené.} many {Pracuje se... {N} zpráv zatím označených jako nepřečtené.} other {Pracuje se... {N} zpráv zatím označených jako nepřečtené.}}", "{comma_separated_usernames} and {last_username} reacted with {emoji_name}": "{comma_separated_usernames} a{last_username} odpověděl {emoji_name}", "{date} at {time}": "{date} v {time}", "{days_old} days ago": "před {days_old} dny", "{full_name} is typing…": "{full_name} píše…", "{hours} hours ago": "před {hours} hodinami", - "{messages_not_allowed_to_move, plural, one {# message} other {# messages}} will remain in the current topic.": "{messages_not_allowed_to_move, plural, one {} few {} many {} other {}} will remain in the current topic.", + "{messages_not_allowed_to_move, plural, one {# message} other {# messages}} will remain in the current topic.": "{messages_not_allowed_to_move, plural, one {# zpráva} few {# zprávy} many {# zpráv} other {# zpráv}} will remain in the current topic.", "{minutes} min to edit": "{minutes} minut na upravení", "{minutes} minutes ago": "před {minutes} minutami", "{modal_heading_text}": "{modal_heading_text}", - "{name} (guest)": "", + "{name} (guest)": "{name} (host)", "{num_of_people, plural, one {This message has been read by {num_of_people} person:} other {This message has been read by {num_of_people} people:}}": "{num_of_people, plural, one {Tato zpráva byla čtena {num_of_people} osobou:} few {Tato zpráva byla čtena {num_of_people} lidmi:} many {Tato zpráva byla čtena {num_of_people} lidmi:} other {Tato zpráva byla čtena {num_of_people} lidmi:}}", "{seconds} sec to edit": "{seconds} sekund na upravení", - "{server_jitsi_server_url} (default)": "", + "{server_jitsi_server_url} (default)": "{server_jitsi_server_url} (výchozí)", "{user_time} local time": "{user_time} místní čas", "{username} [said]({link_to_message}):": "{username} [said]({link_to_message}):", "{username} reacted with {emoji_name}": "{username} odpověděl {emoji_name}" diff --git a/locale/cy/LC_MESSAGES/django.po b/locale/cy/LC_MESSAGES/django.po index 086863879a2c4..2c04f571d6041 100644 --- a/locale/cy/LC_MESSAGES/django.po +++ b/locale/cy/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Tim Abbott , 2022\n" "Language-Team: Welsh (http://app.transifex.com/zulip/zulip/language/cy/)\n" @@ -63,7 +63,7 @@ msgstr "Mae'r amser cychwyn yn hwyrach na'r amser gorffen. Dechreuwch: {start}, msgid "No analytics data available. Please contact your server administrator." msgstr "Nid oes data dadansoddeg ar gael. Cysylltwch â gweinyddwr eich gweinydd." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -134,124 +134,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} yn gorffen yn {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Dull talu anhysbys. Cysylltwch â {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Aeth rhywbeth o'i le. Cysylltwch â {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Aeth rhywbeth o'i le. Ail-lwythwch y dudalen." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Aeth rhywbeth o'i le. Arhoswch ychydig eiliadau a rhoi cynnig arall arni." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Methu diweddaru'r cynllun. Mae'r cynllun wedi dod i ben a rhoi cynllun newydd yn ei le." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Methu diweddaru'r cynllun. Mae'r cynllun wedi dod i ben." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Methu diweddaru trwyddedau â llaw. Mae eich cynllun ar reoli trwydded yn awtomatig." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Mae eich cynllun eisoes ar drwyddedau {licenses} yn y cyfnod bilio cyfredol." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Ni allwch ostwng y trwyddedau yn y cyfnod bilio cyfredol." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Disgwylir i'ch cynllun adnewyddu eisoes gyda thrwyddedau {licenses_at_next_renewal}." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Dim byd i newid." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Dim cwsmer i'r sefydliad hwn!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Ni ddaethpwyd o hyd i sesiwn" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Rhaid bod yn weinyddwr biliau neu'n berchennog sefydliad" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Ni ddarganfuwyd bwriad talu" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Pasio stripe_session_id neu stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -259,33 +259,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -312,7 +316,7 @@ msgid "" msgstr "\n Os yw'r gwall hwn yn annisgwyl, gallwch chi\n cysylltwch â chefnogaeth.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Gwall gweinydd mewnol" @@ -561,31 +565,31 @@ msgstr "Sicrhewch eich bod wedi copïo'r ddolen yn gywir i'ch porwr. Os ydych ch msgid "Billing" msgstr "Bilio" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Caewch y moddol" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Canslo" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Cadarnhau" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -684,6 +688,10 @@ msgstr "Prisio addysg" msgid "View pricing" msgstr "Gweld prisio" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip am busnes" @@ -825,8 +833,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2248,7 +2254,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2257,7 +2262,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2265,7 +2270,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2281,7 +2285,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2298,15 +2301,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3351,65 +3363,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Neges(au) annilys" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Methu rhoi neges" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Disgwylir yn union un ffrwd" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Math o ddata annilys ar gyfer ffrwd" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Math data annilys ar gyfer derbynwyr" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Gall rhestrau derbynwyr gynnwys e-byst neu IDau defnyddiwr, ond nid y ddau." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Ceisiodd eich bot {bot_identity} anfon neges i ffrydio ID {stream_id}, ond nid oes ffrwd gyda'r ID hwnnw." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Ceisiodd eich bot {bot_identity} anfon neges i ffrydio {stream_name}, ond nid yw'r ffrwd honno'n bodoli. Cliciwch [yma] ({new_stream_link}) i'w greu." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Ceisiodd eich bot {bot_identity} anfon neges i ffrydio {stream_name}. Mae'r ffrwd yn bodoli ond nid oes ganddo unrhyw danysgrifwyr." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Mae angen pynciau yn y sefydliad hwn" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: Anfonodd rhaglennydd API gynnwys JSON annilys" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3426,28 +3438,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3724,7 +3736,7 @@ msgstr "Digwyddodd gwall wrth ddileu'r atodiad. Rho gynnig Arni eto'n hwyrach." msgid "Message must have recipients!" msgstr "Rhaid bod gan y neges dderbynwyr!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3903,7 +3915,7 @@ msgid "API usage exceeded rate limit" msgstr "Roedd y defnydd o API yn uwch na'r terfyn cyfradd" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON camffurfiedig" @@ -4358,7 +4370,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Nid yw Token yn bodoli" @@ -4411,7 +4423,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Nid yw'r defnyddiwr wedi'i awdurdodi ar gyfer yr ymholiad hwn" @@ -4458,7 +4470,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4571,7 +4583,7 @@ msgid "{var_name} is not a date" msgstr "nid yw {var_name} yn ddyddiad" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "nid yw {var_name} yn ddict" @@ -4609,7 +4621,7 @@ msgid "{var_name} is too large" msgstr "Mae {var_name} yn rhy fawr" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "nid yw {var_name} yn rhestr" @@ -4773,7 +4785,7 @@ msgstr "Math bot annilys" msgid "Invalid interface type" msgstr "Math rhyngwyneb annilys" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4836,46 +4848,46 @@ msgstr "Nid yw {var_name} yn allow_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable}! = {expected_value} ({value} yn anghywir)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "nid yw {var_name} yn URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "Ni all '{item}' fod yn wag." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "Nid yw '{value}' yn ddewis dilys ar gyfer '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "nid yw {var_name} yn llinyn nac yn rhestr gyfanrif" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "nid yw {var_name} yn llinyn nac yn gyfanrif" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "Nid oes gan {var_name} hyd" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} ar goll" @@ -5064,57 +5076,57 @@ msgstr "Dim ond gweinyddwyr a chymedrolwyr sefydliadau all bostio" msgid "Only organization full members can post" msgstr "Dim ond aelodau llawn y sefydliad all bostio" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Emoji Unicode" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoji personol" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Emoji ychwanegol Zulip" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Rhestr o opsiynau" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Codwr person" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Testun byr" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Testun hir" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Codwr dyddiad" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Dolen" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Cyfrif allanol" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5130,20 +5142,20 @@ msgstr "system weithredu anhysbys" msgid "An unknown browser" msgstr "Porwr anhysbys" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Eitem 'queue_id' ar goll" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Eitem 'last_event_id' ar goll" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Mae digwyddiad mwy newydd na {event_id} eisoes wedi'i docio!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Nid oedd digwyddiad {event_id} yn y ciw hwn" @@ -5315,19 +5327,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Anfonwr ar goll" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Ni chaniateir adlewyrchu gydag IDau defnyddiwr sy'n eu derbyn" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Neges annilys wedi'i adlewyrchu" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Ni chaniateir adlewyrchu Zephyr yn y sefydliad hwn" @@ -5393,26 +5405,26 @@ msgstr "Rhaid io leiaf un o'r dadleuon canlynol fod yn bresennol: emoji_name, em msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Rhaid galluogi o leiaf un dull dilysu." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Rhaid bod yn sefydliad demo." @@ -5890,11 +5902,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Subdomain annilys ar gyfer hysbysiadau gwthio bouncer" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Rhaid dilysu gydag allwedd API gweinydd Zulip dilys" @@ -5908,43 +5920,43 @@ msgstr "UUID annilys" msgid "Invalid token type" msgstr "Math tocyn annilys" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "user_id neu user_uuid ar goll" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Mae'r data allan o drefn." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/da/LC_MESSAGES/django.po b/locale/da/LC_MESSAGES/django.po index 00f9bb3fb565b..2c0bbd3cc4fe7 100644 --- a/locale/da/LC_MESSAGES/django.po +++ b/locale/da/LC_MESSAGES/django.po @@ -3,14 +3,15 @@ # This file is distributed under the same license as the PACKAGE package. # # Translators: -# Jens Hymøller, 2023 +# jens_hymoller, 2023 +# jens_hymoller, 2023 msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" -"Last-Translator: Jens Hymøller, 2023\n" +"Last-Translator: jens_hymoller, 2023\n" "Language-Team: Danish (http://app.transifex.com/zulip/zulip/language/da/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -62,7 +63,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -133,124 +134,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -258,33 +259,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -311,7 +316,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -560,31 +565,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Annuller" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Bekræft" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -683,6 +688,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -824,8 +833,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -920,7 +927,7 @@ msgstr "" #: templates/zerver/change_email_address_visibility_modal.html:15 msgid "Who can access your email address" -msgstr "" +msgstr "Hvem kan se din e-mail adresse" #: templates/zerver/change_email_address_visibility_modal.html:22 #, python-format @@ -2247,7 +2254,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2256,7 +2262,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2264,7 +2270,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2280,7 +2285,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2297,15 +2301,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2397,7 +2410,7 @@ msgstr "" #: templates/zerver/footer.html:14 msgid "Integrations" -msgstr "" +msgstr " Integrationer" #: templates/zerver/footer.html:15 msgid "Desktop & mobile apps" @@ -2521,7 +2534,7 @@ msgstr "" #: templates/zerver/footer.html:95 msgid "Support Zulip" -msgstr "" +msgstr "Støt Zulip" #: templates/zerver/footer.html:98 msgid "X (Twitter)" @@ -3264,12 +3277,12 @@ msgstr "" #: zerver/actions/message_edit.py:170 #, python-brace-format msgid "{user} has marked this topic as resolved." -msgstr "" +msgstr "{user} har markeret dette emne som løst." #: zerver/actions/message_edit.py:172 #, python-brace-format msgid "{user} has marked this topic as unresolved." -msgstr "" +msgstr "{user} har markeret dette emne som uløst." #: zerver/actions/message_edit.py:1005 #, python-brace-format @@ -3350,65 +3363,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Emne skal udfyldes" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3425,28 +3438,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3723,7 +3736,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "Beskeder skal have modtagere!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3902,7 +3915,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4357,7 +4370,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4410,7 +4423,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4457,7 +4470,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4570,7 +4583,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4608,7 +4621,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4772,7 +4785,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4835,46 +4848,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -4910,7 +4923,7 @@ msgstr "" #: zerver/models.py:556 msgid "stream events" -msgstr "Kanelhændelse" +msgstr "kanalhistorik" #: zerver/models.py:687 msgid "Available on Zulip Cloud Standard. Upgrade to access." @@ -5063,59 +5076,59 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" -msgstr "" +msgstr "Pronomener" #: zerver/signals.py:105 msgid "Unknown IP address" @@ -5129,20 +5142,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5314,19 +5327,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5392,26 +5405,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5559,7 +5572,7 @@ msgstr "nye kanaler" #: zerver/views/streams.py:807 #, python-brace-format msgid "**{policy}** stream created by {user_name}. **Description:**" -msgstr "" +msgstr "**{policy}** kanal oprettet af {user_name}. **Beskrivelse:**" #: zerver/views/streams.py:1070 #, python-brace-format @@ -5889,11 +5902,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5907,43 +5920,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/da/translations.json b/locale/da/translations.json index 155db83152806..f2407a6078297 100644 --- a/locale/da/translations.json +++ b/locale/da/translations.json @@ -49,13 +49,13 @@ "A deactivated bot cannot send messages, access data, or take any other action.": "", "A deactivated emoji will remain visible in existing messages and emoji reactions, but cannot be used on new messages.": "", "A language is marked as 100% translated only if every string in the web, desktop, and mobile apps is translated, including administrative UI and error messages.": "", - "A poll must be an entire message.": "", + "A poll must be an entire message.": "En afstemning opretter en ny besked.", "A stream needs to have a name": "Kanalens navn kan ikke være tomt", "A stream with this name already exists": "Kanal eksisterer i forvejen", "A user group needs to have a name": "", "A user group with this name already exists": "", "A wide image (200×25 pixels) for the upper left corner of the app.": "", - "API documentation": "", + "API documentation": "API Dokumentation", "API key": "API-nøgler", "About Zulip": "Om Zulip", "Account": "", @@ -65,7 +65,7 @@ "Active": "Aktiv", "Active an hour ago": "Aktiv for en time siden", "Active bots": "", - "Active more than 2 weeks ago": "", + "Active more than 2 weeks ago": "Aktiv for mere end 2 uger siden", "Active now": "Aktiv nu", "Active users": "Aktive brugere", "Active yesterday": "Aktiv i går", @@ -98,7 +98,7 @@ "Add members. Use usergroup or #streamname to bulk add members.": "", "Add one or more users": "Tilføj én eller flere brugere", "Add option": "Tilføj valg", - "Add poll": "", + "Add poll": "Tilføj afstemning", "Add question": "", "Add stream": "Tilføj kanal", "Add streams": "Tilføj kanal", @@ -130,13 +130,13 @@ "Alert words allow you to be notified as if you were @-mentioned when certain words or phrases are used in Zulip. Alert words are not case sensitive.": "", "Alerted messages": "", "All direct messages": "Alle private beskeder", - "All groups": "", + "All groups": "Alle grupper", "All messages": "Alle beskeder", "All messages including muted streams": "", "All streams": "Alle kanaler", "All time": "Altid", "All topics": "Alle emner", - "All unmuted topics": "", + "All unmuted topics": "Alle emner med lyd", "All unread messages": "", "All users will need to log in again at your new organization URL.": "", "Allow creating web-public streams (visible to anyone on the Internet)": "", @@ -158,7 +158,7 @@ "Announce new stream in": "", "Any organization administrator can conduct an export.": "", "Any time": "", - "Anyone can add more options after the poll is posted.": "", + "Anyone can add more options after the poll is posted.": "Alle kan filføje flere valg efter afstemmningen er slået op.", "April": "April", "Archive ?": "", "Archive stream": "", @@ -181,14 +181,14 @@ "Are you sure you want to unstar all messages in ? This action cannot be undone.": "", "Are you sure you want to unstar all starred messages? This action cannot be undone.": "", "Ask me later": "", - "At the office": "", - "Audible": "", + "At the office": "På kontoret", + "Audible": "Lyd", "Audible desktop notifications": "Afspil lyd ved popup", "August": "August", "Authentication methods": "", "Author": "", "Automated messages and emails": "", - "Automatic": "", + "Automatic": "Automatisk", "Automatic (follows system settings)": "", "Automatically follow topics": "", "Automatically follow topics where I'm mentioned": "", @@ -216,7 +216,7 @@ "Browse {can_subscribe_stream_count} more streams": "Gennemse {can_subscribe_stream_count} andre kanaler", "Bulleted list": "Punktliste", "Business": "", - "Busy": "", + "Busy": "Optaget", "By deactivating your account, you will be logged out immediately.": "", "Call provider": "", "Cancel": "Annuller", @@ -255,16 +255,16 @@ "Close": "Luk", "Close modal": "", "Close this dialog window": "", - "Code": "", + "Code": "Kode", "Code playgrounds": "", "Code playgrounds are interactive in-browser development environments, such as replit, that are designed to make it convenient to edit and debug code. Zulip code blocks that are tagged with a programming language will have a button visible on hover that allows users to open the code block on the code playground site.": "", - "Collapse compose": "", - "Collapse direct messages": "", - "Collapse message": "", - "Collapse views": "", - "Collapse/show selected message": "", + "Collapse compose": "Skjul", + "Collapse direct messages": "Skjul direkte beskeder", + "Collapse message": "Skjul besked", + "Collapse views": "Skjul visninger", + "Collapse/show selected message": "Skjul/vis valgte besked", "Community": "", - "Commuting": "", + "Commuting": "På vejen", "Compact": "Kompakt", "Complete": "", "Complete your organization profile, which is displayed on your organization's registration and login pages.": "", @@ -325,7 +325,7 @@ "DMs and mentions": "", "DMs, mentions, and alerts": "Private beskeder, henvendelser, og alarmer", "DMs, mentions, and followed topics": "", - "Dark": "", + "Dark": "Mørk", "Dark theme": "", "Dark theme logo": "", "Data exports": "", @@ -356,7 +356,7 @@ "Delete": "Slet", "Delete alert word": "", "Delete all drafts": "", - "Delete all selected drafts": "", + "Delete all selected drafts": "Slet alle valgte kladder", "Delete code playground?": "", "Delete custom profile field?": "", "Delete data export?": "", @@ -380,7 +380,7 @@ "Deleted successfully!": "", "Deleting a message permanently removes it for everyone.": "", "Deleting a topic will immediately remove it and its messages for everyone. Other users may find this confusing, especially if they had received an email or push notification related to the deleted messages.": "", - "Demote inactive streams": "", + "Demote inactive streams": "Flyt inaktive emner til bunden", "Dense mode": "", "Depending on the size of your organization, an export can take anywhere from seconds to an hour.": "", "Deprecation notice": "", @@ -496,10 +496,10 @@ "Everyone sees this in their own time zone.": "", "Exclude messages with topic .": "", "Exit search": "", - "Expand compose": "", - "Expand direct messages": "", - "Expand message": "Udvid besked", - "Expand views": "", + "Expand compose": "Udvid", + "Expand direct messages": "Vis direkte beskeder", + "Expand message": "Vis besked", + "Expand views": "Udvid visninger", "Expires at": "", "Expires on {date} at {time}": "", "Export failed": "", @@ -532,7 +532,7 @@ "Filter default streams": "Filtrer standardkanaler", "Filter emoji": "", "Filter exports": "", - "Filter groups": "", + "Filter groups": "Filtrer grupper", "Filter invites": "", "Filter linkifiers": "", "Filter members": "", @@ -581,8 +581,8 @@ "Got it!": "", "Government": "", "Grant Zulip the Kerberos tickets needed to run your Zephyr mirror via Webathena": "", - "Group permissions": "", - "Group settings": "", + "Group permissions": "Gruppe rettigheder", + "Group settings": "Gruppeindstillinger", "Guest": "Gæst", "Guests": "", "Guests cannot edit custom emoji.": "", @@ -604,7 +604,7 @@ "If you haven't updated your name, it's a good idea to do so before inviting other users to join you!": "", "Ignored deactivated users:": "", "Image": "", - "In a meeting": "", + "In a meeting": "I møde", "In muted streams, stream notification settings apply only to unmuted topics.": "", "Inactive": "Inaktiv", "Inactive bots": "", @@ -613,12 +613,12 @@ "Include content of direct messages in desktop notifications": "", "Include message content in message notification emails": "", "Include organization name in subject of message notification emails": "", - "Includes muted streams and topics": "", + "Includes muted streams and topics": "Inkluderer emner og kanaler med slukket lyd", "Initiate a search": "", "Insert new line": "", - "Integration": "", + "Integration": " integration", "Integration URL will appear here.": "", - "Integrations": "", + "Integrations": " Integrationer", "Interface": "", "Invalid URL": "", "Invalid stream ID": "", @@ -675,11 +675,11 @@ "Leave group {name}": "", "Leave {group_name}": "", "Let others see when I've read messages": "", - "Let recipients see when I'm typing direct messages": "", - "Let recipients see when I'm typing messages in streams": "", - "Let recipients see when a user is typing direct messages": "", - "Let recipients see when a user is typing stream messages": "", - "Light": "", + "Let recipients see when I'm typing direct messages": "Lad modtager se når jeg skriver private beskeder", + "Let recipients see when I'm typing messages in streams": "Lad modtagerer se når jeg skriver i kanaler", + "Let recipients see when a user is typing direct messages": "Lad modtagere se når en bruger skriver en privat besked", + "Let recipients see when a user is typing stream messages": "Lad modtagere se når en bruger skriver kanal beskeder", + "Light": "Lys", "Light theme": "", "Light theme logo": "", "Link": "", @@ -744,7 +744,7 @@ "Mobile": "Mobiltelefon", "Mobile message notifications": "Mobiltelefon notifikationer", "Mobile notifications": "Popup på mobiltelefon", - "Mobile push notifications are not enabled on this server.": "", + "Mobile push notifications are not enabled on this server.": "Mobilpush beskeder er ikke aktiveret på denne server.", "Mobile push notifications are not enabled on this server. Learn more": "", "Moderator": "Moderator", "Moderators": "Moderatorer", @@ -774,30 +774,30 @@ "Muted users": "Lydløse brugere", "Name": "Navn", "Name changes are disabled in this organization. Contact an administrator to change your name.": "", - "Narrow to all direct messages": "Begræns til alle private beskeder", + "Narrow to all direct messages": "Vis alle private beskeder", "Narrow to all unmuted messages": "", "Narrow to current compose box recipient": "", "Narrow to direct messages that include .": "", - "Narrow to direct messages with .": "", - "Narrow to direct messages.": "", + "Narrow to direct messages with .": "Vis direkte beskeder med .", + "Narrow to direct messages.": "Vis direkte beskeder.", "Narrow to just message ID .": "", - "Narrow to messages containing images.": "", - "Narrow to messages containing links.": "", - "Narrow to messages containing uploads.": "", - "Narrow to messages in resolved topics.": "", - "Narrow to messages on stream .": "", - "Narrow to messages sent by .": "", - "Narrow to messages sent by you.": "", - "Narrow to messages that mention you.": "", + "Narrow to messages containing images.": "Vis beskeder som indeholder billeder.", + "Narrow to messages containing links.": "Vis beskeder som indeholder links.", + "Narrow to messages containing uploads.": "Vis beskeder som indeholder uploads.", + "Narrow to messages in resolved topics.": "Vis beskeder i løste emner.", + "Narrow to messages on stream .": "Vis beskeder sendt i kanal .", + "Narrow to messages sent by .": "Vis beskeder sendt af .", + "Narrow to messages sent by you.": "Vis beskeder sendt af dig.", + "Narrow to messages that mention you.": "Vis beskeder som nævner dig.", "Narrow to messages with alert words.": "", - "Narrow to messages with topic .": "", - "Narrow to next unread direct message": "", - "Narrow to next unread topic": "", - "Narrow to starred messages.": "", + "Narrow to messages with topic .": "Vis direkte beskeder med .", + "Narrow to next unread direct message": "Vis næste ulæste besked", + "Narrow to next unread topic": "Vis næste ulæste emne", + "Narrow to starred messages.": "Vis favoritbeskeder.", "Narrow to stream from topic view": "", "Narrow to topic or DM conversation": "", - "Narrow to unread messages.": "", - "Narrow to {message_recipient}": "", + "Narrow to unread messages.": "Vis ulæste beskeder", + "Narrow to {message_recipient}": "Vis {message_recipient}", "Narrowing": "", "Navigation": "", "Never": "Aldrig", @@ -854,7 +854,7 @@ "No stream subscribers match your current filter.": "", "No stream subscriptions.": "", "No streams": "Ingen kanaler", - "No topics are marked as resolved.": "", + "No topics are marked as resolved.": "Ingen emner er markeret som løst.", "No topics match your current filter.": "Ingen emner matcher dit nuværende filter.", "No uploaded files match your current filter.": "", "No user group subscriptions.": "", @@ -879,7 +879,7 @@ "Notify recipients": "Notificer modtagere", "Notify stream": "Notificer kanal", "Notify this user by email?": "", - "Notify topic": "", + "Notify topic": "Notificer emne", "November": "November", "Now following {stream_topic}.": "", "Numbered list": "Nummeret liste", @@ -900,7 +900,7 @@ "Only stream members can add users to a private stream.": "", "Only subscribers can access or join private streams, so you will lose access to this stream if you convert it to a private stream while not subscribed to it.": "", "Only subscribers to this stream can edit stream permissions.": "", - "Only topics you follow": "", + "Only topics you follow": "Kun emner du følger", "Open": "Åbn", "Open help menu": "Åbn hjælpemenu", "Open message menu": "Åbn beskedmenu", @@ -909,7 +909,7 @@ "Open-source project": "", "Option already present.": "Valg allerede tilstede.", "Optional": "Valgfri", - "Options": "", + "Options": "Valgmuligheder", "Organization": "Organisation", "Organization URL": "Organization URL", "Organization administrators": "", @@ -932,7 +932,7 @@ "Other permissions": "", "Other settings": "", "Other users in this Zulip organization will be able to see this email address.": "", - "Out sick": "", + "Out sick": "Syg", "Outgoing webhook message format": "", "Override default emoji?": "", "Owner": "Ejer", @@ -973,18 +973,18 @@ "Prevent users from changing their name": "", "Preview": "Forhåndsvisning", "Preview organization profile": "", - "Preview profile": "", - "Previous message": "", + "Preview profile": "Gennemse profil", + "Previous message": "Forudgående besked", "Privacy": "", "Privacy settings": "", "Private streams cannot be default streams for new users.": "Private kanaler kan ikke være standardkanaler for nye brugere.", "Private, protected history": "Privat, beskyttet historik", "Private, shared history": "Privat, fuld adgang til historik", "Profile": "Profil", - "Pronouns": "", + "Pronouns": "Pronomener", "Public": "Offentlig", - "Question": "", - "Quote": "", + "Question": "Spørgsmål", + "Quote": "Citér", "Quote and reply": "Citér og besvar", "Quote and reply to message": "Citér og besvar besked", "Quoted original email (in replies)": "Citeret original email (i svar)", @@ -1005,13 +1005,13 @@ "Removed successfully.": "", "Rename topic": "Omdøb emne", "Rename topic to:": "Omdøbe emne til:", - "Reply @-mentioning sender": "", + "Reply @-mentioning sender": "Svar og @-nævn afsender", "Reply directly to sender": "Svar direkte til afsender", - "Reply mentioning bot": "", - "Reply mentioning user": "", - "Reply to message": "", + "Reply mentioning bot": "Svar nævnende bot", + "Reply mentioning user": "Svar nævnende bruger", + "Reply to message": "Svar på besked", "Reply to selected conversation": "Svar til valgte samtale", - "Reply to selected message": "", + "Reply to selected message": "Svar på valgte besked", "Request education pricing": "", "Request sponsorship": "", "Requesting user": "", @@ -1031,41 +1031,41 @@ "Revoke invitation link": "", "Revoke invitation to {email}": "", "Role": "Rolle", - "SAVING": "", + "SAVING": "GEMMER", "STREAMS": "KANALER", "Saturday": "Lørdag", "Save": "Save", "Save changes": "Gem ændringer", "Save failed": "", "Saved": "Gemt", - "Saved as draft": "", + "Saved as draft": "Gemt som kladde", "Saved. Please reload for the change to take effect.": "", - "Saving": "", + "Saving": "Gemmer", "Schedule for {deliver_at}": "", - "Schedule for {formatted_send_later_time}": "", + "Schedule for {formatted_send_later_time}": "Planlagt til {formatted_send_later_time}", "Schedule message": "Planlæg afsendelse", "Scheduled messages": "Planlagte beskeder", - "Scroll down": "", - "Scroll down to view your message.": "", + "Scroll down": "Scroll ned", + "Scroll down to view your message.": "Scroll ned for at vise din besked.", "Scroll through streams": "Bladr gennem kanaler", - "Scroll to bottom": "", - "Scroll up": "", + "Scroll to bottom": "Scroll til bunden", + "Scroll up": "Scroll op", "Search": "Søg", "Search GIFs": "", "Search all public streams in the organization.": "", "Search filters": "", "Search for in the topic or message content.": "", - "Search people": "Søg mennesker", - "Search results": "", + "Search people": "Søg brugere", + "Search results": "Søgeresultater", "See how to configure email.": "", "Select a stream": "Vælg en kanal", - "Select a stream below or change topic name.": "", + "Select a stream below or change topic name.": "Vælg kanal herunder eller skift navn på emnet.", "Select a stream to subscribe": "", - "Select all drafts": "", + "Select all drafts": "Vælg alle kladder", "Select an integration": "", "Select draft": "", - "Select emoji": "", - "Select language": "", + "Select emoji": "Vælg emoji", + "Select language": "Vælg sprog", "Select stream": "", "Send": "Send", "Send all notifications to a single topic": "", @@ -1079,20 +1079,20 @@ "Send emails introducing Zulip to new users": "", "Send me Zulip's low-traffic newsletter (a few emails a year)": "", "Send message": "Send besked", - "Send mobile notifications even if I'm online": "", + "Send mobile notifications even if I'm online": "Send mobil notifikationer selv hvis jeg er online", "Send mobile notifications even if user is online": "", - "Send options": "", + "Send options": "Indstillinger for afsendelse", "Send weekly digest emails to inactive users": "", "Sent!": "", - "Sent! Your message is outside your current narrow.": "", - "Sent! Your recent message is outside the current search.": "", + "Sent! Your message is outside your current narrow.": "Beskeden er sendt men er udenfor din valgte visning.", + "Sent! Your recent message is outside the current search.": "Beskeden er sendt men er udenfor din nuværende søgning.", "September": "September", "Set a status": "", "Set status": "", "Set up two factor authentication": "", "Settings": "Indstillinger", "Setup": "", - "Several people are typing…": "", + "Several people are typing…": "Flere brugere skriver...", "Show API key": "", "Show counts for starred messages": "", "Show fewer": "", @@ -1122,14 +1122,14 @@ "Sort by unread message count": "", "Spoiler": "", "Sponsorship request pending": "", - "Standard view": "", + "Standard view": "Standardvisning", "Star": "", "Star selected message": "", "Star this message": "Gem som favorit", "Starred messages": "Favoritbeskeder", "Start a new topic or select one from the list.": "Start et nyt emne eller vælg et fra listen.", "Start export of public data": "", - "Start new conversation": "Start samtale", + "Start new conversation": "Nyt emne", "Status": "", "Stream": "Kanal", "Stream color": "Kanalfarve", @@ -1143,7 +1143,7 @@ "Stream successfully created!": "Oprettede kanal!", "Streams": "Kanaler", "Streams they should join": "", - "Strikethrough": "", + "Strikethrough": "Gennemstreget", "Subject": "Emne", "Subscribe": "Tilmeld", "Subscribe them": "", @@ -1158,7 +1158,7 @@ "Successfully subscribed user:": "", "Successfully subscribed users:": "", "Sunday": "Søndag", - "Support Zulip": "", + "Support Zulip": "Støt Zulip", "Switch between tabs": "", "Switch to dark theme": "", "Switch to light theme": "", @@ -1243,7 +1243,7 @@ "Toggle subscription": "", "Toggle the gear menu": "", "Toggle topic mute": "", - "Tomorrow at {time}": "", + "Tomorrow at {time}": "I morgen kl {time}", "Topic": "Emne", "Topic muted": "Emne lydløst", "Topic notifications": "Emnenotifikationer", @@ -1318,7 +1318,7 @@ "User group creation": "", "User group description": "", "User group name": "", - "User group settings": "", + "User group settings": "Indstillinger for brugergruppe", "User groups": "Brugergrupper", "User identity": "", "User is already not subscribed.": "", @@ -1331,10 +1331,10 @@ "Users can always disable their personal read receipts.": "", "Users join as": "", "VIEWS": "VISNINGER", - "Vacationing": "", + "Vacationing": "På ferie", "Version {zulip_version}": "", "View all streams": "Vis alle kanaler", - "View all user groups": "", + "View all user groups": "Vis alle brugergrupper", "View direct messages": "Vis private beskeder", "View drafts": "Vis kladder", "View edit and move history": "", @@ -1347,15 +1347,15 @@ "View messages sent": "Vis sendte beskeder", "View messages with yourself": "Vis beskeder til dig selv", "View profile": "Vis profil", - "View read receipts": "", + "View read receipts": "Vis hvem der har læst", "View recent conversations": "Vis seneste samtaler", "View scheduled messages": "Vis planlagte beskeder", "View stream": "Vis kanal", "View stream messages": "Vis kanalbeskeder", "View streams": "Vis kanaler", - "View user card": "", + "View user card": "Vis brugerens kort", "View your profile": "Vis din profil", - "Visual": "", + "Visual": "Visuel", "Visual desktop notifications": "Popup på skrivebordet", "Waiting period (days)": "Venteperiode (days)", "Waiting period before new members turn into full members": "", @@ -1374,7 +1374,7 @@ "Which parts of the email should be included in the Zulip message sent to this stream?": "", "Who can access the stream?": "Hvem kan tilgå kanalen?", "Who can access user's email address": "", - "Who can access your email address": "", + "Who can access your email address": "Hvem kan se din e-mail adresse", "Who can add bots": "", "Who can add custom emoji": "", "Who can add users to streams": "", @@ -1384,7 +1384,7 @@ "Who can create reusable invitation links": "", "Who can create web-public streams": "", "Who can delete their own messages": "", - "Who can mention this group?": "", + "Who can mention this group?": "Hvem kan nævne denne gruppe?", "Who can move messages to another stream": "", "Who can move messages to another topic": "", "Who can notify a large number of users with a wildcard mention": "", @@ -1417,7 +1417,7 @@ "You are not currently subscribed to this stream.": "Du er ikke tilmeldt denne kanal.", "You are not subscribed to any streams.": "Du har endnu ikke tilmeldt nogen kanaler", "You are not subscribed to stream ": "Du er ikke tilmeldt kanal ", - "You are sending a message to a resolved topic. You can send as-is or unresolve the topic first.": "", + "You are sending a message to a resolved topic. You can send as-is or unresolve the topic first.": "Du sender en besked til et emne som er løst. Du kan enten sende til emnet som-det-er eller markere det som uløst først.", "You are using an old version of the Zulip desktop app with known security bugs.": "", "You are viewing all the bots in this organization.": "", "You aren't subscribed to this stream and nobody has talked about that yet!": "", @@ -1517,7 +1517,7 @@ "Zulip needs your permission to enable desktop notifications.": "Zulip skal have din tilladelse for at aktivere notifikationer på skrivebordet.", "Zulip's translations are contributed by our amazing community of volunteer translators. If you'd like to help, see the Zulip translation guidelines.": "", "[Configure]": "", - "[Quoting…]": "", + "[Quoting…]": "[Citérer…]", "acme": "", "and {remaining_senders, plural, one {1 other} other {# others}}.": "", "back to streams": "tilbage til kanaler", @@ -1530,7 +1530,7 @@ "direct messages with {recipient}": "", "does not apply to administrators": "", "does not apply to moderators and administrators": "", - "group direct messages with {recipient}": "", + "group direct messages with {recipient}": "private gruppebeskeder med {recipient}", "guest": "", "he/him": "han/ham", "invisible mode off": "", @@ -1547,7 +1547,7 @@ "{N, plural, one {Working… {N} message marked as read so far.} other {Working… {N} messages marked as read so far.}}": "{N, plural, one {} other {}}", "{N, plural, one {Working… {N} message marked as unread so far.} other {Working… {N} messages marked as unread so far.}}": "{N, plural, one {} other {}}", "{comma_separated_usernames} and {last_username} reacted with {emoji_name}": "", - "{date} at {time}": "", + "{date} at {time}": "{date} kl {time}", "{days_old} days ago": "{days_old} dage siden", "{full_name} is typing…": "", "{hours} hours ago": "{hours} timer siden", diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index f8663b736d193..a472ca3e0ecfc 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -41,7 +41,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Viktor, 2023\n" "Language-Team: German (http://app.transifex.com/zulip/zulip/language/de/)\n" @@ -95,7 +95,7 @@ msgstr "Die Startzeit ist später als die Endzeit. Start: {start}, Ende: {end}" msgid "No analytics data available. Please contact your server administrator." msgstr "Keine Analysedaten verfügbar. Bitte kontaktiere den:die Server-Administrator:in." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -166,124 +166,124 @@ msgstr "Registrierungen sind deaktiviert" msgid "Invalid remote server." msgstr "Ungültiger Remote-Server." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "Du musst Lizenzen für alle aktiven Nutzer:innen in deiner Organisation erwerben (mindestens {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Rechnungen mit mehr als {max_licenses} Lizenzen können nicht von dieser Seite aus verarbeitet werden. Um das Upgrade zu vollenden, kontaktiere bitte {email}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "Keine Zahlungsmethode hinterlegt." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} endet in {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Unbekannte Zahlungsmethode. Bitte kontaktiere {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Etwas hat nicht geklappt. Bitte {email} kontaktieren." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Etwas ist schief gelaufen. Bitte lade die Seite erneut." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Etwas ist schief gelaufen. Bitte warte ein paar Sekunden und versuche es erneut." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "Bitte füge vor Beginn deiner kostenlosen Testphase eine Kreditkarte hinzu." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "Bitte füge eine Kreditkarte hinzu, um ein Upgrade zu planen." -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Konnte den Tarif nicht erneuern. Das Angebot ist abgelaufen und wurde durch ein neues ersetzt." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Konnte den Tarif nicht erneuern. Das Angebot wurde beendet." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "Kann Lizenzen im aktuellen Abrechnungszeitraum für das kostenlose Testabonnement nicht aktualisieren." -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Konnte die Lizenzen nicht manuell erneuern. Dein Tarif sieht ein automatisches Lizenzmanagement vor." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Dein Tarif hat bereits {licenses} Lizenzen im aktuellen Abrechnungszeitraum." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Du kannst die Anzahl der Lizenzen im aktuellen Abrechnungszeitraum nicht reduzieren." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "Für einen Tarif, der herabgestuft wird, können Lizenzen für den nächsten Abrechnungszyklus nicht geändert werden." -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Dein Tarif sieht bereits vor, mit {licenses_at_next_renewal} verlängert zu werden." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Es gibt nichts zu ändern." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Du bist kein Kunde dieser Organisation!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Session nicht gefunden" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Muss ein:e Rechnungs-Administrator:in oder Eigentümer:in der Organisation sein." -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Keine Zahlabsicht gefunden" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Übergib stripe_session_id oder stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -291,33 +291,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "Der Antrag deiner Organisation auf gesponsertes Hosting wurde genehmigt! Ihr wurdet kostenlos auf {plan_name} hochgestuft. {emoji}\n\nWenn ihr {begin_link}Zulip als Sponsor auf eurer Website auflisten könntet{end_link}, wären wir euch sehr dankbar!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "Parameter 'confirmed' ist erforderlich" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "Zugriffstoken für die Abrechnung ist abgelaufen." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "Zugriffstoken für die Abrechnung ist ungültig." -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." -msgstr "" +msgstr "Nutzeraccount existiert noch nicht." -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "Du musst den Nutzungsbedingungen zustimmen, um fortzufahren." -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." -msgstr "" +msgstr "Diese zulip_org_id ist nicht im Abrechnungssystem von Zulip registriert." -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." -msgstr "" +msgstr "Ungültiger zulip_org_key für diese zulip_org_id." -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "Deine Server-Registrierung wurde deaktiviert." @@ -344,7 +348,7 @@ msgid "" msgstr "\n Sollte dieser Fehler unerwartet kommen, bitte\n den Support kontaktieren.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Interner Serverfehler" @@ -593,31 +597,31 @@ msgstr "Vergewissere dich, dass du den Link korrekt in deinen Browser kopiert ha msgid "Billing" msgstr "Abrechnung" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Fenster schließen" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Abbrechen" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "Herabstufen" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Bestätigen" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "Abbrechen" @@ -716,6 +720,10 @@ msgstr "Preise für Ausbildung" msgid "View pricing" msgstr "Preise ansehen" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "Serverregistrierung deaktivieren?" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip für Business" @@ -857,8 +865,6 @@ msgstr "Du hast bereits ein Konto?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2270,53 +2276,50 @@ msgstr "Klicke den Button unten, um deine Organisation zu reaktivieren." msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "Entweder du oder jemand in deinem Auftrag hat einen Anmeldelink angefordert, um den Zulip-Tarif für %(remote_server_hostname)s zu verwalten." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:12 msgid "" "\n" " Click the button below to log in.\n" " " -msgstr "" +msgstr "\n Klicke den Button unten, um dich anzumelden.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" " This link will expire in %(validity_in_hours)s hours.\n" " " -msgstr "" +msgstr "\n Dieser Link läuft in %(validity_in_hours)s Stunden ab.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Fragen? Erfahre mehr oder kontaktiere %(billing_contact_email)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" -msgstr "" +msgstr "Bei der Zulip-Tarifverwaltung anmelden." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:1 #, python-format msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "Entweder du oder jemand in deinem Auftrag hat einen Anmeldelink angefordert, um den Zulip-Tarif für %(remote_server_hostname)s zu verwalten." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 msgid "Click the link below to log in." -msgstr "" +msgstr "Klicke auf den Link unten, um dich anzumelden." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." -msgstr "" +msgstr "Dieser Link läuft in %(validity_in_hours)s Stunden ab." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:8 #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:6 @@ -2324,22 +2327,31 @@ msgstr "" msgid "" "Questions? Learn more at %(billing_help_link)s or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Fragen? Erfahre mehr unter %(billing_help_link)s oder kontaktiere %(billing_contact_email)s." #: templates/zerver/emails/remote_realm_billing_confirm_login.html:9 #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " -msgstr "" +msgstr "\n Klicke auf den Link unten, um deine E-Mail zu bestätigen und dich bei der Zulip-Tarifverwaltung für %(remote_realm_host)s anzumelden." + +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "Bestätigen und anmelden" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "E-Mail für die Zulip-Tarifverwaltung bestätigen" #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." -msgstr "" +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." +msgstr "Klicke auf den Link unten, um deine E-Mail zu bestätigen und dich bei der Zulip-Tarifverwaltung für %(remote_realm_host)s anzumelden." #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 #, python-format @@ -3150,7 +3162,7 @@ msgstr "\n Du kannst auch die contact Zulip support\n" " for assistance in resolving this issue.\n" " " -msgstr "" +msgstr "\n Deine Zulip-Organisation ist einer anderen \n Zulip-Serverinstallation zugeordnet.\n\n Bitte kontaktiere den Zulip-Support,\n um Hilfe bei der Lösung dieses Problems zu erhalten.\n " #: zerver/actions/create_user.py:98 msgid "signups" @@ -3383,65 +3395,65 @@ msgstr "Ungültige Nachrichten-Flag-Operation: '{operation}'" msgid "Invalid message(s)" msgstr "Ungültige Nachricht(en)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Kann Nachricht nicht rendern" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Erwarte genau einen Stream" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Ungültiger Datentyp für Stream" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Ungültiger Datentyp für Empfänger:innen" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Empfängerlisten können E-Mail-Adressen oder Nutzer-IDs enthalten, aber nicht beides." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Dein Bot {bot_identity} hat versucht, eine Nachricht an den Stream mit der ID {stream_id} zu senden, aber es gibt keinen Stream mit dieser ID." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Dein Bot {bot_identity} hat versucht, eine Nachricht an den Stream {stream_name} zu senden, aber dieser Stream existiert nicht. Klicke [hier]({new_stream_link}), um ihn zu erstellen." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Dein Bot {bot_identity} hat versucht, eine Nachricht an den Stream {stream_name} zu senden. Der Stream existiert, hat aber keine Abonnent:innen." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Direktnachrichten sind in dieser Organisation deaktiviert." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "Du bist nicht berechtigt, auf einige der Empfänger:innen zuzugreifen." -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "In dieser Organisation müssen Themen angegeben werden." -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: Der API-Programmierer hat ungültiges JSON gesendet" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3458,28 +3470,28 @@ msgstr "Die geordnete Liste darf keine doppelten Linkifier enthalten" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "Die geordnete Liste muss alle vorhandenen Linkifier genau einmal aufführen" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "Die geplante Nachricht wurde bereits gesendet" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "Die geplante Sendezeit muss in der Zukunft liegen." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "Die Nachricht konnte zum geplanten Zeitpunkt nicht gesendet werden." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "Die für {delivery_datetime} geplante Nachricht wurde aufgrund des folgenden Fehlers nicht gesendet:" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "[Geplante Nachrichten anzeigen](#scheduled)" @@ -3756,7 +3768,7 @@ msgstr "Beim Löschen des Anhangs ist ein Fehler aufgetreten. Bitte versuche es msgid "Message must have recipients!" msgstr "Nachricht muss Empfänger:innen haben!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "{service_name} Zusammenfassung" @@ -3935,7 +3947,7 @@ msgid "API usage exceeded rate limit" msgstr "API-Nutzungsrate überschritten" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Fehlerhaftes JSON" @@ -4030,7 +4042,7 @@ msgstr "Reaktion existiert nicht." msgid "" "Your organization is registered to a different Zulip server. Please contact " "Zulip support for assistance in resolving this issue." -msgstr "" +msgstr "Deine Zulip-Organisation ist einer anderen Zulip-Serverinstallation zugeordnet. Bitte kontaktiere den Zulip-Support, um Hilfe bei der Lösung dieses Problems zu erhalten." #: zerver/lib/exceptions.py:621 msgid "Organization not registered" @@ -4390,7 +4402,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "Ungültige GCM-Option zum Bouncer: {options}" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token existiert nicht" @@ -4443,7 +4455,7 @@ msgstr "Empfängerliste darf nur Nutzer-IDs enthalten" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Nutzer:in für diese Anfrage nicht autorisiert" @@ -4490,7 +4502,7 @@ msgstr "Argument \"{name}\" ist kein gültiges JSON." msgid "Scheduled message does not exist" msgstr "Die geplante Nachricht ist nicht vorhanden" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "{service_name} Accountsicherheit" @@ -4603,7 +4615,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} ist kein Datum" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} ist kein Dict" @@ -4641,7 +4653,7 @@ msgid "{var_name} is too large" msgstr "{var_name} ist zu groß" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} ist keine Liste" @@ -4805,7 +4817,7 @@ msgstr "Ungültiger Bottyp" msgid "Invalid interface type" msgstr "Ungültiger Interface-Typ" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "Ungültige Nutzer-ID: {user_id}" @@ -4868,46 +4880,46 @@ msgstr "{var_name} ist kein allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} ist falsch)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} ist keine URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "Das URL-Muster muss '%(username)s' enthalten." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' darf nicht leer sein." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Das Feld darf keine doppelten Auswahloptionen haben." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' ist keine gültige Option für '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} ist kein String oder Integer-Liste" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} ist kein String oder Integer" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} hat keine Länge" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} fehlt" @@ -5096,57 +5108,57 @@ msgstr "Nur Organisations-Administrator:innen und Moderator:innen können posten msgid "Only organization full members can post" msgstr "Nur vollständige Mitglieder der Organisation können posten" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode Emoji" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Benutzerdefinierte Emoji" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip Extra-Emoji" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "Nutzer:in mit ID {user_id} ist deaktiviert" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "Nutzer:in mit ID {user_id} ist ein Bot" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Liste von Optionen" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Personenauswahl" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Kurzer Text" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Langer Text" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Datumsauswahl" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Externer Account" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Pronomen" @@ -5162,20 +5174,20 @@ msgstr "ein unbekanntes Betriebssystem" msgid "An unknown browser" msgstr "Ein unbekannter Browser" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Fehlendes Argument 'query_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Fehlendes Argument 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Ein neueres Ereignis als {event_id} ist bereits entfernt worden!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Ereignis {event_id} war nicht in dieser Warteschlange enthalten." @@ -5347,19 +5359,19 @@ msgstr "Der Anker kann nur am Ende des Bereichs ausgeschlossen werden" msgid "No such topic '{topic}'" msgstr "Thema '{topic}' nicht vorhanden" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Fehlende:r Absender:in" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Mirroring mit Empfänger-Nutzer-IDs nicht erlaubt" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Ungültige gespiegelte Nachricht" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Zephyr-Spiegelung ist in dieser Organisation nicht erlaubt" @@ -5425,26 +5437,26 @@ msgstr "Mindestens eines der folgenden Argumente muss gegeben sein: emoji_name, msgid "Read receipts are disabled in this organization." msgstr "Empfangsbestätigungen sind in dieser Organisation deaktiviert." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "Ungültige Sprache '{language}'" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Es muss mindestens eine Authentifizierungs-Möglichkeit aktiviert sein." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "Ungültiger video_chat_provider {video_chat_provider}" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "Ungültiges giphy_rating {giphy_rating}" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Muss eine Demo-Organisation sein." @@ -5922,11 +5934,11 @@ msgid "" "exports]({export_settings_link})." msgstr "Dein Datenexport ist abgeschlossen. [Exporte anzeigen und herunterladen]({export_settings_link})." -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Ungültige Subdomain für Push-Notification-Bouncer" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Muss mit gültigem Zulip-Server-API-Schlüssel validiert werden." @@ -5940,43 +5952,43 @@ msgstr "Ungültige UUID" msgid "Invalid token type" msgstr "Ungültiger Token-Typ" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "{hostname} ist kein gültiger Hostname" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "Fehlende ios_app_id" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "Fehlende user_id oder user_uuid" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "Ungültige Eigenschaft {property}" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." -msgstr "" +msgstr "Ungültiger Event-Typ." -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Die Daten sind nicht in Ordnung." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "Doppelte Registrierung erkannt." -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." -msgstr "" +msgstr "Fehler beim Migrieren des:der Kund:in von Server zu Realms. Bitte kontaktiere den Support für Hilfe." -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "Fehlerhafte Audit-Log-Daten" diff --git a/locale/de/translations.json b/locale/de/translations.json index 5b5ca7a9dcb12..0c2a1eb7a116f 100644 --- a/locale/de/translations.json +++ b/locale/de/translations.json @@ -191,7 +191,7 @@ "Automatic": "Automatisch", "Automatic (follows system settings)": "Automatisch (folgt den Systemeinstellungen)", "Automatically follow topics": "Themen automatisch folgen", - "Automatically follow topics where I'm mentioned": "", + "Automatically follow topics where I'm mentioned": "Themen, in denen ich erwähnt werde, automatisch folgen", "Automatically mark messages as read": "Nachrichten automatisch als gelesen markieren", "Automatically unmute topics in muted streams": "Themen in stummgeschalteten Streams automatisch lautschalten", "Available on Zulip Cloud Standard. Upgrade or request sponsorship to access.": "Verfügbar auf Zulip Cloud Standard. Upgrade oder Förderung anfragen, um Zugriff zu erhalten.", @@ -247,7 +247,7 @@ "Clear profile picture": "Profilbild entfernen", "Clear status": "Status leeren", "Clear topic": "Thema leeren", - "Clear your status": "Leere deinen Status", + "Clear your status": "Deinen Status leeren", "Click here to reveal.": "Hier klicken zum Anzeigen.", "Click on the pencil () icon to edit and reschedule a message.": "Klicke auf das Bleistiftsymbol (), um eine Nachricht zu bearbeiten und zu verschieben.", "Click to change notifications for this topic.": "Klicken, um Benachrichtigungen für dieses Thema zu ändern.", @@ -951,7 +951,7 @@ "Pin stream to top": "Stream oben anpinnen", "Pin stream to top of left sidebar": "Stream in der linken Seitenleiste oben anheften", "Pinned": "Angepinnt", - "Plan management": "", + "Plan management": "Tarifverwaltung", "Plans and pricing": "Angebote und Preise", "Play sound": "Ton abspielen", "Please contact support for an exception or add users with a reusable invite link.": "Bitte wende dich an den Support, um eine Ausnahme zu bekommen oder füge Nutzer:innen mit einem wiederverwendbaren Link hinzu.", @@ -1318,7 +1318,7 @@ "User group creation": "Erstellung von Nutzergruppen", "User group description": "Beschreibung der Nutzergruppen", "User group name": "Name der Nutzergruppe", - "User group settings": "Einstellungen der Nutzergruppe", + "User group settings": "Nutzergruppen-Einstellungen", "User groups": "Nutzergruppen", "User identity": "Nutzeridentität", "User is already not subscribed.": "Nutzer:in ist bereits deabonniert.", @@ -1392,7 +1392,7 @@ "Who can send email invitations to new users": "Wer kann E-Mail-Einladungen an neue Nutzer:innen senden", "Who can unsubscribe others from this stream?": "Wer kann andere von diesem Stream abmelden?", "Who can use direct messages": "Wer kann Direktnachrichten nutzen", - "Who can view all other users in the organization": "", + "Who can view all other users in the organization": "Wer kann alle anderen Nutzer:innen in der Organisation anzeigen", "Why not start a conversation with yourself?": "Warum beginnst du nicht einfach eine Unterhaltung mit dir selbst?", "Why not start the conversation?": "Wieso beginnst du nicht einfach die Unterhaltung?", "Word": "Wort", diff --git a/locale/en_GB/LC_MESSAGES/django.po b/locale/en_GB/LC_MESSAGES/django.po index 249dd20a45ed6..decdc5567ba9d 100644 --- a/locale/en_GB/LC_MESSAGES/django.po +++ b/locale/en_GB/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: David Wood , 2022-2023\n" "Language-Team: English (United Kingdom) (http://app.transifex.com/zulip/zulip/language/en_GB/)\n" @@ -65,7 +65,7 @@ msgstr "Start time is later than end time. Start: {start}, End: {end}" msgid "No analytics data available. Please contact your server administrator." msgstr "No analytics data available. Please contact your server administrator." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -136,124 +136,124 @@ msgstr "Registration is deactivated" msgid "Invalid remote server." msgstr "Invalid remote server." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "You must purchase licenses for all active users in your organisation (minimum {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Invoices with more than {max_licenses} licences can't be processed from this page. To complete the upgrade, please contact {email}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "No payment method on file." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} ending in {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Unknown payment method. Please contact {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Something went wrong. Please contact {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Something went wrong. Please reload the page." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Something went wrong. Please wait a few seconds and try again." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "Please add a credit card before starting your free trial." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "Please add a credit card to schedule upgrade." -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Unable to update the plan. The plan has been expired and replaced with a new plan." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Unable to update the plan. The plan has ended." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "Cannot update licenses in the current billing period for free trial plan." -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Unable to update licenses manually. Your plan is on automatic license management." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Your plan is already on {licenses} licenses in the current billing period." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "You cannot decrease the licenses in the current billing period." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "Cannot change the licenses for next billing cycle for a plan that is being downgraded." -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Your plan is already scheduled to renew with {licenses_at_next_renewal} licenses." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Nothing to change." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "No customer for this organisation!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Session not found" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Must be a billing administrator or an organisation owner" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Payment intent not found" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Pass stripe_session_id or stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -261,33 +261,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "Your organisation's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n\nIf you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "Parameter 'confirmed' is required" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "Billing access token expired." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "Invalid billing access token." -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." -msgstr "" +msgstr "User account doesn't exist yet." -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "You must accept the Terms of Service to proceed." -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." -msgstr "" +msgstr "This zulip_org_id is not registered with Zulip's billing management system." -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." -msgstr "" +msgstr "Invalid zulip_org_key for this zulip_org_id." -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "Your server registration has been deactivated." @@ -314,7 +318,7 @@ msgid "" msgstr "\n If this error is unexpected, you can\n contact support.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Internal server error" @@ -563,31 +567,31 @@ msgstr "Make sure you copied the link correctly in to your browser. If you're st msgid "Billing" msgstr "Billing" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Close modal" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Cancel" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "Downgrade" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Confirm" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "Never mind" @@ -686,6 +690,10 @@ msgstr "Education pricing" msgid "View pricing" msgstr "View pricing" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "Deactivate server registration?" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip for business" @@ -827,8 +835,6 @@ msgstr "Already have an account?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2240,53 +2246,50 @@ msgstr "Click the link below to reactivate your organisation." msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "Either you, or someone on your behalf, has requested a log in link to manage the Zulip plan for %(remote_server_hostname)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:12 msgid "" "\n" " Click the button below to log in.\n" " " -msgstr "" +msgstr "\n Click the button below to log in.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" " This link will expire in %(validity_in_hours)s hours.\n" " " -msgstr "" +msgstr "\n This link will expire in %(validity_in_hours)s hours.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Questions? Learn more or contact %(billing_contact_email)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" -msgstr "" +msgstr "Log in to Zulip plan management" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:1 #, python-format msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "Either you, or someone on your behalf, has requested a log in link to manage the Zulip plan for %(remote_server_hostname)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 msgid "Click the link below to log in." -msgstr "" +msgstr "Click the link below to log in." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." -msgstr "" +msgstr "This link will expire in %(validity_in_hours)s hours." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:8 #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:6 @@ -2294,22 +2297,31 @@ msgstr "" msgid "" "Questions? Learn more at %(billing_help_link)s or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Questions? Learn more at %(billing_help_link)s or contact %(billing_contact_email)s." #: templates/zerver/emails/remote_realm_billing_confirm_login.html:9 #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " -msgstr "" +msgstr "\n Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n " + +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "Confirm and log in" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "Confirm email for Zulip plan management" #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." -msgstr "" +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." +msgstr "Click the link below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s." #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 #, python-format @@ -3120,7 +3132,7 @@ msgstr "\n You can also use the contact Zulip support\n" " for assistance in resolving this issue.\n" " " -msgstr "" +msgstr "\n Your Zulip organisation is registered as associated with a\n different Zulip server installation.\n\n Please contact Zulip support\n for assistance in resolving this issue.\n " #: zerver/actions/create_user.py:98 msgid "signups" @@ -3353,65 +3365,65 @@ msgstr "Invalid message flag operation: '{operation}'" msgid "Invalid message(s)" msgstr "Invalid message(s)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Unable to render message" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Expected exactly one stream" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Invalid data type for stream" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Invalid data type for recipients" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Recipient lists may contain emails or user IDs, but not both." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, but there is no stream with that ID." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Your bot {bot_identity} tried to send a message to stream {stream_name}, but that stream does not exist. Click [here]({new_stream_link}) to create it." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Your bot {bot_identity} tried to send a message to stream {stream_name}. The stream exists but does not have any subscribers." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Direct messages are disabled in this organisation." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "You do not have permission to access some of the recipients." -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Topics are required in this organisation" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: API programmer sent invalid JSON content" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3428,28 +3440,28 @@ msgstr "The ordered list must not contain duplicated linkifiers" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "The ordered list must enumerate all existing linkifiers exactly once" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "Scheduled message was already sent" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "Scheduled delivery time must be in the future." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "Message could not be sent at the scheduled time." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "The message you scheduled for {delivery_datetime} was not sent because of the following error:" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "[View scheduled messages](#scheduled)" @@ -3726,7 +3738,7 @@ msgstr "An error occurred while deleting the attachment. Please try again later. msgid "Message must have recipients!" msgstr "Message must have recipients!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "{service_name} digest" @@ -3905,7 +3917,7 @@ msgid "API usage exceeded rate limit" msgstr "API usage exceeded rate limit" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Malformed JSON" @@ -4000,7 +4012,7 @@ msgstr "Reaction doesn't exist." msgid "" "Your organization is registered to a different Zulip server. Please contact " "Zulip support for assistance in resolving this issue." -msgstr "" +msgstr "Your organisation is registered to a different Zulip server. Please contact Zulip support for assistance in resolving this issue." #: zerver/lib/exceptions.py:621 msgid "Organization not registered" @@ -4360,7 +4372,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "Invalid GCM options to bouncer: {options}" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token does not exist" @@ -4413,7 +4425,7 @@ msgstr "Recipient list may only contain user IDs" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "User not authorised for this query" @@ -4460,7 +4472,7 @@ msgstr "Argument \"{name}\" is not valid JSON." msgid "Scheduled message does not exist" msgstr "Scheduled message does not exist" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "{service_name} account security" @@ -4573,7 +4585,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} is not a date" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} is not a dict" @@ -4611,7 +4623,7 @@ msgid "{var_name} is too large" msgstr "{var_name} is too large" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} is not a list" @@ -4775,7 +4787,7 @@ msgstr "Invalid bot type" msgid "Invalid interface type" msgstr "Invalid interface type" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "Invalid user ID: {user_id}" @@ -4838,46 +4850,46 @@ msgstr "{var_name} is not an allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} is wrong)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} is not a URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "URL pattern must contain '%(username)s'." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' cannot be blank." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Field must not have duplicate choices." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' is not a valid choice for '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} is not a string or an integer list" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} is not a string or integer" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} does not have a length" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} is missing" @@ -5066,57 +5078,57 @@ msgstr "Only organisation administrators and moderators can post" msgid "Only organization full members can post" msgstr "Only organisation full members can post" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode emoji" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Custom emoji" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip extra emoji" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "User with ID {user_id} is deactivated" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "User with ID {user_id} is a bot" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "List of options" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Person picker" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Short text" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Long text" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Date picker" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "External account" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Pronouns" @@ -5132,20 +5144,20 @@ msgstr "an unknown operating system" msgid "An unknown browser" msgstr "An unknown browser" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Missing 'queue_id' argument" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Missing 'last_event_id' argument" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "An event newer than {event_id} has already been pruned!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Event {event_id} was not in this queue" @@ -5317,19 +5329,19 @@ msgstr "The anchor can only be excluded at an end of the range" msgid "No such topic '{topic}'" msgstr "No such topic '{topic}'" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Missing sender" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Mirroring not allowed with recipient user IDs" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Invalid mirrored message" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Zephyr mirroring is not allowed in this organisation" @@ -5395,26 +5407,26 @@ msgstr "At least one of the following arguments must be present: emoji_name, emo msgid "Read receipts are disabled in this organization." msgstr "Read receipts are disabled in this organisation." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "Invalid language '{language}'" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "At least one authentication method must be enabled." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "Invalid video_chat_provider {video_chat_provider}" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "Invalid giphy_rating {giphy_rating}" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Must be a demo organisation." @@ -5892,11 +5904,11 @@ msgid "" "exports]({export_settings_link})." msgstr "Your data export is complete. [View and download exports]({export_settings_link})." -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Invalid subdomain for push notifications bouncer" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Must validate with valid Zulip server API key" @@ -5910,43 +5922,43 @@ msgstr "Invalid UUID" msgid "Invalid token type" msgstr "Invalid token type" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "{hostname} is not a valid hostname" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "Missing ios_app_id" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "Missing user_id or user_uuid" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "Invalid property {property}" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." -msgstr "" +msgstr "Invalid event type." -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Data is out of order." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "Duplicate registration detected." -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." -msgstr "" +msgstr "Failed to migrate customer from server to realms. Please contact support for assistance." -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "Malformed audit log data" diff --git a/locale/en_GB/translations.json b/locale/en_GB/translations.json index 14ed3e9ebee64..0769a975c3d38 100644 --- a/locale/en_GB/translations.json +++ b/locale/en_GB/translations.json @@ -191,7 +191,7 @@ "Automatic": "Automatic", "Automatic (follows system settings)": "Automatic (follows system settings)", "Automatically follow topics": "Automatically follow topics", - "Automatically follow topics where I'm mentioned": "", + "Automatically follow topics where I'm mentioned": "Automatically follow topics where I'm mentioned", "Automatically mark messages as read": "Automatically mark messages as read", "Automatically unmute topics in muted streams": "Automatically unmute topics in muted streams", "Available on Zulip Cloud Standard. Upgrade or request sponsorship to access.": "Available on Zulip Cloud Standard. Upgrade or request sponsorship to access.", @@ -951,7 +951,7 @@ "Pin stream to top": "Pin stream to top", "Pin stream to top of left sidebar": "Pin stream to top of left sidebar", "Pinned": "Pinned", - "Plan management": "", + "Plan management": "Plan management", "Plans and pricing": "Plans and pricing", "Play sound": "Play sound", "Please contact support for an exception or add users with a reusable invite link.": "Please contact support for an exception or add users with a reusable invite link.", @@ -1392,7 +1392,7 @@ "Who can send email invitations to new users": "Who can send email invitations to new users", "Who can unsubscribe others from this stream?": "Who can unsubscribe others from this stream?", "Who can use direct messages": "Who can use direct messages", - "Who can view all other users in the organization": "", + "Who can view all other users in the organization": "Who can view all other users in the organisation", "Why not start a conversation with yourself?": "Why not start a conversation with yourself?", "Why not start the conversation?": "Why not start the conversation?", "Word": "Word", diff --git a/locale/eo/LC_MESSAGES/django.po b/locale/eo/LC_MESSAGES/django.po index ebb6a285eb35c..00e20259896ca 100644 --- a/locale/eo/LC_MESSAGES/django.po +++ b/locale/eo/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:58+0000\n" +"POT-Creation-Date: 2023-12-15 17:44+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -61,7 +61,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -129,124 +129,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this " "page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new " "plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2901 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You " @@ -256,33 +256,37 @@ msgid "" "we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:136 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:138 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:275 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:280 +#: corporate/views/remote_billing_page.py:693 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:506 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:513 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:517 msgid "Your server registration has been deactivated." msgstr "" @@ -310,7 +314,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -567,31 +571,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:313 templates/corporate/billing.html:339 +#: templates/corporate/billing.html:368 templates/corporate/billing.html:397 +#: templates/corporate/billing.html:423 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:324 templates/corporate/billing.html:353 +#: templates/corporate/billing.html:382 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:326 templates/corporate/billing.html:410 +#: templates/corporate/billing.html:433 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:355 templates/corporate/billing.html:384 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:408 templates/corporate/billing.html:431 msgid "Never mind" msgstr "" @@ -690,6 +694,62 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:4 +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:16 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:4 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:16 +msgid "Plan management not available" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:18 +#, python-format +msgid "" +" Plan management is not available for this\n" +" organization, because your Zulip server is already " +"on a\n" +" %(server_plan_name)s plan, which covers all\n" +" organizations on this server. Follow the log\n" +" in instructions\n" +" for All older versions\n" +" of the Zulip server to manage your plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:29 +msgid "" +" To move the plan from the server to this\n" +" organization, or for other questions, contact support.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:18 +msgid "" +"\n" +" Plan management for this server is not available " +"because at least one organization\n" +" hosted on this server already has an active plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:24 +#, python-format +msgid "" +"\n" +" Log in to plan management for your\n" +" organization instead, or contact support with any questions.\n" +" " +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -832,8 +892,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2277,7 +2335,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2286,7 +2343,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2294,7 +2351,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2310,7 +2366,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2327,16 +2382,25 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for " -"%(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2367,7 +2431,7 @@ msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " -"appreciate it!." +"appreciate it!" msgstr "" #: templates/zerver/find_account.html:4 @@ -3396,65 +3460,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but " "that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The " "stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3471,27 +3535,27 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 -#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:128 +#: zerver/actions/scheduled_messages.py:155 +#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3770,7 +3834,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3949,7 +4013,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4402,45 +4466,45 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:739 zilencer/views.py:241 msgid "Token does not exist" msgstr "" -#: zerver/lib/push_notifications.py:902 +#: zerver/lib/push_notifications.py:910 msgid "" "This organization has disabled including message content in mobile push " "notifications" msgstr "" -#: zerver/lib/push_notifications.py:1015 +#: zerver/lib/push_notifications.py:1023 #, python-brace-format msgid "{full_name} mentioned @{user_group_name}:" msgstr "" -#: zerver/lib/push_notifications.py:1019 +#: zerver/lib/push_notifications.py:1027 #, python-brace-format msgid "{full_name} mentioned you:" msgstr "" -#: zerver/lib/push_notifications.py:1026 +#: zerver/lib/push_notifications.py:1034 #, python-brace-format msgid "{full_name} mentioned everyone:" msgstr "" -#: zerver/lib/push_notifications.py:1436 +#: zerver/lib/push_notifications.py:1438 msgid "Test notification" msgstr "" -#: zerver/lib/push_notifications.py:1437 +#: zerver/lib/push_notifications.py:1439 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." msgstr "" -#: zerver/lib/push_notifications.py:1488 +#: zerver/lib/push_notifications.py:1490 msgid "Device not recognized" msgstr "" -#: zerver/lib/push_notifications.py:1500 +#: zerver/lib/push_notifications.py:1502 msgid "Device not recognized by the push bouncer" msgstr "" @@ -4455,7 +4519,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4502,7 +4566,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4615,7 +4679,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4653,7 +4717,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4880,46 +4944,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5108,15 +5172,15 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" @@ -5174,20 +5238,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5359,19 +5423,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5437,26 +5501,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5934,61 +5998,61 @@ msgid "" "({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" #. error -#: zilencer/views.py:77 zilencer/views.py:79 +#: zilencer/views.py:81 zilencer/views.py:83 msgid "Invalid UUID" msgstr "" #. error -#: zilencer/views.py:84 +#: zilencer/views.py:88 msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:126 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:182 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:185 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:610 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:613 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:620 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:696 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:957 msgid "" "Failed to migrate customer from server to realms. Please contact support for " "assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:1009 msgid "Malformed audit log data" msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index b4d622178ac25..d39a585a40fc7 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -24,7 +24,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Eduardo, 2023\n" "Language-Team: Spanish (http://app.transifex.com/zulip/zulip/language/es/)\n" @@ -78,7 +78,7 @@ msgstr "La hora de inicio es más tarde que la hora de fin. Inicio: {start}, Fin msgid "No analytics data available. Please contact your server administrator." msgstr "No hay datos de analíticas disponible. Por favor, contacta con el administrador de tu servidor." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -149,124 +149,124 @@ msgstr "El registro está desactivado." msgid "Invalid remote server." msgstr "Servidor remoto no válido." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "Debes comprar licencias para todos los usuarios activos en tu organización (mínimo {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Las facturas con más de {max_licenses} licencias no pueden ser procesadas a través de esta página. Para completar esta compra, por favor contactar con {email}" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} terminada en {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Método de pago desconocido. Por favor contactar a {email}" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Algo ha fallado. Contacta en {email}" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Algo salió mal. Por favor, recarga la página." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Algo salió mal. Por favor, espera unos segundos e inténtalo de nuevo." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "Por favor, añada una tarjeta de crédito antes de comenzar su periodo de prueba." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "No se puede actualizar el plan. El plan venció y se reemplazó con un nuevo plan." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "No es posible actualizar el plan. El plan ha finalizado." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "No es posible actualizar las licencias manualmente. Tu plan gestiona las licencias automáticamente." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Tu plan ya dispone de {licenses} licencias en el periodo de facturación actual." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "No puedes disminuir las licencias en el periodo de facturación actual." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Tu plan ya está planeado para renovarse con {licenses_at_next_renewal} licencias." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Nada que cambiar" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "¡No hay clientes para esta organización!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Sesión no encontrada" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Debes ser un administrador de facturación o un administrador de la organización." -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Intento de pago no encontrado" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -274,33 +274,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "¡La solicitud de alojamiento patrocinado de su organización ha sido aprobada! Se ha actualizado tu plan a {plan_name} de forma gratuita. {emoji}\n\nAgradeceríamos mucho si pudieras {begin_link} añadir a Zulip como un patrocinador de tu página web{end_link}" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "El token de acceso a facturación expiró." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "El token de acceso a facturación es incorrecto." -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -327,7 +331,7 @@ msgid "" msgstr "\nSi este error no es esperado, puede \ncontactar con el soporte." #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Error interno del servidor" @@ -576,31 +580,31 @@ msgstr "Asegúrate de haber copiado correctamente el enlace en tu navegador. Si msgid "Billing" msgstr "Facturación" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Cerrar diálogo" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Cancelar" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Confirmar" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -699,6 +703,10 @@ msgstr "" msgid "View pricing" msgstr "Mostrar precios" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip para empresas" @@ -840,8 +848,6 @@ msgstr "¿Ya tienes una cuenta?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2263,7 +2269,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2272,7 +2277,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2280,7 +2285,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2296,7 +2300,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2313,15 +2316,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3366,65 +3378,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Mensaje(s) inválido(s)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "No se pudo renderizar el mensaje" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Los mensajes directos están deshabilitados para esta organización" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: el programador de la API envió contenido JSON inválido" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3441,28 +3453,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3739,7 +3751,7 @@ msgstr "Un error ocurrió al eliminar el adjunto. Por favor, inténtalo de nuevo msgid "Message must have recipients!" msgstr "El mensaje debe tener destinatarios!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3918,7 +3930,7 @@ msgid "API usage exceeded rate limit" msgstr "El uso de la API excedió el límite" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON malformado" @@ -4373,7 +4385,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "El token no existe" @@ -4426,7 +4438,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Usuario no autorizado para esta petición" @@ -4473,7 +4485,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4586,7 +4598,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4624,7 +4636,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4788,7 +4800,7 @@ msgstr "Tipo de bot inválido" msgid "Invalid interface type" msgstr "Tipo de interfaz inválido" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4851,46 +4863,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' no puede estar vacío." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' no es una opción válida para '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5079,57 +5091,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "Solo los miembros definitivos de la organización pueden publicar aquí." -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Emoticonos unicode" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoticonos personalizados" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Emoticonos extra de Zulip" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Lista de opciones" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Selector de personas" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Texto corto" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Texto largo" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Selector de fechas" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Enlace" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Cuenta externa" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Pronombres" @@ -5145,20 +5157,20 @@ msgstr "un sistema operativo desconocido" msgid "An unknown browser" msgstr "Un navegador desconocido" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Falta el argumento 'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Falta el argumento 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5330,19 +5342,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Falta el remitente" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "El mirroring no esta permitido con los IDs de usuario de los destinatarios" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Mensaje reflejado inválido" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "El mirroring de Zephyr no está permitido en esta organización" @@ -5408,26 +5420,26 @@ msgstr "Al menos uno de los siguientes arguments debe estar presente: emoji_name msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Al menos un método de autentificación debe ser activado" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5905,11 +5917,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Dominio inválido para el bouncer de notificaciones push" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Debe validarse con una clave de API válida del servidor de Zulip" @@ -5923,43 +5935,43 @@ msgstr "" msgid "Invalid token type" msgstr "Tipo de token inválido" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Los datos no están ordenados." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/fa/LC_MESSAGES/django.po b/locale/fa/LC_MESSAGES/django.po index d89caf3b47622..323c39d833405 100644 --- a/locale/fa/LC_MESSAGES/django.po +++ b/locale/fa/LC_MESSAGES/django.po @@ -14,7 +14,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: rahim agh , 2023\n" "Language-Team: Persian (http://app.transifex.com/zulip/zulip/language/fa/)\n" @@ -68,7 +68,7 @@ msgstr "تاریخ شروع پس از تاریخ پایان است. شروع: {s msgid "No analytics data available. Please contact your server administrator." msgstr "داده های تحلیلی در دسترس نیست. لطفا با ادمین سرور تماس بگيريد. " -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -139,124 +139,124 @@ msgstr "ثبت‌نام غیرفعال شده‌است" msgid "Invalid remote server." msgstr "سرور راه دور نامعتبر است." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "شما باید برای تمام کاربران فعال در سازمان خود مجوز خریداری کنید (حداقل {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "فاکتورهای با بیش از {max_licenses} مجوز، امکان پردازش از این صفحه را ندارند، برای تکمیل به‌روزرسانی، با {email} تماس بگیرید." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "روش پرداختی در فایل وجود ندارد." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} در {last4} به پایان می‌رسد" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "چنین روش پرداختی وجود ندارد. لطفاً با {email} مکاتبه کنید." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "مشکلی پیش آمده. لطفاً با {email} مکاتبه کنید." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "مشکلی وجود دارد. لطفا دوباره صفحه را بارگذاری کنید." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "مشکلی وجود دارد. لطفا چند لحظه صبر کرده و دوباره تلاش کنید." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "لطفاً قبل از شروع آزمایش رایگان، یک کارت اعتباری اضافه کنید." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." -msgstr "" +msgstr "لطفاً یک کارت اعتباری اضافه کنید تا برنامه‌ریزی ارتقا انجام شود." -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "امکان به روزرسانی این طرح نیست. این طرح منقضی شده و با طرح جدیدتری جابجا شده است." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "امکان به‌روزرسانی این طرح نیست. این طرح پایان یافته است." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." -msgstr "" +msgstr "نمی‌توان محوزها را در دوره فعلی صورتحساب، برای طرح آزمایشی رایگان به‌روزرسانی کرد." -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "امکان به‌روزرسانی مجوزها به صورت غیر اتوماتیک نیست. طرح شما بر روی حالت مدیریت اتوماتیک مجوزها است. " -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "طرح شما بر روی {licenses} مجوز، در دوره فاکتور جاری است." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "شما نمی‌توانید در دوره فاکتور جاری تعدا مجوزها را کاهش دهید." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." -msgstr "" +msgstr "نمی‌توانید مجوزها را برای دوره صورتحساب بعدی، برای یک طرح که در حال کاهش است، تغییر دهید." -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "طرح شما در حال حاضر برای به روزرسانی با {licenses_at_next_renewal} مجوز، برنامه‌ریزی شده است." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "چیزی برای تغییر نیست" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "مشتری برای این سازمان نیست!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "جلسه پیدا نشد" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "باید یک ادمین فاکتور یا یک مالک سازمان باشد" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "هدف پرداخت پیدا نشد" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "stripe_session_id یا stripe_payment_intent_id را ارسال کنید" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -264,33 +264,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "درخواست سازمان شما برای میزبانی اسپانسری پذیرفته شده است! شما بدون هزینه به طرح {plan_name} ارتقا پیدا کرده‌اید. {emoji}\n\nاگر بتوانید {begin_link}زولیپ را به عنوان اسپانسر در وب‌سایت خود فهرست کنید{end_link}، ما بسیار سپاسپگزار خواهیم بود!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "توکن دسترسی به صورت‌حساب منقضی شده است." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "توکن دسترسی به صورتحساب نامعتبر" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." -msgstr "" +msgstr "حساب کاربری هنوز وجود ندارد." -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." -msgstr "" +msgstr "برای ادامه دادن مراحل، باید شرایط خدمات را بپذیرید." -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." -msgstr "" +msgstr "این zulip_org_id با سیستم مدیریت صورتحساب زولیپ ثبت نشده است." -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." -msgstr "" +msgstr "کلید zulip_org_key برای این zulip_org_id نامعتبر است." -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "ثبت نام سرور شما غیرفعال شده است." @@ -317,7 +321,7 @@ msgid "" msgstr "\n اگر این خطا غیرمنتظره است می‌توانید\n با پشتیبانی تماس بگیرید.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "خطای داخلی در سرور" @@ -566,31 +570,31 @@ msgstr "مطمئن شوید که لینک تایید را به درستی در msgid "Billing" msgstr "صورتحساب" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "بستن پنجره" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "لغو کردن" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "تنزل نسخه" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "تایید" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "بیخیال شو" @@ -689,6 +693,10 @@ msgstr "قیمت‌های آموزشی" msgid "View pricing" msgstr "مشاهده قیمت‌ها" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "زولیپ برای کسب و کار" @@ -830,8 +838,6 @@ msgstr "آیا حساب کاربری دارید؟" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2243,53 +2249,50 @@ msgstr "بر روی لینک زیر کلیک کنید تا سازمان خود msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "شما یا کسی به نمایندگی شما، درخواستی برای دریافت لینک ورود به مدیریت طرح زولیپ برای %(remote_server_hostname)s داده است." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:12 msgid "" "\n" " Click the button below to log in.\n" " " -msgstr "" +msgstr "\nبرای ورود، روی دکمه زیر کلیک کنید." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" " This link will expire in %(validity_in_hours)s hours.\n" " " -msgstr "" +msgstr "\n این لینک در %(validity_in_hours)s ساعت منقضی خواهد شد.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "سوال دارید؟ بیشتر بدانید یا با %(billing_contact_email)s تماس بگیرید." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" -msgstr "" +msgstr "ورود به مدیریت طرح‌های زولیپ" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:1 #, python-format msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "شما یا کسی به نمایندگی شما، درخواستی برای دریافت لینک ورود به مدیریت طرح زولیپ برای %(remote_server_hostname)s داده است." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 msgid "Click the link below to log in." -msgstr "" +msgstr "برای ورود، روی لینک زیر کلیک کنید." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." -msgstr "" +msgstr "این لینک در %(validity_in_hours)s ساعت منقضی می‌شود." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:8 #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:6 @@ -2297,21 +2300,30 @@ msgstr "" msgid "" "Questions? Learn more at %(billing_help_link)s or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "سوال دارید؟ در %(billing_help_link)s بیشتر بدانید یا با ایمیل %(billing_contact_email)s تماس بگیرید." #: templates/zerver/emails/remote_realm_billing_confirm_login.html:9 #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2319,31 +2331,31 @@ msgstr "" msgid "" "Your request for Zulip sponsorship has been approved! Your organization has " "been upgraded to the Zulip Community plan." -msgstr "" +msgstr "درخواست شما برای حمایت از زولیپ تایید شده است! سازمان شما به طرح جامعه زولیپ ارتقا یافته است." #: templates/zerver/emails/sponsorship_approved_community_plan.html:12 #, python-format msgid "" "If you could list Zulip as a sponsor on your " "website, we would really appreciate it!" -msgstr "" +msgstr "اگر شما بتوانید زولیپ را به عنوان یک حامی در وبسایت خود لیست کنید، ما واقعاً از آن قدردانی می‌کنیم!" #: templates/zerver/emails/sponsorship_approved_community_plan.subject.txt:1 #, python-format msgid "Community plan sponsorship approved for %(billing_entity)s!" -msgstr "" +msgstr "طرح حمایت از جامعه برای %(billing_entity)s تأیید شد!" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:1 msgid "" "Your request for Zulip sponsorship has been approved! Your organization has " "been upgraded to the Zulip Community plan." -msgstr "" +msgstr "درخواست شما برای حمایت از زولیپ تایید شده است! سازمان شما به طرح جامعه زولیپ ارتقا یافته است." #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " "appreciate it!." -msgstr "" +msgstr "اگر بتوانید زولیپ را به عنوان یک حامی در وبسایت خود لیست کنید، واقعاً قدردانی می‌کنیم!" #: templates/zerver/find_account.html:4 msgid "Find your accounts" @@ -3123,7 +3135,7 @@ msgstr "\n شما همچنین می‌توانید contact Zulip support\n" " for assistance in resolving this issue.\n" " " -msgstr "" +msgstr "\nسازمان زولیپ شما به عنوان یک سازمان مرتبط با \nنصب سرور زولیپ دیگری، ثبت شده است.\n\nلطفاً برای کمک در حل این مشکل ب\nا پشتیبانی زولیپ تماس بگیرید." #: zerver/actions/create_user.py:98 msgid "signups" @@ -3356,65 +3368,65 @@ msgstr "عملکرد پرچم پیام نامعتبر است: '{operation}'" msgid "Invalid message(s)" msgstr "پیام‌(های) نامعتبر " -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "امکان پردازش پیام وجود ندارد" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "دقیقاً یک جریان انتظار می‌رود" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "نوع داده برای جریان نامعتبر است" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "نوع داده برای گیرندگان نامعتبر است" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "فهرست گیرندگان ممکن است شامل یکی از دو مورد ایمیل یا شناسه کاربری باشد، نه هردو آنها." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "ربات {bot_identity} شما تلاش کرده تا پیامی را به شناسه جریان {stream_id} بفرستد، اما جریانی با این شناسه وجود ندارد." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "ربات {bot_identity} شما تلاش کرده تا پیامی را به جریان {stream_name} بفرستد، اما این جریان وجود ندارد. برای ساخت آن [اینجا]({new_stream_link}) را کلید کنید." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "ربات {bot_identity} تلاش کرده تا پیامی را به جریان {stream_name} بفرستد. این جریان وجود دارد اما هیچ مشترکی ندارد." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "پیام‌های مستقیم در این سازمان غیر فعال شده‌اند." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "شما اجازه ندارید به برخی گیرندگان دسترسی داشته باشید." -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "موضوعات در این سازمان ضروری هستند" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "گجت ها: API برنامه نویسی، محتوای Json نامعتبری ارسال کرده است" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "ابزارک‌ها: {error_msg}" @@ -3431,28 +3443,28 @@ msgstr "لیست مرتب‌شده نباید حاوی پیونددهنده تک msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "لیست مرتب شده باید تمام پیونددهنده‌های موجود را دقیقاً یک بار شمارش کند" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "پیام برنامه‌ریزی شده قبلاً ارسال شده‌است" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "برنامه‌ریزی زمان ارسال باید در آینده باشد." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "پیام نمی‌تواند در زمان برنامه‌ریزی شده ارسال شود." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "پیامی که برای {delivery_datetime} برنامه‌ریزی کرده بودید به دلیل خطای زیر ارسال نشد:" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "[مشاهده پیام‌های برنامه‌ریزی شده](#scheduled)" @@ -3729,7 +3741,7 @@ msgstr "هنگام حذف پیوست خطایی رخ داد. لطفا بعدا msgid "Message must have recipients!" msgstr "پیام باید دریافت کننده داشته باشد! " -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "خلاصه {service_name}" @@ -3908,7 +3920,7 @@ msgid "API usage exceeded rate limit" msgstr "استفاده از API بیش از نرخ مجاز است" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Json نادرست " @@ -4003,7 +4015,7 @@ msgstr "واکنش وجود ندارد" msgid "" "Your organization is registered to a different Zulip server. Please contact " "Zulip support for assistance in resolving this issue." -msgstr "" +msgstr "سازمان شما در یک سرور زولیپ متفاوت ثبت شده است. لطفاً برای کمک در حل این مشکل با پشتیبانی زولیپ تماس بگیرید." #: zerver/lib/exceptions.py:621 msgid "Organization not registered" @@ -4363,7 +4375,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "گزینه‌های GCM نامعتبر برای بانسر: {options}" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "توکن وجود ندارد" @@ -4395,7 +4407,7 @@ msgstr "اعلان آزمایشی" #: zerver/lib/push_notifications.py:1437 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." -msgstr "" +msgstr "این یک اعلان آزمایشی از {realm_name} ({realm_uri}) است." #: zerver/lib/push_notifications.py:1488 msgid "Device not recognized" @@ -4416,7 +4428,7 @@ msgstr "لیست گیرندگان فقط می‌تواند شامل شناسه #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "کاربر اجازه این درخواست را ندارد " @@ -4463,7 +4475,7 @@ msgstr "آرگومان \"{name}\" یک جیسون معتبر نیست" msgid "Scheduled message does not exist" msgstr "پیام برنامه‌ریزی شده وجود ندارد" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "امنیت اکانت {service_name}" @@ -4576,7 +4588,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} یک تاریخ نیست" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} یک دیکشنری نیست" @@ -4614,7 +4626,7 @@ msgid "{var_name} is too large" msgstr "{var_name} زیادی بلند است" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} یک لیست نیست" @@ -4778,7 +4790,7 @@ msgstr "نوع ربات اشتباه است" msgid "Invalid interface type" msgstr "نوع رابط اشتباه است" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "شناسه کاربر نامعتبر: {user_id}" @@ -4841,46 +4853,46 @@ msgstr "{var_name} یک نوع داده مجاز نیست" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} اشتباه است)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} یک URL نیست" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "قالب آدرس باید شامل '%(username)s' باشد." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' نمی تواند خالی باشد." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "فیلد نباید دارای گزینه‌های تکراری باشد." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' یک گزینه صحیح برای '{field_name}' نیست." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} یک فهرست از رشته حروف یا اعداد صحیح نیست" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} از نوع رشته حروف یا عدد صحیح نیست" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} یک طول مشخص ندارد" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} گم شده است" @@ -4924,7 +4936,7 @@ msgstr "در زولیپ کلود استاندارد در دسترس است. ار #: zerver/models.py:688 msgid "Available on Zulip Cloud Plus. Upgrade to access." -msgstr "" +msgstr "بر روی زولیپ ابری پلاس در دسترس است. ارتقا دهید تا دسترسی داشته باشید." #: zerver/models.py:758 msgid "GIPHY integration disabled" @@ -5069,57 +5081,57 @@ msgstr "فقط ادمین‌های سازمان و مدیران می‌توان msgid "Only organization full members can post" msgstr "فقط اعضای اصلی سازمان می‌توانند پست ارسال کنند" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "ایموجی یونیکد" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "ایموجی سفارشی" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "ایموجی‌های بیشتر زولیپ" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "کاربر با شناسه {user_id} غیر فعال شده است" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "کاربر با شناسه {user_id} یک ربات است" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "لیست گزینه ها" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "انتخاب کننده فرد" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "متن کوتاه" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "متن طولانی" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "انتخاب تاریخ" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "لینک" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "حساب کاربری خارجی" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "ضمایر" @@ -5135,20 +5147,20 @@ msgstr "یک سیستم عامل ناشناخته" msgid "An unknown browser" msgstr "یک مرورگر ناشناخته" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "آرگومان 'شناسه صف' خالی است" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "آرگومان 'شناسه آخرین رویداد' خالی است" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "یک رویداد جدیدتر از {event_id} قبلاً پاک شده است!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "رویداد {event_id} در این صف نبود" @@ -5260,7 +5272,7 @@ msgstr "نمی‌توان درخواست به ممکش ارسال کرد" #: zerver/views/hotspots.py:20 #, python-brace-format msgid "Unknown onboarding_step: {onboarding_step}" -msgstr "" +msgstr "مرحله ورود ناشناخته: {onboarding_step}" #: zerver/views/invite.py:77 msgid "You must specify at least one email address." @@ -5320,19 +5332,19 @@ msgstr "لنگر فقط در انتهای محدوده قابل حذف است" msgid "No such topic '{topic}'" msgstr "چنین موضوعی وجود ندارد '{topic}'" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "ارسال کننده خالی است" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "آیینه کردن برای شناسه کاربرهای دریافت کننده مجاز نیست" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "پیام آیینه شده اشتباه است" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "آیینه سازی Zephyr در این سازمان مجاز نیست" @@ -5398,26 +5410,26 @@ msgstr "حداقل یکی از آرگومان های زیر باید باشند: msgid "Read receipts are disabled in this organization." msgstr "رسید خوانده‌شدن، در این سازمان غیرفعال شده است." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "زبان نامعتبر '{language}'" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "حداقل یک روش احرازهویت باید فعال باشد." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "تأمین‌کننده تماس تصویری نامعتبر {video_chat_provider}" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "giphy_rating نامعتبر {giphy_rating}" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "باید یک سازمان آزمایشی باشد." @@ -5895,11 +5907,11 @@ msgid "" "exports]({export_settings_link})." msgstr "خروجی گرفتن از داده‌های شما کامل شد. [مشاهده و دانلود خروجی‌ها]({export_settings_link})." -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "زیردامنه نامعتبر برای اعلان‌های فشاری" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "باید با کلید صحیح API سرور زولیپ اعتبارسنجی شود" @@ -5913,43 +5925,43 @@ msgstr "UUID نامعتبر" msgid "Invalid token type" msgstr "نوع توکن نامعتبر است" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "{hostname} یک نام میزبان معتبر نیست" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "ios_app_id گم شده است" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "شناسه کاربری یا uuid کاربر گم شده است" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "دارایی نامعتبر {property}" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." -msgstr "" +msgstr "نوع رویداد نامعتبر است." -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "داده خارج از ترتیب است." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "ثبت نام تکراری تشخیص داده شد." -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." -msgstr "" +msgstr "خطا در مهاجرت مشتری از سرور به حوزه‌ها رخ داد. لطفاً برای کمک با پشتیبانی تماس بگیرید." -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "گزارش لاگ داده‌ها نادرست است" diff --git a/locale/fa/translations.json b/locale/fa/translations.json index 68d2a661b2809..bc2288ec2024f 100644 --- a/locale/fa/translations.json +++ b/locale/fa/translations.json @@ -191,7 +191,7 @@ "Automatic": "خودکار", "Automatic (follows system settings)": "خودکار (دنبال کردن تنظیمات سیستم)", "Automatically follow topics": "دنبال کردن موضوعات به صورت خودکار", - "Automatically follow topics where I'm mentioned": "", + "Automatically follow topics where I'm mentioned": "موضوعاتی که در آنها به من اشاره شده است، به صورت خودکار دنبال کن", "Automatically mark messages as read": "به صورت خودکار پیام‌ها را به خوانده‌شده تبدیل کن", "Automatically unmute topics in muted streams": "صدادار کردن خودکار موضوعات در جریان‌های بی‌صدا شده", "Available on Zulip Cloud Standard. Upgrade or request sponsorship to access.": "در زولیپ ابری استاندارد در دسترس است. ارتقا یا درخواست اسپانسرشدن برای دسترسی.", @@ -582,7 +582,7 @@ "Government": "دولت", "Grant Zulip the Kerberos tickets needed to run your Zephyr mirror via Webathena": "برای اجرای Zephyr موازی از طریق Webathena لازم است اجازه تیکت‌های Kerberos را به زولیپ بدهید.", "Group permissions": "مجوزهای گروه", - "Group settings": "", + "Group settings": "تنظیمات گروه", "Guest": "مهمان", "Guests": "مهمان‌ها", "Guests cannot edit custom emoji.": "مهمان ها نمی توانند ایموجی سفارشی را ویرایش کنند.", @@ -744,8 +744,8 @@ "Mobile": "موبایل", "Mobile message notifications": "اطلاع‌رسانی‌های موبایلی پیام", "Mobile notifications": "اطلاع رسانی‌های موبایل", - "Mobile push notifications are not enabled on this server.": "", - "Mobile push notifications are not enabled on this server. Learn more": "", + "Mobile push notifications are not enabled on this server.": "اعلان‌های فشاری موبایل در این سرور فعال نیستند.", + "Mobile push notifications are not enabled on this server. Learn more": "اعلان‌های فشاری موبایل در این سرور فعال نیستند. بیشتر بدانید", "Moderator": "مجری", "Moderators": "مدیران", "Monday": "دوشنبه", @@ -951,7 +951,7 @@ "Pin stream to top": "سنجاق کردن جریان به بالا", "Pin stream to top of left sidebar": "جریان را در بالا گوشه سمت چپ بچسبان", "Pinned": "سنجاق شده", - "Plan management": "", + "Plan management": "مدیریت طرح", "Plans and pricing": "طرح ها و قیمت ها", "Play sound": "پخش کردن صدا", "Please contact support for an exception or add users with a reusable invite link.": "لطفاً برای یک استثنا با پشتیبانی تماس بگیرید یا کاربرانی با لینک دعوت چندبار مصرف اضافه کنید.", @@ -1191,7 +1191,7 @@ "This bot has been deactivated.": "این ربات غیرفعال شده است.", "This conversation may have additional messages not shown in this view.": "این مکالمه ممکن است پیام‌های بیشتری داشته باشد که در اینجا نمایش داده نشود.", "This demo organization will be automatically deleted in {days_remaining} days, unless it's converted into a permanent organization.": "این سازمان آزمایشی به صورت خودکار بعد از {days_remaining} روز حذف خواهد شد مگر آنکه به یک سازمان دائمی تبدیل شود.", - "This feature is available on Zulip Cloud Plus. Upgrade to access.": "", + "This feature is available on Zulip Cloud Plus. Upgrade to access.": "این ویژگی بر روی زولیپ ابری پلاس در دسترس است. ارتقا دهید تا دسترسی داشته باشید.", "This group has no members.": "این گروه هیچ عضوی ندارد.", "This is a demo organization and will be automatically deleted in {days_remaining} days, unless it's converted into a permanent organization.": "این یک سازمان آزمایشی است و به صورت خودکار بعد از {days_remaining} روز حذف خواهد شد مگر آنکه به یک سازمان دائمی تبدیل شود.", "This is not a publicly accessible conversation.": "این یک مکالمه با دسترسی عمومی نیست.", @@ -1392,7 +1392,7 @@ "Who can send email invitations to new users": "چه کسی می‌تواند دعوتنامه ایمیلی برای کاربران جدید بفرستد", "Who can unsubscribe others from this stream?": "چه کسی می‌تواند سایرین را از این جریان لغو اشتراک کند؟", "Who can use direct messages": "چه کسی می‌تواند از پیام مستقیم استفاده کند", - "Who can view all other users in the organization": "", + "Who can view all other users in the organization": "چه کسی می‌تواند تمام کاربران دیگر در سازمان را مشاهده کند", "Why not start a conversation with yourself?": "چرا که نه، یک مکالمه با خودتان شروع کنید؟", "Why not start the conversation?": "چرا شروع مکالمه را امتحان نکنید؟", "Word": "کلمه", diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index 67884c17f590c..6a23a2c7db3df 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -16,7 +16,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Flammie , 2020,2022\n" "Language-Team: Finnish (http://app.transifex.com/zulip/zulip/language/fi/)\n" @@ -70,7 +70,7 @@ msgstr "Aloitusaika on myöhemmin kuin lopetusaika. Aloitus:{start}, Lopetus: {e msgid "No analytics data available. Please contact your server administrator." msgstr "Analytiikan tietoja ei ole saatavilla. Ota yhteyttä palvelimesi järjestelmänvalvojaan." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -141,124 +141,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} päättyy {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Tuntematon maksutapa. Ota yhteyttä {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Jotain meni pieleen. Lähetä meille sähköpostia osoitteeseen {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Jotain meni pieleen. Lataa sivu uudestaan." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Jotain meni pieleen. Odota hetki ja lataa sivu uudestaan." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Tilausta ei voi päivittää. Tilaus on vanhentunut ja korvattu uudella. " -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Tilauksen päivitys epäonnistui. Tilaus on päättäynyt." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Lisenssien manuaalinen päivitys epäonnistui. Tilauksesi on asetettu automaattiseen lisenssienhallintaan." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Tilauksesi löytyy jo lisensseistä {licenses} nykyiseltä laskutusjaksolta." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Et voi vähentää lisenssejä nykyiseltä laskutusjaksolta." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "TIlauksesi uudistaminen on jo ajastettu seuraavien lisenssien osalta {licenses_at_next_renewal}." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Ei mitään muutettavaa." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Tälle organisaatiolle ei ole asiakasta!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Istuntoa ei löytynyt" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "On oltava laskutuksen järjestelmänvalvoja tai organisaation omistaja" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Maksutarkoitusta ei löytynyt" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Anna stripe_session_id tai stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -266,33 +266,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -319,7 +323,7 @@ msgid "" msgstr "\nJos tämä virhe tuli odottamatta voit\n ottaa yhteyttä tukeen." #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Palvelimen sisäinen virhe" @@ -568,31 +572,31 @@ msgstr "Varmista, että kopioit linkin oikein selaimeesi. Jos näet tämän sivu msgid "Billing" msgstr "Laskutus" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Sulje näkymä" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Peruuta" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Vahvista" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -691,6 +695,10 @@ msgstr "Koulutuksen hinnoittelu" msgid "View pricing" msgstr "Katso hinnoittelu" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip yrityksille" @@ -832,8 +840,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2255,7 +2261,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2264,7 +2269,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2272,7 +2277,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2288,7 +2292,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2305,15 +2308,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3358,65 +3370,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Virheellinen viesti(t)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Viestiä ei kyetä esittämään" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Odotettiin täsmälleen yhtä kanavaa" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Virheellinen tietotyyppi kanavalle" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Virheellinen tieto vastaanottajille" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Vastaanottajalista voi sisältää sähköpostiosoitteita tai käyttäjätunnuksia, mutta ei molempia." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Bottisi {bot_identity} yritti lähettää viestiä kanvatunnukselle {stream_id}, mutta kanavaa ei ole olemassa." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Bottisi {bot_identity} yritti lähettää viestiä kanavalle {stream_name}, mutta tätä kanavaa ei ole olemassa. Luo se napsauttamalla [tästä]({new_stream_link})." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Bottisi {bot_identity} yritti lähettää viestiä kanavalle {stream_name}. Kanava on olemassa mutta sillä ei ole yhtään tilaajaa." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Suoraviestit on poistettu käytöstä tässä organisaatiossa" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Aiheet ovat pakollisia tässä organisaatiossa" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgetit: API-ohjelmoija lähetti virheellisen JSON-sisällön" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgetit: {error_msg}" @@ -3433,28 +3445,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3731,7 +3743,7 @@ msgstr "Virhe tapahtui liitettä poistettaessa. Yritä myöhemmin uudelleen." msgid "Message must have recipients!" msgstr "Viestillä tulee olla vastaanottajiat!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3910,7 +3922,7 @@ msgid "API usage exceeded rate limit" msgstr "APIn käyttö ylitti raja-arvon" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Virheellisesti muotoiltu JSON" @@ -4365,7 +4377,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token ei ole olemassa" @@ -4418,7 +4430,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Käyttäjällä ei ole valtuutusta tähän kyselyyn" @@ -4465,7 +4477,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4578,7 +4590,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} ei ole päivämäärä" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} ei ole sanakirja" @@ -4616,7 +4628,7 @@ msgid "{var_name} is too large" msgstr "{var_name} on liian pitkä" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} ei ole lista-tyyppinen" @@ -4780,7 +4792,7 @@ msgstr "Virheellinen bottityyppi" msgid "Invalid interface type" msgstr "Virheellinen käyttöliittymätyyppi" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4843,46 +4855,46 @@ msgstr "{var_name} ei ole allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} on väärin)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} ei ole URL-osoite" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "URL-kuvion pitää sisältää %(username)s" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' ei voi olla tyhjä." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Kentällä ei voi olla samaa valintaa monesti." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' ei ole kelvollinen vaihtoehto kentälle '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} ei ole merkkijono tai kokonaislukulista" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} ei ole merkkijono tai kokonaisluku" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} ei ole pituutta" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} puuttuu" @@ -5071,57 +5083,57 @@ msgstr "Vain organisaation järjestelmänvalvojat ja moderaattorit voivat lähet msgid "Only organization full members can post" msgstr "Vain organisaation täysivaltaiset jäsenet voivat lähettää viestejä" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode-emoji" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Mukautetut emojit" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulipin ekstraemoji" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Lista vaihtoehdoista" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Henkilövalitsin" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Lyhyt teksti" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Pitkä teksti" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Päivämäärän valitsin" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Linkki" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Ulkoinen tili" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Pronominit" @@ -5137,20 +5149,20 @@ msgstr "tuntematon käyttöjärjestelmä" msgid "An unknown browser" msgstr "Tuntematon selain" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Puuttuva 'queue_id' argumentti" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Puuttuva 'last_event_id' argumentti" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "{event_id} uudempi tapahtuma on jo leikattu!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Tapahtuma {event_id} ei ollut tässä jonossa" @@ -5322,19 +5334,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Puuttuva lähettäjä" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Peilaus ei ole sallittu vastaanottajan käyttäjätunnukselle" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Virheellinen toistettu viesti" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Zephyr-peilaus ei ole sallittua tässä organisaatiossa" @@ -5400,26 +5412,26 @@ msgstr "Vähintään yksi argumentti seuraavista on oltava: emoji_name, emoji_co msgid "Read receipts are disabled in this organization." msgstr "Vastaanottoilmoitukset on poistettu käytössä tässä organisaatiossa" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Vähintään yksi todennusmenetelmä on oltava käytössä." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "On oltava demo-organisaatio." @@ -5897,11 +5909,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Virheellinen aliverkkotunnus push-ilmoituksen ohjaajalle" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Täytyy vahvistaa oikealla Zulip-palvelimen API-avaimella" @@ -5915,43 +5927,43 @@ msgstr "Virheellinen UUID" msgid "Invalid token type" msgstr "Virheellinen token-tyyppi" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "user_id tai user_uuid puuttuu" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Tiedot ovat epäkunnossa." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 21c6cf7d831a2..5500b9b4c2944 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -33,7 +33,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Tim Abbott , 2018,2020-2023\n" "Language-Team: French (http://app.transifex.com/zulip/zulip/language/fr/)\n" @@ -87,7 +87,7 @@ msgstr "L'heure de début est postérieure à l'heure de fin. Début : {start}, msgid "No analytics data available. Please contact your server administrator." msgstr "Aucune donnée d'analyse disponible. Veuillez contacter votre administrateur système." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -158,124 +158,124 @@ msgstr "L'enregistrement est désactivé" msgid "Invalid remote server." msgstr "Serveur à distance non valide." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "Vous devez acheter des licenses pour tous les utilisateurs actifs dans votre organisation (minimum {min_licenses})" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Les factures comportant plus de {max_licenses} licences ne peuvent pas être traitées à partir de cette page. Pour terminer la mise à niveau, veuillez contacter {email}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "Aucune méthode de paiement n'est enregistrée." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} termine dans {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Méthode de paiement inconnue. Merci de contacter {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Quelque chose s'est mal passé. Merci de contacter {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Quelque chose s'est mal passé. Merci de recharger la page." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Quelque chose s'est mal passé. Merci de patienter quelques secondes." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "Veuillez ajouter une carte de crédit avant de commencer votre essai gratuit." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Impossible de mettre le plan à jour. Le plan a expiré et a été remplacé par un nouveau plan." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Impossible de mettre le plan à jour. Ce type de plan n'est plus disponible." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Impossible de mettre à jour les licences manuellement. Votre plan induit une gestion automatique des licences." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Votre forfait comprend déjà {licenses} licences pour la période de facturation en cours." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Vous ne pouvez pas réduire les licences dans la période de facturation en cours." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Le renouvellement de votre plan est déjà programmé avec {licenses_at_next_renewal} licences." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Rien à changer" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Aucun client pour cette organisation !" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Session introuvable" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Doit être un gestionnaire de facturation ou un propriétaire d'organisation" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Intention de paiement introuvable" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Passer le stripe_session_id ou le stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -283,33 +283,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "La demande d'hébergement sponsorisé de votre organisation a été approuvée ! Vous avez été mis à niveau vers {plan_name}, gratuitement. {emoji}\n\nSi vous pouviez {begin_link} mentionner Zulip comme sponsor sur votre site web {end_link}, nous vous en serions très reconnaissants !" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "Le jeton d'accès à la facturation a expiré." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "Jeton d'accès à la facturation non valide." -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "L'enregistrement de votre serveur a été désactivé." @@ -336,7 +340,7 @@ msgid "" msgstr "\nSi cette erreur est inattendue, vous pouvez contacter l'assistance ." #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Erreur de serveur interne" @@ -585,31 +589,31 @@ msgstr "Assurez-vous d'avoir correctement copié le lien dans votre navigateur. msgid "Billing" msgstr "Facturation" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Fermer le modal" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Annuler" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "Rétrograder" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Confirmer" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "Peu importe" @@ -708,6 +712,10 @@ msgstr "Tarifs éducation" msgid "View pricing" msgstr "Voir les prix" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip pour les entreprises" @@ -849,8 +857,6 @@ msgstr "Vous possédez déjà un compte ?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2272,7 +2278,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2281,7 +2286,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2289,7 +2294,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2305,7 +2309,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2322,15 +2325,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3375,65 +3387,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Message(s) invalide(s)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Impossible d'afficher le message" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Exactement un canal attendu" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Type de données non valide pour un canal" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Type de données non valide pour un destinataire" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Les listes de destinataires peuvent intégrer des adresses courriel ou ID d'utilisateurs, mais pas les deux." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Votre robot {bot_identity} a essayé d'envoyer un message sur le canal avec l'ID {stream_id}, mais aucun canal n'existe avec cette ID." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Votre robot {bot_identity} a essayé d'envoyer un message sur le canal {stream_name}, mais ce canal n'existe pas. Cliquez [ici]({new_stream_link}) pour le créer." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Votre robot {bot_identity} a essayé d'envoyer un message sur le canal {stream_name}. Ce canal existe mais n'a aucun abonné." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Les messages directs sont désactivés dans cette organisation." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Les sujets sont obligatoires dans cette organisation" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: Contenu JSON non valide reçu du programmeur de l'API" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets : {error_msg}" @@ -3450,28 +3462,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "Le message programmé a déjà été envoyé" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "La date de livraison prévue doit se situer dans le futur." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "Le message ne peut pas être envoyé à l'heure programmée." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "Ce message que vous avez programmé pour {delivery_datetime} n'a pas été envoyé à cause de cette erreur :" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "[Voir les messages programmés](#scheduled)" @@ -3748,7 +3760,7 @@ msgstr "Une erreur est survenue pendant la suppression de la pièce-jointe. Merc msgid "Message must have recipients!" msgstr "Le message doit avoir un destinataire !" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3927,7 +3939,7 @@ msgid "API usage exceeded rate limit" msgstr "L'utilisation de l'API a dépassé la limite" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON malformé" @@ -4382,7 +4394,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Le jeton n'existe pas." @@ -4435,7 +4447,7 @@ msgstr "La liste des destinataires ne peut contenir que des identifiants d'utili #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "L'utilisateur n'est pas autorisé pour cette requête" @@ -4482,7 +4494,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "Le message programmé n'existe pas" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4595,7 +4607,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} n'est pas une date" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} n'est pas un dict" @@ -4633,7 +4645,7 @@ msgid "{var_name} is too large" msgstr "{var_name} est trop grand" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} n'est pas une liste" @@ -4797,7 +4809,7 @@ msgstr "Type de robot non valide" msgid "Invalid interface type" msgstr "Type d'interface non valide" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4860,46 +4872,46 @@ msgstr "{var_name} n'est pas un allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} est faux)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} n'est pas une URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "Le modèle d'URL doit contenir "%(username)s"." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' ne peut pas être vide." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Le champ ne doit pas avoir de choix en double." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' n'est pas une option valide pour '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} n'est ni une chaîne ni une liste d'entiers" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} n'est ni une chaîne ni un entier" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} n'a pas de longueur" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} est manquant" @@ -5088,57 +5100,57 @@ msgstr "Seuls les administrateurs de l'organisation et les modérateurs peuvent msgid "Only organization full members can post" msgstr "Seuls les membres complets peuvent envoyer des messages" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Emoji unicode" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoji personnalisé" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Emoji spécial Zulip" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Liste des options" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Sélecteur de personne" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Texte court" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Texte long" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Sélecteur de date" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Lien" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Compte externe" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Pronoms" @@ -5154,20 +5166,20 @@ msgstr "un système d'exploitation inconnu" msgid "An unknown browser" msgstr "Un navigateur inconnu" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Argument 'queue_id' manquant" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Argument 'last_event_id' manquant" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Un événement plus récent que {event_id} a déjà été nettoyé !" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "L'événement {event_id} introuvable dans cette file d'attente" @@ -5339,19 +5351,19 @@ msgstr "L'ancre ne peut être exclue qu'à une extrémité de la plage" msgid "No such topic '{topic}'" msgstr "Aucun sujet tel que '{topic}'" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Expéditeur manquant" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Mise en mirror non autorisée avec un ID utilisateur de destinataire" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Message mirroir non valide" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Le mirroir Zephyr n'est pas autorisé dans cette organisation." @@ -5417,26 +5429,26 @@ msgstr "Au moins un des arguments suivants doit être présent : emoji_nam, emoj msgid "Read receipts are disabled in this organization." msgstr "Les confirmations de lecture sont désactivées dans cette organisation." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "Langage non valide '{language}'" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Au moins une méthode d'authentification doit être activée." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "video_chat_provider non valide {video_chat_provider}" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "giphy_rating non valide {giphy_rating}" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Doit être une organisation de démonstration." @@ -5914,11 +5926,11 @@ msgid "" "exports]({export_settings_link})." msgstr "Votre exportation de données est terminée. [Voir et télécharger les exportations]({export_settings_link})." -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Sous-domaine non valide pour le serveur de relai des notifications push" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr " À valider avec une clé API valide de serveur Zulip" @@ -5932,43 +5944,43 @@ msgstr "UUID non valide" msgid "Invalid token type" msgstr "Type de jeton invalide" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "{hostname} n'est pas un nom d'hôte valide" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "ios_app_id manquant" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "User_id ou user_uuid manquant" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "Propriété non valide {property}" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Les données sont hors d'ordre." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "Double enregistrement détecté." -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "Données de journal d'audit malformées" diff --git a/locale/fr/translations.json b/locale/fr/translations.json index ed17d4aa2f4bd..fa0a2bce41d6c 100644 --- a/locale/fr/translations.json +++ b/locale/fr/translations.json @@ -135,9 +135,9 @@ "All messages including muted streams": "Tous les messages incluant les canaux rendus muets", "All streams": "Tous les canaux", "All time": "Tout le temps", - "All topics": "", - "All unmuted topics": "", - "All unread messages": "", + "All topics": "Tous les sujets", + "All unmuted topics": "Tous les sujets non muets", + "All unread messages": "Tous les messages non lus", "All users will need to log in again at your new organization URL.": "", "Allow creating web-public streams (visible to anyone on the Internet)": "Autoriser la création de canaux publics sur le Web (visibles par tous sur Internet)", "Allow message content in message notification emails": "Inclure de contenu des messages dans les courriels de notification", @@ -322,9 +322,9 @@ "Cycle between stream narrows": "Basculer entre les canaux", "DIRECT MESSAGES": "MESSAGES DIRECTS", "DM": "MP", - "DMs and mentions": "", + "DMs and mentions": "MPs et mentions", "DMs, mentions, and alerts": "MPs, mentions et alertes", - "DMs, mentions, and followed topics": "", + "DMs, mentions, and followed topics": "MPs, mentions et sujets suivis", "Dark": "Sombre", "Dark theme": "Thème sombre", "Dark theme logo": "Logo thème sombre", @@ -545,8 +545,8 @@ "Filter uploads": "Filtrer les envois", "Filter users": "Filtrer les utilisateurs", "First message": "Premier message", - "First time? Read our guidelines for creating and naming streams.": "Première fois? Lisez nos instructions pour créer et nommer des canaux.", - "First time? Read our guidelines for creating user groups.": "Première fois? Lisez nosdirectives pour créer des groupes d'utilisateurs.", + "First time? Read our guidelines for creating and naming streams.": "Première fois ? Lisez nos instructions pour créer et nommer des canaux.", + "First time? Read our guidelines for creating user groups.": "Première fois ? Lisez nosdirectives pour créer des groupes d'utilisateurs.", "Follow": "Suivre", "Followed": "Suivi", "Followed topics": "Sujets suivis", @@ -582,7 +582,7 @@ "Government": "Gouvernement", "Grant Zulip the Kerberos tickets needed to run your Zephyr mirror via Webathena": "Accorder à Zulip le ticket Kerberos nécessaire pour faire fonctionner le mirroir Zephyr via Webathena", "Group permissions": "", - "Group settings": "", + "Group settings": "Paramètres du groupe", "Guest": "Invité", "Guests": "Invités", "Guests cannot edit custom emoji.": "Les invités ne peuvent pas modifier un émoji personalisé.", @@ -613,7 +613,7 @@ "Include content of direct messages in desktop notifications": "Inclure le contenu des messages directs dans les notifications de bureau", "Include message content in message notification emails": "Inclure de contenu des messages dans les courriels de notification", "Include organization name in subject of message notification emails": "Inclure le nom de l'organisation dans le sujet des courriels de notification", - "Includes muted streams and topics": "", + "Includes muted streams and topics": "Inclut les canaux et les sujets muets", "Initiate a search": "Lancer une recherche", "Insert new line": "Insérer une nouvelle ligne", "Integration": "", @@ -900,7 +900,7 @@ "Only stream members can add users to a private stream.": "Seuls les membres du canal peuvent ajouter des utilisateurs à un canal privé.", "Only subscribers can access or join private streams, so you will lose access to this stream if you convert it to a private stream while not subscribed to it.": "", "Only subscribers to this stream can edit stream permissions.": "Seuls les abonnés à ce canal peuvent modifier les permissions du canal.", - "Only topics you follow": "", + "Only topics you follow": "Seulement les sujets que vous suivez", "Open": "Ouvrir", "Open help menu": "Ouvrir le menu d'aide", "Open message menu": "Ouvrir le menu des messages", @@ -966,7 +966,7 @@ "Please specify at least one valid recipient.": "Veuillez indiquer au moins un destinataire valide.", "Political group": "Groupe politique", "Posted by {full_name}": "Publié par {full_name}", - "Preferences": "", + "Preferences": "Préférences", "Press > for list of topics": "Appuyez sur > pour afficher la liste des sujets", "Prevent users from changing their avatar": "Empêcher les utilisateurs de modifier leur avatar", "Prevent users from changing their email address": "Empêcher les utilisateurs de changer leur adresse courriel", @@ -1122,14 +1122,14 @@ "Sort by unread message count": "", "Spoiler": "Contenu masqué", "Sponsorship request pending": "", - "Standard view": "", + "Standard view": "Vue standard", "Star": "Mettre en favori", "Star selected message": "Mettre en favori le message sélectionné", "Star this message": "Marquer ce message comme favori", "Starred messages": "Messages favoris", "Start a new topic or select one from the list.": "Commencez un nouveau sujet ou sélectionnez-en un dans la liste.", "Start export of public data": "Lancer l'exportation des données publiques", - "Start new conversation": "", + "Start new conversation": "Démarrer une nouvelle conversation", "Status": "Statut", "Stream": "Canal", "Stream color": "Couleur du canal", @@ -1249,9 +1249,9 @@ "Topic notifications": "", "Topic settings": "Paramètre du sujet", "Topics": "Sujets", - "Topics I participate in": "", - "Topics I send a message to": "", - "Topics I start": "", + "Topics I participate in": "Sujets auxquels je participe", + "Topics I send a message to": "Sujets auxquels j'envoie un message", + "Topics I start": "Sujets que je commence", "Topics are required in this organization.": "Les sujets sont obligatoires dans cette organisation.", "Topics marked as resolved": "Sujets marqués comme résolus", "Tuesday": "Mardi", diff --git a/locale/gl/LC_MESSAGES/django.po b/locale/gl/LC_MESSAGES/django.po index ebb6a285eb35c..00e20259896ca 100644 --- a/locale/gl/LC_MESSAGES/django.po +++ b/locale/gl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:58+0000\n" +"POT-Creation-Date: 2023-12-15 17:44+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -61,7 +61,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -129,124 +129,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this " "page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new " "plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2901 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You " @@ -256,33 +256,37 @@ msgid "" "we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:136 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:138 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:275 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:280 +#: corporate/views/remote_billing_page.py:693 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:506 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:513 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:517 msgid "Your server registration has been deactivated." msgstr "" @@ -310,7 +314,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -567,31 +571,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:313 templates/corporate/billing.html:339 +#: templates/corporate/billing.html:368 templates/corporate/billing.html:397 +#: templates/corporate/billing.html:423 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:324 templates/corporate/billing.html:353 +#: templates/corporate/billing.html:382 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:326 templates/corporate/billing.html:410 +#: templates/corporate/billing.html:433 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:355 templates/corporate/billing.html:384 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:408 templates/corporate/billing.html:431 msgid "Never mind" msgstr "" @@ -690,6 +694,62 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:4 +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:16 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:4 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:16 +msgid "Plan management not available" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:18 +#, python-format +msgid "" +" Plan management is not available for this\n" +" organization, because your Zulip server is already " +"on a\n" +" %(server_plan_name)s plan, which covers all\n" +" organizations on this server. Follow the log\n" +" in instructions\n" +" for All older versions\n" +" of the Zulip server to manage your plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:29 +msgid "" +" To move the plan from the server to this\n" +" organization, or for other questions, contact support.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:18 +msgid "" +"\n" +" Plan management for this server is not available " +"because at least one organization\n" +" hosted on this server already has an active plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:24 +#, python-format +msgid "" +"\n" +" Log in to plan management for your\n" +" organization instead, or contact support with any questions.\n" +" " +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -832,8 +892,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2277,7 +2335,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2286,7 +2343,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2294,7 +2351,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2310,7 +2366,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2327,16 +2382,25 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for " -"%(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2367,7 +2431,7 @@ msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " -"appreciate it!." +"appreciate it!" msgstr "" #: templates/zerver/find_account.html:4 @@ -3396,65 +3460,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but " "that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The " "stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3471,27 +3535,27 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 -#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:128 +#: zerver/actions/scheduled_messages.py:155 +#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3770,7 +3834,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3949,7 +4013,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4402,45 +4466,45 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:739 zilencer/views.py:241 msgid "Token does not exist" msgstr "" -#: zerver/lib/push_notifications.py:902 +#: zerver/lib/push_notifications.py:910 msgid "" "This organization has disabled including message content in mobile push " "notifications" msgstr "" -#: zerver/lib/push_notifications.py:1015 +#: zerver/lib/push_notifications.py:1023 #, python-brace-format msgid "{full_name} mentioned @{user_group_name}:" msgstr "" -#: zerver/lib/push_notifications.py:1019 +#: zerver/lib/push_notifications.py:1027 #, python-brace-format msgid "{full_name} mentioned you:" msgstr "" -#: zerver/lib/push_notifications.py:1026 +#: zerver/lib/push_notifications.py:1034 #, python-brace-format msgid "{full_name} mentioned everyone:" msgstr "" -#: zerver/lib/push_notifications.py:1436 +#: zerver/lib/push_notifications.py:1438 msgid "Test notification" msgstr "" -#: zerver/lib/push_notifications.py:1437 +#: zerver/lib/push_notifications.py:1439 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." msgstr "" -#: zerver/lib/push_notifications.py:1488 +#: zerver/lib/push_notifications.py:1490 msgid "Device not recognized" msgstr "" -#: zerver/lib/push_notifications.py:1500 +#: zerver/lib/push_notifications.py:1502 msgid "Device not recognized by the push bouncer" msgstr "" @@ -4455,7 +4519,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4502,7 +4566,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4615,7 +4679,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4653,7 +4717,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4880,46 +4944,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5108,15 +5172,15 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" @@ -5174,20 +5238,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5359,19 +5423,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5437,26 +5501,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5934,61 +5998,61 @@ msgid "" "({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" #. error -#: zilencer/views.py:77 zilencer/views.py:79 +#: zilencer/views.py:81 zilencer/views.py:83 msgid "Invalid UUID" msgstr "" #. error -#: zilencer/views.py:84 +#: zilencer/views.py:88 msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:126 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:182 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:185 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:610 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:613 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:620 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:696 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:957 msgid "" "Failed to migrate customer from server to realms. Please contact support for " "assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:1009 msgid "Malformed audit log data" msgstr "" diff --git a/locale/hi/LC_MESSAGES/django.po b/locale/hi/LC_MESSAGES/django.po index 0ad8b70dedc27..0491f9e341590 100644 --- a/locale/hi/LC_MESSAGES/django.po +++ b/locale/hi/LC_MESSAGES/django.po @@ -14,7 +14,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Aruna Maurya, 2022\n" "Language-Team: Hindi (http://app.transifex.com/zulip/zulip/language/hi/)\n" @@ -68,7 +68,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "कोई विश्लेषण डेटा उपलब्ध नहीं है। कृपया अपने सर्वर व्यवस्थापक से संपर्क करें।" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -139,124 +139,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "अज्ञात भुगतान विधि। कृपया {email} से संपर्क करें।" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "कुछ गलत हो गया। कृपया पृष्ठ को फिर से लोड करें।" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "कुछ गलत हो गया। कृपया कुछ सेकंड प्रतीक्षा करें और पुनः प्रयास करें।" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -264,33 +264,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -317,7 +321,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -566,31 +570,31 @@ msgstr "" msgid "Billing" msgstr "बिलिंग" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "रद्द करना" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -689,6 +693,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -830,8 +838,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2253,7 +2259,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2262,7 +2267,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2270,7 +2275,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2286,7 +2290,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2303,15 +2306,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3356,65 +3368,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3431,28 +3443,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3729,7 +3741,7 @@ msgstr "कुर्की हटाते समय एक त्रुटि msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3908,7 +3920,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4363,7 +4375,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4416,7 +4428,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4463,7 +4475,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4576,7 +4588,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4614,7 +4626,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4778,7 +4790,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4841,46 +4853,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5069,57 +5081,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5135,20 +5147,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5320,19 +5332,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5398,26 +5410,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5895,11 +5907,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5913,43 +5925,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/hu/LC_MESSAGES/django.po b/locale/hu/LC_MESSAGES/django.po index f6651da593c51..fcd6e8963df57 100644 --- a/locale/hu/LC_MESSAGES/django.po +++ b/locale/hu/LC_MESSAGES/django.po @@ -16,7 +16,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Eekain, 2021-2022\n" "Language-Team: Hungarian (http://app.transifex.com/zulip/zulip/language/hu/)\n" @@ -70,7 +70,7 @@ msgstr "A kezdés ideje a befejezési idő utánra esik. Kezdés: {start}, befej msgid "No analytics data available. Please contact your server administrator." msgstr "Nincs elemzési adat. Lépj kapcsolatba a szerver adminisztrátorával!" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -141,124 +141,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} végződése {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Ismeretlen fizetési mód. Kérjük, lépjen kapcsolatba a következővel: {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Valami elromlott. Kérjük, lépjen kapcsolatba a következővel: {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Valami gond van. Töltsd újra az oldalt!" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Valami gond van. Várj pár másodpercet és próbáld újra." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Szerződésmódosítás nem lehetséges. A szerződés lejárt, vagy új szerződés köttetett." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Szerződésmódosítás nem lehetséges. A szerződés lejárt." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Nem tudsz manuálisan változtatni a lincenszek számán. A szerződésed automatikus licensz-osztásra szól." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "A szerződésed szerint már {licenses} licensz érhető el a jelen számlázási időszakban." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Ebben a számlázási időszakban már nem csökkentheted licenszeid számát." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "A szerződésed már ütemezetten átáll {licenses_at_next_renewal} licenszre." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Nincs változtatnivaló." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Ennél a szervezetnél nincs felhasználó!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Böngészési folyamat nem található" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Számlázási adminisztrátornak vagy a szervezet tulajdonosának kell lenned" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Fizetési szándék nem található" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Írd be a stripe_session_id vagy stripe_payment_intent_id változót" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -266,33 +266,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -319,7 +323,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Belső kiszolgáló hiba." @@ -568,31 +572,31 @@ msgstr "Győződj meg róla, hogy helyesen másoltad be a linket a böngészőbe msgid "Billing" msgstr "Számlázás" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Felhatalmazások bezárása" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Mégse" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Megerősít" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -691,6 +695,10 @@ msgstr "Oktatási áraink" msgid "View pricing" msgstr "Áraink" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip üzleti használatra" @@ -832,8 +840,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2255,7 +2261,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2264,7 +2269,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2272,7 +2277,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2288,7 +2292,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2305,15 +2308,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3358,65 +3370,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Érvénytelen üzenet(ek)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Az üzenet megjelenítése nem sikerült" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Csak egy üzenetfolyamra van szükség" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Érvénytelen adattípus az üzenetfolyam számára" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Érvénytelen adattípus a címzettek számára" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "A címzettek listája vagy e-maileket, vagy felhasználói azonosítókat tartalmazhat, de ezek nem vegyíthetőek." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "A/az {bot_identity} robot megpróbált üzenetet küldeni a/az {stream_id} üzenetfolyam ID-vel, de ilyen azonosítójú üzenetfolyam nem létezik." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "A/az {bot_identity} robot megpróbált üzenetet küldeni a/az {stream_name} üzenetfolyamba, de az nem létezik. Kattints [ide] ({new_stream_link}) a létrehozásához." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "A/az {bot_identity} robot megpróbált üzenetet küldeni a/az {stream_name} üzenetfolyamba. Az üzenetfolyam létezik, de nincsenek feliratkozói." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgetek: az API programozó érvénytelen JSON tartalmat küldött" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgetek: {error_msg}" @@ -3433,28 +3445,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3731,7 +3743,7 @@ msgstr "Hiba történt a csatolmány törlése közben. Próbáld újra később msgid "Message must have recipients!" msgstr "Az üzenethez tartoznia kell címzettnek!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3910,7 +3922,7 @@ msgid "API usage exceeded rate limit" msgstr "Az API használat túllépte a limitet" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Hibás formátumú JSON" @@ -4365,7 +4377,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "A token nem létezik" @@ -4418,7 +4430,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Erre a lekérdezésre a felhasználó nem jogosult" @@ -4465,7 +4477,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4578,7 +4590,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} nem dátum" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} nem dict" @@ -4616,7 +4628,7 @@ msgid "{var_name} is too large" msgstr "{var_name} túl nagy" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} nem lista" @@ -4780,7 +4792,7 @@ msgstr "Érvénytelen robot típus" msgid "Invalid interface type" msgstr "Érvénytelen interfész típus" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4843,46 +4855,46 @@ msgstr "{var_name} nem allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} hibás)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} nem URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' nem lehet üres." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' nem érvényes választási lehetőség '{field_name}' számára" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} nem string vagy integer lista" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} nem string vagy integer" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} hiányzik" @@ -5071,57 +5083,57 @@ msgstr "Csak szervezeti adminisztrátorok és moderátorok üzenhetnek" msgid "Only organization full members can post" msgstr "Csak a szervezeti teljes jogú tagjai üzenhetnek" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode hangulatjel" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Egyedi hangulatjel" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip extra hangulatjel" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Opciók listája" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Személy kiválasztó" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Rövid szöveg" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Hosszú szöveg" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Dátum kiválasztó" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Külső fiók" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5137,20 +5149,20 @@ msgstr "egy ismeretlen operációs rendszer" msgid "An unknown browser" msgstr "Egy ismeretlen böngésző" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Hiányzó 'queue_id' paraméter" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Hiányzó 'last_event_id' paraméter" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Egy eseményt ami újabb, mint {event_id} már töröltek!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Az esemény {event_id} ebben a sorban nem található" @@ -5322,19 +5334,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Hiányzó küldő" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Tükrözés nem engedélyezett a címzett felhasználó azonosítókkal" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Érvénytelen tükrözött üzenet" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Zephyr tükrözés nem engedélyezett ebben a szervezetben." @@ -5400,26 +5412,26 @@ msgstr "Legalább egyet meg kell adni ezek közül: emoji_name, emoji_code" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Valamilyen authentikációs lehetőséget meg kell adni." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Csak bemutató szerveződés lehet." @@ -5897,11 +5909,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Érvénytelen aldomain a push értesítés bouncerhez" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Érvényesítsd a Zulip szervert érvényes API kulccsal" @@ -5915,43 +5927,43 @@ msgstr "Érvénytelen UUID" msgid "Invalid token type" msgstr "Érvénytelen token típus" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Váratlan adatrend." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/id/LC_MESSAGES/django.po b/locale/id/LC_MESSAGES/django.po index f25a3820cb6d0..4558d7e2627ac 100644 --- a/locale/id/LC_MESSAGES/django.po +++ b/locale/id/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Tim Abbott , 2018\n" "Language-Team: Indonesian (http://app.transifex.com/zulip/zulip/language/id/)\n" @@ -65,7 +65,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "Data analisis tidak tersedia. Mohon hubungi administrator server anda." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -136,124 +136,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -261,33 +261,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -314,7 +318,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Eror internal server" @@ -563,31 +567,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Batal" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -686,6 +690,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -827,8 +835,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2250,7 +2256,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2259,7 +2264,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2267,7 +2272,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2283,7 +2287,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2300,15 +2303,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3353,65 +3365,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Pesan invalid" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Tidak dapat menampilkan pesan." -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3428,28 +3440,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3726,7 +3738,7 @@ msgstr "Sebuah eror telah terjadi saat menghapus lampiran tersebut. Silakan coba msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3905,7 +3917,7 @@ msgid "API usage exceeded rate limit" msgstr "Penggunaan API melewati batas" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Kesalahan bentuk JSON" @@ -4360,7 +4372,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token tidak ada" @@ -4413,7 +4425,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Pengguna tidak berotoritas untuk melakukan perintah ini." @@ -4460,7 +4472,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4573,7 +4585,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4611,7 +4623,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4775,7 +4787,7 @@ msgstr "Tipe robot invalid" msgid "Invalid interface type" msgstr "Tipe antarmuka invalid" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4838,46 +4850,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5066,57 +5078,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Emoji unicode" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoji khusus" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Ekstra emoji Zulip" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5132,20 +5144,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Tidak ada argumen 'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Tidak ada argumen 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5317,19 +5329,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Pengirim tidak ada" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5395,26 +5407,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Setidaknya satu metode autentikasi harus diaktifkan" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5892,11 +5904,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Subdomain invalid untuk bouncer push notifications" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5910,43 +5922,43 @@ msgstr "" msgid "Invalid token type" msgstr "Tipe token invalid" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/it/LC_MESSAGES/django.po b/locale/it/LC_MESSAGES/django.po index 5181ba8367e7d..d5be31759f9c2 100644 --- a/locale/it/LC_MESSAGES/django.po +++ b/locale/it/LC_MESSAGES/django.po @@ -18,7 +18,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Andrea, 2020-2023\n" "Language-Team: Italian (http://app.transifex.com/zulip/zulip/language/it/)\n" @@ -72,7 +72,7 @@ msgstr "L'ora di inizio è successiva all'ora di fine. Inizio: {start}, Fine: {e msgid "No analytics data available. Please contact your server administrator." msgstr "Nessun dato analitico disponibile. Si prega di contattare l'amministratore del server." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -143,124 +143,124 @@ msgstr "La registrazione è disattivata" msgid "Invalid remote server." msgstr "Server remoto non valido." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "È necessario acquistare licenze per tutti gli utenti attivi nella tua organizzazione (minimo {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Le fatture con più di {max_licenses} licenze non possono essere elaborate da questa pagina. Per completare l'aggiornamento, contattare {email}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "Nessun metodo di pagamento registrato." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} termina in {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Metodo di pagamento sconosciuto. Si prega di contattare {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Qualcosa è andato storto. Per favore contatta {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Qualcosa è andato storto. Si prega di ricaricare la pagina." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Qualcosa è andato storto. Per favore attendi qualche secondo e riprova." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "Si prega di aggiungere una carta di credito prima di iniziare la prova gratuita." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "Per favore aggiungi una carta di credito per pianificare l'aggiornamento." -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Impossibile aggiornare il piano. Il piano è scaduto e sostituito con un nuovo piano." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Impossibile aggiornare il piano. Il piano è terminato." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "Non è possibile aggiornare le licenze nel periodo di fatturazione corrente per il piano di prova gratuito." -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Impossibile aggiornare le licenze manualmente. Il tuo piano prevede la gestione automatica delle licenze." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Il tuo piano è già su licenze {licenses} nel periodo di fatturazione corrente." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Non è possibile ridurre le licenze nel periodo di fatturazione corrente." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "Non è possibile modificare le licenze per il prossimo ciclo di fatturazione per un piano che sta venendo declassato." -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Il tuo piano è già programmato per il rinnovo con licenze {licenses_at_next_renewal}." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Niente da cambiare." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Nessun cliente per questa organizzazione!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Sessione non trovata" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Deve essere un responsabile della fatturazione o un proprietario dell'organizzazione" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Tentativo di pagamento non trovato" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Passare stripe_session_id o stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -268,33 +268,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "La richiesta della tua organizzazione per l'hosting sponsorizzato è stata approvata! Sei stato aggiornato a {plan_name}, gratuitamente. {emoji}\n\nSe potessi {begin_link}indicare Zulip come sponsor sul tuo sito web{end_link}, saremmo davvero grati!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "Il parametro 'confirmed' è richiesto" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "L'accesso al token di fatturazione è scaduto." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "Token di accesso alla fatturazione non valido." -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." -msgstr "" +msgstr "L'account utente non esiste ancora." -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "Devi accettare i Termini di Servizio per procedere." -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." -msgstr "" +msgstr "Questo zulip_org_id non è registrato nel sistema di gestione della fatturazione di Zulip." -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." -msgstr "" +msgstr "Chiave zulip_org_key non valida per questo zulip_org_id." -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "La registrazione del tuo server è stata disattivata." @@ -321,7 +325,7 @@ msgid "" msgstr "\n Se questo errore è imprevisto, puoi\n contattare il supporto.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Internal server error" @@ -570,31 +574,31 @@ msgstr "Assicurati di aver copiato correttamente il link nel tuo browser. Se sta msgid "Billing" msgstr "Fatturazione" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Chiudi finestra" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Annulla" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "Downgrade" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Confermare" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "Non importa" @@ -693,6 +697,10 @@ msgstr "Prezzi per l'istruzione" msgid "View pricing" msgstr "Visualizza i prezzi" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "Disattivare la registrazione del server?" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip per business" @@ -834,8 +842,6 @@ msgstr "Hai già un account?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2247,53 +2253,50 @@ msgstr "Clicca sul link sotto per riattivare la tua organizzazione." msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "Tu o qualcuno per tuo conto ha richiesto un link di accesso per gestire il piano Zulip per %(remote_server_hostname)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:12 msgid "" "\n" " Click the button below to log in.\n" " " -msgstr "" +msgstr "\n Clicca il pulsante qui sotto per accedere.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" " This link will expire in %(validity_in_hours)s hours.\n" " " -msgstr "" +msgstr "\n Questo link scadrà in %(validity_in_hours)s ore.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Domande? Scopri di più o contatta %(billing_contact_email)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" -msgstr "" +msgstr "Accedi alla gestione del piano Zulip" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:1 #, python-format msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "Tu o qualcun altro per tuo conto, ha richiesto un link di accesso per gestire il piano Zulip per %(remote_server_hostname)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 msgid "Click the link below to log in." -msgstr "" +msgstr "Clicca sul link sottostante per accedere." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." -msgstr "" +msgstr "Questo link scadrà tra %(validity_in_hours)s ore." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:8 #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:6 @@ -2301,22 +2304,31 @@ msgstr "" msgid "" "Questions? Learn more at %(billing_help_link)s or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Domande? Per saperne di più, visita %(billing_help_link)s o contatta %(billing_contact_email)s." #: templates/zerver/emails/remote_realm_billing_confirm_login.html:9 #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " -msgstr "" +msgstr "\n Fai clic sul pulsante in basso per confermare la tua email e accedere alla gestione del piano Zulip per %(remote_realm_host)s.\n " + +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "Conferma e accedi" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "Conferma email per la gestione del piano Zulip" #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." -msgstr "" +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." +msgstr "Clicca sul link sottostante per confermare la tua email e accedere alla gestione del piano Zulip per %(remote_realm_host)s." #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 #, python-format @@ -3127,7 +3139,7 @@ msgstr "\n Puoi usare anche l' contact Zulip support\n" " for assistance in resolving this issue.\n" " " -msgstr "" +msgstr "\n la tua organizzazione Zulip è registrata a una\n diversa installazione del server Zulip.\n\n Per favore contatta il supporto Zulip\n per assistenza per risolvere questo problema.\n " #: zerver/actions/create_user.py:98 msgid "signups" @@ -3360,65 +3372,65 @@ msgstr "Operazione non valida del flag del messaggio: '{operation}'" msgid "Invalid message(s)" msgstr "Messaggio(i) non valido" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Impossibile rilasciare il messaggio" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Previsto esattamente un canale" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Tipo di dato non valido per il canale" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Tipo di dato non valido per i destinatari" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Gli elenchi dei destinatari possono contenere e-mail o ID , ma non entrambi." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Il tuo bot {bot_identity} ha provato ad inviare un messaggio al canale ID {stream_id}, ma non c'è un canale con questo ID." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Il tuo bot {bot_identity} ha provato ad inviare un messaggio al canale {stream_name}, ma il canale non esiste. Clicca [here]({new_stream_link}) per crearlo." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Il tuo bot {bot_identity} ha provato ad inviare un messaggio al canale {stream_name}. Il canale esiste ma non ha iscritti." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "I messaggi diretti sono disattivati in questa organizzazione." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "Non hai il permesso di accedere ad alcuni dei destinatari." -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Gli argomenti sono obbligatori in questa organizzazione" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widget: il programmatore API ha inviato contenuto JSON non valido" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3435,28 +3447,28 @@ msgstr "L'elenco ordinato non deve contenere linkifiers duplicati." msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "L'elenco ordinato deve enumerare tutti gli esistenti linkifiers esattamente una volta." -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "Il messaggio programmato è già stato inviato" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "L'orario di consegna programmato deve essere nel futuro." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "Non è stato possibile inviare il messaggio all'orario pianificato." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "Il messaggio pianificato per le ore {delivery_datetime} non è stato inviato a causa del seguente errore:" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "[Vedi messaggi pianificati](#scheduled)" @@ -3733,7 +3745,7 @@ msgstr "Si è verificato un errore durante la cancellazione dell'allegato. Per f msgid "Message must have recipients!" msgstr "Il messaggio deve avere destinatari!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "{service_name} riepilogo" @@ -3912,7 +3924,7 @@ msgid "API usage exceeded rate limit" msgstr "L'utilizzo dell'API ha superato il limite di velocità" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON malformato" @@ -4007,7 +4019,7 @@ msgstr "La reazione non esiste" msgid "" "Your organization is registered to a different Zulip server. Please contact " "Zulip support for assistance in resolving this issue." -msgstr "" +msgstr "La tua organizzazione è registrata su un server Zulip diverso. Contatta il supporto di Zulip per assistenza nella risoluzione di questo problema." #: zerver/lib/exceptions.py:621 msgid "Organization not registered" @@ -4367,7 +4379,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "Opzioni GCM non valide per il bouncer: {options}" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token inesistente" @@ -4420,7 +4432,7 @@ msgstr "L'elenco dei destinatari può contenere solo ID utente" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Utente non autorizzato per questa query" @@ -4467,7 +4479,7 @@ msgstr "L'argomento \"{name}\" non è un JSON valido." msgid "Scheduled message does not exist" msgstr "Il messaggio programmato non esiste" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "account di sicurezza {service_name}" @@ -4580,7 +4592,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} non è una data" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} non è un dict " @@ -4618,7 +4630,7 @@ msgid "{var_name} is too large" msgstr "{var_name} è troppo grande" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} non è una lista" @@ -4782,7 +4794,7 @@ msgstr "Tipo di bot non valido" msgid "Invalid interface type" msgstr "Tipo di interfaccia non valido" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "ID utente non valido: {user_id}" @@ -4845,46 +4857,46 @@ msgstr "{var_name} non è un allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} è sbagliata )" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} non è un URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "Il pattern URL deve contenere '%(username)s'." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' non può essere vuoto." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Il campo non deve avere scelte duplicate." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' non è una scelta valida per '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} non è una stringa o un elenco intero" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} non è una stringa o un numero intero" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} non ha una lunghezza" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} è mancante" @@ -5073,57 +5085,57 @@ msgstr "Solo gli amministratori e i moderatori dell'organizzazione possono pubbl msgid "Only organization full members can post" msgstr "Solo i membri completi dell'organizzazione possono pubblicare messaggi" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Emoji Unicode" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoji personalizzate" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Emoji extra di Zulip" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "Utente con ID {user_id} è disattivato" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "Utente con ID {user_id} è un bot" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Elenco di opzioni" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Raccoglitore di persone" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Testo breve" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Testo lungo" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Selettore data" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Account esterno" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Pronomi" @@ -5139,20 +5151,20 @@ msgstr "un sistema operativo sconosciuto" msgid "An unknown browser" msgstr "Browser sconosciuto" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Argomento 'queue_is' mancante" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Argomento 'last_event_is' mancante" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Un evento più recente di {event_id} è già stato eliminato!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Evento {event_id} non è in questa coda" @@ -5324,19 +5336,19 @@ msgstr "L'ancora può essere esclusa solo alla fine del range" msgid "No such topic '{topic}'" msgstr "Non esiste alcun argomento '{topic}'" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Mittente mancante" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Il mirroring non è consentito con gli ID utente del destinatario" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Messaggio con mirroring non valido" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Il mirroring Zephyr non è permesso in questa organizzazione" @@ -5402,26 +5414,26 @@ msgstr "Almeno uno dei seguenti argomenti deve essere presente: emoji_name, emoj msgid "Read receipts are disabled in this organization." msgstr "Le conferme di lettura sono disabilitate in questa organizzazione." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "Lingua '{language}' non valida" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Almeno un metodo di autenticazione deve essere abilitato." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "video_chat_provider {video_chat_provider} non valido" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "giphy_rating {giphy_rating} non valido" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Deve essere un'organizzazione demo." @@ -5899,11 +5911,11 @@ msgid "" "exports]({export_settings_link})." msgstr "La tua esportazione dati è completa. [Visualizza e scarica le esportazioni]({export_settings_link})." -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Sottodominio non valido per bouncer di notifiche push" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "È necessario convalidare con la chiave API del server Zulip valida" @@ -5917,43 +5929,43 @@ msgstr "UUID non valido" msgid "Invalid token type" msgstr "Tipo di token non valido" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "{hostname} non è un hostname valido" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "ios_app_id mancante" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "User_id o user_uuid mancanti" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "Proprietà {property} non valida" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." -msgstr "" +msgstr "Tipo di evento non valido." -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "I dati sono fuori servizio." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "Rilevata duplicazione di registrazione." -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." -msgstr "" +msgstr "Impossibile migrare il cliente dal server ai domini. Si prega di contattare il supporto per assistenza." -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "Dati di registro di audit malformati" diff --git a/locale/it/translations.json b/locale/it/translations.json index 22afa3e25de7d..a054065dcca1e 100644 --- a/locale/it/translations.json +++ b/locale/it/translations.json @@ -191,7 +191,7 @@ "Automatic": "Automatico", "Automatic (follows system settings)": "Automatico (segue le impostazioni di sistema)", "Automatically follow topics": "Seguire automaticamente argomenti", - "Automatically follow topics where I'm mentioned": "", + "Automatically follow topics where I'm mentioned": "Segui automaticamente gli argomenti in cui vengo menzionato.", "Automatically mark messages as read": "Contrassegna automaticamente i messaggi come letti", "Automatically unmute topics in muted streams": "Togli il mute automaticamente agli argomenti nei canali silenziati", "Available on Zulip Cloud Standard. Upgrade or request sponsorship to access.": "Disponibile su Zulip Cloud Standard. Upgrade o richiedi la sponsorizzazione per accedere.", @@ -951,7 +951,7 @@ "Pin stream to top": "Fissa canale in alto", "Pin stream to top of left sidebar": "Mantieni il canale bloccato in cima all'area di sinistra", "Pinned": "Bloccato in evidenziato", - "Plan management": "", + "Plan management": "Gestione del piano", "Plans and pricing": "Piani e prezzi", "Play sound": "Anteprima suono", "Please contact support for an exception or add users with a reusable invite link.": "Per favore contatta il supporto per un'eccezione oppure aggiungi utenti con un link di invito riutilizzabile.", @@ -1392,7 +1392,7 @@ "Who can send email invitations to new users": "Chi può inviare inviti via email ai nuovi utenti", "Who can unsubscribe others from this stream?": "Chi può annullare l'iscrizione di altri a questo canale?", "Who can use direct messages": "Chi può usare i messaggi diretti", - "Who can view all other users in the organization": "", + "Who can view all other users in the organization": "Chi può visualizzare tutti gli altri utenti nell'organizzazione", "Why not start a conversation with yourself?": "Perché non iniziare una conversazione con te stesso?", "Why not start the conversation?": "Perché non iniziare una conversazione?", "Word": "Parola", diff --git a/locale/ja/LC_MESSAGES/django.po b/locale/ja/LC_MESSAGES/django.po index df9e34cd3740f..79fdd4e3789a2 100644 --- a/locale/ja/LC_MESSAGES/django.po +++ b/locale/ja/LC_MESSAGES/django.po @@ -19,7 +19,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: oribe1115, 2023\n" "Language-Team: Japanese (http://app.transifex.com/zulip/zulip/language/ja/)\n" @@ -73,7 +73,7 @@ msgstr "開始時刻が終了時刻より遅いです。 開始:{start}、終 msgid "No analytics data available. Please contact your server administrator." msgstr "解析用データが利用できません。サーバー管理者に連絡してください。" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -144,124 +144,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} / 末尾 {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "不明なお支払い方法です。{email} にお問い合わせください。" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "エラーが発生しました。{email} に問い合わせてください。" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "エラーが発生しました。ページを再読み込みしてください。" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "エラーが発生しました。少し待ってからもう一度試してください。" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "プランを更新できません。このプランは終了し、新しいプランに置き換えられました。" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "プランを更新できません。このプランは終了しました。" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "ライセンスを手動で更新できません。現在、自動ライセンス管理のプランを使用しています。" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "現在の支払い期間のライセンスは下げられません" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "変更するものがありません" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "セッションが見つかりません" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "請求管理者または組織オーナーである必要があります。" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -269,33 +269,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -322,7 +326,7 @@ msgid "" msgstr "\n予期しないエラーの場合は、サポートにお問い合わせください。" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "内部のサーバーエラー" @@ -571,31 +575,31 @@ msgstr "リンクを正しくブラウザーにコピーしているか確認し msgid "Billing" msgstr "請求書" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "この画面を閉じる" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "キャンセル" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "確認" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -694,6 +698,10 @@ msgstr "教育用価格" msgid "View pricing" msgstr "料金設定を見る" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "ビジネスのためのZulip" @@ -835,8 +843,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2258,7 +2264,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2267,7 +2272,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2275,7 +2280,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2291,7 +2295,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2308,15 +2311,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3361,65 +3373,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "不正なメッセージ" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "メッセージをレンダリングできません" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "ちょうど一つのストリームを期待しています" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "ストリームのデータータイプが不正です" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "受信者のデータータイプが不正です" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "受信者リストはメールアドレスかユーザーIDのどちらかを含むことができますが、両方を含むことはできません。" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "あなたのBot「{bot_identity}」がストリームID「{stream_id}」にメッセージを送ろうとしましたが、そのIDのストリームはありません。" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "あなたのBot「{bot_identity}」がストリーム「{stream_name}」にメッセージを送信しようとしましたが、そのストリームはありません。[ここ]({new_stream_link})をクリックして作成してください。" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "あなたのBot「{bot_identity}」がストリームID「{stream_name}」にメッセージを送信しようとしました。ストリームは存在しますがフォロワーがいません。" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "この組織では、ダイレクトメッセージが無効化されています。" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "この組織ではトピックが必須です" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "ウィジェット:APIプログラマは不正なJSONを送信しました" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "ウィジェット:{error_msg}" @@ -3436,28 +3448,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "予約メッセージが送信されました" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3734,7 +3746,7 @@ msgstr "添付ファイルを削除するときにエラーが発生しました msgid "Message must have recipients!" msgstr "メッセージは受信者が必要です" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3913,7 +3925,7 @@ msgid "API usage exceeded rate limit" msgstr "API使用量が制限を超えました" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "不正なJSON" @@ -4368,7 +4380,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "トークンがありません" @@ -4421,7 +4433,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "このクエリーを許可されていないユーザーです" @@ -4468,7 +4480,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "予約メッセージがありません" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4581,7 +4593,7 @@ msgid "{var_name} is not a date" msgstr "{var_name}が日付ではありません" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name}は辞書でありません" @@ -4619,7 +4631,7 @@ msgid "{var_name} is too large" msgstr "{var_name} は大きすぎます" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name}がリストでありません" @@ -4783,7 +4795,7 @@ msgstr "不正なボット種別" msgid "Invalid interface type" msgstr "不正なインターフェース種別" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4846,46 +4858,46 @@ msgstr "{var_name}が許可された型でありません" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value}が不正です)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name}がURLでありません" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "{item}は空白であることはできません" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "{value}は{field_name}の正しい選択肢でありません" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name}が文字列か整数のリストでありません" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name}が文字列か整数でありません" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name}が見つかりません" @@ -5074,57 +5086,57 @@ msgstr "組織の管理者とモデレーターのみが投稿できます。" msgid "Only organization full members can post" msgstr "組織の正式メンバーのみ投稿可能" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode絵文字" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "カスタム絵文字" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip拡張絵文字" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "オプションのリスト" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "人物ピッカー" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "ショートテキスト" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "ロングテキスト" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "日付ピッカー" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "リンク" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "外部赤雲tの" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5140,20 +5152,20 @@ msgstr "不明なOS" msgid "An unknown browser" msgstr "不明なブラウザ" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "'queue_id' 引数がありません" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "'last_event_id' 引数がありません" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "{event_id}より新しいイベントはすでにpruneされています。" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "イベント {event_id}はこのキューに存在しません" @@ -5325,19 +5337,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "送信者がいません" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "受信者のユーザーIDでミラーリングは認められていません" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "不正なミラーリングされたメッセージ" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Zephyr ミラーリングはこの組織で許可されていません" @@ -5403,26 +5415,26 @@ msgstr "これらのうち1つ以上の引数が渡されるべきです:emo msgid "Read receipts are disabled in this organization." msgstr "この組織では既読表示が無効化されています。" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "有効な認証方法が必要です。" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5900,11 +5912,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Push通知転送サービス用のサブドメインが不正です" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "正しいZulipサーバーでAPIキーを検証する必要があります" @@ -5918,43 +5930,43 @@ msgstr "" msgid "Invalid token type" msgstr "不正なトークン種別" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "データが破損しています" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/ja/translations.json b/locale/ja/translations.json index 5dd59da4db61e..fca1ea907f56c 100644 --- a/locale/ja/translations.json +++ b/locale/ja/translations.json @@ -613,7 +613,7 @@ "Include content of direct messages in desktop notifications": "DM内容をデスクトップ通知に含む ", "Include message content in message notification emails": "通知メールにメッセージ内容を含める", "Include organization name in subject of message notification emails": "新規メッセージ受信を知らせる通知メールのタイトルに組織名を含める", - "Includes muted streams and topics": "ミュートしたストリームとトピックを含む", + "Includes muted streams and topics": "ミュート対象を含む", "Initiate a search": "検索開始", "Insert new line": "改行", "Integration": "", @@ -900,7 +900,7 @@ "Only stream members can add users to a private stream.": "", "Only subscribers can access or join private streams, so you will lose access to this stream if you convert it to a private stream while not subscribed to it.": "", "Only subscribers to this stream can edit stream permissions.": "このストリームのフォロワーのみがストリームの権限を変更できます。", - "Only topics you follow": "", + "Only topics you follow": "フォローしているトピックのみ", "Open": "開く", "Open help menu": "ヘルプメニューを開く", "Open message menu": "メッセージメニューを開く", @@ -1122,7 +1122,7 @@ "Sort by unread message count": "", "Spoiler": "ネタバレ", "Sponsorship request pending": "", - "Standard view": "標準の表示", + "Standard view": "標準", "Star": "スター", "Star selected message": "選択したメッセージをスター付け", "Star this message": "メッセージにスターを付ける", @@ -1467,7 +1467,7 @@ "You have no inactive bots.": "非アクティブなボットが存在しません。", "You have no more unread direct messages.": "", "You have no more unread topics.": "", - "You have no starred messages.": "", + "You have no starred messages.": "スター付きメッセージがありません。", "You have no unread messages in followed topics.": "", "You have no unread messages!": "未読のメッセージはありません!", "You have not configured any topics yet.": "", diff --git a/locale/ko/LC_MESSAGES/django.po b/locale/ko/LC_MESSAGES/django.po index 9a89d0c415d05..6a21d5be5333c 100644 --- a/locale/ko/LC_MESSAGES/django.po +++ b/locale/ko/LC_MESSAGES/django.po @@ -20,7 +20,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: sung yong kim , 2021\n" "Language-Team: Korean (http://app.transifex.com/zulip/zulip/language/ko/)\n" @@ -74,7 +74,7 @@ msgstr "시작시간이 종료 시간 보다 늦습니다. 시작시간: {start} msgid "No analytics data available. Please contact your server administrator." msgstr "이용가는한 분석 데이터가 없습니다. 서버 운영자에게 문의하세요." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -145,124 +145,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{last4}로 끝나는 {brand} " -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "알수없는 지불 방법입니다. {email}로 연락 주세요." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "문제가 발생했습니다. {email}로 연락 주세요." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "무언가 잘못 되었습니다. 페이지를 새로고침해 주십시오." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "무언가 잘못 되었습니다. 몇 초간 기다린 후 다시 시도 해주세요." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -270,33 +270,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -323,7 +327,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "내부 서버 오류" @@ -572,31 +576,31 @@ msgstr "너의 브라우저에 올바른 링크를 복사하는 것을 확실히 msgid "Billing" msgstr "청구서" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "현재 창을 종료" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "취소" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "확인" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -695,6 +699,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -836,8 +844,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2259,7 +2265,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2268,7 +2273,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2276,7 +2281,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2292,7 +2296,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2309,15 +2312,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3362,65 +3374,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "유효하지 않은 메시지" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "메시지를 렌더링 할 수 없습니다." -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "위젯: API 프로그래머가 유효하지 않은 JSON 콘텐츠를 보냈습니다" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3437,28 +3449,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3735,7 +3747,7 @@ msgstr "첨부 파일을 삭제하는 중 오류가 발생했습니다. 나중 msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3914,7 +3926,7 @@ msgid "API usage exceeded rate limit" msgstr "API 사용량이 속도 제한을 초과했습니다." #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "조작된 JSON" @@ -4369,7 +4381,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "토큰이 존재하지 않습니다." @@ -4422,7 +4434,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "이 쿼리에 대해 승인되지 않은 사용자" @@ -4469,7 +4481,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4582,7 +4594,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4620,7 +4632,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4784,7 +4796,7 @@ msgstr "유효하지 않은 봇 유형" msgid "Invalid interface type" msgstr "인터페이스 유형이 유효하지 않습니다." -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4847,46 +4859,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}'은 비워 둘 수 없습니다." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}'는 '{field_name}'에 대한 유효한 선택이 아닙니다." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5075,57 +5087,57 @@ msgstr "오직 조직 관리자들 그리고 중재자들만 게시할 수 있 msgid "Only organization full members can post" msgstr "오직 조직 멤버들만 게시할 수 있습니다." -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "유니코드 이모티콘" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "사용자 정의 이모티콘" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip 추가 이모티콘" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "옵션 리스트" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "사람 선택" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "짧은 텍스트" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "긴 텍스트" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "날짜 선택" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "연결" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5141,20 +5153,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "누락된 'queue_id' 인수" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "누락된 'last_event_id' 인수" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5326,19 +5338,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "누락된 발신자" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "미러된 메시지가 유효하지 않습니다." -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "이 조직에서 Zephyr 미러링은 사용하실 수 없습니다." @@ -5404,26 +5416,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "하나 이상의 인증 방법이 사용 가능해야합니다." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5901,11 +5913,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "푸시 알림 Bouncer의 하위 도메인이 유효하지 않습니다." -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "유효한 Zulip 서버 API키로 유효화해야 한다." @@ -5919,43 +5931,43 @@ msgstr "" msgid "Invalid token type" msgstr "유효하지 않은 토큰 타입" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/lt/LC_MESSAGES/django.po b/locale/lt/LC_MESSAGES/django.po index 679dea74e3e21..b6b8e7e0d65c6 100644 --- a/locale/lt/LC_MESSAGES/django.po +++ b/locale/lt/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: R S , 2020\n" "Language-Team: Lithuanian (http://app.transifex.com/zulip/zulip/language/lt/)\n" @@ -62,7 +62,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "Analitiniai duomenys negalimi. Susisiekite su serverio administratoriumi." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -133,124 +133,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -258,33 +258,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -311,7 +315,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Vidinė serverio klaida" @@ -560,31 +564,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Atšaukti" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -683,6 +687,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -824,8 +832,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2247,7 +2253,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2256,7 +2261,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2264,7 +2269,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2280,7 +2284,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2297,15 +2300,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3350,65 +3362,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Negaliojanti žinutė" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Nepavyko perteikti žinutės" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3425,28 +3437,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3723,7 +3735,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3902,7 +3914,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Blogas JSON" @@ -4357,7 +4369,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Tokenas neegzistuoja" @@ -4410,7 +4422,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Naudotojas neturi teisių šiai užklausai" @@ -4457,7 +4469,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4570,7 +4582,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4608,7 +4620,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4772,7 +4784,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4835,46 +4847,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5063,57 +5075,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Tinkintos šypsenėlės" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5129,20 +5141,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Trūksta „eilės_id“" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Trūksta „paskutinio_įvykio_id“" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5314,19 +5326,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Nėra siuntėjo" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Negaliojanti veidrodinė žinutė" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5392,26 +5404,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Turi būti įgalintas bent vienas autentifikacijos būdas." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5889,11 +5901,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5907,43 +5919,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/lv/LC_MESSAGES/django.po b/locale/lv/LC_MESSAGES/django.po index 9420c268130ea..6f58c606aad10 100644 --- a/locale/lv/LC_MESSAGES/django.po +++ b/locale/lv/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:58+0000\n" +"POT-Creation-Date: 2023-12-15 17:44+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -62,7 +62,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -130,124 +130,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this " "page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new " "plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2901 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You " @@ -257,33 +257,37 @@ msgid "" "we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:136 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:138 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:275 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:280 +#: corporate/views/remote_billing_page.py:693 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:506 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:513 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:517 msgid "Your server registration has been deactivated." msgstr "" @@ -311,7 +315,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -568,31 +572,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:313 templates/corporate/billing.html:339 +#: templates/corporate/billing.html:368 templates/corporate/billing.html:397 +#: templates/corporate/billing.html:423 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:324 templates/corporate/billing.html:353 +#: templates/corporate/billing.html:382 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:326 templates/corporate/billing.html:410 +#: templates/corporate/billing.html:433 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:355 templates/corporate/billing.html:384 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:408 templates/corporate/billing.html:431 msgid "Never mind" msgstr "" @@ -691,6 +695,62 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:4 +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:16 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:4 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:16 +msgid "Plan management not available" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:18 +#, python-format +msgid "" +" Plan management is not available for this\n" +" organization, because your Zulip server is already " +"on a\n" +" %(server_plan_name)s plan, which covers all\n" +" organizations on this server. Follow the log\n" +" in instructions\n" +" for All older versions\n" +" of the Zulip server to manage your plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:29 +msgid "" +" To move the plan from the server to this\n" +" organization, or for other questions, contact support.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:18 +msgid "" +"\n" +" Plan management for this server is not available " +"because at least one organization\n" +" hosted on this server already has an active plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:24 +#, python-format +msgid "" +"\n" +" Log in to plan management for your\n" +" organization instead, or contact support with any questions.\n" +" " +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -833,8 +893,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2278,7 +2336,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2287,7 +2344,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2295,7 +2352,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2311,7 +2367,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2328,16 +2383,25 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for " -"%(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2368,7 +2432,7 @@ msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " -"appreciate it!." +"appreciate it!" msgstr "" #: templates/zerver/find_account.html:4 @@ -3397,65 +3461,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but " "that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The " "stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3472,27 +3536,27 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 -#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:128 +#: zerver/actions/scheduled_messages.py:155 +#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3771,7 +3835,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3950,7 +4014,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4403,45 +4467,45 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:739 zilencer/views.py:241 msgid "Token does not exist" msgstr "" -#: zerver/lib/push_notifications.py:902 +#: zerver/lib/push_notifications.py:910 msgid "" "This organization has disabled including message content in mobile push " "notifications" msgstr "" -#: zerver/lib/push_notifications.py:1015 +#: zerver/lib/push_notifications.py:1023 #, python-brace-format msgid "{full_name} mentioned @{user_group_name}:" msgstr "" -#: zerver/lib/push_notifications.py:1019 +#: zerver/lib/push_notifications.py:1027 #, python-brace-format msgid "{full_name} mentioned you:" msgstr "" -#: zerver/lib/push_notifications.py:1026 +#: zerver/lib/push_notifications.py:1034 #, python-brace-format msgid "{full_name} mentioned everyone:" msgstr "" -#: zerver/lib/push_notifications.py:1436 +#: zerver/lib/push_notifications.py:1438 msgid "Test notification" msgstr "" -#: zerver/lib/push_notifications.py:1437 +#: zerver/lib/push_notifications.py:1439 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." msgstr "" -#: zerver/lib/push_notifications.py:1488 +#: zerver/lib/push_notifications.py:1490 msgid "Device not recognized" msgstr "" -#: zerver/lib/push_notifications.py:1500 +#: zerver/lib/push_notifications.py:1502 msgid "Device not recognized by the push bouncer" msgstr "" @@ -4456,7 +4520,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4503,7 +4567,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4616,7 +4680,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4654,7 +4718,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4881,46 +4945,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5109,15 +5173,15 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" @@ -5175,20 +5239,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5360,19 +5424,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5438,26 +5502,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5935,61 +5999,61 @@ msgid "" "({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" #. error -#: zilencer/views.py:77 zilencer/views.py:79 +#: zilencer/views.py:81 zilencer/views.py:83 msgid "Invalid UUID" msgstr "" #. error -#: zilencer/views.py:84 +#: zilencer/views.py:88 msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:126 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:182 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:185 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:610 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:613 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:620 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:696 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:957 msgid "" "Failed to migrate customer from server to realms. Please contact support for " "assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:1009 msgid "Malformed audit log data" msgstr "" diff --git a/locale/ml/LC_MESSAGES/django.po b/locale/ml/LC_MESSAGES/django.po index 0dc3f877b7bd8..c3ae079ad0c21 100644 --- a/locale/ml/LC_MESSAGES/django.po +++ b/locale/ml/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:58+0000\n" +"POT-Creation-Date: 2023-12-15 17:44+0000\n" "PO-Revision-Date: 2021-05-27 07:08+0000\n" "Last-Translator: Akash Nimare \n" "Language-Team: Malayalam (http://www.transifex.com/zulip/zulip/language/" @@ -68,7 +68,7 @@ msgid "No analytics data available. Please contact your server administrator." msgstr "" "അനലിറ്റിക്സ് ഡാറ്റയൊന്നും ലഭ്യമല്ല. ദയവായി നിങ്ങളുടെ സെർവർ അഡ്മിനിസ്ട്രേറ്ററെ ബന്ധപ്പെടുക." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -136,126 +136,126 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this " "page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new " "plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 #, fuzzy #| msgid "Deactivated organization" msgid "No customer for this organization!" msgstr "നിഷ്ക്രീയമാക്കിയ സംഘടന" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2901 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You " @@ -265,35 +265,39 @@ msgid "" "we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:136 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:138 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:275 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:280 +#: corporate/views/remote_billing_page.py:693 #, fuzzy #| msgid "Terms of Service" msgid "You must accept the Terms of Service to proceed." msgstr "സേവന നിബന്ധനകൾ" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:506 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:513 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:517 msgid "Your server registration has been deactivated." msgstr "" @@ -321,7 +325,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -586,9 +590,9 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:313 templates/corporate/billing.html:339 +#: templates/corporate/billing.html:368 templates/corporate/billing.html:397 +#: templates/corporate/billing.html:423 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 #, fuzzy @@ -596,25 +600,25 @@ msgstr "" msgid "Close modal" msgstr "അടയ്‌ക്കുക" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:324 templates/corporate/billing.html:353 +#: templates/corporate/billing.html:382 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "റദ്ദാക്കുക" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:326 templates/corporate/billing.html:410 +#: templates/corporate/billing.html:433 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:355 templates/corporate/billing.html:384 #: templates/zerver/change_email_address_visibility_modal.html:28 #, fuzzy #| msgid "Confirm password" msgid "Confirm" msgstr "രഹസ്യവാക്ക് ഉറപ്പിക" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:408 templates/corporate/billing.html:431 msgid "Never mind" msgstr "" @@ -715,6 +719,64 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +#, fuzzy +#| msgid "Deactivated organization" +msgid "Deactivate server registration?" +msgstr "നിഷ്ക്രീയമാക്കിയ സംഘടന" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:4 +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:16 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:4 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:16 +msgid "Plan management not available" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:18 +#, python-format +msgid "" +" Plan management is not available for this\n" +" organization, because your Zulip server is already " +"on a\n" +" %(server_plan_name)s plan, which covers all\n" +" organizations on this server. Follow the log\n" +" in instructions\n" +" for All older versions\n" +" of the Zulip server to manage your plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:29 +msgid "" +" To move the plan from the server to this\n" +" organization, or for other questions, contact support.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:18 +msgid "" +"\n" +" Plan management for this server is not available " +"because at least one organization\n" +" hosted on this server already has an active plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:24 +#, python-format +msgid "" +"\n" +" Log in to plan management for your\n" +" organization instead, or contact support with any questions.\n" +" " +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -869,8 +931,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2336,7 +2396,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2345,7 +2404,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2353,7 +2412,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2369,7 +2427,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2386,16 +2443,27 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for " -"%(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +#, fuzzy +#| msgid "Confirm password" +msgid "Confirm and log in" +msgstr "രഹസ്യവാക്ക് ഉറപ്പിക" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2426,7 +2494,7 @@ msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " -"appreciate it!." +"appreciate it!" msgstr "" #: templates/zerver/find_account.html:4 @@ -3491,69 +3559,69 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but " "that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The " "stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 #, fuzzy #| msgid "Deactivated organization" msgid "Direct messages are disabled in this organization." msgstr "നിഷ്ക്രീയമാക്കിയ സംഘടന" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 #, fuzzy #| msgid "Deactivated organization" msgid "Topics are required in this organization" msgstr "നിഷ്ക്രീയമാക്കിയ സംഘടന" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3570,27 +3638,27 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 -#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:128 +#: zerver/actions/scheduled_messages.py:155 +#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3869,7 +3937,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -4049,7 +4117,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4506,47 +4574,47 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:739 zilencer/views.py:241 msgid "Token does not exist" msgstr "" -#: zerver/lib/push_notifications.py:902 +#: zerver/lib/push_notifications.py:910 msgid "" "This organization has disabled including message content in mobile push " "notifications" msgstr "" -#: zerver/lib/push_notifications.py:1015 +#: zerver/lib/push_notifications.py:1023 #, python-brace-format msgid "{full_name} mentioned @{user_group_name}:" msgstr "" -#: zerver/lib/push_notifications.py:1019 +#: zerver/lib/push_notifications.py:1027 #, python-brace-format msgid "{full_name} mentioned you:" msgstr "" -#: zerver/lib/push_notifications.py:1026 +#: zerver/lib/push_notifications.py:1034 #, python-brace-format msgid "{full_name} mentioned everyone:" msgstr "" -#: zerver/lib/push_notifications.py:1436 +#: zerver/lib/push_notifications.py:1438 #, fuzzy #| msgid "Notifications" msgid "Test notification" msgstr "അറിയിപ്പുകൾ" -#: zerver/lib/push_notifications.py:1437 +#: zerver/lib/push_notifications.py:1439 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." msgstr "" -#: zerver/lib/push_notifications.py:1488 +#: zerver/lib/push_notifications.py:1490 msgid "Device not recognized" msgstr "" -#: zerver/lib/push_notifications.py:1500 +#: zerver/lib/push_notifications.py:1502 msgid "Device not recognized by the push bouncer" msgstr "" @@ -4561,7 +4629,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4608,7 +4676,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4721,7 +4789,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4759,7 +4827,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4986,46 +5054,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5218,15 +5286,15 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "നിങ്ങൾക്ക് വേണ്ട ഇമോജി" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" @@ -5284,20 +5352,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5473,19 +5541,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5553,26 +5621,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "നിഷ്ക്രീയമാക്കിയ സംഘടന" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 #, fuzzy #| msgid "Deactivated organization" msgid "Must be a demo organization." @@ -6052,61 +6120,61 @@ msgid "" "({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" #. error -#: zilencer/views.py:77 zilencer/views.py:79 +#: zilencer/views.py:81 zilencer/views.py:83 msgid "Invalid UUID" msgstr "" #. error -#: zilencer/views.py:84 +#: zilencer/views.py:88 msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:126 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:182 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:185 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:610 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:613 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:620 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:696 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:957 msgid "" "Failed to migrate customer from server to realms. Please contact support for " "assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:1009 msgid "Malformed audit log data" msgstr "" @@ -6126,11 +6194,6 @@ msgstr "" msgid "Can't use both mobile_flow_otp and desktop_flow_otp together." msgstr "" -#, fuzzy -#~| msgid "Confirm password" -#~ msgid "Confirm login" -#~ msgstr "രഹസ്യവാക്ക് ഉറപ്പിക" - #, fuzzy #~| msgid "Find accounts" #~ msgid "Your account details:" diff --git a/locale/mn/LC_MESSAGES/django.po b/locale/mn/LC_MESSAGES/django.po index b8c0765fd3909..bd5e0497bf114 100644 --- a/locale/mn/LC_MESSAGES/django.po +++ b/locale/mn/LC_MESSAGES/django.po @@ -18,7 +18,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Tim Abbott , 2023\n" "Language-Team: Mongolian (http://app.transifex.com/zulip/zulip/language/mn/)\n" @@ -72,7 +72,7 @@ msgstr "Эхлэх цаг дуусах хугацаанаас хожуу бай msgid "No analytics data available. Please contact your server administrator." msgstr "Аналитик мэдээлэл байхгүй. Серверийн админтайгаа холбогдоно уу." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -143,124 +143,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} төгсгөл нь {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Тодорхойгүй төлбөрийн хэлбэр байна. {email} холбогдоно уу." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Алдаа гарлаа. {email} холбогдоно уу." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Алдаа гарлаа. Хуудсаа дахин уншуулна уу." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Алдаа гарлаа. Хэсэг хугацаанд хүлээж байгаад дахин оролдоно уу." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Үйлчилгээний ангилал шинэчлэх боломжгүй. Хугацаа нь дууссаны дараа шинэ ангилал идэвхжүүлэх боломжтой." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Үйлчилгээний ангилал шинэчлэх боломжгүй. Хугацаа нь дууссан байна." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Лицензүүдийг гараар шинэчлэх боломжгүй. Таны план автомат лицензийн менежмент дээр байна." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Таны үйлчилгээ {licenses} лицензээр сунгагдах боломжтой." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Та одоогийн төлбөрийн хугацаандаа лицензийг бууруулах боломжгүй." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Таны үйлчилгээ {licenses_at_next_renewal}  лицензээр автомат сунгалт хийгдэхээр байна." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Өөрчлөх зүйл алга." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Энэ байгууллагад үйлчлүүлэгч байхгүй!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Холболт салсан" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Төлбөрийн администратор эсвэл байгууллагын эзэмшигч байх ёстой." -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Төлбөрийн зориулалт олдсонгүй" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "stripe_session_id эсвэл stripe_payment_intent_id оруулах" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -268,33 +268,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "Танай бүлгийн ивээн хостын хүсэлтийг хүлээн авлаа! Та {plan_name} руу үнэ төлбөргүй шинэчлэгдлээ. {emoji}\n\nХэрэв та {begin_link}Оффис чат-ийг вэбсайтдаа байршуулвал{end_link} бид талархах болно!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -321,7 +325,7 @@ msgid "" msgstr "\nХэрэв энэ алдаа гэнэт гарвал, та \nбидэнтэй холбогдоорой\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Сервер дээрх алдаа" @@ -570,31 +574,31 @@ msgstr "Та хөтөч дээрээ холбоосыг зөв хуулсан э msgid "Billing" msgstr "Төлбөр" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Модалыг хаах" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Цуцлах" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Баталгаажуулалт" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -693,6 +697,10 @@ msgstr "Сургалтын үнэ" msgid "View pricing" msgstr "Үнэ харах" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Бизнесд зориулсан Zulip" @@ -834,8 +842,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2257,7 +2263,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2266,7 +2271,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2274,7 +2279,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2290,7 +2294,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2307,15 +2310,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3360,65 +3372,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Буруу мессеж(үүд)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Мессежийг үзүүлэх боломжгүй" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Яг нэг stream хүлээж байсан" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Stream-ын өгөгдлийн төрөл буруу" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Хүлээн авагчийн өгөгдлийн төрөл буруу байна" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Хүлээн авагчийн лист дотор имэйл хаяг эсвэл хэрэглэгчийн ID байх боломжтой, хоёулаа байх боломжгүй." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Таны {bot_identity} бот {stream_id} stream ID-руу мессеж илгээхийг оролдсон боловч ийм ID-тай stream байхгүй байна." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Таны бот {bot_identity} {stream_name} stream-руу мессеж илгээхийг оролдсон боловч энэ stream байхгүй байна. Stream үүсгэхийн тулд [энд]({new_stream_link}) дарна уу." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Таны {bot_identity} бот {stream_name} руу мессеж илгээхийг оролдсон. Stream байгаа ч гишүүн байхгүй." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Энэ бүлэгт хувийн чатыг идэвхгүй болгосон." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Уг байгууллагад сэдэв шаардлагатай" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: API программист хүчингүй JSON контент илгээсэн" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3435,28 +3447,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "Илгээхээр төлөвлөсөн мессежийг аль хэдийн илгээсэн байна" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "Төлөвлөсөн хүргэх хугацааг тохируулахгүй байх хэрэгтэй." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "Мессежийг товлосон цагт илгээж чадсангүй." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "Таны {delivery_datetime}-д товлосон мессежийг дараах алдааны улмаас илгээгээгүй:" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "[Илгээхээр төлөвлөсөн мессежүүдийг харах](#scheduled)" @@ -3733,7 +3745,7 @@ msgstr "Хавсралтыг устгах явцад алдаа гарлаа. Д msgid "Message must have recipients!" msgstr "Мессеж нь хүлээн авагчтай байх ёстой!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3912,7 +3924,7 @@ msgid "API usage exceeded rate limit" msgstr "API ашиглалт хурдны хязгаараас хэтэрсэн" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Алдаатай JSON" @@ -4367,7 +4379,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Токен үүсээгүй байна" @@ -4420,7 +4432,7 @@ msgstr "Хүлээн авагчийн жагсаалт нь зөвхөн хэр #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Тухайн хэрэглэгч энэ үйлдэлийг хийх эрхгүй байна." @@ -4467,7 +4479,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "Илгээхээр төлөвлөсөн мессеж байхгүй байна" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4580,7 +4592,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} нь огноо биш" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} алдаатай үйлдэл байна." @@ -4618,7 +4630,7 @@ msgid "{var_name} is too large" msgstr "{var_name} нь хэт том байна" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} нь лист биш байна" @@ -4782,7 +4794,7 @@ msgstr "Ботын төрөл буруу байна" msgid "Invalid interface type" msgstr "Интерфэйсийн төрөл буруу байна." -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4845,46 +4857,46 @@ msgstr "{var_name} нь зөвшөөрөгдсөн_төрөл биш юм" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} буруу)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} нь URL биш байна" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "URL загвар нь '%(username)s'-г агуулсан байх ёстой." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' хоосон байж болохгүй." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Талбарт давхардсан сонголт байх ёсгүй." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' нь '{field_name}'-д тохирох сонголт биш байна." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} нь мөр эсвэл бүхэл тооны жагсаалт биш" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} нь мөр эсвэл бүхэл тоо биш" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} урт байхгүй" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} нь алга байна" @@ -5073,57 +5085,57 @@ msgstr "Зөвхөн бүлгийн админ болон модераторуу msgid "Only organization full members can post" msgstr "Зөвхөн бүлгийн бүрэн эрхтэй гишүүд постлох боломжтой" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Юникод эможи" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Тусгай эможи" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip нэмэлт эможи" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Хувилбаруудын жагсаалт" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Хүн сонгох" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Богино текст" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Урт текст" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Өдөр сонгох" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Холбоос" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Гадаад аккаунт" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "төлөөний үг" @@ -5139,20 +5151,20 @@ msgstr "Үл мэдэгдэх үйлдлийн систем" msgid "An unknown browser" msgstr "Үл мэдэгдэх интернэт хөтөч" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "'queue_id' аргумент алга" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "'last_event_id' аргумент алга" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "{event_id}-с шинэ арга хэмжээг аль хэдийн хассан байна!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "{event_id} үйл ажиллагаа нь тухайн дараалалд байхгүй байна." @@ -5324,19 +5336,19 @@ msgstr "Anchor-ийг зөвхөн төгсгөлд нь хасч болно" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Илгээгч алга байна" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Хүлээн авагчийн хэрэглэгчийн ID -г харахыг зөвшөөрөхгүй" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Мессеж хуулбарлахад алдаа гарлаа" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Zephyr mirroring нь энэхүү бүлэг зөвшөөрөгдөөгүй байна." @@ -5402,26 +5414,26 @@ msgstr "Дараах аргументуудын дор хаяж нэг нь ба msgid "Read receipts are disabled in this organization." msgstr "Энэ бүлэгт уншсан баримтыг идэвхгүй болгосон." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Дор хаяж нэг баталгаажуулалтын аргыг идэвхжүүлсэн байх ёстой." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Нэг тест бүлэг байх ёстой." @@ -5899,11 +5911,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Дэд домэйн буруу байна." -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Хүчинтэй Zulip сервер API түлхүүрээр баталгаажуулах ёстой" @@ -5917,43 +5929,43 @@ msgstr "UUID буруу" msgid "Invalid token type" msgstr "токен төрөл буруу байна" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "user_id эсвэл user_uuid алга байна" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Өгөгдөл ажиллахаа больсон." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/mobile_info.json b/locale/mobile_info.json index 5b2a47737938e..4376532b4b491 100644 --- a/locale/mobile_info.json +++ b/locale/mobile_info.json @@ -20,7 +20,7 @@ "total": 405 }, "cs": { - "not_translated": 64, + "not_translated": 60, "total": 405 }, "cy": { @@ -28,7 +28,7 @@ "total": 405 }, "da": { - "not_translated": 200, + "not_translated": 167, "total": 405 }, "de": { @@ -48,7 +48,7 @@ "total": 405 }, "fa": { - "not_translated": 9, + "not_translated": 0, "total": 405 }, "fi": { @@ -136,7 +136,7 @@ "total": 405 }, "sr": { - "not_translated": 9, + "not_translated": 0, "total": 405 }, "sv": { diff --git a/locale/nl/LC_MESSAGES/django.po b/locale/nl/LC_MESSAGES/django.po index 77ac81a39b8b3..27f1c5d4752e4 100644 --- a/locale/nl/LC_MESSAGES/django.po +++ b/locale/nl/LC_MESSAGES/django.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Tim Abbott , 2021,2023\n" "Language-Team: Dutch (http://app.transifex.com/zulip/zulip/language/nl/)\n" @@ -66,7 +66,7 @@ msgstr "Starttiijd ligt na de eindtijd. Start: {start}, Eind: {end}" msgid "No analytics data available. Please contact your server administrator." msgstr "Geen statistieken beschikbaar. Pas dit aan bij de admin instellingen." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -137,124 +137,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Aankopen met meer dan {max_licenses} kunnen niet verwerkt worden via deze pagina. Contacteer alsjeblieft {email} om de upgrade te voltooien." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} eindigt over {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Dit is een onbekende betaalmethode. Neem alsjeblieft contact op met {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Er is iets mis gegaan. Neem alsjeblieft contact op met {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Er is iets mis gegaan. Herlaad alsjeblieft de pagina." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Er is iets mis gegaan. Wacht alsjeblieft een paar seconden en probeer het opnieuw." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -262,33 +262,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "Jouw organisatie's aanvraag voor gesponsorde hosting is aanvaard! Je bent geüpgraded naar {plan_name}, zonder meerkost. {emoji}\n\nWe zouden het ten zeerste op prijs stellen als je {begin_link} Zulip zou kunnen weergeven als sponsor op jouw website{end_link}!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -315,7 +319,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Interne serverfout" @@ -564,31 +568,31 @@ msgstr "" msgid "Billing" msgstr "Facturering" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Annuleren" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Bevestig" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -687,6 +691,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -828,8 +836,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2251,7 +2257,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2260,7 +2265,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2268,7 +2273,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2284,7 +2288,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2301,15 +2304,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3354,65 +3366,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Ongeldig(e) bericht(en)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Kan bericht niet opmaken" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3429,28 +3441,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3727,7 +3739,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3906,7 +3918,7 @@ msgid "API usage exceeded rate limit" msgstr "API-gebruik overschrijdt gebruikslimiet" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Misvormde JSON" @@ -4361,7 +4373,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token bestaat niet" @@ -4414,7 +4426,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Gebruiker niet geautoriseerd voor deze opvraag" @@ -4461,7 +4473,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4574,7 +4586,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4612,7 +4624,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4776,7 +4788,7 @@ msgstr "Ongeldig bottype" msgid "Invalid interface type" msgstr "Ongeldig interfacetype" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4839,46 +4851,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} is onjuist)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} is geen URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5067,57 +5079,57 @@ msgstr "Alleen organisatiebeheerders en moderators kunnen plaatsen." msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode emoji" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Aangepaste emoji" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip extra emoji" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5133,20 +5145,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Ontbrekend 'queue_id' argument" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Ontbrekend 'last_event_id' argument" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5318,19 +5330,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Ontbrekende afzender" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Ongeldig gespiegeld bericht" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5396,26 +5408,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Er moet minimaal één authenticatiemethode worden ingeschakeld." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5893,11 +5905,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Moet worden gevalideerd met een geldige Zulip-server-API-sleutel" @@ -5911,43 +5923,43 @@ msgstr "" msgid "Invalid token type" msgstr "Ongeldig tokentype" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/no/LC_MESSAGES/django.po b/locale/no/LC_MESSAGES/django.po index 7146a6d764619..f5c9468860c72 100644 --- a/locale/no/LC_MESSAGES/django.po +++ b/locale/no/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:58+0000\n" +"POT-Creation-Date: 2023-12-15 17:44+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -60,7 +60,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -128,124 +128,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this " "page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new " "plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2901 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You " @@ -255,33 +255,37 @@ msgid "" "we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:136 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:138 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:275 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:280 +#: corporate/views/remote_billing_page.py:693 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:506 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:513 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:517 msgid "Your server registration has been deactivated." msgstr "" @@ -309,7 +313,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -566,31 +570,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:313 templates/corporate/billing.html:339 +#: templates/corporate/billing.html:368 templates/corporate/billing.html:397 +#: templates/corporate/billing.html:423 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:324 templates/corporate/billing.html:353 +#: templates/corporate/billing.html:382 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:326 templates/corporate/billing.html:410 +#: templates/corporate/billing.html:433 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:355 templates/corporate/billing.html:384 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:408 templates/corporate/billing.html:431 msgid "Never mind" msgstr "" @@ -689,6 +693,62 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:4 +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:16 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:4 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:16 +msgid "Plan management not available" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:18 +#, python-format +msgid "" +" Plan management is not available for this\n" +" organization, because your Zulip server is already " +"on a\n" +" %(server_plan_name)s plan, which covers all\n" +" organizations on this server. Follow the log\n" +" in instructions\n" +" for All older versions\n" +" of the Zulip server to manage your plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:29 +msgid "" +" To move the plan from the server to this\n" +" organization, or for other questions, contact support.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:18 +msgid "" +"\n" +" Plan management for this server is not available " +"because at least one organization\n" +" hosted on this server already has an active plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:24 +#, python-format +msgid "" +"\n" +" Log in to plan management for your\n" +" organization instead, or contact support with any questions.\n" +" " +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -831,8 +891,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2276,7 +2334,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2285,7 +2342,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2293,7 +2350,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2309,7 +2365,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2326,16 +2381,25 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for " -"%(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2366,7 +2430,7 @@ msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " -"appreciate it!." +"appreciate it!" msgstr "" #: templates/zerver/find_account.html:4 @@ -3395,65 +3459,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but " "that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The " "stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3470,27 +3534,27 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 -#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:128 +#: zerver/actions/scheduled_messages.py:155 +#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3769,7 +3833,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3948,7 +4012,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4401,45 +4465,45 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:739 zilencer/views.py:241 msgid "Token does not exist" msgstr "" -#: zerver/lib/push_notifications.py:902 +#: zerver/lib/push_notifications.py:910 msgid "" "This organization has disabled including message content in mobile push " "notifications" msgstr "" -#: zerver/lib/push_notifications.py:1015 +#: zerver/lib/push_notifications.py:1023 #, python-brace-format msgid "{full_name} mentioned @{user_group_name}:" msgstr "" -#: zerver/lib/push_notifications.py:1019 +#: zerver/lib/push_notifications.py:1027 #, python-brace-format msgid "{full_name} mentioned you:" msgstr "" -#: zerver/lib/push_notifications.py:1026 +#: zerver/lib/push_notifications.py:1034 #, python-brace-format msgid "{full_name} mentioned everyone:" msgstr "" -#: zerver/lib/push_notifications.py:1436 +#: zerver/lib/push_notifications.py:1438 msgid "Test notification" msgstr "" -#: zerver/lib/push_notifications.py:1437 +#: zerver/lib/push_notifications.py:1439 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." msgstr "" -#: zerver/lib/push_notifications.py:1488 +#: zerver/lib/push_notifications.py:1490 msgid "Device not recognized" msgstr "" -#: zerver/lib/push_notifications.py:1500 +#: zerver/lib/push_notifications.py:1502 msgid "Device not recognized by the push bouncer" msgstr "" @@ -4454,7 +4518,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4501,7 +4565,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4614,7 +4678,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4652,7 +4716,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4879,46 +4943,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5107,15 +5171,15 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" @@ -5173,20 +5237,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5358,19 +5422,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5436,26 +5500,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5933,61 +5997,61 @@ msgid "" "({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" #. error -#: zilencer/views.py:77 zilencer/views.py:79 +#: zilencer/views.py:81 zilencer/views.py:83 msgid "Invalid UUID" msgstr "" #. error -#: zilencer/views.py:84 +#: zilencer/views.py:88 msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:126 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:182 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:185 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:610 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:613 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:620 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:696 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:957 msgid "" "Failed to migrate customer from server to realms. Please contact support for " "assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:1009 msgid "Malformed audit log data" msgstr "" diff --git a/locale/pl/LC_MESSAGES/django.po b/locale/pl/LC_MESSAGES/django.po index 1470ea23abd7a..6a4c49d78cf78 100644 --- a/locale/pl/LC_MESSAGES/django.po +++ b/locale/pl/LC_MESSAGES/django.po @@ -18,7 +18,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Mateusz Galazka , 2021\n" "Language-Team: Polish (http://app.transifex.com/zulip/zulip/language/pl/)\n" @@ -72,7 +72,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "Dane analityczne niedostępne. Skontaktuj się z administratorem serwera." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -143,124 +143,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} kończy się w {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Nieznana metoda płatności. Proszę skontaktuj się z {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Coś poszło nie tak. Proszę skontaktuj się z {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Coś poszło nie tak. Spróbuj odświeżyć stronę." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Coś poszło nie tak. Poczekaj kilka sekund i spróbuj ponownie." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -268,33 +268,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -321,7 +325,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Wewnętrzny błąd serwera" @@ -570,31 +574,31 @@ msgstr "" msgid "Billing" msgstr "Rozliczenie" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Anuluj" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Powterdź" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -693,6 +697,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -834,8 +842,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2257,7 +2263,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2266,7 +2271,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2274,7 +2279,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2290,7 +2294,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2307,15 +2310,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3360,65 +3372,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Nieprawidłowa wiadomość" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Nie można pokazać wiadomości" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3435,28 +3447,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3733,7 +3745,7 @@ msgstr "Wystąpił błąd podczas usuwania załącznika. Spróbuj ponownie póź msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3912,7 +3924,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Nieprawidłowy JSON" @@ -4367,7 +4379,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token nie istnieje" @@ -4420,7 +4432,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Użytkownik nie jest uprawniony do tego zapytania." @@ -4467,7 +4479,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4580,7 +4592,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4618,7 +4630,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4782,7 +4794,7 @@ msgstr "Nieprawidłowy rodzaj bota" msgid "Invalid interface type" msgstr "Nieprawidłowy interfejs" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4845,46 +4857,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5073,57 +5085,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Spersonalizowane emoji" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Lista opcji" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Krótki tekst" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Długi tekst" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5139,20 +5151,20 @@ msgstr "nieznany system operacyjny" msgid "An unknown browser" msgstr "Nieznana przeglądarka" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Brak argumentu 'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Brak argumentu 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5324,19 +5336,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Brak nadawcy" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "nieprawdłowa wiadomość-mirror" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5402,26 +5414,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Należy włączyć przynajmniej jedną metodę uwierzytelniania." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5899,11 +5911,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Należy zweryfikować poprawnym kluczem API serwera Zulp" @@ -5917,43 +5929,43 @@ msgstr "" msgid "Invalid token type" msgstr "Nieprawidłowy rodzaj tokenu." -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/pt/LC_MESSAGES/django.po b/locale/pt/LC_MESSAGES/django.po index 851230a589fc5..924549ebdd145 100644 --- a/locale/pt/LC_MESSAGES/django.po +++ b/locale/pt/LC_MESSAGES/django.po @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Andrei Bernardo Simoni , 2021\n" "Language-Team: Portuguese (http://app.transifex.com/zulip/zulip/language/pt/)\n" @@ -69,7 +69,7 @@ msgstr "Data de início é depois da data de fim. Início: {start}, Fim: {end}" msgid "No analytics data available. Please contact your server administrator." msgstr "Nenhuma dado analítico disponível. Por favor contate o administrador do servidor" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -140,124 +140,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Método de pagamento desconhecido. Por favor contacte {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Erro inesperado. Por favor contacte {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Algo deu errado. Por favor, recarregue a página." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Algo deu errado. Por favor, aguarde alguns segundos e tente novamente." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Deve ser um administrador de faturamento ou proprietário da organização" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -265,33 +265,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -318,7 +322,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Erro interno do servidor" @@ -567,31 +571,31 @@ msgstr "Certifique-se de copiar o link corretamente para o seu navegador. Se voc msgid "Billing" msgstr "Faturamento" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Cancelar" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Confirmar" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -690,6 +694,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -831,8 +839,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2254,7 +2260,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2263,7 +2268,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2271,7 +2276,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2287,7 +2291,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2304,15 +2307,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3357,65 +3369,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Mensagem inválida" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Não é possível processar a mensagem" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: o programador da API enviou conteúdo JSON inválido" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3432,28 +3444,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3730,7 +3742,7 @@ msgstr "Ocorreu um erro ao excluir o anexo. Por favor, tente novamente mais tard msgid "Message must have recipients!" msgstr "Mensagem deve ter destinatários!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3909,7 +3921,7 @@ msgid "API usage exceeded rate limit" msgstr "Uso de API excedeu a taxa limite" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON mal formado" @@ -4364,7 +4376,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token não existe" @@ -4417,7 +4429,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Usuário não autorizado para esta consulta" @@ -4464,7 +4476,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4577,7 +4589,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4615,7 +4627,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4779,7 +4791,7 @@ msgstr "Tipo do bot inválido" msgid "Invalid interface type" msgstr "Tipo de interface inválido" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4842,46 +4854,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' não pode estar em branco." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' não é uma opção válida para '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5070,57 +5082,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "Apenas membros plenos da organização podem postar" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Emoji unicode" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoji personalizado" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Emoji extra do Zulip" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Lista de opções" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Selecionador de pessoa" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Texto curto" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Texto longo" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Selecionador de data" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Conta externa" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5136,20 +5148,20 @@ msgstr "um sistema operacional desconhecido" msgid "An unknown browser" msgstr "Um navegador desconhecido" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Argumento 'queue_id' ausente" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Argumento 'last_event_id' ausente" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5321,19 +5333,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Remetente ausente" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Espelhamento não permitido com IDs de usuário do destinatário" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Mensagem espelhada inválida" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Espelhamento do Zephyr não é permitido nesta organização" @@ -5399,26 +5411,26 @@ msgstr "Pelo menos um dos seguintes argumentos deve estar presente: emoji_name, msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Pelo menos um método de autenticação deve estar habilitado." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5896,11 +5908,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Subdomínio inválido para retorno de notificações via push" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Deve validar com chave API válida do servidor Zulip" @@ -5914,43 +5926,43 @@ msgstr "" msgid "Invalid token type" msgstr "Tipo de token inválido" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Dados estão fora de ordem." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/pt_BR/LC_MESSAGES/django.po b/locale/pt_BR/LC_MESSAGES/django.po index 08160d9f57092..dae0863565c83 100644 --- a/locale/pt_BR/LC_MESSAGES/django.po +++ b/locale/pt_BR/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:58+0000\n" +"POT-Creation-Date: 2023-12-15 17:44+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Luiz François , 2021\n" "Language-Team: Portuguese (Brazil) (http://app.transifex.com/zulip/zulip/" @@ -67,7 +67,7 @@ msgstr "" "Sem dados de analytics disponíveis. Por favor contate seu administrador do " "servidor." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -156,14 +156,14 @@ msgstr "" msgid "Invalid remote server." msgstr "Código de emoji inválido." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, fuzzy, python-brace-format #| msgid "" #| "Invoices with more than {} licenses can't be processed from this page. To " @@ -175,45 +175,45 @@ msgstr "" "Faturas com mais de {} licenças não podem ser processadas por esta página. " "Para completar o upgrade, por favor. contate {}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 #, fuzzy #| msgid "No payment method on file" msgid "No payment method on file." msgstr "Nenhum método de pagamento cadastrado" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} terminando em {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Método de pagamento desconhecido. Por favor, contate {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Algo errado aconteceu. Por favor contate {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Algo errado aconteceu. Por favor, recarregue a página." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" "Algo errado aconteceu. Por favor aguarde alguns segundos e tente de novo." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new " "plan." @@ -221,11 +221,11 @@ msgstr "" "Não foi possível atualizar o plano. O plano foi expirado e substituído por " "um novo plano." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Não foi possível atualizar o plano. O plano terminou." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 #, fuzzy #| msgid "" #| "Your plan is already on {licenses} licenses in the current billing period." @@ -233,7 +233,7 @@ msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "Seu planos já está com {licenses} licenças no periodo atual de fatura." -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." @@ -241,54 +241,54 @@ msgstr "" "Não foi possível atualizar as licenças manualmente. O seu plano está em " "gestão automática de licenças." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Seu planos já está com {licenses} licenças no periodo atual de fatura." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Nada a alterar." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Sem clientes para esta organização!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Sessão não encontrada" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2901 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You " @@ -298,39 +298,43 @@ msgid "" "we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:136 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:138 #, fuzzy #| msgid "Invalid interface type" msgid "Invalid billing access token." msgstr "Tipo de interface inválido" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:275 #, fuzzy #| msgid "Reaction doesn't exist." msgid "User account doesn't exist yet." msgstr "Esta reação não existe." -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:280 +#: corporate/views/remote_billing_page.py:693 #, fuzzy #| msgid "Accept the Terms of Service" msgid "You must accept the Terms of Service to proceed." msgstr "Aceitar os Termos de Serviço" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:506 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:513 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:517 #, fuzzy #| msgid "This custom emoji has been deactivated." msgid "Your server registration has been deactivated." @@ -360,7 +364,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Erro interno do servidor" @@ -627,31 +631,31 @@ msgstr "" msgid "Billing" msgstr "Faturamento" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:313 templates/corporate/billing.html:339 +#: templates/corporate/billing.html:368 templates/corporate/billing.html:397 +#: templates/corporate/billing.html:423 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:324 templates/corporate/billing.html:353 +#: templates/corporate/billing.html:382 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Cancelar" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:326 templates/corporate/billing.html:410 +#: templates/corporate/billing.html:433 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:355 templates/corporate/billing.html:384 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Confirmar" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:408 templates/corporate/billing.html:431 msgid "Never mind" msgstr "" @@ -750,6 +754,64 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +#, fuzzy +#| msgid "Deactivated organization" +msgid "Deactivate server registration?" +msgstr "Organização desativada" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:4 +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:16 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:4 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:16 +msgid "Plan management not available" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:18 +#, python-format +msgid "" +" Plan management is not available for this\n" +" organization, because your Zulip server is already " +"on a\n" +" %(server_plan_name)s plan, which covers all\n" +" organizations on this server. Follow the log\n" +" in instructions\n" +" for All older versions\n" +" of the Zulip server to manage your plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:29 +msgid "" +" To move the plan from the server to this\n" +" organization, or for other questions, contact support.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:18 +msgid "" +"\n" +" Plan management for this server is not available " +"because at least one organization\n" +" hosted on this server already has an active plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:24 +#, python-format +msgid "" +"\n" +" Log in to plan management for your\n" +" organization instead, or contact support with any questions.\n" +" " +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -894,8 +956,6 @@ msgstr "Já tem uma conta." #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2351,7 +2411,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2360,7 +2419,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2368,7 +2427,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2384,7 +2442,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2401,16 +2458,29 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for " -"%(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +#, fuzzy +#| msgid "Confirm" +msgid "Confirm and log in" +msgstr "Confirmar" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +#, fuzzy +#| msgid "Confirm email change" +msgid "Confirm email for Zulip plan management" +msgstr "Confirmar alteração de e-mail" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2441,7 +2511,7 @@ msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " -"appreciate it!." +"appreciate it!" msgstr "" #: templates/zerver/find_account.html:4 @@ -3475,67 +3545,67 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Incapaz de processar a mensagem" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but " "that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The " "stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 #, fuzzy #| msgid "You don't have permission to edit this message" msgid "You do not have permission to access some of the recipients." msgstr "Você não tem permissão para editar esta mensagem" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3552,27 +3622,27 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 -#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:128 +#: zerver/actions/scheduled_messages.py:155 +#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3856,7 +3926,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -4036,7 +4106,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON corrompido" @@ -4497,45 +4567,45 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:739 zilencer/views.py:241 msgid "Token does not exist" msgstr "A token não existe" -#: zerver/lib/push_notifications.py:902 +#: zerver/lib/push_notifications.py:910 msgid "" "This organization has disabled including message content in mobile push " "notifications" msgstr "" -#: zerver/lib/push_notifications.py:1015 +#: zerver/lib/push_notifications.py:1023 #, python-brace-format msgid "{full_name} mentioned @{user_group_name}:" msgstr "" -#: zerver/lib/push_notifications.py:1019 +#: zerver/lib/push_notifications.py:1027 #, python-brace-format msgid "{full_name} mentioned you:" msgstr "" -#: zerver/lib/push_notifications.py:1026 +#: zerver/lib/push_notifications.py:1034 #, python-brace-format msgid "{full_name} mentioned everyone:" msgstr "" -#: zerver/lib/push_notifications.py:1436 +#: zerver/lib/push_notifications.py:1438 msgid "Test notification" msgstr "" -#: zerver/lib/push_notifications.py:1437 +#: zerver/lib/push_notifications.py:1439 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." msgstr "" -#: zerver/lib/push_notifications.py:1488 +#: zerver/lib/push_notifications.py:1490 msgid "Device not recognized" msgstr "" -#: zerver/lib/push_notifications.py:1500 +#: zerver/lib/push_notifications.py:1502 msgid "Device not recognized by the push bouncer" msgstr "" @@ -4550,7 +4620,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Usuário não autorizado para esta consulta" @@ -4597,7 +4667,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4715,7 +4785,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4753,7 +4823,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4983,46 +5053,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5213,15 +5283,15 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoji customizado" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Emoji extra do Zulip" @@ -5279,20 +5349,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Faltando o argumento 'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Faltando o argumento 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5464,19 +5534,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Falta o remetente" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Mensagem espelhada inválida" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5545,26 +5615,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Pelo menos um método de autenticação deve ser ativado." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -6050,66 +6120,66 @@ msgid "" "({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" #. error -#: zilencer/views.py:77 zilencer/views.py:79 +#: zilencer/views.py:81 zilencer/views.py:83 msgid "Invalid UUID" msgstr "" #. error -#: zilencer/views.py:84 +#: zilencer/views.py:88 msgid "Invalid token type" msgstr "Tipo de token inválido" -#: zilencer/views.py:121 +#: zilencer/views.py:126 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:182 #, fuzzy #| msgid "Missing sender" msgid "Missing ios_app_id" msgstr "Falta o remetente" -#: zilencer/views.py:177 +#: zilencer/views.py:185 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:610 #, fuzzy, python-brace-format #| msgid "Invalid parameters" msgid "Invalid property {property}" msgstr "Parâmetros inválidos" -#: zilencer/views.py:605 +#: zilencer/views.py:613 #, fuzzy #| msgid "Invalid token type" msgid "Invalid event type." msgstr "Tipo de token inválido" -#: zilencer/views.py:612 +#: zilencer/views.py:620 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:696 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:957 msgid "" "Failed to migrate customer from server to realms. Please contact support for " "assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:1009 msgid "Malformed audit log data" msgstr "" @@ -6134,11 +6204,6 @@ msgstr "" #~ msgid "Invalid server_org_secret." #~ msgstr "Cache do Kerberos inválido" -#, fuzzy -#~| msgid "Confirm" -#~ msgid "Confirm login" -#~ msgstr "Confirmar" - #~ msgid "Payment processing." #~ msgstr "Pagamento em processamento." diff --git a/locale/pt_PT/LC_MESSAGES/django.po b/locale/pt_PT/LC_MESSAGES/django.po index 19d8ee5ac59f2..880530685f2a5 100644 --- a/locale/pt_PT/LC_MESSAGES/django.po +++ b/locale/pt_PT/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: João Alves , 2021-2022\n" "Language-Team: Portuguese (Portugal) (http://app.transifex.com/zulip/zulip/language/pt_PT/)\n" @@ -63,7 +63,7 @@ msgstr "A hora de início é posterior à hora de fim. Início: {start}, Fim: {e msgid "No analytics data available. Please contact your server administrator." msgstr "Sem dados analíticos disponíveis. Por favor contacte o administrador do servidor." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -134,124 +134,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Método de pagamento desconhecido. Por favor contacte {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Algo correu mal. Por favor contacte {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Algo correu mal. Por favor recarregue a página." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Algo correu mal. Por favor aguarde alguns segundos e volte a tentar." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Não foi possível actualizar o plano. O plano foi expirado e substituído com um novo plano." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Não foi possível actualizar o plano. O plano terminou." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Não foi possível actualizar as licenças manualmente. O seu plano está em gestão automática de licenças." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "O seu plano já está em {licenses} licenças no período de facturação actual." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Não pode diminuir as licenças no período de facturação actual." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "O seu plano já está agendado para renovação com {licenses_at_next_renewal} licenças." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Nada a alterar." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Sem clientes para esta organização!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Sessão não encontrada" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Tem de ser administrador da facturação ou proprietário da organização" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -259,33 +259,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -312,7 +316,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Erro interno do servidor" @@ -561,31 +565,31 @@ msgstr "Certifique-se de que copiou o link correctamente para o seu navegador. S msgid "Billing" msgstr "Faturação" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Fechar modal" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Cancelar" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Confirmar" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -684,6 +688,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -825,8 +833,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2248,7 +2254,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2257,7 +2262,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2265,7 +2270,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2281,7 +2285,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2298,15 +2301,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3351,65 +3363,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Os tópicos são obrigatórios nesta organização" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3426,28 +3438,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3724,7 +3736,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "A mensagem tem de ter destinatários!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3903,7 +3915,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4358,7 +4370,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4411,7 +4423,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4458,7 +4470,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4571,7 +4583,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} não é uma data" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4609,7 +4621,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} não é uma lista" @@ -4773,7 +4785,7 @@ msgstr "Tipo de bot inválido" msgid "Invalid interface type" msgstr "Tipo de interface inválido" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4836,46 +4848,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} está errado)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} não é um URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' não é uma opção válida para '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5064,57 +5076,57 @@ msgstr "Apenas administradores da organização e moderadores podem publicar" msgid "Only organization full members can post" msgstr "Apenas membros integrais da organização podem publicar" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoji personalizados" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Lista de opções" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Texto curto" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Texto longo" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Conta externa" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Pronomes" @@ -5130,20 +5142,20 @@ msgstr "um sistema operativo desconhecido" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Argumento 'queue_id' em falta" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Argumento 'last_event_id' em falta" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5315,19 +5327,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Destinatário em falta" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5393,26 +5405,26 @@ msgstr "Pelo menos um dos seguintes argumentos tem de estar presente: emoji_name msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Pelo menos um método de autenticação tem de ser permitido." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5890,11 +5902,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5908,43 +5920,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/ro/LC_MESSAGES/django.po b/locale/ro/LC_MESSAGES/django.po index 94bc7cb2a03c0..d8f4bed8a6a12 100644 --- a/locale/ro/LC_MESSAGES/django.po +++ b/locale/ro/LC_MESSAGES/django.po @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Viorel-Cosmin Miron , 2022-2023\n" "Language-Team: Romanian (http://app.transifex.com/zulip/zulip/language/ro/)\n" @@ -69,7 +69,7 @@ msgstr "Începutul este mai târziu decât sfârșitul. Început: {start}, Sfâr msgid "No analytics data available. Please contact your server administrator." msgstr "Nu există date analitice. Vă rugăm contactați administratorul serverului." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -140,124 +140,124 @@ msgstr "Înregistrarea este dezactivată" msgid "Invalid remote server." msgstr "Server la distanță invalid." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "Trebuie să achiziționați licențe pentru toți utilizatorii activi din organizația dvs. (minim {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Facturile cu mai mult de {max_licenses} licențe nu pot fi procesate din această pagină. Pentru a finaliza actualizarea, vă rugăm să contactați {email}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "Nu există nicio metodă de plată în dosar." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} se termină în {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Metodă de plată necunoscută. Vă rugam contactați-ne aici {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Ceva nu a mers bine. Vă rugam contactați {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Ceva a funcționat greșit. Te rugam reîncărcă pagina." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Ceva a funcționat greșit. Te rugam să aștepți câteva secunde apoi reîncearcă." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "Vă rugăm să adăugați un card de credit înainte de a începe testarea gratuită." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "Vă rugăm să adăugați un card de credit pentru a programa upgrade-ul." -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Nu se poate actualiza planul. Planul a fost expirat și înlocuit cu un nou plan." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Nu se poate actualiza planul. Planul s-a încheiat." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "Nu se pot actualiza licențele în perioada de facturare curentă pentru planul de testare gratuită." -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Nu se pot actualiza manual licențele. Planul dvs. este de gestionare automată a licențelor." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Planul dvs. este deja pe licențe {licenses} în perioada de facturare curentă." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Nu puteți reduce licențele în perioada curentă de facturare." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "Nu se pot modifica licențele pentru următorul ciclu de facturare pentru un plan care este retrogradat." -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Planul dvs. este deja programat să fie reînnoit cu licențe {licenses_at_next_renewal}." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Nimic de schimbat." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Niciun client pentru această organizație!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Sesiune nu a fost găsită" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Trebuie să fie administrator care facturează sau proprietarul organizaţiei" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Intenția de plată nu a fost găsită" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Treceți stripe_session_id sau stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -265,33 +265,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "Cererea organizației dumneavoastră pentru găzduire sponsorizată a fost aprobată! Ați fost actualizat la {plan_name}, gratuit. {emoji}\n\nDacă ați putea {begin_link}să menționați Zulip ca sponsor pe site-ul dvs.{end_link}, am aprecia foarte mult!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "Tokenul de acces la facturare a expirat." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "Token de acces la facturare nevalabil." -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "Trebuie să acceptați Termenii de utilizare pentru a continua." -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "Înregistrarea serverului dvs. a fost dezactivată." @@ -318,7 +322,7 @@ msgid "" msgstr "\n Dacă această eroare este neașteptată, puteți\n contactați asistența.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Eroare internă a serverului" @@ -567,31 +571,31 @@ msgstr "Asigurați-vă că ați copiat corect link-ul în browserul dumneavoastr msgid "Billing" msgstr "Facturare" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Închidere modal" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Renunță" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "Retrogradare" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Confirmă" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "Nu contează" @@ -690,6 +694,10 @@ msgstr "Prețuri pentru educație" msgid "View pricing" msgstr "Vedeți prețurile" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip pentru afaceri" @@ -831,8 +839,6 @@ msgstr "Ai deja un cont?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2254,7 +2260,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2263,7 +2268,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2271,7 +2276,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2287,7 +2291,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2304,15 +2307,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3357,65 +3369,65 @@ msgstr "Operațiune de marcare a mesajului invalidă: \"{operation}" msgid "Invalid message(s)" msgstr "Mesaj nevalid(e)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Imposibil de redat mesajul" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Se așteaptă exact un hub" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Tip nevalid de date pentru hub" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Tip de date nevalid pentru destinatari" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Lista destinatarilor poate conține emailuri sau ID, dar nu ambele." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Botul tău {bot_identity} a încercat să trimită un mesaj în hubul cu ID {stream_id}, dar nu există nici un hub cu acest ID." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Botul tău {bot_identity} a încercat să trimită un mesaj în hubul {stream_name}, dar acesta nu există. Apasă [aici]({new_stream_link}) pentru a-l crea." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Botul tău {bot_identity} a încercat să trimită un mesaj în hubul {stream_name}. Hubul există dar nu are abonați." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Mesajele directe sunt dezactivate în această organizație." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "Nu aveți permisiunea de a accesa unii dintre destinatari." -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Subiectele sunt necesare în această organizație" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: Programatorul API a trimis JSON conținut nevalid" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3432,28 +3444,28 @@ msgstr "Lista ordonată nu trebuie să conțină linkificatori duplicați" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "Lista ordonată trebuie să enumere toți linkificatorii existenți exact o dată" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "Mesajul programat a fost deja trimis" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "Termenul de livrare programat trebuie să fie în viitor." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "Mesajul nu a putut fi trimis la ora programată." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "Mesajul pe care l-ați programat pentru {delivery_datetime} nu a fost trimis din cauza următoarei erori:" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "[Vezi mesajele programate](#scheduled)" @@ -3730,7 +3742,7 @@ msgstr "A apărut o eroare la ștergerea atașamentului. Te rog încearcă mai t msgid "Message must have recipients!" msgstr "Mesajul trebuie să aibă destinatari!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "{service_name} digest" @@ -3909,7 +3921,7 @@ msgid "API usage exceeded rate limit" msgstr "Utilizarea API-ului depășește rata limită" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON malformat" @@ -4364,7 +4376,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "Opțiuni MCG nevalabile pentru bouncer: {options}" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Tokenul nu există" @@ -4417,7 +4429,7 @@ msgstr "Lista de destinatari poate conține numai ID-uri de utilizator" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Utilizator neautorizat pentru această interogare" @@ -4464,7 +4476,7 @@ msgstr "Argumentul \"{name}\" nu este JSON valid." msgid "Scheduled message does not exist" msgstr "Mesajul programat nu există" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "{service_name} securitatea contului" @@ -4577,7 +4589,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} nu este o dată" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} nu este dict" @@ -4615,7 +4627,7 @@ msgid "{var_name} is too large" msgstr "{var_name} este prea mare" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} nu este o listă" @@ -4779,7 +4791,7 @@ msgstr "Tip de bot nevalid" msgid "Invalid interface type" msgstr "Tip de interfață nevalidă" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "ID-ul de utilizator invalid: {user_id}" @@ -4842,46 +4854,46 @@ msgstr "{var_name} nu este allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} este greșit/ă)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} nu este un URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "Modelul adresei URL trebuie să conțină \"%(username)s\"." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' nu poate fi gol." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Câmpul nu trebuie să aibă opțiuni dublate." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' nu este o opțiune validă pentru '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} nu este un șir sau o listă integer" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} nu este un string sau integer" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} nu are o lungime" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} lipsește" @@ -5070,57 +5082,57 @@ msgstr "Numai administratorii și moderatorii organizației pot posta" msgid "Only organization full members can post" msgstr "Numai membrii cu drepturi depline al organizației pot posta" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Emoji unicode" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Emoji personalizat" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Emoji extra Zulip" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "Utilizatorul cu ID {user_id} este dezactivat" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "Utilizatorul cu ID-ul {user_id} este un bot" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Lista opțiunilor" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Alege persoana" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Test scurt" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Test lung" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Alege data" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Cont extern" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Pronume" @@ -5136,20 +5148,20 @@ msgstr "un sistem de operare necunoscut" msgid "An unknown browser" msgstr "Un browser necunoscut" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Lipsește argumentul 'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Lipsește argumentul 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Un eveniment mai nou decât {event_id} a fost șters!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Evenimentul {event_id} nu a fost în această listă" @@ -5321,19 +5333,19 @@ msgstr "Ancora poate fi exclusă numai la un capăt al intervalului" msgid "No such topic '{topic}'" msgstr "Nu există un astfel de subiect '{topic}'" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Lipseste expeditorul" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Mirroring nu este permis cu ID-urile de utilizator destinatar" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Mesaj mirrored nevalid" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Mirroring Zephyr nu este permis în această organizație" @@ -5399,26 +5411,26 @@ msgstr "Cel puțin unul din următoarele argumente trebuie să fie prezente: emo msgid "Read receipts are disabled in this organization." msgstr "Confirmările de citire sunt dezactivate în această organizație." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "Limba invalidă '{language}'" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Cel puțin o metodă de autentificare trebuie activată." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "Video_chat_provider invalid {video_chat_provider}" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "Giphy_rating nevalabil {giphy_rating}" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Trebuie să fie o organizație demonstrativă." @@ -5896,11 +5908,11 @@ msgid "" "exports]({export_settings_link})." msgstr "Exportul de date este finalizat. [Vizualizați și descărcați exporturile]({export_settings_link})." -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Subdomeniu nevalid pentru bouncerul cu notificări push" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Trebuie validat cu o cheie API validă Zulip" @@ -5914,43 +5926,43 @@ msgstr "UUID nevalid" msgid "Invalid token type" msgstr "Tip de token nevalid" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "{hostname} nu este un nume de gazdă valid" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "Lipsește ios_app_id" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "Lipsă user_id sau user_uuid" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "Proprietate invalidă {property}" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Datele sunt în dezordine." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "Înregistrare duplicată detectată." -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "Date de jurnal de audit malformate" diff --git a/locale/ru/LC_MESSAGES/django.po b/locale/ru/LC_MESSAGES/django.po index b36c742c92d21..77ad9c20ba716 100644 --- a/locale/ru/LC_MESSAGES/django.po +++ b/locale/ru/LC_MESSAGES/django.po @@ -25,7 +25,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Lev Shereshevsky, 2022-2023\n" "Language-Team: Russian (http://app.transifex.com/zulip/zulip/language/ru/)\n" @@ -79,7 +79,7 @@ msgstr "Начальное время больше конечного. Нача msgid "No analytics data available. Please contact your server administrator." msgstr "Данные аналитики недоступны. Обратитесь к администратору сервера." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -150,124 +150,124 @@ msgstr "Регистрация отключена" msgid "Invalid remote server." msgstr "Неверный удаленный сервер." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "Вам необходимо приобрести лицензии для всех активных пользователей вашей организации (минимум {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Счета на более чем {max_licenses} лицензий не могут быть обработаны на этой странице. Для завершения апгрейда, пожалуйста, свяжитесь с {email}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "Не указан способ оплаты." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} заканчивается через {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Неизвестный способ оплаты. Пожалуйста, свяжитесь с {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Что-то пошло не так. Пожалуйста, обратитесь к {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Что-то пошло не так. Пожалуйста, перезагрузите страницу." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Что-то пошло не так. Пожалуйста, подождите несколько секунд и попробуйте еще раз." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "Пожалуйста, укажите данные платежной карты перед началом бесплатного пробного использования." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "Пожалуйста, укажите данные платежной карты для планирования обновления." -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Невозможно обновить тариф. Тарифный план истек и заменен новым. " -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Невозможно обновить тариф. Тарифный план закончился." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "Не удается обновить лицензии в текущем расчетном периоде для бесплатного пробного тарифа." -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Невозможно обновить лицензии вручную. На вашем тарифе лицензия управляется автоматически." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Ваш тариф уже включает {licenses} лицензий в текущем расчетном периоде. " -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Вы не можете уменьшить количество лицензий в текущем расчетном периоде. " -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "Не удается изменить лицензии для следующего платежного периода для тарифа, уровень которого будет понижен." -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Ваш тариф уже запланирован к продлению на {licenses_at_next_renewal} лицензий." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Ничего не изменилось." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "У этой организации нет клиентов!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Сеанс не найден" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Нужно быть администратором, выставляющим счета, либо владельцем организации" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Назначение платежа не найдено" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Передайте stripe_session_id или stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -275,33 +275,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "Запрос вашей организации на спонсируемый хостинг одобрен! Вы были переведены на {plan_name} бесплатно. {emoji}\n\nЕсли вы сможете {begin_link}указать Zulip в качестве спонсора на своем веб-сайте{end_link}, мы будем вам очень признательны!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "Срок действия токена платежного доступа истек." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "Недействительный токен платежного доступа." -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." -msgstr "" +msgstr "Учетная запись пользователя еще не создана." -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "Чтобы продолжить, вы должны принять Условия использования." -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." -msgstr "" +msgstr "Этот zulip_org_id не зарегистрирован в системе управления платежами Zulip." -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." -msgstr "" +msgstr "Неверный zulip_org_key для данного zulip_org_id." -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "Регистрация вашего сервера отозвана." @@ -328,7 +332,7 @@ msgid "" msgstr "\n Если эта ошибка непредвиденная, вы можете\n связаться с поддержкой.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Внутренняя ошибка сервера" @@ -577,31 +581,31 @@ msgstr "Убедитесь, что вы правильно скопировал msgid "Billing" msgstr "Платежи" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Закрыть модальное окно" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Отмена" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "Даунгрейд" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Подтвердить" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "Не имеет значения" @@ -700,6 +704,10 @@ msgstr "Цены на образование" msgid "View pricing" msgstr "Показать цены" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip для бизнеса" @@ -841,8 +849,6 @@ msgstr "Уже есть учетная запись?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -1049,11 +1055,11 @@ msgstr "\n Пожалуйста, свяжитесь с %(realm_name)s.\n" " " -msgstr "Вы подключились к организации Zulip %(realm_name)s." +msgstr "Вы присоединились к организации Zulip %(realm_name)s." #: templates/zerver/emails/account_registered.html:32 #, python-format @@ -1258,7 +1264,7 @@ msgstr "Вы создали новую организацию Zulip: %(realm_nam #: templates/zerver/emails/account_registered.txt:10 #, python-format msgid "You've joined the Zulip organization %(realm_name)s." -msgstr "Вы подключились к организации Zulip %(realm_name)s." +msgstr "Вы присоединились к организации Zulip %(realm_name)s." #: templates/zerver/emails/account_registered.txt:14 #, python-format @@ -2254,53 +2260,50 @@ msgstr "Нажмите кнопку ниже, чтобы реактивиров msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "Вы или кто-то от вашего имени запросил ссылку для входа в систему управления тарифом Zulip для %(remote_server_hostname)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:12 msgid "" "\n" " Click the button below to log in.\n" " " -msgstr "" +msgstr "\n Для входа нажмите кнопку ниже.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" " This link will expire in %(validity_in_hours)s hours.\n" " " -msgstr "" +msgstr "\n Срок действия этой ссылки истечет через %(validity_in_hours)s ч.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Есть вопросы? Узнайте больше или свяжитесь с %(billing_contact_email)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" -msgstr "" +msgstr "Вход в систему управления тарифом Zulip" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:1 #, python-format msgid "" "Either you, or someone on your behalf, has requested a log in link to manage" " the Zulip plan for %(remote_server_hostname)s." -msgstr "" +msgstr "Вы или кто-то от вашего имени запросил ссылку для входа в систему управления тарифом Zulip для %(remote_server_hostname)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 msgid "Click the link below to log in." -msgstr "" +msgstr "Для входа нажмите ссылку ниже." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." -msgstr "" +msgstr "Срок действия этой ссылки истечет через %(validity_in_hours)s ч." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:8 #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:6 @@ -2308,21 +2311,30 @@ msgstr "" msgid "" "Questions? Learn more at %(billing_help_link)s or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Есть вопросы? Узнайте больше на %(billing_help_link)s или свяжитесь с %(billing_contact_email)s." #: templates/zerver/emails/remote_realm_billing_confirm_login.html:9 #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2579,7 +2591,7 @@ msgid "" " You are using old version of the Zulip desktop\n" " app that is no longer supported.\n" " " -msgstr "\n Вы используете устаревшую версию приложения Zulip для\n рабочего стола, которая больше не поддерживается.\n " +msgstr "\n Вы используете устаревшую версию приложения Zulip\n для компьютера, которая больше не поддерживается.\n " #: templates/zerver/insecure_desktop_app.html:25 msgid "" @@ -2587,7 +2599,7 @@ msgid "" " The auto-update feature in this old version of\n" " Zulip desktop app no longer works.\n" " " -msgstr "\n Функция автоматического обновления в настоящей, устаревшей версии\n приложения Zulip для рабочего стола более не работает.\n " +msgstr "\n Функция автообновления в этой устаревшей версии\n приложения Zulip для компьютера уже не работает.\n " #: templates/zerver/insecure_desktop_app.html:34 msgid "Download the latest release." @@ -3130,11 +3142,11 @@ msgid "" "\n" " You can also use the Zulip desktop app.\n" " " -msgstr "\n Вы также можете\n использовать приложение\n Zulip для рабочего стола.\n " +msgstr "\n Вы также можете\n использовать приложение\n Zulip для компьютера.\n " #: templates/zilencer/remote_realm_server_mismatch_error.html:16 msgid "Unexpected Zulip server registration" -msgstr "" +msgstr "Неожиданная регистрация сервера Zulip" #: templates/zilencer/remote_realm_server_mismatch_error.html:18 #, python-format @@ -3146,7 +3158,7 @@ msgid "" " Please contact Zulip support\n" " for assistance in resolving this issue.\n" " " -msgstr "" +msgstr "\n Ваша организация привязана к \n другому серверу Zulip.\n\n Свяжитесь с поддержкой Zulip\n для помощи в решении этой проблемы.\n " #: zerver/actions/create_user.py:98 msgid "signups" @@ -3367,65 +3379,65 @@ msgstr "Неверное действие над флагом сообщения msgid "Invalid message(s)" msgstr "Неверное сообщение(я)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Невозможно отобразить сообщение" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Ожидал ровно один канал" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Неверный тип данных для канала" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Неверный тип данных для получателей" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "В списках получателей могут быть адреса электронной почты или ID пользователей, но не то и другое." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Ваш бот {bot_identity} попытался послать сообщение в канал с ID {stream_id}, но канала с таким ID не существует." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Ваш бот {bot_identity} попытался послать сообщение в канал {stream_name}, но такого канала не существует. Нажмите [здесь]({new_stream_link}), чтобы его создать." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Ваш бот {bot_identity} попытался послать сообщение в канал {stream_name}. Канал существует, но на него никто не подписан." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Личные сообщения в этой организации отключены." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "У вас нет права доступа к некоторым получателям." -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Темы необходимы в этой организации" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Виджеты: программист API послал неверное JSON-содержание" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Виджеты: {error_msg}" @@ -3442,28 +3454,28 @@ msgstr "В упорядоченном списке не должно быть п msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "Упорядоченный список должен включать все имеющиеся шаблоны ссылок ровно по одному разу" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "Запланированное сообщение уже отправлено" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "Запланированное время доставки должно быть в будущем." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "Сообщение не может быть отправлено в запланированное время." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "Сообщение, которое вы запланировали на {delivery_datetime}, не было отправлено из-за следующей ошибки:" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "[Показать запланированные сообщения](#scheduled)" @@ -3740,7 +3752,7 @@ msgstr "При удалении вложения произошла ошибка msgid "Message must have recipients!" msgstr "У сообщения должен быть получатель!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "дайджест {service_name}" @@ -3919,7 +3931,7 @@ msgid "API usage exceeded rate limit" msgstr "Частота использования API превысила ограничение." #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Неверный JSON" @@ -4014,7 +4026,7 @@ msgstr "Реакция не существует." msgid "" "Your organization is registered to a different Zulip server. Please contact " "Zulip support for assistance in resolving this issue." -msgstr "" +msgstr "Ваша организация привязана к другому серверу Zulip. Пожалуйста, свяжитесь с поддержкой Zulip для помощи в решении этой проблемы." #: zerver/lib/exceptions.py:621 msgid "Organization not registered" @@ -4374,7 +4386,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "Неверные параметры GCM для отражателя: {options}" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Токен не существует" @@ -4427,7 +4439,7 @@ msgstr "Список получателей может содержать тол #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "У пользователя нет полномочий для такого запроса" @@ -4474,7 +4486,7 @@ msgstr "Параметр \"{name}\" не является корректным J msgid "Scheduled message does not exist" msgstr "Запланированное сообщение не существует" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "безопасность аккаунта {service_name} " @@ -4587,7 +4599,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} не является датой" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} не является словарем" @@ -4625,7 +4637,7 @@ msgid "{var_name} is too large" msgstr "{var_name} слишком большая" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} не является списком" @@ -4789,7 +4801,7 @@ msgstr "Неверный тип бота" msgid "Invalid interface type" msgstr "Неверный тип интерфейса" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "Неверный ID пользователя: {user_id}" @@ -4852,46 +4864,46 @@ msgstr "{var_name} не является allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} неверно)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} не является URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "Шаблон URL должен содержать '%(username)s'." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' не может быть пустым." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Поле не должно содержать одинаковые варианты." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' не является допустимым выбором для '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} не является списком строк или целых чисел" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} не является строкой или цельным числом" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} не имеет длины" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} отсутствует" @@ -5080,57 +5092,57 @@ msgstr "Только администраторы организации и мо msgid "Only organization full members can post" msgstr "Только полноценные члены организации могут писать" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Юникод эмодзи" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Дополнительные эмодзи" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Дополнительные эмодзи Zulip" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "Пользователь с ID {user_id} отключен" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "Пользователь с ID {user_id} является ботом" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Список вариантов" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Выбор человека" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Короткий текст" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Длинный текст" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Выбор даты" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Ссылка" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Внешняя учетная запись" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Местоимения" @@ -5146,20 +5158,20 @@ msgstr "неизвестная операционная система" msgid "An unknown browser" msgstr "Неизвестный браузер" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Отсутствует параметр 'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Отсутствует параметр 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Более новое событие чем {event_id} уже было удалено!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "События {event_id} не было в этой очереди" @@ -5331,19 +5343,19 @@ msgstr "Якорь можно исключить только в конце ди msgid "No such topic '{topic}'" msgstr "Нет такой темы '{topic}'" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Не указан отправитель" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Зеркалирование не разрешено с ID пользователя получателей" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Неверное зеркалированное сообщение" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "В этой организации зеркалирование Zephyr не разрешено" @@ -5409,26 +5421,26 @@ msgstr "Как минимум один из следующих параметр msgid "Read receipts are disabled in this organization." msgstr "Уведомления о прочтении в настоящее время отключены в этой организации." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "Неверный язык '{language}'" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Включите хотя бы один способ аутентификации" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "Неверный провайдер видеочатов {video_chat_provider}" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "Неверный giphy_rating {giphy_rating}" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Должна быть демонстрационная организация." @@ -5906,11 +5918,11 @@ msgid "" "exports]({export_settings_link})." msgstr "Экспорт ваших данных завершен. [Просмотрите и скачайте их]({export_settings_link})." -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Неверный поддомен для отражателя push-уведомлений" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Необходимо проверить с помощью действительного API-ключа сервера Zulip" @@ -5924,43 +5936,43 @@ msgstr "Неверный UUID" msgid "Invalid token type" msgstr "Неверный тип токена" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "{hostname} не является корректным именем хоста" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "Отсутствует ios_app_id" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "Отсутствует user_id или user_uuid" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "Неверное свойство {property}" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." -msgstr "" +msgstr "Неверный тип события." -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Данные не в порядке." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "Обнаружена повторная регистрация." -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." -msgstr "" +msgstr "Не удалось перенести клиента с сервера в облако. Пожалуйста, свяжитесь с поддержкой для получения помощи." -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "Искаженные данные журнала аудита" diff --git a/locale/ru/translations.json b/locale/ru/translations.json index f13665962798f..9faefb1414650 100644 --- a/locale/ru/translations.json +++ b/locale/ru/translations.json @@ -48,7 +48,7 @@ "A Topic Move already in progress.": "Перемещение темы уже в процессе.", "A deactivated bot cannot send messages, access data, or take any other action.": "Отключенный бот не может посылать сообщения, не имеет доступ к данным и не может совершать какое-либо другое действие.", "A deactivated emoji will remain visible in existing messages and emoji reactions, but cannot be used on new messages.": "Отключенные эмодзи останутся видимыми в существующих сообщениях и реакциях эмодзи, но их нельзя использовать в новых сообщениях.", - "A language is marked as 100% translated only if every string in the web, desktop, and mobile apps is translated, including administrative UI and error messages.": "Язык отмечается переведенным на 100% только если все строки в web, настольном и мобильном приложениях переведены, включая интерфейс администратора и сообщения об ошибках.", + "A language is marked as 100% translated only if every string in the web, desktop, and mobile apps is translated, including administrative UI and error messages.": "Язык отмечается переведенным на 100%, только если все строки в web, настольном и мобильном приложениях переведены, включая интерфейс администратора и сообщения об ошибках.", "A poll must be an entire message.": "Опрос должен быть целым сообщением.", "A stream needs to have a name": "Канал должен иметь название", "A stream with this name already exists": "Канал с таким названием уже существует", @@ -191,7 +191,7 @@ "Automatic": "Автоматически", "Automatic (follows system settings)": "Автоматически (как настроено в системе)", "Automatically follow topics": "Автоматически отслеживать темы", - "Automatically follow topics where I'm mentioned": "", + "Automatically follow topics where I'm mentioned": "Автоматически отслеживать темы, упоминающие меня", "Automatically mark messages as read": "Автоматически отмечать сообщения как прочитанные", "Automatically unmute topics in muted streams": "Автоматически включать темы в заглушенных каналах", "Available on Zulip Cloud Standard. Upgrade or request sponsorship to access.": "Доступно в Zulip Cloud Standard. Выполните апгрейд или запросите спонсорство, чтобы получить доступ.", @@ -386,7 +386,7 @@ "Deprecation notice": "Уведомление об удалении", "Description": "Описание", "Deselect draft": "Снять выделение с черновика", - "Desktop": "Рабочий стол", + "Desktop": "Компьютер", "Desktop & mobile apps": "Приложения для компьютера и смартфонов", "Desktop message notifications": "Всплывающие оповещения", "Detailed keyboard shortcuts documentation": "Подробная документация по горячим клавишам", @@ -500,7 +500,7 @@ "Expand direct messages": "Развернуть личные сообщения", "Expand message": "Развернуть сообщение", "Expand views": "Развернуть виды", - "Expires at": "Истекает в", + "Expires at": "Истекает", "Expires on {date} at {time}": "Истекает {date} в {time}", "Export failed": "Ошибка экспорта", "Export organization": "Выгрузка организации", @@ -647,8 +647,8 @@ "Join video call.": "Присоединиться к видеовызову.", "Join voice call.": "Присоединиться к звонку.", "Join {realm_name}": "Вступить в {realm_name}", - "Joined": "Дата присоединения", - "Joined {date_joined}": "Присоединился {date_joined}", + "Joined": "Присоединение", + "Joined {date_joined}": "Присоединение {date_joined}", "Joining the organization": "Присоединение к организации", "July": "Июль", "June": "Июнь", @@ -951,7 +951,7 @@ "Pin stream to top": "Закрепить канал сверху", "Pin stream to top of left sidebar": "Закрепить наверху в списке каналов", "Pinned": "Закреплены", - "Plan management": "", + "Plan management": "Управление тарифом", "Plans and pricing": "Тарифы и цены", "Play sound": "Воспроизвести звук", "Please contact support for an exception or add users with a reusable invite link.": "Пожалуйста, в качестве исключениясвяжитесь с поддержкой или добавьте пользователей с помощью многократно используемой ссылки приглашения.", @@ -1292,7 +1292,7 @@ "Unsubscribe from ": "Отписаться от ", "Unsubscribe {full_name} from ?": "Отписать {full_name} от ?", "Unsubscribed successfully!": "Отмена подписки успешна!", - "Up to {time_limit} after posting": "Не более, чем {time_limit} после публикации", + "Up to {time_limit} after posting": "Не более чем {time_limit} после публикации", "Update successful: Subdomains allowed for {domain}": "Обновлено успешно: поддомены разрешены для {domain}", "Update successful: Subdomains no longer allowed for {domain}": "Обновлено успешно: поддомены больше не разрешены для {domain}", "Upgrade to the latest release": "Обновление до последней версии", @@ -1392,7 +1392,7 @@ "Who can send email invitations to new users": "Кто может отправлять приглашения по почте новым пользователям", "Who can unsubscribe others from this stream?": "Кто может отписать других от данного канала?", "Who can use direct messages": "Кто может использовать личные сообщения", - "Who can view all other users in the organization": "", + "Who can view all other users in the organization": "Кто может видеть всех пользователей организации", "Why not start a conversation with yourself?": "Почему бы не начать общение с собой?", "Why not start the conversation?": "Почему бы не начать общение?", "Word": "Слово", diff --git a/locale/si/LC_MESSAGES/django.po b/locale/si/LC_MESSAGES/django.po index bd93c7a8bdbe4..1bcd482537f78 100644 --- a/locale/si/LC_MESSAGES/django.po +++ b/locale/si/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: HelaBasa Group , 2022\n" "Language-Team: Sinhala (http://app.transifex.com/zulip/zulip/language/si/)\n" @@ -64,7 +64,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "විශ්ලේෂණ දත්ත නැත. ඔබගේ සේවාදායක පරිපාලක අමතන්න." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -135,124 +135,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} {last4} න් අවසන් වේ" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "යම් වැරැද්දක් සිදුවී ඇත. පිටුව නැවත පූරණය කරන්න." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "යම් වැරැද්දක් සිදු වී ඇත. තත්පර කිහිපයක් රැඳී සිට යළි උත්සාහ කරන්න." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "සැලසුම යාවත්කාල කිරීමට නොහැකිය. සැලසුම කල් ඉකුත් වී නව සැලසුමක් සමඟ ප්‍රතිස්ථාපනය කර ඇත." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "සැලසුම යාවත්කාල කිරීමට නොහැකිය. සැලසුම අවසන් වී ඇත." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "බලපත්‍ර අතින් යාවත්කාල කිරීමට නොහැකිය. ඔබගේ සැලසුම ස්වයංක්‍රීය බලපත්‍ර කළමනාකරණය යටතේ ය." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "වත්මන් ලදුපත් කාල සීමාව තුළ ඔබගේ සැලසුම දැනටමත් බලපත්‍ර {licenses} යටතේ ඇත." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "වත්මන් ලදුපත් කාල සීමාව තුළ ඔබට බලපත්‍ර අඩු කළ නොහැකිය." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "ඔබගේ සැලසුම දැනටමත් {licenses_at_next_renewal} බලපත්‍ර සමඟ අලුත් කිරීමට නියමිතය." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "සංශෝධනයට කිසිත් නැත." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "මෙම සංවිධානයට පාරිභෝගිකයින් නැත!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "ලදුපත් පරිපාලකයෙකු හෝ සංවිධාන හිමිකරුවෙකු විය යුතුය" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -260,33 +260,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -313,7 +317,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "අභ්යන්තර සේවාදායකයේ දෝෂයකි" @@ -562,31 +566,31 @@ msgstr "සබැඳිය ඔබගේ අතිරික්සුවට නි msgid "Billing" msgstr "ලදුපත්" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "අවලංගු" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -685,6 +689,10 @@ msgstr "අධ්යාපනික මිලකරණය" msgid "View pricing" msgstr "මිලකරණය දකින්න" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -826,8 +834,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2249,7 +2255,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2258,7 +2263,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2266,7 +2271,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2282,7 +2286,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2299,15 +2302,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3352,65 +3364,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "පණිවිඩ(ය) වලංගු නොවේ" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "පණිවිඩය ගෙනහැර දැක්වීමට නොහැකිය" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "ලබන්නන් සඳහා වලංගු නොවන දත්ත වර්ගයකි" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "ලබන්නන්ගේ ලැයිස්තු වල වි-තැපැල් හෝ පරිශීලක හැඳු. අඩංගු විය හැකිය නමුත් දෙකම නොවේ." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3427,28 +3439,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3725,7 +3737,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "පණිවිඩයට ලබන්නන් සිටිය යුතුය!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3904,7 +3916,7 @@ msgid "API usage exceeded rate limit" msgstr "යෙ.ක්‍ර.මු. භාවිතය අනුපාත සීමාව ඉක්මවා ඇත" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4359,7 +4371,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4412,7 +4424,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4459,7 +4471,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4572,7 +4584,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} දිනයක් නොවේ" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4610,7 +4622,7 @@ msgid "{var_name} is too large" msgstr "{var_name} ඉතා විශාලයි" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} ලැයිස්තුවක් නොවේ" @@ -4774,7 +4786,7 @@ msgstr "ස්වයංක්‍රමලේඛ වර්ගය වලංගු msgid "Invalid interface type" msgstr "අතුරු මුහුණතෙහි වර්ගය වලංගු නොවේ" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4837,46 +4849,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable}! = {expected_value} ({value} වැරදියි)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} යනු ඒ.ස.නි. නොවේ" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' හිස් විය නොහැකිය." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5065,57 +5077,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "පළ කළ හැකි වන්නේ පූර්ණ සාමාජිකයින්ට පමණි" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "යුනිකේත ඉමොජි" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "අභිරුචි ඉමොජි" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "සුලිප් අමතර ඉමොජි" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "විකල්ප ලැයිස්තුව" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "සබැඳිය" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "බාහිර ගිණුම" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5131,20 +5143,20 @@ msgstr "නොදන්නා මෙහෙයුම් පද්ධතියක msgid "An unknown browser" msgstr "නොදන්නා අතිරික්සුවකි" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5316,19 +5328,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5394,26 +5406,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5891,11 +5903,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5909,43 +5921,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/sr/LC_MESSAGES/django.po b/locale/sr/LC_MESSAGES/django.po index 41a4d08d90fc7..50224247c0cfb 100644 --- a/locale/sr/LC_MESSAGES/django.po +++ b/locale/sr/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Iorek Byrnison , 2023\n" "Language-Team: Serbian (http://app.transifex.com/zulip/zulip/language/sr/)\n" @@ -64,7 +64,7 @@ msgstr "Време почетка је касније од времена зав msgid "No analytics data available. Please contact your server administrator." msgstr "Нема доступних података за аналитику. Молим вас контактирајте вашег администратора сервера." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -135,124 +135,124 @@ msgstr "Регистрација је деактивирана" msgid "Invalid remote server." msgstr "Неисправан удаљени сервер." -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "Морате купити лиценце за све активне кориснике у вашој организацији (минимално {min_licenses})." -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "Фактуре са више од {max_licenses} лиценци не могу бити обрађене путем ове странице. Да би сте завршили надоградњу, молим вас контактирајте {email}." -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "Није наведен начин плаћања у датотеци." -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} се завршава за {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Непознат начин плаћања. Молим вас контактирајте {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Десило се нешто непредвиђено. Молим вас контактирајте {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Десило се нешто непредвиђено. Молим вас учитајте поново страницу." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Десило се нешто непредвиђено. Молим вас сачекајте неколико секунди па покушајте поново." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "Молимо додајте кредитну картицу пре почетка бесплатног пробног периода." -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." -msgstr "" +msgstr "Молим вас додајте кредитну картицу да би сте заказали надоградњу." -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Није могуће освежити план. План је истекао и замењен је новим планом." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Није могуће освежити план. План је завршен." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Није могуће ручно освежавање лиценце. Ваш план је на аутоматском управљању лиценце." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Ваш план већ има {licenses} лиценци у тренутном обрачунском периоду." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Не можете да смањите лиценце у тренутном обрачунском периоду." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "За ваш план је већ организована обнова са {licenses_at_next_renewal} лиценци." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Ништа за измену." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Нема купца за ову организацију!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Сесија није пронађена" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Морате бити администратор за плаћање или власник организације" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Сврха плаћања није пронађена" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "Проследите stripe_session_id или stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -260,33 +260,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "Захтев ваше организације за спонзорисаним хостингом је одобрен! Унапређени сте на {plan_name}, потпуно бесплатно. {emoji}\n\nАко би сте могли да {begin_link}наведете Зулип као спонзора на вашем веб сајту{end_link}, били би смо вам много захвални!" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "Истекао је приступни токен за наплату." -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "Неисправан приступни токен за наплату." -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." -msgstr "" +msgstr "Кориснички налог још увек не постоји." -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." -msgstr "" +msgstr "Морате прихватити Услове сервиса да би сте наставили." -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." -msgstr "" +msgstr "Ова zulip_org_id није регистрована са Зулиповим системом за управљањем наплате." -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." -msgstr "" +msgstr "Неисправан zulip_org_key за ову zulip_org_id." -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "Регистрација вашег сервера је деактивирана." @@ -313,7 +317,7 @@ msgid "" msgstr "\n Ако ова грешка није очекивана, можете\n контактирати подршку.\n " #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Интерна грешка сервера" @@ -562,31 +566,31 @@ msgstr "Проверите да сте исправно ископирали в msgid "Billing" msgstr "Рачун" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Затвори прозор" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Откажи" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Потврди" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "Заборави" @@ -685,6 +689,10 @@ msgstr "Цена за образовне институције" msgid "View pricing" msgstr "Погледајте ценовник" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Зулип за пословне кориснике" @@ -826,8 +834,6 @@ msgstr "Већ имате налог?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2246,29 +2252,27 @@ msgid "" "\n" " Click the button below to log in.\n" " " -msgstr "" +msgstr "\n Кликните на дугме испод да се пријавите.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" " This link will expire in %(validity_in_hours)s hours.\n" " " -msgstr "" +msgstr "\n Ова веза ће истећи за %(validity_in_hours)s сати.\n " #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Питања? Сазнајте више или контактирајте %(billing_contact_email)s." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" -msgstr "" +msgstr "Пријавите се у Зулипово управљање планом" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:1 #, python-format @@ -2279,13 +2283,12 @@ msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 msgid "Click the link below to log in." -msgstr "" +msgstr "Кликните на дугме испод да се пријавите." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." -msgstr "" +msgstr "Ова веза ће истећи за %(validity_in_hours)s сати." #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:8 #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:6 @@ -2293,21 +2296,30 @@ msgstr "" msgid "" "Questions? Learn more at %(billing_help_link)s or contact " "%(billing_contact_email)s." -msgstr "" +msgstr "Питања? Сазнајте више на %(billing_help_link)s или контактирајте %(billing_contact_email)s." #: templates/zerver/emails/remote_realm_billing_confirm_login.html:9 #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2315,14 +2327,14 @@ msgstr "" msgid "" "Your request for Zulip sponsorship has been approved! Your organization has " "been upgraded to the Zulip Community plan." -msgstr "" +msgstr "Ваш захтев за Зулип спонзорством је одобрен! Ваша организација је надограђена на Зулип Заједница план." #: templates/zerver/emails/sponsorship_approved_community_plan.html:12 #, python-format msgid "" "If you could list Zulip as a sponsor on your " "website, we would really appreciate it!" -msgstr "" +msgstr "Ако би сте могли наведите Зулип као спонзора на вашем веб сајту, то би нам заиста значило!" #: templates/zerver/emails/sponsorship_approved_community_plan.subject.txt:1 #, python-format @@ -2333,13 +2345,13 @@ msgstr "" msgid "" "Your request for Zulip sponsorship has been approved! Your organization has " "been upgraded to the Zulip Community plan." -msgstr "" +msgstr "Ваш захтев за Зулип спонзорством је одобрен! Ваша организација је надограђена на Зулип Заједница план." #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " "appreciate it!." -msgstr "" +msgstr "Ако би сте могли наведите Зулип као спонзора на вашем веб сајту, то би нам заиста значило!" #: templates/zerver/find_account.html:4 msgid "Find your accounts" @@ -3352,65 +3364,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Неисправна порука(е)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Није могуће приказати поруку" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Очекиван је тачно један ток" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Неисправан тип података за ток" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Неисправан тип података за примаоце" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Списак прималаца може да садржи адресе е-поште или ИД корисника, али не оба." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Ваш бот {bot_identity} је покушао да пошаље поруку у ток са ИД {stream_id}, али нема тока са тим ИД-ом." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Ваш бот {bot_identity} је покушао да пошаље поруку у ток {stream_name}, али тај ток не постоји. Кликните [овде]({new_stream_link}) да би сте га направили." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Ваш бот {bot_identity} је покушао да пошаље поруку у ток {stream_name}. Ток постоји али нема ниједног претплатника." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "Директне поруке су онемогућене у овој организацији." -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Теме су обавезне у овој организацији" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Виџети: {error_msg}" @@ -3427,28 +3439,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "Заказана порука је већ послата" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "Време заказане доставе мора бити у будућности." -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "Порука није могла бити послата у заказано време." -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "Порука коју сте заказали за {delivery_datetime} није послата због следеће грешке:" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3725,7 +3737,7 @@ msgstr "Појавила се грешка при брисању прилога. msgid "Message must have recipients!" msgstr "Порука мора имати примаоце!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3904,7 +3916,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Лоше формиран JSON" @@ -4359,7 +4371,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Токен не постоји" @@ -4412,7 +4424,7 @@ msgstr "Списак прималаца може садржати само ИД #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Корисник није овлашћен за овај упит" @@ -4459,7 +4471,7 @@ msgstr "Аргумент \"{name}\" није исправан JSON." msgid "Scheduled message does not exist" msgstr "Заказана порука не постоји" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4572,7 +4584,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} није датум" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4610,7 +4622,7 @@ msgid "{var_name} is too large" msgstr "{var_name} је превелико" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} није листа" @@ -4774,7 +4786,7 @@ msgstr "Неисправан тип бота" msgid "Invalid interface type" msgstr "Неисправан тип интерфејса" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4837,46 +4849,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} је погрешно)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} није адреса" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "Шаблон адресе мора садржати '%(username)s'." -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' не може бити празно." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "Поље не сме садржати дуплиране изборе." -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' није исправан избор за '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} није текстуална вредност или листа целобројних вредности" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} није текстуална вредност или листа целобројних вредности" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} не садржи дужину" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} недостаје" @@ -5065,57 +5077,57 @@ msgstr "Само администратори и модератори орган msgid "Only organization full members can post" msgstr "Само пуни чланови организације могу да пишу" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Уникод емотикон" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Прилагођени емотикон" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Зулип додатни емотикон" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Листа опција" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Бирач особе" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Кратки текст" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Дуги текст" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Бирач датума" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Веза" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Екстерни налог" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Заменице" @@ -5131,20 +5143,20 @@ msgstr "непознати оперативни систем" msgid "An unknown browser" msgstr "Непознати прегледач" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Недостаје 'queue_id' аргумент" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Недостаје 'last_event_id' аргумент" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5316,19 +5328,19 @@ msgstr "Сидро може бити изузето само на крају о msgid "No such topic '{topic}'" msgstr "Нема такве теме '{topic}'" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Недостаје пошиљалац" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5394,26 +5406,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "Потврде читања су онемогућене у овој организацији." -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "Неисправан језик '{language}'" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Мора бити омогућен барем један начин аутентификације." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Мора бити демонстративна организација." @@ -5891,11 +5903,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5909,43 +5921,43 @@ msgstr "Неисправан UUID" msgid "Invalid token type" msgstr "Неисправна врста токена" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Подаци нису правилно сложени." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/sr/translations.json b/locale/sr/translations.json index 2e54594133410..4a6c2efe3c876 100644 --- a/locale/sr/translations.json +++ b/locale/sr/translations.json @@ -49,7 +49,7 @@ "A deactivated bot cannot send messages, access data, or take any other action.": "Деактивирани бот не може да шаље поруке, приступа подацима или извршава било коју другу акцију.", "A deactivated emoji will remain visible in existing messages and emoji reactions, but cannot be used on new messages.": "Деактивирани емотикон ће остати видљив у постојећим порукама и реаговањима емотиконом, али не може бити коришћен у новим порукама.", "A language is marked as 100% translated only if every string in the web, desktop, and mobile apps is translated, including administrative UI and error messages.": "Језик је означен као 100% преведен једино када је сваки текст на вебу, рачунару и мобилној апликацији преведен, укључујући административни интерфејс и поруке о грешкама.", - "A poll must be an entire message.": "", + "A poll must be an entire message.": "Анкета мора бити цела порука.", "A stream needs to have a name": "Ток мора да има назив", "A stream with this name already exists": "Ток са овим именом већ постоји", "A user group needs to have a name": "Корисничка група мора да има назив", @@ -98,7 +98,7 @@ "Add members. Use usergroup or #streamname to bulk add members.": "Додајте чланове. Користите групе корисника или #називтока да масовно додате чланове.", "Add one or more users": "Додајте једног или више корисника", "Add option": "Додај опцију", - "Add poll": "", + "Add poll": "Додај анкету", "Add question": "Додајте питање", "Add stream": "Додајте ток", "Add streams": "Додајте токове", @@ -136,8 +136,8 @@ "All streams": "Сви токови", "All time": "Све време", "All topics": "Све теме", - "All unmuted topics": "", - "All unread messages": "", + "All unmuted topics": "Све озвучене теме", + "All unread messages": "Све непрочитане поруке", "All users will need to log in again at your new organization URL.": "Сви корисници морају да се поново пријаве путем нове адресе ваше организације.", "Allow creating web-public streams (visible to anyone on the Internet)": "Дозволи креирање токова доступних на вебу (видљиви свима на интернету)", "Allow message content in message notification emails": "Дозволи садржај поруке у е-порукама обавештења", @@ -158,7 +158,7 @@ "Announce new stream in": "Објавите вест о новом току у", "Any organization administrator can conduct an export.": "Сваки администратор организације може да изврши извоз.", "Any time": "Било када", - "Anyone can add more options after the poll is posted.": "", + "Anyone can add more options after the poll is posted.": "Свако може да дода опцију након што је анкета направљена.", "April": "Април", "Archive ?": "Архивирај ?", "Archive stream": "Архивирај ток", @@ -177,7 +177,7 @@ "Are you sure you want to revoke the invitation to {email}?": "Да ли заиста желите да повучете позивницу за {email}?", "Are you sure you want to revoke this invitation link created by {referred_by}?": "Да ли заиста желите да повучете ову позивницу креирану од стране {referred_by}?", "Are you sure you want to revoke this invitation link?": "Да ли заиста желите да опозовете везу позива?", - "Are you sure you want to send @-mention notifications to the {subscriber_count} users subscribed to #{stream_name}? If not, please edit your message to remove the @{stream_wildcard_mention} mention.": "", + "Are you sure you want to send @-mention notifications to the {subscriber_count} users subscribed to #{stream_name}? If not, please edit your message to remove the @{stream_wildcard_mention} mention.": "Да ли сте сигурни да желите да пошаљете @-помињање обавештење ка {subscriber_count} корисника који су претплаћени на #{stream_name}? Ако не желите, измените вашу поруку да би сте уклонили @{stream_wildcard_mention} помињање.", "Are you sure you want to unstar all messages in ? This action cannot be undone.": "Да ли заиста желите да уклоните звездицу са свих порука у ? Ова акција се не може поништити.", "Are you sure you want to unstar all starred messages? This action cannot be undone.": "Да ли заиста желите да уклоните звездицу са свих порука? Ова акција се не може поништити.", "Ask me later": "Питај ме касније", @@ -191,7 +191,7 @@ "Automatic": "Аутоматски", "Automatic (follows system settings)": "Аутоматски (следи подешавања система)", "Automatically follow topics": "Аутоматски прати теме", - "Automatically follow topics where I'm mentioned": "", + "Automatically follow topics where I'm mentioned": "Аутоматски прати теме где сам поменут", "Automatically mark messages as read": "Аутоматски означи поруке као прочитане", "Automatically unmute topics in muted streams": "Аутоматски озвучи теме у утишаним токовима", "Available on Zulip Cloud Standard. Upgrade or request sponsorship to access.": "Доступно са Зулип стандард. Надоградите или затражите спонзорство за приступ.", @@ -222,9 +222,9 @@ "Cancel": "Откажи", "Cancel compose": "Откажи састављање", "Cancel compose and save draft": "Поништи састављање и сачувај нацрт", - "Cannot join group {name}": "", - "Cannot leave group {name}": "", - "Cannot send message while files are being uploaded.": "", + "Cannot join group {name}": "Није могуће приступити групи {name}", + "Cannot leave group {name}": "Није могуће напустити групу {name}", + "Cannot send message while files are being uploaded.": "Није могуће послати поруку док се постављају датотеке.", "Cannot subscribe to ": "Није могућа претплата на ", "Cannot subscribe to private stream ": "Није могуће претплатити се на приватан ток ", "Cannot view stream": "Није могуће прегледати ток", @@ -299,9 +299,9 @@ "Could not resolve topic": "Није могуће решити тему", "Could not unresolve topic": "Није могуће поништити решење теме", "Create": "Направи", - "Create a poll": "", + "Create a poll": "Направи анкету", "Create a stream": "Направи ток", - "Create a user group": "", + "Create a user group": "Направи корисничку групу", "Create new stream": "Направи нови ток", "Create new user group": "Направи нову групу корисника", "Create stream": "Направи ток", @@ -322,9 +322,9 @@ "Cycle between stream narrows": "Мењај између сужених токова", "DIRECT MESSAGES": "ДИРЕКТНЕ ПОРУКЕ", "DM": "Директна порука", - "DMs and mentions": "", + "DMs and mentions": "ДП и помињања", "DMs, mentions, and alerts": "ДП, помињања и упозорења", - "DMs, mentions, and followed topics": "", + "DMs, mentions, and followed topics": "ДП, помињања и праћене теме", "Dark": "Тамна", "Dark theme": "Тамна тема", "Dark theme logo": "Лого тамне теме", @@ -576,13 +576,13 @@ "Go to conversation": "Иди на разговор", "Go to direct messages with {display_reply_to_for_tooltip}": "Иди на директне поруке са {display_reply_to_for_tooltip}", "Go to home view": "Иди на почетни приказ", - "Go to stream settings": "", + "Go to stream settings": "Иди на подешавања тока", "Got it": "Разумео сам", "Got it!": "Разумео сам!", "Government": "Државна установа", "Grant Zulip the Kerberos tickets needed to run your Zephyr mirror via Webathena": "Дозволите Зулипу Керберос тикете који су потребни да би се покренула ваша Zephyr копија путем Webathena-е", - "Group permissions": "", - "Group settings": "", + "Group permissions": "Дозволе групе", + "Group settings": "Подешавања групе", "Guest": "Гост", "Guests": "Гости", "Guests cannot edit custom emoji.": "Гости не могу да уређују прилагођене емотиконе.", @@ -613,7 +613,7 @@ "Include content of direct messages in desktop notifications": "Укључи садржај директних порука у обавештењима на рачунару", "Include message content in message notification emails": "Прикажи садржај поруке у е-порукама обавештења", "Include organization name in subject of message notification emails": "Прикажи назив организације у наслову е-порука обавештења", - "Includes muted streams and topics": "", + "Includes muted streams and topics": "Укључујући утишане токове и теме", "Initiate a search": "Покрени претрагу", "Insert new line": "Уметни нову линију", "Integration": "Интеграција", @@ -642,8 +642,8 @@ "Italic": "Искошено", "January": "Јануар", "Jitsi server URL": "УРЛ Jitsi сервера", - "Join group": "", - "Join group {name}": "", + "Join group": "Приступи групи", + "Join group {name}": "Приступи групи {name}", "Join video call.": "Придружи се видео позиву.", "Join voice call.": "Придружи се гласовном позиву.", "Join {realm_name}": "Приступи {realm_name}", @@ -654,7 +654,7 @@ "June": "Јун", "Just now": "Управо сада", "Keyboard shortcuts": "Пречице на тастатури", - "LaTeX": "", + "LaTeX": "LaTeX", "Label": "Ознака", "Language": "Језик", "Language for automated messages and invitation emails": "Језик за аутоматизоване поруке и е-поруке позивница", @@ -671,8 +671,8 @@ "Learn more": "Сазнај више", "Learn more about mentions here.": "Сазнајте више о помињањима овде.", "Learn more about starring messages here.": "Сазнајте више о означавању порука са звездицом овде.", - "Leave group": "", - "Leave group {name}": "", + "Leave group": "Напусти групу", + "Leave group {name}": "Напусти групу {name}", "Leave {group_name}": "Напусти {group_name}", "Let others see when I've read messages": "Нека остали виде када сам прочитао поруке", "Let recipients see when I'm typing direct messages": "Нека примаоци виде када куцкам директне поруке", @@ -729,7 +729,7 @@ "Message edit history": "Историја измене порука", "Message editing": "Измена порука", "Message formatting": "Форматирање поруке", - "Message length shouldn't be greater than 10000 characters.": "", + "Message length shouldn't be greater than 10000 characters.": "Порука не би требало да буде дужа од 10000 карактера.", "Message length shouldn't be greater than {max_length} characters.": "Дужина поруке не би требала бити дужа од {max_length} карактера.", "Message retention": "Чување порука", "Message retention period": "Период чувања порука", @@ -744,8 +744,8 @@ "Mobile": "Телефон", "Mobile message notifications": "Обавештења поруком на телефону", "Mobile notifications": "Обавештења на телефону", - "Mobile push notifications are not enabled on this server.": "", - "Mobile push notifications are not enabled on this server. Learn more": "", + "Mobile push notifications are not enabled on this server.": "Обавештења на телефону нису омогућена на овом серверу.", + "Mobile push notifications are not enabled on this server. Learn more": "Обавештења на телефону нису омогућена на овом серверу. Сазнај више", "Moderator": "Модератор", "Moderators": "Модератори", "Monday": "Понедељак", @@ -834,7 +834,7 @@ "No drafts selected": "Ниједан нацрт није изабран", "No drafts.": "Нема нацрта.", "No exports found.": "Ниједан извоз није пронађен.", - "No group members match your current filter.": "", + "No group members match your current filter.": "Ниједан члан групе се не поклапа са вашим тренутним филтером.", "No invites found.": "Ниједна позивница није пронађена.", "No invites match your current filter.": "Ни једна позивница се не поклапа са тренутним филтером.", "No language set": "Није изабран језик", @@ -851,7 +851,7 @@ "No scheduled messages.": "Нема заказаних порука.", "No search results.": "Нема резултата претраге.", "No status text": "Нема текста статуса", - "No stream subscribers match your current filter.": "", + "No stream subscribers match your current filter.": "Ниједан претплатник на ток се не поклапа са вашим тренутним филтером.", "No stream subscriptions.": "Нема чланства у токовима.", "No streams": "Нема токова", "No topics are marked as resolved.": "Ниједна тема није означена као решена.", @@ -900,7 +900,7 @@ "Only stream members can add users to a private stream.": "Само чланови тока могу додавати кориснике у приватне токове.", "Only subscribers can access or join private streams, so you will lose access to this stream if you convert it to a private stream while not subscribed to it.": "Само претплатници могу да приступе или придруже се приватним токовима, тако да ћете приступ овом току ако га претворите у приватни ток док нисте пријављени на њега.", "Only subscribers to this stream can edit stream permissions.": "Само чланови овог тока могу да уређују дозволе тока.", - "Only topics you follow": "", + "Only topics you follow": "Само теме које пратите", "Open": "Отвори", "Open help menu": "Отвори мени помоћи", "Open message menu": "Отвори мени поруке", @@ -909,7 +909,7 @@ "Open-source project": "Пројекат отвореног кода", "Option already present.": "Опција је већ присутна.", "Optional": "Опционо", - "Options": "", + "Options": "Опције", "Organization": "Организација", "Organization URL": "УРЛ организације", "Organization administrators": "Администратор организације", @@ -951,13 +951,13 @@ "Pin stream to top": "Закачи ток на врх", "Pin stream to top of left sidebar": "Закачи ток на врх траке са леве стране", "Pinned": "Закачено", - "Plan management": "", + "Plan management": "Управљање планом", "Plans and pricing": "Планови и ценовник", "Play sound": "Репродукуј звук", "Please contact support for an exception or add users with a reusable invite link.": "МОлим ваас контактирајте подршку за изузеће или додајте кориснике са везом вишекратне позивнице.", "Please ask a billing administrator to increase the number of licenses or deactivate inactive users, and try again.": "Молим вас контактирајте администратора који је задужен за плаћање да повећа број лиценци или да деактивира неактивне кориснике, па покушајте поново.", "Please choose a new password": "Молим вас упишите нову лозинку", - "Please enter a question.": "", + "Please enter a question.": "Молим вас унесите питање.", "Please enter your password": "Молим вас унесите вашу лозинку", "Please just upload one file.": "Молим вас поставите само једну датотеку.", "Please only use characters that are valid in an email address": "Молим вас користите само карактере који су исправни у адресама е-поште", @@ -984,7 +984,7 @@ "Pronouns": "Заменице", "Public": "Јавно", "Question": "Питање", - "Quote": "", + "Quote": "Цитирај", "Quote and reply": "Цитирај и одговори", "Quote and reply to message": "Цитирај и одговори на поруку", "Quoted original email (in replies)": "Цитирана оригинална е-порука (у одговорима)", @@ -1103,7 +1103,7 @@ "Show more": "Прикажи више", "Show password": "Прикажи лозинку", "Show previews of linked websites": "Прикажи преглед вебсајта у поруци са везом", - "Show previews of uploaded and linked images and videos": "", + "Show previews of uploaded and linked images and videos": "Прикажи преглед постављених и повезаних слика и видеа", "Show starred message count": "Прикажи број порука са звездицом", "Show status text": "Прикажи текст статуса", "Show unread counts for": "Прикажи број непрочитаних за", @@ -1122,7 +1122,7 @@ "Sort by unread message count": "Сложено према броју непрочитаних порука", "Spoiler": "Спојлер", "Sponsorship request pending": "Захтев за спонзорством је на чекању", - "Standard view": "", + "Standard view": "Стандардни приказ", "Star": "Означи звездицом", "Star selected message": "Означи звездицом изабране поруке", "Star this message": "Означи звездицом ову поруку", @@ -1143,7 +1143,7 @@ "Stream successfully created!": "Ток је успешно креиран!", "Streams": "Токови", "Streams they should join": "Токови којима они могу приступити", - "Strikethrough": "", + "Strikethrough": "Прецртано", "Subject": "Наслов", "Subscribe": "Учлани се", "Subscribe them": "Учлани их", @@ -1180,7 +1180,7 @@ "There are no messages here.": "Овде нема порука.", "There are no streams you can view in this organization.": "Нема токова у овој организацији које можете видети.", "There are no unread messages in your inbox.": "Нема непрочитаних порука у вашем пријемном сандучету.", - "There are no user groups you can view in this organization.": "", + "There are no user groups you can view in this organization.": "Нема корисничких група које можете видети у овој организацији.", "There is a default emoji with this name. Do you want to override it with a custom emoji? The name :{emoji_name}: will no longer work to access the default emoji.": "Постоји предефинисани емотикон са овим именом. Да ли желите да га преснимите са прилагођеним? Име :{emoji_name}: више неће радити за предефинисани емотикон.", "They administer the following bots:": "Они администрирају следеће ботове:", "This Zulip server is running an old version and should be upgraded.": "Овај Зулип сервер је покренут на старој верзији и требало би га надоградити.", @@ -1189,10 +1189,10 @@ "This bot cannot be deactivated.": "Бот не може бити деактивиран.", "This bot cannot be edited.": "Бот не може бити измењен.", "This bot has been deactivated.": "Овај бот је деактивиран.", - "This conversation may have additional messages not shown in this view.": "", + "This conversation may have additional messages not shown in this view.": "Овај разговор можда садржи додатне поруке које нису приказане у овом приказу.", "This demo organization will be automatically deleted in {days_remaining} days, unless it's converted into a permanent organization.": "Ова демонстративна организација ће аутоматски бити обрисана за {days_remaining} дана, осим ако се не конвертује у сталну организацију.", - "This feature is available on Zulip Cloud Plus. Upgrade to access.": "", - "This group has no members.": "", + "This feature is available on Zulip Cloud Plus. Upgrade to access.": "Ова функционалност је доступна за Зулип Облак Плус. Надоградите за приступ.", + "This group has no members.": "Ова група нема чланове.", "This is a demo organization and will be automatically deleted in {days_remaining} days, unless it's converted into a permanent organization.": "Ово је демонстративна организација и биће аутоматски обрисана за {days_remaining} дана, осим ако сене конвертује у сталну организацију.", "This is not a publicly accessible conversation.": "Ово није јавно доступан разговор.", "This is what a Zulip notification looks like.": "Овако изгледа Зулип обавештење.", @@ -1216,7 +1216,7 @@ "This stream does not exist or is private.": "Овај ток не постоји или је приватан.", "This stream does not yet have a description.": "Овај ток још увек нема опис.", "This stream has been deactivated": "Овај ток је деактивиран", - "This stream has no subscribers.": "", + "This stream has no subscribers.": "Овај ток нема претплатнике.", "This stream has {sub_count, plural, =0 {no subscribers} one {# subscriber} other {# subscribers}}.": "", "This user does not exist!": "Овај корисник не постоји!", "This user has been deactivated.": "Овај корисник је деактивиран.", @@ -1329,12 +1329,12 @@ "User role": "Улога корисника", "Users": "Корисници", "Users can always disable their personal read receipts.": "Корисници увек могу да онемогуће своје потврде читања.", - "Users join as": "", + "Users join as": "Корисници приступају као", "VIEWS": "ПРИКАЗИ", "Vacationing": "На годишњем одмору", "Version {zulip_version}": "Верзија {zulip_version}", "View all streams": "Прикажи све токове", - "View all user groups": "", + "View all user groups": "Прикажи све корисничке групе", "View direct messages": "Прикажи директне поруке", "View drafts": "Прикажи нацрте", "View edit and move history": "Прикажи историју измена и премештања", @@ -1384,15 +1384,15 @@ "Who can create reusable invitation links": "Ко може да направи вишекратне везе позива", "Who can create web-public streams": "Ко може да креира јавне токове на вебу", "Who can delete their own messages": "Ко може да брише своје поруке", - "Who can mention this group?": "", + "Who can mention this group?": "Ко може да помене ову групу?", "Who can move messages to another stream": "Ко може да премешта поруке у други ток", "Who can move messages to another topic": "Ко може да премешта поруке у другу тему", - "Who can notify a large number of users with a wildcard mention": "", + "Who can notify a large number of users with a wildcard mention": "Ко може да обавести велику групу корисника помињањем заменицом", "Who can post to the stream?": "Ко може да пише у ток?", "Who can send email invitations to new users": "Ко може да шаље позивнице е-поштом за нове кориснике", "Who can unsubscribe others from this stream?": "Ко може да прекине претплату осталих из овог тока?", "Who can use direct messages": "Ко може да користи директне поруке", - "Who can view all other users in the organization": "", + "Who can view all other users in the organization": "Ко може да види све остале кориснике у овој организацији", "Why not start a conversation with yourself?": "Зашто не би сте започели разговор за самим собом?", "Why not start the conversation?": "Зашто не би сте започели разговор?", "Word": "Реч", @@ -1412,7 +1412,7 @@ "You and ": "Ви и ", "You and {recipients}": "Ви и {recipients}", "You are about to disable all notifications for direct messages, @‑mentions and alerts, which may cause you to miss messages that require your timely attention. If you would like to temporarily disable all desktop notifications, consider turning on \"Do not disturb\" instead.": "Сада ћете онемогућити сва обавештења за директне поруке, @‑помињања и упозорења, што може проузроковати да вам промакну поруке које захтевају ваше правовремено реаговање. Ако би сте желели да привремено онемогућите сва обавештења на рачунару, размотрите укључивање \"Без ометања\" уместо тога.", - "You are not a member of any user groups.": "", + "You are not a member of any user groups.": "Нисте члан ниједне корисничке групе.", "You are not allowed to send direct messages in this organization.": "Није вам дозвољено да шаљете директне поруке у овој организацији.", "You are not currently subscribed to this stream.": "Тренутно нисте учлањени у овај ток.", "You are not subscribed to any streams.": "Нисте претплаћени ни на један ток.", @@ -1436,8 +1436,8 @@ "You cannot send messages to deactivated users.": "Не можете слати поруке деактивираним корисницима.", "You do not have permission to add custom emoji.": "Немате овлашћење да додајете прилагођене еметиконе.", "You do not have permission to add other users to streams in this organization.": "Немате овлашћење да додате друге кориснике у токове ове организације.", - "You do not have permission to join this group.": "", - "You do not have permission to leave this group.": "", + "You do not have permission to join this group.": "Немате овлашћења да приступите овој групи.", + "You do not have permission to leave this group.": "Немате овлашћења да напустите ову групу.", "You do not have permission to move some of the messages in this topic. Contact a moderator to move all messages.": "Немате овлашћење да преместите неке поруке у овој теми. Контактирајте модератора да би сте преместили све поруке.", "You do not have permission to post in this stream.": "Немате дозволу да пишете у овом току.", "You do not have permission to resolve topics with messages older than {N, plural, one {# day} other {# days}} in this organization.": "", @@ -1446,8 +1446,8 @@ "You do not have permission to unresolve topics with messages older than {N, plural, one {# day} other {# days}} in this organization.": "", "You do not have permission to unresolve topics with messages older than {N, plural, one {# hour} other {# hours}} in this organization.": "", "You do not have permission to unresolve topics with messages older than {N, plural, one {# minute} other {# minutes}} in this organization.": "", - "You do not have permission to use @topic mentions in this topic.": "", - "You do not have permission to use @{stream_wildcard_mention} mentions in this stream.": "", + "You do not have permission to use @topic mentions in this topic.": "Немате овлашћење да користите @тема помињања у овој теми.", + "You do not have permission to use @{stream_wildcard_mention} mentions in this stream.": "Немате овлашћење да користите @{stream_wildcard_mention} помињања у овом току.", "You do not have permissions to generate invite links in this organization.": "Немате овлашћења да генеришете везе позивница у овој организацији.", "You do not have permissions to send email invitations in this organization.": "Немате овлашћења да шаљете позивнице е-порукама у овој организацији.", "You don't have any direct message conversations yet.": "Још увек немате разговоре директним порукама.", @@ -1465,8 +1465,8 @@ "You have no direct messages with {person} yet.": "Још увек немате директне поруке са {person}.", "You have no direct messages yet!": "Још увек немате директних порука!", "You have no inactive bots.": "Немате неактивних ботова.", - "You have no more unread direct messages.": "", - "You have no more unread topics.": "", + "You have no more unread direct messages.": "Више немате непрочитаних директних порука.", + "You have no more unread topics.": "Више немате непрочитаних тема.", "You have no starred messages.": "Немате порука са звездицом.", "You have no unread messages in followed topics.": "Немате непрочитаних порука у праћеним темама.", "You have no unread messages!": "Немате непрочитаних порука!", @@ -1477,9 +1477,9 @@ "You have unmuted this topic": "Озвучили сте ову тему", "You haven't been mentioned yet!": "Још увек нисте поменути!", "You haven't received any messages sent by {person} yet.": "Још увек нисте примили ниједну поруку од {person}.", - "You may want to configure your organization's login page prior to inviting users.": "", - "You may want to upload a profile picture for your organization prior to inviting users.": "", - "You may want to configure default new user settings and custom profile fields prior to inviting users.": "", + "You may want to configure your organization's login page prior to inviting users.": "Можда би сте желели да подесите страницу за пријаву у вашу организацију пре позивања корисника.", + "You may want to upload a profile picture for your organization prior to inviting users.": "Можда би сте желели да поставите профилну слику ваше организације пре позивања корисника.", + "You may want to configure default new user settings and custom profile fields prior to inviting users.": "Можда би сте желели да подесите подразумевана подешавања нових корисника и прилагођена поља профила пре позивања корисника.", "You might be interested in recent conversations.": "Можда би вас занимали недавни разговори.", "You must configure your email to access this feature.": "Морате подесити адресу ваше е-поште да би сте приступили овоме.", "You must be an organization administrator to create a stream without subscribing.": "МОрате бити администратор организације да би сте направили ток без учлањивања.", @@ -1502,7 +1502,7 @@ "Your message was sent to a stream you have muted.": "Ваша порука је послата у ток који сте утишали.", "Your message was sent to a topic you have muted.": "Ваша порука је послата у тему коју сте утишали.", "Your password": "Ваша лозинка", - "Your question": "", + "Your question": "Ваше питање", "Your status": "Ваш статус", "Your time zone:": "Ваша временска зона", "Zoom to message in conversation context": "Пребаци се на поруку у контекст разговора", diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po index 03ea48428db68..978c20ba4cbe3 100644 --- a/locale/sv/LC_MESSAGES/django.po +++ b/locale/sv/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Sven Stark , 2020\n" "Language-Team: Swedish (http://app.transifex.com/zulip/zulip/language/sv/)\n" @@ -62,7 +62,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "Inga anlysdata tillgängliga. Var god kontakta serverns administratör." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -133,124 +133,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Något gick fel. Uppdatera sidan." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Något gick fel. Vänta några sekunder och försök igen." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -258,33 +258,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -311,7 +315,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -560,31 +564,31 @@ msgstr "" msgid "Billing" msgstr "Fakturering" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Avbryt" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -683,6 +687,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -824,8 +832,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2247,7 +2253,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2256,7 +2261,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2264,7 +2269,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2280,7 +2284,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2297,15 +2300,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3350,65 +3362,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3425,28 +3437,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3723,7 +3735,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3902,7 +3914,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4357,7 +4369,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4410,7 +4422,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4457,7 +4469,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4570,7 +4582,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4608,7 +4620,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4772,7 +4784,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4835,46 +4847,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5063,57 +5075,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "egna emoji" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5129,20 +5141,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5314,19 +5326,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5392,26 +5404,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5889,11 +5901,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5907,43 +5919,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/ta/LC_MESSAGES/django.po b/locale/ta/LC_MESSAGES/django.po index be2213cd12552..593f62dffdf95 100644 --- a/locale/ta/LC_MESSAGES/django.po +++ b/locale/ta/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:58+0000\n" +"POT-Creation-Date: 2023-12-15 17:44+0000\n" "PO-Revision-Date: 2018-04-11 21:06+0000\n" "Last-Translator: Tim Abbott \n" "Language-Team: Tamil (http://www.transifex.com/zulip/zulip/language/ta/)\n" @@ -69,7 +69,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 #, fuzzy @@ -141,126 +141,126 @@ msgstr "" msgid "Invalid remote server." msgstr "அறிவிப்புகளை இயக்கு" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this " "page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new " "plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 #, fuzzy #| msgid "Administrators" msgid "No customer for this organization!" msgstr "நிர்வாகிகள்" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2901 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You " @@ -270,33 +270,37 @@ msgid "" "we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:136 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:138 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:275 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:280 +#: corporate/views/remote_billing_page.py:693 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:506 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:513 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:517 msgid "Your server registration has been deactivated." msgstr "" @@ -324,7 +328,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -583,9 +587,9 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:313 templates/corporate/billing.html:339 +#: templates/corporate/billing.html:368 templates/corporate/billing.html:397 +#: templates/corporate/billing.html:423 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 #, fuzzy @@ -593,25 +597,25 @@ msgstr "" msgid "Close modal" msgstr "மூடு" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:324 templates/corporate/billing.html:353 +#: templates/corporate/billing.html:382 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "இரத்து" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:326 templates/corporate/billing.html:410 +#: templates/corporate/billing.html:433 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:355 templates/corporate/billing.html:384 #: templates/zerver/change_email_address_visibility_modal.html:28 #, fuzzy #| msgid "Confirm password" msgid "Confirm" msgstr "கடவுச்சொல்லை உறுதிப்படுத்துக" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:408 templates/corporate/billing.html:431 msgid "Never mind" msgstr "" @@ -710,6 +714,62 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:4 +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:16 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:4 +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:16 +msgid "Plan management not available" +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:18 +#, python-format +msgid "" +" Plan management is not available for this\n" +" organization, because your Zulip server is already " +"on a\n" +" %(server_plan_name)s plan, which covers all\n" +" organizations on this server. Follow the log\n" +" in instructions\n" +" for All older versions\n" +" of the Zulip server to manage your plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_realm_login_error_for_server_on_active_plan.html:29 +msgid "" +" To move the plan from the server to this\n" +" organization, or for other questions, contact support.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:18 +msgid "" +"\n" +" Plan management for this server is not available " +"because at least one organization\n" +" hosted on this server already has an active plan.\n" +" " +msgstr "" + +#: templates/corporate/remote_server_login_error_for_any_realm_on_active_plan.html:24 +#, python-format +msgid "" +"\n" +" Log in to plan management for your\n" +" organization instead, or contact support with any questions.\n" +" " +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -860,8 +920,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2339,7 +2397,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2348,7 +2405,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2356,7 +2413,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2372,7 +2428,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2389,16 +2444,27 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for " -"%(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +#, fuzzy +#| msgid "Confirm password" +msgid "Confirm and log in" +msgstr "கடவுச்சொல்லை உறுதிப்படுத்துக" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -2429,7 +2495,7 @@ msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.txt:4 msgid "" "If you could list Zulip as a sponsor on your website, we would really " -"appreciate it!." +"appreciate it!" msgstr "" #: templates/zerver/find_account.html:4 @@ -3490,69 +3556,69 @@ msgstr "அறிவிப்புகளை இயக்கு" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but " "that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The " "stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 #, fuzzy #| msgid "Administrators" msgid "Direct messages are disabled in this organization." msgstr "நிர்வாகிகள்" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 #, fuzzy #| msgid "Administrators" msgid "Topics are required in this organization" msgstr "நிர்வாகிகள்" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3569,27 +3635,27 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 -#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:128 +#: zerver/actions/scheduled_messages.py:155 +#: zerver/views/scheduled_messages.py:88 zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3872,7 +3938,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -4054,7 +4120,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4522,47 +4588,47 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "அறிவிப்புகளை இயக்கு" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:739 zilencer/views.py:241 msgid "Token does not exist" msgstr "" -#: zerver/lib/push_notifications.py:902 +#: zerver/lib/push_notifications.py:910 msgid "" "This organization has disabled including message content in mobile push " "notifications" msgstr "" -#: zerver/lib/push_notifications.py:1015 +#: zerver/lib/push_notifications.py:1023 #, python-brace-format msgid "{full_name} mentioned @{user_group_name}:" msgstr "" -#: zerver/lib/push_notifications.py:1019 +#: zerver/lib/push_notifications.py:1027 #, python-brace-format msgid "{full_name} mentioned you:" msgstr "" -#: zerver/lib/push_notifications.py:1026 +#: zerver/lib/push_notifications.py:1034 #, python-brace-format msgid "{full_name} mentioned everyone:" msgstr "" -#: zerver/lib/push_notifications.py:1436 +#: zerver/lib/push_notifications.py:1438 #, fuzzy #| msgid "Enable notifications" msgid "Test notification" msgstr "அறிவிப்புகளை இயக்கு" -#: zerver/lib/push_notifications.py:1437 +#: zerver/lib/push_notifications.py:1439 #, python-brace-format msgid "This is a test notification from {realm_name} ({realm_uri})." msgstr "" -#: zerver/lib/push_notifications.py:1488 +#: zerver/lib/push_notifications.py:1490 msgid "Device not recognized" msgstr "" -#: zerver/lib/push_notifications.py:1500 +#: zerver/lib/push_notifications.py:1502 msgid "Device not recognized by the push bouncer" msgstr "" @@ -4579,7 +4645,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4626,7 +4692,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4744,7 +4810,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4782,7 +4848,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -5015,46 +5081,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5263,15 +5329,15 @@ msgstr "நிர்வாகிகள்" msgid "Only organization full members can post" msgstr "நிர்வாகிகள்" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" @@ -5329,20 +5395,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5516,19 +5582,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5601,28 +5667,28 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "நிர்வாகிகள்" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, fuzzy, python-brace-format #| msgid "Enable notifications" msgid "Invalid language '{language}'" msgstr "அறிவிப்புகளை இயக்கு" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, fuzzy, python-brace-format #| msgid "Enable notifications" msgid "Invalid giphy_rating {giphy_rating}" msgstr "அறிவிப்புகளை இயக்கு" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 #, fuzzy #| msgid "Administrators" msgid "Must be a demo organization." @@ -6119,66 +6185,66 @@ msgid "" "({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" #. error -#: zilencer/views.py:77 zilencer/views.py:79 +#: zilencer/views.py:81 zilencer/views.py:83 #, fuzzy #| msgid "Enable notifications" msgid "Invalid UUID" msgstr "அறிவிப்புகளை இயக்கு" #. error -#: zilencer/views.py:84 +#: zilencer/views.py:88 msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:126 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:182 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:185 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:610 #, fuzzy, python-brace-format #| msgid "Enable notifications" msgid "Invalid property {property}" msgstr "அறிவிப்புகளை இயக்கு" -#: zilencer/views.py:605 +#: zilencer/views.py:613 #, fuzzy #| msgid "Enable notifications" msgid "Invalid event type." msgstr "அறிவிப்புகளை இயக்கு" -#: zilencer/views.py:612 +#: zilencer/views.py:620 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:696 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:957 msgid "" "Failed to migrate customer from server to realms. Please contact support for " "assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:1009 msgid "Malformed audit log data" msgstr "" @@ -6203,11 +6269,6 @@ msgstr "" #~ msgid "Invalid server_org_secret." #~ msgstr "அறிவிப்புகளை இயக்கு" -#, fuzzy -#~| msgid "Confirm password" -#~ msgid "Confirm login" -#~ msgstr "கடவுச்சொல்லை உறுதிப்படுத்துக" - #, fuzzy #~| msgid "Zulip" #~ msgid "Jira logo" diff --git a/locale/tl/LC_MESSAGES/django.po b/locale/tl/LC_MESSAGES/django.po index d387c48915b3c..d410ac9b2f8ab 100644 --- a/locale/tl/LC_MESSAGES/django.po +++ b/locale/tl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Gabriel Ferrer, 2022\n" "Language-Team: Tagalog (http://app.transifex.com/zulip/zulip/language/tl/)\n" @@ -62,7 +62,7 @@ msgstr "" msgid "No analytics data available. Please contact your server administrator." msgstr "" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -133,124 +133,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Hindi kilalang pamamaraan ng pambayad. Maaaring ipaalam {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "May nangyaring masama. Ipaalam {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "May nangyaring masama. Maaaring buksan muli ang pahina." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "May nangyaring masama. Maaaring maghintay ng ilang segundo at subukan muli." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Walang dapat baguhin" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Walang mamimili para sa kapisanan!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -258,33 +258,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -311,7 +315,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -560,31 +564,31 @@ msgstr "" msgid "Billing" msgstr "Paniningil" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -683,6 +687,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -824,8 +832,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2247,7 +2253,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2256,7 +2261,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2264,7 +2269,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2280,7 +2284,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2297,15 +2300,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3350,65 +3362,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "(Mga) Imbalidong Mensahe" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Hindi maipakita ang mensahe" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Inaaasahan ang eksaktong isang kapangkatan ng mga paksa" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Imbalidong tipo ng dato para sa Kapangkatan ng mga Paksa" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3425,28 +3437,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3723,7 +3735,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3902,7 +3914,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Maling anyo ng JSON" @@ -4357,7 +4369,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4410,7 +4422,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4457,7 +4469,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4570,7 +4582,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4608,7 +4620,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4772,7 +4784,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4835,46 +4847,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5063,57 +5075,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5129,20 +5141,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5314,19 +5326,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Nawawalang tagapagpadala" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5392,26 +5404,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5889,11 +5901,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5907,43 +5919,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/tr/LC_MESSAGES/django.po b/locale/tr/LC_MESSAGES/django.po index a5bb93cf9ae47..93a714e631319 100644 --- a/locale/tr/LC_MESSAGES/django.po +++ b/locale/tr/LC_MESSAGES/django.po @@ -18,7 +18,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: xe1st, 2023\n" "Language-Team: Turkish (http://app.transifex.com/zulip/zulip/language/tr/)\n" @@ -72,7 +72,7 @@ msgstr "Başlangıç zamanı bitiş zamanında sonra. Başlangıç: {start}, Bit msgid "No analytics data available. Please contact your server administrator." msgstr "Çözümleme verisi bulunmamaktadır. Lütfen sunucu yöneticinizle temasa geçiniz." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -143,124 +143,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} {last4} içinde sonra eriyor" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Bilinmeyen ödeme yöntemi. Lütfen {email} ile iletişime geçiniz" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Bazı şeyler yanlış gitti. Lütfen {email} ile iletişime geçiniz" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Birşeyler yanlış gitti. Lütfen sayfayı yeniden yükleyiniz." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Birşeyler yanlış gitti. Lütfen birkaç saniye sonra bekleyin ve yeniden deneyiniz." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "Plan yükseltilemedi. Plan tarihi geçmiş ve yeni bir plan ile değişmiştir." -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "Plan yükseltilemedi. Plan bitmiştir." -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "Lisanslar manuel olarak yükseltilemedi. Planınız otomatik lisans yönetimindedir." -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "Mevcut fatura döneminde planınız zaten {licenses} lisanslarında bulunuyor." -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "Mevcut fatura döneminde lisansları azaltamazsınız." -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "Planınızın {licenses_at_next_renewal} lisanslarıyla yenilenmesi zaten planlandı." -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "Değişecek bir şey yok." -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "Bu organizasyon için müşteri yok!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "Oturum bulunamadı" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Ödeme yöneticisi veya organizasyon sahibi olmak zorunda" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "Ödeme amacı bulunamadı" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -268,33 +268,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -321,7 +325,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Dahili sunucu hatası" @@ -570,31 +574,31 @@ msgstr "Bağlantıyı tarayıcınıza doğru şekilde kopyaladığınızdan emin msgid "Billing" msgstr "Faturalandırma" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "Modali Kapat" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "İptal" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Onayla" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -693,6 +697,10 @@ msgstr "Eğitim fiyatlandırması" msgid "View pricing" msgstr "Fiyatlandırmayı görüntüle" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "İş için Zulip" @@ -834,8 +842,6 @@ msgstr "Zaten bir hesabınız var mı?" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2257,7 +2263,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2266,7 +2271,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2274,7 +2279,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2290,7 +2294,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2307,15 +2310,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3360,65 +3372,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Geçersiz ileti(ler)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "İleti gönderime hazırlanamadı" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Sadece bir kanal bekleniyor" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Kanal için geçersiz veri tipi" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Alıcılar için geçersiz veri tipi" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Alıcılar listesi e-posta veya kullanıcı ID içerebilir ama ikisi birden olamaz." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "{bot_identity} adlı botunuz stream ID {stream_id} nolu kanala ileti göndermeye çalıştı, fakat bu numara ile bir kanal bulunmamakta." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "{bot_identity} adlı botunuz {stream_name} adlı kanala ileti göndermeye çalıştı, fakat böyle bir kanal bulunmamakta. Oluşturmak için [buraya]({new_stream_link}) tıklayınız." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Your bot {bot_identity} adlı botunuz {stream_name} adlı kanala ileti göndermeye çalıştı. Bu kanal mevcut fakat hiç abonesi bulunmamakta." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Eklentiler: API programıcısı yanlış JSON içeriği gönderdi" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgetlar: {error_msg}" @@ -3435,28 +3447,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3733,7 +3745,7 @@ msgstr "Ek silinirken bir hata oluştu. Lütfen daha sonra tekrar deneyiniz." msgid "Message must have recipients!" msgstr "İleti alıcı içermelidir!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3912,7 +3924,7 @@ msgid "API usage exceeded rate limit" msgstr "API kullanım sınırı aşıldı" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Hatalı biçimlendirilmiş JSON" @@ -4367,7 +4379,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Jeton (token) bulunamadı" @@ -4420,7 +4432,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Kullanıcı bu sorgu için yetkilendirilmemiş" @@ -4467,7 +4479,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4580,7 +4592,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} bir tarih değil" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} bir dict değil" @@ -4618,7 +4630,7 @@ msgid "{var_name} is too large" msgstr "{var_name} çok büyük" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} bir liste değil" @@ -4782,7 +4794,7 @@ msgstr "Geçersiz bot türü" msgid "Invalid interface type" msgstr "Geçersiz arayüz türü" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4845,46 +4857,46 @@ msgstr "{var_name} allowed_type değil" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} yanlış)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} bir URL değil" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' boş geçilemez." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}', '{field_name}' için uygun bir seçenek değil." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} bir karakter ya da sayı listesi değil" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} bir karakter ya da sayı değil" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5073,57 +5085,57 @@ msgstr "Yalnızca organizasyon yöneticileri ve moderatörler gönderi yayınlay msgid "Only organization full members can post" msgstr "Sadece organizasyon tam üyeleri ileti gönderebilir" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode emoji" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Özel emoji" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip ekstra emoji" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Seçenekler listesi" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Kişi seçicisi" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Kısa metin" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Uzun metin" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Tarih seçicisi" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Dış hesap" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "Zamiler" @@ -5139,20 +5151,20 @@ msgstr "Bilinmeyen bir işletim sistemi" msgid "An unknown browser" msgstr "Bilinmeyen bir tarayıcı" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "'queue_id' argümanı eksik" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "'last_event_id' argümanı eksik" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "{event_id} olayından daha yeni bir olay kesildi!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "{event_id} olayı bu sırada değildi" @@ -5324,19 +5336,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Eksik gönderici" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Aynalama, alıcı kullanıcı kimlikleri ile izin verilmemektedir" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Geçersiz kopyalanmış ileti" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Bu organizasyonda Zephyr kopyalamaya (mirroring) izin verilmemektedir" @@ -5402,26 +5414,26 @@ msgstr "Aşağıdaki argümanların en az biri gerekli: emoji_name, emoji_code" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "En az bir kimlik doğrulama yöntemi etkinleştirilmelidir." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Demo organizasyon olmalı." @@ -5899,11 +5911,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Push bildirimleri bouncer'ı için geçersiz alt alan." -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Geçerli Zulip sunucusu API anahtarı ile doğrulama yapmalısınız" @@ -5917,43 +5929,43 @@ msgstr "Geçersiz UUID" msgid "Invalid token type" msgstr "Geçersiz jeton (token) türü" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Veri sırası doğru değil" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/uk/LC_MESSAGES/django.po b/locale/uk/LC_MESSAGES/django.po index bd797d880a55d..e9dfb317ce7c7 100644 --- a/locale/uk/LC_MESSAGES/django.po +++ b/locale/uk/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Mykola Ronik , 2018-2021\n" "Language-Team: Ukrainian (http://app.transifex.com/zulip/zulip/language/uk/)\n" @@ -65,7 +65,7 @@ msgstr "Час початку пізніше, ніж час закінчення msgid "No analytics data available. Please contact your server administrator." msgstr "Аналітичні дані відсутні. Будь ласка, зв'яжіться зі своїм адміністратором сервера." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -136,124 +136,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} закінчується за {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "Невідомий спосіб оплати. Зверніться до {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Щось пішло не так. Зверніться до {email}." -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Щось пішло не так. Перезавантажте сторінку." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Щось пішло не так. Будь ласка, зачекайте кілька секунд і повторіть спробу." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "Повинен бути адміністратором платежів або власником організації" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -261,33 +261,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -314,7 +318,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Внутрішня помилка сервера" @@ -563,31 +567,31 @@ msgstr "Переконайтеся, що ви правильно скопіюв msgid "Billing" msgstr "Платежі" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Скасувати" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "Підтвердити" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -686,6 +690,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -827,8 +835,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2250,7 +2256,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2259,7 +2264,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2267,7 +2272,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2283,7 +2287,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2300,15 +2303,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3353,65 +3365,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Недійсне(і) повідомлення" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Неможливо відобразити повідомлення" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Очікується рівно один канал" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Недійсний тип даних для каналу" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Недійсний тип даних для одержувачів" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Списки одержувачів можуть містити електронні адреси або ID користувачів, але не обидва разом." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Ваш бот {bot_identity} намагався надіслати повідомлення в ID каналу {stream_id}, але каналу з цим ID немає." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Ваш бот {bot_identity} намагався надіслати повідомлення в канал {stream_name}, але такого каналу немає. Клацніть [тут]({new_stream_link}) щоб створити його." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Ваш бот {bot_identity} намагався надіслати повідомлення в канал {stream_name}. Канал існує, але не має підписників." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Віджети: API розробник надіслав JSON з помилковим вмістом" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Віджети: {error_msg}" @@ -3428,28 +3440,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3726,7 +3738,7 @@ msgstr "Під час видалення вкладення сталася по msgid "Message must have recipients!" msgstr "Повідомлення повинно мати отримувачів!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3905,7 +3917,7 @@ msgid "API usage exceeded rate limit" msgstr "Використання API перевищило встановлений ліміт" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Помилковий JSON" @@ -4360,7 +4372,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Токен не існує" @@ -4413,7 +4425,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "Користувач не авторизований для цього запиту" @@ -4460,7 +4472,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4573,7 +4585,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} не є датою" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} не є словником «dict»" @@ -4611,7 +4623,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} не є списком «list»" @@ -4775,7 +4787,7 @@ msgstr "Невірний тип бота" msgid "Invalid interface type" msgstr "Недійсний тип інтерфейсу" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4838,46 +4850,46 @@ msgstr "{var_name} не є дозволеним типом allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} невірне)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} не є URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' не може бути порожнім." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' не є правильним вибором для '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} не є рядком або списком цілих чисел" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} не є рядком або цілим числом" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5066,57 +5078,57 @@ msgstr "Тільки адміністратори та модератори ор msgid "Only organization full members can post" msgstr "Лише повноправні учасники організації можуть залишати повідомлення" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Юнікод емодзі" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Власні емодзі" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Додаткова Zulip емодзі" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "Список опцій" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Вибір людини" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Короткий текст" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Довгий текст" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Вибір дати" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Посилання" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "Зовнішній обліковий запис" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5132,20 +5144,20 @@ msgstr "невідома операційна система" msgid "An unknown browser" msgstr "Невідомий браузер" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Відсутній аргумент 'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Відсутній аргумент 'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "Подія, новіша за {event_id} уже була очищена!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Подія {event_id} була не в цій черзі" @@ -5317,19 +5329,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Відсутній відправник" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Віддзеркалення недозволене робити для ID отримувачів" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Помилкове віддзеркалене повідомлення" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Віддзеркалення Zephyr недозволене для цієї організації" @@ -5395,26 +5407,26 @@ msgstr "Принаймні один з наступних аргументів msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "Необхідно ввімкнути принаймні один метод автентифікації." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5892,11 +5904,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Недійсний піддомен для виклику push-сповіщень" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Повинен перевірити з правильним ключем API для Zulip сервера" @@ -5910,43 +5922,43 @@ msgstr "" msgid "Invalid token type" msgstr "Недійсний тип токену" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Дані невпорядковані." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index 0e083bd8641aa..981953d535bfc 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Gyver Chang, 2022\n" "Language-Team: Vietnamese (http://app.transifex.com/zulip/zulip/language/vi/)\n" @@ -65,7 +65,7 @@ msgstr "Thời gian bắt đầu muộn hơn thời gian kết thúc. Start: {st msgid "No analytics data available. Please contact your server administrator." msgstr "Không có sẵn dữ liệu phân tích. Vui lòng liên hệ với quản trị viên máy chủ của bạn." -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -136,124 +136,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "Đã xảy ra lỗi. Vui lòng liên hệ {email}" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "Thao tác gửi lại bị lỗi. Xin tải lại trang và thử lần nữa." -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "Đã xảy ra lỗi. Vui lòng đợi vài giây và thử lại." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -261,33 +261,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -314,7 +318,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "Lỗi server nội bộ" @@ -563,31 +567,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "Hủy" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "xác nhận" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -686,6 +690,10 @@ msgstr "Education pricing" msgid "View pricing" msgstr "View pricing" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "Zulip for business" @@ -827,8 +835,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2250,7 +2256,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2259,7 +2264,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2267,7 +2272,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2283,7 +2287,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2300,15 +2303,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3353,65 +3365,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "Tin nhắn không hợp lệ" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "Không thể chuẩn bị tin nhắn" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "Expected exactly one stream" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "Invalid data type for stream" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "Invalid data type for recipients" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "Recipient lists may contain emails or user IDs, but not both." -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, but there is no stream with that ID." -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "Your bot {bot_identity} tried to send a message to stream {stream_name}, but that stream does not exist. Click [here]({new_stream_link}) to create it." -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "Your bot {bot_identity} tried to send a message to stream {stream_name}. The stream exists but does not have any subscribers." -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "Topics are required in this organization" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "Widgets: API programmer sent invalid JSON content" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "Widgets: {error_msg}" @@ -3428,28 +3440,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3726,7 +3738,7 @@ msgstr "An error occurred while deleting the attachment. Please try again later. msgid "Message must have recipients!" msgstr "Message must have recipients!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3905,7 +3917,7 @@ msgid "API usage exceeded rate limit" msgstr "API usage exceeded rate limit" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "Malformed JSON" @@ -4360,7 +4372,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token không tồn tại" @@ -4413,7 +4425,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "User not authorized for this query" @@ -4460,7 +4472,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4573,7 +4585,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} is not a date" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} is not a dict" @@ -4611,7 +4623,7 @@ msgid "{var_name} is too large" msgstr "{var_name} is too large" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} is not a list" @@ -4775,7 +4787,7 @@ msgstr "Invalid bot type" msgid "Invalid interface type" msgstr "Invalid interface type" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4838,46 +4850,46 @@ msgstr "{var_name} is not an allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} is wrong)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} is not a URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' cannot be blank." -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' is not a valid choice for '{field_name}'." -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} is not a string or an integer list" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} is not a string or integer" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} does not have a length" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} is missing" @@ -5066,57 +5078,57 @@ msgstr "Only organization administrators and moderators can post" msgid "Only organization full members can post" msgstr "Only organization full members can post" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode emoji" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "Custom emoji" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip extra emoji" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "List of options" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "Person picker" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "Short text" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "Long text" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "Date picker" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "Link" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "External account" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5132,20 +5144,20 @@ msgstr "an unknown operating system" msgid "An unknown browser" msgstr "An unknown browser" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "Missing 'queue_id' argument" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "Missing 'last_event_id' argument" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "An event newer than {event_id} has already been pruned!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "Event {event_id} was not in this queue" @@ -5317,19 +5329,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "Thiếu người gửi" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Mirroring not allowed with recipient user IDs" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "Invalid mirrored message" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "Zephyr mirroring is not allowed in this organization" @@ -5395,26 +5407,26 @@ msgstr "At least one of the following arguments must be present: emoji_name, emo msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "At least one authentication method must be enabled." -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "Must be a demo organization." @@ -5892,11 +5904,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "Invalid subdomain for push notifications bouncer" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "Must validate with valid Zulip server API key" @@ -5910,43 +5922,43 @@ msgstr "Invalid UUID" msgid "Invalid token type" msgstr "Invalid token type" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "Missing user_id or user_uuid" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "Data is out of order." -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/zh_Hans/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po index eec352d259ecd..b36e967c25876 100644 --- a/locale/zh_Hans/LC_MESSAGES/django.po +++ b/locale/zh_Hans/LC_MESSAGES/django.po @@ -18,7 +18,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Clay Cheng <569735195@qq.com>, 2023\n" "Language-Team: Chinese Simplified (http://app.transifex.com/zulip/zulip/language/zh-Hans/)\n" @@ -72,7 +72,7 @@ msgstr "开始时间晚于结束时间。 开始: {start}, 结束: {end}" msgid "No analytics data available. Please contact your server administrator." msgstr "没有分析数据可用。 请联系您的服务器管理员。" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -143,124 +143,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} 以 {last4} 结尾" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "未知的付款方式。请联系{email}。" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "发生了一些错误。请联系 {email}。" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "出错啦!请刷新页面!" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "出错啦!请过几秒再试." -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "无法更新计划。该计划已过期并被新计划取代。" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "无法更新计划。计划已经结束。" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "无法手动更新许可证。您的计划是自动许可证管理。" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "在当前计费周期内,您的计划已使用 {licenses} 许可。" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "您不能减少当前计费周期内的许可证。" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "您的计划已安排续订 {licenses_at_next_renewal} 许可证。" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "没什么可改变的。" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "该组织没有客户!" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "找不到会话" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "必须是帐单管理员或组织所有者" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "未找到付款意图" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "传递 stripe_session_id 或 stripe_payment_intent_id" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -268,33 +268,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -321,7 +325,7 @@ msgid "" msgstr "\n如果此错误是意外的,你可以联系支持。" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "服务器内部错误" @@ -570,31 +574,31 @@ msgstr "确保将链接正确复制到浏览器中。如果您仍然遇到此页 msgid "Billing" msgstr "账单" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "取消" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "确认" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -693,6 +697,10 @@ msgstr "教育定价" msgid "View pricing" msgstr "查看定价" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "商务用Zulip" @@ -834,8 +842,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2257,7 +2263,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2266,7 +2271,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2274,7 +2279,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2290,7 +2294,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2307,15 +2310,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3360,65 +3372,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "消息不正确" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "不能渲染消息" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "预计只有一个频道" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "频道的数据类型无效" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "收件人的数据类型无效" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "收件人列表可能包含电子邮件或用户 ID,但不能同时包含两者。" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "您的机器人 {bot_identity} 尝试向频道 ID {stream_id} 发送消息,但没有具有该 ID 的频道。" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "您的机器人 {bot_identity} 尝试向频道 {stream_name} 发送消息,但该频道不存在。单击 [此处]({new_stream_link}) 以创建它。" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "您的机器人 {bot_identity} 尝试向频道 {stream_name} 发送消息。频道存在但没有任何订阅者。" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "此社群需要主题" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "组件:API程序发送了无效的JSON内容" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "小部件:{error_msg}" @@ -3435,28 +3447,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3733,7 +3745,7 @@ msgstr "删除附件时发生错误。请稍后再试。" msgid "Message must have recipients!" msgstr "信息必须指定接收人" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3912,7 +3924,7 @@ msgid "API usage exceeded rate limit" msgstr "API请求超过了限制" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "JSON格式不正确" @@ -4367,7 +4379,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token不存在" @@ -4420,7 +4432,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "用户没有授权本次查询" @@ -4467,7 +4479,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4580,7 +4592,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} 不是日期" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} 不是字典" @@ -4618,7 +4630,7 @@ msgid "{var_name} is too large" msgstr "{var_name} 太大" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} 不是列表" @@ -4782,7 +4794,7 @@ msgstr "无效的机器人类型" msgid "Invalid interface type" msgstr "无效的接口类型" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4845,46 +4857,46 @@ msgstr "{var_name} 不是 allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value}({value} 错误)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} 不是网址" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "URL 模式必须包含'%(username)s'。" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' 不能为空格" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "字段不能有重复的选择。" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' 不是 '{field_name}'字段的合法选项" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} 不是字符串或整数列表" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} 不是字符串或整数" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "{var_name} 没有长度" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "{var_name} 丢失" @@ -5073,57 +5085,57 @@ msgstr "只有社群管理员和版主可以发帖" msgid "Only organization full members can post" msgstr "只有社群正式成员才能发帖" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode 表情符号" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "自定义表情" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip额外表情" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "选项列表" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "用户选择" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "短文本" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "长文本" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "日期选择" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "链接" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "外部帐户" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5139,20 +5151,20 @@ msgstr "未知的操作系统" msgid "An unknown browser" msgstr "未知的浏览器" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "缺少参数'queue_id'" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "缺少参数'last_event_id'" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "比 {event_id} 更新的事件已被修剪!" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "事件 {event_id} 不在此队列中" @@ -5324,19 +5336,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "缺少发送人" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "不允许对收件人用户ID进行镜像" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "镜像消息不正确(Invalid mirrored message)" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "这个社群禁止Zephyr镜像" @@ -5402,26 +5414,26 @@ msgstr "至少提供一个下面的参数:表情名称,表情代码" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "必须至少启用一种身份验证方法。" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "必须是演示社群。" @@ -5899,11 +5911,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "推送通知到无效子域" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "必须验证有效的Zulip服务器 API Key" @@ -5917,43 +5929,43 @@ msgstr "无效的 UUID" msgid "Invalid token type" msgstr "无效的Token类型" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "缺少 user_id 或 user_uuid" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "数据超出要求" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/zh_Hant/LC_MESSAGES/django.po b/locale/zh_Hant/LC_MESSAGES/django.po index f09f5afaa649c..bab136fb98bae 100644 --- a/locale/zh_Hant/LC_MESSAGES/django.po +++ b/locale/zh_Hant/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Ivan Lau , 2020\n" "Language-Team: Chinese Traditional (http://app.transifex.com/zulip/zulip/language/zh-Hant/)\n" @@ -64,7 +64,7 @@ msgstr "開始時間比完結時間更遲。開始:{start},完結:{end}" msgid "No analytics data available. Please contact your server administrator." msgstr "沒有可用分析數據。請聯繫您的伺服器管理員。" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -135,124 +135,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "" -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "發生錯誤,請聯絡 {email} 。" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "發生錯誤,請重新整理此頁。" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "發生錯誤,請稍後數秒再試。" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -260,33 +260,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -313,7 +317,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -562,31 +566,31 @@ msgstr "" msgid "Billing" msgstr "" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "取消" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -685,6 +689,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -826,8 +834,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2249,7 +2255,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2258,7 +2263,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2266,7 +2271,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2282,7 +2286,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2299,15 +2302,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3352,65 +3364,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3427,28 +3439,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3725,7 +3737,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3904,7 +3916,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4359,7 +4371,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "" @@ -4412,7 +4424,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "" @@ -4459,7 +4471,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4572,7 +4584,7 @@ msgid "{var_name} is not a date" msgstr "" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "" @@ -4610,7 +4622,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "" @@ -4774,7 +4786,7 @@ msgstr "" msgid "Invalid interface type" msgstr "" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4837,46 +4849,46 @@ msgstr "" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5065,57 +5077,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "自訂表情" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5131,20 +5143,20 @@ msgstr "" msgid "An unknown browser" msgstr "" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5316,19 +5328,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5394,26 +5406,26 @@ msgstr "" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5891,11 +5903,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "" @@ -5909,43 +5921,43 @@ msgstr "" msgid "Invalid token type" msgstr "" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/locale/zh_TW/LC_MESSAGES/django.po b/locale/zh_TW/LC_MESSAGES/django.po index 1fd4fcdb2cff9..75a51de62a91d 100644 --- a/locale/zh_TW/LC_MESSAGES/django.po +++ b/locale/zh_TW/LC_MESSAGES/django.po @@ -13,7 +13,7 @@ msgid "" msgstr "" "Project-Id-Version: Zulip\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-12 17:56+0000\n" +"POT-Creation-Date: 2023-12-14 20:29+0000\n" "PO-Revision-Date: 2015-12-06 00:18+0000\n" "Last-Translator: Tim Abbott , 2021-2022\n" "Language-Team: Chinese (Taiwan) (http://app.transifex.com/zulip/zulip/language/zh_TW/)\n" @@ -67,7 +67,7 @@ msgstr "開始時間晚於結束時間。開始時間:{start}、結束時間 msgid "No analytics data available. Please contact your server administrator." msgstr "沒有分析資料。請聯繫您的伺服器管理員。" -#: analytics/views/support.py:183 analytics/views/support.py:422 +#: analytics/views/support.py:188 analytics/views/support.py:427 #: zerver/views/streams.py:291 zerver/views/streams.py:295 #: zerver/views/streams.py:303 msgid "Invalid parameters" @@ -138,124 +138,124 @@ msgstr "" msgid "Invalid remote server." msgstr "" -#: corporate/lib/stripe.py:200 +#: corporate/lib/stripe.py:204 #, python-brace-format msgid "" "You must purchase licenses for all active users in your organization " "(minimum {min_licenses})." msgstr "" -#: corporate/lib/stripe.py:206 +#: corporate/lib/stripe.py:210 #, python-brace-format msgid "" "Invoices with more than {max_licenses} licenses can't be processed from this" " page. To complete the upgrade, please contact {email}." msgstr "" -#: corporate/lib/stripe.py:370 +#: corporate/lib/stripe.py:352 msgid "No payment method on file." msgstr "" -#: corporate/lib/stripe.py:378 +#: corporate/lib/stripe.py:360 #, python-brace-format msgid "{brand} ending in {last4}" msgstr "{brand} 的末四碼為 {last4}" -#: corporate/lib/stripe.py:386 +#: corporate/lib/stripe.py:368 #, python-brace-format msgid "Unknown payment method. Please contact {email}." msgstr "未知的付款方式。請聯繫 {email}." -#: corporate/lib/stripe.py:402 +#: corporate/lib/stripe.py:384 #, python-brace-format msgid "Something went wrong. Please contact {email}." msgstr "付款失敗,請聯絡 {email}" -#: corporate/lib/stripe.py:403 +#: corporate/lib/stripe.py:385 msgid "Something went wrong. Please reload the page." msgstr "發生錯誤。請重新更新頁面。" -#: corporate/lib/stripe.py:486 +#: corporate/lib/stripe.py:476 msgid "Something went wrong. Please wait a few seconds and try again." msgstr "發生錯誤。請稍待數秒後再試一次。" -#: corporate/lib/stripe.py:1204 +#: corporate/lib/stripe.py:1226 msgid "Please add a credit card before starting your free trial." msgstr "" -#: corporate/lib/stripe.py:1225 +#: corporate/lib/stripe.py:1247 msgid "Please add a credit card to schedule upgrade." msgstr "" -#: corporate/lib/stripe.py:1954 +#: corporate/lib/stripe.py:1995 msgid "" "Unable to update the plan. The plan has been expired and replaced with a new" " plan." msgstr "無法更新方案。該方案已經過期並由新方案取代。" -#: corporate/lib/stripe.py:1959 +#: corporate/lib/stripe.py:2000 msgid "Unable to update the plan. The plan has ended." msgstr "無法更新方案。該方案已經結束。" -#: corporate/lib/stripe.py:2014 +#: corporate/lib/stripe.py:2055 msgid "" "Cannot update licenses in the current billing period for free trial plan." msgstr "" -#: corporate/lib/stripe.py:2019 corporate/lib/stripe.py:2047 +#: corporate/lib/stripe.py:2060 corporate/lib/stripe.py:2088 msgid "" "Unable to update licenses manually. Your plan is on automatic license " "management." msgstr "" -#: corporate/lib/stripe.py:2025 +#: corporate/lib/stripe.py:2066 #, python-brace-format msgid "" "Your plan is already on {licenses} licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2030 +#: corporate/lib/stripe.py:2071 msgid "You cannot decrease the licenses in the current billing period." msgstr "" -#: corporate/lib/stripe.py:2056 +#: corporate/lib/stripe.py:2097 msgid "" "Cannot change the licenses for next billing cycle for a plan that is being " "downgraded." msgstr "" -#: corporate/lib/stripe.py:2062 +#: corporate/lib/stripe.py:2103 #, python-brace-format msgid "" "Your plan is already scheduled to renew with {licenses_at_next_renewal} " "licenses." msgstr "" -#: corporate/lib/stripe.py:2077 +#: corporate/lib/stripe.py:2118 msgid "Nothing to change." msgstr "" -#: corporate/lib/stripe.py:2282 +#: corporate/lib/stripe.py:2323 msgid "No customer for this organization!" msgstr "" -#: corporate/lib/stripe.py:2291 +#: corporate/lib/stripe.py:2332 msgid "Session not found" msgstr "" -#: corporate/lib/stripe.py:2297 zerver/decorator.py:197 +#: corporate/lib/stripe.py:2338 zerver/decorator.py:197 msgid "Must be a billing administrator or an organization owner" msgstr "" -#: corporate/lib/stripe.py:2308 +#: corporate/lib/stripe.py:2349 msgid "Payment intent not found" msgstr "" -#: corporate/lib/stripe.py:2311 +#: corporate/lib/stripe.py:2352 msgid "Pass stripe_session_id or stripe_payment_intent_id" msgstr "" -#: corporate/lib/stripe.py:2817 +#: corporate/lib/stripe.py:2891 #, python-brace-format msgid "" "Your organization's request for sponsored hosting has been approved! You have been upgraded to {plan_name}, free of charge. {emoji}\n" @@ -263,33 +263,37 @@ msgid "" "If you could {begin_link}list Zulip as a sponsor on your website{end_link}, we would really appreciate it!" msgstr "" -#: corporate/views/remote_billing_page.py:125 +#: corporate/views/billing_page.py:328 +msgid "Parameter 'confirmed' is required" +msgstr "" + +#: corporate/views/remote_billing_page.py:130 msgid "Billing access token expired." msgstr "" -#: corporate/views/remote_billing_page.py:127 +#: corporate/views/remote_billing_page.py:132 msgid "Invalid billing access token." msgstr "" -#: corporate/views/remote_billing_page.py:242 +#: corporate/views/remote_billing_page.py:256 msgid "User account doesn't exist yet." msgstr "" -#: corporate/views/remote_billing_page.py:247 -#: corporate/views/remote_billing_page.py:612 +#: corporate/views/remote_billing_page.py:261 +#: corporate/views/remote_billing_page.py:659 msgid "You must accept the Terms of Service to proceed." msgstr "" -#: corporate/views/remote_billing_page.py:447 +#: corporate/views/remote_billing_page.py:487 msgid "" "This zulip_org_id is not registered with Zulip's billing management system." msgstr "" -#: corporate/views/remote_billing_page.py:454 +#: corporate/views/remote_billing_page.py:494 msgid "Invalid zulip_org_key for this zulip_org_id." msgstr "" -#: corporate/views/remote_billing_page.py:458 +#: corporate/views/remote_billing_page.py:498 msgid "Your server registration has been deactivated." msgstr "" @@ -316,7 +320,7 @@ msgid "" msgstr "" #: templates/500.html:4 templates/500.html:21 -#: zerver/actions/scheduled_messages.py:387 zerver/middleware.py:393 +#: zerver/actions/scheduled_messages.py:401 zerver/middleware.py:393 msgid "Internal server error" msgstr "" @@ -565,31 +569,31 @@ msgstr "請確認您正確地將連結複製貼上於瀏覽器上。若您仍看 msgid "Billing" msgstr "帳單" -#: templates/corporate/billing.html:301 templates/corporate/billing.html:327 -#: templates/corporate/billing.html:356 templates/corporate/billing.html:385 -#: templates/corporate/billing.html:411 +#: templates/corporate/billing.html:312 templates/corporate/billing.html:338 +#: templates/corporate/billing.html:367 templates/corporate/billing.html:396 +#: templates/corporate/billing.html:422 #: templates/zerver/change_email_address_visibility_modal.html:8 #: templates/zerver/development/email_log.html:36 msgid "Close modal" msgstr "" -#: templates/corporate/billing.html:312 templates/corporate/billing.html:341 -#: templates/corporate/billing.html:370 +#: templates/corporate/billing.html:323 templates/corporate/billing.html:352 +#: templates/corporate/billing.html:381 #: templates/zerver/change_email_address_visibility_modal.html:26 msgid "Cancel" msgstr "取消" -#: templates/corporate/billing.html:314 templates/corporate/billing.html:398 -#: templates/corporate/billing.html:421 +#: templates/corporate/billing.html:325 templates/corporate/billing.html:409 +#: templates/corporate/billing.html:432 msgid "Downgrade" msgstr "" -#: templates/corporate/billing.html:343 templates/corporate/billing.html:372 +#: templates/corporate/billing.html:354 templates/corporate/billing.html:383 #: templates/zerver/change_email_address_visibility_modal.html:28 msgid "Confirm" msgstr "確認" -#: templates/corporate/billing.html:396 templates/corporate/billing.html:419 +#: templates/corporate/billing.html:407 templates/corporate/billing.html:430 msgid "Never mind" msgstr "" @@ -688,6 +692,10 @@ msgstr "" msgid "View pricing" msgstr "" +#: templates/corporate/remote_billing_server_deactivate.html:5 +msgid "Deactivate server registration?" +msgstr "" + #: templates/corporate/self-hosting.html:31 msgid "Zulip for business" msgstr "" @@ -829,8 +837,6 @@ msgstr "" #: templates/zerver/accounts_home.html:92 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:21 #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:6 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:18 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 #: templates/zerver/footer.html:17 #: templates/zerver/log_into_subdomain_token_invalid.html:13 #: templates/zerver/login.html:5 templates/zerver/login.html:128 @@ -2252,7 +2258,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:16 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:13 #, python-format msgid "" "\n" @@ -2261,7 +2266,7 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.html:24 -#: templates/zerver/emails/remote_realm_billing_confirm_login.html:21 +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:17 #, python-format msgid "" "Questions? Learn more or contact " @@ -2269,7 +2274,6 @@ msgid "" msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.subject.txt:1 -#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 msgid "Log in to Zulip plan management" msgstr "" @@ -2285,7 +2289,6 @@ msgid "Click the link below to log in." msgstr "" #: templates/zerver/emails/remote_billing_legacy_server_confirm_login.txt:4 -#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "This link will expire in %(validity_in_hours)s hours." msgstr "" @@ -2302,15 +2305,24 @@ msgstr "" #, python-format msgid "" "\n" -" Click the button below to log in to Zulip plan management for %(remote_realm_host)s.\n" +" Click the button below to confirm your email and log in to Zulip plan management for %(remote_realm_host)s.\n" " " msgstr "" +#: templates/zerver/emails/remote_realm_billing_confirm_login.html:14 +#: templates/zerver/emails/remote_realm_billing_confirm_login.txt:4 +msgid "Confirm and log in" +msgstr "" + +#: templates/zerver/emails/remote_realm_billing_confirm_login.subject.txt:1 +msgid "Confirm email for Zulip plan management" +msgstr "" + #: templates/zerver/emails/remote_realm_billing_confirm_login.txt:1 #, python-format msgid "" -"Click the link below to log in to Zulip plan management for " -"%(remote_realm_host)s." +"Click the link below to confirm your email and log in to Zulip plan " +"management for %(remote_realm_host)s." msgstr "" #: templates/zerver/emails/sponsorship_approved_community_plan.html:9 @@ -3355,65 +3367,65 @@ msgstr "" msgid "Invalid message(s)" msgstr "無效的訊息(s)" -#: zerver/actions/message_send.py:177 +#: zerver/actions/message_send.py:176 msgid "Unable to render message" msgstr "無法渲染訊息" -#: zerver/actions/message_send.py:1285 +#: zerver/actions/message_send.py:1271 msgid "Expected exactly one stream" msgstr "預期至少一個串流" -#: zerver/actions/message_send.py:1296 +#: zerver/actions/message_send.py:1282 msgid "Invalid data type for stream" msgstr "無效的串流資料類型" -#: zerver/actions/message_send.py:1312 zerver/actions/message_send.py:1322 +#: zerver/actions/message_send.py:1298 zerver/actions/message_send.py:1308 #: zerver/lib/recipient_parsing.py:25 msgid "Invalid data type for recipients" msgstr "無效的 recipients 資料類型" -#: zerver/actions/message_send.py:1330 zerver/actions/message_send.py:1338 +#: zerver/actions/message_send.py:1316 zerver/actions/message_send.py:1324 msgid "Recipient lists may contain emails or user IDs, but not both." msgstr "接收者清單可包含 emails 或是使用者 IDs,但不能兩個同時存在。" -#: zerver/actions/message_send.py:1483 +#: zerver/actions/message_send.py:1474 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream ID {stream_id}, " "but there is no stream with that ID." msgstr "您的機器人 {bot_identity} 嘗試發送訊息至串流 ID {stream_id},但無串流是該 ID。" -#: zerver/actions/message_send.py:1494 +#: zerver/actions/message_send.py:1485 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}, but" " that stream does not exist. Click [here]({new_stream_link}) to create it." msgstr "您的機器人 {bot_identity} 嘗試發送訊息至串流 {stream_name},但該串流不存在。點擊 [此處]({new_stream_link}) 以新建他。" -#: zerver/actions/message_send.py:1506 +#: zerver/actions/message_send.py:1497 #, python-brace-format msgid "" "Your bot {bot_identity} tried to send a message to stream {stream_name}. The" " stream exists but does not have any subscribers." msgstr "您的機器人 {bot_identity} 嘗試發送訊息至串流 {stream_name}。該串流存在,但沒有訂閱者。" -#: zerver/actions/message_send.py:1553 +#: zerver/actions/message_send.py:1544 msgid "Direct messages are disabled in this organization." msgstr "" -#: zerver/actions/message_send.py:1568 +#: zerver/actions/message_send.py:1554 msgid "You do not have permission to access some of the recipients." msgstr "" -#: zerver/actions/message_send.py:1699 +#: zerver/actions/message_send.py:1685 msgid "Topics are required in this organization" msgstr "" -#: zerver/actions/message_send.py:1761 +#: zerver/actions/message_send.py:1747 msgid "Widgets: API programmer sent invalid JSON content" msgstr "" -#: zerver/actions/message_send.py:1767 +#: zerver/actions/message_send.py:1753 #, python-brace-format msgid "Widgets: {error_msg}" msgstr "" @@ -3430,28 +3442,28 @@ msgstr "" msgid "The ordered list must enumerate all existing linkifiers exactly once" msgstr "" -#: zerver/actions/scheduled_messages.py:136 +#: zerver/actions/scheduled_messages.py:150 msgid "Scheduled message was already sent" msgstr "" -#: zerver/actions/scheduled_messages.py:141 +#: zerver/actions/scheduled_messages.py:155 #: zerver/views/scheduled_messages.py:88 -#: zerver/views/scheduled_messages.py:128 +#: zerver/views/scheduled_messages.py:129 msgid "Scheduled delivery time must be in the future." msgstr "" -#: zerver/actions/scheduled_messages.py:276 +#: zerver/actions/scheduled_messages.py:290 msgid "Message could not be sent at the scheduled time." msgstr "" -#: zerver/actions/scheduled_messages.py:327 +#: zerver/actions/scheduled_messages.py:341 #, python-brace-format msgid "" "The message you scheduled for {delivery_datetime} was not sent because of " "the following error:" msgstr "" -#: zerver/actions/scheduled_messages.py:332 +#: zerver/actions/scheduled_messages.py:346 msgid "[View scheduled messages](#scheduled)" msgstr "" @@ -3728,7 +3740,7 @@ msgstr "" msgid "Message must have recipients!" msgstr "訊息必須有接受者!" -#: zerver/lib/digest.py:412 +#: zerver/lib/digest.py:415 #, python-brace-format msgid "{service_name} digest" msgstr "" @@ -3907,7 +3919,7 @@ msgid "API usage exceeded rate limit" msgstr "" #: zerver/lib/exceptions.py:255 zerver/lib/request.py:445 -#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:779 +#: zerver/lib/typed_endpoint.py:361 zerver/lib/validator.py:795 msgid "Malformed JSON" msgstr "" @@ -4362,7 +4374,7 @@ msgid "Invalid GCM options to bouncer: {options}" msgstr "" #. error -#: zerver/lib/push_notifications.py:731 zilencer/views.py:233 +#: zerver/lib/push_notifications.py:731 zilencer/views.py:234 msgid "Token does not exist" msgstr "Token 不存在" @@ -4415,7 +4427,7 @@ msgstr "" #: zerver/lib/recipient_users.py:29 zerver/lib/streams.py:281 #: zerver/lib/streams.py:289 zerver/lib/streams.py:896 #: zerver/tornado/views.py:167 zerver/views/events_register.py:97 -#: zerver/views/message_send.py:176 zerver/views/message_send.py:197 +#: zerver/views/message_send.py:177 zerver/views/message_send.py:198 msgid "User not authorized for this query" msgstr "使用者此查詢未授權" @@ -4462,7 +4474,7 @@ msgstr "" msgid "Scheduled message does not exist" msgstr "" -#: zerver/lib/send_email.py:73 +#: zerver/lib/send_email.py:76 #, python-brace-format msgid "{service_name} account security" msgstr "" @@ -4575,7 +4587,7 @@ msgid "{var_name} is not a date" msgstr "{var_name} 不是 date" #: zerver/lib/typed_endpoint.py:317 zerver/lib/validator.py:287 -#: zerver/lib/validator.py:671 +#: zerver/lib/validator.py:687 #, python-brace-format msgid "{var_name} is not a dict" msgstr "{var_name} 不是 dict" @@ -4613,7 +4625,7 @@ msgid "{var_name} is too large" msgstr "" #: zerver/lib/typed_endpoint.py:327 zerver/lib/validator.py:236 -#: zerver/lib/validator.py:668 +#: zerver/lib/validator.py:684 #, python-brace-format msgid "{var_name} is not a list" msgstr "{var_name} 不是 list" @@ -4777,7 +4789,7 @@ msgstr "無效的機器人類型" msgid "Invalid interface type" msgstr "無效的介面類型" -#: zerver/lib/users.py:217 zerver/models.py:4979 +#: zerver/lib/users.py:217 zerver/models.py:4978 #, python-brace-format msgid "Invalid user ID: {user_id}" msgstr "" @@ -4840,46 +4852,46 @@ msgstr "{var_name} 不是 allowed_type" msgid "{variable} != {expected_value} ({value} is wrong)" msgstr "{variable} != {expected_value} ({value} 是錯的)" -#: zerver/lib/validator.py:389 +#: zerver/lib/validator.py:389 zerver/lib/validator.py:403 #, python-brace-format msgid "{var_name} is not a URL" msgstr "{var_name} 不是 URL" -#: zerver/lib/validator.py:396 +#: zerver/lib/validator.py:412 #, python-format msgid "URL pattern must contain '%(username)s'." msgstr "" -#: zerver/lib/validator.py:420 +#: zerver/lib/validator.py:436 #, python-brace-format msgid "'{item}' cannot be blank." msgstr "'{item}' 不能是空白。" -#: zerver/lib/validator.py:429 +#: zerver/lib/validator.py:445 msgid "Field must not have duplicate choices." msgstr "" -#: zerver/lib/validator.py:442 +#: zerver/lib/validator.py:458 #, python-brace-format msgid "'{value}' is not a valid choice for '{field_name}'." msgstr "'{value}' 對於 '{field_name}' 不是有效的選擇。" -#: zerver/lib/validator.py:620 +#: zerver/lib/validator.py:636 #, python-brace-format msgid "{var_name} is not a string or an integer list" msgstr "{var_name} 不是 string 或 integer list" -#: zerver/lib/validator.py:630 +#: zerver/lib/validator.py:646 #, python-brace-format msgid "{var_name} is not a string or integer" msgstr "{var_name} 不是 string 或 integer" -#: zerver/lib/validator.py:659 +#: zerver/lib/validator.py:675 #, python-brace-format msgid "{var_name} does not have a length" msgstr "" -#: zerver/lib/validator.py:719 zerver/lib/validator.py:741 +#: zerver/lib/validator.py:735 zerver/lib/validator.py:757 #, python-brace-format msgid "{var_name} is missing" msgstr "" @@ -5068,57 +5080,57 @@ msgstr "" msgid "Only organization full members can post" msgstr "只有組織成員可以發表" -#: zerver/models.py:3448 +#: zerver/models.py:3446 msgid "Unicode emoji" msgstr "Unicode 表情符號" -#: zerver/models.py:3449 +#: zerver/models.py:3447 msgid "Custom emoji" msgstr "自定義表情符號" -#: zerver/models.py:3450 +#: zerver/models.py:3448 msgid "Zulip extra emoji" msgstr "Zulip 額外的表情符號" -#: zerver/models.py:4983 +#: zerver/models.py:4982 #, python-brace-format msgid "User with ID {user_id} is deactivated" msgstr "" -#: zerver/models.py:4987 +#: zerver/models.py:4986 #, python-brace-format msgid "User with ID {user_id} is a bot" msgstr "" -#: zerver/models.py:5027 +#: zerver/models.py:5026 msgid "List of options" msgstr "" -#: zerver/models.py:5030 +#: zerver/models.py:5029 msgid "Person picker" msgstr "" -#: zerver/models.py:5042 +#: zerver/models.py:5041 msgid "Short text" msgstr "" -#: zerver/models.py:5043 +#: zerver/models.py:5042 msgid "Long text" msgstr "" -#: zerver/models.py:5044 +#: zerver/models.py:5043 msgid "Date picker" msgstr "日期選擇器" -#: zerver/models.py:5045 +#: zerver/models.py:5044 msgid "Link" msgstr "連結" -#: zerver/models.py:5048 +#: zerver/models.py:5047 msgid "External account" msgstr "" -#: zerver/models.py:5053 +#: zerver/models.py:5052 msgid "Pronouns" msgstr "" @@ -5134,20 +5146,20 @@ msgstr "未知的作業系統" msgid "An unknown browser" msgstr "未知的瀏覽器" -#: zerver/tornado/event_queue.py:678 +#: zerver/tornado/event_queue.py:679 msgid "Missing 'queue_id' argument" msgstr "缺少 'queue_id' 參數" -#: zerver/tornado/event_queue.py:681 +#: zerver/tornado/event_queue.py:682 msgid "Missing 'last_event_id' argument" msgstr "缺少 'last_event_id' 參數" -#: zerver/tornado/event_queue.py:688 +#: zerver/tornado/event_queue.py:689 #, python-brace-format msgid "An event newer than {event_id} has already been pruned!" msgstr "" -#: zerver/tornado/event_queue.py:698 +#: zerver/tornado/event_queue.py:699 #, python-brace-format msgid "Event {event_id} was not in this queue" msgstr "" @@ -5319,19 +5331,19 @@ msgstr "" msgid "No such topic '{topic}'" msgstr "" -#: zerver/views/message_send.py:195 +#: zerver/views/message_send.py:196 msgid "Missing sender" msgstr "缺少發送者" -#: zerver/views/message_send.py:202 +#: zerver/views/message_send.py:203 msgid "Mirroring not allowed with recipient user IDs" msgstr "Mirroring not allowed with recipient 使用者 IDs" -#: zerver/views/message_send.py:214 zerver/views/message_send.py:221 +#: zerver/views/message_send.py:215 zerver/views/message_send.py:222 msgid "Invalid mirrored message" msgstr "無效的鏡像訊息" -#: zerver/views/message_send.py:217 +#: zerver/views/message_send.py:218 msgid "Zephyr mirroring is not allowed in this organization" msgstr "" @@ -5397,26 +5409,26 @@ msgstr "以下參數至少必須存在一個:emoji_name, emoji_code" msgid "Read receipts are disabled in this organization." msgstr "" -#: zerver/views/realm.py:189 +#: zerver/views/realm.py:195 #, python-brace-format msgid "Invalid language '{language}'" msgstr "" -#: zerver/views/realm.py:194 +#: zerver/views/realm.py:200 msgid "At least one authentication method must be enabled." msgstr "" -#: zerver/views/realm.py:199 +#: zerver/views/realm.py:205 #, python-brace-format msgid "Invalid video_chat_provider {video_chat_provider}" msgstr "" -#: zerver/views/realm.py:207 +#: zerver/views/realm.py:213 #, python-brace-format msgid "Invalid giphy_rating {giphy_rating}" msgstr "" -#: zerver/views/realm.py:420 +#: zerver/views/realm.py:426 msgid "Must be a demo organization." msgstr "" @@ -5894,11 +5906,11 @@ msgid "" "exports]({export_settings_link})." msgstr "" -#: zilencer/auth.py:90 +#: zilencer/auth.py:97 msgid "Invalid subdomain for push notifications bouncer" msgstr "無效的推送通知 bouncer 子域名" -#: zilencer/auth.py:105 +#: zilencer/auth.py:116 msgid "Must validate with valid Zulip server API key" msgstr "需驗證是否為可用的 Zulip server API" @@ -5912,43 +5924,43 @@ msgstr "無效的 UUID" msgid "Invalid token type" msgstr "無效的 token 類型" -#: zilencer/views.py:121 +#: zilencer/views.py:122 #, python-brace-format msgid "{hostname} is not a valid hostname" msgstr "" -#: zilencer/views.py:174 +#: zilencer/views.py:175 msgid "Missing ios_app_id" msgstr "" -#: zilencer/views.py:177 +#: zilencer/views.py:178 msgid "Missing user_id or user_uuid" msgstr "未填寫 user_id 或 user_uuid" -#: zilencer/views.py:602 +#: zilencer/views.py:603 #, python-brace-format msgid "Invalid property {property}" msgstr "" -#: zilencer/views.py:605 +#: zilencer/views.py:606 msgid "Invalid event type." msgstr "" -#: zilencer/views.py:612 +#: zilencer/views.py:613 msgid "Data is out of order." msgstr "" -#: zilencer/views.py:674 +#: zilencer/views.py:675 msgid "Duplicate registration detected." msgstr "" -#: zilencer/views.py:893 +#: zilencer/views.py:905 msgid "" "Failed to migrate customer from server to realms. Please contact support for" " assistance." msgstr "" -#: zilencer/views.py:940 +#: zilencer/views.py:957 msgid "Malformed audit log data" msgstr "" diff --git a/puppet/zulip/files/nagios_plugins/zulip_app_frontend/check_send_receive_time b/puppet/zulip/files/nagios_plugins/zulip_app_frontend/check_send_receive_time index 4907ad217ba04..7ff669e565fc5 100755 --- a/puppet/zulip/files/nagios_plugins/zulip_app_frontend/check_send_receive_time +++ b/puppet/zulip/files/nagios_plugins/zulip_app_frontend/check_send_receive_time @@ -66,7 +66,8 @@ django.setup() from django.conf import settings -from zerver.models import get_realm, get_system_bot +from zerver.models.realms import get_realm +from zerver.models.users import get_system_bot states = { "OK": 0, diff --git a/puppet/zulip_ops/files/nagios_plugins/zulip_zephyr_mirror/check_user_zephyr_mirror_liveness b/puppet/zulip_ops/files/nagios_plugins/zulip_zephyr_mirror/check_user_zephyr_mirror_liveness index 3c3a94ecd9477..3d82924b98b14 100755 --- a/puppet/zulip_ops/files/nagios_plugins/zulip_zephyr_mirror/check_user_zephyr_mirror_liveness +++ b/puppet/zulip_ops/files/nagios_plugins/zulip_zephyr_mirror/check_user_zephyr_mirror_liveness @@ -28,7 +28,8 @@ django.setup() from typing import Dict -from zerver.models import UserActivity, get_client +from zerver.models import UserActivity +from zerver.models.clients import get_client states: Dict[str, int] = { "OK": 0, diff --git a/scripts/lib/zulip_tools.py b/scripts/lib/zulip_tools.py index f79cd78d02720..ce031d1dad215 100755 --- a/scripts/lib/zulip_tools.py +++ b/scripts/lib/zulip_tools.py @@ -341,7 +341,7 @@ def get_caches_to_be_purged( # and threshold days, this function return a list of caches # which can be purged. Remove the cache only if it is: # 1: Not in use by the current installation(in dev as well as in prod). - # 2: Not in use by a deployment not older than `threshold_days`(in prod). + # 2: Not in use by a deployment not older than `threshold_days` (in prod). # 3: Not in use by '/root/zulip'. # 4: Not older than `threshold_days`. caches_to_purge = set() diff --git a/templates/analytics/remote_activity_key.html b/templates/analytics/remote_activity_key.html index 816cb3f339b15..4f3d862ab8c75 100644 --- a/templates/analytics/remote_activity_key.html +++ b/templates/analytics/remote_activity_key.html @@ -2,6 +2,7 @@

    Chart key:

    • Zulip version - as of last update time
    • Mobile pushes forwarded - last 7 days, including today's current count
    • +
    • ARR (annual recurring revenue) - the number of users they are paying for * annual price/user
    • Links
      • - remote server's stats page
      • diff --git a/templates/analytics/remote_realm_details.html b/templates/analytics/remote_realm_details.html index b00222d9bc2ae..6704bba68960f 100644 --- a/templates/analytics/remote_realm_details.html +++ b/templates/analytics/remote_realm_details.html @@ -4,6 +4,7 @@

        {{ remote_realm.name }}

        Remote server hostname: {{ remote_realm.host }}
        Date created: {{ remote_realm.realm_date_created.strftime('%d %B %Y') }}
        Org type: {{ get_org_type_display_name(remote_realm.org_type) }}
        + Plan type: {{ get_plan_type_name(remote_realm.plan_type) }}
        {% if remote_realm.plan_type == SPONSORED_PLAN_TYPE %}

        On 100% sponsored Zulip Community plan.

        {% endif %} @@ -11,25 +12,24 @@

        On 100% sponsored Zulip Community plan.

        {% if remote_realm.plan_type != SPONSORED_PLAN_TYPE %} {% with %} - {% set customer = plan_data[remote_realm.id].customer %} + {% set sponsorship_data = support_data[remote_realm.id].sponsorship_data %} {% set remote_id = remote_realm.id %} {% set remote_type = "remote_realm_id" %} - {% set has_fixed_price = plan_data[remote_realm.id].has_fixed_price %} - {% set get_discount = get_discount %} + {% set has_fixed_price = support_data[remote_realm.id].plan_data.has_fixed_price %} {% include 'analytics/sponsorship_forms_support.html' %} {% endwith %} {% endif %} -{% if plan_data[remote_realm.id].current_plan %} +{% if support_data[remote_realm.id].plan_data.current_plan %}
        {% with %} - {% set plan_data = plan_data[remote_realm.id] %} + {% set plan_data = support_data[remote_realm.id].plan_data %} {% include 'analytics/current_plan_details.html' %} {% endwith %}
        {% with %} - {% set current_plan = plan_data[remote_realm.id].current_plan %} + {% set current_plan = support_data[remote_realm.id].plan_data.current_plan %} {% set remote_id = remote_realm.id %} {% set remote_type = "remote_realm_id" %} {% include 'analytics/current_plan_forms_support.html' %} diff --git a/templates/analytics/remote_server_support.html b/templates/analytics/remote_server_support.html index 9bc2354863503..5f9df9b7d16ae 100644 --- a/templates/analytics/remote_server_support.html +++ b/templates/analytics/remote_server_support.html @@ -54,25 +54,24 @@

        On 100% sponsored Zulip Community plan.

        {% if remote_server.plan_type != SPONSORED_PLAN_TYPE %} {% with %} - {% set customer = remote_servers_plan_data[remote_server.id].customer %} + {% set sponsorship_data = remote_servers_support_data[remote_server.id].sponsorship_data %} {% set remote_id = remote_server.id %} {% set remote_type = "remote_server_id" %} - {% set has_fixed_price = remote_servers_plan_data[remote_server.id].has_fixed_price %} - {% set get_discount = get_discount %} + {% set has_fixed_price = remote_servers_support_data[remote_server.id].plan_data.has_fixed_price %} {% include 'analytics/sponsorship_forms_support.html' %} {% endwith %} {% endif %} - {% if remote_servers_plan_data[remote_server.id].current_plan %} + {% if remote_servers_support_data[remote_server.id].plan_data.current_plan %}
        {% with %} - {% set plan_data = remote_servers_plan_data[remote_server.id] %} + {% set plan_data = remote_servers_support_data[remote_server.id].plan_data %} {% include 'analytics/current_plan_details.html' %} {% endwith %}
        {% with %} - {% set current_plan = remote_servers_plan_data[remote_server.id].current_plan %} + {% set current_plan = remote_servers_support_data[remote_server.id].plan_data.current_plan %} {% set remote_id = remote_server.id %} {% set remote_type = "remote_server_id" %} {% include 'analytics/current_plan_forms_support.html' %} @@ -83,8 +82,8 @@

        On 100% sponsored Zulip Community plan.


        {% with %} - {% set plan_data = remote_realms_plan_data %} - {% set get_discount = get_discount %} + {% set support_data = remote_realms_support_data %} + {% set get_plan_type_name = get_plan_type_name %} {% include "analytics/remote_realm_details.html" %} {% endwith %}
        diff --git a/templates/analytics/sponsorship_forms_support.html b/templates/analytics/sponsorship_forms_support.html index e0cb18463b160..1c99267fc07ca 100644 --- a/templates/analytics/sponsorship_forms_support.html +++ b/templates/analytics/sponsorship_forms_support.html @@ -3,13 +3,13 @@ {{ csrf_input }} -{% if customer and customer.sponsorship_pending %} +{% if sponsorship_data.sponsorship_pending %}
        {{ csrf_input }} @@ -25,10 +25,29 @@ {{ csrf_input }} {% if has_fixed_price %} - + {% else %} - + {% endif %}
        + +{% if sponsorship_data.sponsorship_pending %} +
        +

        Sponsorship request information:

        + {% if sponsorship_data.latest_sponsorship_request %} +
          +
        • Organization type: {{ sponsorship_data.latest_sponsorship_request.org_type }}
        • +
        • Organization website: {{ sponsorship_data.latest_sponsorship_request.org_website }}
        • +
        • Organization description: {{ sponsorship_data.latest_sponsorship_request.org_description }}
        • +
        • Estimated total users: {{ sponsorship_data.latest_sponsorship_request.total_users }}
        • +
        • Paid users: {{ sponsorship_data.latest_sponsorship_request.paid_users }}
        • +
        • Description of paid users: {{ sponsorship_data.latest_sponsorship_request.paid_users_description }}
        • +
        • Requested plan: {{ sponsorship_data.latest_sponsorship_request.requested_plan }}
        • +
        + {% else %} + No sponsorship requests have been submitted.

        + {% endif %} +
        +{% endif %} diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index f067baf5ebd31..14784ea0f93d1 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -45,11 +45,18 @@

        {% if free_trial or downgrade_at_end_of_free_trial %} - {{ plan_name }} (free trial) + + {{ plan_name }} (free trial) + {% elif is_server_on_legacy_plan %} - {{ plan_name }} (legacy plan) + + {{ plan_name.replace(" (legacy plan)", "") -}} + + (legacy plan) {% else %} + {{ plan_name }} + {% endif %}
        @@ -99,13 +106,21 @@

        - {% if automanage_licenses %} + {% if is_server_on_legacy_plan %} + {% elif automanage_licenses %}
        {{ licenses }} (managed automatically) @@ -116,9 +131,15 @@

        @@ -180,13 +201,31 @@

        {% if renewal_amount %} {% if downgrade_at_end_of_cycle %} + {% if is_self_hosted_billing %} + Your organization will be downgraded to Self-managed at the end of the + current billing period ({{ renewal_date }}). You will lose access to the + benefits + of your current plan. For organizations with more than 10 users, this + includes losing access to the + Mobile Push Notification Service. + {% else %} Your organization will be downgraded to Zulip Cloud Free at the end of the current billing period ({{ renewal_date }}). You will lose access to unlimited search history and other features of your current plan. + {% endif %} {% elif downgrade_at_end_of_free_trial %} + {% if is_self_hosted_billing %} + Your organization will be downgraded to Self-managed at the end of the free trial + ({{ renewal_date }}). You will lose access to the + benefits + of your current plan. For organizations with more than 10 users, this + includes losing access to the + Mobile Push Notification Service. + {% else %} Your organization will be downgraded to Zulip Cloud Free at the end of the free trial ({{ renewal_date }}). You will lose access to unlimited search history and other features of your current plan. + {% endif %} {% else %} {% if charge_automatically %} {% if is_server_on_legacy_plan %} @@ -208,12 +247,13 @@

        {%- else %} {{ "1 month" if billing_frequency == "Monthly" else "12 months" }} {%- endif -%}) - {% if using_min_licenses_for_plan %} - (minimum purchase) - {% endif %} {% if discount_percent %}
        - Includes {{ discount_percent }}% discount. + Includes: {{ discount_percent }}% discount + {% endif %} + {% if using_min_licenses_for_plan %} +
        + Minimum purchase for this plan: 10 licenses {% endif %} {% endif %} {% endif %} @@ -253,7 +293,7 @@

        {% else %}
        - @@ -300,20 +340,34 @@

        Zulip Cloud billing for {{ org_name }}

        -

      - If you have any other questions, please don’t hesitate to reach out to - sales@zulip.com. + Please reach out to sales@zulip.com if you + have any further questions or would like to discuss Enterprise pricing.

      + +
      + {% include "corporate/comparison_table_self_hosted.html" %} +
      {% if not is_self_hosted_realm %} diff --git a/templates/corporate/pricing_model.html b/templates/corporate/pricing_model.html index 2ee36b0a205fa..f49ab9149c332 100644 --- a/templates/corporate/pricing_model.html +++ b/templates/corporate/pricing_model.html @@ -103,7 +103,7 @@

      Standard

      {% elif is_cloud_realm and sponsorship_pending %} - Sponsorship pending + Sponsorship requested {% else %} @@ -149,76 +149,124 @@

      Plus

      - {% if user_is_authenticated and not is_self_hosted_realm %} -
      - {% else %}

      Self-hosted

      Retain full control over your data.
      100% open-source software.

      - {% endif %}
      -

      Free

      +

      Self-managed

      +
      +
      Free
      +
      {% if is_self_hosted_realm and on_free_tier %} - + - Current plan - + Unlimited push notifications
      until February 15, 2024 + {% elif not is_self_hosted_realm %} Self-host Zulip + {% else %} +
      + {% endif %} +
      +
      +
      + +
      +
      +

      Community

      + +
      +
      +
      +
      +
      Free
      +
      + {% if is_self_hosted_realm and is_sponsored %} + + + Current plan + + {% elif is_self_hosted_realm and sponsorship_pending and requested_sponsorship_plan == "Community" %} + + Sponsorship requested + + {% elif is_self_hosted_realm %} + + Request sponsorship + + {% else %} + + Log in to apply + {% endif %}
      - {% if development_environment %}
      -

      Business

      +

      Business 10 users minimum

      +
      +
      $6.67
      +
      +

      + /user/month billed annually + or $8 billed monthly +

      +
      +
      {% if is_legacy_server_with_scheduled_upgrade and legacy_server_new_plan.tier == legacy_server_new_plan.TIER_SELF_HOSTED_BUSINESS %} Upgrade is scheduled - {% elif is_self_hosted_realm and on_free_tier and not sponsorship_pending %} - - Request sponsorship + {% elif is_self_hosted_realm and sponsorship_pending and requested_sponsorship_plan == "Business" %} + + Sponsorship requested + {% elif is_self_hosted_realm and on_free_tier and not sponsorship_pending %} {% if free_trial_days %} Start {{ free_trial_days }}-day free trial @@ -226,7 +274,7 @@

      Business

      Upgrade to Business {% endif %}
      - {% elif is_self_hosted_realm and (is_sponsored or (customer_plan and customer_plan.tier == customer_plan.TIER_SELF_HOSTED_BUSINESS)) %} + {% elif is_self_hosted_realm and customer_plan and customer_plan.tier == customer_plan.TIER_SELF_HOSTED_BUSINESS %} {% if on_free_trial %} @@ -235,14 +283,7 @@

      Business

      Current plan {% endif %}
      - {% elif is_self_hosted_realm and sponsorship_pending %} - - Sponsorship pending - {% elif is_self_hosted_realm %} - - Request sponsorship - {% if free_trial_days %} Start {{ free_trial_days }}-day free trial @@ -251,25 +292,27 @@

      Business

      {% endif %}
      {% else %} - - Contact sales + + Log in to upgrade {% endif %}
      - {% endif %}

      Enterprise

      diff --git a/templates/corporate/remote_billing_confirm_email_form.html b/templates/corporate/remote_billing_confirm_email_form.html index 83122de8a6a00..14d704b7879ef 100644 --- a/templates/corporate/remote_billing_confirm_email_form.html +++ b/templates/corporate/remote_billing_confirm_email_form.html @@ -2,14 +2,22 @@ {% set entrypoint = "upgrade" %} {% block title %} +{% if remote_server_hostname %} Enter log in email | Zulip +{% else %} +Enter email | Zulip +{% endif %} {% endblock %} {% block portico_content %}
    • {{/if}} - {{#if (and is_development_environment show_remote_billing) }} - {{! This is only shown in development environment until the UI is ready.}} + {{#if show_remote_billing }} {{/if}} @@ -145,7 +144,7 @@ {{!-- This will be hidden for self hosted realms since they will have corporate disabled. --}} {{/if}} diff --git a/web/templates/settings/organization_settings_admin.hbs b/web/templates/settings/organization_settings_admin.hbs index 7bb8db69f19f6..414699428ced7 100644 --- a/web/templates/settings/organization_settings_admin.hbs +++ b/web/templates/settings/organization_settings_admin.hbs @@ -139,7 +139,7 @@ {{t 'URL' }} + name="realm_jitsi_server_url_custom_input" class="realm_jitsi_server_url_custom_input settings_url_input" maxlength="200" /> diff --git a/web/tests/activity.test.js b/web/tests/activity.test.js index f13aba4f27ad7..b14d966ffe372 100644 --- a/web/tests/activity.test.js +++ b/web/tests/activity.test.js @@ -3,14 +3,14 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, with_overrides, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {page_params, user_settings} = require("./lib/zpage_params"); const $window_stub = $.create("window-stub"); set_global("to_$", () => $window_stub); -$(window).idle = () => {}; +$(window).idle = noop; const _document = { hasFocus() { @@ -212,7 +212,7 @@ test("huddle_data.process_loaded_messages", () => { }); test("presence_list_full_update", ({override, mock_template}) => { - override(padded_widget, "update_padding", () => {}); + override(padded_widget, "update_padding", noop); mock_template("presence_rows.hbs", false, (data) => { assert.equal(data.presence_rows.length, 7); assert.equal(data.presence_rows[0].user_id, me.user_id); @@ -275,17 +275,17 @@ test("direct_message_update_dom_counts", () => { test("handlers", ({override, override_rewire, mock_template}) => { let filter_key_handlers; - mock_template("presence_rows.hbs", false, () => {}); + mock_template("presence_rows.hbs", false, noop); override(keydown_util, "handle", (opts) => { filter_key_handlers = opts.handlers; }); - override(scroll_util, "scroll_element_into_container", () => {}); - override(padded_widget, "update_padding", () => {}); - override(popovers, "hide_all", () => {}); - override(sidebar_ui, "hide_all", () => {}); - override(sidebar_ui, "show_userlist_sidebar", () => {}); - override(resize, "resize_sidebars", () => {}); + override(scroll_util, "scroll_element_into_container", noop); + override(padded_widget, "update_padding", noop); + override(popovers, "hide_all", noop); + override(sidebar_ui, "hide_all", noop); + override(sidebar_ui, "show_userlist_sidebar", noop); + override(resize, "resize_sidebars", noop); // This is kind of weak coverage; we are mostly making sure that // keys and clicks got mapped to functions that don't crash. @@ -306,12 +306,12 @@ test("handlers", ({override, override_rewire, mock_template}) => { all_user_ids: [me.user_id, alice.user_id, fred.user_id], }); - buddy_list.start_scroll_handler = () => {}; - override_rewire(util, "call_function_periodically", () => {}); - override_rewire(activity, "send_presence_to_server", () => {}); + buddy_list.start_scroll_handler = noop; + override_rewire(util, "call_function_periodically", noop); + override_rewire(activity, "send_presence_to_server", noop); activity_ui.initialize({narrow_by_email}); - $("#buddy-list-users-matching-view").empty = () => {}; + $("#buddy-list-users-matching-view").empty = noop; $me_li = $.create("me stub"); $alice_li = $.create("alice stub"); @@ -435,13 +435,13 @@ test("first/prev/next", ({override, mock_template}) => { } }); - override(padded_widget, "update_padding", () => {}); + override(padded_widget, "update_padding", noop); assert.equal(buddy_list.first_key(), undefined); assert.equal(buddy_list.prev_key(alice.user_id), undefined); assert.equal(buddy_list.next_key(alice.user_id), undefined); - override(buddy_list.$container, "append", () => {}); + override(buddy_list.$container, "append", noop); activity_ui.redraw_user(alice.user_id); activity_ui.redraw_user(fred.user_id); @@ -503,7 +503,7 @@ test("insert_one_user_into_empty_list", ({override, mock_template}) => { return html; }); - override(padded_widget, "update_padding", () => {}); + override(padded_widget, "update_padding", noop); let appended_html; override(buddy_list.$container, "append", (html) => { @@ -522,7 +522,7 @@ test("insert_alice_then_fred", ({override, mock_template}) => { override(buddy_list.$container, "append", (html) => { appended_html = html; }); - override(padded_widget, "update_padding", () => {}); + override(padded_widget, "update_padding", noop); activity_ui.redraw_user(alice.user_id); assert.ok(appended_html.indexOf('data-user-id="1"') > 0); @@ -540,7 +540,7 @@ test("insert_fred_then_alice_then_rename", ({override, mock_template}) => { override(buddy_list.$container, "append", (html) => { appended_html = html; }); - override(padded_widget, "update_padding", () => {}); + override(padded_widget, "update_padding", noop); activity_ui.redraw_user(fred.user_id); assert.ok(appended_html.indexOf('data-user-id="2"') > 0); @@ -609,7 +609,7 @@ test("redraw_muted_user", () => { }); test("update_presence_info", ({override}) => { - override(pm_list, "update_private_messages", () => {}); + override(pm_list, "update_private_messages", noop); page_params.realm_presence_disabled = false; page_params.server_presence_ping_interval_seconds = 60; @@ -661,10 +661,10 @@ test("update_presence_info", ({override}) => { }); test("initialize", ({override, mock_template}) => { - mock_template("presence_rows.hbs", false, () => {}); - override(padded_widget, "update_padding", () => {}); - override(pm_list, "update_private_messages", () => {}); - override(watchdog, "check_for_unsuspend", () => {}); + mock_template("presence_rows.hbs", false, noop); + override(padded_widget, "update_padding", noop); + override(pm_list, "update_private_messages", noop); + override(watchdog, "check_for_unsuspend", noop); let payload; override(channel, "post", (arg) => { @@ -678,7 +678,7 @@ test("initialize", ({override, mock_template}) => { function clear() { $.clear_all_elements(); buddy_list.$container = $("#buddy-list-users-matching-view"); - buddy_list.$container.append = () => {}; + buddy_list.$container.append = noop; clear_buddy_list(); page_params.presences = {}; } @@ -750,7 +750,7 @@ test("initialize", ({override, mock_template}) => { }); test("electron_bridge", ({override_rewire}) => { - override_rewire(activity, "send_presence_to_server", () => {}); + override_rewire(activity, "send_presence_to_server", noop); function with_bridge_idle(bridge_idle, f) { with_overrides(({override}) => { diff --git a/web/tests/alert_words_ui.test.js b/web/tests/alert_words_ui.test.js index 6efcdf6ccfc11..e4f1d41f2a567 100644 --- a/web/tests/alert_words_ui.test.js +++ b/web/tests/alert_words_ui.test.js @@ -4,7 +4,7 @@ const {strict: assert} = require("assert"); const {$t} = require("./lib/i18n"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const channel = mock_esm("../src/channel"); @@ -15,7 +15,6 @@ const alert_words_ui = zrequire("alert_words_ui"); alert_words.initialize({ alert_words: ["foo", "bar"], }); -const noop = () => {}; run_test("rerender_alert_words_ui", ({mock_template}) => { let list_widget_create_called = false; @@ -45,7 +44,7 @@ run_test("rerender_alert_words_ui", ({mock_template}) => { }); run_test("remove_alert_word", ({override_rewire}) => { - override_rewire(alert_words_ui, "rerender_alert_words_ui", () => {}); + override_rewire(alert_words_ui, "rerender_alert_words_ui", noop); alert_words_ui.set_up_alert_words(); const $word_list = $("#alert-words-table"); @@ -95,7 +94,7 @@ run_test("remove_alert_word", ({override_rewire}) => { }); run_test("close_status_message", ({override_rewire}) => { - override_rewire(alert_words_ui, "rerender_alert_words_ui", () => {}); + override_rewire(alert_words_ui, "rerender_alert_words_ui", noop); alert_words_ui.set_up_alert_words(); const $alert_word_settings = $("#alert-word-settings"); diff --git a/web/tests/bot_data.test.js b/web/tests/bot_data.test.js index e6b6c0354321a..f4bd9946dd14c 100644 --- a/web/tests/bot_data.test.js +++ b/web/tests/bot_data.test.js @@ -10,7 +10,7 @@ const bot_data = zrequire("bot_data"); const people = zrequire("people"); // Bot types and service bot types can be found -// in zerver/models.py - UserProfile Class or +// in zerver/models/users.py - UserProfile Class or // zever/openapi/zulip.yaml const me = { diff --git a/web/tests/buddy_list.test.js b/web/tests/buddy_list.test.js index 0c536ae94a1d7..8d60a383ff690 100644 --- a/web/tests/buddy_list.test.js +++ b/web/tests/buddy_list.test.js @@ -5,7 +5,7 @@ const {strict: assert} = require("assert"); const _ = require("lodash"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); @@ -69,7 +69,7 @@ run_test("basics", ({override}) => { }); override(message_viewport, "height", () => 550); - override(padded_widget, "update_padding", () => {}); + override(padded_widget, "update_padding", noop); let appended; $("#buddy-list-users-matching-view").append = (html) => { diff --git a/web/tests/common.test.js b/web/tests/common.test.js index 326b41f4cb4e3..d9be65885f8c7 100644 --- a/web/tests/common.test.js +++ b/web/tests/common.test.js @@ -3,11 +3,9 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); -const noop = () => {}; - mock_esm("tippy.js", { default(arg) { arg._tippy = {setContent: noop}; @@ -224,7 +222,7 @@ run_test("adjust_mac_tooltip_keys mac random", ({override}) => { run_test("show password", () => { const password_selector = "#id_password ~ .password_visibility_toggle"; - $(password_selector)[0] = () => {}; + $(password_selector)[0] = noop; function set_attribute(type) { $("#id_password").attr("type", type); diff --git a/web/tests/components.test.js b/web/tests/components.test.js index 521cfdefafed7..650f219d41984 100644 --- a/web/tests/components.test.js +++ b/web/tests/components.test.js @@ -4,7 +4,7 @@ const {strict: assert} = require("assert"); const {$t} = require("./lib/i18n"); const {mock_jquery, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); let env; @@ -166,8 +166,6 @@ mock_jquery((sel) => { const components = zrequire("components"); -const noop = () => {}; - const LEFT_KEY = {key: "ArrowLeft", preventDefault: noop, stopPropagation: noop}; const RIGHT_KEY = {key: "ArrowRight", preventDefault: noop, stopPropagation: noop}; diff --git a/web/tests/compose.test.js b/web/tests/compose.test.js index 886552b10b022..87fb08ebbb193 100644 --- a/web/tests/compose.test.js +++ b/web/tests/compose.test.js @@ -8,14 +8,12 @@ const {mock_stream_header_colorblock} = require("./lib/compose"); const {mock_banners} = require("./lib/compose_banner"); const {$t} = require("./lib/i18n"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params, user_settings} = require("./lib/zpage_params"); const settings_config = zrequire("settings_config"); -const noop = () => {}; - set_global("document", { querySelector() {}, }); @@ -29,8 +27,8 @@ set_global( const fake_now = 555; -const autosize = () => {}; -autosize.update = () => {}; +const autosize = noop; +autosize.update = noop; mock_esm("autosize", {default: autosize}); const channel = mock_esm("../src/channel"); @@ -125,8 +123,8 @@ function test_ui(label, f) { function initialize_handlers({override}) { override(page_params, "realm_available_video_chat_providers", {disabled: {id: 0}}); override(page_params, "realm_video_chat_provider", 0); - override(upload, "feature_check", () => {}); - override(resize, "watch_manual_resize", () => {}); + override(upload, "feature_check", noop); + override(resize, "watch_manual_resize", noop); compose_setup.initialize(); } @@ -237,8 +235,8 @@ test_ui("send_message", ({override, override_rewire, mock_template}) => { override(compose_pm_pill, "get_emails", () => "alice@example.com"); const server_message_id = 127; - override(markdown, "apply_markdown", () => {}); - override(markdown, "add_topic_links", () => {}); + override(markdown, "apply_markdown", noop); + override(markdown, "add_topic_links", noop); override_rewire(echo, "try_deliver_locally", (message_request) => { const local_id_float = 123.04; @@ -336,7 +334,7 @@ test_ui("send_message", ({override, override_rewire, mock_template}) => { $(".compose-submit-button .loader").show(); $("textarea#compose-textarea").off("select"); echo_error_msg_checked = false; - override_rewire(echo, "try_deliver_locally", () => {}); + override_rewire(echo, "try_deliver_locally", noop); override(sent_messages, "get_new_local_id", () => "loc-55"); @@ -360,7 +358,7 @@ test_ui("enter_with_preview_open", ({override, override_rewire}) => { mock_banners(); $("textarea#compose-textarea").toggleClass = noop; mock_stream_header_colorblock(); - override_rewire(compose_banner, "clear_message_sent_banners", () => {}); + override_rewire(compose_banner, "clear_message_sent_banners", noop); override(document, "to_$", () => $("document-stub")); let show_button_spinner_called = false; override(loading, "show_button_spinner", ($spinner) => { @@ -411,7 +409,7 @@ test_ui("finish", ({override, override_rewire}) => { mock_banners(); mock_stream_header_colorblock(); - override_rewire(compose_banner, "clear_message_sent_banners", () => {}); + override_rewire(compose_banner, "clear_message_sent_banners", noop); override(document, "to_$", () => $("document-stub")); let show_button_spinner_called = false; override(loading, "show_button_spinner", ($spinner) => { @@ -500,7 +498,7 @@ test_ui("initialize", ({override}) => { uppy_cancel_all_called = true; }, }); - override(upload, "feature_check", () => {}); + override(upload, "feature_check", noop); compose_setup.initialize(); @@ -607,7 +605,7 @@ test_ui("on_events", ({override, override_rewire}) => { initialize_handlers({override}); - override(rendered_markdown, "update_elements", () => {}); + override(rendered_markdown, "update_elements", noop); (function test_attach_files_compose_clicked() { const handler = $("#compose").get_on_handler("click", ".compose_upload_file"); diff --git a/web/tests/compose_actions.test.js b/web/tests/compose_actions.test.js index 3fbfb1ea91f6b..9cc5baa5ec159 100644 --- a/web/tests/compose_actions.test.js +++ b/web/tests/compose_actions.test.js @@ -5,20 +5,18 @@ const {strict: assert} = require("assert"); const {mock_stream_header_colorblock} = require("./lib/compose"); const {mock_banners} = require("./lib/compose_banner"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); const settings_config = zrequire("settings_config"); -const noop = () => {}; - set_global("document", { to_$: () => $("document-stub"), }); -const autosize = () => {}; -autosize.update = () => {}; +const autosize = noop; +autosize.update = noop; mock_esm("autosize", {default: autosize}); const channel = mock_esm("../src/channel"); @@ -98,8 +96,8 @@ function override_private_message_recipient({override}) { function test(label, f) { run_test(label, (helpers) => { // We don't test the css calls; we just skip over them. - $("#compose").css = () => {}; - $(".new_message_textarea").css = () => {}; + $("#compose").css = noop; + $(".new_message_textarea").css = noop; people.init(); compose_state.set_message_type(false); @@ -116,14 +114,14 @@ test("initial_state", () => { test("start", ({override, override_rewire, mock_template}) => { mock_banners(); override_private_message_recipient({override}); - override_rewire(compose_actions, "autosize_message_content", () => {}); - override_rewire(compose_actions, "expand_compose_box", () => {}); - override_rewire(compose_actions, "complete_starting_tasks", () => {}); - override_rewire(compose_actions, "blur_compose_inputs", () => {}); - override_rewire(compose_actions, "clear_textarea", () => {}); - override_rewire(compose_recipient, "on_compose_select_recipient_update", () => {}); - override_rewire(compose_recipient, "check_posting_policy_for_compose_box", () => {}); - mock_template("inline_decorated_stream_name.hbs", false, () => {}); + override_rewire(compose_actions, "autosize_message_content", noop); + override_rewire(compose_actions, "expand_compose_box", noop); + override_rewire(compose_actions, "complete_starting_tasks", noop); + override_rewire(compose_actions, "blur_compose_inputs", noop); + override_rewire(compose_actions, "clear_textarea", noop); + override_rewire(compose_recipient, "on_compose_select_recipient_update", noop); + override_rewire(compose_recipient, "check_posting_policy_for_compose_box", noop); + mock_template("inline_decorated_stream_name.hbs", false, noop); mock_stream_header_colorblock(); let compose_defaults; @@ -244,12 +242,12 @@ test("start", ({override, override_rewire, mock_template}) => { test("respond_to_message", ({override, override_rewire, mock_template}) => { mock_banners(); - override_rewire(compose_actions, "complete_starting_tasks", () => {}); - override_rewire(compose_actions, "clear_textarea", () => {}); + override_rewire(compose_actions, "complete_starting_tasks", noop); + override_rewire(compose_actions, "clear_textarea", noop); override_rewire(compose_recipient, "on_compose_select_recipient_update", noop); override_rewire(compose_recipient, "check_posting_policy_for_compose_box", noop); override_private_message_recipient({override}); - mock_template("inline_decorated_stream_name.hbs", false, () => {}); + mock_template("inline_decorated_stream_name.hbs", false, noop); mock_stream_header_colorblock(); // Test direct message @@ -300,12 +298,12 @@ test("reply_with_mention", ({override, override_rewire, mock_template}) => { mock_banners(); mock_stream_header_colorblock(); compose_state.set_message_type("stream"); - override_rewire(compose_recipient, "on_compose_select_recipient_update", () => {}); - override_rewire(compose_actions, "complete_starting_tasks", () => {}); - override_rewire(compose_actions, "clear_textarea", () => {}); + override_rewire(compose_recipient, "on_compose_select_recipient_update", noop); + override_rewire(compose_actions, "complete_starting_tasks", noop); + override_rewire(compose_actions, "clear_textarea", noop); override_private_message_recipient({override}); override_rewire(compose_recipient, "check_posting_policy_for_compose_box", noop); - mock_template("inline_decorated_stream_name.hbs", false, () => {}); + mock_template("inline_decorated_stream_name.hbs", false, noop); const denmark = { subscribed: true, @@ -368,8 +366,8 @@ test("quote_and_reply", ({disallow, override, override_rewire}) => { }; people.add_active_user(steve); - override_rewire(compose_actions, "complete_starting_tasks", () => {}); - override_rewire(compose_actions, "clear_textarea", () => {}); + override_rewire(compose_actions, "complete_starting_tasks", noop); + override_rewire(compose_actions, "clear_textarea", noop); override_private_message_recipient({override}); let selected_message; diff --git a/web/tests/compose_closed_ui.test.js b/web/tests/compose_closed_ui.test.js index b69709bea07b4..92b07c5d0eb8f 100644 --- a/web/tests/compose_closed_ui.test.js +++ b/web/tests/compose_closed_ui.test.js @@ -4,10 +4,9 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); -const noop = () => {}; // Mocking and stubbing things set_global("document", "document-stub"); const message_lists = mock_esm("../src/message_lists"); diff --git a/web/tests/compose_state.test.js b/web/tests/compose_state.test.js index 564156757537d..c2057138cc874 100644 --- a/web/tests/compose_state.test.js +++ b/web/tests/compose_state.test.js @@ -4,7 +4,7 @@ const {strict: assert} = require("assert"); const {mock_stream_header_colorblock} = require("./lib/compose"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const compose_pm_pill = mock_esm("../src/compose_pm_pill"); @@ -12,8 +12,6 @@ const compose_pm_pill = mock_esm("../src/compose_pm_pill"); const compose_state = zrequire("compose_state"); const stream_data = zrequire("stream_data"); -const noop = () => {}; - run_test("private_message_recipient", ({override}) => { let emails; override(compose_pm_pill, "set_from_emails", (value) => { diff --git a/web/tests/compose_ui.test.js b/web/tests/compose_ui.test.js index cbd325cd53ee0..5271d39ab9380 100644 --- a/web/tests/compose_ui.test.js +++ b/web/tests/compose_ui.test.js @@ -4,16 +4,14 @@ const {strict: assert} = require("assert"); const {$t} = require("./lib/i18n"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); -const noop = () => {}; - set_global("navigator", {}); -const autosize = () => {}; -autosize.update = () => {}; +const autosize = noop; +autosize.update = noop; mock_esm("autosize", {default: autosize}); mock_esm("../src/message_lists", { diff --git a/web/tests/compose_validate.test.js b/web/tests/compose_validate.test.js index c802c3f960ff5..06d756344e943 100644 --- a/web/tests/compose_validate.test.js +++ b/web/tests/compose_validate.test.js @@ -5,7 +5,7 @@ const {strict: assert} = require("assert"); const {mock_banners} = require("./lib/compose_banner"); const {$t} = require("./lib/i18n"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); @@ -164,13 +164,13 @@ test_ui("validate", ({mock_template}) => { $("#private_message_recipient")[0] = {}; $("#private_message_recipient").set_parent($pm_pill_container); $pm_pill_container.set_find_results(".input", $("#private_message_recipient")); - $("#private_message_recipient").before = () => {}; + $("#private_message_recipient").before = noop; compose_pm_pill.initialize({ on_pill_create_or_remove: compose_recipient.update_placeholder_text, }); - $("#zephyr-mirror-error").is = () => {}; + $("#zephyr-mirror-error").is = noop; mock_template("input_pill.hbs", false, () => "
      pill-html
      "); diff --git a/web/tests/compose_video.test.js b/web/tests/compose_video.test.js index a4e883ec51ed1..de7fe4b3a9304 100644 --- a/web/tests/compose_video.test.js +++ b/web/tests/compose_video.test.js @@ -4,7 +4,7 @@ const {strict: assert} = require("assert"); const events = require("./lib/events"); const {mock_esm, set_global, with_overrides, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); @@ -70,7 +70,7 @@ function test(label, f) { test("videos", ({override}) => { page_params.realm_video_chat_provider = realm_available_video_chat_providers.disabled.id; - override(upload, "feature_check", () => {}); + override(upload, "feature_check", noop); stub_out_video_calls(); @@ -247,7 +247,7 @@ test("videos", ({override}) => { }); test("test_video_chat_button_toggle disabled", ({override}) => { - override(upload, "feature_check", () => {}); + override(upload, "feature_check", noop); page_params.realm_video_chat_provider = realm_available_video_chat_providers.disabled.id; compose_setup.initialize(); @@ -255,7 +255,7 @@ test("test_video_chat_button_toggle disabled", ({override}) => { }); test("test_video_chat_button_toggle no url", ({override}) => { - override(upload, "feature_check", () => {}); + override(upload, "feature_check", noop); page_params.realm_video_chat_provider = realm_available_video_chat_providers.jitsi_meet.id; page_params.jitsi_server_url = null; @@ -264,7 +264,7 @@ test("test_video_chat_button_toggle no url", ({override}) => { }); test("test_video_chat_button_toggle enabled", ({override}) => { - override(upload, "feature_check", () => {}); + override(upload, "feature_check", noop); page_params.realm_video_chat_provider = realm_available_video_chat_providers.jitsi_meet.id; page_params.realm_jitsi_server_url = "https://meet.jit.si"; diff --git a/web/tests/composebox_typeahead.test.js b/web/tests/composebox_typeahead.test.js index 3fac8d8f4aecd..adb613827ca8b 100644 --- a/web/tests/composebox_typeahead.test.js +++ b/web/tests/composebox_typeahead.test.js @@ -5,12 +5,10 @@ const {strict: assert} = require("assert"); const {mock_stream_header_colorblock} = require("./lib/compose"); const {mock_banners} = require("./lib/compose_banner"); const {mock_esm, set_global, with_overrides, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params, user_settings} = require("./lib/zpage_params"); -const noop = () => {}; - let autosize_called; const compose_ui = mock_esm("../src/compose_ui", { @@ -487,7 +485,7 @@ test("content_typeahead_selected", ({override}) => { // mention fake_this.completing = "mention"; - override(compose_validate, "warn_if_mentioning_unsubscribed_user", () => {}); + override(compose_validate, "warn_if_mentioning_unsubscribed_user", noop); override( compose_validate, "convert_mentions_to_silent_in_direct_messages", @@ -755,7 +753,7 @@ test("initialize", ({override, override_rewire, mock_template}) => { assert.equal(typeof data.has_image, "boolean"); return html; }); - override(stream_topic_history_util, "get_server_history", () => {}); + override(stream_topic_history_util, "get_server_history", noop); let topic_typeahead_called = false; $("input#stream_message_recipient_topic").typeahead = (options) => { @@ -1307,7 +1305,7 @@ test("begins_typeahead", ({override, override_rewire}) => { assert.equal(stream_id, sweden_stream.stream_id); return sweden_topics_to_show; }); - override(stream_topic_history_util, "get_server_history", () => {}); + override(stream_topic_history_util, "get_server_history", noop); const begin_typehead_this = { options: { diff --git a/web/tests/dispatch.test.js b/web/tests/dispatch.test.js index 1da9b79cb7abc..6ce0ddbd1c6fc 100644 --- a/web/tests/dispatch.test.js +++ b/web/tests/dispatch.test.js @@ -5,13 +5,11 @@ const {strict: assert} = require("assert"); const events = require("./lib/events"); const {mock_esm, set_global, with_overrides, zrequire} = require("./lib/namespace"); const {make_stub} = require("./lib/stub"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {page_params, realm_user_settings_defaults, user_settings} = require("./lib/zpage_params"); -const noop = () => {}; - const event_fixtures = events.fixtures; const test_message = events.test_message; const test_user = events.test_user; @@ -593,7 +591,7 @@ run_test("realm_bot add", ({override}) => { const event = event_fixtures.realm_bot__add; const bot_stub = make_stub(); override(bot_data, "add", bot_stub.f); - override(settings_bots, "render_bots", () => {}); + override(settings_bots, "render_bots", noop); dispatch(event); assert.equal(bot_stub.num_calls, 1); @@ -605,7 +603,7 @@ run_test("realm_bot delete", ({override}) => { const event = event_fixtures.realm_bot__delete; const bot_stub = make_stub(); override(bot_data, "del", bot_stub.f); - override(settings_bots, "render_bots", () => {}); + override(settings_bots, "render_bots", noop); dispatch(event); assert.equal(bot_stub.num_calls, 1); @@ -617,7 +615,7 @@ run_test("realm_bot update", ({override}) => { const event = event_fixtures.realm_bot__update; const bot_stub = make_stub(); override(bot_data, "update", bot_stub.f); - override(settings_bots, "render_bots", () => {}); + override(settings_bots, "render_bots", noop); dispatch(event); @@ -834,7 +832,7 @@ run_test("stream_typing", ({override}) => { }); run_test("user_settings", ({override}) => { - settings_display.set_default_language_name = () => {}; + settings_display.set_default_language_name = noop; let event = event_fixtures.user_settings__default_language; user_settings.default_language = "en"; override(settings_display, "update_page", noop); diff --git a/web/tests/dispatch_subs.test.js b/web/tests/dispatch_subs.test.js index 2fa0e05c8e7b4..4397d1e4932c0 100644 --- a/web/tests/dispatch_subs.test.js +++ b/web/tests/dispatch_subs.test.js @@ -5,7 +5,7 @@ const {strict: assert} = require("assert"); const events = require("./lib/events"); const {mock_esm, zrequire} = require("./lib/namespace"); const {make_stub} = require("./lib/stub"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const {page_params} = require("./lib/zpage_params"); @@ -30,8 +30,6 @@ const server_events_dispatch = zrequire("server_events_dispatch"); const stream_data = zrequire("stream_data"); const sub_store = zrequire("sub_store"); -const noop = () => {}; - people.add_active_user(test_user); const me = { @@ -137,7 +135,7 @@ test("add error handling", () => { }); test("peer event error handling (bad stream_ids/user_ids)", ({override}) => { - override(stream_events, "process_subscriber_update", () => {}); + override(stream_events, "process_subscriber_update", noop); const add_event = { type: "subscription", @@ -260,7 +258,7 @@ test("stream delete (special streams)", ({override}) => { }); test("stream delete (stream is selected in compose)", ({override, override_rewire}) => { - override_rewire(compose_recipient, "on_compose_select_recipient_update", () => {}); + override_rewire(compose_recipient, "on_compose_select_recipient_update", noop); const event = event_fixtures.stream__delete; diff --git a/web/tests/drafts.test.js b/web/tests/drafts.test.js index 9d830d923dd5e..a7950333482c3 100644 --- a/web/tests/drafts.test.js +++ b/web/tests/drafts.test.js @@ -5,7 +5,7 @@ const {strict: assert} = require("assert"); const {mock_stream_header_colorblock} = require("./lib/compose"); const {mock_banners} = require("./lib/compose_banner"); const {mock_esm, set_global, zrequire, with_overrides} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {user_settings} = require("./lib/zpage_params"); @@ -25,8 +25,6 @@ const aaron = { }; people.add_active_user(aaron); -const noop = () => {}; - const setTimeout_delay = 3000; set_global("setTimeout", (f, delay) => { assert.equal(delay, setTimeout_delay); @@ -88,7 +86,7 @@ const short_msg = { function test(label, f) { run_test(label, (helpers) => { - $("#draft_overlay").css = () => {}; + $("#draft_overlay").css = noop; window.localStorage.clear(); f(helpers); }); @@ -609,7 +607,7 @@ test("format_drafts", ({override_rewire, mock_template}) => { $.clear_all_elements(); $.create("#drafts_table .overlay-message-row", {children: []}); - $("#draft_overlay").css = () => {}; + $("#draft_overlay").css = noop; override_rewire(sub_store, "get", (stream_id) => { assert.ok([30, 40].includes(stream_id)); diff --git a/web/tests/echo.test.js b/web/tests/echo.test.js index a89bfa1701add..61666909d3c7c 100644 --- a/web/tests/echo.test.js +++ b/web/tests/echo.test.js @@ -6,7 +6,7 @@ const MockDate = require("mockdate"); const {mock_esm, zrequire} = require("./lib/namespace"); const {make_stub} = require("./lib/stub"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const {page_params} = require("./lib/zpage_params"); const compose_notifications = mock_esm("../src/compose_notifications"); @@ -34,7 +34,6 @@ const message_store = mock_esm("../src/message_store", { set_message_booleans() {}, }); -const noop = () => {}; message_lists.current = { view: { rerender_messages: noop, @@ -307,8 +306,8 @@ run_test("insert_local_message direct message", ({override}) => { run_test("test reify_message_id", ({override}) => { const local_id_float = 103.01; - override(markdown, "apply_markdown", () => {}); - override(markdown, "add_topic_links", () => {}); + override(markdown, "apply_markdown", noop); + override(markdown, "add_topic_links", noop); const message_request = { type: "stream", @@ -318,7 +317,7 @@ run_test("test reify_message_id", ({override}) => { sender_id: 123, draft_id: 100, }; - echo.insert_local_message(message_request, local_id_float, () => {}); + echo.insert_local_message(message_request, local_id_float, noop); let message_store_reify_called = false; let notifications_reify_called = false; diff --git a/web/tests/example1.test.js b/web/tests/example1.test.js index 1c0356bb42465..326945422c977 100644 --- a/web/tests/example1.test.js +++ b/web/tests/example1.test.js @@ -39,7 +39,7 @@ const isaac = { full_name: "Isaac Newton", }; -// The `people`object is a very fundamental object in the +// The `people` object is a very fundamental object in the // Zulip app. You can learn a lot more about it by reading // the tests in people.test.js in the same directory as this file. diff --git a/web/tests/example4.test.js b/web/tests/example4.test.js index 2b5fa7164f02e..9fca1cdc8eabf 100644 --- a/web/tests/example4.test.js +++ b/web/tests/example4.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); /* @@ -84,7 +84,7 @@ run_test("add users with event", ({override}) => { // We need to override a stub here before dispatching the event. // Keep reading to see how overriding works! - override(settings_users, "redraw_bots_list", () => {}); + override(settings_users, "redraw_bots_list", noop); // Let's simulate dispatching our event! server_events_dispatch.dispatch_normal_event(event); @@ -132,11 +132,11 @@ run_test("update user with event", ({override}) => { // verify that they run. Fortunately, the run_test() // wrapper will tell us if we override a method that // doesn't get called! - override(activity_ui, "redraw", () => {}); - override(message_live_update, "update_user_full_name", () => {}); - override(pm_list, "update_private_messages", () => {}); - override(settings_users, "update_user_data", () => {}); - override(settings_users, "update_bot_data", () => {}); + override(activity_ui, "redraw", noop); + override(message_live_update, "update_user_full_name", noop); + override(pm_list, "update_private_messages", noop); + override(settings_users, "update_user_data", noop); + override(settings_users, "update_bot_data", noop); // Dispatch the realm_user/update event, which will update // data structures and have other side effects that are diff --git a/web/tests/example5.test.js b/web/tests/example5.test.js index 4ebbaf3ec99bb..6c55c5cea9cfc 100644 --- a/web/tests/example5.test.js +++ b/web/tests/example5.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); /* Our test from an earlier example verifies that the update events @@ -85,7 +85,7 @@ function test_helper({override}) { run_test("insert_message", ({override}) => { message_store.clear_for_testing(); - override(pm_list, "update_private_messages", () => {}); + override(pm_list, "update_private_messages", noop); const helper = test_helper({override}); diff --git a/web/tests/example6.test.js b/web/tests/example6.test.js index ec8ef0b940fea..b7a3c1f3beabf 100644 --- a/web/tests/example6.test.js +++ b/web/tests/example6.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {make_stub} = require("./lib/stub"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); /* The previous example was a bit extreme. Generally we just @@ -43,8 +43,8 @@ run_test("explore make_stub", ({override}) => { // effects get in the way. We have to override them to do // the simple test here. - override(app, "notify_server_of_deposit", () => {}); - override(app, "pop_up_fancy_confirmation_screen", () => {}); + override(app, "notify_server_of_deposit", noop); + override(app, "pop_up_fancy_confirmation_screen", noop); deposit_paycheck(10); assert.equal(balance, 50); diff --git a/web/tests/example7.test.js b/web/tests/example7.test.js index f6d7ce9cb9a1e..57b3aca58ec5d 100644 --- a/web/tests/example7.test.js +++ b/web/tests/example7.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); /* @@ -106,11 +106,11 @@ run_test("unread_ops", ({override}) => { override(message_lists.current, "all_messages", () => test_messages); // Ignore these interactions for now: - override(message_lists.current.view, "show_message_as_read", () => {}); - override(message_lists.home.view, "show_message_as_read", () => {}); - override(desktop_notifications, "close_notification", () => {}); - override(unread_ui, "update_unread_counts", () => {}); - override(unread_ui, "notify_messages_remain_unread", () => {}); + override(message_lists.current.view, "show_message_as_read", noop); + override(message_lists.home.view, "show_message_as_read", noop); + override(desktop_notifications, "close_notification", noop); + override(unread_ui, "update_unread_counts", noop); + override(unread_ui, "notify_messages_remain_unread", noop); // Set up a way to capture the options passed in to channel.post. let channel_post_opts; diff --git a/web/tests/input_pill.test.js b/web/tests/input_pill.test.js index 84bfe23345674..82ecff5c7b62d 100644 --- a/web/tests/input_pill.test.js +++ b/web/tests/input_pill.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); @@ -11,7 +11,6 @@ set_global("document", {}); class ClipboardEvent {} set_global("ClipboardEvent", ClipboardEvent); -const noop = () => {}; const example_img_link = "http://example.com/example.png"; mock_esm("../src/ui_util", { @@ -114,7 +113,7 @@ function set_up() { const $pill_input = $.create("pill_input"); $pill_input[0] = {}; - $pill_input.before = () => {}; + $pill_input.before = noop; const create_item_from_text = (text) => items[text]; @@ -503,7 +502,7 @@ run_test("exit button on pill", ({mock_template}) => { const pills = widget._get_pills_for_testing(); for (const pill of pills) { - pill.$element.remove = () => {}; + pill.$element.remove = noop; } let next_pill_focused = false; @@ -613,7 +612,7 @@ run_test("appendValue/clear", ({mock_template}) => { get_text_from_item: /* istanbul ignore next */ (s) => s.display_value, }; - $pill_input.before = () => {}; + $pill_input.before = noop; $pill_input[0] = {}; const widget = input_pill.create(config); diff --git a/web/tests/lib/compose.js b/web/tests/lib/compose.js index c98f551e8cf41..ef87cddd1626f 100644 --- a/web/tests/lib/compose.js +++ b/web/tests/lib/compose.js @@ -1,11 +1,12 @@ "use strict"; +const {noop} = require("./test"); const $ = require("./zjquery"); exports.mock_stream_header_colorblock = () => { const $stream_selection_dropdown = $("#compose_select_recipient_widget_wrapper"); const $stream_header_colorblock = $(".stream_header_colorblock"); - $("#compose_select_recipient_widget_wrapper .stream_header_colorblock").css = () => {}; + $("#compose_select_recipient_widget_wrapper .stream_header_colorblock").css = noop; $stream_selection_dropdown.set_find_results( ".stream_header_colorblock", $stream_header_colorblock, diff --git a/web/tests/lib/compose_banner.js b/web/tests/lib/compose_banner.js index d3c5bdda6470e..abcd66bfe687d 100644 --- a/web/tests/lib/compose_banner.js +++ b/web/tests/lib/compose_banner.js @@ -2,6 +2,7 @@ const compose_banner = require("../../src/compose_banner"); +const {noop} = require("./test"); const $ = require("./zjquery"); exports.mock_banners = () => { @@ -13,16 +14,16 @@ exports.mock_banners = () => { .split(" ") .map((classname) => CSS.escape(classname)) .join(".")}`, - ).remove = () => {}; + ).remove = noop; } - $("#compose_banners .warning").remove = () => {}; - $("#compose_banners .error").remove = () => {}; - $("#compose_banners .upload_banner").remove = () => {}; + $("#compose_banners .warning").remove = noop; + $("#compose_banners .error").remove = noop; + $("#compose_banners .upload_banner").remove = noop; const $stub = $.create("stub_to_remove"); const $cb = $("#compose_banners"); - $stub.remove = () => {}; + $stub.remove = noop; $stub.length = 0; $cb.closest = () => []; diff --git a/web/tests/lib/test.js b/web/tests/lib/test.js index 31250168ceffe..af86996cce88d 100644 --- a/web/tests/lib/test.js +++ b/web/tests/lib/test.js @@ -17,6 +17,8 @@ exports.set_verbose = (value) => { verbose = value; }; +exports.noop = () => {}; + exports.suite = []; async function execute_test(label, f, opts) { diff --git a/web/tests/lib/zjquery.js b/web/tests/lib/zjquery.js index fc9665370e66e..b80045018a6d8 100644 --- a/web/tests/lib/zjquery.js +++ b/web/tests/lib/zjquery.js @@ -194,7 +194,7 @@ function make_zjquery() { over that aspect of the module for the purpose of testing, see if you can wrap the code that extends $.fn and use override() to - replace the wrapper with () => {}. + replace the wrapper with tests.lib.noop. `); }, }); diff --git a/web/tests/list_cursor.test.js b/web/tests/list_cursor.test.js index 314f3e6e910f0..66fe198aecbee 100644 --- a/web/tests/list_cursor.test.js +++ b/web/tests/list_cursor.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); @@ -71,7 +71,7 @@ run_test("single item list", ({override}) => { }; override(conf.list, "find_li", () => $li_stub); - override(cursor, "adjust_scroll", () => {}); + override(cursor, "adjust_scroll", noop); cursor.go_to(valid_key); @@ -91,7 +91,7 @@ run_test("multiple item list", ({override}) => { prev_key: (key) => (key > 1 ? key - 1 : undefined), }); const cursor = new ListCursor(conf); - override(cursor, "adjust_scroll", () => {}); + override(cursor, "adjust_scroll", noop); function li(key) { return $.create(`item-${key}`, {children: ["stub"]}); diff --git a/web/tests/list_widget.test.js b/web/tests/list_widget.test.js index cc50fc26675a2..555258abd24e1 100644 --- a/web/tests/list_widget.test.js +++ b/web/tests/list_widget.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, mock_jquery, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); @@ -45,8 +45,8 @@ const ListWidget = zrequire("list_widget"); function make_container() { const $container = {}; - $container.empty = () => {}; - $container.data = () => {}; + $container.empty = noop; + $container.data = noop; // Make our append function just set a field we can // check in our tests. diff --git a/web/tests/message_fetch.test.js b/web/tests/message_fetch.test.js index 6c6f5886451c2..9938338063ad1 100644 --- a/web/tests/message_fetch.test.js +++ b/web/tests/message_fetch.test.js @@ -5,14 +5,12 @@ const {strict: assert} = require("assert"); const _ = require("lodash"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); set_global("document", "document-stub"); -const noop = () => {}; - function MessageListView() { return { maybe_rerender: noop, @@ -365,7 +363,7 @@ run_test("loading_newer", () => { }); msg_list = simulate_narrow(); - msg_list.append_to_view = () => {}; + msg_list.append_to_view = noop; // Instead of using 444 as page_param.pointer, we // should have a message with that id in the message_list. msg_list.append(message_range(444, 445), false); diff --git a/web/tests/message_list_view.test.js b/web/tests/message_list_view.test.js index 767dc5932fde5..1bb0464e74bb5 100644 --- a/web/tests/message_list_view.test.js +++ b/web/tests/message_list_view.test.js @@ -5,13 +5,11 @@ const {strict: assert} = require("assert"); const _ = require("lodash"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); set_global("document", "document-stub"); -const noop = () => {}; - // timerender calls setInterval when imported mock_esm("../src/timerender", { render_date(time) { @@ -445,8 +443,8 @@ test("merge_message_groups", () => { const view = new MessageListView(list, table_name, true); view._message_groups = message_groups; - view.list.unsubscribed_bookend_content = () => {}; - view.list.subscribed_bookend_content = () => {}; + view.list.unsubscribed_bookend_content = noop; + view.list.subscribed_bookend_content = noop; return view; } diff --git a/web/tests/message_store.test.js b/web/tests/message_store.test.js index ff1f382bdcaa9..a7f1f1fa49407 100644 --- a/web/tests/message_store.test.js +++ b/web/tests/message_store.test.js @@ -3,12 +3,10 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const {page_params} = require("./lib/zpage_params"); -const noop = () => {}; - mock_esm("../src/stream_topic_history", { add_message: noop, }); @@ -338,13 +336,13 @@ test("update_property", () => { assert.equal(message1.sender_full_name, alice.full_name); assert.equal(message2.sender_full_name, bob.full_name); - message_store.update_property("sender_full_name", "Bobby", {user_id: bob.user_id}); + message_store.update_sender_full_name(bob.user_id, "Bobby"); assert.equal(message1.sender_full_name, alice.full_name); assert.equal(message2.sender_full_name, "Bobby"); assert.equal(message1.small_avatar_url, "alice_url"); assert.equal(message2.small_avatar_url, "bob_url"); - message_store.update_property("small_avatar_url", "bobby_url", {user_id: bob.user_id}); + message_store.update_small_avatar_url(bob.user_id, "bobby_url"); assert.equal(message1.small_avatar_url, "alice_url"); assert.equal(message2.small_avatar_url, "bobby_url"); @@ -352,7 +350,7 @@ test("update_property", () => { assert.equal(message1.display_recipient, devel.name); assert.equal(message2.stream_id, denmark.stream_id); assert.equal(message2.display_recipient, denmark.name); - message_store.update_property("stream_name", "Prod", {stream_id: devel.stream_id}); + message_store.update_stream_name(devel.stream_id, "Prod"); assert.equal(message1.stream_id, devel.stream_id); assert.equal(message1.display_recipient, "Prod"); assert.equal(message2.stream_id, denmark.stream_id); diff --git a/web/tests/narrow_activate.test.js b/web/tests/narrow_activate.test.js index adb96977890a1..fcb0be8ee9f8d 100644 --- a/web/tests/narrow_activate.test.js +++ b/web/tests/narrow_activate.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); mock_esm("../src/resize", { @@ -98,7 +98,7 @@ function test_helper({override}) { stub(compose_closed_ui, "update_buttons_for_stream_views"); stub(compose_closed_ui, "update_buttons_for_private"); // We don't test the css calls; we just skip over them. - $("#mark_read_on_scroll_state_banner").toggleClass = () => {}; + $("#mark_read_on_scroll_state_banner").toggleClass = noop; return { assert_events(expected_events) { diff --git a/web/tests/reactions.test.js b/web/tests/reactions.test.js index 9dbfa510ae611..3faf703a3088a 100644 --- a/web/tests/reactions.test.js +++ b/web/tests/reactions.test.js @@ -4,7 +4,7 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); const {make_stub} = require("./lib/stub"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {page_params, user_settings} = require("./lib/zpage_params"); @@ -309,8 +309,8 @@ test("sending", ({override, override_rewire}) => { let emoji_name = "smile"; // should be a current reaction - override_rewire(reactions, "add_reaction", () => {}); - override_rewire(reactions, "remove_reaction", () => {}); + override_rewire(reactions, "add_reaction", noop); + override_rewire(reactions, "remove_reaction", noop); { const stub = make_stub(); @@ -397,7 +397,7 @@ test("prevent_simultaneous_requests_updating_reaction", ({override, override_rew assert.equal(message_id, message.id); return message; }); - override_rewire(reactions, "add_reaction", () => {}); + override_rewire(reactions, "add_reaction", noop); const stub = make_stub(); channel.post = stub.f; @@ -485,8 +485,8 @@ test("update_vote_text_on_message", ({override_rewire}) => { user_settings.display_emoji_reaction_users = true; - override_rewire(reactions, "find_reaction", () => {}); - override_rewire(reactions, "set_reaction_vote_text", () => {}); + override_rewire(reactions, "find_reaction", noop); + override_rewire(reactions, "set_reaction_vote_text", noop); reactions.update_vote_text_on_message(message); @@ -1168,7 +1168,7 @@ test("view.remove_reaction (last person)", () => { }); test("error_handling", ({override, override_rewire}) => { - override(message_store, "get", () => {}); + override(message_store, "get", noop); blueslip.expect("error", "reactions: Bad message id"); @@ -1208,7 +1208,7 @@ test("remove last user", ({override}) => { const message = {...sample_message}; override(message_store, "get", () => message); - override(reactions.view, "remove_reaction", () => {}); + override(reactions.view, "remove_reaction", noop); function assert_names(names) { assert.deepEqual( @@ -1241,7 +1241,7 @@ test("local_reaction_id", () => { }); test("process_reaction_click", ({override}) => { - override(reactions.view, "remove_reaction", () => {}); + override(reactions.view, "remove_reaction", noop); const message = {...sample_message}; override(message_store, "get", () => message); diff --git a/web/tests/recent_view.test.js b/web/tests/recent_view.test.js index ba5a80b4c3dc0..e7ed794a80494 100644 --- a/web/tests/recent_view.test.js +++ b/web/tests/recent_view.test.js @@ -3,11 +3,10 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); -const noop = () => {}; const test_url = () => "https://www.example.com"; // We assign this in our test() wrapper. @@ -435,7 +434,7 @@ function stub_out_filter_buttons() { function test(label, f) { run_test(label, (helpers) => { - $(".header").css = () => {}; + $(".header").css = noop; page_params.development_environment = true; messages = sample_messages.map((message) => ({...message})); @@ -462,11 +461,11 @@ test("test_recent_view_show", ({mock_template}) => { return ""; }); - mock_template("recent_view_row.hbs", false, () => {}); + mock_template("recent_view_row.hbs", false, noop); stub_out_filter_buttons(); // We don't test the css calls; we just skip over them. - $("#mark_read_on_scroll_state_banner").toggleClass = () => {}; + $("#mark_read_on_scroll_state_banner").toggleClass = noop; rt.clear_for_tests(); rt.process_messages(messages); @@ -817,7 +816,7 @@ test("basic assertions", ({mock_template, override_rewire}) => { override_rewire(rt, "inplace_rerender", noop); rt.clear_for_tests(); - mock_template("recent_view_table.hbs", false, () => {}); + mock_template("recent_view_table.hbs", false, noop); mock_template("recent_view_row.hbs", true, (_data, html) => { assert.ok(html.startsWith(' { }); test("test_reify_local_echo_message", ({mock_template}) => { - mock_template("recent_view_table.hbs", false, () => {}); - mock_template("recent_view_row.hbs", false, () => {}); + mock_template("recent_view_table.hbs", false, noop); + mock_template("recent_view_row.hbs", false, noop); rt.clear_for_tests(); stub_out_filter_buttons(); diff --git a/web/tests/reload.test.js b/web/tests/reload.test.js index 44389cadc0b2c..32d4d0488fe6b 100644 --- a/web/tests/reload.test.js +++ b/web/tests/reload.test.js @@ -3,10 +3,11 @@ const {strict: assert} = require("assert"); const {zrequire} = require("./lib/namespace"); +const {run_test, noop} = require("./lib/test"); + // override file-level function call in reload.js -window.addEventListener = () => {}; +window.addEventListener = noop; const reload = zrequire("reload"); -const {run_test} = require("./lib/test"); run_test("old_metadata_string_is_stale", () => { assert.ok(reload.is_stale_refresh_token("1663886962834", "1663883954033"), true); diff --git a/web/tests/rendered_markdown.test.js b/web/tests/rendered_markdown.test.js index bb1a791d0caab..5a2f91c41c4fc 100644 --- a/web/tests/rendered_markdown.test.js +++ b/web/tests/rendered_markdown.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_cjs, mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {page_params, user_settings} = require("./lib/zpage_params"); @@ -586,7 +586,7 @@ run_test("code playground none", ({override, mock_template}) => { return undefined; }); - override(copied_tooltip, "show_copied_confirmation", () => {}); + override(copied_tooltip, "show_copied_confirmation", noop); const {prepends, $copy_code, $view_code} = test_code_playground(mock_template, false); assert.deepEqual(prepends, [$copy_code]); @@ -602,7 +602,7 @@ run_test("code playground single", ({override, mock_template}) => { return [{name: "Some Javascript Playground"}]; }); - override(copied_tooltip, "show_copied_confirmation", () => {}); + override(copied_tooltip, "show_copied_confirmation", noop); const {prepends, $copy_code, $view_code} = test_code_playground(mock_template, true); assert.deepEqual(prepends, [$view_code, $copy_code]); @@ -622,7 +622,7 @@ run_test("code playground multiple", ({override, mock_template}) => { return ["whatever", "whatever"]; }); - override(copied_tooltip, "show_copied_confirmation", () => {}); + override(copied_tooltip, "show_copied_confirmation", noop); const {prepends, $copy_code, $view_code} = test_code_playground(mock_template, true); assert.deepEqual(prepends, [$view_code, $copy_code]); diff --git a/web/tests/search.test.js b/web/tests/search.test.js index 273eba19ed1d1..3eb3bfb615f84 100644 --- a/web/tests/search.test.js +++ b/web/tests/search.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const narrow_state = mock_esm("../src/narrow_state"); @@ -294,7 +294,7 @@ run_test("initialize", ({override_rewire, mock_template}) => { assert.ok(!is_blurred); - override_rewire(search, "exit_search", () => {}); + override_rewire(search, "exit_search", noop); ev.key = "Enter"; $search_query_box.is = () => true; $searchbox_form.trigger(ev); diff --git a/web/tests/search_suggestion.test.js b/web/tests/search_suggestion.test.js index c1203fbb04dc7..3c6daba4ac725 100644 --- a/web/tests/search_suggestion.test.js +++ b/web/tests/search_suggestion.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const {page_params} = require("./lib/zpage_params"); const narrow_state = mock_esm("../src/narrow_state"); @@ -459,7 +459,7 @@ test("has_suggestions", ({override, mock_template}) => { let query = "h"; stream_data.add_sub({stream_id: 44, name: "devel", subscribed: true}); stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); - override(narrow_state, "stream_name", () => {}); + override(narrow_state, "stream_name", noop); let suggestions = get_suggestions(query); let expected = ["h", "has:link", "has:image", "has:attachment"]; @@ -518,7 +518,7 @@ test("check_is_suggestions", ({override, mock_template}) => { stream_data.add_sub({stream_id: 44, name: "devel", subscribed: true}); stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); - override(narrow_state, "stream_name", () => {}); + override(narrow_state, "stream_name", noop); let query = "i"; let suggestions = get_suggestions(query); @@ -599,7 +599,7 @@ test("check_is_suggestions", ({override, mock_template}) => { test("sent_by_me_suggestions", ({override, mock_template}) => { mock_template("search_description.hbs", true, (_data, html) => html); - override(narrow_state, "stream_name", () => {}); + override(narrow_state, "stream_name", noop); let query = ""; let suggestions = get_suggestions(query); @@ -675,7 +675,7 @@ test("topic_suggestions", ({override, mock_template}) => { let suggestions; let expected; - override(stream_topic_history_util, "get_server_history", () => {}); + override(stream_topic_history_util, "get_server_history", noop); stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); override(narrow_state, "stream_name", () => "office"); @@ -800,7 +800,7 @@ test("whitespace_glitch", ({override, mock_template}) => { const query = "stream:office "; // note trailing space - override(stream_topic_history_util, "get_server_history", () => {}); + override(stream_topic_history_util, "get_server_history", noop); stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); const suggestions = get_suggestions(query); @@ -816,7 +816,7 @@ test("stream_completion", ({override, mock_template}) => { stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); stream_data.add_sub({stream_id: 88, name: "dev help", subscribed: true}); - override(narrow_state, "stream_name", () => {}); + override(narrow_state, "stream_name", noop); let query = "stream:of"; let suggestions = get_suggestions(query); @@ -839,7 +839,7 @@ test("people_suggestions", ({override, mock_template}) => { let query = "te"; - override(narrow_state, "stream_name", () => {}); + override(narrow_state, "stream_name", noop); const ted = { email: "ted@zulip.com", diff --git a/web/tests/server_events.test.js b/web/tests/server_events.test.js index 560437ff182fe..420b13b2a7b2a 100644 --- a/web/tests/server_events.test.js +++ b/web/tests/server_events.test.js @@ -3,12 +3,10 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const {page_params} = require("./lib/zpage_params"); -const noop = () => {}; - set_global("addEventListener", noop); const channel = mock_esm("../src/channel"); diff --git a/web/tests/settings_muted_users.test.js b/web/tests/settings_muted_users.test.js index 273da800f7d2c..dce7b75520da9 100644 --- a/web/tests/settings_muted_users.test.js +++ b/web/tests/settings_muted_users.test.js @@ -3,11 +3,9 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); -const noop = () => {}; - const channel = mock_esm("../src/channel"); const list_widget = mock_esm("../src/list_widget", { generic_sort_functions: noop, diff --git a/web/tests/settings_org.test.js b/web/tests/settings_org.test.js index 9194e3014fffb..612fe4db58aa3 100644 --- a/web/tests/settings_org.test.js +++ b/web/tests/settings_org.test.js @@ -4,13 +4,11 @@ const {strict: assert} = require("assert"); const {$t} = require("./lib/i18n"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); -const noop = () => {}; - const realm_icon = mock_esm("../src/realm_icon"); const channel = mock_esm("../src/channel"); @@ -30,7 +28,7 @@ const dropdown_widget = zrequire("dropdown_widget"); function test(label, f) { run_test(label, (helpers) => { - $("#realm-icon-upload-widget .upload-spinner-background").css = () => {}; + $("#realm-icon-upload-widget .upload-spinner-background").css = noop; page_params.is_admin = false; page_params.realm_domains = [ {domain: "example.com", allow_subdomains: true}, diff --git a/web/tests/settings_profile_fields.test.js b/web/tests/settings_profile_fields.test.js index 9c01150a71ec7..1791cc0251246 100644 --- a/web/tests/settings_profile_fields.test.js +++ b/web/tests/settings_profile_fields.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); @@ -63,15 +63,15 @@ function test_populate(opts, template_data) { $table[0] = "stub"; - $rows.remove = () => {}; - $form.remove = () => {}; + $rows.remove = noop; + $form.remove = noop; let num_appends = 0; $table.append = () => { num_appends += 1; }; - loading.destroy_indicator = () => {}; + loading.destroy_indicator = noop; settings_profile_fields.do_populate_profile_fields(fields_data); @@ -83,7 +83,7 @@ run_test("populate_profile_fields", ({mock_template}) => { page_params.custom_profile_fields = {}; page_params.realm_default_external_accounts = JSON.stringify({}); - $("#admin_profile_fields_table .display_in_profile_summary_false").toggleClass = () => {}; + $("#admin_profile_fields_table .display_in_profile_summary_false").toggleClass = noop; const template_data = []; mock_template("settings/admin_profile_field_list.hbs", false, (data) => { diff --git a/web/tests/settings_realm_domains.test.js b/web/tests/settings_realm_domains.test.js index 511cdf9fb7637..1ddee1d3003f8 100644 --- a/web/tests/settings_realm_domains.test.js +++ b/web/tests/settings_realm_domains.test.js @@ -3,11 +3,10 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const channel = mock_esm("../src/channel"); -const noop = () => {}; mock_esm("../src/ui_report", { success(msg, elem) { elem.val(msg); diff --git a/web/tests/settings_user_topics.test.js b/web/tests/settings_user_topics.test.js index 078c3e7e53e70..b8da97bc9ab2c 100644 --- a/web/tests/settings_user_topics.test.js +++ b/web/tests/settings_user_topics.test.js @@ -3,11 +3,9 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); -const noop = () => {}; - const list_widget = mock_esm("../src/list_widget", { generic_sort_functions: noop, }); diff --git a/web/tests/spoilers.test.js b/web/tests/spoilers.test.js index 184b051b34bce..dfd15d92c85bd 100644 --- a/web/tests/spoilers.test.js +++ b/web/tests/spoilers.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const spoilers = zrequire("spoilers"); @@ -22,7 +22,7 @@ const get_spoiler_elem = (title) => { const $block = $.create(`block-${title}`); const $header = $.create(`header-${title}`); const $content = $.create(`content-${title}`); - $content.remove = () => {}; + $content.remove = noop; $header.text(title); $block.set_find_results(".spoiler-header", $header); $block.set_find_results(".spoiler-content", $content); diff --git a/web/tests/stream_events.test.js b/web/tests/stream_events.test.js index 18b5bcfb4a57d..9cf75209cd38a 100644 --- a/web/tests/stream_events.test.js +++ b/web/tests/stream_events.test.js @@ -4,12 +4,10 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); const {make_stub} = require("./lib/stub"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); -const noop = () => {}; - const color_data = mock_esm("../src/color_data"); const compose_fade = mock_esm("../src/compose_fade"); const stream_color_events = mock_esm("../src/stream_color_events"); diff --git a/web/tests/stream_list.test.js b/web/tests/stream_list.test.js index 6372ecf90b8bf..3f9dc04d6d651 100644 --- a/web/tests/stream_list.test.js +++ b/web/tests/stream_list.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params, user_settings} = require("./lib/zpage_params"); @@ -13,9 +13,8 @@ page_params.is_admin = false; page_params.realm_users = []; // We use this with override. -let num_unread_for_stream; +let unread_unmuted_count; let stream_has_any_unread_mentions; -const noop = () => {}; mock_esm("../src/narrow_state", { active: () => false, @@ -26,8 +25,8 @@ const scroll_util = mock_esm("../src/scroll_util", { get_scroll_element: ($element) => $element, }); mock_esm("../src/unread", { - num_unread_for_stream: () => ({ - unmuted_count: num_unread_for_stream, + unread_count_info_for_stream: () => ({ + unmuted_count: unread_unmuted_count, stream_is_muted: false, muted_count: 0, }), @@ -76,7 +75,7 @@ function create_devel_sidebar_row({mock_template}) { return ""; }); - num_unread_for_stream = 42; + unread_unmuted_count = 42; stream_has_any_unread_mentions = false; stream_list.create_sidebar_row(devel); assert.equal($devel_count.text(), "42"); @@ -99,7 +98,7 @@ function create_social_sidebar_row({mock_template}) { return ""; }); - num_unread_for_stream = 99; + unread_unmuted_count = 99; stream_has_any_unread_mentions = true; stream_list.create_sidebar_row(social); assert.equal($social_count.text(), "99"); diff --git a/web/tests/stream_search.test.js b/web/tests/stream_search.test.js index 52a651815b57a..650ded7c14788 100644 --- a/web/tests/stream_search.test.js +++ b/web/tests/stream_search.test.js @@ -3,14 +3,12 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); // This tests the stream searching functionality which currently // lives in stream_list.js. -const noop = () => {}; - mock_esm("../src/resize", { resize_page_components: noop, diff --git a/web/tests/stream_topic_history.test.js b/web/tests/stream_topic_history.test.js index 530f86207bb73..61e749f564961 100644 --- a/web/tests/stream_topic_history.test.js +++ b/web/tests/stream_topic_history.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const channel = mock_esm("../src/channel"); const message_util = mock_esm("../src/message_util"); @@ -320,11 +320,11 @@ test("server_history_end_to_end", () => { get_error_callback = opts.error; }; - stream_topic_history_util.get_server_history(stream_id, () => {}); + stream_topic_history_util.get_server_history(stream_id, noop); // Another call. Early return because a request is already in progress // for stream_id = 99. This function call adds coverage. - stream_topic_history_util.get_server_history(stream_id, () => {}); + stream_topic_history_util.get_server_history(stream_id, noop); assert.ok(stream_topic_history.is_request_pending_for(stream_id)); get_error_callback(); diff --git a/web/tests/transmit.test.js b/web/tests/transmit.test.js index 01fd6aa7986aa..59d5c41f275e1 100644 --- a/web/tests/transmit.test.js +++ b/web/tests/transmit.test.js @@ -3,12 +3,10 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const {page_params} = require("./lib/zpage_params"); -const noop = () => {}; - const channel = mock_esm("../src/channel"); const reload = mock_esm("../src/reload"); const reload_state = mock_esm("../src/reload_state"); diff --git a/web/tests/unread.test.js b/web/tests/unread.test.js index 82db19a280022..9013799e2e441 100644 --- a/web/tests/unread.test.js +++ b/web/tests/unread.test.js @@ -239,7 +239,7 @@ test("muting", () => { let counts = unread.get_counts(); assert.equal(counts.stream_count.get(stream_id).unmuted_count, 1); assert.equal(counts.home_unread_messages, 1); - assert.equal(unread.num_unread_for_stream(stream_id).unmuted_count, 1); + assert.equal(unread.unread_count_info_for_stream(stream_id).unmuted_count, 1); assert.deepEqual(unread.get_msg_ids_for_stream(stream_id), [message.id]); test_notifiable_count(counts.home_unread_messages, 0); @@ -251,14 +251,19 @@ test("muting", () => { counts = unread.get_counts(); assert.equal(counts.stream_count.get(stream_id).unmuted_count, 0); assert.equal(counts.home_unread_messages, 0); - assert.equal(unread.num_unread_for_stream(stream_id).unmuted_count, 0); + assert.equal(unread.unread_count_info_for_stream(stream_id).unmuted_count, 0); assert.deepEqual(unread.get_msg_ids_for_stream(stream_id), []); test_notifiable_count(counts.home_unread_messages, 0); // we still find the message id here (muting is ignored) assert.deepEqual(unread.get_all_msg_ids(), [message.id]); - assert.equal(unread.num_unread_for_stream(unknown_stream_id), 0); + assert.deepEqual(unread.unread_count_info_for_stream(unknown_stream_id), { + unmuted_count: 0, + muted_count: 0, + followed_count: 0, + stream_is_muted: false, + }); }); test("num_unread_for_topic", () => { diff --git a/web/tests/upload.test.js b/web/tests/upload.test.js index 02d4f72c9c38f..064a3d172d163 100644 --- a/web/tests/upload.test.js +++ b/web/tests/upload.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); @@ -181,7 +181,7 @@ test("get_item", () => { }); test("show_error_message", ({mock_template}) => { - $("#compose_banners .upload_banner .moving_bar").css = () => {}; + $("#compose_banners .upload_banner .moving_bar").css = noop; $("#compose_banners .upload_banner").length = 0; let banner_shown = false; @@ -206,8 +206,8 @@ test("show_error_message", ({mock_template}) => { }); test("upload_files", async ({mock_template, override_rewire}) => { - $("#compose_banners .upload_banner").remove = () => {}; - $("#compose_banners .upload_banner .moving_bar").css = () => {}; + $("#compose_banners .upload_banner").remove = noop; + $("#compose_banners .upload_banner .moving_bar").css = noop; $("#compose_banners .upload_banner").length = 0; let files = [ @@ -502,9 +502,9 @@ test("copy_paste", ({override, override_rewire}) => { }); test("uppy_events", ({override_rewire, mock_template}) => { - $("#compose_banners .upload_banner .moving_bar").css = () => {}; + $("#compose_banners .upload_banner .moving_bar").css = noop; $("#compose_banners .upload_banner").length = 0; - override_rewire(compose_ui, "smart_insert_inline", () => {}); + override_rewire(compose_ui, "smart_insert_inline", noop); const callbacks = {}; let state = {}; diff --git a/web/tests/user_events.test.js b/web/tests/user_events.test.js index 27c5e808cbdd4..0efb9760e92b0 100644 --- a/web/tests/user_events.test.js +++ b/web/tests/user_events.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); @@ -210,7 +210,7 @@ run_test("updates", () => { assert.equal(user_id, isaac.user_id); assert.equal(person.avatar_url, avatar_url); - $("#personal-menu .header-button-avatar").css = () => {}; + $("#personal-menu .header-button-avatar").css = noop; user_events.update_person({user_id: me.user_id, avatar_url: "http://gravatar.com/789456"}); person = people.get_by_email(me.email); @@ -275,7 +275,7 @@ run_test("updates", () => { user_events.update_person({user_id: isaac.user_id, is_active: true}); assert.ok(people.is_person_active(isaac.user_id)); - stream_events.remove_deactivated_user_from_all_streams = () => {}; + stream_events.remove_deactivated_user_from_all_streams = noop; let bot_data_updated = false; settings_users.update_bot_data = (user_id) => { diff --git a/web/tests/user_search.test.js b/web/tests/user_search.test.js index 03e066b556e0e..53e3d8d07c378 100644 --- a/web/tests/user_search.test.js +++ b/web/tests/user_search.test.js @@ -3,7 +3,7 @@ const {strict: assert} = require("assert"); const {mock_esm, zrequire} = require("./lib/namespace"); -const {run_test} = require("./lib/test"); +const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); @@ -79,8 +79,8 @@ function set_input_val(val) { test("clear_search", ({override}) => { override(presence, "get_status", () => "active"); override(presence, "get_user_ids", () => all_user_ids); - override(popovers, "hide_all", () => {}); - override(resize, "resize_sidebars", () => {}); + override(popovers, "hide_all", noop); + override(resize, "resize_sidebars", noop); // Empty because no users match this search string. override(fake_buddy_list, "populate", (user_ids) => { @@ -102,8 +102,8 @@ test("clear_search", ({override}) => { test("escape_search", ({override}) => { page_params.realm_presence_disabled = true; - override(resize, "resize_sidebars", () => {}); - override(popovers, "hide_all", () => {}); + override(resize, "resize_sidebars", noop); + override(popovers, "hide_all", noop); set_input_val("somevalue"); activity_ui.escape_search(); @@ -116,9 +116,9 @@ test("escape_search", ({override}) => { }); test("blur search right", ({override}) => { - override(sidebar_ui, "show_userlist_sidebar", () => {}); - override(popovers, "hide_all", () => {}); - override(resize, "resize_sidebars", () => {}); + override(sidebar_ui, "show_userlist_sidebar", noop); + override(popovers, "hide_all", noop); + override(resize, "resize_sidebars", noop); $(".user-list-filter").closest = (selector) => { assert.equal(selector, ".app-main [class^='column-']"); @@ -132,9 +132,9 @@ test("blur search right", ({override}) => { }); test("blur search left", ({override}) => { - override(sidebar_ui, "show_streamlist_sidebar", () => {}); - override(popovers, "hide_all", () => {}); - override(resize, "resize_sidebars", () => {}); + override(sidebar_ui, "show_streamlist_sidebar", noop); + override(popovers, "hide_all", noop); + override(resize, "resize_sidebars", noop); $(".user-list-filter").closest = (selector) => { assert.equal(selector, ".app-main [class^='column-']"); @@ -204,9 +204,9 @@ test("filter_user_ids", ({override}) => { test("click on user header to toggle display", ({override}) => { const $user_filter = $(".user-list-filter"); - override(popovers, "hide_all", () => {}); - override(sidebar_ui, "show_userlist_sidebar", () => {}); - override(resize, "resize_sidebars", () => {}); + override(popovers, "hide_all", noop); + override(sidebar_ui, "show_userlist_sidebar", noop); + override(resize, "resize_sidebars", noop); page_params.realm_presence_disabled = true; diff --git a/web/webpack.assets.json b/web/webpack.assets.json index 91e2115c923d6..cf6136a50a8bd 100644 --- a/web/webpack.assets.json +++ b/web/webpack.assets.json @@ -72,7 +72,8 @@ "./styles/portico/hello.css", "./styles/portico/navbar.css", "./styles/portico/footer.css", - "./styles/portico/pricing_plans.css" + "./styles/portico/pricing_plans.css", + "./styles/portico/comparison_table.css" ], "integrations": [ "./src/bundles/portico", diff --git a/zerver/actions/bots.py b/zerver/actions/bots.py index c790d2b690cf0..5c30ee50a2452 100644 --- a/zerver/actions/bots.py +++ b/zerver/actions/bots.py @@ -6,7 +6,8 @@ from zerver.actions.create_user import created_bot_event from zerver.actions.streams import bulk_remove_subscriptions from zerver.lib.streams import get_subscribed_private_streams_for_user -from zerver.models import RealmAuditLog, Stream, UserProfile, active_user_ids, bot_owner_user_ids +from zerver.models import RealmAuditLog, Stream, UserProfile +from zerver.models.users import active_user_ids, bot_owner_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index e23118fb8f178..ca38c889a61a0 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -31,10 +31,9 @@ RealmUserDefault, Stream, UserProfile, - get_org_type_display_name, - get_realm, - get_system_bot, ) +from zerver.models.realms import get_org_type_display_name, get_realm +from zerver.models.users import get_system_bot from zproject.backends import all_implemented_backend_names if settings.CORPORATE_ENABLED: diff --git a/zerver/actions/create_user.py b/zerver/actions/create_user.py index f30f2f5302ac2..af7dc0b211728 100644 --- a/zerver/actions/create_user.py +++ b/zerver/actions/create_user.py @@ -44,15 +44,13 @@ Recipient, Stream, Subscription, - SystemGroups, UserGroup, UserGroupMembership, UserMessage, UserProfile, - active_user_ids, - bot_owner_user_ids, - get_system_bot, ) +from zerver.models.groups import SystemGroups +from zerver.models.users import active_user_ids, bot_owner_user_ids, get_system_bot from zerver.tornado.django_api import send_event_on_commit if settings.BILLING_ENABLED: diff --git a/zerver/actions/custom_profile_fields.py b/zerver/actions/custom_profile_fields.py index 8f2c60fdf3290..5fab80482c0bc 100644 --- a/zerver/actions/custom_profile_fields.py +++ b/zerver/actions/custom_profile_fields.py @@ -9,14 +9,9 @@ from zerver.lib.streams import render_stream_description from zerver.lib.types import ProfileDataElementUpdateDict, ProfileFieldData from zerver.lib.users import get_user_ids_who_can_access_user -from zerver.models import ( - CustomProfileField, - CustomProfileFieldValue, - Realm, - UserProfile, - active_user_ids, - custom_profile_fields_for_realm, -) +from zerver.models import CustomProfileField, CustomProfileFieldValue, Realm, UserProfile +from zerver.models.custom_profile_fields import custom_profile_fields_for_realm +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event diff --git a/zerver/actions/default_streams.py b/zerver/actions/default_streams.py index 467d278648110..cc04538a8f276 100644 --- a/zerver/actions/default_streams.py +++ b/zerver/actions/default_streams.py @@ -8,14 +8,9 @@ get_default_streams_for_realm_as_dicts, ) from zerver.lib.exceptions import JsonableError -from zerver.models import ( - DefaultStream, - DefaultStreamGroup, - Realm, - Stream, - active_non_guest_user_ids, - get_default_stream_groups, -) +from zerver.models import DefaultStream, DefaultStreamGroup, Realm, Stream +from zerver.models.streams import get_default_stream_groups +from zerver.models.users import active_non_guest_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/invites.py b/zerver/actions/invites.py index 48807c852159c..0fa5cb9614f1d 100644 --- a/zerver/actions/invites.py +++ b/zerver/actions/invites.py @@ -24,15 +24,8 @@ from zerver.lib.send_email import FromAddress, clear_scheduled_invitation_emails, send_email from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.types import UnspecifiedValue -from zerver.models import ( - Message, - MultiuseInvite, - PreregistrationUser, - Realm, - Stream, - UserProfile, - filter_to_valid_prereg_users, -) +from zerver.models import Message, MultiuseInvite, PreregistrationUser, Realm, Stream, UserProfile +from zerver.models.prereg_users import filter_to_valid_prereg_users from zerver.tornado.django_api import send_event diff --git a/zerver/actions/message_edit.py b/zerver/actions/message_edit.py index d542de77bfde7..ecb45b6a6bfa1 100644 --- a/zerver/actions/message_edit.py +++ b/zerver/actions/message_edit.py @@ -76,9 +76,9 @@ UserMessage, UserProfile, UserTopic, - get_stream_by_id_in_realm, - get_system_bot, ) +from zerver.models.streams import get_stream_by_id_in_realm +from zerver.models.users import get_system_bot from zerver.tornado.django_api import send_event diff --git a/zerver/actions/message_send.py b/zerver/actions/message_send.py index f4aab56406490..c18147361ddfd 100644 --- a/zerver/actions/message_send.py +++ b/zerver/actions/message_send.py @@ -68,6 +68,7 @@ get_user_group_mentions_data, user_allows_notifications_in_StreamTopic, ) +from zerver.lib.query_helpers import query_for_ids from zerver.lib.queue import queue_json_publish from zerver.lib.recipient_users import recipient_for_user_profiles from zerver.lib.stream_subscription import ( @@ -83,8 +84,7 @@ from zerver.lib.user_message import UserMessageLite, bulk_insert_ums from zerver.lib.users import ( check_can_access_user, - check_user_can_access_all_users, - get_accessible_user_ids, + get_inaccessible_user_ids, get_subscribers_of_target_user_subscriptions, get_user_ids_who_can_access_user, get_users_involved_in_dms_with_target_users, @@ -95,24 +95,20 @@ from zerver.models import ( Client, Message, - NotificationTriggers, Realm, Recipient, Stream, - SystemGroups, UserMessage, UserPresence, UserProfile, UserTopic, - get_client, - get_huddle_user_ids, - get_stream, - get_stream_by_id_in_realm, - get_system_bot, - get_user_by_delivery_email, - is_cross_realm_bot_email, - query_for_ids, ) +from zerver.models.clients import get_client +from zerver.models.groups import SystemGroups +from zerver.models.recipients import get_huddle_user_ids +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.streams import get_stream, get_stream_by_id_in_realm +from zerver.models.users import get_system_bot, get_user_by_delivery_email, is_cross_realm_bot_email from zerver.tornado.django_api import send_event @@ -733,13 +729,11 @@ def create_user_messages( followed_topic_email_user_ids: AbstractSet[int], mark_as_read_user_ids: Set[int], limit_unread_user_ids: Optional[Set[int]], - scheduled_message_to_self: bool, topic_participant_user_ids: Set[int], ) -> List[UserMessageLite]: # These properties on the Message are set via # render_markdown by code in the Markdown inline patterns ids_with_alert_words = rendering_result.user_ids_with_alert_words - sender_id = message.sender.id is_stream_message = message.is_stream_message() base_flags = 0 @@ -770,17 +764,8 @@ def create_user_messages( user_messages = [] for user_profile_id in um_eligible_user_ids: flags = base_flags - if ( - ( - # Messages you sent from a non-API client are - # automatically marked as read for yourself; scheduled - # messages to yourself only are not. - user_profile_id == sender_id - and message.sent_by_human() - and not scheduled_message_to_self - ) - or user_profile_id in mark_as_read_user_ids - or (limit_unread_user_ids is not None and user_profile_id not in limit_unread_user_ids) + if user_profile_id in mark_as_read_user_ids or ( + limit_unread_user_ids is not None and user_profile_id not in limit_unread_user_ids ): flags |= UserMessage.flags.read if user_profile_id in mentioned_user_ids: @@ -866,7 +851,6 @@ def do_send_messages( send_message_requests_maybe_none: Sequence[Optional[SendMessageRequest]], *, email_gateway: bool = False, - scheduled_message_to_self: bool = False, mark_as_read: Sequence[int] = [], ) -> List[SentMessageResult]: """See @@ -916,7 +900,6 @@ def do_send_messages( followed_topic_email_user_ids=send_request.followed_topic_email_user_ids, mark_as_read_user_ids=mark_as_read_user_ids, limit_unread_user_ids=send_request.limit_unread_user_ids, - scheduled_message_to_self=scheduled_message_to_self, topic_participant_user_ids=send_request.topic_participant_user_ids, ) @@ -1346,11 +1329,15 @@ def check_send_stream_message( stream_name: str, topic: str, body: str, + *, realm: Optional[Realm] = None, + read_by_sender: bool = False, ) -> int: addressee = Addressee.for_stream_name(stream_name, topic) message = check_message(sender, client, addressee, body, realm) - sent_message_result = do_send_messages([message])[0] + sent_message_result = do_send_messages( + [message], mark_as_read=[sender.id] if read_by_sender else [] + )[0] return sent_message_result.message_id @@ -1395,6 +1382,7 @@ def check_send_message( widget_content: Optional[str] = None, *, skip_stream_access_check: bool = False, + read_by_sender: bool = False, ) -> SentMessageResult: addressee = Addressee.legacy_build(sender, recipient_type_name, message_to, topic_name) try: @@ -1414,7 +1402,7 @@ def check_send_message( ) except ZephyrMessageAlreadySentError as e: return SentMessageResult(message_id=e.message_id) - return do_send_messages([message])[0] + return do_send_messages([message], mark_as_read=[sender.id] if read_by_sender else [])[0] def send_rate_limited_pm_notification_to_bot_owner( @@ -1556,14 +1544,9 @@ def check_private_message_policy( def check_sender_can_access_recipients( realm: Realm, sender: UserProfile, user_profiles: Sequence[UserProfile] ) -> None: - if check_user_can_access_all_users(sender): - return - - users_accessible_to_sender = set(get_accessible_user_ids(realm, sender)) - # Guest users can access all the bots (including cross-realm bots). - non_bot_recipient_user_ids = {user.id for user in user_profiles if not user.is_bot} + recipient_user_ids = [user.id for user in user_profiles] + inaccessible_recipients = get_inaccessible_user_ids(recipient_user_ids, sender) - inaccessible_recipients = non_bot_recipient_user_ids - users_accessible_to_sender if inaccessible_recipients: raise JsonableError(_("You do not have permission to access some of the recipients.")) diff --git a/zerver/actions/presence.py b/zerver/actions/presence.py index 25031bec6c54d..00a9d8a0141b0 100644 --- a/zerver/actions/presence.py +++ b/zerver/actions/presence.py @@ -12,7 +12,9 @@ from zerver.lib.queue import queue_json_publish from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.users import get_user_ids_who_can_access_user -from zerver.models import Client, UserPresence, UserProfile, active_user_ids, get_client +from zerver.models import Client, UserPresence, UserProfile +from zerver.models.clients import get_client +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event diff --git a/zerver/actions/realm_domains.py b/zerver/actions/realm_domains.py index 57526dfab7caa..1ea401beae496 100644 --- a/zerver/actions/realm_domains.py +++ b/zerver/actions/realm_domains.py @@ -4,15 +4,9 @@ from django.utils.timezone import now as timezone_now from zerver.actions.realm_settings import do_set_realm_property -from zerver.models import ( - Realm, - RealmAuditLog, - RealmDomain, - RealmDomainDict, - UserProfile, - active_user_ids, - get_realm_domains, -) +from zerver.models import Realm, RealmAuditLog, RealmDomain, UserProfile +from zerver.models.realms import RealmDomainDict, get_realm_domains +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_emoji.py b/zerver/actions/realm_emoji.py index 86fbc83f11528..35010cd92d0c9 100644 --- a/zerver/actions/realm_emoji.py +++ b/zerver/actions/realm_emoji.py @@ -10,15 +10,9 @@ from zerver.lib.exceptions import JsonableError from zerver.lib.pysa import mark_sanitized from zerver.lib.upload import upload_emoji_image -from zerver.models import ( - EmojiInfo, - Realm, - RealmAuditLog, - RealmEmoji, - UserProfile, - active_user_ids, - get_all_custom_emoji_for_realm, -) +from zerver.models import Realm, RealmAuditLog, RealmEmoji, UserProfile +from zerver.models.realm_emoji import EmojiInfo, get_all_custom_emoji_for_realm +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_icon.py b/zerver/actions/realm_icon.py index df0f949fd91b2..820eeee36c4f7 100644 --- a/zerver/actions/realm_icon.py +++ b/zerver/actions/realm_icon.py @@ -4,7 +4,8 @@ from django.utils.timezone import now as timezone_now from zerver.lib.realm_icon import realm_icon_url -from zerver.models import Realm, RealmAuditLog, UserProfile, active_user_ids +from zerver.models import Realm, RealmAuditLog, UserProfile +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_linkifiers.py b/zerver/actions/realm_linkifiers.py index 4d7d1a22c7512..c5f243d0a8ea3 100644 --- a/zerver/actions/realm_linkifiers.py +++ b/zerver/actions/realm_linkifiers.py @@ -7,15 +7,9 @@ from zerver.lib.exceptions import JsonableError from zerver.lib.types import LinkifierDict -from zerver.models import ( - Realm, - RealmAuditLog, - RealmFilter, - UserProfile, - active_user_ids, - flush_linkifiers, - linkifiers_for_realm, -) +from zerver.models import Realm, RealmAuditLog, RealmFilter, UserProfile +from zerver.models.linkifiers import flush_linkifiers, linkifiers_for_realm +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_logo.py b/zerver/actions/realm_logo.py index ec0b7451d4d23..687c530062b31 100644 --- a/zerver/actions/realm_logo.py +++ b/zerver/actions/realm_logo.py @@ -4,7 +4,8 @@ from django.utils.timezone import now as timezone_now from zerver.lib.realm_logo import get_realm_logo_data -from zerver.models import Realm, RealmAuditLog, UserProfile, active_user_ids +from zerver.models import Realm, RealmAuditLog, UserProfile +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_playgrounds.py b/zerver/actions/realm_playgrounds.py index 6fe4788c5bb9c..b08bcf046a4d5 100644 --- a/zerver/actions/realm_playgrounds.py +++ b/zerver/actions/realm_playgrounds.py @@ -6,14 +6,9 @@ from zerver.lib.exceptions import ValidationFailureError from zerver.lib.types import RealmPlaygroundDict -from zerver.models import ( - Realm, - RealmAuditLog, - RealmPlayground, - UserProfile, - active_user_ids, - get_realm_playgrounds, -) +from zerver.models import Realm, RealmAuditLog, RealmPlayground, UserProfile +from zerver.models.realm_playgrounds import get_realm_playgrounds +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index bc974401f04c1..1005bda63c3f0 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -31,12 +31,12 @@ ScheduledEmail, Stream, Subscription, - SystemGroups, UserGroup, UserProfile, - active_user_ids, - get_realm, ) +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_realm +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event, send_event_on_commit if settings.BILLING_ENABLED: diff --git a/zerver/actions/scheduled_messages.py b/zerver/actions/scheduled_messages.py index cc5a9adbe469d..c7be7b679b7f3 100644 --- a/zerver/actions/scheduled_messages.py +++ b/zerver/actions/scheduled_messages.py @@ -15,20 +15,14 @@ ) from zerver.actions.uploads import check_attachment_reference_change, do_claim_attachments from zerver.lib.addressee import Addressee +from zerver.lib.display_recipient import get_recipient_ids from zerver.lib.exceptions import JsonableError, RealmDeactivatedError, UserDeactivatedError from zerver.lib.message import SendMessageRequest, render_markdown, truncate_topic from zerver.lib.recipient_parsing import extract_direct_message_recipient_ids, extract_stream_id from zerver.lib.scheduled_messages import access_scheduled_message from zerver.lib.string_validation import check_stream_topic -from zerver.models import ( - Client, - Realm, - ScheduledMessage, - Subscription, - UserProfile, - get_recipient_ids, - get_system_bot, -) +from zerver.models import Client, Realm, ScheduledMessage, Subscription, UserProfile +from zerver.models.users import get_system_bot from zerver.tornado.django_api import send_event SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES = 10 @@ -43,7 +37,9 @@ def check_schedule_message( message_content: str, deliver_at: datetime, realm: Optional[Realm] = None, + *, forwarder_user_profile: Optional[UserProfile] = None, + read_by_sender: Optional[bool] = None, ) -> int: addressee = Addressee.legacy_build(sender, recipient_type_name, message_to, topic_name) send_request = check_message( @@ -56,11 +52,22 @@ def check_schedule_message( ) send_request.deliver_at = deliver_at - return do_schedule_messages([send_request], sender)[0] + if read_by_sender is None: + # Legacy default: a scheduled message you sent from a non-API client is + # automatically marked as read for yourself, unless it was sent to + # yourself only. + read_by_sender = ( + client.default_read_by_sender() and send_request.message.recipient != sender.recipient + ) + + return do_schedule_messages([send_request], sender, read_by_sender=read_by_sender)[0] def do_schedule_messages( - send_message_requests: Sequence[SendMessageRequest], sender: UserProfile + send_message_requests: Sequence[SendMessageRequest], + sender: UserProfile, + *, + read_by_sender: bool = False, ) -> List[int]: scheduled_messages: List[Tuple[ScheduledMessage, SendMessageRequest]] = [] @@ -80,6 +87,7 @@ def do_schedule_messages( scheduled_message.realm = send_request.realm assert send_request.deliver_at is not None scheduled_message.scheduled_timestamp = send_request.deliver_at + scheduled_message.read_by_sender = read_by_sender scheduled_message.delivery_type = ScheduledMessage.SEND_LATER scheduled_messages.append((scheduled_message, send_request)) @@ -301,9 +309,9 @@ def send_scheduled_message(scheduled_message: ScheduledMessage) -> None: scheduled_message.realm, ) - scheduled_message_to_self = scheduled_message.recipient == scheduled_message.sender.recipient sent_message_result = do_send_messages( - [send_request], scheduled_message_to_self=scheduled_message_to_self + [send_request], + mark_as_read=[scheduled_message.sender_id] if scheduled_message.read_by_sender else [], )[0] scheduled_message.delivered_message_id = sent_message_result.message_id scheduled_message.delivered = True diff --git a/zerver/actions/streams.py b/zerver/actions/streams.py index 065aaee36d9c6..53251558eb3a3 100644 --- a/zerver/actions/streams.py +++ b/zerver/actions/streams.py @@ -64,13 +64,11 @@ Recipient, Stream, Subscription, - SystemGroups, UserGroup, UserProfile, - active_non_guest_user_ids, - active_user_ids, - get_system_bot, ) +from zerver.models.groups import SystemGroups +from zerver.models.users import active_non_guest_user_ids, active_user_ids, get_system_bot from zerver.tornado.django_api import send_event, send_event_on_commit diff --git a/zerver/actions/typing.py b/zerver/actions/typing.py index a2fcbaf5e212c..79753d6847cb4 100644 --- a/zerver/actions/typing.py +++ b/zerver/actions/typing.py @@ -5,7 +5,8 @@ from zerver.lib.exceptions import JsonableError from zerver.lib.stream_subscription import get_active_subscriptions_for_stream_id -from zerver.models import Realm, Stream, UserProfile, get_user_by_id_in_realm_including_cross_realm +from zerver.models import Realm, Stream, UserProfile +from zerver.models.users import get_user_by_id_in_realm_including_cross_realm from zerver.tornado.django_api import send_event diff --git a/zerver/actions/uploads.py b/zerver/actions/uploads.py index 075d7b16a0696..4ab509fb5275b 100644 --- a/zerver/actions/uploads.py +++ b/zerver/actions/uploads.py @@ -1,17 +1,10 @@ import logging from typing import Any, Dict, List, Union +from zerver.lib.attachments import get_old_unclaimed_attachments, validate_attachment_request from zerver.lib.markdown import MessageRenderingResult from zerver.lib.upload import claim_attachment, delete_message_attachment -from zerver.models import ( - Attachment, - Message, - ScheduledMessage, - Stream, - UserProfile, - get_old_unclaimed_attachments, - validate_attachment_request, -) +from zerver.models import Attachment, Message, ScheduledMessage, Stream, UserProfile from zerver.tornado.django_api import send_event diff --git a/zerver/actions/user_groups.py b/zerver/actions/user_groups.py index d90f4b598332d..6194e76959fef 100644 --- a/zerver/actions/user_groups.py +++ b/zerver/actions/user_groups.py @@ -15,12 +15,12 @@ GroupGroupMembership, Realm, RealmAuditLog, - SystemGroups, UserGroup, UserGroupMembership, UserProfile, - active_user_ids, ) +from zerver.models.groups import SystemGroups +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event, send_event_on_commit diff --git a/zerver/actions/user_settings.py b/zerver/actions/user_settings.py index e73f014fa77c3..1c0405dff53fd 100644 --- a/zerver/actions/user_settings.py +++ b/zerver/actions/user_settings.py @@ -39,10 +39,9 @@ ScheduledMessageNotificationEmail, UserPresence, UserProfile, - bot_owner_user_ids, - get_client, - get_user_profile_by_id, ) +from zerver.models.clients import get_client +from zerver.models.users import bot_owner_user_ids, get_user_profile_by_id from zerver.tornado.django_api import send_event, send_event_on_commit diff --git a/zerver/actions/users.py b/zerver/actions/users.py index 187238c095093..df1cd44be6bbf 100644 --- a/zerver/actions/users.py +++ b/zerver/actions/users.py @@ -41,12 +41,14 @@ Subscription, UserGroupMembership, UserProfile, +) +from zerver.models.bots import get_bot_services +from zerver.models.realms import get_fake_email_domain +from zerver.models.users import ( active_non_guest_user_ids, active_user_ids, bot_owner_user_ids, get_bot_dicts_in_realm, - get_bot_services, - get_fake_email_domain, get_user_profile_by_id, ) from zerver.tornado.django_api import send_event, send_event_on_commit diff --git a/zerver/context_processors.py b/zerver/context_processors.py index 9ecf9fcf7ff5d..b7d8df6565904 100644 --- a/zerver/context_processors.py +++ b/zerver/context_processors.py @@ -20,7 +20,8 @@ from zerver.lib.request import RequestNotes from zerver.lib.send_email import FromAddress from zerver.lib.subdomains import get_subdomain, is_root_domain_available -from zerver.models import Realm, UserProfile, get_realm +from zerver.models import Realm, UserProfile +from zerver.models.realms import get_realm from zproject.backends import ( AUTH_BACKEND_NAME_MAP, auth_enabled_helper, diff --git a/zerver/data_import/rocketchat.py b/zerver/data_import/rocketchat.py index 7a88ad2d6ead6..4d6e7e181d9be 100644 --- a/zerver/data_import/rocketchat.py +++ b/zerver/data_import/rocketchat.py @@ -182,7 +182,7 @@ def convert_channel_data( # should be allowed to post in the converted Zulip stream. # For more details: https://zulip.com/help/stream-sending-policy # - # See `Stream` model in `zerver/models.py` to know about what each + # See `Stream` model in `zerver/models/streams.py` to know about what each # number represent. stream_post_policy = 4 if channel_dict.get("ro", False) else 1 @@ -886,7 +886,7 @@ def map_receiver_id_to_recipient_id( user_id_to_recipient_id[recipient["type_id"]] = recipient["id"] -# This is inspired by get_huddle_hash from zerver/models.py. It +# This is inspired by get_huddle_hash from zerver/models/recipients.py. It # expects strings identifying Rocket.Chat users, like # `LdBZ7kPxtKESyHPEe`, not integer IDs. # diff --git a/zerver/decorator.py b/zerver/decorator.py index fe20058b7bdbd..7da88ab351eca 100644 --- a/zerver/decorator.py +++ b/zerver/decorator.py @@ -61,7 +61,9 @@ from zerver.lib.users import is_2fa_verified from zerver.lib.utils import has_api_key_format from zerver.lib.webhooks.common import notify_bot_owner_about_invalid_json -from zerver.models import UserProfile, get_client, get_user_profile_by_api_key +from zerver.models import UserProfile +from zerver.models.clients import get_client +from zerver.models.users import get_user_profile_by_api_key if TYPE_CHECKING: from django.http.request import _ImmutableQueryDict diff --git a/zerver/forms.py b/zerver/forms.py index 467983e29f779..fdf253787b2df 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -35,16 +35,14 @@ from zerver.lib.soft_deactivation import queue_soft_reactivation from zerver.lib.subdomains import get_subdomain, is_root_domain_available from zerver.lib.users import check_full_name -from zerver.models import ( +from zerver.models import Realm, UserProfile +from zerver.models.realms import ( DisposableEmailError, DomainNotAllowedForRealmError, EmailContainsPlusError, - Realm, - UserProfile, get_realm, - get_user_by_delivery_email, - is_cross_realm_bot_email, ) +from zerver.models.users import get_user_by_delivery_email, is_cross_realm_bot_email from zproject.backends import check_password_strength, email_auth_enabled, email_belongs_to_ldap if settings.BILLING_ENABLED: diff --git a/zerver/lib/addressee.py b/zerver/lib/addressee.py index 06a786eff8dd9..24fff0cf9c39e 100644 --- a/zerver/lib/addressee.py +++ b/zerver/lib/addressee.py @@ -4,10 +4,8 @@ from zerver.lib.exceptions import JsonableError from zerver.lib.string_validation import check_stream_topic -from zerver.models import ( - Realm, - Stream, - UserProfile, +from zerver.models import Realm, Stream, UserProfile +from zerver.models.users import ( get_user_by_id_in_realm_including_cross_realm, get_user_including_cross_realm, ) diff --git a/zerver/lib/alert_words.py b/zerver/lib/alert_words.py index 3466789681d55..70544049d62dd 100644 --- a/zerver/lib/alert_words.py +++ b/zerver/lib/alert_words.py @@ -8,7 +8,8 @@ realm_alert_words_automaton_cache_key, realm_alert_words_cache_key, ) -from zerver.models import AlertWord, Realm, UserProfile, flush_realm_alert_words +from zerver.models import AlertWord, Realm, UserProfile +from zerver.models.alert_words import flush_realm_alert_words @cache_with_key(lambda realm: realm_alert_words_cache_key(realm.id), timeout=3600 * 24) diff --git a/zerver/lib/attachments.py b/zerver/lib/attachments.py index e7717e9161b55..f0e06192125e5 100644 --- a/zerver/lib/attachments.py +++ b/zerver/lib/attachments.py @@ -1,10 +1,25 @@ -from typing import Any, Dict, List +from datetime import timedelta +from typing import Any, Dict, List, Optional, Tuple, Union +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from django.db.models import Exists, OuterRef, QuerySet +from django.utils.timezone import now as timezone_now from django.utils.translation import gettext as _ -from zerver.lib.exceptions import JsonableError +from zerver.lib.exceptions import JsonableError, RateLimitedError from zerver.lib.upload import delete_message_attachment -from zerver.models import Attachment, UserProfile +from zerver.models import ( + ArchivedAttachment, + Attachment, + Message, + Realm, + Recipient, + Stream, + Subscription, + UserMessage, + UserProfile, +) def user_attachments(user_profile: UserProfile) -> List[Dict[str, Any]]: @@ -33,3 +48,150 @@ def remove_attachment(user_profile: UserProfile, attachment: Attachment) -> None _("An error occurred while deleting the attachment. Please try again later.") ) attachment.delete() + + +def validate_attachment_request_for_spectator_access( + realm: Realm, attachment: Attachment +) -> Optional[bool]: + if attachment.realm != realm: + return False + + # Update cached is_web_public property, if necessary. + if attachment.is_web_public is None: + # Fill the cache in a single query. This is important to avoid + # a potential race condition between checking and setting, + # where the attachment could have been moved again. + Attachment.objects.filter(id=attachment.id, is_web_public__isnull=True).update( + is_web_public=Exists( + Message.objects.filter( + # Uses index: zerver_attachment_messages_attachment_id_message_id_key + realm_id=realm.id, + attachment=OuterRef("id"), + recipient__stream__invite_only=False, + recipient__stream__is_web_public=True, + ), + ), + ) + attachment.refresh_from_db() + + if not attachment.is_web_public: + return False + + if settings.RATE_LIMITING: + try: + from zerver.lib.rate_limiter import rate_limit_spectator_attachment_access_by_file + + rate_limit_spectator_attachment_access_by_file(attachment.path_id) + except RateLimitedError: + return False + + return True + + +def validate_attachment_request( + maybe_user_profile: Union[UserProfile, AnonymousUser], + path_id: str, + realm: Optional[Realm] = None, +) -> Optional[bool]: + try: + attachment = Attachment.objects.get(path_id=path_id) + except Attachment.DoesNotExist: + return None + + if isinstance(maybe_user_profile, AnonymousUser): + assert realm is not None + return validate_attachment_request_for_spectator_access(realm, attachment) + + user_profile = maybe_user_profile + assert isinstance(user_profile, UserProfile) + + # Update cached is_realm_public property, if necessary. + if attachment.is_realm_public is None: + # Fill the cache in a single query. This is important to avoid + # a potential race condition between checking and setting, + # where the attachment could have been moved again. + Attachment.objects.filter(id=attachment.id, is_realm_public__isnull=True).update( + is_realm_public=Exists( + Message.objects.filter( + # Uses index: zerver_attachment_messages_attachment_id_message_id_key + realm_id=user_profile.realm_id, + attachment=OuterRef("id"), + recipient__stream__invite_only=False, + ), + ), + ) + attachment.refresh_from_db() + + if user_profile == attachment.owner: + # If you own the file, you can access it. + return True + if ( + attachment.is_realm_public + and attachment.realm == user_profile.realm + and user_profile.can_access_public_streams() + ): + # Any user in the realm can access realm-public files + return True + + messages = attachment.messages.all() + if UserMessage.objects.filter(user_profile=user_profile, message__in=messages).exists(): + # If it was sent in a direct message or private stream + # message, then anyone who received that message can access it. + return True + + # The user didn't receive any of the messages that included this + # attachment. But they might still have access to it, if it was + # sent to a stream they are on where history is public to + # subscribers. + + # These are subscriptions to a stream one of the messages was sent to + relevant_stream_ids = Subscription.objects.filter( + user_profile=user_profile, + active=True, + recipient__type=Recipient.STREAM, + recipient__in=[m.recipient_id for m in messages], + ).values_list("recipient__type_id", flat=True) + if len(relevant_stream_ids) == 0: + return False + + return Stream.objects.filter( + id__in=relevant_stream_ids, history_public_to_subscribers=True + ).exists() + + +def get_old_unclaimed_attachments( + weeks_ago: int, +) -> Tuple[QuerySet[Attachment], QuerySet[ArchivedAttachment]]: + """ + The logic in this function is fairly tricky. The essence is that + a file should be cleaned up if and only if it not referenced by any + Message, ScheduledMessage or ArchivedMessage. The way to find that out is through the + Attachment and ArchivedAttachment tables. + The queries are complicated by the fact that an uploaded file + may have either only an Attachment row, only an ArchivedAttachment row, + or both - depending on whether some, all or none of the messages + linking to it have been archived. + """ + delta_weeks_ago = timezone_now() - timedelta(weeks=weeks_ago) + + # The Attachment vs ArchivedAttachment queries are asymmetric because only + # Attachment has the scheduled_messages relation. + old_attachments = Attachment.objects.annotate( + has_other_messages=Exists( + ArchivedAttachment.objects.filter(id=OuterRef("id")).exclude(messages=None) + ) + ).filter( + messages=None, + scheduled_messages=None, + create_time__lt=delta_weeks_ago, + has_other_messages=False, + ) + old_archived_attachments = ArchivedAttachment.objects.annotate( + has_other_messages=Exists( + Attachment.objects.filter(id=OuterRef("id")).exclude( + messages=None, scheduled_messages=None + ) + ) + ).filter(messages=None, create_time__lt=delta_weeks_ago, has_other_messages=False) + + return old_attachments, old_archived_attachments diff --git a/zerver/lib/bot_lib.py b/zerver/lib/bot_lib.py index 4355d80679e32..ae7cebf0e11bb 100644 --- a/zerver/lib/bot_lib.py +++ b/zerver/lib/bot_lib.py @@ -20,7 +20,8 @@ ) from zerver.lib.integrations import EMBEDDED_BOTS from zerver.lib.topic import get_topic_from_message_info -from zerver.models import UserProfile, get_active_user +from zerver.models import UserProfile +from zerver.models.users import get_active_user def get_bot_handler(service_name: str) -> Any: diff --git a/zerver/lib/bulk_create.py b/zerver/lib/bulk_create.py index 4b66fac499668..8232d1b69ef85 100644 --- a/zerver/lib/bulk_create.py +++ b/zerver/lib/bulk_create.py @@ -13,11 +13,11 @@ Recipient, Stream, Subscription, - SystemGroups, UserGroup, UserGroupMembership, UserProfile, ) +from zerver.models.groups import SystemGroups def bulk_create_users( diff --git a/zerver/lib/cache.py b/zerver/lib/cache.py index b72a8e97c1d18..4fbc05c69b892 100644 --- a/zerver/lib/cache.py +++ b/zerver/lib/cache.py @@ -518,7 +518,7 @@ def bot_dicts_in_realm_cache_key(realm_id: int) -> str: def delete_user_profile_caches(user_profiles: Iterable["UserProfile"], realm: "Realm") -> None: # Imported here to avoid cyclic dependency. from zerver.lib.users import get_all_api_keys - from zerver.models import is_cross_realm_bot_email + from zerver.models.users import is_cross_realm_bot_email keys = [] for user_profile in user_profiles: @@ -554,7 +554,7 @@ def changed(update_fields: Optional[Sequence[str]], fields: List[str]) -> bool: return any(f in update_fields_set for f in fields) -# Called by models.py to flush the user_profile cache whenever we save +# Called by models/users.py to flush the user_profile cache whenever we save # a user_profile object def flush_user_profile( *, @@ -591,7 +591,7 @@ def flush_muting_users_cache(*, instance: "MutedUser", **kwargs: object) -> None cache_delete(get_muting_users_cache_key(mute_object.muted_user_id)) -# Called by models.py to flush various caches whenever we save +# Called by models/realms.py to flush various caches whenever we save # a Realm object. The main tricky thing here is that Realm info is # generally cached indirectly through user_profile objects. def flush_realm( @@ -639,7 +639,7 @@ def realm_text_description_cache_key(realm: "Realm") -> str: return f"realm_text_description:{realm.string_id}" -# Called by models.py to flush the stream cache whenever we save a stream +# Called by models/streams.py to flush the stream cache whenever we save a stream # object. def flush_stream( *, diff --git a/zerver/lib/cache_helpers.py b/zerver/lib/cache_helpers.py index d83c2a40bb63a..7d96cb7ad1bf1 100644 --- a/zerver/lib/cache_helpers.py +++ b/zerver/lib/cache_helpers.py @@ -24,7 +24,8 @@ from zerver.lib.safe_session_cached_db import SessionStore from zerver.lib.sessions import session_engine from zerver.lib.users import get_all_api_keys -from zerver.models import Client, UserProfile, get_client_cache_key +from zerver.models import Client, UserProfile +from zerver.models.clients import get_client_cache_key def user_cache_items( diff --git a/zerver/lib/create_user.py b/zerver/lib/create_user.py index b5a969df6210a..cc4776dbd190e 100644 --- a/zerver/lib/create_user.py +++ b/zerver/lib/create_user.py @@ -18,8 +18,8 @@ Subscription, UserBaseSettings, UserProfile, - get_fake_email_domain, ) +from zerver.models.realms import get_fake_email_domain def copy_default_settings( diff --git a/zerver/lib/digest.py b/zerver/lib/digest.py index d0e91cdf783c1..a79b7f0ed1da8 100644 --- a/zerver/lib/digest.py +++ b/zerver/lib/digest.py @@ -29,8 +29,8 @@ Subscription, UserActivityInterval, UserProfile, - get_active_streams, ) +from zerver.models.streams import get_active_streams logger = logging.getLogger(__name__) log_to_file(logger, settings.DIGEST_LOG_PATH) @@ -56,7 +56,7 @@ def add_message(self, message: Message) -> None: if len(self.sample_messages) < 2: self.sample_messages.append(message) - if message.sent_by_human(): + if not message.sender.is_bot: self.human_senders.add(message.sender.full_name) self.num_human_messages += 1 @@ -195,12 +195,15 @@ def get_recent_topics( .select_related( "recipient", # build_message_list looks up recipient.type "sender", # we need the sender's full name - "sending_client", # for Message.sent_by_human ) .defer( - # This construction, to only fetch the sender's full_name, + # This construction, to only fetch the sender's full_name and is_bot, # is because `.only()` doesn't work with select_related tables. - *{f"sender__{f.name}" for f in UserProfile._meta.fields if f.name not in {"full_name"}} + *{ + f"sender__{f.name}" + for f in UserProfile._meta.fields + if f.name not in {"full_name", "is_bot"} + } ) ) diff --git a/zerver/lib/display_recipient.py b/zerver/lib/display_recipient.py index f72d11166dd18..b3488c4dbce55 100644 --- a/zerver/lib/display_recipient.py +++ b/zerver/lib/display_recipient.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Set, Tuple, TypedDict +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, TypedDict from django_stubs_ext import ValuesQuerySet @@ -9,8 +9,11 @@ generic_bulk_cached_fetch, single_user_display_recipient_cache_key, ) +from zerver.lib.per_request_cache import return_same_value_during_entire_request from zerver.lib.types import DisplayRecipientT, UserDisplayRecipient -from zerver.models import Recipient, Stream, UserProfile, bulk_get_huddle_user_ids + +if TYPE_CHECKING: + from zerver.models import Recipient display_recipient_fields = [ "id", @@ -44,6 +47,8 @@ def get_display_recipient_remote_cache( Do not use this for streams. """ + from zerver.models import Recipient, UserProfile + assert recipient_type != Recipient.STREAM # The main priority for ordering here is being deterministic. @@ -64,6 +69,8 @@ def user_dict_id_fetcher(user_dict: UserDisplayRecipient) -> int: def bulk_fetch_single_user_display_recipients(uids: List[int]) -> Dict[int, UserDisplayRecipient]: + from zerver.models import UserProfile + return bulk_cached_fetch( # Use a separate cache key to protect us from conflicts with # the get_user_profile_by_id cache. @@ -85,6 +92,8 @@ def bulk_fetch_stream_names( Returns dict mapping recipient_id to corresponding display_recipient """ + from zerver.models import Stream + if len(recipient_tuples) == 0: return {} @@ -125,6 +134,9 @@ def bulk_fetch_user_display_recipients( Returns dict mapping recipient_id to corresponding display_recipient """ + from zerver.models import Recipient + from zerver.models.recipients import bulk_get_huddle_user_ids + if len(recipient_tuples) == 0: return {} @@ -172,6 +184,8 @@ def bulk_fetch_display_recipients( Returns dict mapping recipient_id to corresponding display_recipient """ + from zerver.models import Recipient + stream_recipients = { recipient for recipient in recipient_tuples if recipient[1] == Recipient.STREAM } @@ -184,3 +198,50 @@ def bulk_fetch_display_recipients( # Glue the dicts together and return: return {**stream_display_recipients, **personal_and_huddle_display_recipients} + + +@return_same_value_during_entire_request +def get_display_recipient_by_id( + recipient_id: int, recipient_type: int, recipient_type_id: Optional[int] +) -> List[UserDisplayRecipient]: + """ + returns: an object describing the recipient (using a cache). + If the type is a stream, the type_id must be an int; a string is returned. + Otherwise, type_id may be None; an array of recipient dicts is returned. + """ + # Have to import here, to avoid circular dependency. + from zerver.lib.display_recipient import get_display_recipient_remote_cache + + return get_display_recipient_remote_cache(recipient_id, recipient_type, recipient_type_id) + + +def get_display_recipient(recipient: "Recipient") -> List[UserDisplayRecipient]: + return get_display_recipient_by_id( + recipient.id, + recipient.type, + recipient.type_id, + ) + + +def get_recipient_ids( + recipient: Optional["Recipient"], user_profile_id: int +) -> Tuple[List[int], str]: + from zerver.models import Recipient + + if recipient is None: + recipient_type_str = "" + to = [] + elif recipient.type == Recipient.STREAM: + recipient_type_str = "stream" + to = [recipient.type_id] + else: + recipient_type_str = "private" + if recipient.type == Recipient.PERSONAL: + to = [recipient.type_id] + else: + to = [] + for r in get_display_recipient(recipient): + assert not isinstance(r, str) # It will only be a string for streams + if r["id"] != user_profile_id: + to.append(r["id"]) + return to, recipient_type_str diff --git a/zerver/lib/email_mirror.py b/zerver/lib/email_mirror.py index 7b02c86e190b0..b156487d35703 100644 --- a/zerver/lib/email_mirror.py +++ b/zerver/lib/email_mirror.py @@ -14,6 +14,7 @@ internal_send_private_message, internal_send_stream_message, ) +from zerver.lib.display_recipient import get_display_recipient from zerver.lib.email_mirror_helpers import ( ZulipEmailForwardError, ZulipEmailForwardUserError, @@ -28,19 +29,10 @@ from zerver.lib.send_email import FromAddress from zerver.lib.string_validation import is_character_printable from zerver.lib.upload import upload_message_attachment -from zerver.models import ( - Message, - MissedMessageEmailAddress, - Realm, - Recipient, - Stream, - UserProfile, - get_client, - get_display_recipient, - get_stream_by_id_in_realm, - get_system_bot, - get_user_profile_by_id, -) +from zerver.models import Message, MissedMessageEmailAddress, Realm, Recipient, Stream, UserProfile +from zerver.models.clients import get_client +from zerver.models.streams import get_stream_by_id_in_realm +from zerver.models.users import get_system_bot, get_user_profile_by_id from zproject.backends import is_user_active logger = logging.getLogger(__name__) diff --git a/zerver/lib/email_notifications.py b/zerver/lib/email_notifications.py index 11e59340ebf21..56c4bb85b90a8 100644 --- a/zerver/lib/email_notifications.py +++ b/zerver/lib/email_notifications.py @@ -21,6 +21,7 @@ from lxml.html import builder as e from confirmation.models import one_click_unsubscribe_link +from zerver.lib.display_recipient import get_display_recipient from zerver.lib.markdown.fenced_code import FENCE_RE from zerver.lib.message import bulk_access_messages from zerver.lib.notification_data import get_mentioned_user_group_name @@ -35,18 +36,10 @@ stream_narrow_url, topic_narrow_url, ) -from zerver.models import ( - Message, - NotificationTriggers, - Realm, - Recipient, - Stream, - UserMessage, - UserProfile, - get_context_for_message, - get_display_recipient, - get_user_profile_by_id, -) +from zerver.models import Message, Realm, Recipient, Stream, UserMessage, UserProfile +from zerver.models.messages import get_context_for_message +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.users import get_user_profile_by_id if sys.version_info < (3, 9): # nocoverage from backports import zoneinfo diff --git a/zerver/lib/email_validation.py b/zerver/lib/email_validation.py index 0f37829d6170a..b8d418863cc2c 100644 --- a/zerver/lib/email_validation.py +++ b/zerver/lib/email_validation.py @@ -8,15 +8,13 @@ from zerver.lib.name_restrictions import is_disposable_domain # TODO: Move DisposableEmailError, etc. into here. -from zerver.models import ( +from zerver.models import Realm, RealmDomain +from zerver.models.realms import ( DisposableEmailError, DomainNotAllowedForRealmError, EmailContainsPlusError, - Realm, - RealmDomain, - get_users_by_delivery_email, - is_cross_realm_bot_email, ) +from zerver.models.users import get_users_by_delivery_email, is_cross_realm_bot_email def validate_disposable(email: str) -> None: diff --git a/zerver/lib/emoji.py b/zerver/lib/emoji.py index 818fd72e0c57b..b14c564b59d5f 100644 --- a/zerver/lib/emoji.py +++ b/zerver/lib/emoji.py @@ -9,11 +9,8 @@ from zerver.lib.exceptions import JsonableError from zerver.lib.storage import static_path from zerver.lib.upload import upload_backend -from zerver.models import ( - Reaction, - Realm, - RealmEmoji, - UserProfile, +from zerver.models import Reaction, Realm, RealmEmoji, UserProfile +from zerver.models.realm_emoji import ( get_all_custom_emoji_for_realm, get_name_keyed_dict_for_active_realm_emoji, ) diff --git a/zerver/lib/events.py b/zerver/lib/events.py index c594365140d66..d2cd1eaa160b0 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -68,7 +68,6 @@ max_message_id_for_user, ) from zerver.models import ( - MAX_TOPIC_NAME_LENGTH, Client, CustomProfileField, Draft, @@ -80,13 +79,14 @@ UserProfile, UserStatus, UserTopic, - custom_profile_fields_for_realm, - get_all_custom_emoji_for_realm, - get_default_stream_groups, - get_realm_domains, - get_realm_playgrounds, - linkifiers_for_realm, ) +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.custom_profile_fields import custom_profile_fields_for_realm +from zerver.models.linkifiers import linkifiers_for_realm +from zerver.models.realm_emoji import get_all_custom_emoji_for_realm +from zerver.models.realm_playgrounds import get_realm_playgrounds +from zerver.models.realms import get_realm_domains +from zerver.models.streams import get_default_stream_groups from zerver.tornado.django_api import get_user_events, request_event_queue from zproject.backends import email_auth_enabled, password_auth_enabled @@ -453,7 +453,7 @@ def fetch_initial_state_data( assert spectator_requested_language is not None # When UserProfile=None, we want to serve the values for various # settings as the defaults. Instead of copying the default values - # from models.py here, we access these default values from a + # from models/users.py here, we access these default values from a # temporary UserProfile object that will not be saved to the database. # # We also can set various fields to avoid duplicating code diff --git a/zerver/lib/export.py b/zerver/lib/export.py index e25edbb1e5898..136e582f86f91 100644 --- a/zerver/lib/export.py +++ b/zerver/lib/export.py @@ -69,10 +69,9 @@ UserProfile, UserStatus, UserTopic, - get_realm, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.realms import get_realm +from zerver.models.users import get_system_bot, get_user_profile_by_id # Custom mypy types follow: Record: TypeAlias = Dict[str, Any] @@ -307,7 +306,7 @@ class MessagePartial(TypedDict): def sanity_check_output(data: TableData) -> None: # First, we verify that the export tool has a declared - # configuration for every table declared in the `models.py` files. + # configuration for every table declared in the `models` modules. target_models = [ *apps.get_app_config("analytics").get_models(include_auto_created=True), *apps.get_app_config("django_otp").get_models(include_auto_created=True), diff --git a/zerver/lib/import_realm.py b/zerver/lib/import_realm.py index 7ceaf49bcbb0f..d496d7e8d6be0 100644 --- a/zerver/lib/import_realm.py +++ b/zerver/lib/import_realm.py @@ -67,7 +67,6 @@ Service, Stream, Subscription, - SystemGroups, UserActivity, UserActivityInterval, UserGroup, @@ -77,11 +76,11 @@ UserProfile, UserStatus, UserTopic, - get_huddle_hash, - get_realm, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_realm +from zerver.models.recipients import get_huddle_hash +from zerver.models.users import get_system_bot, get_user_profile_by_id realm_tables = [ ("zerver_realmauthenticationmethod", RealmAuthenticationMethod, "realmauthenticationmethod"), diff --git a/zerver/lib/management.py b/zerver/lib/management.py index e9de2e826e133..70f3e94eb2877 100644 --- a/zerver/lib/management.py +++ b/zerver/lib/management.py @@ -13,7 +13,8 @@ from typing_extensions import override from zerver.lib.initial_password import initial_password -from zerver.models import Client, Realm, UserProfile, get_client +from zerver.models import Client, Realm, UserProfile +from zerver.models.clients import get_client def is_integer_string(val: str) -> bool: diff --git a/zerver/lib/markdown/__init__.py b/zerver/lib/markdown/__init__.py index ff15043ecd64c..b3ff0cd630792 100644 --- a/zerver/lib/markdown/__init__.py +++ b/zerver/lib/markdown/__init__.py @@ -74,13 +74,9 @@ from zerver.lib.types import LinkifierDict from zerver.lib.url_encoding import encode_stream, hash_util_encode from zerver.lib.url_preview.types import UrlEmbedData, UrlOEmbedData -from zerver.models import ( - EmojiInfo, - Message, - Realm, - get_name_keyed_dict_for_active_realm_emoji, - linkifiers_for_realm, -) +from zerver.models import Message, Realm +from zerver.models.linkifiers import linkifiers_for_realm +from zerver.models.realm_emoji import EmojiInfo, get_name_keyed_dict_for_active_realm_emoji ReturnT = TypeVar("ReturnT") diff --git a/zerver/lib/markdown/help_relative_links.py b/zerver/lib/markdown/help_relative_links.py index 24fa7a51a7fd5..77d971418a434 100644 --- a/zerver/lib/markdown/help_relative_links.py +++ b/zerver/lib/markdown/help_relative_links.py @@ -37,7 +37,7 @@ ], "stats": [' Usage statistics', "/stats"], "integrations": [' Integrations', "/integrations/"], - "plans": [' Plans and pricing', "/plans/"], + "plans": [' Plans and pricing', "/plans/"], "billing": [' Billing', "/billing/"], "about-zulip": ["About Zulip", "/#about-zulip"], } diff --git a/zerver/lib/mention.py b/zerver/lib/mention.py index a67f696f99ac2..18d0ded2389c7 100644 --- a/zerver/lib/mention.py +++ b/zerver/lib/mention.py @@ -7,7 +7,8 @@ from django.db.models import Q from zerver.lib.users import get_inaccessible_user_ids -from zerver.models import UserGroup, UserProfile, get_linkable_streams +from zerver.models import UserGroup, UserProfile +from zerver.models.streams import get_linkable_streams BEFORE_MENTION_ALLOWED_REGEX = r"(? bool: diff --git a/zerver/lib/outgoing_webhook.py b/zerver/lib/outgoing_webhook.py index cf8f604d7e450..d1a7597e96c5e 100644 --- a/zerver/lib/outgoing_webhook.py +++ b/zerver/lib/outgoing_webhook.py @@ -19,15 +19,10 @@ from zerver.lib.queue import retry_event from zerver.lib.topic import get_topic_from_message_info from zerver.lib.url_encoding import near_message_url -from zerver.models import ( - GENERIC_INTERFACE, - SLACK_INTERFACE, - Realm, - Service, - UserProfile, - get_client, - get_user_profile_by_id, -) +from zerver.models import Realm, Service, UserProfile +from zerver.models.bots import GENERIC_INTERFACE, SLACK_INTERFACE +from zerver.models.clients import get_client +from zerver.models.users import get_user_profile_by_id class OutgoingWebhookServiceInterface(metaclass=abc.ABCMeta): diff --git a/zerver/lib/presence.py b/zerver/lib/presence.py index bb30b11ae6ce0..06e09517da60d 100644 --- a/zerver/lib/presence.py +++ b/zerver/lib/presence.py @@ -6,9 +6,10 @@ from django.conf import settings from django.utils.timezone import now as timezone_now +from zerver.lib.query_helpers import query_for_ids from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.users import check_user_can_access_all_users, get_accessible_user_ids -from zerver.models import PushDeviceToken, Realm, UserPresence, UserProfile, query_for_ids +from zerver.models import PushDeviceToken, Realm, UserPresence, UserProfile def get_presence_dicts_for_rows( diff --git a/zerver/lib/push_notifications.py b/zerver/lib/push_notifications.py index a8766a557380d..7ccf41f2322df 100644 --- a/zerver/lib/push_notifications.py +++ b/zerver/lib/push_notifications.py @@ -39,6 +39,7 @@ do_set_realm_property, ) from zerver.lib.avatar import absolute_avatar_url, get_avatar_for_inaccessible_user +from zerver.lib.display_recipient import get_display_recipient from zerver.lib.emoji_utils import hex_codepoint_to_emoji from zerver.lib.exceptions import ErrorCode, JsonableError from zerver.lib.message import access_message, huddle_users @@ -56,7 +57,6 @@ AbstractPushDeviceToken, ArchivedMessage, Message, - NotificationTriggers, PushDeviceToken, Realm, Recipient, @@ -64,10 +64,10 @@ UserGroup, UserMessage, UserProfile, - get_display_recipient, - get_fake_email_domain, - get_user_profile_by_id, ) +from zerver.models.realms import get_fake_email_domain +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.users import get_user_profile_by_id if TYPE_CHECKING: import aioapns @@ -588,11 +588,14 @@ def send_notifications_to_bouncer( gcm_options: Dict[str, Any], android_devices: Sequence[DeviceToken], apple_devices: Sequence[DeviceToken], -) -> Tuple[int, int]: - if not android_devices and not apple_devices: - # Avoid making a useless API request to the bouncer if there - # are no mobile devices registered for this user. - return 0, 0 +) -> None: + if len(android_devices) + len(apple_devices) == 0: + logger.info( + "Skipping contacting the bouncer for user %s because there are no registered devices", + user_profile.id, + ) + + return post_data = { "user_uuid": str(user_profile.uuid), @@ -656,7 +659,12 @@ def send_notifications_to_bouncer( user_profile.realm, remote_realm_dict["expected_end_timestamp"], acting_user=None ) - return total_android_devices, total_apple_devices + logger.info( + "Sent mobile push notifications for user %s through bouncer: %s via FCM devices, %s via APNs devices", + user_profile.id, + total_android_devices, + total_apple_devices, + ) # @@ -1378,15 +1386,9 @@ def handle_push_notification(user_profile_id: int, missed_message: Dict[str, Any PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.APNS).order_by("id") ) if uses_notification_bouncer(): - total_android_devices, total_apple_devices = send_notifications_to_bouncer( + send_notifications_to_bouncer( user_profile, apns_payload, gcm_payload, gcm_options, android_devices, apple_devices ) - logger.info( - "Sent mobile push notifications for user %s through bouncer: %s via FCM devices, %s via APNs devices", - user_profile_id, - total_android_devices, - total_apple_devices, - ) return logger.info( diff --git a/zerver/lib/query_helpers.py b/zerver/lib/query_helpers.py new file mode 100644 index 0000000000000..52f1dea666fbf --- /dev/null +++ b/zerver/lib/query_helpers.py @@ -0,0 +1,30 @@ +from typing import List, TypeVar + +from django.db import models +from django_stubs_ext import ValuesQuerySet + +ModelT = TypeVar("ModelT", bound=models.Model) +RowT = TypeVar("RowT") + + +def query_for_ids( + query: ValuesQuerySet[ModelT, RowT], + user_ids: List[int], + field: str, +) -> ValuesQuerySet[ModelT, RowT]: + """ + This function optimizes searches of the form + `user_profile_id in (1, 2, 3, 4)` by quickly + building the where clauses. Profiling shows significant + speedups over the normal Django-based approach. + + Use this very carefully! Also, the caller should + guard against empty lists of user_ids. + """ + assert user_ids + clause = f"{field} IN %s" + query = query.extra( + where=[clause], + params=(tuple(user_ids),), + ) + return query diff --git a/zerver/lib/recipient_users.py b/zerver/lib/recipient_users.py index 1a9542f3bd775..84364350bd5b1 100644 --- a/zerver/lib/recipient_users.py +++ b/zerver/lib/recipient_users.py @@ -3,7 +3,9 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ -from zerver.models import Recipient, UserProfile, get_or_create_huddle, is_cross_realm_bot_email +from zerver.models import Recipient, UserProfile +from zerver.models.recipients import get_or_create_huddle +from zerver.models.users import is_cross_realm_bot_email def get_recipient_from_user_profiles( diff --git a/zerver/lib/remote_server.py b/zerver/lib/remote_server.py index e29f2950a36af..3d92b01493cec 100644 --- a/zerver/lib/remote_server.py +++ b/zerver/lib/remote_server.py @@ -22,7 +22,8 @@ ) from zerver.lib.outgoing_http import OutgoingSession from zerver.lib.queue import queue_event_on_commit -from zerver.models import OrgTypeEnum, Realm, RealmAuditLog +from zerver.models import Realm, RealmAuditLog +from zerver.models.realms import OrgTypeEnum class PushBouncerSession(OutgoingSession): @@ -168,11 +169,11 @@ def send_to_push_bouncer( ) if res.status_code >= 500: - # 500s should be resolved by the people who run the push + # 5xx's should be resolved by the people who run the push # notification bouncer service, and they'll get an appropriate # error notification from the server. We raise an exception to signal # to the callers that the attempt failed and they can retry. - error_msg = "Received 500 from push notification bouncer" + error_msg = f"Received {res.status_code} from push notification bouncer" logging.warning(error_msg) raise PushNotificationBouncerServerError(error_msg) elif res.status_code >= 400: @@ -239,6 +240,30 @@ def send_json_to_push_bouncer( ) +def maybe_mark_pushes_disabled( + e: Union[JsonableError, orjson.JSONDecodeError], logger: logging.Logger +) -> None: + if isinstance(e, PushNotificationBouncerServerError): + # We don't fall through and deactivate the flag, since this is + # not under the control of the caller. + return + + if isinstance(e, JsonableError): + logger.warning(e.msg) + else: + logger.exception("Exception communicating with %s", settings.PUSH_NOTIFICATION_BOUNCER_URL) + + # An exception was thrown talking to the push bouncer. There may + # be certain transient failures that we could ignore here, but the + # default explanation is that there is something wrong either with + # our credentials being corrupted or our ability to reach the + # bouncer service over the network, so we immediately move to + # reporting push notifications as likely not working. + for realm in Realm.objects.filter(push_notifications_enabled=True): + do_set_realm_property(realm, "push_notifications_enabled", False, acting_user=None) + do_set_push_notifications_enabled_end_timestamp(realm, None, acting_user=None) + + def build_analytics_data( realm_count_query: QuerySet[RealmCount], installation_count_query: QuerySet[InstallationCount], @@ -319,8 +344,8 @@ def send_server_data_to_push_bouncer(consider_usage_statistics: bool = True) -> # first, check what's latest try: result = send_to_push_bouncer("GET", "server/analytics/status", {}) - except PushNotificationBouncerRetryLaterError as e: - logger.warning(e.msg, exc_info=True) + except (JsonableError, orjson.JSONDecodeError) as e: + maybe_mark_pushes_disabled(e, logger) return # Gather only entries with IDs greater than the last ID received by the push bouncer. @@ -364,35 +389,23 @@ def send_server_data_to_push_bouncer(consider_usage_statistics: bool = True) -> response = send_to_push_bouncer( "POST", "server/analytics", request.model_dump(round_trip=True) ) - except PushNotificationBouncerServerError: # nocoverage - # 50x errors from the bouncer cannot be addressed by the - # administrator of this server, and may be localized to - # this endpoint; don't rashly mark push notifications as - # disabled when they are likely working perfectly fine. - return - except Exception as e: - if isinstance(e, JsonableError): - # Log exceptions with server error messages to the analytics log. - logger.warning(e.msg) - - # An exception was thrown trying to ask the bouncer service whether we can send - # push notifications or not. There may be certain transient failures that we could - # ignore here, but the default explanation is that there is something wrong either - # with our credentials being corrupted or our ability to reach the bouncer service - # over the network, so we immediately move to reporting push notifications as likely not working, - # as whatever failed here is likely to also fail when trying to send a push notification. - for realm in Realm.objects.filter(push_notifications_enabled=True): - do_set_realm_property(realm, "push_notifications_enabled", False, acting_user=None) - do_set_push_notifications_enabled_end_timestamp(realm, None, acting_user=None) - if not isinstance(e, JsonableError): - # Log this generic error only if we haven't already logged specific error above. - logger.exception("Exception asking push bouncer if notifications will work.") + except (JsonableError, orjson.JSONDecodeError) as e: + maybe_mark_pushes_disabled(e, logger) return assert isinstance(response["realms"], dict) # for mypy realms = response["realms"] for realm_uuid, data in realms.items(): - realm = Realm.objects.get(uuid=realm_uuid) + try: + realm = Realm.objects.get(uuid=realm_uuid) + except Realm.DoesNotExist: + # This occurs if the installation's database was rebuilt + # from scratch or a realm was hard-deleted from the local + # database, after generating secrets and talking to the + # bouncer. + logger.warning("Received unexpected realm UUID from bouncer %s", realm_uuid) + continue + do_set_realm_property( realm, "push_notifications_enabled", data["can_push"], acting_user=None ) diff --git a/zerver/lib/scheduled_messages.py b/zerver/lib/scheduled_messages.py index 39fbc625c8a77..4f6b4219bfa0b 100644 --- a/zerver/lib/scheduled_messages.py +++ b/zerver/lib/scheduled_messages.py @@ -3,11 +3,10 @@ from django.utils.translation import gettext as _ from zerver.lib.exceptions import ResourceNotFoundError -from zerver.models import ( +from zerver.models import ScheduledMessage, UserProfile +from zerver.models.scheduled_jobs import ( APIScheduledDirectMessageDict, APIScheduledStreamMessageDict, - ScheduledMessage, - UserProfile, ) diff --git a/zerver/lib/scim.py b/zerver/lib/scim.py index ceb0d80b4817c..24971c1bc8539 100644 --- a/zerver/lib/scim.py +++ b/zerver/lib/scim.py @@ -15,11 +15,11 @@ from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm from zerver.lib.request import RequestNotes from zerver.lib.subdomains import get_subdomain -from zerver.models import ( +from zerver.models import UserProfile +from zerver.models.realms import ( DisposableEmailError, DomainNotAllowedForRealmError, EmailContainsPlusError, - UserProfile, ) diff --git a/zerver/lib/send_email.py b/zerver/lib/send_email.py index 5b39cd20daf8c..35b5c3ed28b87 100644 --- a/zerver/lib/send_email.py +++ b/zerver/lib/send_email.py @@ -8,7 +8,7 @@ from email.parser import Parser from email.policy import default from email.utils import formataddr, parseaddr -from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union import backoff import css_inline @@ -29,9 +29,14 @@ from confirmation.models import generate_key from zerver.lib.logging_util import log_to_file -from zerver.models import EMAIL_TYPES, Realm, ScheduledEmail, UserProfile, get_user_profile_by_id +from zerver.models import Realm, ScheduledEmail, UserProfile +from zerver.models.scheduled_jobs import EMAIL_TYPES +from zerver.models.users import get_user_profile_by_id from zproject.email_backends import EmailLogBackEnd, get_forward_address +if settings.ZILENCER_ENABLED: + from zilencer.models import RemoteZulipServer + MAX_CONNECTION_TRIES = 3 ## Logging setup ## @@ -195,7 +200,8 @@ def render_templates() -> Tuple[str, str, str]: # have not implemented. if "unsubscribe_link" in context: extra_headers["List-Unsubscribe"] = f"<{context['unsubscribe_link']}>" - extra_headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click" + if not context.get("remote_server_email", False): + extra_headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click" reply_to = None if reply_to_email is not None: @@ -507,25 +513,16 @@ def get_header(option: Optional[str], header: Optional[str], name: str) -> str: return str(option or header) -def send_custom_email( - users: QuerySet[UserProfile], - *, - target_emails: Sequence[str] = [], - options: Dict[str, Any], - add_context: Optional[Callable[[Dict[str, object], UserProfile], None]] = None, -) -> None: - """ - Helper for `manage.py send_custom_email`. - - Can be used directly with from a management shell with - send_custom_email(user_profile_list, dict( - markdown_template_path="/path/to/markdown/file.md", - subject="Email subject", - from_name="Sender Name") - ) - """ - - with open(options["markdown_template_path"]) as f: +def custom_email_sender( + markdown_template_path: str, + dry_run: bool, + subject: Optional[str] = None, + from_address: str = FromAddress.SUPPORT, + from_name: Optional[str] = None, + reply_to: Optional[str] = None, + **kwargs: Any, +) -> Callable[..., None]: + with open(markdown_template_path) as f: text = f.read() parsed_email_template = Parser(policy=default).parsestr(text) email_template_hash = hashlib.sha256(text.encode()).hexdigest()[0:32] @@ -558,12 +555,46 @@ def send_custom_email( f.write(base_template.read().replace("{{ rendered_input }}", rendered_input)) with open(subject_path, "w") as f: - f.write(get_header(options.get("subject"), parsed_email_template.get("subject"), "subject")) + f.write(get_header(subject, parsed_email_template.get("subject"), "subject")) - # Finally, we send the actual emails. + def send_one_email( + context: Dict[str, Any], to_user_id: Optional[int] = None, to_email: Optional[str] = None + ) -> None: + assert to_user_id is not None or to_email is not None + with suppress(EmailNotDeliveredError): + send_email( + email_id, + to_user_ids=[to_user_id] if to_user_id is not None else None, + to_emails=[to_email] if to_email is not None else None, + from_address=from_address, + reply_to_email=reply_to, + from_name=get_header(from_name, parsed_email_template.get("from"), "from_name"), + context=context, + dry_run=dry_run, + ) + + return send_one_email + + +def send_custom_email( + users: QuerySet[UserProfile], + *, + dry_run: bool, + options: Dict[str, str], + add_context: Optional[Callable[[Dict[str, object], UserProfile], None]] = None, +) -> None: + """ + Helper for `manage.py send_custom_email`. + + Can be used directly with from a management shell with + send_custom_email(user_profile_list, dict( + markdown_template_path="/path/to/markdown/file.md", + subject="Email subject", + from_name="Sender Name") + ) + """ + email_sender = custom_email_sender(**options, dry_run=dry_run) for user_profile in users.select_related("realm").order_by("id"): - if options.get("admins_only") and not user_profile.is_realm_admin: - continue context: Dict[str, object] = { "realm": user_profile.realm, "realm_string_id": user_profile.realm.string_id, @@ -572,38 +603,48 @@ def send_custom_email( } if add_context is not None: add_context(context, user_profile) - with suppress(EmailNotDeliveredError): - send_email( - email_id, - to_user_ids=[user_profile.id], - from_address=FromAddress.SUPPORT, - reply_to_email=options.get("reply_to"), - from_name=get_header( - options.get("from_name"), parsed_email_template.get("from"), "from_name" - ), - context=context, - dry_run=options["dry_run"], - ) + email_sender( + to_user_id=user_profile.id, + context=context, + ) - if options["dry_run"]: + if dry_run: break - # Now send emails to any recipients without a user account. - # This code path is intended for rare RemoteZulipServer emails. - for email_address in target_emails: - send_email( - email_id, - to_emails=[email_address], - from_address=FromAddress.SUPPORT, - reply_to_email=options.get("reply_to"), - from_name=get_header( - options.get("from_name"), parsed_email_template.get("from"), "from_name" + +def send_custom_server_email( + remote_servers: QuerySet["RemoteZulipServer"], + *, + dry_run: bool, + options: Dict[str, str], + add_context: Optional[Callable[[Dict[str, object], "RemoteZulipServer"], None]] = None, +) -> None: + assert settings.CORPORATE_ENABLED + from corporate.lib.stripe import BILLING_SUPPORT_EMAIL + from corporate.views.remote_billing_page import ( + generate_confirmation_link_for_server_deactivation, + ) + + email_sender = custom_email_sender( + **options, dry_run=dry_run, from_address=BILLING_SUPPORT_EMAIL + ) + + for server in remote_servers: + context = { + "remote_server_email": True, + "hostname": server.hostname, + "unsubscribe_link": generate_confirmation_link_for_server_deactivation( + server, 60 * 24 * 2 ), - context={"remote_server_email": True}, - dry_run=options["dry_run"], + } + if add_context is not None: + add_context(context, server) + email_sender( + to_email=server.contact_email, + context=context, ) - if options["dry_run"]: + if dry_run: break diff --git a/zerver/lib/server_initialization.py b/zerver/lib/server_initialization.py index c1e11414c7994..e8a0fd40aef95 100644 --- a/zerver/lib/server_initialization.py +++ b/zerver/lib/server_initialization.py @@ -11,9 +11,9 @@ RealmAuthenticationMethod, RealmUserDefault, UserProfile, - get_client, - get_system_bot, ) +from zerver.models.clients import get_client +from zerver.models.users import get_system_bot from zproject.backends import all_implemented_backend_names diff --git a/zerver/lib/sessions.py b/zerver/lib/sessions.py index 96c7e3e856153..b34636a84a0d4 100644 --- a/zerver/lib/sessions.py +++ b/zerver/lib/sessions.py @@ -10,7 +10,8 @@ from django.utils.timezone import now as timezone_now from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime -from zerver.models import Realm, UserProfile, get_user_profile_by_id +from zerver.models import Realm, UserProfile +from zerver.models.users import get_user_profile_by_id class SessionEngine(Protocol): diff --git a/zerver/lib/soft_deactivation.py b/zerver/lib/soft_deactivation.py index 07d13de98cea1..73da6849372e0 100644 --- a/zerver/lib/soft_deactivation.py +++ b/zerver/lib/soft_deactivation.py @@ -14,7 +14,6 @@ from zerver.lib.utils import assert_is_not_none from zerver.models import ( Message, - NotificationTriggers, Realm, RealmAuditLog, Recipient, @@ -23,6 +22,7 @@ UserMessage, UserProfile, ) +from zerver.models.scheduled_jobs import NotificationTriggers logger = logging.getLogger("zulip.soft_deactivation") log_to_file(logger, settings.SOFT_DEACTIVATION_LOG_PATH) diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index 0aaf0d9e24651..10107d953977f 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -28,17 +28,17 @@ Recipient, Stream, Subscription, - SystemGroups, UserGroup, UserProfile, - active_non_guest_user_ids, - active_user_ids, +) +from zerver.models.groups import SystemGroups +from zerver.models.streams import ( bulk_get_streams, get_realm_stream, get_stream, get_stream_by_id_in_realm, - is_cross_realm_bot_email, ) +from zerver.models.users import active_non_guest_user_ids, active_user_ids, is_cross_realm_bot_email from zerver.tornado.django_api import send_event diff --git a/zerver/lib/subscription_info.py b/zerver/lib/subscription_info.py index 97931a550731d..717366b872edf 100644 --- a/zerver/lib/subscription_info.py +++ b/zerver/lib/subscription_info.py @@ -25,7 +25,8 @@ SubscriptionInfo, SubscriptionStreamDict, ) -from zerver.models import Realm, Stream, Subscription, UserProfile, get_active_streams +from zerver.models import Realm, Stream, Subscription, UserProfile +from zerver.models.streams import get_active_streams def get_web_public_subs(realm: Realm) -> SubscriptionInfo: diff --git a/zerver/lib/test_classes.py b/zerver/lib/test_classes.py index 6be8b861d2d54..821139d612d10 100644 --- a/zerver/lib/test_classes.py +++ b/zerver/lib/test_classes.py @@ -102,20 +102,16 @@ Recipient, Stream, Subscription, - SystemGroups, UserGroup, UserGroupMembership, UserMessage, UserProfile, UserStatus, - clear_supported_auth_backends_cache, - get_realm, - get_realm_stream, - get_stream, - get_system_bot, - get_user, - get_user_by_delivery_email, ) +from zerver.models.groups import SystemGroups +from zerver.models.realms import clear_supported_auth_backends_cache, get_realm +from zerver.models.streams import get_realm_stream, get_stream +from zerver.models.users import get_system_bot, get_user, get_user_by_delivery_email from zerver.openapi.openapi import validate_against_openapi_schema, validate_request from zerver.tornado.event_queue import clear_client_event_queues_for_testing @@ -602,7 +598,6 @@ def client_get( desdemona="desdemona@zulip.com", shiva="shiva@zulip.com", webhook_bot="webhook-bot@zulip.com", - welcome_bot="welcome-bot@zulip.com", outgoing_webhook_bot="outgoing-webhook@zulip.com", default_bot="default-bot@zulip.com", ) @@ -1076,10 +1071,11 @@ def send_personal_message( from_user: UserProfile, to_user: UserProfile, content: str = "test content", - sending_client_name: str = "test suite", + *, + read_by_sender: bool = True, ) -> int: recipient_list = [to_user.id] - (sending_client, _) = Client.objects.get_or_create(name=sending_client_name) + (sending_client, _) = Client.objects.get_or_create(name="test suite") sent_message_result = check_send_message( from_user, @@ -1088,6 +1084,7 @@ def send_personal_message( recipient_list, None, content, + read_by_sender=read_by_sender, ) return sent_message_result.message_id @@ -1096,12 +1093,13 @@ def send_huddle_message( from_user: UserProfile, to_users: List[UserProfile], content: str = "test content", - sending_client_name: str = "test suite", + *, + read_by_sender: bool = True, ) -> int: to_user_ids = [u.id for u in to_users] assert len(to_user_ids) >= 2 - (sending_client, _) = Client.objects.get_or_create(name=sending_client_name) + (sending_client, _) = Client.objects.get_or_create(name="test suite") sent_message_result = check_send_message( from_user, @@ -1110,6 +1108,7 @@ def send_huddle_message( to_user_ids, None, content, + read_by_sender=read_by_sender, ) return sent_message_result.message_id @@ -1120,10 +1119,11 @@ def send_stream_message( content: str = "test content", topic_name: str = "test", recipient_realm: Optional[Realm] = None, - sending_client_name: str = "test suite", + *, allow_unsubscribed_sender: bool = False, + read_by_sender: bool = True, ) -> int: - (sending_client, _) = Client.objects.get_or_create(name=sending_client_name) + (sending_client, _) = Client.objects.get_or_create(name="test suite") message_id = check_send_stream_message( sender=sender, @@ -1132,6 +1132,7 @@ def send_stream_message( topic=topic_name, body=content, realm=recipient_realm, + read_by_sender=read_by_sender, ) if ( not UserMessage.objects.filter(user_profile=sender, message_id=message_id).exists() diff --git a/zerver/lib/test_helpers.py b/zerver/lib/test_helpers.py index e31576932dcf3..ac655fa260e1e 100644 --- a/zerver/lib/test_helpers.py +++ b/zerver/lib/test_helpers.py @@ -52,17 +52,10 @@ from zerver.lib.rate_limiter import RateLimitedIPAddr, rules from zerver.lib.request import RequestNotes from zerver.lib.upload.s3 import S3UploadBackend -from zerver.models import ( - Client, - Message, - RealmUserDefault, - Subscription, - UserMessage, - UserProfile, - get_client, - get_realm, - get_stream, -) +from zerver.models import Client, Message, RealmUserDefault, Subscription, UserMessage, UserProfile +from zerver.models.clients import get_client +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream from zerver.tornado.handlers import AsyncDjangoHandler, allocate_handler_id from zilencer.models import RemoteZulipServer from zproject.backends import ExternalAuthDataDict, ExternalAuthResult diff --git a/zerver/lib/upload/base.py b/zerver/lib/upload/base.py index 38fd894a00c10..560a5a5b5ebfd 100644 --- a/zerver/lib/upload/base.py +++ b/zerver/lib/upload/base.py @@ -10,7 +10,8 @@ from PIL.Image import DecompressionBombError from zerver.lib.exceptions import ErrorCode, JsonableError -from zerver.models import Attachment, Realm, UserProfile, is_cross_realm_bot_email +from zerver.models import Attachment, Realm, UserProfile +from zerver.models.users import is_cross_realm_bot_email DEFAULT_AVATAR_SIZE = 100 MEDIUM_AVATAR_SIZE = 500 diff --git a/zerver/lib/user_groups.py b/zerver/lib/user_groups.py index 072aaec09ae5a..b27f0194adb8c 100644 --- a/zerver/lib/user_groups.py +++ b/zerver/lib/user_groups.py @@ -16,11 +16,11 @@ Realm, RealmAuditLog, Stream, - SystemGroups, UserGroup, UserGroupMembership, UserProfile, ) +from zerver.models.groups import SystemGroups class UserGroupDict(TypedDict): diff --git a/zerver/lib/user_topics.py b/zerver/lib/user_topics.py index a5b64aa2b0322..8f3e6e499c6ca 100644 --- a/zerver/lib/user_topics.py +++ b/zerver/lib/user_topics.py @@ -12,7 +12,8 @@ from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.topic import topic_match_sa from zerver.lib.types import UserTopicDict -from zerver.models import UserProfile, UserTopic, get_stream +from zerver.models import UserProfile, UserTopic +from zerver.models.streams import get_stream def get_user_topics( diff --git a/zerver/lib/users.py b/zerver/lib/users.py index 4423890920c1d..b693d6502c1a4 100644 --- a/zerver/lib/users.py +++ b/zerver/lib/users.py @@ -34,12 +34,14 @@ Recipient, Service, Subscription, - SystemGroups, UserMessage, UserProfile, +) +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_fake_email_domain +from zerver.models.users import ( active_non_guest_user_ids, active_user_ids, - get_fake_email_domain, get_realm_user_dicts, get_user, get_user_by_id_in_realm_including_cross_realm, diff --git a/zerver/lib/validator.py b/zerver/lib/validator.py index a6b82eaa5c91c..5824ae3d53840 100644 --- a/zerver/lib/validator.py +++ b/zerver/lib/validator.py @@ -389,6 +389,22 @@ def check_url(var_name: str, val: object) -> str: raise ValidationError(_("{var_name} is not a URL").format(var_name=var_name)) +def check_capped_url(max_length: int) -> Validator[str]: + def validator(var_name: str, val: object) -> str: + # Ensure val is a string and length of the string does not + # exceed max_length. + s = check_capped_string(max_length)(var_name, val) + # Validate as URL. + validate = URLValidator() + try: + validate(s) + return s + except ValidationError: + raise ValidationError(_("{var_name} is not a URL").format(var_name=var_name)) + + return validator + + def check_external_account_url_pattern(var_name: str, val: object) -> str: s = check_string(var_name, val) diff --git a/zerver/management/commands/change_user_role.py b/zerver/management/commands/change_user_role.py index f6f2db8a54219..ca02f4fe870c8 100644 --- a/zerver/management/commands/change_user_role.py +++ b/zerver/management/commands/change_user_role.py @@ -13,6 +13,17 @@ from zerver.lib.management import ZulipBaseCommand from zerver.models import UserProfile +ROLE_CHOICES = [ + "owner", + "admin", + "moderator", + "member", + "guest", + "can_forge_sender", + "can_create_users", + "is_billing_admin", +] + class Command(ZulipBaseCommand): help = """Change role of an existing user in their (own) Realm. @@ -26,17 +37,8 @@ def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "new_role", metavar="", - choices=[ - "owner", - "admin", - "moderator", - "member", - "guest", - "can_forge_sender", - "can_create_users", - "is_billing_admin", - ], - help="new role of the user", + choices=ROLE_CHOICES, + help="new role of the user; choose from " + ", ".join(ROLE_CHOICES), ) parser.add_argument( "--revoke", diff --git a/zerver/management/commands/check_redis.py b/zerver/management/commands/check_redis.py index 179bbf16b49a9..b68d5e84922ae 100644 --- a/zerver/management/commands/check_redis.py +++ b/zerver/management/commands/check_redis.py @@ -8,7 +8,7 @@ from typing_extensions import override from zerver.lib.rate_limiter import RateLimitedUser, client -from zerver.models import get_user_profile_by_id +from zerver.models.users import get_user_profile_by_id class Command(BaseCommand): diff --git a/zerver/management/commands/delete_old_unclaimed_attachments.py b/zerver/management/commands/delete_old_unclaimed_attachments.py index 83ce211ea02d3..107943953dcac 100644 --- a/zerver/management/commands/delete_old_unclaimed_attachments.py +++ b/zerver/management/commands/delete_old_unclaimed_attachments.py @@ -7,8 +7,9 @@ from typing_extensions import override from zerver.actions.uploads import do_delete_old_unclaimed_attachments +from zerver.lib.attachments import get_old_unclaimed_attachments from zerver.lib.upload import all_message_attachments, delete_message_attachments -from zerver.models import ArchivedAttachment, Attachment, get_old_unclaimed_attachments +from zerver.models import ArchivedAttachment, Attachment class Command(BaseCommand): diff --git a/zerver/management/commands/edit_linkifiers.py b/zerver/management/commands/edit_linkifiers.py index 233a63ba036cd..819f5dc0bfdbe 100644 --- a/zerver/management/commands/edit_linkifiers.py +++ b/zerver/management/commands/edit_linkifiers.py @@ -7,7 +7,7 @@ from zerver.actions.realm_linkifiers import do_add_linkifier, do_remove_linkifier from zerver.lib.management import ZulipBaseCommand -from zerver.models import linkifiers_for_realm +from zerver.models.linkifiers import linkifiers_for_realm class Command(ZulipBaseCommand): diff --git a/zerver/management/commands/export_search.py b/zerver/management/commands/export_search.py index c583f6afb0c06..ceda82d18dc5e 100644 --- a/zerver/management/commands/export_search.py +++ b/zerver/management/commands/export_search.py @@ -16,14 +16,8 @@ from zerver.lib.management import ZulipBaseCommand from zerver.lib.soft_deactivation import reactivate_user_if_soft_deactivated from zerver.lib.upload import save_attachment_contents -from zerver.models import ( - Attachment, - Message, - Recipient, - Stream, - UserProfile, - get_user_by_delivery_email, -) +from zerver.models import Attachment, Message, Recipient, Stream, UserProfile +from zerver.models.users import get_user_by_delivery_email def write_attachment(base_path: str, attachment: Attachment) -> None: diff --git a/zerver/management/commands/merge_streams.py b/zerver/management/commands/merge_streams.py index 1f72b551cbb27..b85a7c9e9858d 100644 --- a/zerver/management/commands/merge_streams.py +++ b/zerver/management/commands/merge_streams.py @@ -5,7 +5,7 @@ from zerver.actions.streams import merge_streams from zerver.lib.management import ZulipBaseCommand -from zerver.models import get_stream +from zerver.models.streams import get_stream class Command(ZulipBaseCommand): diff --git a/zerver/management/commands/rate_limit.py b/zerver/management/commands/rate_limit.py index 98caf8228e8f3..c45de0932ae48 100644 --- a/zerver/management/commands/rate_limit.py +++ b/zerver/management/commands/rate_limit.py @@ -6,7 +6,8 @@ from zerver.lib.management import ZulipBaseCommand from zerver.lib.rate_limiter import RateLimitedUser -from zerver.models import UserProfile, get_user_profile_by_api_key +from zerver.models import UserProfile +from zerver.models.users import get_user_profile_by_api_key class Command(ZulipBaseCommand): diff --git a/zerver/management/commands/realm_domain.py b/zerver/management/commands/realm_domain.py index f8c70956e809f..e33ab287112a4 100644 --- a/zerver/management/commands/realm_domain.py +++ b/zerver/management/commands/realm_domain.py @@ -9,7 +9,8 @@ from zerver.lib.domains import validate_domain from zerver.lib.management import ZulipBaseCommand -from zerver.models import RealmDomain, get_realm_domains +from zerver.models import RealmDomain +from zerver.models.realms import get_realm_domains class Command(ZulipBaseCommand): diff --git a/zerver/management/commands/register_server.py b/zerver/management/commands/register_server.py index 8ec48d101f57b..ec565cda25003 100644 --- a/zerver/management/commands/register_server.py +++ b/zerver/management/commands/register_server.py @@ -124,7 +124,7 @@ def _request_push_notification_bouncer_url(self, url: str, params: Dict[str, Any registration_url = settings.PUSH_NOTIFICATION_BOUNCER_URL + url session = PushBouncerSession() try: - response = session.post(registration_url, params=params) + response = session.post(registration_url, data=params) except requests.RequestException: raise CommandError( "Network error connecting to push notifications service " diff --git a/zerver/management/commands/remove_users_from_stream.py b/zerver/management/commands/remove_users_from_stream.py index 0849f0b331b2a..c47db24d2083b 100644 --- a/zerver/management/commands/remove_users_from_stream.py +++ b/zerver/management/commands/remove_users_from_stream.py @@ -5,7 +5,7 @@ from zerver.actions.streams import bulk_remove_subscriptions from zerver.lib.management import ZulipBaseCommand -from zerver.models import get_stream +from zerver.models.streams import get_stream class Command(ZulipBaseCommand): diff --git a/zerver/management/commands/send_custom_email.py b/zerver/management/commands/send_custom_email.py index 0303f4569c5f1..56753a0646738 100644 --- a/zerver/management/commands/send_custom_email.py +++ b/zerver/management/commands/send_custom_email.py @@ -1,5 +1,5 @@ from argparse import ArgumentParser -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, Optional import orjson from django.conf import settings @@ -8,9 +8,12 @@ from confirmation.models import one_click_unsubscribe_link from zerver.lib.management import ZulipBaseCommand -from zerver.lib.send_email import send_custom_email +from zerver.lib.send_email import send_custom_email, send_custom_server_email from zerver.models import Realm, UserProfile +if settings.ZILENCER_ENABLED: + from zilencer.models import RemoteZulipServer + class Command(ZulipBaseCommand): help = """ @@ -42,11 +45,6 @@ def add_arguments(self, parser: ArgumentParser) -> None: action="store_true", help="Send to all organization administrators of sponsored organizations.", ) - targets.add_argument( - "--json-file", - help="Load the JSON file, and send to the users whose ids are the keys in that dict; " - "the context for each email will be extended by each value in the dict.", - ) self.add_user_list_args( targets, help="Email addresses of user(s) to send emails to.", @@ -56,6 +54,15 @@ def add_arguments(self, parser: ArgumentParser) -> None: # not mutually exclusive with the rest of the above. self.add_realm_args(parser) + # This is an additional filter on the above. It ideally would + # be in the mutually-exclusive set, but we would like to reuse + # it with --remote-servers + parser.add_argument( + "--json-file", + help="Load the JSON file, and send to the users whose ids are the keys in that dict; " + "the context for each email will be extended by each value in the dict.", + ) + # This is an additional filter which is composed with the above parser.add_argument( "--admins-only", @@ -86,11 +93,41 @@ def add_arguments(self, parser: ArgumentParser) -> None: ) @override - def handle(self, *args: Any, **options: str) -> None: - target_emails: List[str] = [] + def handle( + self, *args: Any, dry_run: bool = False, admins_only: bool = False, **options: str + ) -> None: users: QuerySet[UserProfile] = UserProfile.objects.none() add_context: Optional[Callable[[Dict[str, object], UserProfile], None]] = None + if options["remote_servers"]: + servers = RemoteZulipServer.objects.filter(deactivated=False) + add_server_context = None + if options["json_file"]: + with open(options["json_file"]) as f: + server_data: Dict[str, Dict[str, object]] = orjson.loads(f.read()) + servers = RemoteZulipServer.objects.filter( + id__in=[int(server_id) for server_id in server_data] + ) + + def add_server_context_from_dict( + context: Dict[str, object], server: RemoteZulipServer + ) -> None: + context.update(server_data.get(str(server.id), {})) + + add_server_context = add_server_context_from_dict + + send_custom_server_email( + servers, + dry_run=dry_run, + options=options, + add_context=add_server_context, + ) + if dry_run: + print("Would send the above email to:") + for server in servers: + print(f" {server.contact_email} ({server.hostname})") + return + if options["entire_server"]: users = UserProfile.objects.filter( is_active=True, is_bot=False, is_mirror_dummy=False, realm__deactivated=False @@ -111,16 +148,7 @@ def add_marketing_unsubscribe(context: Dict[str, object], user: UserProfile) -> context["unsubscribe_link"] = one_click_unsubscribe_link(user, "marketing") add_context = add_marketing_unsubscribe - elif options["remote_servers"]: - from zilencer.models import RemoteZulipServer - - target_emails = list( - set( - RemoteZulipServer.objects.filter(deactivated=False).values_list( - "contact_email", flat=True - ) - ) - ) + elif options["all_sponsored_org_admins"]: # Sends at most one copy to each email address, even if it # is an administrator in several organizations. @@ -136,19 +164,22 @@ def add_marketing_unsubscribe(context: Dict[str, object], user: UserProfile) -> realm__deactivated=False, realm__in=sponsored_realms, ).distinct("delivery_email") - elif options["json_file"]: + else: + realm = self.get_realm(options) + users = self.get_users(options, realm, is_bot=False) + + if options["json_file"]: with open(options["json_file"]) as f: user_data: Dict[str, Dict[str, object]] = orjson.loads(f.read()) - users = UserProfile.objects.filter(id__in=[int(user_id) for user_id in user_data]) + users = users.filter(id__in=[int(user_id) for user_id in user_data]) def add_context_from_dict(context: Dict[str, object], user: UserProfile) -> None: context.update(user_data.get(str(user.id), {})) add_context = add_context_from_dict - else: - realm = self.get_realm(options) - users = self.get_users(options, realm, is_bot=False) + if admins_only: + users = users.filter(is_realm_admin=True) # Only email users who've agreed to the terms of service. if settings.TERMS_OF_SERVICE_VERSION is not None: @@ -156,12 +187,13 @@ def add_context_from_dict(context: Dict[str, object], user: UserProfile) -> None Q(tos_version=None) | Q(tos_version=UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN) ) send_custom_email( - users, target_emails=target_emails, options=options, add_context=add_context + users, + dry_run=dry_run, + options=options, + add_context=add_context, ) - if options["dry_run"]: + if dry_run: print("Would send the above email to:") for user in users: print(f" {user.delivery_email} ({user.realm.string_id})") - for email in target_emails: - print(f" {email}") diff --git a/zerver/management/commands/send_to_email_mirror.py b/zerver/management/commands/send_to_email_mirror.py index 8674f07ce6466..1bdee441a7c0f 100644 --- a/zerver/management/commands/send_to_email_mirror.py +++ b/zerver/management/commands/send_to_email_mirror.py @@ -13,7 +13,9 @@ from zerver.lib.email_mirror import mirror_email_message from zerver.lib.email_mirror_helpers import encode_email_address from zerver.lib.management import ZulipBaseCommand -from zerver.models import Realm, get_realm, get_stream +from zerver.models import Realm +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream # This command loads an email from a specified file and sends it # to the email mirror. Simple emails can be passed in a JSON file, diff --git a/zerver/management/commands/send_webhook_fixture_message.py b/zerver/management/commands/send_webhook_fixture_message.py index d88f04055fbf0..3e6456d034f45 100644 --- a/zerver/management/commands/send_webhook_fixture_message.py +++ b/zerver/management/commands/send_webhook_fixture_message.py @@ -9,7 +9,7 @@ from zerver.lib.management import ZulipBaseCommand from zerver.lib.webhooks.common import standardize_headers -from zerver.models import get_realm +from zerver.models.realms import get_realm class Command(ZulipBaseCommand): diff --git a/zerver/middleware.py b/zerver/middleware.py index cd1b3a9b29a06..5e4c7ecc789ef 100644 --- a/zerver/middleware.py +++ b/zerver/middleware.py @@ -42,7 +42,8 @@ ) from zerver.lib.subdomains import get_subdomain from zerver.lib.user_agent import parse_user_agent -from zerver.models import Realm, get_realm +from zerver.models import Realm +from zerver.models.realms import get_realm ParamT = ParamSpec("ParamT") logger = logging.getLogger("zulip.requests") diff --git a/zerver/migrations/0001_initial.py b/zerver/migrations/0001_initial.py index ee29c5468c8bc..c5145b64b36bb 100644 --- a/zerver/migrations/0001_initial.py +++ b/zerver/migrations/0001_initial.py @@ -12,7 +12,7 @@ from django.db.migrations.state import StateApps from django.db.models.functions import Upper -from zerver.models import generate_email_token_for_stream +from zerver.models.streams import generate_email_token_for_stream def migrate_existing_attachment_data( diff --git a/zerver/migrations/0212_make_stream_email_token_unique.py b/zerver/migrations/0212_make_stream_email_token_unique.py index aa63c493265a2..2e77c08631d4d 100644 --- a/zerver/migrations/0212_make_stream_email_token_unique.py +++ b/zerver/migrations/0212_make_stream_email_token_unique.py @@ -2,7 +2,7 @@ from django.db import migrations, models -from zerver.models import generate_email_token_for_stream +from zerver.models.streams import generate_email_token_for_stream class Migration(migrations.Migration): diff --git a/zerver/migrations/0298_fix_realmauditlog_format.py b/zerver/migrations/0298_fix_realmauditlog_format.py index d63496e9916d3..c243ce3f5ea9b 100644 --- a/zerver/migrations/0298_fix_realmauditlog_format.py +++ b/zerver/migrations/0298_fix_realmauditlog_format.py @@ -31,7 +31,7 @@ def update_realmauditlog_values(apps: StateApps, schema_editor: BaseDatabaseSche } """ RealmAuditLog = apps.get_model("zerver", "RealmAuditLog") - # Constants from models.py + # Constants from models/realm_audit_logs.py USER_DEFAULT_SENDING_STREAM_CHANGED = 129 USER_DEFAULT_REGISTER_STREAM_CHANGED = 130 USER_DEFAULT_ALL_PUBLIC_STREAMS_CHANGED = 131 diff --git a/zerver/migrations/0440_realmfilter_url_template.py b/zerver/migrations/0440_realmfilter_url_template.py index 6f714d47deef4..15d4913c88a7a 100644 --- a/zerver/migrations/0440_realmfilter_url_template.py +++ b/zerver/migrations/0440_realmfilter_url_template.py @@ -2,7 +2,7 @@ from django.db import migrations, models -from zerver.models import url_template_validator +from zerver.models.linkifiers import url_template_validator class Migration(migrations.Migration): diff --git a/zerver/migrations/0442_remove_realmfilter_url_format_string.py b/zerver/migrations/0442_remove_realmfilter_url_format_string.py index e1256e5dc53b1..be7c6df8a9dc4 100644 --- a/zerver/migrations/0442_remove_realmfilter_url_format_string.py +++ b/zerver/migrations/0442_remove_realmfilter_url_format_string.py @@ -1,6 +1,6 @@ from django.db import migrations, models -from zerver.models import url_template_validator +from zerver.models.linkifiers import url_template_validator class Migration(migrations.Migration): diff --git a/zerver/migrations/0462_realmplayground_url_template.py b/zerver/migrations/0462_realmplayground_url_template.py index 4b17a43ed4283..72515b06f914a 100644 --- a/zerver/migrations/0462_realmplayground_url_template.py +++ b/zerver/migrations/0462_realmplayground_url_template.py @@ -2,7 +2,7 @@ from django.db import migrations, models -from zerver.models import url_template_validator +from zerver.models.linkifiers import url_template_validator class Migration(migrations.Migration): diff --git a/zerver/migrations/0464_remove_realmplayground_url_prefix.py b/zerver/migrations/0464_remove_realmplayground_url_prefix.py index 3b5358ff049a3..05e61aae59fee 100644 --- a/zerver/migrations/0464_remove_realmplayground_url_prefix.py +++ b/zerver/migrations/0464_remove_realmplayground_url_prefix.py @@ -2,7 +2,7 @@ from django.db import migrations, models -from zerver.models import url_template_validator +from zerver.models.linkifiers import url_template_validator class Migration(migrations.Migration): diff --git a/zerver/migrations/0468_rename_followup_day_email_templates.py b/zerver/migrations/0468_rename_followup_day_email_templates.py index 56532243b839b..1d393adc761eb 100644 --- a/zerver/migrations/0468_rename_followup_day_email_templates.py +++ b/zerver/migrations/0468_rename_followup_day_email_templates.py @@ -6,7 +6,7 @@ from django.db.models import F, Func, JSONField, TextField, Value from django.db.models.functions import Cast -# ScheduledMessage.type for onboarding emails from zerver/models.py +# ScheduledMessage.type for onboarding emails from zerver/models/scheduled_jobs.py WELCOME = 1 diff --git a/zerver/migrations/0481_alter_realm_uuid_alter_realm_uuid_owner_secret.py b/zerver/migrations/0481_alter_realm_uuid_alter_realm_uuid_owner_secret.py index c30da3f7db20b..bacf183d63d40 100644 --- a/zerver/migrations/0481_alter_realm_uuid_alter_realm_uuid_owner_secret.py +++ b/zerver/migrations/0481_alter_realm_uuid_alter_realm_uuid_owner_secret.py @@ -4,7 +4,7 @@ from django.db import migrations, models -from zerver.models import generate_realm_uuid_owner_secret +from zerver.models.realms import generate_realm_uuid_owner_secret class Migration(migrations.Migration): diff --git a/zerver/migrations/0495_scheduledmessage_read_by_sender.py b/zerver/migrations/0495_scheduledmessage_read_by_sender.py new file mode 100644 index 0000000000000..6563cde63dfa0 --- /dev/null +++ b/zerver/migrations/0495_scheduledmessage_read_by_sender.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.8 on 2023-12-14 00:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0494_realmuserdefault_automatically_follow_topics_where_mentioned_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="scheduledmessage", + name="read_by_sender", + field=models.BooleanField(null=True), + ), + ] diff --git a/zerver/migrations/0496_alter_scheduledmessage_read_by_sender.py b/zerver/migrations/0496_alter_scheduledmessage_read_by_sender.py new file mode 100644 index 0000000000000..2411325e11108 --- /dev/null +++ b/zerver/migrations/0496_alter_scheduledmessage_read_by_sender.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.8 on 2023-12-14 06:18 + +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps +from django.db.models import F, Q +from django.db.models.functions import Lower + + +def populate_read_by_sender(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None: + ScheduledMessage = apps.get_model("zerver", "ScheduledMessage") + ScheduledMessage.objects.annotate( + sending_client_name_lower=Lower("sending_client__name") + ).filter( + Q( + sending_client_name_lower__in=( + "zulipandroid", + "zulipios", + "zulipdesktop", + "zulipmobile", + "zulipelectron", + "zulipterminal", + "snipe", + "website", + "ios", + "android", + ) + ) + | Q(sending_client_name_lower__contains="desktop app"), + ~Q(recipient=F("sender__recipient")), + read_by_sender=None, + ).update( + read_by_sender=True + ) + ScheduledMessage.objects.filter(read_by_sender=None).update(read_by_sender=False) + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0495_scheduledmessage_read_by_sender"), + ] + + operations = [ + migrations.RunPython( + populate_read_by_sender, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name="scheduledmessage", + name="read_by_sender", + field=models.BooleanField(), + ), + ] diff --git a/zerver/models.py b/zerver/models.py deleted file mode 100644 index ad506b5153073..0000000000000 --- a/zerver/models.py +++ /dev/null @@ -1,5255 +0,0 @@ -# https://github.com/typeddjango/django-stubs/issues/1698 -# mypy: disable-error-code="explicit-override" - -import hashlib -import secrets -import time -from collections import defaultdict -from datetime import datetime, timedelta, timezone -from email.headerregistry import Address -from enum import Enum -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - List, - Optional, - Pattern, - Set, - Tuple, - TypedDict, - TypeVar, - Union, -) -from uuid import uuid4 - -import django.contrib.auth -import orjson -import re2 -import uri_template -from bitfield import BitField -from bitfield.types import Bit, BitHandler -from django.conf import settings -from django.contrib.auth.models import ( - AbstractBaseUser, - AnonymousUser, - PermissionsMixin, - UserManager, -) -from django.contrib.contenttypes.fields import GenericRelation -from django.contrib.postgres.indexes import GinIndex -from django.contrib.postgres.search import SearchVectorField -from django.core.exceptions import ValidationError -from django.core.serializers.json import DjangoJSONEncoder -from django.core.validators import MinLengthValidator, RegexValidator, validate_email -from django.db import models, transaction -from django.db.backends.base.base import BaseDatabaseWrapper -from django.db.models import CASCADE, Exists, F, OuterRef, Q, QuerySet, Sum -from django.db.models.functions import Lower, Upper -from django.db.models.signals import post_delete, post_save, pre_delete -from django.db.models.sql.compiler import SQLCompiler -from django.utils.timezone import now as timezone_now -from django.utils.translation import gettext as _ -from django.utils.translation import gettext_lazy -from django_cte import CTEManager -from django_stubs_ext import StrPromise, ValuesQuerySet -from typing_extensions import override - -from confirmation import settings as confirmation_settings -from zerver.lib import cache -from zerver.lib.cache import ( - active_non_guest_user_ids_cache_key, - active_user_ids_cache_key, - bot_dict_fields, - bot_dicts_in_realm_cache_key, - bot_profile_cache_key, - cache_delete, - cache_set, - cache_with_key, - flush_message, - flush_muting_users_cache, - flush_realm, - flush_stream, - flush_submessage, - flush_used_upload_space_cache, - flush_user_profile, - get_realm_used_upload_space_cache_key, - realm_alert_words_automaton_cache_key, - realm_alert_words_cache_key, - realm_user_dict_fields, - realm_user_dicts_cache_key, - user_profile_by_api_key_cache_key, - user_profile_by_id_cache_key, - user_profile_cache_key, -) -from zerver.lib.exceptions import JsonableError, RateLimitedError -from zerver.lib.per_request_cache import ( - flush_per_request_cache, - return_same_value_during_entire_request, -) -from zerver.lib.pysa import mark_sanitized -from zerver.lib.timestamp import datetime_to_timestamp -from zerver.lib.types import ( - DefaultStreamDict, - ExtendedFieldElement, - ExtendedValidator, - FieldElement, - GroupPermissionSetting, - LinkifierDict, - ProfileData, - ProfileDataElementBase, - ProfileDataElementValue, - RawUserDict, - RealmPlaygroundDict, - RealmUserValidator, - UnspecifiedValue, - UserDisplayRecipient, - UserFieldElement, - Validator, -) -from zerver.lib.utils import generate_api_key -from zerver.lib.validator import ( - check_date, - check_int, - check_list, - check_long_string, - check_short_string, - check_url, - validate_select_field, -) - -MAX_TOPIC_NAME_LENGTH = 60 -MAX_LANGUAGE_ID_LENGTH: int = 50 - -SECONDS_PER_DAY = 86400 - -if TYPE_CHECKING: - # We use ModelBackend only for typing. Importing it otherwise causes circular dependency. - from django.contrib.auth.backends import ModelBackend - - -class EmojiInfo(TypedDict): - id: str - name: str - source_url: str - deactivated: bool - author_id: Optional[int] - still_url: Optional[str] - - -@models.Field.register_lookup -class AndZero(models.Lookup[int]): - lookup_name = "andz" - - @override - def as_sql( - self, compiler: SQLCompiler, connection: BaseDatabaseWrapper - ) -> Tuple[str, List[Union[str, int]]]: # nocoverage # currently only used in migrations - lhs, lhs_params = self.process_lhs(compiler, connection) - rhs, rhs_params = self.process_rhs(compiler, connection) - return f"{lhs} & {rhs} = 0", lhs_params + rhs_params - - -@models.Field.register_lookup -class AndNonZero(models.Lookup[int]): - lookup_name = "andnz" - - @override - def as_sql( - self, compiler: SQLCompiler, connection: BaseDatabaseWrapper - ) -> Tuple[str, List[Union[str, int]]]: # nocoverage # currently only used in migrations - lhs, lhs_params = self.process_lhs(compiler, connection) - rhs, rhs_params = self.process_rhs(compiler, connection) - return f"{lhs} & {rhs} != 0", lhs_params + rhs_params - - -ModelT = TypeVar("ModelT", bound=models.Model) -RowT = TypeVar("RowT") - - -def query_for_ids( - query: ValuesQuerySet[ModelT, RowT], - user_ids: List[int], - field: str, -) -> ValuesQuerySet[ModelT, RowT]: - """ - This function optimizes searches of the form - `user_profile_id in (1, 2, 3, 4)` by quickly - building the where clauses. Profiling shows significant - speedups over the normal Django-based approach. - - Use this very carefully! Also, the caller should - guard against empty lists of user_ids. - """ - assert user_ids - clause = f"{field} IN %s" - query = query.extra( - where=[clause], - params=(tuple(user_ids),), - ) - return query - - -@return_same_value_during_entire_request -def get_display_recipient_by_id( - recipient_id: int, recipient_type: int, recipient_type_id: Optional[int] -) -> List[UserDisplayRecipient]: - """ - returns: an object describing the recipient (using a cache). - If the type is a stream, the type_id must be an int; a string is returned. - Otherwise, type_id may be None; an array of recipient dicts is returned. - """ - # Have to import here, to avoid circular dependency. - from zerver.lib.display_recipient import get_display_recipient_remote_cache - - return get_display_recipient_remote_cache(recipient_id, recipient_type, recipient_type_id) - - -def get_display_recipient(recipient: "Recipient") -> List[UserDisplayRecipient]: - return get_display_recipient_by_id( - recipient.id, - recipient.type, - recipient.type_id, - ) - - -def get_recipient_ids( - recipient: Optional["Recipient"], user_profile_id: int -) -> Tuple[List[int], str]: - if recipient is None: - recipient_type_str = "" - to = [] - elif recipient.type == Recipient.STREAM: - recipient_type_str = "stream" - to = [recipient.type_id] - else: - recipient_type_str = "private" - if recipient.type == Recipient.PERSONAL: - to = [recipient.type_id] - else: - to = [] - for r in get_display_recipient(recipient): - assert not isinstance(r, str) # It will only be a string for streams - if r["id"] != user_profile_id: - to.append(r["id"]) - return to, recipient_type_str - - -def get_all_custom_emoji_for_realm_cache_key(realm_id: int) -> str: - return f"realm_emoji:{realm_id}" - - -# This simple call-once caching saves ~500us in auth_enabled_helper, -# which is a significant optimization for common_context. Note that -# these values cannot change in a running production system, but do -# regularly change within unit tests; we address the latter by calling -# clear_supported_auth_backends_cache in our standard tearDown code. -supported_backends: Optional[List["ModelBackend"]] = None - - -def supported_auth_backends() -> List["ModelBackend"]: - global supported_backends - # Caching temporarily disabled for debugging - supported_backends = django.contrib.auth.get_backends() - assert supported_backends is not None - return supported_backends - - -def clear_supported_auth_backends_cache() -> None: - global supported_backends - supported_backends = None - - -class SystemGroups: - FULL_MEMBERS = "role:fullmembers" - EVERYONE_ON_INTERNET = "role:internet" - OWNERS = "role:owners" - ADMINISTRATORS = "role:administrators" - MODERATORS = "role:moderators" - MEMBERS = "role:members" - EVERYONE = "role:everyone" - NOBODY = "role:nobody" - - -class RealmAuthenticationMethod(models.Model): - """ - Tracks which authentication backends are enabled for a realm. - An enabled backend is represented in this table a row with appropriate - .realm value and .name matching the name of the target backend in the - AUTH_BACKEND_NAME_MAP dict. - """ - - realm = models.ForeignKey("Realm", on_delete=CASCADE, db_index=True) - name = models.CharField(max_length=80) - - class Meta: - unique_together = ("realm", "name") - - -def generate_realm_uuid_owner_secret() -> str: - token = generate_api_key() - - # We include a prefix to facilitate scanning for accidental - # disclosure of secrets e.g. in Github commit pushes. - return f"zuliprealm_{token}" - - -class OrgTypeEnum(Enum): - Unspecified = 0 - Business = 10 - OpenSource = 20 - EducationNonProfit = 30 - Education = 35 - Research = 40 - Event = 50 - NonProfit = 60 - Government = 70 - PoliticalGroup = 80 - Community = 90 - Personal = 100 - Other = 1000 - - -class OrgTypeDict(TypedDict): - name: str - id: int - hidden: bool - display_order: int - onboarding_zulip_guide_url: Optional[str] - - -class Realm(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 - MAX_REALM_NAME_LENGTH = 40 - MAX_REALM_DESCRIPTION_LENGTH = 1000 - MAX_REALM_SUBDOMAIN_LENGTH = 40 - MAX_REALM_REDIRECT_URL_LENGTH = 128 - - INVITES_STANDARD_REALM_DAILY_MAX = 3000 - MESSAGE_VISIBILITY_LIMITED = 10000 - SUBDOMAIN_FOR_ROOT_DOMAIN = "" - WILDCARD_MENTION_THRESHOLD = 15 - - # User-visible display name and description used on e.g. the organization homepage - name = models.CharField(max_length=MAX_REALM_NAME_LENGTH) - description = models.TextField(default="") - - # A short, identifier-like name for the organization. Used in subdomains; - # e.g. on a server at example.com, an org with string_id `foo` is reached - # at `foo.example.com`. - string_id = models.CharField(max_length=MAX_REALM_SUBDOMAIN_LENGTH, unique=True) - - # uuid and a secret for the sake of per-realm authentication with the push notification - # bouncer. - uuid = models.UUIDField(default=uuid4, unique=True) - uuid_owner_secret = models.TextField(default=generate_realm_uuid_owner_secret) - # Whether push notifications are working for this realm, and - # whether there is a specific date at which we expect that to - # cease to be the case. - push_notifications_enabled = models.BooleanField(default=False, db_index=True) - push_notifications_enabled_end_timestamp = models.DateTimeField(default=None, null=True) - - date_created = models.DateTimeField(default=timezone_now) - demo_organization_scheduled_deletion_date = models.DateTimeField(default=None, null=True) - deactivated = models.BooleanField(default=False) - - # Redirect URL if the Realm has moved to another server - deactivated_redirect = models.URLField(max_length=MAX_REALM_REDIRECT_URL_LENGTH, null=True) - - # See RealmDomain for the domains that apply for a given organization. - emails_restricted_to_domains = models.BooleanField(default=False) - - invite_required = models.BooleanField(default=True) - - _max_invites = models.IntegerField(null=True, db_column="max_invites") - disallow_disposable_email_addresses = models.BooleanField(default=True) - - # Allow users to access web-public streams without login. This - # setting also controls API access of web-public streams. - enable_spectator_access = models.BooleanField(default=False) - - # Whether organization has given permission to be advertised in the - # Zulip communities directory. - want_advertise_in_communities_directory = models.BooleanField(default=False, db_index=True) - - # Whether the organization has enabled inline image and URL previews. - inline_image_preview = models.BooleanField(default=True) - inline_url_embed_preview = models.BooleanField(default=False) - - # Whether digest emails are enabled for the organization. - digest_emails_enabled = models.BooleanField(default=False) - # Day of the week on which the digest is sent (default: Tuesday). - digest_weekday = models.SmallIntegerField(default=1) - - send_welcome_emails = models.BooleanField(default=True) - message_content_allowed_in_email_notifications = models.BooleanField(default=True) - - mandatory_topics = models.BooleanField(default=False) - - name_changes_disabled = models.BooleanField(default=False) - email_changes_disabled = models.BooleanField(default=False) - avatar_changes_disabled = models.BooleanField(default=False) - - POLICY_MEMBERS_ONLY = 1 - POLICY_ADMINS_ONLY = 2 - POLICY_FULL_MEMBERS_ONLY = 3 - POLICY_MODERATORS_ONLY = 4 - POLICY_EVERYONE = 5 - POLICY_NOBODY = 6 - POLICY_OWNERS_ONLY = 7 - - COMMON_POLICY_TYPES = [ - POLICY_MEMBERS_ONLY, - POLICY_ADMINS_ONLY, - POLICY_FULL_MEMBERS_ONLY, - POLICY_MODERATORS_ONLY, - ] - - COMMON_MESSAGE_POLICY_TYPES = [ - POLICY_MEMBERS_ONLY, - POLICY_ADMINS_ONLY, - POLICY_FULL_MEMBERS_ONLY, - POLICY_MODERATORS_ONLY, - POLICY_EVERYONE, - ] - - INVITE_TO_REALM_POLICY_TYPES = [ - POLICY_MEMBERS_ONLY, - POLICY_ADMINS_ONLY, - POLICY_FULL_MEMBERS_ONLY, - POLICY_MODERATORS_ONLY, - POLICY_NOBODY, - ] - - # We don't allow granting roles less than Moderator access to - # create web-public streams, since it's a sensitive feature that - # can be used to send spam. - CREATE_WEB_PUBLIC_STREAM_POLICY_TYPES = [ - POLICY_ADMINS_ONLY, - POLICY_MODERATORS_ONLY, - POLICY_OWNERS_ONLY, - POLICY_NOBODY, - ] - - EDIT_TOPIC_POLICY_TYPES = [ - POLICY_MEMBERS_ONLY, - POLICY_ADMINS_ONLY, - POLICY_FULL_MEMBERS_ONLY, - POLICY_MODERATORS_ONLY, - POLICY_EVERYONE, - POLICY_NOBODY, - ] - - MOVE_MESSAGES_BETWEEN_STREAMS_POLICY_TYPES = INVITE_TO_REALM_POLICY_TYPES - - DEFAULT_MOVE_MESSAGE_LIMIT_SECONDS = 7 * SECONDS_PER_DAY - - move_messages_within_stream_limit_seconds = models.PositiveIntegerField( - default=DEFAULT_MOVE_MESSAGE_LIMIT_SECONDS, null=True - ) - - move_messages_between_streams_limit_seconds = models.PositiveIntegerField( - default=DEFAULT_MOVE_MESSAGE_LIMIT_SECONDS, null=True - ) - - # Who in the organization is allowed to add custom emojis. - add_custom_emoji_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) - - # Who in the organization is allowed to create streams. - create_public_stream_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) - create_private_stream_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) - create_web_public_stream_policy = models.PositiveSmallIntegerField(default=POLICY_OWNERS_ONLY) - - # Who in the organization is allowed to delete messages they themselves sent. - delete_own_message_policy = models.PositiveSmallIntegerField(default=POLICY_ADMINS_ONLY) - - # Who in the organization is allowed to edit topics of any message. - edit_topic_policy = models.PositiveSmallIntegerField(default=POLICY_EVERYONE) - - # Who in the organization is allowed to invite other users to organization. - invite_to_realm_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) - - # UserGroup whose members are allowed to create invite link. - create_multiuse_invite_group = models.ForeignKey( - "UserGroup", on_delete=models.RESTRICT, related_name="+" - ) - - # on_delete field here is set to RESTRICT because we don't want to allow - # deleting a user group in case it is referenced by this setting. - # We are not using PROTECT since we want to allow deletion of user groups - # when realm itself is deleted. - can_access_all_users_group = models.ForeignKey( - "UserGroup", on_delete=models.RESTRICT, related_name="+" - ) - - # Who in the organization is allowed to invite other users to streams. - invite_to_stream_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) - - # Who in the organization is allowed to move messages between streams. - move_messages_between_streams_policy = models.PositiveSmallIntegerField( - default=POLICY_ADMINS_ONLY - ) - - user_group_edit_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) - - PRIVATE_MESSAGE_POLICY_UNLIMITED = 1 - PRIVATE_MESSAGE_POLICY_DISABLED = 2 - private_message_policy = models.PositiveSmallIntegerField( - default=PRIVATE_MESSAGE_POLICY_UNLIMITED - ) - PRIVATE_MESSAGE_POLICY_TYPES = [ - PRIVATE_MESSAGE_POLICY_UNLIMITED, - PRIVATE_MESSAGE_POLICY_DISABLED, - ] - - # Global policy for who is allowed to use wildcard mentions in - # streams with a large number of subscribers. Anyone can use - # wildcard mentions in small streams regardless of this setting. - WILDCARD_MENTION_POLICY_EVERYONE = 1 - WILDCARD_MENTION_POLICY_MEMBERS = 2 - WILDCARD_MENTION_POLICY_FULL_MEMBERS = 3 - WILDCARD_MENTION_POLICY_ADMINS = 5 - WILDCARD_MENTION_POLICY_NOBODY = 6 - WILDCARD_MENTION_POLICY_MODERATORS = 7 - wildcard_mention_policy = models.PositiveSmallIntegerField( - default=WILDCARD_MENTION_POLICY_ADMINS, - ) - WILDCARD_MENTION_POLICY_TYPES = [ - WILDCARD_MENTION_POLICY_EVERYONE, - WILDCARD_MENTION_POLICY_MEMBERS, - WILDCARD_MENTION_POLICY_FULL_MEMBERS, - WILDCARD_MENTION_POLICY_ADMINS, - WILDCARD_MENTION_POLICY_NOBODY, - WILDCARD_MENTION_POLICY_MODERATORS, - ] - - # Threshold in days for new users to create streams, and potentially take - # some other actions. - waiting_period_threshold = models.PositiveIntegerField(default=0) - - DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS = ( - 600 # if changed, also change in admin.js, setting_org.js - ) - MESSAGE_TIME_LIMIT_SETTING_SPECIAL_VALUES_MAP = { - "unlimited": None, - } - message_content_delete_limit_seconds = models.PositiveIntegerField( - default=DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS, null=True - ) - - allow_message_editing = models.BooleanField(default=True) - DEFAULT_MESSAGE_CONTENT_EDIT_LIMIT_SECONDS = ( - 600 # if changed, also change in admin.js, setting_org.js - ) - message_content_edit_limit_seconds = models.PositiveIntegerField( - default=DEFAULT_MESSAGE_CONTENT_EDIT_LIMIT_SECONDS, null=True - ) - - # Whether users have access to message edit history - allow_edit_history = models.BooleanField(default=True) - - # Defaults for new users - default_language = models.CharField(default="en", max_length=MAX_LANGUAGE_ID_LENGTH) - - DEFAULT_NOTIFICATION_STREAM_NAME = "general" - INITIAL_PRIVATE_STREAM_NAME = "core team" - STREAM_EVENTS_NOTIFICATION_TOPIC = gettext_lazy("stream events") - notifications_stream = models.ForeignKey( - "Stream", - related_name="+", - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - signup_notifications_stream = models.ForeignKey( - "Stream", - related_name="+", - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - - MESSAGE_RETENTION_SPECIAL_VALUES_MAP = { - "unlimited": -1, - } - # For old messages being automatically deleted - message_retention_days = models.IntegerField(null=False, default=-1) - - # When non-null, all but the latest this many messages in the organization - # are inaccessible to users (but not deleted). - message_visibility_limit = models.IntegerField(null=True) - - # Messages older than this message ID in the organization are inaccessible. - first_visible_message_id = models.IntegerField(default=0) - - # Valid org types - ORG_TYPES: Dict[str, OrgTypeDict] = { - "unspecified": { - "name": "Unspecified", - "id": OrgTypeEnum.Unspecified.value, - "hidden": True, - "display_order": 0, - "onboarding_zulip_guide_url": None, - }, - "business": { - "name": "Business", - "id": OrgTypeEnum.Business.value, - "hidden": False, - "display_order": 1, - "onboarding_zulip_guide_url": "https://zulip.com/for/business/", - }, - "opensource": { - "name": "Open-source project", - "id": OrgTypeEnum.OpenSource.value, - "hidden": False, - "display_order": 2, - "onboarding_zulip_guide_url": "https://zulip.com/for/open-source/", - }, - "education_nonprofit": { - "name": "Education (non-profit)", - "id": OrgTypeEnum.EducationNonProfit.value, - "hidden": False, - "display_order": 3, - "onboarding_zulip_guide_url": "https://zulip.com/for/education/", - }, - "education": { - "name": "Education (for-profit)", - "id": OrgTypeEnum.Education.value, - "hidden": False, - "display_order": 4, - "onboarding_zulip_guide_url": "https://zulip.com/for/education/", - }, - "research": { - "name": "Research", - "id": OrgTypeEnum.Research.value, - "hidden": False, - "display_order": 5, - "onboarding_zulip_guide_url": "https://zulip.com/for/research/", - }, - "event": { - "name": "Event or conference", - "id": OrgTypeEnum.Event.value, - "hidden": False, - "display_order": 6, - "onboarding_zulip_guide_url": "https://zulip.com/for/events/", - }, - "nonprofit": { - "name": "Non-profit (registered)", - "id": OrgTypeEnum.NonProfit.value, - "hidden": False, - "display_order": 7, - "onboarding_zulip_guide_url": "https://zulip.com/for/communities/", - }, - "government": { - "name": "Government", - "id": OrgTypeEnum.Government.value, - "hidden": False, - "display_order": 8, - "onboarding_zulip_guide_url": None, - }, - "political_group": { - "name": "Political group", - "id": OrgTypeEnum.PoliticalGroup.value, - "hidden": False, - "display_order": 9, - "onboarding_zulip_guide_url": None, - }, - "community": { - "name": "Community", - "id": OrgTypeEnum.Community.value, - "hidden": False, - "display_order": 10, - "onboarding_zulip_guide_url": "https://zulip.com/for/communities/", - }, - "personal": { - "name": "Personal", - "id": OrgTypeEnum.Personal.value, - "hidden": False, - "display_order": 100, - "onboarding_zulip_guide_url": None, - }, - "other": { - "name": "Other", - "id": OrgTypeEnum.Other.value, - "hidden": False, - "display_order": 1000, - "onboarding_zulip_guide_url": None, - }, - } - - ORG_TYPE_IDS: List[int] = [t["id"] for t in ORG_TYPES.values()] - - org_type = models.PositiveSmallIntegerField( - default=ORG_TYPES["unspecified"]["id"], - choices=[(t["id"], t["name"]) for t in ORG_TYPES.values()], - ) - - UPGRADE_TEXT_STANDARD = gettext_lazy("Available on Zulip Cloud Standard. Upgrade to access.") - UPGRADE_TEXT_PLUS = gettext_lazy("Available on Zulip Cloud Plus. Upgrade to access.") - # plan_type controls various features around resource/feature - # limitations for a Zulip organization on multi-tenant installations - # like Zulip Cloud. - PLAN_TYPE_SELF_HOSTED = 1 - PLAN_TYPE_LIMITED = 2 - PLAN_TYPE_STANDARD = 3 - PLAN_TYPE_STANDARD_FREE = 4 - PLAN_TYPE_PLUS = 10 - - # Used for creating realms with different plan types. - ALL_PLAN_TYPES = { - PLAN_TYPE_SELF_HOSTED: "self-hosted-plan", - PLAN_TYPE_LIMITED: "limited-plan", - PLAN_TYPE_STANDARD: "standard-plan", - PLAN_TYPE_STANDARD_FREE: "standard-free-plan", - PLAN_TYPE_PLUS: "plus-plan", - } - plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_HOSTED) - - # This value is also being used in web/src/settings_bots.bot_creation_policy_values. - # On updating it here, update it there as well. - BOT_CREATION_EVERYONE = 1 - BOT_CREATION_LIMIT_GENERIC_BOTS = 2 - BOT_CREATION_ADMINS_ONLY = 3 - bot_creation_policy = models.PositiveSmallIntegerField(default=BOT_CREATION_EVERYONE) - BOT_CREATION_POLICY_TYPES = [ - BOT_CREATION_EVERYONE, - BOT_CREATION_LIMIT_GENERIC_BOTS, - BOT_CREATION_ADMINS_ONLY, - ] - - # See upload_quota_bytes; don't interpret upload_quota_gb directly. - UPLOAD_QUOTA_LIMITED = 5 - UPLOAD_QUOTA_STANDARD = 50 - upload_quota_gb = models.IntegerField(null=True) - - VIDEO_CHAT_PROVIDERS = { - "disabled": { - "name": "None", - "id": 0, - }, - "jitsi_meet": { - "name": "Jitsi Meet", - "id": 1, - }, - # ID 2 was used for the now-deleted Google Hangouts. - # ID 3 reserved for optional Zoom, see below. - # ID 4 reserved for optional BigBlueButton, see below. - } - - if settings.VIDEO_ZOOM_CLIENT_ID is not None and settings.VIDEO_ZOOM_CLIENT_SECRET is not None: - VIDEO_CHAT_PROVIDERS["zoom"] = { - "name": "Zoom", - "id": 3, - } - - if settings.BIG_BLUE_BUTTON_SECRET is not None and settings.BIG_BLUE_BUTTON_URL is not None: - VIDEO_CHAT_PROVIDERS["big_blue_button"] = {"name": "BigBlueButton", "id": 4} - - video_chat_provider = models.PositiveSmallIntegerField( - default=VIDEO_CHAT_PROVIDERS["jitsi_meet"]["id"] - ) - - JITSI_SERVER_SPECIAL_VALUES_MAP = {"default": None} - jitsi_server_url = models.URLField(null=True, default=None) - - # Please access this via get_giphy_rating_options. - GIPHY_RATING_OPTIONS = { - "disabled": { - "name": gettext_lazy("GIPHY integration disabled"), - "id": 0, - }, - # Source: https://github.com/Giphy/giphy-js/blob/master/packages/fetch-api/README.md#shared-options - "y": { - "name": gettext_lazy("Allow GIFs rated Y (Very young audience)"), - "id": 1, - }, - "g": { - "name": gettext_lazy("Allow GIFs rated G (General audience)"), - "id": 2, - }, - "pg": { - "name": gettext_lazy("Allow GIFs rated PG (Parental guidance)"), - "id": 3, - }, - "pg-13": { - "name": gettext_lazy("Allow GIFs rated PG-13 (Parental guidance - under 13)"), - "id": 4, - }, - "r": { - "name": gettext_lazy("Allow GIFs rated R (Restricted)"), - "id": 5, - }, - } - - # maximum rating of the GIFs that will be retrieved from GIPHY - giphy_rating = models.PositiveSmallIntegerField(default=GIPHY_RATING_OPTIONS["g"]["id"]) - - default_code_block_language = models.TextField(default="") - - # Whether read receipts are enabled in the organization. If disabled, - # they will not be available regardless of users' personal settings. - enable_read_receipts = models.BooleanField(default=False) - - # Whether clients should display "(guest)" after names of guest users. - enable_guest_user_indicator = models.BooleanField(default=True) - - # Define the types of the various automatically managed properties - property_types: Dict[str, Union[type, Tuple[type, ...]]] = dict( - add_custom_emoji_policy=int, - allow_edit_history=bool, - allow_message_editing=bool, - avatar_changes_disabled=bool, - bot_creation_policy=int, - create_private_stream_policy=int, - create_public_stream_policy=int, - create_web_public_stream_policy=int, - default_code_block_language=str, - default_language=str, - delete_own_message_policy=int, - description=str, - digest_emails_enabled=bool, - digest_weekday=int, - disallow_disposable_email_addresses=bool, - edit_topic_policy=int, - email_changes_disabled=bool, - emails_restricted_to_domains=bool, - enable_guest_user_indicator=bool, - enable_read_receipts=bool, - enable_spectator_access=bool, - giphy_rating=int, - inline_image_preview=bool, - inline_url_embed_preview=bool, - invite_required=bool, - invite_to_realm_policy=int, - invite_to_stream_policy=int, - jitsi_server_url=(str, type(None)), - mandatory_topics=bool, - message_content_allowed_in_email_notifications=bool, - message_content_edit_limit_seconds=(int, type(None)), - message_content_delete_limit_seconds=(int, type(None)), - move_messages_between_streams_limit_seconds=(int, type(None)), - move_messages_within_stream_limit_seconds=(int, type(None)), - message_retention_days=(int, type(None)), - move_messages_between_streams_policy=int, - name=str, - name_changes_disabled=bool, - private_message_policy=int, - push_notifications_enabled=bool, - send_welcome_emails=bool, - user_group_edit_policy=int, - video_chat_provider=int, - waiting_period_threshold=int, - want_advertise_in_communities_directory=bool, - wildcard_mention_policy=int, - ) - - REALM_PERMISSION_GROUP_SETTINGS: Dict[str, GroupPermissionSetting] = dict( - create_multiuse_invite_group=GroupPermissionSetting( - require_system_group=True, - allow_internet_group=False, - allow_owners_group=False, - allow_nobody_group=True, - allow_everyone_group=False, - default_group_name=SystemGroups.ADMINISTRATORS, - id_field_name="create_multiuse_invite_group_id", - ), - can_access_all_users_group=GroupPermissionSetting( - require_system_group=True, - allow_internet_group=False, - allow_owners_group=False, - allow_nobody_group=False, - allow_everyone_group=True, - default_group_name=SystemGroups.EVERYONE, - id_field_name="can_access_all_users_group_id", - allowed_system_groups=[SystemGroups.EVERYONE, SystemGroups.MEMBERS], - ), - ) - - DIGEST_WEEKDAY_VALUES = [0, 1, 2, 3, 4, 5, 6] - - # Icon is the square mobile icon. - ICON_FROM_GRAVATAR = "G" - ICON_UPLOADED = "U" - ICON_SOURCES = ( - (ICON_FROM_GRAVATAR, "Hosted by Gravatar"), - (ICON_UPLOADED, "Uploaded by administrator"), - ) - icon_source = models.CharField( - default=ICON_FROM_GRAVATAR, - choices=ICON_SOURCES, - max_length=1, - ) - icon_version = models.PositiveSmallIntegerField(default=1) - - # Logo is the horizontal logo we show in top-left of web app navbar UI. - LOGO_DEFAULT = "D" - LOGO_UPLOADED = "U" - LOGO_SOURCES = ( - (LOGO_DEFAULT, "Default to Zulip"), - (LOGO_UPLOADED, "Uploaded by administrator"), - ) - logo_source = models.CharField( - default=LOGO_DEFAULT, - choices=LOGO_SOURCES, - max_length=1, - ) - logo_version = models.PositiveSmallIntegerField(default=1) - - night_logo_source = models.CharField( - default=LOGO_DEFAULT, - choices=LOGO_SOURCES, - max_length=1, - ) - night_logo_version = models.PositiveSmallIntegerField(default=1) - - @override - def __str__(self) -> str: - return f"{self.string_id} {self.id}" - - def get_giphy_rating_options(self) -> Dict[str, Dict[str, object]]: - """Wrapper function for GIPHY_RATING_OPTIONS that ensures evaluation - of the lazily evaluated `name` field without modifying the original.""" - return { - rating_type: {"name": str(rating["name"]), "id": rating["id"]} - for rating_type, rating in self.GIPHY_RATING_OPTIONS.items() - } - - def authentication_methods_dict(self) -> Dict[str, bool]: - """Returns the mapping from authentication flags to their status, - showing only those authentication flags that are supported on - the current server (i.e. if EmailAuthBackend is not configured - on the server, this will not return an entry for "Email").""" - # This mapping needs to be imported from here due to the cyclic - # dependency. - from zproject.backends import AUTH_BACKEND_NAME_MAP, all_implemented_backend_names - - ret: Dict[str, bool] = {} - supported_backends = [type(backend) for backend in supported_auth_backends()] - - for backend_name in all_implemented_backend_names(): - backend_class = AUTH_BACKEND_NAME_MAP[backend_name] - if backend_class in supported_backends: - ret[backend_name] = False - for realm_authentication_method in RealmAuthenticationMethod.objects.filter( - realm_id=self.id - ): - backend_class = AUTH_BACKEND_NAME_MAP[realm_authentication_method.name] - if backend_class in supported_backends: - ret[realm_authentication_method.name] = True - return ret - - def get_admin_users_and_bots( - self, include_realm_owners: bool = True - ) -> QuerySet["UserProfile"]: - """Use this in contexts where we want administrative users as well as - bots with administrator privileges, like send_event calls for - notifications to all administrator users. - """ - if include_realm_owners: - roles = [UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER] - else: - roles = [UserProfile.ROLE_REALM_ADMINISTRATOR] - - return UserProfile.objects.filter( - realm=self, - is_active=True, - role__in=roles, - ) - - def get_human_admin_users(self, include_realm_owners: bool = True) -> QuerySet["UserProfile"]: - """Use this in contexts where we want only human users with - administrative privileges, like sending an email to all of a - realm's administrators (bots don't have real email addresses). - """ - if include_realm_owners: - roles = [UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER] - else: - roles = [UserProfile.ROLE_REALM_ADMINISTRATOR] - - return UserProfile.objects.filter( - realm=self, - is_bot=False, - is_active=True, - role__in=roles, - ) - - def get_human_billing_admin_and_realm_owner_users(self) -> QuerySet["UserProfile"]: - return UserProfile.objects.filter( - Q(role=UserProfile.ROLE_REALM_OWNER) | Q(is_billing_admin=True), - realm=self, - is_bot=False, - is_active=True, - ) - - def get_active_users(self) -> QuerySet["UserProfile"]: - return UserProfile.objects.filter(realm=self, is_active=True) - - def get_first_human_user(self) -> Optional["UserProfile"]: - """A useful value for communications with newly created realms. - Has a few fundamental limitations: - - * Its value will be effectively random for realms imported from Slack or - other third-party tools. - * The user may be deactivated, etc., so it's not something that's useful - for features, permissions, etc. - """ - return UserProfile.objects.filter(realm=self, is_bot=False).order_by("id").first() - - def get_human_owner_users(self) -> QuerySet["UserProfile"]: - return UserProfile.objects.filter( - realm=self, is_bot=False, role=UserProfile.ROLE_REALM_OWNER, is_active=True - ) - - def get_bot_domain(self) -> str: - return get_fake_email_domain(self.host) - - def get_notifications_stream(self) -> Optional["Stream"]: - if self.notifications_stream is not None and not self.notifications_stream.deactivated: - return self.notifications_stream - return None - - def get_signup_notifications_stream(self) -> Optional["Stream"]: - if ( - self.signup_notifications_stream is not None - and not self.signup_notifications_stream.deactivated - ): - return self.signup_notifications_stream - return None - - @property - def max_invites(self) -> int: - if self._max_invites is None: - return settings.INVITES_DEFAULT_REALM_DAILY_MAX - return self._max_invites - - @max_invites.setter - def max_invites(self, value: Optional[int]) -> None: - self._max_invites = value - - def upload_quota_bytes(self) -> Optional[int]: - if self.upload_quota_gb is None: - return None - # We describe the quota to users in "GB" or "gigabytes", but actually apply - # it as gibibytes (GiB) to be a bit more generous in case of confusion. - return self.upload_quota_gb << 30 - - # `realm` instead of `self` here to make sure the parameters of the cache key - # function matches the original method. - @cache_with_key( - lambda realm: get_realm_used_upload_space_cache_key(realm.id), timeout=3600 * 24 * 7 - ) - def currently_used_upload_space_bytes(realm) -> int: # noqa: N805 - used_space = Attachment.objects.filter(realm=realm).aggregate(Sum("size"))["size__sum"] - if used_space is None: - return 0 - return used_space - - def ensure_not_on_limited_plan(self) -> None: - if self.plan_type == Realm.PLAN_TYPE_LIMITED: - raise JsonableError(str(self.UPGRADE_TEXT_STANDARD)) - - def can_enable_restricted_user_access_for_guests(self) -> None: - if self.plan_type not in [Realm.PLAN_TYPE_PLUS, Realm.PLAN_TYPE_SELF_HOSTED]: - raise JsonableError(str(self.UPGRADE_TEXT_PLUS)) - - @property - def subdomain(self) -> str: - return self.string_id - - @property - def display_subdomain(self) -> str: - """Likely to be temporary function to avoid signup messages being sent - to an empty topic""" - if self.string_id == "": - return "." - return self.string_id - - @property - def uri(self) -> str: - return settings.EXTERNAL_URI_SCHEME + self.host - - @property - def host(self) -> str: - # Use mark sanitized to prevent false positives from Pysa thinking that - # the host is user controlled. - return mark_sanitized(self.host_for_subdomain(self.subdomain)) - - @staticmethod - def host_for_subdomain(subdomain: str) -> str: - if subdomain == Realm.SUBDOMAIN_FOR_ROOT_DOMAIN: - return settings.EXTERNAL_HOST - default_host = f"{subdomain}.{settings.EXTERNAL_HOST}" - return settings.REALM_HOSTS.get(subdomain, default_host) - - @property - def is_zephyr_mirror_realm(self) -> bool: - return self.string_id == "zephyr" - - @property - def webathena_enabled(self) -> bool: - return self.is_zephyr_mirror_realm - - @property - def presence_disabled(self) -> bool: - return self.is_zephyr_mirror_realm - - def web_public_streams_enabled(self) -> bool: - if not settings.WEB_PUBLIC_STREAMS_ENABLED: - # To help protect against accidentally web-public streams in - # self-hosted servers, we require the feature to be enabled at - # the server level before it is available to users. - return False - - if self.plan_type == Realm.PLAN_TYPE_LIMITED: - # In Zulip Cloud, we also require a paid or sponsored - # plan, to protect against the spam/abuse attacks that - # target every open Internet service that can host files. - return False - - if not self.enable_spectator_access: - return False - - return True - - def has_web_public_streams(self) -> bool: - if not self.web_public_streams_enabled(): - return False - - from zerver.lib.streams import get_web_public_streams_queryset - - return get_web_public_streams_queryset(self).exists() - - def allow_web_public_streams_access(self) -> bool: - """ - If any of the streams in the realm is web - public and `enable_spectator_access` and - settings.WEB_PUBLIC_STREAMS_ENABLED is True, - then the Realm is web-public. - """ - return self.has_web_public_streams() - - -post_save.connect(flush_realm, sender=Realm) - - -# We register realm cache flushing in a duplicate way to be run both -# pre_delete and post_delete on purpose: -# 1. pre_delete is needed because flush_realm wants to flush the UserProfile caches, -# and UserProfile objects are deleted via on_delete=CASCADE before the post_delete handler -# is called, which results in the `flush_realm` logic not having access to the details -# for the deleted users if called at that time. -# 2. post_delete is run as a precaution to reduce the risk of races where items might be -# added to the cache after the pre_delete handler but before the save. -# Note that it does not eliminate this risk, not least because it only flushes -# the realm cache, and not the user caches, for the reasons explained above. -def realm_pre_and_post_delete_handler(*, instance: Realm, **kwargs: object) -> None: - # This would be better as a functools.partial, but for some reason - # Django doesn't call it even when it's registered as a post_delete handler. - flush_realm(instance=instance, from_deletion=True) - - -pre_delete.connect(realm_pre_and_post_delete_handler, sender=Realm) -post_delete.connect(realm_pre_and_post_delete_handler, sender=Realm) - - -def get_realm(string_id: str) -> Realm: - return Realm.objects.get(string_id=string_id) - - -def get_realm_by_id(realm_id: int) -> Realm: - return Realm.objects.get(id=realm_id) - - -def name_changes_disabled(realm: Optional[Realm]) -> bool: - if realm is None: - return settings.NAME_CHANGES_DISABLED - return settings.NAME_CHANGES_DISABLED or realm.name_changes_disabled - - -def avatar_changes_disabled(realm: Realm) -> bool: - return settings.AVATAR_CHANGES_DISABLED or realm.avatar_changes_disabled - - -def get_org_type_display_name(org_type: int) -> str: - for realm_type_details in Realm.ORG_TYPES.values(): - if realm_type_details["id"] == org_type: - return realm_type_details["name"] - - return "" - - -class RealmDomain(models.Model): - """For an organization with emails_restricted_to_domains enabled, the list of - allowed domains""" - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - # should always be stored lowercase - domain = models.CharField(max_length=80, db_index=True) - allow_subdomains = models.BooleanField(default=False) - - class Meta: - unique_together = ("realm", "domain") - - -class DomainNotAllowedForRealmError(Exception): - pass - - -class DisposableEmailError(Exception): - pass - - -class EmailContainsPlusError(Exception): - pass - - -class RealmDomainDict(TypedDict): - domain: str - allow_subdomains: bool - - -def get_realm_domains(realm: Realm) -> List[RealmDomainDict]: - return list(realm.realmdomain_set.values("domain", "allow_subdomains")) - - -class RealmEmoji(models.Model): - author = models.ForeignKey( - "UserProfile", - blank=True, - null=True, - on_delete=CASCADE, - ) - realm = models.ForeignKey(Realm, on_delete=CASCADE) - name = models.TextField( - validators=[ - MinLengthValidator(1), - # The second part of the regex (negative lookbehind) disallows names - # ending with one of the punctuation characters. - RegexValidator( - regex=r"^[0-9a-z.\-_]+(? str: - return f"{self.realm.string_id}: {self.id} {self.name} {self.deactivated} {self.file_name}" - - -def get_all_custom_emoji_for_realm_uncached(realm_id: int) -> Dict[str, EmojiInfo]: - # RealmEmoji objects with file_name=None are still in the process - # of being uploaded, and we expect to be cleaned up by a - # try/finally block if the upload fails, so it's correct to - # exclude them. - query = RealmEmoji.objects.filter(realm_id=realm_id).exclude( - file_name=None, - ) - d = {} - from zerver.lib.emoji import get_emoji_url - - for realm_emoji in query.all(): - author_id = realm_emoji.author_id - assert realm_emoji.file_name is not None - emoji_url = get_emoji_url(realm_emoji.file_name, realm_emoji.realm_id) - - emoji_dict: EmojiInfo = dict( - id=str(realm_emoji.id), - name=realm_emoji.name, - source_url=emoji_url, - deactivated=realm_emoji.deactivated, - author_id=author_id, - still_url=None, - ) - - if realm_emoji.is_animated: - # For animated emoji, we include still_url with a static - # version of the image, so that clients can display the - # emoji in a less distracting (not animated) fashion when - # desired. - emoji_dict["still_url"] = get_emoji_url( - realm_emoji.file_name, realm_emoji.realm_id, still=True - ) - - d[str(realm_emoji.id)] = emoji_dict - - return d - - -@cache_with_key(get_all_custom_emoji_for_realm_cache_key, timeout=3600 * 24 * 7) -def get_all_custom_emoji_for_realm(realm_id: int) -> Dict[str, EmojiInfo]: - return get_all_custom_emoji_for_realm_uncached(realm_id) - - -def get_name_keyed_dict_for_active_realm_emoji(realm_id: int) -> Dict[str, EmojiInfo]: - # It's important to use the cached version here. - realm_emojis = get_all_custom_emoji_for_realm(realm_id) - return {row["name"]: row for row in realm_emojis.values() if not row["deactivated"]} - - -def flush_realm_emoji(*, instance: RealmEmoji, **kwargs: object) -> None: - if instance.file_name is None: - # Because we construct RealmEmoji.file_name using the ID for - # the RealmEmoji object, it will always have file_name=None, - # and then it'll be updated with the actual filename as soon - # as the upload completes successfully. - # - # Doing nothing when file_name=None is the best option, since - # such an object shouldn't have been cached yet, and this - # function will be called again when file_name is set. - return - realm_id = instance.realm_id - cache_set( - get_all_custom_emoji_for_realm_cache_key(realm_id), - get_all_custom_emoji_for_realm_uncached(realm_id), - timeout=3600 * 24 * 7, - ) - - -post_save.connect(flush_realm_emoji, sender=RealmEmoji) -post_delete.connect(flush_realm_emoji, sender=RealmEmoji) - - -def filter_pattern_validator(value: str) -> Pattern[str]: - try: - # Do not write errors to stderr (this still raises exceptions) - options = re2.Options() - options.log_errors = False - - regex = re2.compile(value, options=options) - except re2.error as e: - if len(e.args) >= 1: - if isinstance(e.args[0], str): # nocoverage - raise ValidationError(_("Bad regular expression: {regex}").format(regex=e.args[0])) - if isinstance(e.args[0], bytes): - raise ValidationError( - _("Bad regular expression: {regex}").format(regex=e.args[0].decode()) - ) - raise ValidationError(_("Unknown regular expression error")) # nocoverage - - return regex - - -def url_template_validator(value: str) -> None: - """Validate as a URL template""" - if not uri_template.validate(value): - raise ValidationError(_("Invalid URL template.")) - - -class RealmFilter(models.Model): - """Realm-specific regular expressions to automatically linkify certain - strings inside the Markdown processor. See "Custom filters" in the settings UI. - """ - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - pattern = models.TextField() - url_template = models.TextField(validators=[url_template_validator]) - # Linkifiers are applied in a message/topic in order; the processing order - # is important when there are overlapping patterns. - order = models.IntegerField(default=0) - - class Meta: - unique_together = ("realm", "pattern") - - @override - def __str__(self) -> str: - return f"{self.realm.string_id}: {self.pattern} {self.url_template}" - - @override - def clean(self) -> None: - """Validate whether the set of parameters in the URL template - match the set of parameters in the regular expression. - - Django's `full_clean` calls `clean_fields` followed by `clean` method - and stores all ValidationErrors from all stages to return as JSON. - """ - - # Extract variables present in the pattern - pattern = filter_pattern_validator(self.pattern) - group_set = set(pattern.groupindex.keys()) - - # Do not continue the check if the url template is invalid to begin with. - # The ValidationError for invalid template will only be raised by the validator - # set on the url_template field instead of here to avoid duplicates. - if not uri_template.validate(self.url_template): - return - - # Extract variables used in the URL template. - template_variables_set = set(uri_template.URITemplate(self.url_template).variable_names) - - # Report patterns missing in linkifier pattern. - missing_in_pattern_set = template_variables_set - group_set - if len(missing_in_pattern_set) > 0: - name = min(missing_in_pattern_set) - raise ValidationError( - _("Group %(name)r in URL template is not present in linkifier pattern."), - params={"name": name}, - ) - - missing_in_url_set = group_set - template_variables_set - # Report patterns missing in URL template. - if len(missing_in_url_set) > 0: - # We just report the first missing pattern here. Users can - # incrementally resolve errors if there are multiple - # missing patterns. - name = min(missing_in_url_set) - raise ValidationError( - _("Group %(name)r in linkifier pattern is not present in URL template."), - params={"name": name}, - ) - - -def get_linkifiers_cache_key(realm_id: int) -> str: - return f"{cache.KEY_PREFIX}:all_linkifiers_for_realm:{realm_id}" - - -@return_same_value_during_entire_request -@cache_with_key(get_linkifiers_cache_key, timeout=3600 * 24 * 7) -def linkifiers_for_realm(realm_id: int) -> List[LinkifierDict]: - return [ - LinkifierDict( - pattern=linkifier.pattern, - url_template=linkifier.url_template, - id=linkifier.id, - ) - for linkifier in RealmFilter.objects.filter(realm_id=realm_id).order_by("order") - ] - - -def flush_linkifiers(*, instance: RealmFilter, **kwargs: object) -> None: - realm_id = instance.realm_id - cache_delete(get_linkifiers_cache_key(realm_id)) - flush_per_request_cache("linkifiers_for_realm") - - -post_save.connect(flush_linkifiers, sender=RealmFilter) -post_delete.connect(flush_linkifiers, sender=RealmFilter) - - -class RealmPlayground(models.Model): - """Server side storage model to store playground information needed by our - 'view code in playground' feature in code blocks. - """ - - MAX_PYGMENTS_LANGUAGE_LENGTH = 40 - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - url_template = models.TextField(validators=[url_template_validator]) - - # User-visible display name used when configuring playgrounds in the settings page and - # when displaying them in the playground links popover. - name = models.TextField(db_index=True) - - # This stores the pygments lexer subclass names and not the aliases themselves. - pygments_language = models.CharField( - db_index=True, - max_length=MAX_PYGMENTS_LANGUAGE_LENGTH, - # We validate to see if this conforms to the character set allowed for a - # language in the code block. - validators=[ - RegexValidator( - regex=r"^[ a-zA-Z0-9_+-./#]*$", message=_("Invalid characters in pygments language") - ) - ], - ) - - class Meta: - unique_together = (("realm", "pygments_language", "name"),) - - @override - def __str__(self) -> str: - return f"{self.realm.string_id}: {self.pygments_language} {self.name}" - - @override - def clean(self) -> None: - """Validate whether the URL template is valid for the playground, - ensuring that "code" is the sole variable present in it. - - Django's `full_clean` calls `clean_fields` followed by `clean` method - and stores all ValidationErrors from all stages to return as JSON. - """ - - # Do not continue the check if the url template is invalid to begin - # with. The ValidationError for invalid template will only be raised by - # the validator set on the url_template field instead of here to avoid - # duplicates. - if not uri_template.validate(self.url_template): - return - - # Extract variables used in the URL template. - template_variables = set(uri_template.URITemplate(self.url_template).variable_names) - - if "code" not in template_variables: - raise ValidationError(_('Missing the required variable "code" in the URL template')) - - # The URL template should only contain a single variable, which is "code". - if len(template_variables) != 1: - raise ValidationError( - _('"code" should be the only variable present in the URL template'), - ) - - -def get_realm_playgrounds(realm: Realm) -> List[RealmPlaygroundDict]: - return [ - RealmPlaygroundDict( - id=playground.id, - name=playground.name, - pygments_language=playground.pygments_language, - url_template=playground.url_template, - ) - for playground in RealmPlayground.objects.filter(realm=realm).all() - ] - - -class Recipient(models.Model): - """Represents an audience that can potentially receive messages in Zulip. - - This table essentially functions as a generic foreign key that - allows Message.recipient_id to be a simple ForeignKey representing - the audience for a message, while supporting the different types - of audiences Zulip supports for a message. - - Recipient has just two attributes: The enum type, and a type_id, - which is the ID of the UserProfile/Stream/Huddle object containing - all the metadata for the audience. There are 3 recipient types: - - 1. 1:1 direct message: The type_id is the ID of the UserProfile - who will receive any message to this Recipient. The sender - of such a message is represented separately. - 2. Stream message: The type_id is the ID of the associated Stream. - 3. Group direct message: In Zulip, group direct messages are - represented by Huddle objects, which encode the set of users - in the conversation. The type_id is the ID of the associated Huddle - object; the set of users is usually retrieved via the Subscription - table. See the Huddle model for details. - - See also the Subscription model, which stores which UserProfile - objects are subscribed to which Recipient objects. - """ - - type_id = models.IntegerField(db_index=True) - type = models.PositiveSmallIntegerField(db_index=True) - # Valid types are {personal, stream, huddle} - - # The type for 1:1 direct messages. - PERSONAL = 1 - # The type for stream messages. - STREAM = 2 - # The type group direct messages. - HUDDLE = 3 - - class Meta: - unique_together = ("type", "type_id") - - # N.B. If we used Django's choice=... we would get this for free (kinda) - _type_names = {PERSONAL: "personal", STREAM: "stream", HUDDLE: "huddle"} - - @override - def __str__(self) -> str: - return f"{self.label()} ({self.type_id}, {self.type})" - - def label(self) -> str: - if self.type == Recipient.STREAM: - return Stream.objects.get(id=self.type_id).name - else: - return str(get_display_recipient(self)) - - def type_name(self) -> str: - # Raises KeyError if invalid - return self._type_names[self.type] - - -class UserBaseSettings(models.Model): - """This abstract class is the container for all preferences/personal - settings for users that control the behavior of the application. - - It was extracted from UserProfile to support the RealmUserDefault - model (i.e. allow individual realms to configure the default - values of these preferences for new users in their organization). - - Changing the default value for a field declared here likely - requires a migration to update all RealmUserDefault rows that had - the old default value to have the new default value. Otherwise, - the default change will only affect new users joining Realms - created after the change. - """ - - ### Generic UI settings - enter_sends = models.BooleanField(default=False) - - ### Preferences. ### - # left_side_userlist was removed from the UI in Zulip 6.0; the - # database model is being temporarily preserved in case we want to - # restore a version of the setting, preserving who had it enabled. - left_side_userlist = models.BooleanField(default=False) - default_language = models.CharField(default="en", max_length=MAX_LANGUAGE_ID_LENGTH) - # This setting controls which view is rendered first when Zulip loads. - # Values for it are URL suffix after `#`. - web_home_view = models.TextField(default="inbox") - web_escape_navigates_to_home_view = models.BooleanField(default=True) - dense_mode = models.BooleanField(default=True) - fluid_layout_width = models.BooleanField(default=False) - high_contrast_mode = models.BooleanField(default=False) - translate_emoticons = models.BooleanField(default=False) - display_emoji_reaction_users = models.BooleanField(default=True) - twenty_four_hour_time = models.BooleanField(default=False) - starred_message_counts = models.BooleanField(default=True) - COLOR_SCHEME_AUTOMATIC = 1 - COLOR_SCHEME_NIGHT = 2 - COLOR_SCHEME_LIGHT = 3 - COLOR_SCHEME_CHOICES = [COLOR_SCHEME_AUTOMATIC, COLOR_SCHEME_NIGHT, COLOR_SCHEME_LIGHT] - color_scheme = models.PositiveSmallIntegerField(default=COLOR_SCHEME_AUTOMATIC) - - # UI setting controlling Zulip's behavior of demoting in the sort - # order and graying out streams with no recent traffic. The - # default behavior, automatic, enables this behavior once a user - # is subscribed to 30+ streams in the web app. - DEMOTE_STREAMS_AUTOMATIC = 1 - DEMOTE_STREAMS_ALWAYS = 2 - DEMOTE_STREAMS_NEVER = 3 - DEMOTE_STREAMS_CHOICES = [ - DEMOTE_STREAMS_AUTOMATIC, - DEMOTE_STREAMS_ALWAYS, - DEMOTE_STREAMS_NEVER, - ] - demote_inactive_streams = models.PositiveSmallIntegerField(default=DEMOTE_STREAMS_AUTOMATIC) - - # UI setting controlling whether or not the Zulip web app will - # mark messages as read as it scrolls through the feed. - - MARK_READ_ON_SCROLL_ALWAYS = 1 - MARK_READ_ON_SCROLL_CONVERSATION_ONLY = 2 - MARK_READ_ON_SCROLL_NEVER = 3 - - WEB_MARK_READ_ON_SCROLL_POLICY_CHOICES = [ - MARK_READ_ON_SCROLL_ALWAYS, - MARK_READ_ON_SCROLL_CONVERSATION_ONLY, - MARK_READ_ON_SCROLL_NEVER, - ] - - web_mark_read_on_scroll_policy = models.SmallIntegerField(default=MARK_READ_ON_SCROLL_ALWAYS) - - # Emoji sets - GOOGLE_EMOJISET = "google" - GOOGLE_BLOB_EMOJISET = "google-blob" - TEXT_EMOJISET = "text" - TWITTER_EMOJISET = "twitter" - EMOJISET_CHOICES = ( - (GOOGLE_EMOJISET, "Google"), - (TWITTER_EMOJISET, "Twitter"), - (TEXT_EMOJISET, "Plain text"), - (GOOGLE_BLOB_EMOJISET, "Google blobs"), - ) - emojiset = models.CharField(default=GOOGLE_EMOJISET, choices=EMOJISET_CHOICES, max_length=20) - - # User list style - USER_LIST_STYLE_COMPACT = 1 - USER_LIST_STYLE_WITH_STATUS = 2 - USER_LIST_STYLE_WITH_AVATAR = 3 - USER_LIST_STYLE_CHOICES = [ - USER_LIST_STYLE_COMPACT, - USER_LIST_STYLE_WITH_STATUS, - USER_LIST_STYLE_WITH_AVATAR, - ] - user_list_style = models.PositiveSmallIntegerField(default=USER_LIST_STYLE_WITH_STATUS) - - # Show unread counts for - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_ALL_STREAMS = 1 - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS = 2 - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_NO_STREAMS = 3 - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES = [ - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_ALL_STREAMS, - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS, - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_NO_STREAMS, - ] - web_stream_unreads_count_display_policy = models.PositiveSmallIntegerField( - default=WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS - ) - - ### Notifications settings. ### - - email_notifications_batching_period_seconds = models.IntegerField(default=120) - - # Stream notifications. - enable_stream_desktop_notifications = models.BooleanField(default=False) - enable_stream_email_notifications = models.BooleanField(default=False) - enable_stream_push_notifications = models.BooleanField(default=False) - enable_stream_audible_notifications = models.BooleanField(default=False) - notification_sound = models.CharField(max_length=20, default="zulip") - wildcard_mentions_notify = models.BooleanField(default=True) - - # Followed Topics notifications. - enable_followed_topic_desktop_notifications = models.BooleanField(default=True) - enable_followed_topic_email_notifications = models.BooleanField(default=True) - enable_followed_topic_push_notifications = models.BooleanField(default=True) - enable_followed_topic_audible_notifications = models.BooleanField(default=True) - enable_followed_topic_wildcard_mentions_notify = models.BooleanField(default=True) - - # Direct message + @-mention notifications. - enable_desktop_notifications = models.BooleanField(default=True) - pm_content_in_desktop_notifications = models.BooleanField(default=True) - enable_sounds = models.BooleanField(default=True) - enable_offline_email_notifications = models.BooleanField(default=True) - message_content_in_email_notifications = models.BooleanField(default=True) - enable_offline_push_notifications = models.BooleanField(default=True) - enable_online_push_notifications = models.BooleanField(default=True) - - DESKTOP_ICON_COUNT_DISPLAY_MESSAGES = 1 - DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC = 2 - DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION = 3 - DESKTOP_ICON_COUNT_DISPLAY_NONE = 4 - DESKTOP_ICON_COUNT_DISPLAY_CHOICES = [ - DESKTOP_ICON_COUNT_DISPLAY_MESSAGES, - DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION, - DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC, - DESKTOP_ICON_COUNT_DISPLAY_NONE, - ] - desktop_icon_count_display = models.PositiveSmallIntegerField( - default=DESKTOP_ICON_COUNT_DISPLAY_MESSAGES - ) - - enable_digest_emails = models.BooleanField(default=True) - enable_login_emails = models.BooleanField(default=True) - enable_marketing_emails = models.BooleanField(default=True) - presence_enabled = models.BooleanField(default=True) - - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC = 1 - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_ALWAYS = 2 - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_NEVER = 3 - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES = [ - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC, - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_ALWAYS, - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_NEVER, - ] - realm_name_in_email_notifications_policy = models.PositiveSmallIntegerField( - default=REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC - ) - - # The following two settings control which topics to automatically - # 'follow' or 'unmute in a muted stream', respectively. - # Follow or unmute a topic automatically on: - # - PARTICIPATION: Send a message, React to a message, Participate in a poll or Edit a TO-DO list. - # - SEND: Send a message. - # - INITIATION: Send the first message in the topic. - # - NEVER: Never automatically follow or unmute a topic. - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION = 1 - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND = 2 - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION = 3 - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER = 4 - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES = [ - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION, - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION, - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER, - ] - automatically_follow_topics_policy = models.PositiveSmallIntegerField( - default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION, - ) - automatically_unmute_topics_in_muted_streams_policy = models.PositiveSmallIntegerField( - default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, - ) - automatically_follow_topics_where_mentioned = models.BooleanField(default=True) - - # Whether or not the user wants to sync their drafts. - enable_drafts_synchronization = models.BooleanField(default=True) - - # Privacy settings - send_stream_typing_notifications = models.BooleanField(default=True) - send_private_typing_notifications = models.BooleanField(default=True) - send_read_receipts = models.BooleanField(default=True) - - # Who in the organization has access to users' actual email - # addresses. Controls whether the UserProfile.email field is - # the same as UserProfile.delivery_email, or is instead a fake - # generated value encoding the user ID and realm hostname. - EMAIL_ADDRESS_VISIBILITY_EVERYONE = 1 - EMAIL_ADDRESS_VISIBILITY_MEMBERS = 2 - EMAIL_ADDRESS_VISIBILITY_ADMINS = 3 - EMAIL_ADDRESS_VISIBILITY_NOBODY = 4 - EMAIL_ADDRESS_VISIBILITY_MODERATORS = 5 - email_address_visibility = models.PositiveSmallIntegerField( - default=EMAIL_ADDRESS_VISIBILITY_EVERYONE, - ) - - EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP = { - EMAIL_ADDRESS_VISIBILITY_EVERYONE: gettext_lazy("Admins, moderators, members and guests"), - EMAIL_ADDRESS_VISIBILITY_MEMBERS: gettext_lazy("Admins, moderators and members"), - EMAIL_ADDRESS_VISIBILITY_MODERATORS: gettext_lazy("Admins and moderators"), - EMAIL_ADDRESS_VISIBILITY_ADMINS: gettext_lazy("Admins only"), - EMAIL_ADDRESS_VISIBILITY_NOBODY: gettext_lazy("Nobody"), - } - - EMAIL_ADDRESS_VISIBILITY_TYPES = list(EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP.keys()) - - display_settings_legacy = dict( - # Don't add anything new to this legacy dict. - # Instead, see `modern_settings` below. - color_scheme=int, - default_language=str, - web_home_view=str, - demote_inactive_streams=int, - dense_mode=bool, - emojiset=str, - enable_drafts_synchronization=bool, - enter_sends=bool, - fluid_layout_width=bool, - high_contrast_mode=bool, - left_side_userlist=bool, - starred_message_counts=bool, - translate_emoticons=bool, - twenty_four_hour_time=bool, - ) - - notification_settings_legacy = dict( - # Don't add anything new to this legacy dict. - # Instead, see `modern_notification_settings` below. - desktop_icon_count_display=int, - email_notifications_batching_period_seconds=int, - enable_desktop_notifications=bool, - enable_digest_emails=bool, - enable_login_emails=bool, - enable_marketing_emails=bool, - enable_offline_email_notifications=bool, - enable_offline_push_notifications=bool, - enable_online_push_notifications=bool, - enable_sounds=bool, - enable_stream_audible_notifications=bool, - enable_stream_desktop_notifications=bool, - enable_stream_email_notifications=bool, - enable_stream_push_notifications=bool, - message_content_in_email_notifications=bool, - notification_sound=str, - pm_content_in_desktop_notifications=bool, - presence_enabled=bool, - realm_name_in_email_notifications_policy=int, - wildcard_mentions_notify=bool, - ) - - modern_settings = dict( - # Add new general settings here. - display_emoji_reaction_users=bool, - email_address_visibility=int, - web_escape_navigates_to_home_view=bool, - send_private_typing_notifications=bool, - send_read_receipts=bool, - send_stream_typing_notifications=bool, - web_mark_read_on_scroll_policy=int, - user_list_style=int, - web_stream_unreads_count_display_policy=int, - ) - - modern_notification_settings: Dict[str, Any] = dict( - # Add new notification settings here. - enable_followed_topic_desktop_notifications=bool, - enable_followed_topic_email_notifications=bool, - enable_followed_topic_push_notifications=bool, - enable_followed_topic_audible_notifications=bool, - enable_followed_topic_wildcard_mentions_notify=bool, - automatically_follow_topics_policy=int, - automatically_unmute_topics_in_muted_streams_policy=int, - automatically_follow_topics_where_mentioned=bool, - ) - - notification_setting_types = { - **notification_settings_legacy, - **modern_notification_settings, - } - - # Define the types of the various automatically managed properties - property_types = { - **display_settings_legacy, - **notification_setting_types, - **modern_settings, - } - - class Meta: - abstract = True - - @staticmethod - def emojiset_choices() -> List[Dict[str, str]]: - return [ - dict(key=emojiset[0], text=emojiset[1]) for emojiset in UserProfile.EMOJISET_CHOICES - ] - - -class RealmUserDefault(UserBaseSettings): - """This table stores realm-level default values for user preferences - like notification settings, used when creating a new user account. - """ - - realm = models.OneToOneField(Realm, on_delete=CASCADE) - - -class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): - USERNAME_FIELD = "email" - MAX_NAME_LENGTH = 100 - MIN_NAME_LENGTH = 2 - API_KEY_LENGTH = 32 - NAME_INVALID_CHARS = ["*", "`", "\\", ">", '"', "@"] - - DEFAULT_BOT = 1 - """ - Incoming webhook bots are limited to only sending messages via webhooks. - Thus, it is less of a security risk to expose their API keys to third-party services, - since they can't be used to read messages. - """ - INCOMING_WEBHOOK_BOT = 2 - # This value is also being used in web/src/settings_bots.js. - # On updating it here, update it there as well. - OUTGOING_WEBHOOK_BOT = 3 - """ - Embedded bots run within the Zulip server itself; events are added to the - embedded_bots queue and then handled by a QueueProcessingWorker. - """ - EMBEDDED_BOT = 4 - - BOT_TYPES = { - DEFAULT_BOT: "Generic bot", - INCOMING_WEBHOOK_BOT: "Incoming webhook", - OUTGOING_WEBHOOK_BOT: "Outgoing webhook", - EMBEDDED_BOT: "Embedded bot", - } - - SERVICE_BOT_TYPES = [ - OUTGOING_WEBHOOK_BOT, - EMBEDDED_BOT, - ] - - # For historical reasons, Zulip has two email fields. The - # `delivery_email` field is the user's email address, where all - # email notifications will be sent, and is used for all - # authentication use cases. - # - # The `email` field is the same as delivery_email in organizations - # with EMAIL_ADDRESS_VISIBILITY_EVERYONE. For other - # organizations, it will be a unique value of the form - # user1234@example.com. This field exists for backwards - # compatibility in Zulip APIs where users are referred to by their - # email address, not their ID; it should be used in all API use cases. - # - # Both fields are unique within a realm (in a case-insensitive - # fashion). Since Django's unique_together is case sensitive, this - # is enforced via SQL indexes created by - # zerver/migrations/0295_case_insensitive_email_indexes.py. - delivery_email = models.EmailField(blank=False, db_index=True) - email = models.EmailField(blank=False, db_index=True) - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - # Foreign key to the Recipient object for PERSONAL type messages to this user. - recipient = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) - - INACCESSIBLE_USER_NAME = gettext_lazy("Unknown user") - # The user's name. We prefer the model of a full_name - # over first+last because cultures vary on how many - # names one has, whether the family name is first or last, etc. - # It also allows organizations to encode a bit of non-name data in - # the "name" attribute if desired, like gender pronouns, - # graduation year, etc. - full_name = models.CharField(max_length=MAX_NAME_LENGTH) - - date_joined = models.DateTimeField(default=timezone_now) - - # Terms of Service version number that this user has accepted. We - # use the special value TOS_VERSION_BEFORE_FIRST_LOGIN for users - # whose account was created without direct user interaction (via - # the API or a data import), and null for users whose account is - # fully created on servers that do not have a configured ToS. - TOS_VERSION_BEFORE_FIRST_LOGIN = "-1" - tos_version = models.CharField(null=True, max_length=10) - api_key = models.CharField(max_length=API_KEY_LENGTH, default=generate_api_key, unique=True) - - # A UUID generated on user creation. Introduced primarily to - # provide a unique key for a user for the mobile push - # notifications bouncer that will not have collisions after doing - # a data export and then import. - uuid = models.UUIDField(default=uuid4, unique=True) - - # Whether the user has access to server-level administrator pages, like /activity - is_staff = models.BooleanField(default=False) - - # For a normal user, this is True unless the user or an admin has - # deactivated their account. The name comes from Django; this field - # isn't related to presence or to whether the user has recently used Zulip. - # - # See also `long_term_idle`. - is_active = models.BooleanField(default=True, db_index=True) - - is_billing_admin = models.BooleanField(default=False, db_index=True) - - is_bot = models.BooleanField(default=False, db_index=True) - bot_type = models.PositiveSmallIntegerField(null=True, db_index=True) - bot_owner = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) - - # Each role has a superset of the permissions of the next higher - # numbered role. When adding new roles, leave enough space for - # future roles to be inserted between currently adjacent - # roles. These constants appear in RealmAuditLog.extra_data, so - # changes to them will require a migration of RealmAuditLog. - ROLE_REALM_OWNER = 100 - ROLE_REALM_ADMINISTRATOR = 200 - ROLE_MODERATOR = 300 - ROLE_MEMBER = 400 - ROLE_GUEST = 600 - role = models.PositiveSmallIntegerField(default=ROLE_MEMBER, db_index=True) - - ROLE_TYPES = [ - ROLE_REALM_OWNER, - ROLE_REALM_ADMINISTRATOR, - ROLE_MODERATOR, - ROLE_MEMBER, - ROLE_GUEST, - ] - - # Whether the user has been "soft-deactivated" due to weeks of inactivity. - # For these users we avoid doing UserMessage table work, as an optimization - # for large Zulip organizations with lots of single-visit users. - long_term_idle = models.BooleanField(default=False, db_index=True) - - # When we last added basic UserMessage rows for a long_term_idle user. - last_active_message_id = models.IntegerField(null=True) - - # Mirror dummies are fake (!is_active) users used to provide - # message senders in our cross-protocol Zephyr<->Zulip content - # mirroring integration, so that we can display mirrored content - # like native Zulip messages (with a name + avatar, etc.). - is_mirror_dummy = models.BooleanField(default=False) - - # Users with this flag set are allowed to forge messages as sent by another - # user and to send to private streams; also used for Zephyr/Jabber mirroring. - can_forge_sender = models.BooleanField(default=False, db_index=True) - # Users with this flag set can create other users via API. - can_create_users = models.BooleanField(default=False, db_index=True) - - # Used for rate-limiting certain automated messages generated by bots - last_reminder = models.DateTimeField(default=None, null=True) - - # Minutes to wait before warning a bot owner that their bot sent a message - # to a nonexistent stream - BOT_OWNER_STREAM_ALERT_WAITPERIOD = 1 - - # API rate limits, formatted as a comma-separated list of range:max pairs - rate_limits = models.CharField(default="", max_length=100) - - # Default streams for some deprecated/legacy classes of bot users. - default_sending_stream = models.ForeignKey( - "zerver.Stream", - null=True, - related_name="+", - on_delete=models.SET_NULL, - ) - default_events_register_stream = models.ForeignKey( - "zerver.Stream", - null=True, - related_name="+", - on_delete=models.SET_NULL, - ) - default_all_public_streams = models.BooleanField(default=False) - - # A time zone name from the `tzdata` database, as found in zoneinfo.available_timezones(). - # - # The longest existing name is 32 characters long, so max_length=40 seems - # like a safe choice. - # - # In Django, the convention is to use an empty string instead of NULL/None - # for text-based fields. For more information, see - # https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.Field.null. - timezone = models.CharField(max_length=40, default="") - - AVATAR_FROM_GRAVATAR = "G" - AVATAR_FROM_USER = "U" - AVATAR_SOURCES = ( - (AVATAR_FROM_GRAVATAR, "Hosted by Gravatar"), - (AVATAR_FROM_USER, "Uploaded by user"), - ) - avatar_source = models.CharField( - default=AVATAR_FROM_GRAVATAR, choices=AVATAR_SOURCES, max_length=1 - ) - avatar_version = models.PositiveSmallIntegerField(default=1) - avatar_hash = models.CharField(null=True, max_length=64) - - # TODO: TUTORIAL_STATUS was originally an optimization designed to - # allow us to skip querying the OnboardingStep table when loading - # /. This optimization is no longer effective, so it's possible we - # should delete it. - TUTORIAL_WAITING = "W" - TUTORIAL_STARTED = "S" - TUTORIAL_FINISHED = "F" - TUTORIAL_STATES = ( - (TUTORIAL_WAITING, "Waiting"), - (TUTORIAL_STARTED, "Started"), - (TUTORIAL_FINISHED, "Finished"), - ) - tutorial_status = models.CharField( - default=TUTORIAL_WAITING, choices=TUTORIAL_STATES, max_length=1 - ) - - # Contains serialized JSON of the form: - # [("step 1", true), ("step 2", false)] - # where the second element of each tuple is if the step has been - # completed. - onboarding_steps = models.TextField(default="[]") - - zoom_token = models.JSONField(default=None, null=True) - - objects = UserManager() - - ROLE_ID_TO_NAME_MAP = { - ROLE_REALM_OWNER: gettext_lazy("Organization owner"), - ROLE_REALM_ADMINISTRATOR: gettext_lazy("Organization administrator"), - ROLE_MODERATOR: gettext_lazy("Moderator"), - ROLE_MEMBER: gettext_lazy("Member"), - ROLE_GUEST: gettext_lazy("Guest"), - } - - def get_role_name(self) -> str: - return str(self.ROLE_ID_TO_NAME_MAP[self.role]) - - def profile_data(self) -> ProfileData: - values = CustomProfileFieldValue.objects.filter(user_profile=self) - user_data = { - v.field_id: {"value": v.value, "rendered_value": v.rendered_value} for v in values - } - data: ProfileData = [] - for field in custom_profile_fields_for_realm(self.realm_id): - field_values = user_data.get(field.id, None) - if field_values: - value, rendered_value = field_values.get("value"), field_values.get( - "rendered_value" - ) - else: - value, rendered_value = None, None - field_type = field.field_type - if value is not None: - converter = field.FIELD_CONVERTERS[field_type] - value = converter(value) - - field_data = field.as_dict() - data.append( - { - "id": field_data["id"], - "name": field_data["name"], - "type": field_data["type"], - "hint": field_data["hint"], - "field_data": field_data["field_data"], - "order": field_data["order"], - "value": value, - "rendered_value": rendered_value, - } - ) - - return data - - def can_admin_user(self, target_user: "UserProfile") -> bool: - """Returns whether this user has permission to modify target_user""" - if target_user.bot_owner_id == self.id: - return True - elif self.is_realm_admin and self.realm == target_user.realm: - return True - else: - return False - - @override - def __str__(self) -> str: - return f"{self.email} {self.realm!r}" - - @property - def is_provisional_member(self) -> bool: - if self.is_moderator: - return False - diff = (timezone_now() - self.date_joined).days - if diff < self.realm.waiting_period_threshold: - return True - return False - - @property - def is_realm_admin(self) -> bool: - return self.role in (UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER) - - @is_realm_admin.setter - def is_realm_admin(self, value: bool) -> None: - if value: - self.role = UserProfile.ROLE_REALM_ADMINISTRATOR - elif self.role == UserProfile.ROLE_REALM_ADMINISTRATOR: - # We need to be careful to not accidentally change - # ROLE_GUEST to ROLE_MEMBER here. - self.role = UserProfile.ROLE_MEMBER - - @property - def has_billing_access(self) -> bool: - return self.is_realm_owner or self.is_billing_admin - - @property - def is_realm_owner(self) -> bool: - return self.role == UserProfile.ROLE_REALM_OWNER - - @is_realm_owner.setter - def is_realm_owner(self, value: bool) -> None: - if value: - self.role = UserProfile.ROLE_REALM_OWNER - elif self.role == UserProfile.ROLE_REALM_OWNER: - # We need to be careful to not accidentally change - # ROLE_GUEST to ROLE_MEMBER here. - self.role = UserProfile.ROLE_MEMBER - - @property - def is_guest(self) -> bool: - return self.role == UserProfile.ROLE_GUEST - - @is_guest.setter - def is_guest(self, value: bool) -> None: - if value: - self.role = UserProfile.ROLE_GUEST - elif self.role == UserProfile.ROLE_GUEST: - # We need to be careful to not accidentally change - # ROLE_REALM_ADMINISTRATOR to ROLE_MEMBER here. - self.role = UserProfile.ROLE_MEMBER - - @property - def is_moderator(self) -> bool: - return self.role == UserProfile.ROLE_MODERATOR - - @is_moderator.setter - def is_moderator(self, value: bool) -> None: - if value: - self.role = UserProfile.ROLE_MODERATOR - elif self.role == UserProfile.ROLE_MODERATOR: - # We need to be careful to not accidentally change - # ROLE_GUEST to ROLE_MEMBER here. - self.role = UserProfile.ROLE_MEMBER - - @property - def is_incoming_webhook(self) -> bool: - return self.bot_type == UserProfile.INCOMING_WEBHOOK_BOT - - @property - def allowed_bot_types(self) -> List[int]: - allowed_bot_types = [] - if ( - self.is_realm_admin - or self.realm.bot_creation_policy != Realm.BOT_CREATION_LIMIT_GENERIC_BOTS - ): - allowed_bot_types.append(UserProfile.DEFAULT_BOT) - allowed_bot_types += [ - UserProfile.INCOMING_WEBHOOK_BOT, - UserProfile.OUTGOING_WEBHOOK_BOT, - ] - if settings.EMBEDDED_BOTS_ENABLED: - allowed_bot_types.append(UserProfile.EMBEDDED_BOT) - return allowed_bot_types - - def email_address_is_realm_public(self) -> bool: - # Bots always have EMAIL_ADDRESS_VISIBILITY_EVERYONE. - if self.email_address_visibility == UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE: - return True - return False - - def has_permission(self, policy_name: str) -> bool: - from zerver.lib.user_groups import is_user_in_group - - if policy_name not in [ - "add_custom_emoji_policy", - "create_multiuse_invite_group", - "create_private_stream_policy", - "create_public_stream_policy", - "create_web_public_stream_policy", - "delete_own_message_policy", - "edit_topic_policy", - "invite_to_stream_policy", - "invite_to_realm_policy", - "move_messages_between_streams_policy", - "user_group_edit_policy", - ]: - raise AssertionError("Invalid policy") - - if policy_name in Realm.REALM_PERMISSION_GROUP_SETTINGS: - allowed_user_group = getattr(self.realm, policy_name) - return is_user_in_group(allowed_user_group, self) - - policy_value = getattr(self.realm, policy_name) - if policy_value == Realm.POLICY_NOBODY: - return False - - if policy_value == Realm.POLICY_EVERYONE: - return True - - if self.is_realm_owner: - return True - - if policy_value == Realm.POLICY_OWNERS_ONLY: - return False - - if self.is_realm_admin: - return True - - if policy_value == Realm.POLICY_ADMINS_ONLY: - return False - - if self.is_moderator: - return True - - if policy_value == Realm.POLICY_MODERATORS_ONLY: - return False - - if self.is_guest: - return False - - if policy_value == Realm.POLICY_MEMBERS_ONLY: - return True - - assert policy_value == Realm.POLICY_FULL_MEMBERS_ONLY - return not self.is_provisional_member - - def can_create_public_streams(self) -> bool: - return self.has_permission("create_public_stream_policy") - - def can_create_private_streams(self) -> bool: - return self.has_permission("create_private_stream_policy") - - def can_create_web_public_streams(self) -> bool: - if not self.realm.web_public_streams_enabled(): - return False - return self.has_permission("create_web_public_stream_policy") - - def can_subscribe_other_users(self) -> bool: - return self.has_permission("invite_to_stream_policy") - - def can_invite_users_by_email(self) -> bool: - return self.has_permission("invite_to_realm_policy") - - def can_create_multiuse_invite_to_realm(self) -> bool: - return self.has_permission("create_multiuse_invite_group") - - def can_move_messages_between_streams(self) -> bool: - return self.has_permission("move_messages_between_streams_policy") - - def can_edit_user_groups(self) -> bool: - return self.has_permission("user_group_edit_policy") - - def can_move_messages_to_another_topic(self) -> bool: - return self.has_permission("edit_topic_policy") - - def can_add_custom_emoji(self) -> bool: - return self.has_permission("add_custom_emoji_policy") - - def can_delete_own_message(self) -> bool: - return self.has_permission("delete_own_message_policy") - - def can_access_public_streams(self) -> bool: - return not (self.is_guest or self.realm.is_zephyr_mirror_realm) - - def major_tos_version(self) -> int: - if self.tos_version is not None: - return int(self.tos_version.split(".")[0]) - else: - return -1 - - def format_requester_for_logs(self) -> str: - return "{}@{}".format(self.id, self.realm.string_id or "root") - - @override - def set_password(self, password: Optional[str]) -> None: - if password is None: - self.set_unusable_password() - return - - from zproject.backends import check_password_strength - - if not check_password_strength(password): - raise PasswordTooWeakError - - super().set_password(password) - - class Meta: - indexes = [ - models.Index(Upper("email"), name="upper_userprofile_email_idx"), - ] - - -class PasswordTooWeakError(Exception): - pass - - -class UserGroup(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 - MAX_NAME_LENGTH = 100 - INVALID_NAME_PREFIXES = ["@", "role:", "user:", "stream:", "channel:"] - - objects: CTEManager = CTEManager() - name = models.CharField(max_length=MAX_NAME_LENGTH) - direct_members = models.ManyToManyField( - UserProfile, through="zerver.UserGroupMembership", related_name="direct_groups" - ) - direct_subgroups = models.ManyToManyField( - "self", - symmetrical=False, - through="zerver.GroupGroupMembership", - through_fields=("supergroup", "subgroup"), - related_name="direct_supergroups", - ) - realm = models.ForeignKey(Realm, on_delete=CASCADE) - description = models.TextField(default="") - is_system_group = models.BooleanField(default=False) - - can_mention_group = models.ForeignKey("self", on_delete=models.RESTRICT) - - # We do not have "Full members" and "Everyone on the internet" - # group here since there isn't a separate role value for full - # members and spectators. - SYSTEM_USER_GROUP_ROLE_MAP = { - UserProfile.ROLE_REALM_OWNER: { - "name": SystemGroups.OWNERS, - "description": "Owners of this organization", - }, - UserProfile.ROLE_REALM_ADMINISTRATOR: { - "name": SystemGroups.ADMINISTRATORS, - "description": "Administrators of this organization, including owners", - }, - UserProfile.ROLE_MODERATOR: { - "name": SystemGroups.MODERATORS, - "description": "Moderators of this organization, including administrators", - }, - UserProfile.ROLE_MEMBER: { - "name": SystemGroups.MEMBERS, - "description": "Members of this organization, not including guests", - }, - UserProfile.ROLE_GUEST: { - "name": SystemGroups.EVERYONE, - "description": "Everyone in this organization, including guests", - }, - } - - GROUP_PERMISSION_SETTINGS = { - "can_mention_group": GroupPermissionSetting( - require_system_group=False, - allow_internet_group=False, - allow_owners_group=False, - allow_nobody_group=True, - allow_everyone_group=True, - default_group_name=SystemGroups.EVERYONE, - default_for_system_groups=SystemGroups.NOBODY, - id_field_name="can_mention_group_id", - ), - } - - class Meta: - unique_together = (("realm", "name"),) - - -class UserGroupMembership(models.Model): - user_group = models.ForeignKey(UserGroup, on_delete=CASCADE, related_name="+") - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE, related_name="+") - - class Meta: - unique_together = (("user_group", "user_profile"),) - - -class GroupGroupMembership(models.Model): - supergroup = models.ForeignKey(UserGroup, on_delete=CASCADE, related_name="+") - subgroup = models.ForeignKey(UserGroup, on_delete=CASCADE, related_name="+") - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["supergroup", "subgroup"], name="zerver_groupgroupmembership_uniq" - ) - ] - - -def remote_user_to_email(remote_user: str) -> str: - if settings.SSO_APPEND_DOMAIN is not None: - return Address(username=remote_user, domain=settings.SSO_APPEND_DOMAIN).addr_spec - return remote_user - - -# Make sure we flush the UserProfile object from our remote cache -# whenever we save it. -post_save.connect(flush_user_profile, sender=UserProfile) - - -class PreregistrationRealm(models.Model): - """Data on a partially created realm entered by a user who has - completed the "new organization" form. Used to transfer the user's - selections from the pre-confirmation "new organization" form to - the post-confirmation user registration form. - - Note that the values stored here may not match those of the - created realm (in the event the user creates a realm at all), - because we allow the user to edit these values in the registration - form (and in fact the user will be required to do so if the - `string_id` is claimed by another realm before registraiton is - completed). - """ - - name = models.CharField(max_length=Realm.MAX_REALM_NAME_LENGTH) - org_type = models.PositiveSmallIntegerField( - default=Realm.ORG_TYPES["unspecified"]["id"], - choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()], - ) - default_language = models.CharField( - default="en", - max_length=MAX_LANGUAGE_ID_LENGTH, - ) - string_id = models.CharField(max_length=Realm.MAX_REALM_SUBDOMAIN_LENGTH) - email = models.EmailField() - - confirmation = GenericRelation("confirmation.Confirmation", related_query_name="prereg_realm") - status = models.IntegerField(default=0) - - # The Realm created upon completion of the registration - # for this PregistrationRealm - created_realm = models.ForeignKey(Realm, null=True, related_name="+", on_delete=models.SET_NULL) - - # The UserProfile created upon completion of the registration - # for this PregistrationRealm - created_user = models.ForeignKey( - UserProfile, null=True, related_name="+", on_delete=models.SET_NULL - ) - - -class PreregistrationUser(models.Model): - # Data on a partially created user, before the completion of - # registration. This is used in at least three major code paths: - # * Realm creation, in which case realm is None. - # - # * Invitations, in which case referred_by will always be set. - # - # * Social authentication signup, where it's used to store data - # from the authentication step and pass it to the registration - # form. - - email = models.EmailField() - - confirmation = GenericRelation("confirmation.Confirmation", related_query_name="prereg_user") - # If the pre-registration process provides a suggested full name for this user, - # store it here to use it to prepopulate the full name field in the registration form: - full_name = models.CharField(max_length=UserProfile.MAX_NAME_LENGTH, null=True) - full_name_validated = models.BooleanField(default=False) - referred_by = models.ForeignKey(UserProfile, null=True, on_delete=CASCADE) - streams = models.ManyToManyField("zerver.Stream") - invited_at = models.DateTimeField(auto_now=True) - realm_creation = models.BooleanField(default=False) - # Indicates whether the user needs a password. Users who were - # created via SSO style auth (e.g. GitHub/Google) generally do not. - password_required = models.BooleanField(default=True) - - # status: whether an object has been confirmed. - # if confirmed, set to confirmation.settings.STATUS_USED - status = models.IntegerField(default=0) - - # The realm should only ever be None for PreregistrationUser - # objects created as part of realm creation. - realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE) - - # These values should be consistent with the values - # in settings_config.user_role_values. - INVITE_AS = dict( - REALM_OWNER=100, - REALM_ADMIN=200, - MODERATOR=300, - MEMBER=400, - GUEST_USER=600, - ) - invited_as = models.PositiveSmallIntegerField(default=INVITE_AS["MEMBER"]) - - multiuse_invite = models.ForeignKey("MultiuseInvite", null=True, on_delete=models.SET_NULL) - - # The UserProfile created upon completion of the registration - # for this PregistrationUser - created_user = models.ForeignKey( - UserProfile, null=True, related_name="+", on_delete=models.SET_NULL - ) - - class Meta: - indexes = [ - models.Index(Upper("email"), name="upper_preregistration_email_idx"), - ] - - -def filter_to_valid_prereg_users( - query: QuerySet[PreregistrationUser], - invite_expires_in_minutes: Union[Optional[int], UnspecifiedValue] = UnspecifiedValue(), -) -> QuerySet[PreregistrationUser]: - """ - If invite_expires_in_days is specified, we return only those PreregistrationUser - objects that were created at most that many days in the past. - """ - used_value = confirmation_settings.STATUS_USED - revoked_value = confirmation_settings.STATUS_REVOKED - - query = query.exclude(status__in=[used_value, revoked_value]) - if invite_expires_in_minutes is None: - # Since invite_expires_in_minutes is None, we're invitation will never - # expire, we do not need to check anything else and can simply return - # after excluding objects with active and revoked status. - return query - - assert invite_expires_in_minutes is not None - if not isinstance(invite_expires_in_minutes, UnspecifiedValue): - lowest_datetime = timezone_now() - timedelta(minutes=invite_expires_in_minutes) - return query.filter(invited_at__gte=lowest_datetime) - else: - return query.filter( - Q(confirmation__expiry_date=None) | Q(confirmation__expiry_date__gte=timezone_now()) - ) - - -class MultiuseInvite(models.Model): - referred_by = models.ForeignKey(UserProfile, on_delete=CASCADE) - streams = models.ManyToManyField("zerver.Stream") - realm = models.ForeignKey(Realm, on_delete=CASCADE) - invited_as = models.PositiveSmallIntegerField(default=PreregistrationUser.INVITE_AS["MEMBER"]) - - # status for tracking whether the invite has been revoked. - # If revoked, set to confirmation.settings.STATUS_REVOKED. - # STATUS_USED is not supported, because these objects are supposed - # to be usable multiple times. - status = models.IntegerField(default=0) - - -class EmailChangeStatus(models.Model): - new_email = models.EmailField() - old_email = models.EmailField() - updated_at = models.DateTimeField(auto_now=True) - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - - # status: whether an object has been confirmed. - # if confirmed, set to confirmation.settings.STATUS_USED - status = models.IntegerField(default=0) - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - - -class RealmReactivationStatus(models.Model): - # status: whether an object has been confirmed. - # if confirmed, set to confirmation.settings.STATUS_USED - status = models.IntegerField(default=0) - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - - -class AbstractPushDeviceToken(models.Model): - APNS = 1 - GCM = 2 - - KINDS = ( - (APNS, "apns"), - (GCM, "gcm"), - ) - - kind = models.PositiveSmallIntegerField(choices=KINDS) - - # The token is a unique device-specific token that is - # sent to us from each device: - # - APNS token if kind == APNS - # - GCM registration id if kind == GCM - token = models.CharField(max_length=4096, db_index=True) - - # TODO: last_updated should be renamed date_created, since it is - # no longer maintained as a last_updated value. - last_updated = models.DateTimeField(auto_now=True) - - # [optional] Contains the app id of the device if it is an iOS device - ios_app_id = models.TextField(null=True) - - class Meta: - abstract = True - - -class PushDeviceToken(AbstractPushDeviceToken): - # The user whose device this is - user = models.ForeignKey(UserProfile, db_index=True, on_delete=CASCADE) - - class Meta: - unique_together = ("user", "kind", "token") - - -def generate_email_token_for_stream() -> str: - return secrets.token_hex(16) - - -class Stream(models.Model): - MAX_NAME_LENGTH = 60 - MAX_DESCRIPTION_LENGTH = 1024 - - name = models.CharField(max_length=MAX_NAME_LENGTH, db_index=True) - realm = models.ForeignKey(Realm, db_index=True, on_delete=CASCADE) - date_created = models.DateTimeField(default=timezone_now) - deactivated = models.BooleanField(default=False) - description = models.CharField(max_length=MAX_DESCRIPTION_LENGTH, default="") - rendered_description = models.TextField(default="") - - # Foreign key to the Recipient object for STREAM type messages to this stream. - recipient = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) - - # Various permission policy configurations - PERMISSION_POLICIES: Dict[str, Dict[str, Any]] = { - "web_public": { - "invite_only": False, - "history_public_to_subscribers": True, - "is_web_public": True, - "policy_name": gettext_lazy("Web-public"), - }, - "public": { - "invite_only": False, - "history_public_to_subscribers": True, - "is_web_public": False, - "policy_name": gettext_lazy("Public"), - }, - "private_shared_history": { - "invite_only": True, - "history_public_to_subscribers": True, - "is_web_public": False, - "policy_name": gettext_lazy("Private, shared history"), - }, - "private_protected_history": { - "invite_only": True, - "history_public_to_subscribers": False, - "is_web_public": False, - "policy_name": gettext_lazy("Private, protected history"), - }, - # Public streams with protected history are currently only - # available in Zephyr realms - "public_protected_history": { - "invite_only": False, - "history_public_to_subscribers": False, - "is_web_public": False, - "policy_name": gettext_lazy("Public, protected history"), - }, - } - invite_only = models.BooleanField(default=False) - history_public_to_subscribers = models.BooleanField(default=True) - - # Whether this stream's content should be published by the web-public archive features - is_web_public = models.BooleanField(default=False) - - STREAM_POST_POLICY_EVERYONE = 1 - STREAM_POST_POLICY_ADMINS = 2 - STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS = 3 - STREAM_POST_POLICY_MODERATORS = 4 - # TODO: Implement policy to restrict posting to a user group or admins. - - # Who in the organization has permission to send messages to this stream. - stream_post_policy = models.PositiveSmallIntegerField(default=STREAM_POST_POLICY_EVERYONE) - POST_POLICIES: Dict[int, StrPromise] = { - # These strings should match the strings in the - # stream_post_policy_values object in stream_data.js. - STREAM_POST_POLICY_EVERYONE: gettext_lazy("All stream members can post"), - STREAM_POST_POLICY_ADMINS: gettext_lazy("Only organization administrators can post"), - STREAM_POST_POLICY_MODERATORS: gettext_lazy( - "Only organization administrators and moderators can post" - ), - STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS: gettext_lazy( - "Only organization full members can post" - ), - } - STREAM_POST_POLICY_TYPES = list(POST_POLICIES.keys()) - - # The unique thing about Zephyr public streams is that we never list their - # users. We may try to generalize this concept later, but for now - # we just use a concrete field. (Zephyr public streams aren't exactly like - # invite-only streams--while both are private in terms of listing users, - # for Zephyr we don't even list users to stream members, yet membership - # is more public in the sense that you don't need a Zulip invite to join. - # This field is populated directly from UserProfile.is_zephyr_mirror_realm, - # and the reason for denormalizing field is performance. - is_in_zephyr_realm = models.BooleanField(default=False) - - # Used by the e-mail forwarder. The e-mail RFC specifies a maximum - # e-mail length of 254, and our max stream length is 30, so we - # have plenty of room for the token. - email_token = models.CharField( - max_length=32, - default=generate_email_token_for_stream, - unique=True, - ) - - # For old messages being automatically deleted. - # Value NULL means "use retention policy of the realm". - # Value -1 means "disable retention policy for this stream unconditionally". - # Non-negative values have the natural meaning of "archive messages older than days". - MESSAGE_RETENTION_SPECIAL_VALUES_MAP = { - "unlimited": -1, - "realm_default": None, - } - message_retention_days = models.IntegerField(null=True, default=None) - - # on_delete field here is set to RESTRICT because we don't want to allow - # deleting a user group in case it is referenced by this setting. - # We are not using PROTECT since we want to allow deletion of user groups - # when realm itself is deleted. - can_remove_subscribers_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT) - - # The very first message ID in the stream. Used to help clients - # determine whether they might need to display "more topics" for a - # stream based on what messages they have cached. - first_message_id = models.IntegerField(null=True, db_index=True) - - stream_permission_group_settings = { - "can_remove_subscribers_group": GroupPermissionSetting( - require_system_group=True, - allow_internet_group=False, - allow_owners_group=False, - allow_nobody_group=False, - allow_everyone_group=True, - default_group_name=SystemGroups.ADMINISTRATORS, - id_field_name="can_remove_subscribers_group_id", - ), - } - - class Meta: - indexes = [ - models.Index(Upper("name"), name="upper_stream_name_idx"), - ] - - @override - def __str__(self) -> str: - return self.name - - def is_public(self) -> bool: - # All streams are private in Zephyr mirroring realms. - return not self.invite_only and not self.is_in_zephyr_realm - - def is_history_realm_public(self) -> bool: - return self.is_public() - - def is_history_public_to_subscribers(self) -> bool: - return self.history_public_to_subscribers - - # Stream fields included whenever a Stream object is provided to - # Zulip clients via the API. A few details worth noting: - # * "id" is represented as "stream_id" in most API interfaces. - # * "email_token" is not realm-public and thus is not included here. - # * is_in_zephyr_realm is a backend-only optimization. - # * "deactivated" streams are filtered from the API entirely. - # * "realm" and "recipient" are not exposed to clients via the API. - API_FIELDS = [ - "date_created", - "description", - "first_message_id", - "history_public_to_subscribers", - "id", - "invite_only", - "is_web_public", - "message_retention_days", - "name", - "rendered_description", - "stream_post_policy", - "can_remove_subscribers_group_id", - ] - - def to_dict(self) -> DefaultStreamDict: - return DefaultStreamDict( - can_remove_subscribers_group=self.can_remove_subscribers_group_id, - date_created=datetime_to_timestamp(self.date_created), - description=self.description, - first_message_id=self.first_message_id, - history_public_to_subscribers=self.history_public_to_subscribers, - invite_only=self.invite_only, - is_web_public=self.is_web_public, - message_retention_days=self.message_retention_days, - name=self.name, - rendered_description=self.rendered_description, - stream_id=self.id, - stream_post_policy=self.stream_post_policy, - is_announcement_only=self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS, - ) - - -post_save.connect(flush_stream, sender=Stream) -post_delete.connect(flush_stream, sender=Stream) - - -class UserTopic(models.Model): - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - stream = models.ForeignKey(Stream, on_delete=CASCADE) - recipient = models.ForeignKey(Recipient, on_delete=CASCADE) - topic_name = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH) - # The default value for last_updated is a few weeks before tracking - # of when topics were muted was first introduced. It's designed - # to be obviously incorrect so that one can tell it's backfilled data. - last_updated = models.DateTimeField(default=datetime(2020, 1, 1, 0, 0, tzinfo=timezone.utc)) - - class VisibilityPolicy(models.IntegerChoices): - # A normal muted topic. No notifications and unreads hidden. - MUTED = 1, "Muted topic" - - # This topic will behave like an unmuted topic in an unmuted stream even if it - # belongs to a muted stream. - UNMUTED = 2, "Unmuted topic in muted stream" - - # This topic will behave like `UNMUTED`, plus some additional - # display and/or notifications priority that is TBD and likely to - # be configurable; see #6027. Not yet implemented. - FOLLOWED = 3, "Followed topic" - - # Implicitly, if a UserTopic does not exist, the (user, topic) - # pair should have normal behavior for that (user, stream) pair. - - # We use this in our code to represent the condition in the comment above. - INHERIT = 0, "User's default policy for the stream." - - visibility_policy = models.SmallIntegerField( - choices=VisibilityPolicy.choices, default=VisibilityPolicy.MUTED - ) - - class Meta: - constraints = [ - models.UniqueConstraint( - "user_profile", - "stream", - Lower("topic_name"), - name="usertopic_case_insensitive_topic_uniq", - ), - ] - - indexes = [ - models.Index("stream", Upper("topic_name"), name="zerver_mutedtopic_stream_topic"), - # This index is designed to optimize queries fetching the - # set of users who have special policy for a stream, - # e.g. for the send-message code paths. - models.Index( - fields=("stream", "topic_name", "visibility_policy", "user_profile"), - name="zerver_usertopic_stream_topic_user_visibility_idx", - ), - # This index is useful for handling API requests fetching the - # muted topics for a given user or user/stream pair. - models.Index( - fields=("user_profile", "visibility_policy", "stream", "topic_name"), - name="zerver_usertopic_user_visibility_idx", - ), - ] - - @override - def __str__(self) -> str: - return f"({self.user_profile.email}, {self.stream.name}, {self.topic_name}, {self.last_updated})" - - -class MutedUser(models.Model): - user_profile = models.ForeignKey(UserProfile, related_name="muter", on_delete=CASCADE) - muted_user = models.ForeignKey(UserProfile, related_name="muted", on_delete=CASCADE) - date_muted = models.DateTimeField(default=timezone_now) - - class Meta: - unique_together = ("user_profile", "muted_user") - - @override - def __str__(self) -> str: - return f"{self.user_profile.email} -> {self.muted_user.email}" - - -post_save.connect(flush_muting_users_cache, sender=MutedUser) -post_delete.connect(flush_muting_users_cache, sender=MutedUser) - - -class Client(models.Model): - MAX_NAME_LENGTH = 30 - name = models.CharField(max_length=MAX_NAME_LENGTH, db_index=True, unique=True) - - @override - def __str__(self) -> str: - return self.name - - -get_client_cache: Dict[str, Client] = {} - - -def clear_client_cache() -> None: # nocoverage - global get_client_cache - get_client_cache = {} - - -def get_client(name: str) -> Client: - # Accessing KEY_PREFIX through the module is necessary - # because we need the updated value of the variable. - cache_name = cache.KEY_PREFIX + name[0 : Client.MAX_NAME_LENGTH] - if cache_name not in get_client_cache: - result = get_client_remote_cache(name) - get_client_cache[cache_name] = result - return get_client_cache[cache_name] - - -def get_client_cache_key(name: str) -> str: - return f"get_client:{hashlib.sha1(name.encode()).hexdigest()}" - - -@cache_with_key(get_client_cache_key, timeout=3600 * 24 * 7) -def get_client_remote_cache(name: str) -> Client: - (client, _) = Client.objects.get_or_create(name=name[0 : Client.MAX_NAME_LENGTH]) - return client - - -def get_realm_stream(stream_name: str, realm_id: int) -> Stream: - return Stream.objects.get(name__iexact=stream_name.strip(), realm_id=realm_id) - - -def get_active_streams(realm: Realm) -> QuerySet[Stream]: - """ - Return all streams (including invite-only streams) that have not been deactivated. - """ - return Stream.objects.filter(realm=realm, deactivated=False) - - -def get_linkable_streams(realm_id: int) -> QuerySet[Stream]: - """ - This returns the streams that we are allowed to linkify using - something like "#frontend" in our markup. For now the business - rule is that you can link any stream in the realm that hasn't - been deactivated (similar to how get_active_streams works). - """ - return Stream.objects.filter(realm_id=realm_id, deactivated=False) - - -def get_stream(stream_name: str, realm: Realm) -> Stream: - """ - Callers that don't have a Realm object already available should use - get_realm_stream directly, to avoid unnecessarily fetching the - Realm object. - """ - return get_realm_stream(stream_name, realm.id) - - -def get_stream_by_id_in_realm(stream_id: int, realm: Realm) -> Stream: - return Stream.objects.select_related("realm", "recipient").get(id=stream_id, realm=realm) - - -def bulk_get_streams(realm: Realm, stream_names: Set[str]) -> Dict[str, Any]: - def fetch_streams_by_name(stream_names: Set[str]) -> QuerySet[Stream]: - # - # This should be just - # - # Stream.objects.select_related().filter(name__iexact__in=stream_names, - # realm_id=realm_id) - # - # But chaining __in and __iexact doesn't work with Django's - # ORM, so we have the following hack to construct the relevant where clause - where_clause = ( - "upper(zerver_stream.name::text) IN (SELECT upper(name) FROM unnest(%s) AS name)" - ) - return get_active_streams(realm).extra(where=[where_clause], params=(list(stream_names),)) - - if not stream_names: - return {} - streams = list(fetch_streams_by_name(stream_names)) - return {stream.name.lower(): stream for stream in streams} - - -def get_huddle_user_ids(recipient: Recipient) -> ValuesQuerySet["Subscription", int]: - assert recipient.type == Recipient.HUDDLE - - return ( - Subscription.objects.filter( - recipient=recipient, - ) - .order_by("user_profile_id") - .values_list("user_profile_id", flat=True) - ) - - -def bulk_get_huddle_user_ids(recipient_ids: List[int]) -> Dict[int, Set[int]]: - """ - Takes a list of huddle-type recipient_ids, returns a dict - mapping recipient id to list of user ids in the huddle. - - We rely on our caller to pass us recipient_ids that correspond - to huddles, but technically this function is valid for any type - of subscription. - """ - if not recipient_ids: - return {} - - subscriptions = Subscription.objects.filter( - recipient_id__in=recipient_ids, - ).only("user_profile_id", "recipient_id") - - result_dict: Dict[int, Set[int]] = defaultdict(set) - for subscription in subscriptions: - result_dict[subscription.recipient_id].add(subscription.user_profile_id) - - return result_dict - - -class AbstractMessage(models.Model): - sender = models.ForeignKey(UserProfile, on_delete=CASCADE) - - # The target of the message is signified by the Recipient object. - # See the Recipient class for details. - recipient = models.ForeignKey(Recipient, on_delete=CASCADE) - - # The realm containing the message. Usually this will be the same - # as the realm of the messages's sender; the exception to that is - # cross-realm bot users. - # - # Important for efficient indexes and sharding in multi-realm servers. - realm = models.ForeignKey(Realm, on_delete=CASCADE) - - # The message's topic. - # - # Early versions of Zulip called this concept a "subject", as in an email - # "subject line", before changing to "topic" in 2013 (commit dac5a46fa). - # UI and user documentation now consistently say "topic". New APIs and - # new code should generally also say "topic". - # - # See also the `topic_name` method on `Message`. - subject = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH, db_index=True) - - # The raw Markdown-format text (E.g., what the user typed into the compose box). - content = models.TextField() - - # The HTML rendered content resulting from rendering the content - # with the Markdown processor. - rendered_content = models.TextField(null=True) - # A rarely-incremented version number, theoretically useful for - # tracking which messages have been already rerendered when making - # major changes to the markup rendering process. - rendered_content_version = models.IntegerField(null=True) - - date_sent = models.DateTimeField("date sent", db_index=True) - - # A Client object indicating what type of Zulip client sent this message. - sending_client = models.ForeignKey(Client, on_delete=CASCADE) - - # The last time the message was modified by message editing or moving. - last_edit_time = models.DateTimeField(null=True) - - # A JSON-encoded list of objects describing any past edits to this - # message, oldest first. - edit_history = models.TextField(null=True) - - # Whether the message contains a (link to) an uploaded file. - has_attachment = models.BooleanField(default=False, db_index=True) - # Whether the message contains a visible image element. - has_image = models.BooleanField(default=False, db_index=True) - # Whether the message contains a link. - has_link = models.BooleanField(default=False, db_index=True) - - class Meta: - abstract = True - - @override - def __str__(self) -> str: - return f"{self.recipient.label()} / {self.subject} / {self.sender!r}" - - -class ArchiveTransaction(models.Model): - timestamp = models.DateTimeField(default=timezone_now, db_index=True) - # Marks if the data archived in this transaction has been restored: - restored = models.BooleanField(default=False, db_index=True) - - type = models.PositiveSmallIntegerField(db_index=True) - # Valid types: - RETENTION_POLICY_BASED = 1 # Archiving was executed due to automated retention policies - MANUAL = 2 # Archiving was run manually, via move_messages_to_archive function - - # ForeignKey to the realm with which objects archived in this transaction are associated. - # If type is set to MANUAL, this should be null. - realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE) - - @override - def __str__(self) -> str: - return "id: {id}, type: {type}, realm: {realm}, timestamp: {timestamp}".format( - id=self.id, - type="MANUAL" if self.type == self.MANUAL else "RETENTION_POLICY_BASED", - realm=self.realm.string_id if self.realm else None, - timestamp=self.timestamp, - ) - - -class ArchivedMessage(AbstractMessage): - """Used as a temporary holding place for deleted messages before they - are permanently deleted. This is an important part of a robust - 'message retention' feature. - """ - - archive_transaction = models.ForeignKey(ArchiveTransaction, on_delete=CASCADE) - - -class Message(AbstractMessage): - # Recipient types used when a Message object is provided to - # Zulip clients via the API. - # - # A detail worth noting: - # * "direct" was introduced in 2023 with the goal of - # deprecating the original "private" and becoming the - # preferred way to indicate a personal or huddle - # Recipient type via the API. - API_RECIPIENT_TYPES = ["direct", "private", "stream"] - - search_tsvector = SearchVectorField(null=True) - - DEFAULT_SELECT_RELATED = ["sender", "realm", "recipient", "sending_client"] - - def topic_name(self) -> str: - """ - Please start using this helper to facilitate an - eventual switch over to a separate topic table. - """ - return self.subject - - def set_topic_name(self, topic_name: str) -> None: - self.subject = topic_name - - def is_stream_message(self) -> bool: - """ - Find out whether a message is a stream message by - looking up its recipient.type. TODO: Make this - an easier operation by denormalizing the message - type onto Message, either explicitly (message.type) - or implicitly (message.stream_id is not None). - """ - return self.recipient.type == Recipient.STREAM - - def get_realm(self) -> Realm: - return self.realm - - def save_rendered_content(self) -> None: - self.save(update_fields=["rendered_content", "rendered_content_version"]) - - @staticmethod - def need_to_render_content( - rendered_content: Optional[str], - rendered_content_version: Optional[int], - markdown_version: int, - ) -> bool: - return ( - rendered_content is None - or rendered_content_version is None - or rendered_content_version < markdown_version - ) - - def sent_by_human(self) -> bool: - """Used to determine whether a message was sent by a full Zulip UI - style client (and thus whether the message should be treated - as sent by a human and automatically marked as read for the - sender). The purpose of this distinction is to ensure that - message sent to the user by e.g. a Google Calendar integration - using the user's own API key don't get marked as read - automatically. - """ - sending_client = self.sending_client.name.lower() - - return ( - ( - sending_client - in ( - "zulipandroid", - "zulipios", - "zulipdesktop", - "zulipmobile", - "zulipelectron", - "zulipterminal", - "snipe", - "website", - "ios", - "android", - ) - ) - or ("desktop app" in sending_client) - # Since the vast majority of messages are sent by humans - # in Zulip, treat test suite messages as such. - or (sending_client == "test suite" and settings.TEST_SUITE) - ) - - @staticmethod - def is_status_message(content: str, rendered_content: str) -> bool: - """ - "status messages" start with /me and have special rendering: - /me loves chocolate -> Full Name loves chocolate - """ - if content.startswith("/me "): - return True - return False - - class Meta: - indexes = [ - GinIndex("search_tsvector", fastupdate=False, name="zerver_message_search_tsvector"), - models.Index( - # For moving messages between streams or marking - # streams as read. The "id" at the end makes it easy - # to scan the resulting messages in order, and perform - # batching. - "realm_id", - "recipient_id", - "id", - name="zerver_message_realm_recipient_id", - ), - models.Index( - # For generating digest emails and message archiving, - # which both group by stream. - "realm_id", - "recipient_id", - "date_sent", - name="zerver_message_realm_recipient_date_sent", - ), - models.Index( - # For exports, which want to limit both sender and - # receiver. The prefix of this index (realm_id, - # sender_id) can be used for scrubbing users and/or - # deleting users' messages. - "realm_id", - "sender_id", - "recipient_id", - name="zerver_message_realm_sender_recipient", - ), - models.Index( - # For analytics queries - "realm_id", - "date_sent", - name="zerver_message_realm_date_sent", - ), - models.Index( - # For users searching by topic (but not stream), which - # is done case-insensitively - "realm_id", - Upper("subject"), - F("id").desc(nulls_last=True), - name="zerver_message_realm_upper_subject", - ), - models.Index( - # Most stream/topic searches are case-insensitive by - # topic name (e.g. messages_for_topic). The "id" at - # the end makes it easy to scan the resulting messages - # in order, and perform batching. - "realm_id", - "recipient_id", - Upper("subject"), - F("id").desc(nulls_last=True), - name="zerver_message_realm_recipient_upper_subject", - ), - models.Index( - # Used by already_sent_mirrored_message_id, and when - # determining recent topics (we post-process to merge - # and show the most recent case) - "realm_id", - "recipient_id", - "subject", - F("id").desc(nulls_last=True), - name="zerver_message_realm_recipient_subject", - ), - models.Index( - # Only used by update_first_visible_message_id - "realm_id", - F("id").desc(nulls_last=True), - name="zerver_message_realm_id", - ), - ] - - -def get_context_for_message(message: Message) -> QuerySet[Message]: - return Message.objects.filter( - # Uses index: zerver_message_realm_recipient_upper_subject - realm_id=message.realm_id, - recipient_id=message.recipient_id, - subject__iexact=message.subject, - id__lt=message.id, - date_sent__gt=message.date_sent - timedelta(minutes=15), - ).order_by("-id")[:10] - - -post_save.connect(flush_message, sender=Message) - - -class AbstractSubMessage(models.Model): - # We can send little text messages that are associated with a regular - # Zulip message. These can be used for experimental widgets like embedded - # games, surveys, mini threads, etc. These are designed to be pretty - # generic in purpose. - - sender = models.ForeignKey(UserProfile, on_delete=CASCADE) - msg_type = models.TextField() - content = models.TextField() - - class Meta: - abstract = True - - -class SubMessage(AbstractSubMessage): - message = models.ForeignKey(Message, on_delete=CASCADE) - - @staticmethod - def get_raw_db_rows(needed_ids: List[int]) -> List[Dict[str, Any]]: - fields = ["id", "message_id", "sender_id", "msg_type", "content"] - query = SubMessage.objects.filter(message_id__in=needed_ids).values(*fields) - query = query.order_by("message_id", "id") - return list(query) - - -class ArchivedSubMessage(AbstractSubMessage): - message = models.ForeignKey(ArchivedMessage, on_delete=CASCADE) - - -post_save.connect(flush_submessage, sender=SubMessage) - - -class Draft(models.Model): - """Server-side storage model for storing drafts so that drafts can be synced across - multiple clients/devices. - """ - - user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE) - recipient = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) - topic = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH, db_index=True) - content = models.TextField() # Length should not exceed MAX_MESSAGE_LENGTH - last_edit_time = models.DateTimeField(db_index=True) - - @override - def __str__(self) -> str: - return f"{self.user_profile.email} / {self.id} / {self.last_edit_time}" - - def to_dict(self) -> Dict[str, Any]: - to, recipient_type_str = get_recipient_ids(self.recipient, self.user_profile_id) - return { - "id": self.id, - "type": recipient_type_str, - "to": to, - "topic": self.topic, - "content": self.content, - "timestamp": int(self.last_edit_time.timestamp()), - } - - -class AbstractEmoji(models.Model): - """For emoji reactions to messages (and potentially future reaction types). - - Emoji are surprisingly complicated to implement correctly. For details - on how this subsystem works, see: - https://zulip.readthedocs.io/en/latest/subsystems/emoji.html - """ - - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - - # The user-facing name for an emoji reaction. With emoji aliases, - # there may be multiple accepted names for a given emoji; this - # field encodes which one the user selected. - emoji_name = models.TextField() - - UNICODE_EMOJI = "unicode_emoji" - REALM_EMOJI = "realm_emoji" - ZULIP_EXTRA_EMOJI = "zulip_extra_emoji" - REACTION_TYPES = ( - (UNICODE_EMOJI, gettext_lazy("Unicode emoji")), - (REALM_EMOJI, gettext_lazy("Custom emoji")), - (ZULIP_EXTRA_EMOJI, gettext_lazy("Zulip extra emoji")), - ) - reaction_type = models.CharField(default=UNICODE_EMOJI, choices=REACTION_TYPES, max_length=30) - - # A string with the property that (realm, reaction_type, - # emoji_code) uniquely determines the emoji glyph. - # - # We cannot use `emoji_name` for this purpose, since the - # name-to-glyph mappings for unicode emoji change with time as we - # update our emoji database, and multiple custom emoji can have - # the same `emoji_name` in a realm (at most one can have - # `deactivated=False`). The format for `emoji_code` varies by - # `reaction_type`: - # - # * For Unicode emoji, a dash-separated hex encoding of the sequence of - # Unicode codepoints that define this emoji in the Unicode - # specification. For examples, see "non_qualified" or "unified" in the - # following data, with "non_qualified" taking precedence when both present: - # https://raw.githubusercontent.com/iamcal/emoji-data/master/emoji_pretty.json - # - # * For user uploaded custom emoji (`reaction_type="realm_emoji"`), the stringified ID - # of the RealmEmoji object, computed as `str(realm_emoji.id)`. - # - # * For "Zulip extra emoji" (like :zulip:), the name of the emoji (e.g. "zulip"). - emoji_code = models.TextField() - - class Meta: - abstract = True - - -class AbstractReaction(AbstractEmoji): - class Meta: - abstract = True - unique_together = ("user_profile", "message", "reaction_type", "emoji_code") - - -class Reaction(AbstractReaction): - message = models.ForeignKey(Message, on_delete=CASCADE) - - @staticmethod - def get_raw_db_rows(needed_ids: List[int]) -> List[Dict[str, Any]]: - fields = [ - "message_id", - "emoji_name", - "emoji_code", - "reaction_type", - "user_profile__email", - "user_profile_id", - "user_profile__full_name", - ] - # The ordering is important here, as it makes it convenient - # for clients to display reactions in order without - # client-side sorting code. - return Reaction.objects.filter(message_id__in=needed_ids).values(*fields).order_by("id") - - @override - def __str__(self) -> str: - return f"{self.user_profile.email} / {self.message.id} / {self.emoji_name}" - - -class ArchivedReaction(AbstractReaction): - message = models.ForeignKey(ArchivedMessage, on_delete=CASCADE) - - -# Whenever a message is sent, for each user subscribed to the -# corresponding Recipient object (that is not long-term idle), we add -# a row to the UserMessage table indicating that that user received -# that message. This table allows us to quickly query any user's last -# 1000 messages to generate the home view and search exactly the -# user's message history. -# -# The long-term idle optimization is extremely important for large, -# open organizations, and is described in detail here: -# https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#soft-deactivation -# -# In particular, new messages to public streams will only generate -# UserMessage rows for Members who are long_term_idle if they would -# have nonzero flags for the message (E.g. a mention, alert word, or -# mobile push notification). -# -# The flags field stores metadata like whether the user has read the -# message, starred or collapsed the message, was mentioned in the -# message, etc. We use of postgres partial indexes on flags to make -# queries for "User X's messages with flag Y" extremely fast without -# consuming much storage space. -# -# UserMessage is the largest table in many Zulip installations, even -# though each row is only 4 integers. -class AbstractUserMessage(models.Model): - id = models.BigAutoField(primary_key=True) - - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - # The order here is important! It's the order of fields in the bitfield. - ALL_FLAGS = [ - "read", - "starred", - "collapsed", - "mentioned", - "stream_wildcard_mentioned", - "topic_wildcard_mentioned", - "group_mentioned", - # These next 2 flags are from features that have since been removed. - # We've cleared these 2 flags in migration 0486. - "force_expand", - "force_collapse", - # Whether the message contains any of the user's alert words. - "has_alert_word", - # The historical flag is used to mark messages which the user - # did not receive when they were sent, but later added to - # their history via e.g. starring the message. This is - # important accounting for the "Subscribed to stream" dividers. - "historical", - # Whether the message is a direct message; this flag is a - # denormalization of message.recipient.type to support an - # efficient index on UserMessage for a user's direct messages. - "is_private", - # Whether we've sent a push notification to the user's mobile - # devices for this message that has not been revoked. - "active_mobile_push_notification", - ] - # Certain flags are used only for internal accounting within the - # Zulip backend, and don't make sense to expose to the API. - NON_API_FLAGS = {"is_private", "active_mobile_push_notification"} - # Certain additional flags are just set once when the UserMessage - # row is created. - NON_EDITABLE_FLAGS = { - # These flags are bookkeeping and don't make sense to edit. - "has_alert_word", - "mentioned", - "stream_wildcard_mentioned", - "topic_wildcard_mentioned", - "group_mentioned", - "historical", - # Unused flags can't be edited. - "force_expand", - "force_collapse", - } - flags: BitHandler = BitField(flags=ALL_FLAGS, default=0) - - class Meta: - abstract = True - unique_together = ("user_profile", "message") - - @staticmethod - def where_flag_is_present(flagattr: Bit) -> str: - # Use this for Django ORM queries to access starred messages. - # This custom SQL plays nice with our partial indexes. Grep - # the code for example usage. - # - # The key detail is that e.g. - # UserMessage.objects.filter(user_profile=user_profile, flags=UserMessage.flags.starred) - # will generate a query involving `flags & 2 = 2`, which doesn't match our index. - return f"flags & {1 << flagattr.number} <> 0" - - @staticmethod - def where_flag_is_absent(flagattr: Bit) -> str: - return f"flags & {1 << flagattr.number} = 0" - - @staticmethod - def where_unread() -> str: - return AbstractUserMessage.where_flag_is_absent(AbstractUserMessage.flags.read) - - @staticmethod - def where_read() -> str: - return AbstractUserMessage.where_flag_is_present(AbstractUserMessage.flags.read) - - @staticmethod - def where_starred() -> str: - return AbstractUserMessage.where_flag_is_present(AbstractUserMessage.flags.starred) - - @staticmethod - def where_active_push_notification() -> str: - return AbstractUserMessage.where_flag_is_present( - AbstractUserMessage.flags.active_mobile_push_notification - ) - - def flags_list(self) -> List[str]: - flags = int(self.flags) - return self.flags_list_for_flags(flags) - - @staticmethod - def flags_list_for_flags(val: int) -> List[str]: - """ - This function is highly optimized, because it actually slows down - sending messages in a naive implementation. - """ - flags = [] - mask = 1 - for flag in UserMessage.ALL_FLAGS: - if (val & mask) and flag not in AbstractUserMessage.NON_API_FLAGS: - flags.append(flag) - mask <<= 1 - return flags - - -class UserMessage(AbstractUserMessage): - message = models.ForeignKey(Message, on_delete=CASCADE) - - class Meta(AbstractUserMessage.Meta): - indexes = [ - models.Index( - "user_profile", - "message", - condition=Q(flags__andnz=AbstractUserMessage.flags.starred.mask), - name="zerver_usermessage_starred_message_id", - ), - models.Index( - "user_profile", - "message", - condition=Q(flags__andnz=AbstractUserMessage.flags.mentioned.mask), - name="zerver_usermessage_mentioned_message_id", - ), - models.Index( - "user_profile", - "message", - condition=Q(flags__andz=AbstractUserMessage.flags.read.mask), - name="zerver_usermessage_unread_message_id", - ), - models.Index( - "user_profile", - "message", - condition=Q(flags__andnz=AbstractUserMessage.flags.has_alert_word.mask), - name="zerver_usermessage_has_alert_word_message_id", - ), - models.Index( - "user_profile", - "message", - condition=Q(flags__andnz=AbstractUserMessage.flags.mentioned.mask) - | Q(flags__andnz=AbstractUserMessage.flags.stream_wildcard_mentioned.mask), - name="zerver_usermessage_wildcard_mentioned_message_id", - ), - models.Index( - "user_profile", - "message", - condition=Q( - flags__andnz=AbstractUserMessage.flags.mentioned.mask - | AbstractUserMessage.flags.stream_wildcard_mentioned.mask - | AbstractUserMessage.flags.topic_wildcard_mentioned.mask - | AbstractUserMessage.flags.group_mentioned.mask - ), - name="zerver_usermessage_any_mentioned_message_id", - ), - models.Index( - "user_profile", - "message", - condition=Q(flags__andnz=AbstractUserMessage.flags.is_private.mask), - name="zerver_usermessage_is_private_message_id", - ), - models.Index( - "user_profile", - "message", - condition=Q( - flags__andnz=AbstractUserMessage.flags.active_mobile_push_notification.mask - ), - name="zerver_usermessage_active_mobile_push_notification_id", - ), - ] - - @override - def __str__(self) -> str: - recipient_string = self.message.recipient.label() - return f"{recipient_string} / {self.user_profile.email} ({self.flags_list()})" - - @staticmethod - def select_for_update_query() -> QuerySet["UserMessage"]: - """This SELECT FOR UPDATE query ensures consistent ordering on - the row locks acquired by a bulk update operation to modify - message flags using bitand/bitor. - - This consistent ordering is important to prevent deadlocks when - 2 or more bulk updates to the same rows in the UserMessage table - race against each other (For example, if a client submits - simultaneous duplicate API requests to mark a certain set of - messages as read). - """ - return UserMessage.objects.select_for_update().order_by("message_id") - - @staticmethod - def has_any_mentions(user_profile_id: int, message_id: int) -> bool: - # The query uses the 'zerver_usermessage_any_mentioned_message_id' index. - return UserMessage.objects.filter( - Q( - flags__andnz=UserMessage.flags.mentioned.mask - | UserMessage.flags.stream_wildcard_mentioned.mask - | UserMessage.flags.topic_wildcard_mentioned.mask - | UserMessage.flags.group_mentioned.mask - ), - user_profile_id=user_profile_id, - message_id=message_id, - ).exists() - - -def get_usermessage_by_message_id( - user_profile: UserProfile, message_id: int -) -> Optional[UserMessage]: - try: - return UserMessage.objects.select_related().get( - user_profile=user_profile, message_id=message_id - ) - except UserMessage.DoesNotExist: - return None - - -class ArchivedUserMessage(AbstractUserMessage): - """Used as a temporary holding place for deleted UserMessages objects - before they are permanently deleted. This is an important part of - a robust 'message retention' feature. - """ - - message = models.ForeignKey(ArchivedMessage, on_delete=CASCADE) - - @override - def __str__(self) -> str: - recipient_string = self.message.recipient.label() - return f"{recipient_string} / {self.user_profile.email} ({self.flags_list()})" - - -class AbstractAttachment(models.Model): - file_name = models.TextField(db_index=True) - - # path_id is a storage location agnostic representation of the path of the file. - # If the path of a file is http://localhost:9991/user_uploads/a/b/abc/temp_file.py - # then its path_id will be a/b/abc/temp_file.py. - path_id = models.TextField(db_index=True, unique=True) - owner = models.ForeignKey(UserProfile, on_delete=CASCADE) - realm = models.ForeignKey(Realm, on_delete=CASCADE) - - create_time = models.DateTimeField( - default=timezone_now, - db_index=True, - ) - # Size of the uploaded file, in bytes - size = models.IntegerField() - - # The two fields below serve as caches to let us avoid looking up - # the corresponding messages/streams to check permissions before - # serving these files. - # - # For both fields, the `null` state is used when a change in - # message permissions mean that we need to determine their proper - # value. - - # Whether this attachment has been posted to a public stream, and - # thus should be available to all non-guest users in the - # organization (even if they weren't a recipient of a message - # linking to it). - is_realm_public = models.BooleanField(default=False, null=True) - # Whether this attachment has been posted to a web-public stream, - # and thus should be available to everyone on the internet, even - # if the person isn't logged in. - is_web_public = models.BooleanField(default=False, null=True) - - class Meta: - abstract = True - - @override - def __str__(self) -> str: - return self.file_name - - -class ArchivedAttachment(AbstractAttachment): - """Used as a temporary holding place for deleted Attachment objects - before they are permanently deleted. This is an important part of - a robust 'message retention' feature. - - Unlike the similar archive tables, ArchivedAttachment does not - have an ArchiveTransaction foreign key, and thus will not be - directly deleted by clean_archived_data. Instead, attachments that - were only referenced by now fully deleted messages will leave - ArchivedAttachment objects with empty `.messages`. - - A second step, delete_old_unclaimed_attachments, will delete the - resulting orphaned ArchivedAttachment objects, along with removing - the associated uploaded files from storage. - """ - - messages = models.ManyToManyField( - ArchivedMessage, related_name="attachment_set", related_query_name="attachment" - ) - - -class Attachment(AbstractAttachment): - messages = models.ManyToManyField(Message) - - # This is only present for Attachment and not ArchiveAttachment. - # because ScheduledMessage is not subject to archiving. - scheduled_messages = models.ManyToManyField("zerver.ScheduledMessage") - - def is_claimed(self) -> bool: - return self.messages.exists() or self.scheduled_messages.exists() - - def to_dict(self) -> Dict[str, Any]: - return { - "id": self.id, - "name": self.file_name, - "path_id": self.path_id, - "size": self.size, - # convert to JavaScript-style UNIX timestamp so we can take - # advantage of client time zones. - "create_time": int(time.mktime(self.create_time.timetuple()) * 1000), - "messages": [ - { - "id": m.id, - "date_sent": int(time.mktime(m.date_sent.timetuple()) * 1000), - } - for m in self.messages.all() - ], - } - - -post_save.connect(flush_used_upload_space_cache, sender=Attachment) -post_delete.connect(flush_used_upload_space_cache, sender=Attachment) - - -def validate_attachment_request_for_spectator_access( - realm: Realm, attachment: Attachment -) -> Optional[bool]: - if attachment.realm != realm: - return False - - # Update cached is_web_public property, if necessary. - if attachment.is_web_public is None: - # Fill the cache in a single query. This is important to avoid - # a potential race condition between checking and setting, - # where the attachment could have been moved again. - Attachment.objects.filter(id=attachment.id, is_web_public__isnull=True).update( - is_web_public=Exists( - Message.objects.filter( - # Uses index: zerver_attachment_messages_attachment_id_message_id_key - realm_id=realm.id, - attachment=OuterRef("id"), - recipient__stream__invite_only=False, - recipient__stream__is_web_public=True, - ), - ), - ) - attachment.refresh_from_db() - - if not attachment.is_web_public: - return False - - if settings.RATE_LIMITING: - try: - from zerver.lib.rate_limiter import rate_limit_spectator_attachment_access_by_file - - rate_limit_spectator_attachment_access_by_file(attachment.path_id) - except RateLimitedError: - return False - - return True - - -def validate_attachment_request( - maybe_user_profile: Union[UserProfile, AnonymousUser], - path_id: str, - realm: Optional[Realm] = None, -) -> Optional[bool]: - try: - attachment = Attachment.objects.get(path_id=path_id) - except Attachment.DoesNotExist: - return None - - if isinstance(maybe_user_profile, AnonymousUser): - assert realm is not None - return validate_attachment_request_for_spectator_access(realm, attachment) - - user_profile = maybe_user_profile - assert isinstance(user_profile, UserProfile) - - # Update cached is_realm_public property, if necessary. - if attachment.is_realm_public is None: - # Fill the cache in a single query. This is important to avoid - # a potential race condition between checking and setting, - # where the attachment could have been moved again. - Attachment.objects.filter(id=attachment.id, is_realm_public__isnull=True).update( - is_realm_public=Exists( - Message.objects.filter( - # Uses index: zerver_attachment_messages_attachment_id_message_id_key - realm_id=user_profile.realm_id, - attachment=OuterRef("id"), - recipient__stream__invite_only=False, - ), - ), - ) - attachment.refresh_from_db() - - if user_profile == attachment.owner: - # If you own the file, you can access it. - return True - if ( - attachment.is_realm_public - and attachment.realm == user_profile.realm - and user_profile.can_access_public_streams() - ): - # Any user in the realm can access realm-public files - return True - - messages = attachment.messages.all() - if UserMessage.objects.filter(user_profile=user_profile, message__in=messages).exists(): - # If it was sent in a direct message or private stream - # message, then anyone who received that message can access it. - return True - - # The user didn't receive any of the messages that included this - # attachment. But they might still have access to it, if it was - # sent to a stream they are on where history is public to - # subscribers. - - # These are subscriptions to a stream one of the messages was sent to - relevant_stream_ids = Subscription.objects.filter( - user_profile=user_profile, - active=True, - recipient__type=Recipient.STREAM, - recipient__in=[m.recipient_id for m in messages], - ).values_list("recipient__type_id", flat=True) - if len(relevant_stream_ids) == 0: - return False - - return Stream.objects.filter( - id__in=relevant_stream_ids, history_public_to_subscribers=True - ).exists() - - -def get_old_unclaimed_attachments( - weeks_ago: int, -) -> Tuple[QuerySet[Attachment], QuerySet[ArchivedAttachment]]: - """ - The logic in this function is fairly tricky. The essence is that - a file should be cleaned up if and only if it not referenced by any - Message, ScheduledMessage or ArchivedMessage. The way to find that out is through the - Attachment and ArchivedAttachment tables. - The queries are complicated by the fact that an uploaded file - may have either only an Attachment row, only an ArchivedAttachment row, - or both - depending on whether some, all or none of the messages - linking to it have been archived. - """ - delta_weeks_ago = timezone_now() - timedelta(weeks=weeks_ago) - - # The Attachment vs ArchivedAttachment queries are asymmetric because only - # Attachment has the scheduled_messages relation. - old_attachments = Attachment.objects.annotate( - has_other_messages=Exists( - ArchivedAttachment.objects.filter(id=OuterRef("id")).exclude(messages=None) - ) - ).filter( - messages=None, - scheduled_messages=None, - create_time__lt=delta_weeks_ago, - has_other_messages=False, - ) - old_archived_attachments = ArchivedAttachment.objects.annotate( - has_other_messages=Exists( - Attachment.objects.filter(id=OuterRef("id")).exclude( - messages=None, scheduled_messages=None - ) - ) - ).filter(messages=None, create_time__lt=delta_weeks_ago, has_other_messages=False) - - return old_attachments, old_archived_attachments - - -class Subscription(models.Model): - """Keeps track of which users are part of the - audience for a given Recipient object. - - For 1:1 and group direct message Recipient objects, only the - user_profile and recipient fields have any meaning, defining the - immutable set of users who are in the audience for that Recipient. - - For Recipient objects associated with a Stream, the remaining - fields in this model describe the user's subscription to that stream. - """ - - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - recipient = models.ForeignKey(Recipient, on_delete=CASCADE) - - # Whether the user has since unsubscribed. We mark Subscription - # objects as inactive, rather than deleting them, when a user - # unsubscribes, so we can preserve user customizations like - # notification settings, stream color, etc., if the user later - # resubscribes. - active = models.BooleanField(default=True) - # This is a denormalization designed to improve the performance of - # bulk queries of Subscription objects, Whether the subscribed user - # is active tends to be a key condition in those queries. - # We intentionally don't specify a default value to promote thinking - # about this explicitly, as in some special cases, such as data import, - # we may be creating Subscription objects for a user that's deactivated. - is_user_active = models.BooleanField() - - # Whether this user had muted this stream. - is_muted = models.BooleanField(default=False) - - DEFAULT_STREAM_COLOR = "#c2c2c2" - color = models.CharField(max_length=10, default=DEFAULT_STREAM_COLOR) - pin_to_top = models.BooleanField(default=False) - - # These fields are stream-level overrides for the user's default - # configuration for notification, configured in UserProfile. The - # default, None, means we just inherit the user-level default. - desktop_notifications = models.BooleanField(null=True, default=None) - audible_notifications = models.BooleanField(null=True, default=None) - push_notifications = models.BooleanField(null=True, default=None) - email_notifications = models.BooleanField(null=True, default=None) - wildcard_mentions_notify = models.BooleanField(null=True, default=None) - - class Meta: - unique_together = ("user_profile", "recipient") - indexes = [ - models.Index( - fields=("recipient", "user_profile"), - name="zerver_subscription_recipient_id_user_profile_id_idx", - condition=Q(active=True, is_user_active=True), - ), - ] - - @override - def __str__(self) -> str: - return f"{self.user_profile!r} -> {self.recipient!r}" - - # Subscription fields included whenever a Subscription object is provided to - # Zulip clients via the API. A few details worth noting: - # * These fields will generally be merged with Stream.API_FIELDS - # data about the stream. - # * "user_profile" is usually implied as full API access to Subscription - # is primarily done for the current user; API access to other users' - # subscriptions is generally limited to boolean yes/no. - # * "id" and "recipient_id" are not included as they are not used - # in the Zulip API; it's an internal implementation detail. - # Subscription objects are always looked up in the API via - # (user_profile, stream) pairs. - # * "active" is often excluded in API use cases where it is implied. - # * "is_muted" often needs to be copied to not "in_home_view" for - # backwards-compatibility. - API_FIELDS = [ - "audible_notifications", - "color", - "desktop_notifications", - "email_notifications", - "is_muted", - "pin_to_top", - "push_notifications", - "wildcard_mentions_notify", - ] - - -@cache_with_key(user_profile_by_id_cache_key, timeout=3600 * 24 * 7) -def get_user_profile_by_id(user_profile_id: int) -> UserProfile: - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(id=user_profile_id) - - -def get_user_profile_by_email(email: str) -> UserProfile: - """This function is intended to be used for - manual manage.py shell work; robust code must use get_user or - get_user_by_delivery_email instead, because Zulip supports - multiple users with a given (delivery) email address existing on a - single server (in different realms). - """ - return UserProfile.objects.select_related("realm").get(delivery_email__iexact=email.strip()) - - -@cache_with_key(user_profile_by_api_key_cache_key, timeout=3600 * 24 * 7) -def maybe_get_user_profile_by_api_key(api_key: str) -> Optional[UserProfile]: - try: - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(api_key=api_key) - except UserProfile.DoesNotExist: - # We will cache failed lookups with None. The - # use case here is that broken API clients may - # continually ask for the same wrong API key, and - # we want to handle that as quickly as possible. - return None - - -def get_user_profile_by_api_key(api_key: str) -> UserProfile: - user_profile = maybe_get_user_profile_by_api_key(api_key) - if user_profile is None: - raise UserProfile.DoesNotExist - - return user_profile - - -def get_user_by_delivery_email(email: str, realm: Realm) -> UserProfile: - """Fetches a user given their delivery email. For use in - authentication/registration contexts. Do not use for user-facing - views (e.g. Zulip API endpoints) as doing so would violate the - EMAIL_ADDRESS_VISIBILITY_ADMINS security model. Use get_user in - those code paths. - """ - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(delivery_email__iexact=email.strip(), realm=realm) - - -def get_users_by_delivery_email(emails: Set[str], realm: Realm) -> QuerySet[UserProfile]: - """This is similar to get_user_by_delivery_email, and - it has the same security caveats. It gets multiple - users and returns a QuerySet, since most callers - will only need two or three fields. - - If you are using this to get large UserProfile objects, you are - probably making a mistake, but if you must, - then use `select_related`. - """ - - """ - Django doesn't support delivery_email__iexact__in, so - we simply OR all the filters that we'd do for the - one-email case. - """ - email_filter = Q() - for email in emails: - email_filter |= Q(delivery_email__iexact=email.strip()) - - return UserProfile.objects.filter(realm=realm).filter(email_filter) - - -@cache_with_key(user_profile_cache_key, timeout=3600 * 24 * 7) -def get_user(email: str, realm: Realm) -> UserProfile: - """Fetches the user by its visible-to-other users username (in the - `email` field). For use in API contexts; do not use in - authentication/registration contexts as doing so will break - authentication in organizations using - EMAIL_ADDRESS_VISIBILITY_ADMINS. In those code paths, use - get_user_by_delivery_email. - """ - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(email__iexact=email.strip(), realm=realm) - - -def get_active_user(email: str, realm: Realm) -> UserProfile: - """Variant of get_user_by_email that excludes deactivated users. - See get_user docstring for important usage notes.""" - user_profile = get_user(email, realm) - if not user_profile.is_active: - raise UserProfile.DoesNotExist - return user_profile - - -def get_user_profile_by_id_in_realm(uid: int, realm: Realm) -> UserProfile: - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(id=uid, realm=realm) - - -def get_active_user_profile_by_id_in_realm(uid: int, realm: Realm) -> UserProfile: - user_profile = get_user_profile_by_id_in_realm(uid, realm) - if not user_profile.is_active: - raise UserProfile.DoesNotExist - return user_profile - - -def get_user_including_cross_realm(email: str, realm: Realm) -> UserProfile: - if is_cross_realm_bot_email(email): - return get_system_bot(email, realm.id) - assert realm is not None - return get_user(email, realm) - - -@cache_with_key(bot_profile_cache_key, timeout=3600 * 24 * 7) -def get_system_bot(email: str, realm_id: int) -> UserProfile: - """ - This function doesn't use the realm_id argument yet, but requires - passing it as preparation for adding system bots to each realm instead - of having them all in a separate system bot realm. - If you're calling this function, use the id of the realm in which the system - bot will be after that migration. If the bot is supposed to send a message, - the same realm as the one *to* which the message will be sent should be used - because - cross-realm messages will be eliminated as part of the migration. - """ - return UserProfile.objects.select_related("realm").get(email__iexact=email.strip()) - - -def get_user_by_id_in_realm_including_cross_realm( - uid: int, - realm: Optional[Realm], -) -> UserProfile: - user_profile = get_user_profile_by_id(uid) - if user_profile.realm == realm: - return user_profile - - # Note: This doesn't validate whether the `realm` passed in is - # None/invalid for the is_cross_realm_bot_email case. - if is_cross_realm_bot_email(user_profile.delivery_email): - return user_profile - - raise UserProfile.DoesNotExist - - -@cache_with_key(realm_user_dicts_cache_key, timeout=3600 * 24 * 7) -def get_realm_user_dicts(realm_id: int) -> List[RawUserDict]: - return list( - UserProfile.objects.filter( - realm_id=realm_id, - ).values(*realm_user_dict_fields) - ) - - -@cache_with_key(active_user_ids_cache_key, timeout=3600 * 24 * 7) -def active_user_ids(realm_id: int) -> List[int]: - query = UserProfile.objects.filter( - realm_id=realm_id, - is_active=True, - ).values_list("id", flat=True) - return list(query) - - -@cache_with_key(active_non_guest_user_ids_cache_key, timeout=3600 * 24 * 7) -def active_non_guest_user_ids(realm_id: int) -> List[int]: - query = ( - UserProfile.objects.filter( - realm_id=realm_id, - is_active=True, - ) - .exclude( - role=UserProfile.ROLE_GUEST, - ) - .values_list("id", flat=True) - ) - return list(query) - - -def bot_owner_user_ids(user_profile: UserProfile) -> Set[int]: - is_private_bot = ( - user_profile.default_sending_stream - and user_profile.default_sending_stream.invite_only - or user_profile.default_events_register_stream - and user_profile.default_events_register_stream.invite_only - ) - assert user_profile.bot_owner_id is not None - if is_private_bot: - return {user_profile.bot_owner_id} - else: - users = {user.id for user in user_profile.realm.get_human_admin_users()} - users.add(user_profile.bot_owner_id) - return users - - -def get_source_profile(email: str, realm_id: int) -> Optional[UserProfile]: - try: - return get_user_by_delivery_email(email, get_realm_by_id(realm_id)) - except (Realm.DoesNotExist, UserProfile.DoesNotExist): - return None - - -@cache_with_key(lambda realm: bot_dicts_in_realm_cache_key(realm.id), timeout=3600 * 24 * 7) -def get_bot_dicts_in_realm(realm: Realm) -> List[Dict[str, Any]]: - return list(UserProfile.objects.filter(realm=realm, is_bot=True).values(*bot_dict_fields)) - - -def is_cross_realm_bot_email(email: str) -> bool: - return email.lower() in settings.CROSS_REALM_BOT_EMAILS - - -class Huddle(models.Model): - """ - Represents a group of individuals who may have a - group direct message conversation together. - - The membership of the Huddle is stored in the Subscription table just like with - Streams - for each user in the Huddle, there is a Subscription object - tied to the UserProfile and the Huddle's recipient object. - - A hash of the list of user IDs is stored in the huddle_hash field - below, to support efficiently mapping from a set of users to the - corresponding Huddle object. - """ - - # TODO: We should consider whether using - # CommaSeparatedIntegerField would be better. - huddle_hash = models.CharField(max_length=40, db_index=True, unique=True) - # Foreign key to the Recipient object for this Huddle. - recipient = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) - - -def get_huddle_hash(id_list: List[int]) -> str: - id_list = sorted(set(id_list)) - hash_key = ",".join(str(x) for x in id_list) - return hashlib.sha1(hash_key.encode()).hexdigest() - - -def get_or_create_huddle(id_list: List[int]) -> Huddle: - """ - Takes a list of user IDs and returns the Huddle object for the - group consisting of these users. If the Huddle object does not - yet exist, it will be transparently created. - """ - huddle_hash = get_huddle_hash(id_list) - with transaction.atomic(): - (huddle, created) = Huddle.objects.get_or_create(huddle_hash=huddle_hash) - if created: - recipient = Recipient.objects.create(type_id=huddle.id, type=Recipient.HUDDLE) - huddle.recipient = recipient - huddle.save(update_fields=["recipient"]) - subs_to_create = [ - Subscription( - recipient=recipient, - user_profile_id=user_profile_id, - is_user_active=is_active, - ) - for user_profile_id, is_active in UserProfile.objects.filter(id__in=id_list) - .distinct("id") - .values_list("id", "is_active") - ] - Subscription.objects.bulk_create(subs_to_create) - return huddle - - -class UserActivity(models.Model): - """Data table recording the last time each user hit Zulip endpoints - via which Clients; unlike UserPresence, these data are not exposed - to users via the Zulip API. - - Useful for debugging as well as to answer analytics questions like - "How many users have accessed the Zulip mobile app in the last - month?" or "Which users/organizations have recently used API - endpoint X that is about to be desupported" for communications - and database migration purposes. - """ - - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - client = models.ForeignKey(Client, on_delete=CASCADE) - query = models.CharField(max_length=50, db_index=True) - - count = models.IntegerField() - last_visit = models.DateTimeField("last visit") - - class Meta: - unique_together = ("user_profile", "client", "query") - - -class UserActivityInterval(models.Model): - MIN_INTERVAL_LENGTH = timedelta(minutes=15) - - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - start = models.DateTimeField("start time", db_index=True) - end = models.DateTimeField("end time", db_index=True) - - class Meta: - indexes = [ - models.Index( - fields=["user_profile", "end"], - name="zerver_useractivityinterval_user_profile_id_end_bb3bfc37_idx", - ), - ] - - -class UserPresence(models.Model): - """A record from the last time we heard from a given user on a given client. - - NOTE: Users can disable updates to this table (see UserProfile.presence_enabled), - so this cannot be used to determine if a user was recently active on Zulip. - The UserActivity table is recommended for that purpose. - - This is a tricky subsystem, because it is highly optimized. See the docs: - https://zulip.readthedocs.io/en/latest/subsystems/presence.html - """ - - user_profile = models.OneToOneField(UserProfile, on_delete=CASCADE, unique=True) - - # Realm is just here as denormalization to optimize database - # queries to fetch all presence data for a given realm. - realm = models.ForeignKey(Realm, on_delete=CASCADE) - - # The last time the user had a client connected to Zulip, - # including idle clients where the user hasn't interacted with the - # system recently (and thus might be AFK). - last_connected_time = models.DateTimeField(default=timezone_now, db_index=True, null=True) - # The last time a client connected to Zulip reported that the user - # was actually present (E.g. via focusing a browser window or - # interacting with a computer running the desktop app) - last_active_time = models.DateTimeField(default=timezone_now, db_index=True, null=True) - - # The following constants are used in the presence API for - # communicating whether a user is active (last_active_time recent) - # or idle (last_connected_time recent) or offline (neither - # recent). They're no longer part of the data model. - LEGACY_STATUS_ACTIVE = "active" - LEGACY_STATUS_IDLE = "idle" - LEGACY_STATUS_ACTIVE_INT = 1 - LEGACY_STATUS_IDLE_INT = 2 - - class Meta: - indexes = [ - models.Index( - fields=["realm", "last_active_time"], - name="zerver_userpresence_realm_id_last_active_time_1c5aa9a2_idx", - ), - models.Index( - fields=["realm", "last_connected_time"], - name="zerver_userpresence_realm_id_last_connected_time_98d2fc9f_idx", - ), - ] - - @staticmethod - def status_from_string(status: str) -> Optional[int]: - if status == "active": - return UserPresence.LEGACY_STATUS_ACTIVE_INT - elif status == "idle": - return UserPresence.LEGACY_STATUS_IDLE_INT - - return None - - -class UserStatus(AbstractEmoji): - user_profile = models.OneToOneField(UserProfile, on_delete=CASCADE) - - timestamp = models.DateTimeField() - client = models.ForeignKey(Client, on_delete=CASCADE) - - # Override emoji_name and emoji_code field of (AbstractReaction model) to accept - # default value. - emoji_name = models.TextField(default="") - emoji_code = models.TextField(default="") - - status_text = models.CharField(max_length=255, default="") - - -class DefaultStream(models.Model): - realm = models.ForeignKey(Realm, on_delete=CASCADE) - stream = models.ForeignKey(Stream, on_delete=CASCADE) - - class Meta: - unique_together = ("realm", "stream") - - -class DefaultStreamGroup(models.Model): - MAX_NAME_LENGTH = 60 - - name = models.CharField(max_length=MAX_NAME_LENGTH, db_index=True) - realm = models.ForeignKey(Realm, on_delete=CASCADE) - streams = models.ManyToManyField("zerver.Stream") - description = models.CharField(max_length=1024, default="") - - class Meta: - unique_together = ("realm", "name") - - def to_dict(self) -> Dict[str, Any]: - return dict( - name=self.name, - id=self.id, - description=self.description, - streams=[stream.to_dict() for stream in self.streams.all().order_by("name")], - ) - - -def get_default_stream_groups(realm: Realm) -> QuerySet[DefaultStreamGroup]: - return DefaultStreamGroup.objects.filter(realm=realm) - - -class AbstractScheduledJob(models.Model): - scheduled_timestamp = models.DateTimeField(db_index=True) - # JSON representation of arguments to consumer - data = models.TextField() - realm = models.ForeignKey(Realm, on_delete=CASCADE) - - class Meta: - abstract = True - - -class ScheduledEmail(AbstractScheduledJob): - # Exactly one of users or address should be set. These are - # duplicate values, used to efficiently filter the set of - # ScheduledEmails for use in clear_scheduled_emails; the - # recipients used for actually sending messages are stored in the - # data field of AbstractScheduledJob. - users = models.ManyToManyField(UserProfile) - # Just the address part of a full "name
      " email address - address = models.EmailField(null=True, db_index=True) - - # Valid types are below - WELCOME = 1 - DIGEST = 2 - INVITATION_REMINDER = 3 - type = models.PositiveSmallIntegerField() - - @override - def __str__(self) -> str: - return f"{self.type} {self.address or list(self.users.all())} {self.scheduled_timestamp}" - - -class MissedMessageEmailAddress(models.Model): - message = models.ForeignKey(Message, on_delete=CASCADE) - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - email_token = models.CharField(max_length=34, unique=True, db_index=True) - - # Timestamp of when the missed message address generated. - timestamp = models.DateTimeField(db_index=True, default=timezone_now) - # Number of times the missed message address has been used. - times_used = models.PositiveIntegerField(default=0, db_index=True) - - @override - def __str__(self) -> str: - return settings.EMAIL_GATEWAY_PATTERN % (self.email_token,) - - def increment_times_used(self) -> None: - self.times_used += 1 - self.save(update_fields=["times_used"]) - - -class NotificationTriggers: - # "direct_message" is for 1:1 direct messages as well as huddles - DIRECT_MESSAGE = "direct_message" - MENTION = "mentioned" - TOPIC_WILDCARD_MENTION = "topic_wildcard_mentioned" - STREAM_WILDCARD_MENTION = "stream_wildcard_mentioned" - STREAM_PUSH = "stream_push_notify" - STREAM_EMAIL = "stream_email_notify" - FOLLOWED_TOPIC_PUSH = "followed_topic_push_notify" - FOLLOWED_TOPIC_EMAIL = "followed_topic_email_notify" - TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC = "topic_wildcard_mentioned_in_followed_topic" - STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC = "stream_wildcard_mentioned_in_followed_topic" - - -class ScheduledMessageNotificationEmail(models.Model): - """Stores planned outgoing message notification emails. They may be - processed earlier should Zulip choose to batch multiple messages - in a single email, but typically will be processed just after - scheduled_timestamp. - """ - - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - message = models.ForeignKey(Message, on_delete=CASCADE) - - EMAIL_NOTIFICATION_TRIGGER_CHOICES = [ - (NotificationTriggers.DIRECT_MESSAGE, "Direct message"), - (NotificationTriggers.MENTION, "Mention"), - (NotificationTriggers.TOPIC_WILDCARD_MENTION, "Topic wildcard mention"), - (NotificationTriggers.STREAM_WILDCARD_MENTION, "Stream wildcard mention"), - (NotificationTriggers.STREAM_EMAIL, "Stream notifications enabled"), - (NotificationTriggers.FOLLOWED_TOPIC_EMAIL, "Followed topic notifications enabled"), - ( - NotificationTriggers.TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC, - "Topic wildcard mention in followed topic", - ), - ( - NotificationTriggers.STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC, - "Stream wildcard mention in followed topic", - ), - ] - - trigger = models.TextField(choices=EMAIL_NOTIFICATION_TRIGGER_CHOICES) - mentioned_user_group = models.ForeignKey(UserGroup, null=True, on_delete=CASCADE) - - # Timestamp for when the notification should be processed and sent. - # Calculated from the time the event was received and the batching period. - scheduled_timestamp = models.DateTimeField(db_index=True) - - -class APIScheduledStreamMessageDict(TypedDict): - scheduled_message_id: int - to: int - type: str - content: str - rendered_content: str - topic: str - scheduled_delivery_timestamp: int - failed: bool - - -class APIScheduledDirectMessageDict(TypedDict): - scheduled_message_id: int - to: List[int] - type: str - content: str - rendered_content: str - scheduled_delivery_timestamp: int - failed: bool - - -class ScheduledMessage(models.Model): - sender = models.ForeignKey(UserProfile, on_delete=CASCADE) - recipient = models.ForeignKey(Recipient, on_delete=CASCADE) - subject = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH) - content = models.TextField() - rendered_content = models.TextField() - sending_client = models.ForeignKey(Client, on_delete=CASCADE) - stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE) - realm = models.ForeignKey(Realm, on_delete=CASCADE) - scheduled_timestamp = models.DateTimeField(db_index=True) - delivered = models.BooleanField(default=False) - delivered_message = models.ForeignKey(Message, null=True, on_delete=CASCADE) - has_attachment = models.BooleanField(default=False, db_index=True) - - # Metadata for messages that failed to send when their scheduled - # moment arrived. - failed = models.BooleanField(default=False) - failure_message = models.TextField(null=True) - - SEND_LATER = 1 - REMIND = 2 - - DELIVERY_TYPES = ( - (SEND_LATER, "send_later"), - (REMIND, "remind"), - ) - - delivery_type = models.PositiveSmallIntegerField( - choices=DELIVERY_TYPES, - default=SEND_LATER, - ) - - class Meta: - indexes = [ - # We expect a large number of delivered scheduled messages - # to accumulate over time. This first index is for the - # deliver_scheduled_messages worker. - models.Index( - name="zerver_unsent_scheduled_messages_by_time", - fields=["scheduled_timestamp"], - condition=Q( - delivered=False, - failed=False, - ), - ), - # This index is for displaying scheduled messages to the - # user themself via the API; we don't filter failed - # messages since we will want to display those so that - # failures don't just disappear into a black hole. - models.Index( - name="zerver_realm_unsent_scheduled_messages_by_user", - fields=["realm_id", "sender", "delivery_type", "scheduled_timestamp"], - condition=Q( - delivered=False, - ), - ), - ] - - @override - def __str__(self) -> str: - return f"{self.recipient.label()} {self.subject} {self.sender!r} {self.scheduled_timestamp}" - - def topic_name(self) -> str: - return self.subject - - def set_topic_name(self, topic_name: str) -> None: - self.subject = topic_name - - def is_stream_message(self) -> bool: - return self.recipient.type == Recipient.STREAM - - def to_dict(self) -> Union[APIScheduledStreamMessageDict, APIScheduledDirectMessageDict]: - recipient, recipient_type_str = get_recipient_ids(self.recipient, self.sender.id) - - if recipient_type_str == "private": - # The topic for direct messages should always be an empty string. - assert self.topic_name() == "" - - return APIScheduledDirectMessageDict( - scheduled_message_id=self.id, - to=recipient, - type=recipient_type_str, - content=self.content, - rendered_content=self.rendered_content, - scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp), - failed=self.failed, - ) - - # The recipient for stream messages should always just be the unique stream ID. - assert len(recipient) == 1 - - return APIScheduledStreamMessageDict( - scheduled_message_id=self.id, - to=recipient[0], - type=recipient_type_str, - content=self.content, - rendered_content=self.rendered_content, - topic=self.topic_name(), - scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp), - failed=self.failed, - ) - - -EMAIL_TYPES = { - "account_registered": ScheduledEmail.WELCOME, - "onboarding_zulip_topics": ScheduledEmail.WELCOME, - "onboarding_zulip_guide": ScheduledEmail.WELCOME, - "onboarding_team_to_zulip": ScheduledEmail.WELCOME, - "digest": ScheduledEmail.DIGEST, - "invitation_reminder": ScheduledEmail.INVITATION_REMINDER, -} - - -class AbstractRealmAuditLog(models.Model): - """Defines fields common to RealmAuditLog and RemoteRealmAuditLog.""" - - event_time = models.DateTimeField(db_index=True) - # If True, event_time is an overestimate of the true time. Can be used - # by migrations when introducing a new event_type. - backfilled = models.BooleanField(default=False) - - # Keys within extra_data, when extra_data is a json dict. Keys are strings because - # json keys must always be strings. - OLD_VALUE = "1" - NEW_VALUE = "2" - ROLE_COUNT = "10" - ROLE_COUNT_HUMANS = "11" - ROLE_COUNT_BOTS = "12" - - extra_data = models.JSONField(default=dict, encoder=DjangoJSONEncoder) - - # Event types - USER_CREATED = 101 - USER_ACTIVATED = 102 - USER_DEACTIVATED = 103 - USER_REACTIVATED = 104 - USER_ROLE_CHANGED = 105 - USER_DELETED = 106 - USER_DELETED_PRESERVING_MESSAGES = 107 - - USER_SOFT_ACTIVATED = 120 - USER_SOFT_DEACTIVATED = 121 - USER_PASSWORD_CHANGED = 122 - USER_AVATAR_SOURCE_CHANGED = 123 - USER_FULL_NAME_CHANGED = 124 - USER_EMAIL_CHANGED = 125 - USER_TERMS_OF_SERVICE_VERSION_CHANGED = 126 - USER_API_KEY_CHANGED = 127 - USER_BOT_OWNER_CHANGED = 128 - USER_DEFAULT_SENDING_STREAM_CHANGED = 129 - USER_DEFAULT_REGISTER_STREAM_CHANGED = 130 - USER_DEFAULT_ALL_PUBLIC_STREAMS_CHANGED = 131 - USER_SETTING_CHANGED = 132 - USER_DIGEST_EMAIL_CREATED = 133 - - REALM_DEACTIVATED = 201 - REALM_REACTIVATED = 202 - REALM_SCRUBBED = 203 - REALM_PLAN_TYPE_CHANGED = 204 - REALM_LOGO_CHANGED = 205 - REALM_EXPORTED = 206 - REALM_PROPERTY_CHANGED = 207 - REALM_ICON_SOURCE_CHANGED = 208 - REALM_DISCOUNT_CHANGED = 209 - REALM_SPONSORSHIP_APPROVED = 210 - REALM_BILLING_MODALITY_CHANGED = 211 - REALM_REACTIVATION_EMAIL_SENT = 212 - REALM_SPONSORSHIP_PENDING_STATUS_CHANGED = 213 - REALM_SUBDOMAIN_CHANGED = 214 - REALM_CREATED = 215 - REALM_DEFAULT_USER_SETTINGS_CHANGED = 216 - REALM_ORG_TYPE_CHANGED = 217 - REALM_DOMAIN_ADDED = 218 - REALM_DOMAIN_CHANGED = 219 - REALM_DOMAIN_REMOVED = 220 - REALM_PLAYGROUND_ADDED = 221 - REALM_PLAYGROUND_REMOVED = 222 - REALM_LINKIFIER_ADDED = 223 - REALM_LINKIFIER_CHANGED = 224 - REALM_LINKIFIER_REMOVED = 225 - REALM_EMOJI_ADDED = 226 - REALM_EMOJI_REMOVED = 227 - REALM_LINKIFIERS_REORDERED = 228 - REALM_IMPORTED = 229 - - SUBSCRIPTION_CREATED = 301 - SUBSCRIPTION_ACTIVATED = 302 - SUBSCRIPTION_DEACTIVATED = 303 - SUBSCRIPTION_PROPERTY_CHANGED = 304 - - USER_MUTED = 350 - USER_UNMUTED = 351 - - STRIPE_CUSTOMER_CREATED = 401 - STRIPE_CARD_CHANGED = 402 - STRIPE_PLAN_CHANGED = 403 - STRIPE_PLAN_QUANTITY_RESET = 404 - - CUSTOMER_CREATED = 501 - CUSTOMER_PLAN_CREATED = 502 - CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 503 - CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 504 - - STREAM_CREATED = 601 - STREAM_DEACTIVATED = 602 - STREAM_NAME_CHANGED = 603 - STREAM_REACTIVATED = 604 - STREAM_MESSAGE_RETENTION_DAYS_CHANGED = 605 - STREAM_PROPERTY_CHANGED = 607 - STREAM_GROUP_BASED_SETTING_CHANGED = 608 - - USER_GROUP_CREATED = 701 - USER_GROUP_DELETED = 702 - USER_GROUP_DIRECT_USER_MEMBERSHIP_ADDED = 703 - USER_GROUP_DIRECT_USER_MEMBERSHIP_REMOVED = 704 - USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_ADDED = 705 - USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_REMOVED = 706 - USER_GROUP_DIRECT_SUPERGROUP_MEMBERSHIP_ADDED = 707 - USER_GROUP_DIRECT_SUPERGROUP_MEMBERSHIP_REMOVED = 708 - # 709 to 719 reserved for membership changes - USER_GROUP_NAME_CHANGED = 720 - USER_GROUP_DESCRIPTION_CHANGED = 721 - USER_GROUP_GROUP_BASED_SETTING_CHANGED = 722 - - # The following values are only for RemoteZulipServerAuditLog - # Values should be exactly 10000 greater than the corresponding - # value used for the same purpose in RealmAuditLog (e.g. - # REALM_DEACTIVATED = 201, and REMOTE_SERVER_DEACTIVATED = 10201). - REMOTE_SERVER_DEACTIVATED = 10201 - REMOTE_SERVER_PLAN_TYPE_CHANGED = 10204 - REMOTE_SERVER_DISCOUNT_CHANGED = 10209 - REMOTE_SERVER_SPONSORSHIP_APPROVED = 10210 - REMOTE_SERVER_BILLING_MODALITY_CHANGED = 10211 - REMOTE_SERVER_SPONSORSHIP_PENDING_STATUS_CHANGED = 10213 - REMOTE_SERVER_CREATED = 10215 - - # This value is for RemoteRealmAuditLog entries tracking changes to the - # RemoteRealm model resulting from modified realm information sent to us - # via send_server_data_to_push_bouncer. - REMOTE_REALM_VALUE_UPDATED = 20001 - REMOTE_PLAN_TRANSFERRED_SERVER_TO_REALM = 20002 - - event_type = models.PositiveSmallIntegerField() - - # event_types synced from on-prem installations to Zulip Cloud when - # billing for mobile push notifications is enabled. Every billing - # event_type should have ROLE_COUNT populated in extra_data. - SYNCED_BILLING_EVENTS = [ - USER_CREATED, - USER_ACTIVATED, - USER_DEACTIVATED, - USER_REACTIVATED, - USER_ROLE_CHANGED, - REALM_DEACTIVATED, - REALM_REACTIVATED, - REALM_IMPORTED, - ] - - class Meta: - abstract = True - - -class RealmAuditLog(AbstractRealmAuditLog): - """ - RealmAuditLog tracks important changes to users, streams, and - realms in Zulip. It is intended to support both - debugging/introspection (e.g. determining when a user's left a - given stream?) as well as help with some database migrations where - we might be able to do a better data backfill with it. Here are a - few key details about how this works: - - * acting_user is the user who initiated the state change - * modified_user (if present) is the user being modified - * modified_stream (if present) is the stream being modified - * modified_user_group (if present) is the user group being modified - - For example: - * When a user subscribes another user to a stream, modified_user, - acting_user, and modified_stream will all be present and different. - * When an administrator changes an organization's realm icon, - acting_user is that administrator and modified_user, - modified_stream and modified_user_group will be None. - """ - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - acting_user = models.ForeignKey( - UserProfile, - null=True, - related_name="+", - on_delete=CASCADE, - ) - modified_user = models.ForeignKey( - UserProfile, - null=True, - related_name="+", - on_delete=CASCADE, - ) - modified_stream = models.ForeignKey( - Stream, - null=True, - on_delete=CASCADE, - ) - modified_user_group = models.ForeignKey( - UserGroup, - null=True, - on_delete=CASCADE, - ) - event_last_message_id = models.IntegerField(null=True) - - @override - def __str__(self) -> str: - if self.modified_user is not None: - return f"{self.modified_user!r} {self.event_type} {self.event_time} {self.id}" - if self.modified_stream is not None: - return f"{self.modified_stream!r} {self.event_type} {self.event_time} {self.id}" - if self.modified_user_group is not None: - return f"{self.modified_user_group!r} {self.event_type} {self.event_time} {self.id}" - return f"{self.realm!r} {self.event_type} {self.event_time} {self.id}" - - class Meta: - indexes = [ - models.Index( - name="zerver_realmauditlog_user_subscriptions_idx", - fields=["modified_user", "modified_stream"], - condition=Q( - event_type__in=[ - AbstractRealmAuditLog.SUBSCRIPTION_CREATED, - AbstractRealmAuditLog.SUBSCRIPTION_ACTIVATED, - AbstractRealmAuditLog.SUBSCRIPTION_DEACTIVATED, - ] - ), - ) - ] - - -class OnboardingStep(models.Model): - user = models.ForeignKey(UserProfile, on_delete=CASCADE) - onboarding_step = models.CharField(max_length=30) - timestamp = models.DateTimeField(default=timezone_now) - - class Meta: - unique_together = ("user", "onboarding_step") - - -def check_valid_user_ids(realm_id: int, val: object, allow_deactivated: bool = False) -> List[int]: - user_ids = check_list(check_int)("User IDs", val) - realm = Realm.objects.get(id=realm_id) - for user_id in user_ids: - # TODO: Structurally, we should be doing a bulk fetch query to - # get the users here, not doing these in a loop. But because - # this is a rarely used feature and likely to never have more - # than a handful of users, it's probably mostly OK. - try: - user_profile = get_user_profile_by_id_in_realm(user_id, realm) - except UserProfile.DoesNotExist: - raise ValidationError(_("Invalid user ID: {user_id}").format(user_id=user_id)) - - if not allow_deactivated and not user_profile.is_active: - raise ValidationError( - _("User with ID {user_id} is deactivated").format(user_id=user_id) - ) - - if user_profile.is_bot: - raise ValidationError(_("User with ID {user_id} is a bot").format(user_id=user_id)) - - return user_ids - - -class CustomProfileField(models.Model): - """Defines a form field for the per-realm custom profile fields feature. - - See CustomProfileFieldValue for an individual user's values for one of - these fields. - """ - - HINT_MAX_LENGTH = 80 - NAME_MAX_LENGTH = 40 - MAX_DISPLAY_IN_PROFILE_SUMMARY_FIELDS = 2 - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - name = models.CharField(max_length=NAME_MAX_LENGTH) - hint = models.CharField(max_length=HINT_MAX_LENGTH, default="") - - # Sort order for display of custom profile fields. - order = models.IntegerField(default=0) - - # Whether the field should be displayed in smaller summary - # sections of a page displaying custom profile fields. - display_in_profile_summary = models.BooleanField(default=False) - - SHORT_TEXT = 1 - LONG_TEXT = 2 - SELECT = 3 - DATE = 4 - URL = 5 - USER = 6 - EXTERNAL_ACCOUNT = 7 - PRONOUNS = 8 - - # These are the fields whose validators require more than var_name - # and value argument. i.e. SELECT require field_data, USER require - # realm as argument. - SELECT_FIELD_TYPE_DATA: List[ExtendedFieldElement] = [ - (SELECT, gettext_lazy("List of options"), validate_select_field, str, "SELECT"), - ] - USER_FIELD_TYPE_DATA: List[UserFieldElement] = [ - (USER, gettext_lazy("Person picker"), check_valid_user_ids, orjson.loads, "USER"), - ] - - SELECT_FIELD_VALIDATORS: Dict[int, ExtendedValidator] = { - item[0]: item[2] for item in SELECT_FIELD_TYPE_DATA - } - USER_FIELD_VALIDATORS: Dict[int, RealmUserValidator] = { - item[0]: item[2] for item in USER_FIELD_TYPE_DATA - } - - FIELD_TYPE_DATA: List[FieldElement] = [ - # Type, display name, validator, converter, keyword - (SHORT_TEXT, gettext_lazy("Short text"), check_short_string, str, "SHORT_TEXT"), - (LONG_TEXT, gettext_lazy("Long text"), check_long_string, str, "LONG_TEXT"), - (DATE, gettext_lazy("Date picker"), check_date, str, "DATE"), - (URL, gettext_lazy("Link"), check_url, str, "URL"), - ( - EXTERNAL_ACCOUNT, - gettext_lazy("External account"), - check_short_string, - str, - "EXTERNAL_ACCOUNT", - ), - (PRONOUNS, gettext_lazy("Pronouns"), check_short_string, str, "PRONOUNS"), - ] - - ALL_FIELD_TYPES = [*FIELD_TYPE_DATA, *SELECT_FIELD_TYPE_DATA, *USER_FIELD_TYPE_DATA] - - FIELD_VALIDATORS: Dict[int, Validator[ProfileDataElementValue]] = { - item[0]: item[2] for item in FIELD_TYPE_DATA - } - FIELD_CONVERTERS: Dict[int, Callable[[Any], Any]] = { - item[0]: item[3] for item in ALL_FIELD_TYPES - } - FIELD_TYPE_CHOICES: List[Tuple[int, StrPromise]] = [ - (item[0], item[1]) for item in ALL_FIELD_TYPES - ] - - field_type = models.PositiveSmallIntegerField( - choices=FIELD_TYPE_CHOICES, - default=SHORT_TEXT, - ) - - # A JSON blob of any additional data needed to define the field beyond - # type/name/hint. - # - # The format depends on the type. Field types SHORT_TEXT, LONG_TEXT, - # DATE, URL, and USER leave this empty. Fields of type SELECT store the - # choices' descriptions. - # - # Note: There is no performance overhead of using TextField in PostgreSQL. - # See https://www.postgresql.org/docs/9.0/static/datatype-character.html - field_data = models.TextField(default="") - - class Meta: - unique_together = ("realm", "name") - - @override - def __str__(self) -> str: - return f"{self.realm!r} {self.name} {self.field_type} {self.order}" - - def as_dict(self) -> ProfileDataElementBase: - data_as_dict: ProfileDataElementBase = { - "id": self.id, - "name": self.name, - "type": self.field_type, - "hint": self.hint, - "field_data": self.field_data, - "order": self.order, - } - if self.display_in_profile_summary: - data_as_dict["display_in_profile_summary"] = True - - return data_as_dict - - def is_renderable(self) -> bool: - if self.field_type in [CustomProfileField.SHORT_TEXT, CustomProfileField.LONG_TEXT]: - return True - return False - - -def custom_profile_fields_for_realm(realm_id: int) -> QuerySet[CustomProfileField]: - return CustomProfileField.objects.filter(realm=realm_id).order_by("order") - - -class CustomProfileFieldValue(models.Model): - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - field = models.ForeignKey(CustomProfileField, on_delete=CASCADE) - value = models.TextField() - rendered_value = models.TextField(null=True, default=None) - - class Meta: - unique_together = ("user_profile", "field") - - @override - def __str__(self) -> str: - return f"{self.user_profile!r} {self.field!r} {self.value}" - - -# Interfaces for services -# They provide additional functionality like parsing message to obtain query URL, data to be sent to URL, -# and parsing the response. -GENERIC_INTERFACE = "GenericService" -SLACK_INTERFACE = "SlackOutgoingWebhookService" - - -# A Service corresponds to either an outgoing webhook bot or an embedded bot. -# The type of Service is determined by the bot_type field of the referenced -# UserProfile. -# -# If the Service is an outgoing webhook bot: -# - name is any human-readable identifier for the Service -# - base_url is the address of the third-party site -# - token is used for authentication with the third-party site -# -# If the Service is an embedded bot: -# - name is the canonical name for the type of bot (e.g. 'xkcd' for an instance -# of the xkcd bot); multiple embedded bots can have the same name, but all -# embedded bots with the same name will run the same code -# - base_url and token are currently unused -class Service(models.Model): - name = models.CharField(max_length=UserProfile.MAX_NAME_LENGTH) - # Bot user corresponding to the Service. The bot_type of this user - # determines the type of service. If non-bot services are added later, - # user_profile can also represent the owner of the Service. - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - base_url = models.TextField() - token = models.TextField() - # Interface / API version of the service. - interface = models.PositiveSmallIntegerField(default=1) - - # Valid interfaces are {generic, zulip_bot_service, slack} - GENERIC = 1 - SLACK = 2 - - ALLOWED_INTERFACE_TYPES = [ - GENERIC, - SLACK, - ] - # N.B. If we used Django's choice=... we would get this for free (kinda) - _interfaces: Dict[int, str] = { - GENERIC: GENERIC_INTERFACE, - SLACK: SLACK_INTERFACE, - } - - def interface_name(self) -> str: - # Raises KeyError if invalid - return self._interfaces[self.interface] - - -def get_bot_services(user_profile_id: int) -> List[Service]: - return list(Service.objects.filter(user_profile_id=user_profile_id)) - - -def get_service_profile(user_profile_id: int, service_name: str) -> Service: - return Service.objects.get(user_profile_id=user_profile_id, name=service_name) - - -class BotStorageData(models.Model): - bot_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - key = models.TextField(db_index=True) - value = models.TextField() - - class Meta: - unique_together = ("bot_profile", "key") - - -class BotConfigData(models.Model): - bot_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - key = models.TextField(db_index=True) - value = models.TextField() - - class Meta: - unique_together = ("bot_profile", "key") - - -class InvalidFakeEmailDomainError(Exception): - pass - - -def get_fake_email_domain(realm_host: str) -> str: - try: - # Check that realm.host can be used to form valid email addresses. - validate_email(Address(username="bot", domain=realm_host).addr_spec) - return realm_host - except ValidationError: - pass - - try: - # Check that the fake email domain can be used to form valid email addresses. - validate_email(Address(username="bot", domain=settings.FAKE_EMAIL_DOMAIN).addr_spec) - except ValidationError: - raise InvalidFakeEmailDomainError( - settings.FAKE_EMAIL_DOMAIN + " is not a valid domain. " - "Consider setting the FAKE_EMAIL_DOMAIN setting." - ) - - return settings.FAKE_EMAIL_DOMAIN - - -class AlertWord(models.Model): - # Realm isn't necessary, but it's a nice denormalization. Users - # never move to another realm, so it's static, and having Realm - # here optimizes the main query on this table, which is fetching - # all the alert words in a realm. - realm = models.ForeignKey(Realm, db_index=True, on_delete=CASCADE) - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - # Case-insensitive name for the alert word. - word = models.TextField() - - class Meta: - unique_together = ("user_profile", "word") - - -def flush_realm_alert_words(realm_id: int) -> None: - cache_delete(realm_alert_words_cache_key(realm_id)) - cache_delete(realm_alert_words_automaton_cache_key(realm_id)) - - -def flush_alert_word(*, instance: AlertWord, **kwargs: object) -> None: - realm_id = instance.realm_id - flush_realm_alert_words(realm_id) - - -post_save.connect(flush_alert_word, sender=AlertWord) -post_delete.connect(flush_alert_word, sender=AlertWord) diff --git a/zerver/models/__init__.py b/zerver/models/__init__.py new file mode 100644 index 0000000000000..ce60472b55dfa --- /dev/null +++ b/zerver/models/__init__.py @@ -0,0 +1,67 @@ +from zerver.models import lookups as lookups +from zerver.models.alert_words import AlertWord as AlertWord +from zerver.models.bots import BotConfigData as BotConfigData +from zerver.models.bots import BotStorageData as BotStorageData +from zerver.models.bots import Service as Service +from zerver.models.clients import Client as Client +from zerver.models.custom_profile_fields import CustomProfileField as CustomProfileField +from zerver.models.custom_profile_fields import CustomProfileFieldValue as CustomProfileFieldValue +from zerver.models.drafts import Draft as Draft +from zerver.models.groups import GroupGroupMembership as GroupGroupMembership +from zerver.models.groups import UserGroup as UserGroup +from zerver.models.groups import UserGroupMembership as UserGroupMembership +from zerver.models.linkifiers import RealmFilter as RealmFilter +from zerver.models.messages import AbstractAttachment as AbstractAttachment +from zerver.models.messages import AbstractEmoji as AbstractEmoji +from zerver.models.messages import AbstractMessage as AbstractMessage +from zerver.models.messages import AbstractReaction as AbstractReaction +from zerver.models.messages import AbstractSubMessage as AbstractSubMessage +from zerver.models.messages import AbstractUserMessage as AbstractUserMessage +from zerver.models.messages import ArchivedAttachment as ArchivedAttachment +from zerver.models.messages import ArchivedMessage as ArchivedMessage +from zerver.models.messages import ArchivedReaction as ArchivedReaction +from zerver.models.messages import ArchivedSubMessage as ArchivedSubMessage +from zerver.models.messages import ArchivedUserMessage as ArchivedUserMessage +from zerver.models.messages import ArchiveTransaction as ArchiveTransaction +from zerver.models.messages import Attachment as Attachment +from zerver.models.messages import Message as Message +from zerver.models.messages import Reaction as Reaction +from zerver.models.messages import SubMessage as SubMessage +from zerver.models.messages import UserMessage as UserMessage +from zerver.models.muted_users import MutedUser as MutedUser +from zerver.models.onboarding_steps import OnboardingStep as OnboardingStep +from zerver.models.prereg_users import EmailChangeStatus as EmailChangeStatus +from zerver.models.prereg_users import MultiuseInvite as MultiuseInvite +from zerver.models.prereg_users import PreregistrationRealm as PreregistrationRealm +from zerver.models.prereg_users import PreregistrationUser as PreregistrationUser +from zerver.models.prereg_users import RealmReactivationStatus as RealmReactivationStatus +from zerver.models.presence import UserPresence as UserPresence +from zerver.models.presence import UserStatus as UserStatus +from zerver.models.push_notifications import AbstractPushDeviceToken as AbstractPushDeviceToken +from zerver.models.push_notifications import PushDeviceToken as PushDeviceToken +from zerver.models.realm_audit_logs import AbstractRealmAuditLog as AbstractRealmAuditLog +from zerver.models.realm_audit_logs import RealmAuditLog as RealmAuditLog +from zerver.models.realm_emoji import RealmEmoji as RealmEmoji +from zerver.models.realm_playgrounds import RealmPlayground as RealmPlayground +from zerver.models.realms import Realm as Realm +from zerver.models.realms import RealmAuthenticationMethod as RealmAuthenticationMethod +from zerver.models.realms import RealmDomain as RealmDomain +from zerver.models.recipients import Huddle as Huddle +from zerver.models.recipients import Recipient as Recipient +from zerver.models.scheduled_jobs import AbstractScheduledJob as AbstractScheduledJob +from zerver.models.scheduled_jobs import MissedMessageEmailAddress as MissedMessageEmailAddress +from zerver.models.scheduled_jobs import ScheduledEmail as ScheduledEmail +from zerver.models.scheduled_jobs import ScheduledMessage as ScheduledMessage +from zerver.models.scheduled_jobs import ( + ScheduledMessageNotificationEmail as ScheduledMessageNotificationEmail, +) +from zerver.models.streams import DefaultStream as DefaultStream +from zerver.models.streams import DefaultStreamGroup as DefaultStreamGroup +from zerver.models.streams import Stream as Stream +from zerver.models.streams import Subscription as Subscription +from zerver.models.user_activity import UserActivity as UserActivity +from zerver.models.user_activity import UserActivityInterval as UserActivityInterval +from zerver.models.user_topics import UserTopic as UserTopic +from zerver.models.users import RealmUserDefault as RealmUserDefault +from zerver.models.users import UserBaseSettings as UserBaseSettings +from zerver.models.users import UserProfile as UserProfile diff --git a/zerver/models/alert_words.py b/zerver/models/alert_words.py new file mode 100644 index 0000000000000..ec04c3400bcc6 --- /dev/null +++ b/zerver/models/alert_words.py @@ -0,0 +1,39 @@ +from django.db import models +from django.db.models import CASCADE +from django.db.models.signals import post_delete, post_save + +from zerver.lib.cache import ( + cache_delete, + realm_alert_words_automaton_cache_key, + realm_alert_words_cache_key, +) +from zerver.models.realms import Realm +from zerver.models.users import UserProfile + + +class AlertWord(models.Model): + # Realm isn't necessary, but it's a nice denormalization. Users + # never move to another realm, so it's static, and having Realm + # here optimizes the main query on this table, which is fetching + # all the alert words in a realm. + realm = models.ForeignKey(Realm, db_index=True, on_delete=CASCADE) + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + # Case-insensitive name for the alert word. + word = models.TextField() + + class Meta: + unique_together = ("user_profile", "word") + + +def flush_realm_alert_words(realm_id: int) -> None: + cache_delete(realm_alert_words_cache_key(realm_id)) + cache_delete(realm_alert_words_automaton_cache_key(realm_id)) + + +def flush_alert_word(*, instance: AlertWord, **kwargs: object) -> None: + realm_id = instance.realm_id + flush_realm_alert_words(realm_id) + + +post_save.connect(flush_alert_word, sender=AlertWord) +post_delete.connect(flush_alert_word, sender=AlertWord) diff --git a/zerver/models/bots.py b/zerver/models/bots.py new file mode 100644 index 0000000000000..803d1d6c916e3 --- /dev/null +++ b/zerver/models/bots.py @@ -0,0 +1,82 @@ +from typing import Dict, List + +from django.db import models +from django.db.models import CASCADE + +from zerver.models.users import UserProfile + +# Interfaces for services +# They provide additional functionality like parsing message to obtain query URL, data to be sent to URL, +# and parsing the response. +GENERIC_INTERFACE = "GenericService" +SLACK_INTERFACE = "SlackOutgoingWebhookService" + + +# A Service corresponds to either an outgoing webhook bot or an embedded bot. +# The type of Service is determined by the bot_type field of the referenced +# UserProfile. +# +# If the Service is an outgoing webhook bot: +# - name is any human-readable identifier for the Service +# - base_url is the address of the third-party site +# - token is used for authentication with the third-party site +# +# If the Service is an embedded bot: +# - name is the canonical name for the type of bot (e.g. 'xkcd' for an instance +# of the xkcd bot); multiple embedded bots can have the same name, but all +# embedded bots with the same name will run the same code +# - base_url and token are currently unused +class Service(models.Model): + name = models.CharField(max_length=UserProfile.MAX_NAME_LENGTH) + # Bot user corresponding to the Service. The bot_type of this user + # determines the type of service. If non-bot services are added later, + # user_profile can also represent the owner of the Service. + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + base_url = models.TextField() + token = models.TextField() + # Interface / API version of the service. + interface = models.PositiveSmallIntegerField(default=1) + + # Valid interfaces are {generic, zulip_bot_service, slack} + GENERIC = 1 + SLACK = 2 + + ALLOWED_INTERFACE_TYPES = [ + GENERIC, + SLACK, + ] + # N.B. If we used Django's choice=... we would get this for free (kinda) + _interfaces: Dict[int, str] = { + GENERIC: GENERIC_INTERFACE, + SLACK: SLACK_INTERFACE, + } + + def interface_name(self) -> str: + # Raises KeyError if invalid + return self._interfaces[self.interface] + + +def get_bot_services(user_profile_id: int) -> List[Service]: + return list(Service.objects.filter(user_profile_id=user_profile_id)) + + +def get_service_profile(user_profile_id: int, service_name: str) -> Service: + return Service.objects.get(user_profile_id=user_profile_id, name=service_name) + + +class BotStorageData(models.Model): + bot_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + key = models.TextField(db_index=True) + value = models.TextField() + + class Meta: + unique_together = ("bot_profile", "key") + + +class BotConfigData(models.Model): + bot_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + key = models.TextField(db_index=True) + value = models.TextField() + + class Meta: + unique_together = ("bot_profile", "key") diff --git a/zerver/models/clients.py b/zerver/models/clients.py new file mode 100644 index 0000000000000..4f788ea6d37aa --- /dev/null +++ b/zerver/models/clients.py @@ -0,0 +1,77 @@ +import hashlib +from typing import Dict + +from django.conf import settings +from django.db import models +from typing_extensions import override + +from zerver.lib import cache +from zerver.lib.cache import cache_with_key + + +class Client(models.Model): + MAX_NAME_LENGTH = 30 + name = models.CharField(max_length=MAX_NAME_LENGTH, db_index=True, unique=True) + + @override + def __str__(self) -> str: + return self.name + + def default_read_by_sender(self) -> bool: + """Used to determine whether a message was sent by a full Zulip UI + style client (and thus whether the message should be treated + as sent by a human and automatically marked as read for the + sender). The purpose of this distinction is to ensure that + message sent to the user by e.g. a Google Calendar integration + using the user's own API key don't get marked as read + automatically. + """ + sending_client = self.name.lower() + + return ( + sending_client + in ( + "zulipandroid", + "zulipios", + "zulipdesktop", + "zulipmobile", + "zulipelectron", + "zulipterminal", + "snipe", + "website", + "ios", + "android", + ) + or "desktop app" in sending_client + # Since the vast majority of messages are sent by humans + # in Zulip, treat test suite messages as such. + or (sending_client == "test suite" and settings.TEST_SUITE) + ) + + +get_client_cache: Dict[str, Client] = {} + + +def clear_client_cache() -> None: # nocoverage + global get_client_cache + get_client_cache = {} + + +def get_client(name: str) -> Client: + # Accessing KEY_PREFIX through the module is necessary + # because we need the updated value of the variable. + cache_name = cache.KEY_PREFIX + name[0 : Client.MAX_NAME_LENGTH] + if cache_name not in get_client_cache: + result = get_client_remote_cache(name) + get_client_cache[cache_name] = result + return get_client_cache[cache_name] + + +def get_client_cache_key(name: str) -> str: + return f"get_client:{hashlib.sha1(name.encode()).hexdigest()}" + + +@cache_with_key(get_client_cache_key, timeout=3600 * 24 * 7) +def get_client_remote_cache(name: str) -> Client: + (client, _) = Client.objects.get_or_create(name=name[0 : Client.MAX_NAME_LENGTH]) + return client diff --git a/zerver/models/constants.py b/zerver/models/constants.py new file mode 100644 index 0000000000000..6445f5ed1e1c7 --- /dev/null +++ b/zerver/models/constants.py @@ -0,0 +1,2 @@ +MAX_TOPIC_NAME_LENGTH = 60 +MAX_LANGUAGE_ID_LENGTH: int = 50 diff --git a/zerver/models/custom_profile_fields.py b/zerver/models/custom_profile_fields.py new file mode 100644 index 0000000000000..8c5e27a6ad697 --- /dev/null +++ b/zerver/models/custom_profile_fields.py @@ -0,0 +1,193 @@ +from typing import Any, Callable, Dict, List, Tuple + +import orjson +from django.core.exceptions import ValidationError +from django.db import models +from django.db.models import CASCADE, QuerySet +from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy +from django_stubs_ext import StrPromise +from typing_extensions import override + +from zerver.lib.types import ( + ExtendedFieldElement, + ExtendedValidator, + FieldElement, + ProfileDataElementBase, + ProfileDataElementValue, + RealmUserValidator, + UserFieldElement, + Validator, +) +from zerver.lib.validator import ( + check_date, + check_int, + check_list, + check_long_string, + check_short_string, + check_url, + validate_select_field, +) +from zerver.models.realms import Realm +from zerver.models.users import UserProfile, get_user_profile_by_id_in_realm + + +def check_valid_user_ids(realm_id: int, val: object, allow_deactivated: bool = False) -> List[int]: + user_ids = check_list(check_int)("User IDs", val) + realm = Realm.objects.get(id=realm_id) + for user_id in user_ids: + # TODO: Structurally, we should be doing a bulk fetch query to + # get the users here, not doing these in a loop. But because + # this is a rarely used feature and likely to never have more + # than a handful of users, it's probably mostly OK. + try: + user_profile = get_user_profile_by_id_in_realm(user_id, realm) + except UserProfile.DoesNotExist: + raise ValidationError(_("Invalid user ID: {user_id}").format(user_id=user_id)) + + if not allow_deactivated and not user_profile.is_active: + raise ValidationError( + _("User with ID {user_id} is deactivated").format(user_id=user_id) + ) + + if user_profile.is_bot: + raise ValidationError(_("User with ID {user_id} is a bot").format(user_id=user_id)) + + return user_ids + + +class CustomProfileField(models.Model): + """Defines a form field for the per-realm custom profile fields feature. + + See CustomProfileFieldValue for an individual user's values for one of + these fields. + """ + + HINT_MAX_LENGTH = 80 + NAME_MAX_LENGTH = 40 + MAX_DISPLAY_IN_PROFILE_SUMMARY_FIELDS = 2 + + realm = models.ForeignKey(Realm, on_delete=CASCADE) + name = models.CharField(max_length=NAME_MAX_LENGTH) + hint = models.CharField(max_length=HINT_MAX_LENGTH, default="") + + # Sort order for display of custom profile fields. + order = models.IntegerField(default=0) + + # Whether the field should be displayed in smaller summary + # sections of a page displaying custom profile fields. + display_in_profile_summary = models.BooleanField(default=False) + + SHORT_TEXT = 1 + LONG_TEXT = 2 + SELECT = 3 + DATE = 4 + URL = 5 + USER = 6 + EXTERNAL_ACCOUNT = 7 + PRONOUNS = 8 + + # These are the fields whose validators require more than var_name + # and value argument. i.e. SELECT require field_data, USER require + # realm as argument. + SELECT_FIELD_TYPE_DATA: List[ExtendedFieldElement] = [ + (SELECT, gettext_lazy("List of options"), validate_select_field, str, "SELECT"), + ] + USER_FIELD_TYPE_DATA: List[UserFieldElement] = [ + (USER, gettext_lazy("Person picker"), check_valid_user_ids, orjson.loads, "USER"), + ] + + SELECT_FIELD_VALIDATORS: Dict[int, ExtendedValidator] = { + item[0]: item[2] for item in SELECT_FIELD_TYPE_DATA + } + USER_FIELD_VALIDATORS: Dict[int, RealmUserValidator] = { + item[0]: item[2] for item in USER_FIELD_TYPE_DATA + } + + FIELD_TYPE_DATA: List[FieldElement] = [ + # Type, display name, validator, converter, keyword + (SHORT_TEXT, gettext_lazy("Short text"), check_short_string, str, "SHORT_TEXT"), + (LONG_TEXT, gettext_lazy("Long text"), check_long_string, str, "LONG_TEXT"), + (DATE, gettext_lazy("Date picker"), check_date, str, "DATE"), + (URL, gettext_lazy("Link"), check_url, str, "URL"), + ( + EXTERNAL_ACCOUNT, + gettext_lazy("External account"), + check_short_string, + str, + "EXTERNAL_ACCOUNT", + ), + (PRONOUNS, gettext_lazy("Pronouns"), check_short_string, str, "PRONOUNS"), + ] + + ALL_FIELD_TYPES = [*FIELD_TYPE_DATA, *SELECT_FIELD_TYPE_DATA, *USER_FIELD_TYPE_DATA] + + FIELD_VALIDATORS: Dict[int, Validator[ProfileDataElementValue]] = { + item[0]: item[2] for item in FIELD_TYPE_DATA + } + FIELD_CONVERTERS: Dict[int, Callable[[Any], Any]] = { + item[0]: item[3] for item in ALL_FIELD_TYPES + } + FIELD_TYPE_CHOICES: List[Tuple[int, StrPromise]] = [ + (item[0], item[1]) for item in ALL_FIELD_TYPES + ] + + field_type = models.PositiveSmallIntegerField( + choices=FIELD_TYPE_CHOICES, + default=SHORT_TEXT, + ) + + # A JSON blob of any additional data needed to define the field beyond + # type/name/hint. + # + # The format depends on the type. Field types SHORT_TEXT, LONG_TEXT, + # DATE, URL, and USER leave this empty. Fields of type SELECT store the + # choices' descriptions. + # + # Note: There is no performance overhead of using TextField in PostgreSQL. + # See https://www.postgresql.org/docs/9.0/static/datatype-character.html + field_data = models.TextField(default="") + + class Meta: + unique_together = ("realm", "name") + + @override + def __str__(self) -> str: + return f"{self.realm!r} {self.name} {self.field_type} {self.order}" + + def as_dict(self) -> ProfileDataElementBase: + data_as_dict: ProfileDataElementBase = { + "id": self.id, + "name": self.name, + "type": self.field_type, + "hint": self.hint, + "field_data": self.field_data, + "order": self.order, + } + if self.display_in_profile_summary: + data_as_dict["display_in_profile_summary"] = True + + return data_as_dict + + def is_renderable(self) -> bool: + if self.field_type in [CustomProfileField.SHORT_TEXT, CustomProfileField.LONG_TEXT]: + return True + return False + + +def custom_profile_fields_for_realm(realm_id: int) -> QuerySet[CustomProfileField]: + return CustomProfileField.objects.filter(realm=realm_id).order_by("order") + + +class CustomProfileFieldValue(models.Model): + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + field = models.ForeignKey(CustomProfileField, on_delete=CASCADE) + value = models.TextField() + rendered_value = models.TextField(null=True, default=None) + + class Meta: + unique_together = ("user_profile", "field") + + @override + def __str__(self) -> str: + return f"{self.user_profile!r} {self.field!r} {self.value}" diff --git a/zerver/models/drafts.py b/zerver/models/drafts.py new file mode 100644 index 0000000000000..614adf7895d2a --- /dev/null +++ b/zerver/models/drafts.py @@ -0,0 +1,36 @@ +from typing import Any, Dict + +from django.db import models +from typing_extensions import override + +from zerver.lib.display_recipient import get_recipient_ids +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.recipients import Recipient +from zerver.models.users import UserProfile + + +class Draft(models.Model): + """Server-side storage model for storing drafts so that drafts can be synced across + multiple clients/devices. + """ + + user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE) + recipient = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) + topic = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH, db_index=True) + content = models.TextField() # Length should not exceed MAX_MESSAGE_LENGTH + last_edit_time = models.DateTimeField(db_index=True) + + @override + def __str__(self) -> str: + return f"{self.user_profile.email} / {self.id} / {self.last_edit_time}" + + def to_dict(self) -> Dict[str, Any]: + to, recipient_type_str = get_recipient_ids(self.recipient, self.user_profile_id) + return { + "id": self.id, + "type": recipient_type_str, + "to": to, + "topic": self.topic, + "content": self.content, + "timestamp": int(self.last_edit_time.timestamp()), + } diff --git a/zerver/models/groups.py b/zerver/models/groups.py new file mode 100644 index 0000000000000..eeaf0759dbb25 --- /dev/null +++ b/zerver/models/groups.py @@ -0,0 +1,102 @@ +from django.db import models +from django.db.models import CASCADE +from django_cte import CTEManager + +from zerver.lib.types import GroupPermissionSetting +from zerver.models.users import UserProfile + + +class SystemGroups: + FULL_MEMBERS = "role:fullmembers" + EVERYONE_ON_INTERNET = "role:internet" + OWNERS = "role:owners" + ADMINISTRATORS = "role:administrators" + MODERATORS = "role:moderators" + MEMBERS = "role:members" + EVERYONE = "role:everyone" + NOBODY = "role:nobody" + + +class UserGroup(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 + MAX_NAME_LENGTH = 100 + INVALID_NAME_PREFIXES = ["@", "role:", "user:", "stream:", "channel:"] + + objects: CTEManager = CTEManager() + name = models.CharField(max_length=MAX_NAME_LENGTH) + direct_members = models.ManyToManyField( + UserProfile, through="zerver.UserGroupMembership", related_name="direct_groups" + ) + direct_subgroups = models.ManyToManyField( + "self", + symmetrical=False, + through="zerver.GroupGroupMembership", + through_fields=("supergroup", "subgroup"), + related_name="direct_supergroups", + ) + realm = models.ForeignKey("zerver.Realm", on_delete=CASCADE) + description = models.TextField(default="") + is_system_group = models.BooleanField(default=False) + + can_mention_group = models.ForeignKey("self", on_delete=models.RESTRICT) + + # We do not have "Full members" and "Everyone on the internet" + # group here since there isn't a separate role value for full + # members and spectators. + SYSTEM_USER_GROUP_ROLE_MAP = { + UserProfile.ROLE_REALM_OWNER: { + "name": SystemGroups.OWNERS, + "description": "Owners of this organization", + }, + UserProfile.ROLE_REALM_ADMINISTRATOR: { + "name": SystemGroups.ADMINISTRATORS, + "description": "Administrators of this organization, including owners", + }, + UserProfile.ROLE_MODERATOR: { + "name": SystemGroups.MODERATORS, + "description": "Moderators of this organization, including administrators", + }, + UserProfile.ROLE_MEMBER: { + "name": SystemGroups.MEMBERS, + "description": "Members of this organization, not including guests", + }, + UserProfile.ROLE_GUEST: { + "name": SystemGroups.EVERYONE, + "description": "Everyone in this organization, including guests", + }, + } + + GROUP_PERMISSION_SETTINGS = { + "can_mention_group": GroupPermissionSetting( + require_system_group=False, + allow_internet_group=False, + allow_owners_group=False, + allow_nobody_group=True, + allow_everyone_group=True, + default_group_name=SystemGroups.EVERYONE, + default_for_system_groups=SystemGroups.NOBODY, + id_field_name="can_mention_group_id", + ), + } + + class Meta: + unique_together = (("realm", "name"),) + + +class UserGroupMembership(models.Model): + user_group = models.ForeignKey(UserGroup, on_delete=CASCADE, related_name="+") + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE, related_name="+") + + class Meta: + unique_together = (("user_group", "user_profile"),) + + +class GroupGroupMembership(models.Model): + supergroup = models.ForeignKey(UserGroup, on_delete=CASCADE, related_name="+") + subgroup = models.ForeignKey(UserGroup, on_delete=CASCADE, related_name="+") + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["supergroup", "subgroup"], name="zerver_groupgroupmembership_uniq" + ) + ] diff --git a/zerver/models/linkifiers.py b/zerver/models/linkifiers.py new file mode 100644 index 0000000000000..fca07e4af6134 --- /dev/null +++ b/zerver/models/linkifiers.py @@ -0,0 +1,135 @@ +from typing import List, Pattern + +import re2 +import uri_template +from django.core.exceptions import ValidationError +from django.db import models +from django.db.models import CASCADE +from django.db.models.signals import post_delete, post_save +from django.utils.translation import gettext as _ +from typing_extensions import override + +from zerver.lib import cache +from zerver.lib.cache import cache_delete, cache_with_key +from zerver.lib.per_request_cache import ( + flush_per_request_cache, + return_same_value_during_entire_request, +) +from zerver.lib.types import LinkifierDict +from zerver.models.realms import Realm + + +def filter_pattern_validator(value: str) -> Pattern[str]: + try: + # Do not write errors to stderr (this still raises exceptions) + options = re2.Options() + options.log_errors = False + + regex = re2.compile(value, options=options) + except re2.error as e: + if len(e.args) >= 1: + if isinstance(e.args[0], str): # nocoverage + raise ValidationError(_("Bad regular expression: {regex}").format(regex=e.args[0])) + if isinstance(e.args[0], bytes): + raise ValidationError( + _("Bad regular expression: {regex}").format(regex=e.args[0].decode()) + ) + raise ValidationError(_("Unknown regular expression error")) # nocoverage + + return regex + + +def url_template_validator(value: str) -> None: + """Validate as a URL template""" + if not uri_template.validate(value): + raise ValidationError(_("Invalid URL template.")) + + +class RealmFilter(models.Model): + """Realm-specific regular expressions to automatically linkify certain + strings inside the Markdown processor. See "Custom filters" in the settings UI. + """ + + realm = models.ForeignKey(Realm, on_delete=CASCADE) + pattern = models.TextField() + url_template = models.TextField(validators=[url_template_validator]) + # Linkifiers are applied in a message/topic in order; the processing order + # is important when there are overlapping patterns. + order = models.IntegerField(default=0) + + class Meta: + unique_together = ("realm", "pattern") + + @override + def __str__(self) -> str: + return f"{self.realm.string_id}: {self.pattern} {self.url_template}" + + @override + def clean(self) -> None: + """Validate whether the set of parameters in the URL template + match the set of parameters in the regular expression. + + Django's `full_clean` calls `clean_fields` followed by `clean` method + and stores all ValidationErrors from all stages to return as JSON. + """ + + # Extract variables present in the pattern + pattern = filter_pattern_validator(self.pattern) + group_set = set(pattern.groupindex.keys()) + + # Do not continue the check if the url template is invalid to begin with. + # The ValidationError for invalid template will only be raised by the validator + # set on the url_template field instead of here to avoid duplicates. + if not uri_template.validate(self.url_template): + return + + # Extract variables used in the URL template. + template_variables_set = set(uri_template.URITemplate(self.url_template).variable_names) + + # Report patterns missing in linkifier pattern. + missing_in_pattern_set = template_variables_set - group_set + if len(missing_in_pattern_set) > 0: + name = min(missing_in_pattern_set) + raise ValidationError( + _("Group %(name)r in URL template is not present in linkifier pattern."), + params={"name": name}, + ) + + missing_in_url_set = group_set - template_variables_set + # Report patterns missing in URL template. + if len(missing_in_url_set) > 0: + # We just report the first missing pattern here. Users can + # incrementally resolve errors if there are multiple + # missing patterns. + name = min(missing_in_url_set) + raise ValidationError( + _("Group %(name)r in linkifier pattern is not present in URL template."), + params={"name": name}, + ) + + +def get_linkifiers_cache_key(realm_id: int) -> str: + return f"{cache.KEY_PREFIX}:all_linkifiers_for_realm:{realm_id}" + + +@return_same_value_during_entire_request +@cache_with_key(get_linkifiers_cache_key, timeout=3600 * 24 * 7) +def linkifiers_for_realm(realm_id: int) -> List[LinkifierDict]: + return [ + LinkifierDict( + pattern=linkifier.pattern, + url_template=linkifier.url_template, + id=linkifier.id, + ) + for linkifier in RealmFilter.objects.filter(realm_id=realm_id).order_by("order") + ] + + +def flush_linkifiers(*, instance: RealmFilter, **kwargs: object) -> None: + realm_id = instance.realm_id + cache_delete(get_linkifiers_cache_key(realm_id)) + flush_per_request_cache("linkifiers_for_realm") + + +post_save.connect(flush_linkifiers, sender=RealmFilter) +post_delete.connect(flush_linkifiers, sender=RealmFilter) diff --git a/zerver/models/lookups.py b/zerver/models/lookups.py new file mode 100644 index 0000000000000..484cc889ae396 --- /dev/null +++ b/zerver/models/lookups.py @@ -0,0 +1,32 @@ +from typing import List, Tuple, Union + +from django.db import models +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.models.sql.compiler import SQLCompiler +from typing_extensions import override + + +@models.Field.register_lookup +class AndZero(models.Lookup[int]): + lookup_name = "andz" + + @override + def as_sql( + self, compiler: SQLCompiler, connection: BaseDatabaseWrapper + ) -> Tuple[str, List[Union[str, int]]]: # nocoverage # currently only used in migrations + lhs, lhs_params = self.process_lhs(compiler, connection) + rhs, rhs_params = self.process_rhs(compiler, connection) + return f"{lhs} & {rhs} = 0", lhs_params + rhs_params + + +@models.Field.register_lookup +class AndNonZero(models.Lookup[int]): + lookup_name = "andnz" + + @override + def as_sql( + self, compiler: SQLCompiler, connection: BaseDatabaseWrapper + ) -> Tuple[str, List[Union[str, int]]]: # nocoverage # currently only used in migrations + lhs, lhs_params = self.process_lhs(compiler, connection) + rhs, rhs_params = self.process_rhs(compiler, connection) + return f"{lhs} & {rhs} != 0", lhs_params + rhs_params diff --git a/zerver/models/messages.py b/zerver/models/messages.py new file mode 100644 index 0000000000000..c26344c92bc11 --- /dev/null +++ b/zerver/models/messages.py @@ -0,0 +1,739 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + +import time +from datetime import timedelta +from typing import Any, Dict, List, Optional + +from bitfield import BitField +from bitfield.types import Bit, BitHandler +from django.contrib.postgres.indexes import GinIndex +from django.contrib.postgres.search import SearchVectorField +from django.db import models +from django.db.models import CASCADE, F, Q, QuerySet +from django.db.models.functions import Upper +from django.db.models.signals import post_delete, post_save +from django.utils.timezone import now as timezone_now +from django.utils.translation import gettext_lazy +from typing_extensions import override + +from zerver.lib.cache import flush_message, flush_submessage, flush_used_upload_space_cache +from zerver.models.clients import Client +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.realms import Realm +from zerver.models.recipients import Recipient +from zerver.models.users import UserProfile + + +class AbstractMessage(models.Model): + sender = models.ForeignKey(UserProfile, on_delete=CASCADE) + + # The target of the message is signified by the Recipient object. + # See the Recipient class for details. + recipient = models.ForeignKey(Recipient, on_delete=CASCADE) + + # The realm containing the message. Usually this will be the same + # as the realm of the messages's sender; the exception to that is + # cross-realm bot users. + # + # Important for efficient indexes and sharding in multi-realm servers. + realm = models.ForeignKey(Realm, on_delete=CASCADE) + + # The message's topic. + # + # Early versions of Zulip called this concept a "subject", as in an email + # "subject line", before changing to "topic" in 2013 (commit dac5a46fa). + # UI and user documentation now consistently say "topic". New APIs and + # new code should generally also say "topic". + # + # See also the `topic_name` method on `Message`. + subject = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH, db_index=True) + + # The raw Markdown-format text (E.g., what the user typed into the compose box). + content = models.TextField() + + # The HTML rendered content resulting from rendering the content + # with the Markdown processor. + rendered_content = models.TextField(null=True) + # A rarely-incremented version number, theoretically useful for + # tracking which messages have been already rerendered when making + # major changes to the markup rendering process. + rendered_content_version = models.IntegerField(null=True) + + date_sent = models.DateTimeField("date sent", db_index=True) + + # A Client object indicating what type of Zulip client sent this message. + sending_client = models.ForeignKey(Client, on_delete=CASCADE) + + # The last time the message was modified by message editing or moving. + last_edit_time = models.DateTimeField(null=True) + + # A JSON-encoded list of objects describing any past edits to this + # message, oldest first. + edit_history = models.TextField(null=True) + + # Whether the message contains a (link to) an uploaded file. + has_attachment = models.BooleanField(default=False, db_index=True) + # Whether the message contains a visible image element. + has_image = models.BooleanField(default=False, db_index=True) + # Whether the message contains a link. + has_link = models.BooleanField(default=False, db_index=True) + + class Meta: + abstract = True + + @override + def __str__(self) -> str: + return f"{self.recipient.label()} / {self.subject} / {self.sender!r}" + + +class ArchiveTransaction(models.Model): + timestamp = models.DateTimeField(default=timezone_now, db_index=True) + # Marks if the data archived in this transaction has been restored: + restored = models.BooleanField(default=False, db_index=True) + + type = models.PositiveSmallIntegerField(db_index=True) + # Valid types: + RETENTION_POLICY_BASED = 1 # Archiving was executed due to automated retention policies + MANUAL = 2 # Archiving was run manually, via move_messages_to_archive function + + # ForeignKey to the realm with which objects archived in this transaction are associated. + # If type is set to MANUAL, this should be null. + realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE) + + @override + def __str__(self) -> str: + return "id: {id}, type: {type}, realm: {realm}, timestamp: {timestamp}".format( + id=self.id, + type="MANUAL" if self.type == self.MANUAL else "RETENTION_POLICY_BASED", + realm=self.realm.string_id if self.realm else None, + timestamp=self.timestamp, + ) + + +class ArchivedMessage(AbstractMessage): + """Used as a temporary holding place for deleted messages before they + are permanently deleted. This is an important part of a robust + 'message retention' feature. + """ + + archive_transaction = models.ForeignKey(ArchiveTransaction, on_delete=CASCADE) + + +class Message(AbstractMessage): + # Recipient types used when a Message object is provided to + # Zulip clients via the API. + # + # A detail worth noting: + # * "direct" was introduced in 2023 with the goal of + # deprecating the original "private" and becoming the + # preferred way to indicate a personal or huddle + # Recipient type via the API. + API_RECIPIENT_TYPES = ["direct", "private", "stream"] + + search_tsvector = SearchVectorField(null=True) + + DEFAULT_SELECT_RELATED = ["sender", "realm", "recipient", "sending_client"] + + def topic_name(self) -> str: + """ + Please start using this helper to facilitate an + eventual switch over to a separate topic table. + """ + return self.subject + + def set_topic_name(self, topic_name: str) -> None: + self.subject = topic_name + + def is_stream_message(self) -> bool: + """ + Find out whether a message is a stream message by + looking up its recipient.type. TODO: Make this + an easier operation by denormalizing the message + type onto Message, either explicitly (message.type) + or implicitly (message.stream_id is not None). + """ + return self.recipient.type == Recipient.STREAM + + def get_realm(self) -> Realm: + return self.realm + + def save_rendered_content(self) -> None: + self.save(update_fields=["rendered_content", "rendered_content_version"]) + + @staticmethod + def need_to_render_content( + rendered_content: Optional[str], + rendered_content_version: Optional[int], + markdown_version: int, + ) -> bool: + return ( + rendered_content is None + or rendered_content_version is None + or rendered_content_version < markdown_version + ) + + @staticmethod + def is_status_message(content: str, rendered_content: str) -> bool: + """ + "status messages" start with /me and have special rendering: + /me loves chocolate -> Full Name loves chocolate + """ + if content.startswith("/me "): + return True + return False + + class Meta: + indexes = [ + GinIndex("search_tsvector", fastupdate=False, name="zerver_message_search_tsvector"), + models.Index( + # For moving messages between streams or marking + # streams as read. The "id" at the end makes it easy + # to scan the resulting messages in order, and perform + # batching. + "realm_id", + "recipient_id", + "id", + name="zerver_message_realm_recipient_id", + ), + models.Index( + # For generating digest emails and message archiving, + # which both group by stream. + "realm_id", + "recipient_id", + "date_sent", + name="zerver_message_realm_recipient_date_sent", + ), + models.Index( + # For exports, which want to limit both sender and + # receiver. The prefix of this index (realm_id, + # sender_id) can be used for scrubbing users and/or + # deleting users' messages. + "realm_id", + "sender_id", + "recipient_id", + name="zerver_message_realm_sender_recipient", + ), + models.Index( + # For analytics queries + "realm_id", + "date_sent", + name="zerver_message_realm_date_sent", + ), + models.Index( + # For users searching by topic (but not stream), which + # is done case-insensitively + "realm_id", + Upper("subject"), + F("id").desc(nulls_last=True), + name="zerver_message_realm_upper_subject", + ), + models.Index( + # Most stream/topic searches are case-insensitive by + # topic name (e.g. messages_for_topic). The "id" at + # the end makes it easy to scan the resulting messages + # in order, and perform batching. + "realm_id", + "recipient_id", + Upper("subject"), + F("id").desc(nulls_last=True), + name="zerver_message_realm_recipient_upper_subject", + ), + models.Index( + # Used by already_sent_mirrored_message_id, and when + # determining recent topics (we post-process to merge + # and show the most recent case) + "realm_id", + "recipient_id", + "subject", + F("id").desc(nulls_last=True), + name="zerver_message_realm_recipient_subject", + ), + models.Index( + # Only used by update_first_visible_message_id + "realm_id", + F("id").desc(nulls_last=True), + name="zerver_message_realm_id", + ), + ] + + +def get_context_for_message(message: Message) -> QuerySet[Message]: + return Message.objects.filter( + # Uses index: zerver_message_realm_recipient_upper_subject + realm_id=message.realm_id, + recipient_id=message.recipient_id, + subject__iexact=message.subject, + id__lt=message.id, + date_sent__gt=message.date_sent - timedelta(minutes=15), + ).order_by("-id")[:10] + + +post_save.connect(flush_message, sender=Message) + + +class AbstractSubMessage(models.Model): + # We can send little text messages that are associated with a regular + # Zulip message. These can be used for experimental widgets like embedded + # games, surveys, mini threads, etc. These are designed to be pretty + # generic in purpose. + + sender = models.ForeignKey(UserProfile, on_delete=CASCADE) + msg_type = models.TextField() + content = models.TextField() + + class Meta: + abstract = True + + +class SubMessage(AbstractSubMessage): + message = models.ForeignKey(Message, on_delete=CASCADE) + + @staticmethod + def get_raw_db_rows(needed_ids: List[int]) -> List[Dict[str, Any]]: + fields = ["id", "message_id", "sender_id", "msg_type", "content"] + query = SubMessage.objects.filter(message_id__in=needed_ids).values(*fields) + query = query.order_by("message_id", "id") + return list(query) + + +class ArchivedSubMessage(AbstractSubMessage): + message = models.ForeignKey(ArchivedMessage, on_delete=CASCADE) + + +post_save.connect(flush_submessage, sender=SubMessage) + + +class AbstractEmoji(models.Model): + """For emoji reactions to messages (and potentially future reaction types). + + Emoji are surprisingly complicated to implement correctly. For details + on how this subsystem works, see: + https://zulip.readthedocs.io/en/latest/subsystems/emoji.html + """ + + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + + # The user-facing name for an emoji reaction. With emoji aliases, + # there may be multiple accepted names for a given emoji; this + # field encodes which one the user selected. + emoji_name = models.TextField() + + UNICODE_EMOJI = "unicode_emoji" + REALM_EMOJI = "realm_emoji" + ZULIP_EXTRA_EMOJI = "zulip_extra_emoji" + REACTION_TYPES = ( + (UNICODE_EMOJI, gettext_lazy("Unicode emoji")), + (REALM_EMOJI, gettext_lazy("Custom emoji")), + (ZULIP_EXTRA_EMOJI, gettext_lazy("Zulip extra emoji")), + ) + reaction_type = models.CharField(default=UNICODE_EMOJI, choices=REACTION_TYPES, max_length=30) + + # A string with the property that (realm, reaction_type, + # emoji_code) uniquely determines the emoji glyph. + # + # We cannot use `emoji_name` for this purpose, since the + # name-to-glyph mappings for unicode emoji change with time as we + # update our emoji database, and multiple custom emoji can have + # the same `emoji_name` in a realm (at most one can have + # `deactivated=False`). The format for `emoji_code` varies by + # `reaction_type`: + # + # * For Unicode emoji, a dash-separated hex encoding of the sequence of + # Unicode codepoints that define this emoji in the Unicode + # specification. For examples, see "non_qualified" or "unified" in the + # following data, with "non_qualified" taking precedence when both present: + # https://raw.githubusercontent.com/iamcal/emoji-data/master/emoji_pretty.json + # + # * For user uploaded custom emoji (`reaction_type="realm_emoji"`), the stringified ID + # of the RealmEmoji object, computed as `str(realm_emoji.id)`. + # + # * For "Zulip extra emoji" (like :zulip:), the name of the emoji (e.g. "zulip"). + emoji_code = models.TextField() + + class Meta: + abstract = True + + +class AbstractReaction(AbstractEmoji): + class Meta: + abstract = True + unique_together = ("user_profile", "message", "reaction_type", "emoji_code") + + +class Reaction(AbstractReaction): + message = models.ForeignKey(Message, on_delete=CASCADE) + + @staticmethod + def get_raw_db_rows(needed_ids: List[int]) -> List[Dict[str, Any]]: + fields = [ + "message_id", + "emoji_name", + "emoji_code", + "reaction_type", + "user_profile__email", + "user_profile_id", + "user_profile__full_name", + ] + # The ordering is important here, as it makes it convenient + # for clients to display reactions in order without + # client-side sorting code. + return Reaction.objects.filter(message_id__in=needed_ids).values(*fields).order_by("id") + + @override + def __str__(self) -> str: + return f"{self.user_profile.email} / {self.message.id} / {self.emoji_name}" + + +class ArchivedReaction(AbstractReaction): + message = models.ForeignKey(ArchivedMessage, on_delete=CASCADE) + + +# Whenever a message is sent, for each user subscribed to the +# corresponding Recipient object (that is not long-term idle), we add +# a row to the UserMessage table indicating that that user received +# that message. This table allows us to quickly query any user's last +# 1000 messages to generate the home view and search exactly the +# user's message history. +# +# The long-term idle optimization is extremely important for large, +# open organizations, and is described in detail here: +# https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#soft-deactivation +# +# In particular, new messages to public streams will only generate +# UserMessage rows for Members who are long_term_idle if they would +# have nonzero flags for the message (E.g. a mention, alert word, or +# mobile push notification). +# +# The flags field stores metadata like whether the user has read the +# message, starred or collapsed the message, was mentioned in the +# message, etc. We use of postgres partial indexes on flags to make +# queries for "User X's messages with flag Y" extremely fast without +# consuming much storage space. +# +# UserMessage is the largest table in many Zulip installations, even +# though each row is only 4 integers. +class AbstractUserMessage(models.Model): + id = models.BigAutoField(primary_key=True) + + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + # The order here is important! It's the order of fields in the bitfield. + ALL_FLAGS = [ + "read", + "starred", + "collapsed", + "mentioned", + "stream_wildcard_mentioned", + "topic_wildcard_mentioned", + "group_mentioned", + # These next 2 flags are from features that have since been removed. + # We've cleared these 2 flags in migration 0486. + "force_expand", + "force_collapse", + # Whether the message contains any of the user's alert words. + "has_alert_word", + # The historical flag is used to mark messages which the user + # did not receive when they were sent, but later added to + # their history via e.g. starring the message. This is + # important accounting for the "Subscribed to stream" dividers. + "historical", + # Whether the message is a direct message; this flag is a + # denormalization of message.recipient.type to support an + # efficient index on UserMessage for a user's direct messages. + "is_private", + # Whether we've sent a push notification to the user's mobile + # devices for this message that has not been revoked. + "active_mobile_push_notification", + ] + # Certain flags are used only for internal accounting within the + # Zulip backend, and don't make sense to expose to the API. + NON_API_FLAGS = {"is_private", "active_mobile_push_notification"} + # Certain additional flags are just set once when the UserMessage + # row is created. + NON_EDITABLE_FLAGS = { + # These flags are bookkeeping and don't make sense to edit. + "has_alert_word", + "mentioned", + "stream_wildcard_mentioned", + "topic_wildcard_mentioned", + "group_mentioned", + "historical", + # Unused flags can't be edited. + "force_expand", + "force_collapse", + } + flags: BitHandler = BitField(flags=ALL_FLAGS, default=0) + + class Meta: + abstract = True + unique_together = ("user_profile", "message") + + @staticmethod + def where_flag_is_present(flagattr: Bit) -> str: + # Use this for Django ORM queries to access starred messages. + # This custom SQL plays nice with our partial indexes. Grep + # the code for example usage. + # + # The key detail is that e.g. + # UserMessage.objects.filter(user_profile=user_profile, flags=UserMessage.flags.starred) + # will generate a query involving `flags & 2 = 2`, which doesn't match our index. + return f"flags & {1 << flagattr.number} <> 0" + + @staticmethod + def where_flag_is_absent(flagattr: Bit) -> str: + return f"flags & {1 << flagattr.number} = 0" + + @staticmethod + def where_unread() -> str: + return AbstractUserMessage.where_flag_is_absent(AbstractUserMessage.flags.read) + + @staticmethod + def where_read() -> str: + return AbstractUserMessage.where_flag_is_present(AbstractUserMessage.flags.read) + + @staticmethod + def where_starred() -> str: + return AbstractUserMessage.where_flag_is_present(AbstractUserMessage.flags.starred) + + @staticmethod + def where_active_push_notification() -> str: + return AbstractUserMessage.where_flag_is_present( + AbstractUserMessage.flags.active_mobile_push_notification + ) + + def flags_list(self) -> List[str]: + flags = int(self.flags) + return self.flags_list_for_flags(flags) + + @staticmethod + def flags_list_for_flags(val: int) -> List[str]: + """ + This function is highly optimized, because it actually slows down + sending messages in a naive implementation. + """ + flags = [] + mask = 1 + for flag in UserMessage.ALL_FLAGS: + if (val & mask) and flag not in AbstractUserMessage.NON_API_FLAGS: + flags.append(flag) + mask <<= 1 + return flags + + +class UserMessage(AbstractUserMessage): + message = models.ForeignKey(Message, on_delete=CASCADE) + + class Meta(AbstractUserMessage.Meta): + indexes = [ + models.Index( + "user_profile", + "message", + condition=Q(flags__andnz=AbstractUserMessage.flags.starred.mask), + name="zerver_usermessage_starred_message_id", + ), + models.Index( + "user_profile", + "message", + condition=Q(flags__andnz=AbstractUserMessage.flags.mentioned.mask), + name="zerver_usermessage_mentioned_message_id", + ), + models.Index( + "user_profile", + "message", + condition=Q(flags__andz=AbstractUserMessage.flags.read.mask), + name="zerver_usermessage_unread_message_id", + ), + models.Index( + "user_profile", + "message", + condition=Q(flags__andnz=AbstractUserMessage.flags.has_alert_word.mask), + name="zerver_usermessage_has_alert_word_message_id", + ), + models.Index( + "user_profile", + "message", + condition=Q(flags__andnz=AbstractUserMessage.flags.mentioned.mask) + | Q(flags__andnz=AbstractUserMessage.flags.stream_wildcard_mentioned.mask), + name="zerver_usermessage_wildcard_mentioned_message_id", + ), + models.Index( + "user_profile", + "message", + condition=Q( + flags__andnz=AbstractUserMessage.flags.mentioned.mask + | AbstractUserMessage.flags.stream_wildcard_mentioned.mask + | AbstractUserMessage.flags.topic_wildcard_mentioned.mask + | AbstractUserMessage.flags.group_mentioned.mask + ), + name="zerver_usermessage_any_mentioned_message_id", + ), + models.Index( + "user_profile", + "message", + condition=Q(flags__andnz=AbstractUserMessage.flags.is_private.mask), + name="zerver_usermessage_is_private_message_id", + ), + models.Index( + "user_profile", + "message", + condition=Q( + flags__andnz=AbstractUserMessage.flags.active_mobile_push_notification.mask + ), + name="zerver_usermessage_active_mobile_push_notification_id", + ), + ] + + @override + def __str__(self) -> str: + recipient_string = self.message.recipient.label() + return f"{recipient_string} / {self.user_profile.email} ({self.flags_list()})" + + @staticmethod + def select_for_update_query() -> QuerySet["UserMessage"]: + """This SELECT FOR UPDATE query ensures consistent ordering on + the row locks acquired by a bulk update operation to modify + message flags using bitand/bitor. + + This consistent ordering is important to prevent deadlocks when + 2 or more bulk updates to the same rows in the UserMessage table + race against each other (For example, if a client submits + simultaneous duplicate API requests to mark a certain set of + messages as read). + """ + return UserMessage.objects.select_for_update().order_by("message_id") + + @staticmethod + def has_any_mentions(user_profile_id: int, message_id: int) -> bool: + # The query uses the 'zerver_usermessage_any_mentioned_message_id' index. + return UserMessage.objects.filter( + Q( + flags__andnz=UserMessage.flags.mentioned.mask + | UserMessage.flags.stream_wildcard_mentioned.mask + | UserMessage.flags.topic_wildcard_mentioned.mask + | UserMessage.flags.group_mentioned.mask + ), + user_profile_id=user_profile_id, + message_id=message_id, + ).exists() + + +def get_usermessage_by_message_id( + user_profile: UserProfile, message_id: int +) -> Optional[UserMessage]: + try: + return UserMessage.objects.select_related().get( + user_profile=user_profile, message_id=message_id + ) + except UserMessage.DoesNotExist: + return None + + +class ArchivedUserMessage(AbstractUserMessage): + """Used as a temporary holding place for deleted UserMessages objects + before they are permanently deleted. This is an important part of + a robust 'message retention' feature. + """ + + message = models.ForeignKey(ArchivedMessage, on_delete=CASCADE) + + @override + def __str__(self) -> str: + recipient_string = self.message.recipient.label() + return f"{recipient_string} / {self.user_profile.email} ({self.flags_list()})" + + +class AbstractAttachment(models.Model): + file_name = models.TextField(db_index=True) + + # path_id is a storage location agnostic representation of the path of the file. + # If the path of a file is http://localhost:9991/user_uploads/a/b/abc/temp_file.py + # then its path_id will be a/b/abc/temp_file.py. + path_id = models.TextField(db_index=True, unique=True) + owner = models.ForeignKey(UserProfile, on_delete=CASCADE) + realm = models.ForeignKey(Realm, on_delete=CASCADE) + + create_time = models.DateTimeField( + default=timezone_now, + db_index=True, + ) + # Size of the uploaded file, in bytes + size = models.IntegerField() + + # The two fields below serve as caches to let us avoid looking up + # the corresponding messages/streams to check permissions before + # serving these files. + # + # For both fields, the `null` state is used when a change in + # message permissions mean that we need to determine their proper + # value. + + # Whether this attachment has been posted to a public stream, and + # thus should be available to all non-guest users in the + # organization (even if they weren't a recipient of a message + # linking to it). + is_realm_public = models.BooleanField(default=False, null=True) + # Whether this attachment has been posted to a web-public stream, + # and thus should be available to everyone on the internet, even + # if the person isn't logged in. + is_web_public = models.BooleanField(default=False, null=True) + + class Meta: + abstract = True + + @override + def __str__(self) -> str: + return self.file_name + + +class ArchivedAttachment(AbstractAttachment): + """Used as a temporary holding place for deleted Attachment objects + before they are permanently deleted. This is an important part of + a robust 'message retention' feature. + + Unlike the similar archive tables, ArchivedAttachment does not + have an ArchiveTransaction foreign key, and thus will not be + directly deleted by clean_archived_data. Instead, attachments that + were only referenced by now fully deleted messages will leave + ArchivedAttachment objects with empty `.messages`. + + A second step, delete_old_unclaimed_attachments, will delete the + resulting orphaned ArchivedAttachment objects, along with removing + the associated uploaded files from storage. + """ + + messages = models.ManyToManyField( + ArchivedMessage, related_name="attachment_set", related_query_name="attachment" + ) + + +class Attachment(AbstractAttachment): + messages = models.ManyToManyField(Message) + + # This is only present for Attachment and not ArchiveAttachment. + # because ScheduledMessage is not subject to archiving. + scheduled_messages = models.ManyToManyField("zerver.ScheduledMessage") + + def is_claimed(self) -> bool: + return self.messages.exists() or self.scheduled_messages.exists() + + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.id, + "name": self.file_name, + "path_id": self.path_id, + "size": self.size, + # convert to JavaScript-style UNIX timestamp so we can take + # advantage of client time zones. + "create_time": int(time.mktime(self.create_time.timetuple()) * 1000), + "messages": [ + { + "id": m.id, + "date_sent": int(time.mktime(m.date_sent.timetuple()) * 1000), + } + for m in self.messages.all() + ], + } + + +post_save.connect(flush_used_upload_space_cache, sender=Attachment) +post_delete.connect(flush_used_upload_space_cache, sender=Attachment) diff --git a/zerver/models/muted_users.py b/zerver/models/muted_users.py new file mode 100644 index 0000000000000..e72e038cdf8f7 --- /dev/null +++ b/zerver/models/muted_users.py @@ -0,0 +1,25 @@ +from django.db import models +from django.db.models import CASCADE +from django.db.models.signals import post_delete, post_save +from django.utils.timezone import now as timezone_now +from typing_extensions import override + +from zerver.lib.cache import flush_muting_users_cache +from zerver.models.users import UserProfile + + +class MutedUser(models.Model): + user_profile = models.ForeignKey(UserProfile, related_name="muter", on_delete=CASCADE) + muted_user = models.ForeignKey(UserProfile, related_name="muted", on_delete=CASCADE) + date_muted = models.DateTimeField(default=timezone_now) + + class Meta: + unique_together = ("user_profile", "muted_user") + + @override + def __str__(self) -> str: + return f"{self.user_profile.email} -> {self.muted_user.email}" + + +post_save.connect(flush_muting_users_cache, sender=MutedUser) +post_delete.connect(flush_muting_users_cache, sender=MutedUser) diff --git a/zerver/models/onboarding_steps.py b/zerver/models/onboarding_steps.py new file mode 100644 index 0000000000000..565fb13572197 --- /dev/null +++ b/zerver/models/onboarding_steps.py @@ -0,0 +1,14 @@ +from django.db import models +from django.db.models import CASCADE +from django.utils.timezone import now as timezone_now + +from zerver.models.users import UserProfile + + +class OnboardingStep(models.Model): + user = models.ForeignKey(UserProfile, on_delete=CASCADE) + onboarding_step = models.CharField(max_length=30) + timestamp = models.DateTimeField(default=timezone_now) + + class Meta: + unique_together = ("user", "onboarding_step") diff --git a/zerver/models/prereg_users.py b/zerver/models/prereg_users.py new file mode 100644 index 0000000000000..b00ccf01fd179 --- /dev/null +++ b/zerver/models/prereg_users.py @@ -0,0 +1,175 @@ +from datetime import timedelta +from typing import Optional, Union + +from django.contrib.contenttypes.fields import GenericRelation +from django.db import models +from django.db.models import CASCADE, Q, QuerySet +from django.db.models.functions import Upper +from django.utils.timezone import now as timezone_now + +from confirmation import settings as confirmation_settings +from zerver.lib.types import UnspecifiedValue +from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH +from zerver.models.realms import Realm +from zerver.models.users import UserProfile + + +class PreregistrationRealm(models.Model): + """Data on a partially created realm entered by a user who has + completed the "new organization" form. Used to transfer the user's + selections from the pre-confirmation "new organization" form to + the post-confirmation user registration form. + + Note that the values stored here may not match those of the + created realm (in the event the user creates a realm at all), + because we allow the user to edit these values in the registration + form (and in fact the user will be required to do so if the + `string_id` is claimed by another realm before registraiton is + completed). + """ + + name = models.CharField(max_length=Realm.MAX_REALM_NAME_LENGTH) + org_type = models.PositiveSmallIntegerField( + default=Realm.ORG_TYPES["unspecified"]["id"], + choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()], + ) + default_language = models.CharField( + default="en", + max_length=MAX_LANGUAGE_ID_LENGTH, + ) + string_id = models.CharField(max_length=Realm.MAX_REALM_SUBDOMAIN_LENGTH) + email = models.EmailField() + + confirmation = GenericRelation("confirmation.Confirmation", related_query_name="prereg_realm") + status = models.IntegerField(default=0) + + # The Realm created upon completion of the registration + # for this PregistrationRealm + created_realm = models.ForeignKey(Realm, null=True, related_name="+", on_delete=models.SET_NULL) + + # The UserProfile created upon completion of the registration + # for this PregistrationRealm + created_user = models.ForeignKey( + UserProfile, null=True, related_name="+", on_delete=models.SET_NULL + ) + + +class PreregistrationUser(models.Model): + # Data on a partially created user, before the completion of + # registration. This is used in at least three major code paths: + # * Realm creation, in which case realm is None. + # + # * Invitations, in which case referred_by will always be set. + # + # * Social authentication signup, where it's used to store data + # from the authentication step and pass it to the registration + # form. + + email = models.EmailField() + + confirmation = GenericRelation("confirmation.Confirmation", related_query_name="prereg_user") + # If the pre-registration process provides a suggested full name for this user, + # store it here to use it to prepopulate the full name field in the registration form: + full_name = models.CharField(max_length=UserProfile.MAX_NAME_LENGTH, null=True) + full_name_validated = models.BooleanField(default=False) + referred_by = models.ForeignKey(UserProfile, null=True, on_delete=CASCADE) + streams = models.ManyToManyField("zerver.Stream") + invited_at = models.DateTimeField(auto_now=True) + realm_creation = models.BooleanField(default=False) + # Indicates whether the user needs a password. Users who were + # created via SSO style auth (e.g. GitHub/Google) generally do not. + password_required = models.BooleanField(default=True) + + # status: whether an object has been confirmed. + # if confirmed, set to confirmation.settings.STATUS_USED + status = models.IntegerField(default=0) + + # The realm should only ever be None for PreregistrationUser + # objects created as part of realm creation. + realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE) + + # These values should be consistent with the values + # in settings_config.user_role_values. + INVITE_AS = dict( + REALM_OWNER=100, + REALM_ADMIN=200, + MODERATOR=300, + MEMBER=400, + GUEST_USER=600, + ) + invited_as = models.PositiveSmallIntegerField(default=INVITE_AS["MEMBER"]) + + multiuse_invite = models.ForeignKey("MultiuseInvite", null=True, on_delete=models.SET_NULL) + + # The UserProfile created upon completion of the registration + # for this PregistrationUser + created_user = models.ForeignKey( + UserProfile, null=True, related_name="+", on_delete=models.SET_NULL + ) + + class Meta: + indexes = [ + models.Index(Upper("email"), name="upper_preregistration_email_idx"), + ] + + +def filter_to_valid_prereg_users( + query: QuerySet[PreregistrationUser], + invite_expires_in_minutes: Union[Optional[int], UnspecifiedValue] = UnspecifiedValue(), +) -> QuerySet[PreregistrationUser]: + """ + If invite_expires_in_days is specified, we return only those PreregistrationUser + objects that were created at most that many days in the past. + """ + used_value = confirmation_settings.STATUS_USED + revoked_value = confirmation_settings.STATUS_REVOKED + + query = query.exclude(status__in=[used_value, revoked_value]) + if invite_expires_in_minutes is None: + # Since invite_expires_in_minutes is None, we're invitation will never + # expire, we do not need to check anything else and can simply return + # after excluding objects with active and revoked status. + return query + + assert invite_expires_in_minutes is not None + if not isinstance(invite_expires_in_minutes, UnspecifiedValue): + lowest_datetime = timezone_now() - timedelta(minutes=invite_expires_in_minutes) + return query.filter(invited_at__gte=lowest_datetime) + else: + return query.filter( + Q(confirmation__expiry_date=None) | Q(confirmation__expiry_date__gte=timezone_now()) + ) + + +class MultiuseInvite(models.Model): + referred_by = models.ForeignKey(UserProfile, on_delete=CASCADE) + streams = models.ManyToManyField("zerver.Stream") + realm = models.ForeignKey(Realm, on_delete=CASCADE) + invited_as = models.PositiveSmallIntegerField(default=PreregistrationUser.INVITE_AS["MEMBER"]) + + # status for tracking whether the invite has been revoked. + # If revoked, set to confirmation.settings.STATUS_REVOKED. + # STATUS_USED is not supported, because these objects are supposed + # to be usable multiple times. + status = models.IntegerField(default=0) + + +class EmailChangeStatus(models.Model): + new_email = models.EmailField() + old_email = models.EmailField() + updated_at = models.DateTimeField(auto_now=True) + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + + # status: whether an object has been confirmed. + # if confirmed, set to confirmation.settings.STATUS_USED + status = models.IntegerField(default=0) + + realm = models.ForeignKey(Realm, on_delete=CASCADE) + + +class RealmReactivationStatus(models.Model): + # status: whether an object has been confirmed. + # if confirmed, set to confirmation.settings.STATUS_USED + status = models.IntegerField(default=0) + + realm = models.ForeignKey(Realm, on_delete=CASCADE) diff --git a/zerver/models/presence.py b/zerver/models/presence.py new file mode 100644 index 0000000000000..3a1a47c616e40 --- /dev/null +++ b/zerver/models/presence.py @@ -0,0 +1,84 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + +from typing import Optional + +from django.db import models +from django.db.models import CASCADE +from django.utils.timezone import now as timezone_now + +from zerver.models.clients import Client +from zerver.models.messages import AbstractEmoji +from zerver.models.realms import Realm +from zerver.models.users import UserProfile + + +class UserPresence(models.Model): + """A record from the last time we heard from a given user on a given client. + + NOTE: Users can disable updates to this table (see UserProfile.presence_enabled), + so this cannot be used to determine if a user was recently active on Zulip. + The UserActivity table is recommended for that purpose. + + This is a tricky subsystem, because it is highly optimized. See the docs: + https://zulip.readthedocs.io/en/latest/subsystems/presence.html + """ + + user_profile = models.OneToOneField(UserProfile, on_delete=CASCADE, unique=True) + + # Realm is just here as denormalization to optimize database + # queries to fetch all presence data for a given realm. + realm = models.ForeignKey(Realm, on_delete=CASCADE) + + # The last time the user had a client connected to Zulip, + # including idle clients where the user hasn't interacted with the + # system recently (and thus might be AFK). + last_connected_time = models.DateTimeField(default=timezone_now, db_index=True, null=True) + # The last time a client connected to Zulip reported that the user + # was actually present (E.g. via focusing a browser window or + # interacting with a computer running the desktop app) + last_active_time = models.DateTimeField(default=timezone_now, db_index=True, null=True) + + # The following constants are used in the presence API for + # communicating whether a user is active (last_active_time recent) + # or idle (last_connected_time recent) or offline (neither + # recent). They're no longer part of the data model. + LEGACY_STATUS_ACTIVE = "active" + LEGACY_STATUS_IDLE = "idle" + LEGACY_STATUS_ACTIVE_INT = 1 + LEGACY_STATUS_IDLE_INT = 2 + + class Meta: + indexes = [ + models.Index( + fields=["realm", "last_active_time"], + name="zerver_userpresence_realm_id_last_active_time_1c5aa9a2_idx", + ), + models.Index( + fields=["realm", "last_connected_time"], + name="zerver_userpresence_realm_id_last_connected_time_98d2fc9f_idx", + ), + ] + + @staticmethod + def status_from_string(status: str) -> Optional[int]: + if status == "active": + return UserPresence.LEGACY_STATUS_ACTIVE_INT + elif status == "idle": + return UserPresence.LEGACY_STATUS_IDLE_INT + + return None + + +class UserStatus(AbstractEmoji): + user_profile = models.OneToOneField(UserProfile, on_delete=CASCADE) + + timestamp = models.DateTimeField() + client = models.ForeignKey(Client, on_delete=CASCADE) + + # Override emoji_name and emoji_code field of (AbstractReaction model) to accept + # default value. + emoji_name = models.TextField(default="") + emoji_code = models.TextField(default="") + + status_text = models.CharField(max_length=255, default="") diff --git a/zerver/models/push_notifications.py b/zerver/models/push_notifications.py new file mode 100644 index 0000000000000..079a2b2697465 --- /dev/null +++ b/zerver/models/push_notifications.py @@ -0,0 +1,43 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + +from django.db import models +from django.db.models import CASCADE + +from zerver.models.users import UserProfile + + +class AbstractPushDeviceToken(models.Model): + APNS = 1 + GCM = 2 + + KINDS = ( + (APNS, "apns"), + (GCM, "gcm"), + ) + + kind = models.PositiveSmallIntegerField(choices=KINDS) + + # The token is a unique device-specific token that is + # sent to us from each device: + # - APNS token if kind == APNS + # - GCM registration id if kind == GCM + token = models.CharField(max_length=4096, db_index=True) + + # TODO: last_updated should be renamed date_created, since it is + # no longer maintained as a last_updated value. + last_updated = models.DateTimeField(auto_now=True) + + # [optional] Contains the app id of the device if it is an iOS device + ios_app_id = models.TextField(null=True) + + class Meta: + abstract = True + + +class PushDeviceToken(AbstractPushDeviceToken): + # The user whose device this is + user = models.ForeignKey(UserProfile, db_index=True, on_delete=CASCADE) + + class Meta: + unique_together = ("user", "kind", "token") diff --git a/zerver/models/realm_audit_logs.py b/zerver/models/realm_audit_logs.py new file mode 100644 index 0000000000000..ee64b51f33e89 --- /dev/null +++ b/zerver/models/realm_audit_logs.py @@ -0,0 +1,235 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + +from django.core.serializers.json import DjangoJSONEncoder +from django.db import models +from django.db.models import CASCADE, Q +from typing_extensions import override + +from zerver.models.groups import UserGroup +from zerver.models.realms import Realm +from zerver.models.streams import Stream +from zerver.models.users import UserProfile + + +class AbstractRealmAuditLog(models.Model): + """Defines fields common to RealmAuditLog and RemoteRealmAuditLog.""" + + event_time = models.DateTimeField(db_index=True) + # If True, event_time is an overestimate of the true time. Can be used + # by migrations when introducing a new event_type. + backfilled = models.BooleanField(default=False) + + # Keys within extra_data, when extra_data is a json dict. Keys are strings because + # json keys must always be strings. + OLD_VALUE = "1" + NEW_VALUE = "2" + ROLE_COUNT = "10" + ROLE_COUNT_HUMANS = "11" + ROLE_COUNT_BOTS = "12" + + extra_data = models.JSONField(default=dict, encoder=DjangoJSONEncoder) + + # Event types + USER_CREATED = 101 + USER_ACTIVATED = 102 + USER_DEACTIVATED = 103 + USER_REACTIVATED = 104 + USER_ROLE_CHANGED = 105 + USER_DELETED = 106 + USER_DELETED_PRESERVING_MESSAGES = 107 + + USER_SOFT_ACTIVATED = 120 + USER_SOFT_DEACTIVATED = 121 + USER_PASSWORD_CHANGED = 122 + USER_AVATAR_SOURCE_CHANGED = 123 + USER_FULL_NAME_CHANGED = 124 + USER_EMAIL_CHANGED = 125 + USER_TERMS_OF_SERVICE_VERSION_CHANGED = 126 + USER_API_KEY_CHANGED = 127 + USER_BOT_OWNER_CHANGED = 128 + USER_DEFAULT_SENDING_STREAM_CHANGED = 129 + USER_DEFAULT_REGISTER_STREAM_CHANGED = 130 + USER_DEFAULT_ALL_PUBLIC_STREAMS_CHANGED = 131 + USER_SETTING_CHANGED = 132 + USER_DIGEST_EMAIL_CREATED = 133 + + REALM_DEACTIVATED = 201 + REALM_REACTIVATED = 202 + REALM_SCRUBBED = 203 + REALM_PLAN_TYPE_CHANGED = 204 + REALM_LOGO_CHANGED = 205 + REALM_EXPORTED = 206 + REALM_PROPERTY_CHANGED = 207 + REALM_ICON_SOURCE_CHANGED = 208 + REALM_DISCOUNT_CHANGED = 209 + REALM_SPONSORSHIP_APPROVED = 210 + REALM_BILLING_MODALITY_CHANGED = 211 + REALM_REACTIVATION_EMAIL_SENT = 212 + REALM_SPONSORSHIP_PENDING_STATUS_CHANGED = 213 + REALM_SUBDOMAIN_CHANGED = 214 + REALM_CREATED = 215 + REALM_DEFAULT_USER_SETTINGS_CHANGED = 216 + REALM_ORG_TYPE_CHANGED = 217 + REALM_DOMAIN_ADDED = 218 + REALM_DOMAIN_CHANGED = 219 + REALM_DOMAIN_REMOVED = 220 + REALM_PLAYGROUND_ADDED = 221 + REALM_PLAYGROUND_REMOVED = 222 + REALM_LINKIFIER_ADDED = 223 + REALM_LINKIFIER_CHANGED = 224 + REALM_LINKIFIER_REMOVED = 225 + REALM_EMOJI_ADDED = 226 + REALM_EMOJI_REMOVED = 227 + REALM_LINKIFIERS_REORDERED = 228 + REALM_IMPORTED = 229 + + SUBSCRIPTION_CREATED = 301 + SUBSCRIPTION_ACTIVATED = 302 + SUBSCRIPTION_DEACTIVATED = 303 + SUBSCRIPTION_PROPERTY_CHANGED = 304 + + USER_MUTED = 350 + USER_UNMUTED = 351 + + STRIPE_CUSTOMER_CREATED = 401 + STRIPE_CARD_CHANGED = 402 + STRIPE_PLAN_CHANGED = 403 + STRIPE_PLAN_QUANTITY_RESET = 404 + + CUSTOMER_CREATED = 501 + CUSTOMER_PLAN_CREATED = 502 + CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 503 + CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 504 + + STREAM_CREATED = 601 + STREAM_DEACTIVATED = 602 + STREAM_NAME_CHANGED = 603 + STREAM_REACTIVATED = 604 + STREAM_MESSAGE_RETENTION_DAYS_CHANGED = 605 + STREAM_PROPERTY_CHANGED = 607 + STREAM_GROUP_BASED_SETTING_CHANGED = 608 + + USER_GROUP_CREATED = 701 + USER_GROUP_DELETED = 702 + USER_GROUP_DIRECT_USER_MEMBERSHIP_ADDED = 703 + USER_GROUP_DIRECT_USER_MEMBERSHIP_REMOVED = 704 + USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_ADDED = 705 + USER_GROUP_DIRECT_SUBGROUP_MEMBERSHIP_REMOVED = 706 + USER_GROUP_DIRECT_SUPERGROUP_MEMBERSHIP_ADDED = 707 + USER_GROUP_DIRECT_SUPERGROUP_MEMBERSHIP_REMOVED = 708 + # 709 to 719 reserved for membership changes + USER_GROUP_NAME_CHANGED = 720 + USER_GROUP_DESCRIPTION_CHANGED = 721 + USER_GROUP_GROUP_BASED_SETTING_CHANGED = 722 + + # The following values are only for RemoteZulipServerAuditLog + # Values should be exactly 10000 greater than the corresponding + # value used for the same purpose in RealmAuditLog (e.g. + # REALM_DEACTIVATED = 201, and REMOTE_SERVER_DEACTIVATED = 10201). + REMOTE_SERVER_DEACTIVATED = 10201 + REMOTE_SERVER_PLAN_TYPE_CHANGED = 10204 + REMOTE_SERVER_DISCOUNT_CHANGED = 10209 + REMOTE_SERVER_SPONSORSHIP_APPROVED = 10210 + REMOTE_SERVER_BILLING_MODALITY_CHANGED = 10211 + REMOTE_SERVER_SPONSORSHIP_PENDING_STATUS_CHANGED = 10213 + REMOTE_SERVER_CREATED = 10215 + + # This value is for RemoteRealmAuditLog entries tracking changes to the + # RemoteRealm model resulting from modified realm information sent to us + # via send_server_data_to_push_bouncer. + REMOTE_REALM_VALUE_UPDATED = 20001 + REMOTE_PLAN_TRANSFERRED_SERVER_TO_REALM = 20002 + REMOTE_REALM_LOCALLY_DELETED = 20003 + + event_type = models.PositiveSmallIntegerField() + + # event_types synced from on-prem installations to Zulip Cloud when + # billing for mobile push notifications is enabled. Every billing + # event_type should have ROLE_COUNT populated in extra_data. + SYNCED_BILLING_EVENTS = [ + USER_CREATED, + USER_ACTIVATED, + USER_DEACTIVATED, + USER_REACTIVATED, + USER_ROLE_CHANGED, + REALM_DEACTIVATED, + REALM_REACTIVATED, + REALM_IMPORTED, + ] + + class Meta: + abstract = True + + +class RealmAuditLog(AbstractRealmAuditLog): + """ + RealmAuditLog tracks important changes to users, streams, and + realms in Zulip. It is intended to support both + debugging/introspection (e.g. determining when a user's left a + given stream?) as well as help with some database migrations where + we might be able to do a better data backfill with it. Here are a + few key details about how this works: + + * acting_user is the user who initiated the state change + * modified_user (if present) is the user being modified + * modified_stream (if present) is the stream being modified + * modified_user_group (if present) is the user group being modified + + For example: + * When a user subscribes another user to a stream, modified_user, + acting_user, and modified_stream will all be present and different. + * When an administrator changes an organization's realm icon, + acting_user is that administrator and modified_user, + modified_stream and modified_user_group will be None. + """ + + realm = models.ForeignKey(Realm, on_delete=CASCADE) + acting_user = models.ForeignKey( + UserProfile, + null=True, + related_name="+", + on_delete=CASCADE, + ) + modified_user = models.ForeignKey( + UserProfile, + null=True, + related_name="+", + on_delete=CASCADE, + ) + modified_stream = models.ForeignKey( + Stream, + null=True, + on_delete=CASCADE, + ) + modified_user_group = models.ForeignKey( + UserGroup, + null=True, + on_delete=CASCADE, + ) + event_last_message_id = models.IntegerField(null=True) + + @override + def __str__(self) -> str: + if self.modified_user is not None: + return f"{self.modified_user!r} {self.event_type} {self.event_time} {self.id}" + if self.modified_stream is not None: + return f"{self.modified_stream!r} {self.event_type} {self.event_time} {self.id}" + if self.modified_user_group is not None: + return f"{self.modified_user_group!r} {self.event_type} {self.event_time} {self.id}" + return f"{self.realm!r} {self.event_type} {self.event_time} {self.id}" + + class Meta: + indexes = [ + models.Index( + name="zerver_realmauditlog_user_subscriptions_idx", + fields=["modified_user", "modified_stream"], + condition=Q( + event_type__in=[ + AbstractRealmAuditLog.SUBSCRIPTION_CREATED, + AbstractRealmAuditLog.SUBSCRIPTION_ACTIVATED, + AbstractRealmAuditLog.SUBSCRIPTION_DEACTIVATED, + ] + ), + ) + ] diff --git a/zerver/models/realm_emoji.py b/zerver/models/realm_emoji.py new file mode 100644 index 0000000000000..02766beeab361 --- /dev/null +++ b/zerver/models/realm_emoji.py @@ -0,0 +1,142 @@ +from typing import Dict, Optional, TypedDict + +from django.core.validators import MinLengthValidator, RegexValidator +from django.db import models +from django.db.models import CASCADE, Q +from django.db.models.signals import post_delete, post_save +from django.utils.translation import gettext_lazy +from typing_extensions import override + +from zerver.lib.cache import cache_set, cache_with_key +from zerver.models.realms import Realm + + +class EmojiInfo(TypedDict): + id: str + name: str + source_url: str + deactivated: bool + author_id: Optional[int] + still_url: Optional[str] + + +def get_all_custom_emoji_for_realm_cache_key(realm_id: int) -> str: + return f"realm_emoji:{realm_id}" + + +class RealmEmoji(models.Model): + author = models.ForeignKey( + "UserProfile", + blank=True, + null=True, + on_delete=CASCADE, + ) + realm = models.ForeignKey(Realm, on_delete=CASCADE) + name = models.TextField( + validators=[ + MinLengthValidator(1), + # The second part of the regex (negative lookbehind) disallows names + # ending with one of the punctuation characters. + RegexValidator( + regex=r"^[0-9a-z.\-_]+(? str: + return f"{self.realm.string_id}: {self.id} {self.name} {self.deactivated} {self.file_name}" + + +def get_all_custom_emoji_for_realm_uncached(realm_id: int) -> Dict[str, EmojiInfo]: + # RealmEmoji objects with file_name=None are still in the process + # of being uploaded, and we expect to be cleaned up by a + # try/finally block if the upload fails, so it's correct to + # exclude them. + query = RealmEmoji.objects.filter(realm_id=realm_id).exclude( + file_name=None, + ) + d = {} + from zerver.lib.emoji import get_emoji_url + + for realm_emoji in query.all(): + author_id = realm_emoji.author_id + assert realm_emoji.file_name is not None + emoji_url = get_emoji_url(realm_emoji.file_name, realm_emoji.realm_id) + + emoji_dict: EmojiInfo = dict( + id=str(realm_emoji.id), + name=realm_emoji.name, + source_url=emoji_url, + deactivated=realm_emoji.deactivated, + author_id=author_id, + still_url=None, + ) + + if realm_emoji.is_animated: + # For animated emoji, we include still_url with a static + # version of the image, so that clients can display the + # emoji in a less distracting (not animated) fashion when + # desired. + emoji_dict["still_url"] = get_emoji_url( + realm_emoji.file_name, realm_emoji.realm_id, still=True + ) + + d[str(realm_emoji.id)] = emoji_dict + + return d + + +@cache_with_key(get_all_custom_emoji_for_realm_cache_key, timeout=3600 * 24 * 7) +def get_all_custom_emoji_for_realm(realm_id: int) -> Dict[str, EmojiInfo]: + return get_all_custom_emoji_for_realm_uncached(realm_id) + + +def get_name_keyed_dict_for_active_realm_emoji(realm_id: int) -> Dict[str, EmojiInfo]: + # It's important to use the cached version here. + realm_emojis = get_all_custom_emoji_for_realm(realm_id) + return {row["name"]: row for row in realm_emojis.values() if not row["deactivated"]} + + +def flush_realm_emoji(*, instance: RealmEmoji, **kwargs: object) -> None: + if instance.file_name is None: + # Because we construct RealmEmoji.file_name using the ID for + # the RealmEmoji object, it will always have file_name=None, + # and then it'll be updated with the actual filename as soon + # as the upload completes successfully. + # + # Doing nothing when file_name=None is the best option, since + # such an object shouldn't have been cached yet, and this + # function will be called again when file_name is set. + return + realm_id = instance.realm_id + cache_set( + get_all_custom_emoji_for_realm_cache_key(realm_id), + get_all_custom_emoji_for_realm_uncached(realm_id), + timeout=3600 * 24 * 7, + ) + + +post_save.connect(flush_realm_emoji, sender=RealmEmoji) +post_delete.connect(flush_realm_emoji, sender=RealmEmoji) diff --git a/zerver/models/realm_playgrounds.py b/zerver/models/realm_playgrounds.py new file mode 100644 index 0000000000000..92b8df125e8b2 --- /dev/null +++ b/zerver/models/realm_playgrounds.py @@ -0,0 +1,88 @@ +from typing import List + +import uri_template +from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator +from django.db import models +from django.db.models import CASCADE +from django.utils.translation import gettext as _ +from typing_extensions import override + +from zerver.lib.types import RealmPlaygroundDict +from zerver.models.linkifiers import url_template_validator +from zerver.models.realms import Realm + + +class RealmPlayground(models.Model): + """Server side storage model to store playground information needed by our + 'view code in playground' feature in code blocks. + """ + + MAX_PYGMENTS_LANGUAGE_LENGTH = 40 + + realm = models.ForeignKey(Realm, on_delete=CASCADE) + url_template = models.TextField(validators=[url_template_validator]) + + # User-visible display name used when configuring playgrounds in the settings page and + # when displaying them in the playground links popover. + name = models.TextField(db_index=True) + + # This stores the pygments lexer subclass names and not the aliases themselves. + pygments_language = models.CharField( + db_index=True, + max_length=MAX_PYGMENTS_LANGUAGE_LENGTH, + # We validate to see if this conforms to the character set allowed for a + # language in the code block. + validators=[ + RegexValidator( + regex=r"^[ a-zA-Z0-9_+-./#]*$", message=_("Invalid characters in pygments language") + ) + ], + ) + + class Meta: + unique_together = (("realm", "pygments_language", "name"),) + + @override + def __str__(self) -> str: + return f"{self.realm.string_id}: {self.pygments_language} {self.name}" + + @override + def clean(self) -> None: + """Validate whether the URL template is valid for the playground, + ensuring that "code" is the sole variable present in it. + + Django's `full_clean` calls `clean_fields` followed by `clean` method + and stores all ValidationErrors from all stages to return as JSON. + """ + + # Do not continue the check if the url template is invalid to begin + # with. The ValidationError for invalid template will only be raised by + # the validator set on the url_template field instead of here to avoid + # duplicates. + if not uri_template.validate(self.url_template): + return + + # Extract variables used in the URL template. + template_variables = set(uri_template.URITemplate(self.url_template).variable_names) + + if "code" not in template_variables: + raise ValidationError(_('Missing the required variable "code" in the URL template')) + + # The URL template should only contain a single variable, which is "code". + if len(template_variables) != 1: + raise ValidationError( + _('"code" should be the only variable present in the URL template'), + ) + + +def get_realm_playgrounds(realm: Realm) -> List[RealmPlaygroundDict]: + return [ + RealmPlaygroundDict( + id=playground.id, + name=playground.name, + pygments_language=playground.pygments_language, + url_template=playground.url_template, + ) + for playground in RealmPlayground.objects.filter(realm=realm).all() + ] diff --git a/zerver/models/realms.py b/zerver/models/realms.py new file mode 100644 index 0000000000000..f243e5dd8ec23 --- /dev/null +++ b/zerver/models/realms.py @@ -0,0 +1,1019 @@ +from email.headerregistry import Address +from enum import Enum +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, TypedDict, Union +from uuid import uuid4 + +import django.contrib.auth +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.validators import validate_email +from django.db import models +from django.db.models import CASCADE, Q, QuerySet, Sum +from django.db.models.signals import post_delete, post_save, pre_delete +from django.utils.timezone import now as timezone_now +from django.utils.translation import gettext_lazy +from typing_extensions import override + +from zerver.lib.cache import cache_with_key, flush_realm, get_realm_used_upload_space_cache_key +from zerver.lib.exceptions import JsonableError +from zerver.lib.pysa import mark_sanitized +from zerver.lib.types import GroupPermissionSetting +from zerver.lib.utils import generate_api_key +from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH +from zerver.models.groups import SystemGroups +from zerver.models.users import UserProfile + +if TYPE_CHECKING: + # We use ModelBackend only for typing. Importing it otherwise causes circular dependency. + from django.contrib.auth.backends import ModelBackend + + from zerver.models import Stream + +SECONDS_PER_DAY = 86400 + + +# This simple call-once caching saves ~500us in auth_enabled_helper, +# which is a significant optimization for common_context. Note that +# these values cannot change in a running production system, but do +# regularly change within unit tests; we address the latter by calling +# clear_supported_auth_backends_cache in our standard tearDown code. +supported_backends: Optional[List["ModelBackend"]] = None + + +def supported_auth_backends() -> List["ModelBackend"]: + global supported_backends + # Caching temporarily disabled for debugging + supported_backends = django.contrib.auth.get_backends() + assert supported_backends is not None + return supported_backends + + +def clear_supported_auth_backends_cache() -> None: + global supported_backends + supported_backends = None + + +class RealmAuthenticationMethod(models.Model): + """ + Tracks which authentication backends are enabled for a realm. + An enabled backend is represented in this table a row with appropriate + .realm value and .name matching the name of the target backend in the + AUTH_BACKEND_NAME_MAP dict. + """ + + realm = models.ForeignKey("Realm", on_delete=CASCADE, db_index=True) + name = models.CharField(max_length=80) + + class Meta: + unique_together = ("realm", "name") + + +def generate_realm_uuid_owner_secret() -> str: + token = generate_api_key() + + # We include a prefix to facilitate scanning for accidental + # disclosure of secrets e.g. in Github commit pushes. + return f"zuliprealm_{token}" + + +class OrgTypeEnum(Enum): + Unspecified = 0 + Business = 10 + OpenSource = 20 + EducationNonProfit = 30 + Education = 35 + Research = 40 + Event = 50 + NonProfit = 60 + Government = 70 + PoliticalGroup = 80 + Community = 90 + Personal = 100 + Other = 1000 + + +class OrgTypeDict(TypedDict): + name: str + id: int + hidden: bool + display_order: int + onboarding_zulip_guide_url: Optional[str] + + +class Realm(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 + MAX_REALM_NAME_LENGTH = 40 + MAX_REALM_DESCRIPTION_LENGTH = 1000 + MAX_REALM_SUBDOMAIN_LENGTH = 40 + MAX_REALM_REDIRECT_URL_LENGTH = 128 + + INVITES_STANDARD_REALM_DAILY_MAX = 3000 + MESSAGE_VISIBILITY_LIMITED = 10000 + SUBDOMAIN_FOR_ROOT_DOMAIN = "" + WILDCARD_MENTION_THRESHOLD = 15 + + # User-visible display name and description used on e.g. the organization homepage + name = models.CharField(max_length=MAX_REALM_NAME_LENGTH) + description = models.TextField(default="") + + # A short, identifier-like name for the organization. Used in subdomains; + # e.g. on a server at example.com, an org with string_id `foo` is reached + # at `foo.example.com`. + string_id = models.CharField(max_length=MAX_REALM_SUBDOMAIN_LENGTH, unique=True) + + # uuid and a secret for the sake of per-realm authentication with the push notification + # bouncer. + uuid = models.UUIDField(default=uuid4, unique=True) + uuid_owner_secret = models.TextField(default=generate_realm_uuid_owner_secret) + # Whether push notifications are working for this realm, and + # whether there is a specific date at which we expect that to + # cease to be the case. + push_notifications_enabled = models.BooleanField(default=False, db_index=True) + push_notifications_enabled_end_timestamp = models.DateTimeField(default=None, null=True) + + date_created = models.DateTimeField(default=timezone_now) + demo_organization_scheduled_deletion_date = models.DateTimeField(default=None, null=True) + deactivated = models.BooleanField(default=False) + + # Redirect URL if the Realm has moved to another server + deactivated_redirect = models.URLField(max_length=MAX_REALM_REDIRECT_URL_LENGTH, null=True) + + # See RealmDomain for the domains that apply for a given organization. + emails_restricted_to_domains = models.BooleanField(default=False) + + invite_required = models.BooleanField(default=True) + + _max_invites = models.IntegerField(null=True, db_column="max_invites") + disallow_disposable_email_addresses = models.BooleanField(default=True) + + # Allow users to access web-public streams without login. This + # setting also controls API access of web-public streams. + enable_spectator_access = models.BooleanField(default=False) + + # Whether organization has given permission to be advertised in the + # Zulip communities directory. + want_advertise_in_communities_directory = models.BooleanField(default=False, db_index=True) + + # Whether the organization has enabled inline image and URL previews. + inline_image_preview = models.BooleanField(default=True) + inline_url_embed_preview = models.BooleanField(default=False) + + # Whether digest emails are enabled for the organization. + digest_emails_enabled = models.BooleanField(default=False) + # Day of the week on which the digest is sent (default: Tuesday). + digest_weekday = models.SmallIntegerField(default=1) + + send_welcome_emails = models.BooleanField(default=True) + message_content_allowed_in_email_notifications = models.BooleanField(default=True) + + mandatory_topics = models.BooleanField(default=False) + + name_changes_disabled = models.BooleanField(default=False) + email_changes_disabled = models.BooleanField(default=False) + avatar_changes_disabled = models.BooleanField(default=False) + + POLICY_MEMBERS_ONLY = 1 + POLICY_ADMINS_ONLY = 2 + POLICY_FULL_MEMBERS_ONLY = 3 + POLICY_MODERATORS_ONLY = 4 + POLICY_EVERYONE = 5 + POLICY_NOBODY = 6 + POLICY_OWNERS_ONLY = 7 + + COMMON_POLICY_TYPES = [ + POLICY_MEMBERS_ONLY, + POLICY_ADMINS_ONLY, + POLICY_FULL_MEMBERS_ONLY, + POLICY_MODERATORS_ONLY, + ] + + COMMON_MESSAGE_POLICY_TYPES = [ + POLICY_MEMBERS_ONLY, + POLICY_ADMINS_ONLY, + POLICY_FULL_MEMBERS_ONLY, + POLICY_MODERATORS_ONLY, + POLICY_EVERYONE, + ] + + INVITE_TO_REALM_POLICY_TYPES = [ + POLICY_MEMBERS_ONLY, + POLICY_ADMINS_ONLY, + POLICY_FULL_MEMBERS_ONLY, + POLICY_MODERATORS_ONLY, + POLICY_NOBODY, + ] + + # We don't allow granting roles less than Moderator access to + # create web-public streams, since it's a sensitive feature that + # can be used to send spam. + CREATE_WEB_PUBLIC_STREAM_POLICY_TYPES = [ + POLICY_ADMINS_ONLY, + POLICY_MODERATORS_ONLY, + POLICY_OWNERS_ONLY, + POLICY_NOBODY, + ] + + EDIT_TOPIC_POLICY_TYPES = [ + POLICY_MEMBERS_ONLY, + POLICY_ADMINS_ONLY, + POLICY_FULL_MEMBERS_ONLY, + POLICY_MODERATORS_ONLY, + POLICY_EVERYONE, + POLICY_NOBODY, + ] + + MOVE_MESSAGES_BETWEEN_STREAMS_POLICY_TYPES = INVITE_TO_REALM_POLICY_TYPES + + DEFAULT_MOVE_MESSAGE_LIMIT_SECONDS = 7 * SECONDS_PER_DAY + + move_messages_within_stream_limit_seconds = models.PositiveIntegerField( + default=DEFAULT_MOVE_MESSAGE_LIMIT_SECONDS, null=True + ) + + move_messages_between_streams_limit_seconds = models.PositiveIntegerField( + default=DEFAULT_MOVE_MESSAGE_LIMIT_SECONDS, null=True + ) + + # Who in the organization is allowed to add custom emojis. + add_custom_emoji_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) + + # Who in the organization is allowed to create streams. + create_public_stream_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) + create_private_stream_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) + create_web_public_stream_policy = models.PositiveSmallIntegerField(default=POLICY_OWNERS_ONLY) + + # Who in the organization is allowed to delete messages they themselves sent. + delete_own_message_policy = models.PositiveSmallIntegerField(default=POLICY_ADMINS_ONLY) + + # Who in the organization is allowed to edit topics of any message. + edit_topic_policy = models.PositiveSmallIntegerField(default=POLICY_EVERYONE) + + # Who in the organization is allowed to invite other users to organization. + invite_to_realm_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) + + # UserGroup whose members are allowed to create invite link. + create_multiuse_invite_group = models.ForeignKey( + "UserGroup", on_delete=models.RESTRICT, related_name="+" + ) + + # on_delete field here is set to RESTRICT because we don't want to allow + # deleting a user group in case it is referenced by this setting. + # We are not using PROTECT since we want to allow deletion of user groups + # when realm itself is deleted. + can_access_all_users_group = models.ForeignKey( + "UserGroup", on_delete=models.RESTRICT, related_name="+" + ) + + # Who in the organization is allowed to invite other users to streams. + invite_to_stream_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) + + # Who in the organization is allowed to move messages between streams. + move_messages_between_streams_policy = models.PositiveSmallIntegerField( + default=POLICY_ADMINS_ONLY + ) + + user_group_edit_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) + + PRIVATE_MESSAGE_POLICY_UNLIMITED = 1 + PRIVATE_MESSAGE_POLICY_DISABLED = 2 + private_message_policy = models.PositiveSmallIntegerField( + default=PRIVATE_MESSAGE_POLICY_UNLIMITED + ) + PRIVATE_MESSAGE_POLICY_TYPES = [ + PRIVATE_MESSAGE_POLICY_UNLIMITED, + PRIVATE_MESSAGE_POLICY_DISABLED, + ] + + # Global policy for who is allowed to use wildcard mentions in + # streams with a large number of subscribers. Anyone can use + # wildcard mentions in small streams regardless of this setting. + WILDCARD_MENTION_POLICY_EVERYONE = 1 + WILDCARD_MENTION_POLICY_MEMBERS = 2 + WILDCARD_MENTION_POLICY_FULL_MEMBERS = 3 + WILDCARD_MENTION_POLICY_ADMINS = 5 + WILDCARD_MENTION_POLICY_NOBODY = 6 + WILDCARD_MENTION_POLICY_MODERATORS = 7 + wildcard_mention_policy = models.PositiveSmallIntegerField( + default=WILDCARD_MENTION_POLICY_ADMINS, + ) + WILDCARD_MENTION_POLICY_TYPES = [ + WILDCARD_MENTION_POLICY_EVERYONE, + WILDCARD_MENTION_POLICY_MEMBERS, + WILDCARD_MENTION_POLICY_FULL_MEMBERS, + WILDCARD_MENTION_POLICY_ADMINS, + WILDCARD_MENTION_POLICY_NOBODY, + WILDCARD_MENTION_POLICY_MODERATORS, + ] + + # Threshold in days for new users to create streams, and potentially take + # some other actions. + waiting_period_threshold = models.PositiveIntegerField(default=0) + + DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS = ( + 600 # if changed, also change in admin.js, setting_org.js + ) + MESSAGE_TIME_LIMIT_SETTING_SPECIAL_VALUES_MAP = { + "unlimited": None, + } + message_content_delete_limit_seconds = models.PositiveIntegerField( + default=DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS, null=True + ) + + allow_message_editing = models.BooleanField(default=True) + DEFAULT_MESSAGE_CONTENT_EDIT_LIMIT_SECONDS = ( + 600 # if changed, also change in admin.js, setting_org.js + ) + message_content_edit_limit_seconds = models.PositiveIntegerField( + default=DEFAULT_MESSAGE_CONTENT_EDIT_LIMIT_SECONDS, null=True + ) + + # Whether users have access to message edit history + allow_edit_history = models.BooleanField(default=True) + + # Defaults for new users + default_language = models.CharField(default="en", max_length=MAX_LANGUAGE_ID_LENGTH) + + DEFAULT_NOTIFICATION_STREAM_NAME = "general" + INITIAL_PRIVATE_STREAM_NAME = "core team" + STREAM_EVENTS_NOTIFICATION_TOPIC = gettext_lazy("stream events") + notifications_stream = models.ForeignKey( + "Stream", + related_name="+", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + signup_notifications_stream = models.ForeignKey( + "Stream", + related_name="+", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + + MESSAGE_RETENTION_SPECIAL_VALUES_MAP = { + "unlimited": -1, + } + # For old messages being automatically deleted + message_retention_days = models.IntegerField(null=False, default=-1) + + # When non-null, all but the latest this many messages in the organization + # are inaccessible to users (but not deleted). + message_visibility_limit = models.IntegerField(null=True) + + # Messages older than this message ID in the organization are inaccessible. + first_visible_message_id = models.IntegerField(default=0) + + # Valid org types + ORG_TYPES: Dict[str, OrgTypeDict] = { + "unspecified": { + "name": "Unspecified", + "id": OrgTypeEnum.Unspecified.value, + "hidden": True, + "display_order": 0, + "onboarding_zulip_guide_url": None, + }, + "business": { + "name": "Business", + "id": OrgTypeEnum.Business.value, + "hidden": False, + "display_order": 1, + "onboarding_zulip_guide_url": "https://zulip.com/for/business/", + }, + "opensource": { + "name": "Open-source project", + "id": OrgTypeEnum.OpenSource.value, + "hidden": False, + "display_order": 2, + "onboarding_zulip_guide_url": "https://zulip.com/for/open-source/", + }, + "education_nonprofit": { + "name": "Education (non-profit)", + "id": OrgTypeEnum.EducationNonProfit.value, + "hidden": False, + "display_order": 3, + "onboarding_zulip_guide_url": "https://zulip.com/for/education/", + }, + "education": { + "name": "Education (for-profit)", + "id": OrgTypeEnum.Education.value, + "hidden": False, + "display_order": 4, + "onboarding_zulip_guide_url": "https://zulip.com/for/education/", + }, + "research": { + "name": "Research", + "id": OrgTypeEnum.Research.value, + "hidden": False, + "display_order": 5, + "onboarding_zulip_guide_url": "https://zulip.com/for/research/", + }, + "event": { + "name": "Event or conference", + "id": OrgTypeEnum.Event.value, + "hidden": False, + "display_order": 6, + "onboarding_zulip_guide_url": "https://zulip.com/for/events/", + }, + "nonprofit": { + "name": "Non-profit (registered)", + "id": OrgTypeEnum.NonProfit.value, + "hidden": False, + "display_order": 7, + "onboarding_zulip_guide_url": "https://zulip.com/for/communities/", + }, + "government": { + "name": "Government", + "id": OrgTypeEnum.Government.value, + "hidden": False, + "display_order": 8, + "onboarding_zulip_guide_url": None, + }, + "political_group": { + "name": "Political group", + "id": OrgTypeEnum.PoliticalGroup.value, + "hidden": False, + "display_order": 9, + "onboarding_zulip_guide_url": None, + }, + "community": { + "name": "Community", + "id": OrgTypeEnum.Community.value, + "hidden": False, + "display_order": 10, + "onboarding_zulip_guide_url": "https://zulip.com/for/communities/", + }, + "personal": { + "name": "Personal", + "id": OrgTypeEnum.Personal.value, + "hidden": False, + "display_order": 100, + "onboarding_zulip_guide_url": None, + }, + "other": { + "name": "Other", + "id": OrgTypeEnum.Other.value, + "hidden": False, + "display_order": 1000, + "onboarding_zulip_guide_url": None, + }, + } + + ORG_TYPE_IDS: List[int] = [t["id"] for t in ORG_TYPES.values()] + + org_type = models.PositiveSmallIntegerField( + default=ORG_TYPES["unspecified"]["id"], + choices=[(t["id"], t["name"]) for t in ORG_TYPES.values()], + ) + + UPGRADE_TEXT_STANDARD = gettext_lazy("Available on Zulip Cloud Standard. Upgrade to access.") + UPGRADE_TEXT_PLUS = gettext_lazy("Available on Zulip Cloud Plus. Upgrade to access.") + # plan_type controls various features around resource/feature + # limitations for a Zulip organization on multi-tenant installations + # like Zulip Cloud. + PLAN_TYPE_SELF_HOSTED = 1 + PLAN_TYPE_LIMITED = 2 + PLAN_TYPE_STANDARD = 3 + PLAN_TYPE_STANDARD_FREE = 4 + PLAN_TYPE_PLUS = 10 + + # Used for creating realms with different plan types. + ALL_PLAN_TYPES = { + PLAN_TYPE_SELF_HOSTED: "self-hosted-plan", + PLAN_TYPE_LIMITED: "limited-plan", + PLAN_TYPE_STANDARD: "standard-plan", + PLAN_TYPE_STANDARD_FREE: "standard-free-plan", + PLAN_TYPE_PLUS: "plus-plan", + } + plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_HOSTED) + + # This value is also being used in web/src/settings_bots.bot_creation_policy_values. + # On updating it here, update it there as well. + BOT_CREATION_EVERYONE = 1 + BOT_CREATION_LIMIT_GENERIC_BOTS = 2 + BOT_CREATION_ADMINS_ONLY = 3 + bot_creation_policy = models.PositiveSmallIntegerField(default=BOT_CREATION_EVERYONE) + BOT_CREATION_POLICY_TYPES = [ + BOT_CREATION_EVERYONE, + BOT_CREATION_LIMIT_GENERIC_BOTS, + BOT_CREATION_ADMINS_ONLY, + ] + + # See upload_quota_bytes; don't interpret upload_quota_gb directly. + UPLOAD_QUOTA_LIMITED = 5 + UPLOAD_QUOTA_STANDARD = 50 + upload_quota_gb = models.IntegerField(null=True) + + VIDEO_CHAT_PROVIDERS = { + "disabled": { + "name": "None", + "id": 0, + }, + "jitsi_meet": { + "name": "Jitsi Meet", + "id": 1, + }, + # ID 2 was used for the now-deleted Google Hangouts. + # ID 3 reserved for optional Zoom, see below. + # ID 4 reserved for optional BigBlueButton, see below. + } + + if settings.VIDEO_ZOOM_CLIENT_ID is not None and settings.VIDEO_ZOOM_CLIENT_SECRET is not None: + VIDEO_CHAT_PROVIDERS["zoom"] = { + "name": "Zoom", + "id": 3, + } + + if settings.BIG_BLUE_BUTTON_SECRET is not None and settings.BIG_BLUE_BUTTON_URL is not None: + VIDEO_CHAT_PROVIDERS["big_blue_button"] = {"name": "BigBlueButton", "id": 4} + + video_chat_provider = models.PositiveSmallIntegerField( + default=VIDEO_CHAT_PROVIDERS["jitsi_meet"]["id"] + ) + + JITSI_SERVER_SPECIAL_VALUES_MAP = {"default": None} + jitsi_server_url = models.URLField(null=True, default=None) + + # Please access this via get_giphy_rating_options. + GIPHY_RATING_OPTIONS = { + "disabled": { + "name": gettext_lazy("GIPHY integration disabled"), + "id": 0, + }, + # Source: https://github.com/Giphy/giphy-js/blob/master/packages/fetch-api/README.md#shared-options + "y": { + "name": gettext_lazy("Allow GIFs rated Y (Very young audience)"), + "id": 1, + }, + "g": { + "name": gettext_lazy("Allow GIFs rated G (General audience)"), + "id": 2, + }, + "pg": { + "name": gettext_lazy("Allow GIFs rated PG (Parental guidance)"), + "id": 3, + }, + "pg-13": { + "name": gettext_lazy("Allow GIFs rated PG-13 (Parental guidance - under 13)"), + "id": 4, + }, + "r": { + "name": gettext_lazy("Allow GIFs rated R (Restricted)"), + "id": 5, + }, + } + + # maximum rating of the GIFs that will be retrieved from GIPHY + giphy_rating = models.PositiveSmallIntegerField(default=GIPHY_RATING_OPTIONS["g"]["id"]) + + default_code_block_language = models.TextField(default="") + + # Whether read receipts are enabled in the organization. If disabled, + # they will not be available regardless of users' personal settings. + enable_read_receipts = models.BooleanField(default=False) + + # Whether clients should display "(guest)" after names of guest users. + enable_guest_user_indicator = models.BooleanField(default=True) + + # Define the types of the various automatically managed properties + property_types: Dict[str, Union[type, Tuple[type, ...]]] = dict( + add_custom_emoji_policy=int, + allow_edit_history=bool, + allow_message_editing=bool, + avatar_changes_disabled=bool, + bot_creation_policy=int, + create_private_stream_policy=int, + create_public_stream_policy=int, + create_web_public_stream_policy=int, + default_code_block_language=str, + default_language=str, + delete_own_message_policy=int, + description=str, + digest_emails_enabled=bool, + digest_weekday=int, + disallow_disposable_email_addresses=bool, + edit_topic_policy=int, + email_changes_disabled=bool, + emails_restricted_to_domains=bool, + enable_guest_user_indicator=bool, + enable_read_receipts=bool, + enable_spectator_access=bool, + giphy_rating=int, + inline_image_preview=bool, + inline_url_embed_preview=bool, + invite_required=bool, + invite_to_realm_policy=int, + invite_to_stream_policy=int, + jitsi_server_url=(str, type(None)), + mandatory_topics=bool, + message_content_allowed_in_email_notifications=bool, + message_content_edit_limit_seconds=(int, type(None)), + message_content_delete_limit_seconds=(int, type(None)), + move_messages_between_streams_limit_seconds=(int, type(None)), + move_messages_within_stream_limit_seconds=(int, type(None)), + message_retention_days=(int, type(None)), + move_messages_between_streams_policy=int, + name=str, + name_changes_disabled=bool, + private_message_policy=int, + push_notifications_enabled=bool, + send_welcome_emails=bool, + user_group_edit_policy=int, + video_chat_provider=int, + waiting_period_threshold=int, + want_advertise_in_communities_directory=bool, + wildcard_mention_policy=int, + ) + + REALM_PERMISSION_GROUP_SETTINGS: Dict[str, GroupPermissionSetting] = dict( + create_multiuse_invite_group=GroupPermissionSetting( + require_system_group=True, + allow_internet_group=False, + allow_owners_group=False, + allow_nobody_group=True, + allow_everyone_group=False, + default_group_name=SystemGroups.ADMINISTRATORS, + id_field_name="create_multiuse_invite_group_id", + ), + can_access_all_users_group=GroupPermissionSetting( + require_system_group=True, + allow_internet_group=False, + allow_owners_group=False, + allow_nobody_group=False, + allow_everyone_group=True, + default_group_name=SystemGroups.EVERYONE, + id_field_name="can_access_all_users_group_id", + allowed_system_groups=[SystemGroups.EVERYONE, SystemGroups.MEMBERS], + ), + ) + + DIGEST_WEEKDAY_VALUES = [0, 1, 2, 3, 4, 5, 6] + + # Icon is the square mobile icon. + ICON_FROM_GRAVATAR = "G" + ICON_UPLOADED = "U" + ICON_SOURCES = ( + (ICON_FROM_GRAVATAR, "Hosted by Gravatar"), + (ICON_UPLOADED, "Uploaded by administrator"), + ) + icon_source = models.CharField( + default=ICON_FROM_GRAVATAR, + choices=ICON_SOURCES, + max_length=1, + ) + icon_version = models.PositiveSmallIntegerField(default=1) + + # Logo is the horizontal logo we show in top-left of web app navbar UI. + LOGO_DEFAULT = "D" + LOGO_UPLOADED = "U" + LOGO_SOURCES = ( + (LOGO_DEFAULT, "Default to Zulip"), + (LOGO_UPLOADED, "Uploaded by administrator"), + ) + logo_source = models.CharField( + default=LOGO_DEFAULT, + choices=LOGO_SOURCES, + max_length=1, + ) + logo_version = models.PositiveSmallIntegerField(default=1) + + night_logo_source = models.CharField( + default=LOGO_DEFAULT, + choices=LOGO_SOURCES, + max_length=1, + ) + night_logo_version = models.PositiveSmallIntegerField(default=1) + + @override + def __str__(self) -> str: + return f"{self.string_id} {self.id}" + + def get_giphy_rating_options(self) -> Dict[str, Dict[str, object]]: + """Wrapper function for GIPHY_RATING_OPTIONS that ensures evaluation + of the lazily evaluated `name` field without modifying the original.""" + return { + rating_type: {"name": str(rating["name"]), "id": rating["id"]} + for rating_type, rating in self.GIPHY_RATING_OPTIONS.items() + } + + def authentication_methods_dict(self) -> Dict[str, bool]: + """Returns the mapping from authentication flags to their status, + showing only those authentication flags that are supported on + the current server (i.e. if EmailAuthBackend is not configured + on the server, this will not return an entry for "Email").""" + # This mapping needs to be imported from here due to the cyclic + # dependency. + from zproject.backends import AUTH_BACKEND_NAME_MAP, all_implemented_backend_names + + ret: Dict[str, bool] = {} + supported_backends = [type(backend) for backend in supported_auth_backends()] + + for backend_name in all_implemented_backend_names(): + backend_class = AUTH_BACKEND_NAME_MAP[backend_name] + if backend_class in supported_backends: + ret[backend_name] = False + for realm_authentication_method in RealmAuthenticationMethod.objects.filter( + realm_id=self.id + ): + backend_class = AUTH_BACKEND_NAME_MAP[realm_authentication_method.name] + if backend_class in supported_backends: + ret[realm_authentication_method.name] = True + return ret + + def get_admin_users_and_bots( + self, include_realm_owners: bool = True + ) -> QuerySet["UserProfile"]: + """Use this in contexts where we want administrative users as well as + bots with administrator privileges, like send_event calls for + notifications to all administrator users. + """ + if include_realm_owners: + roles = [UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER] + else: + roles = [UserProfile.ROLE_REALM_ADMINISTRATOR] + + return UserProfile.objects.filter( + realm=self, + is_active=True, + role__in=roles, + ) + + def get_human_admin_users(self, include_realm_owners: bool = True) -> QuerySet["UserProfile"]: + """Use this in contexts where we want only human users with + administrative privileges, like sending an email to all of a + realm's administrators (bots don't have real email addresses). + """ + if include_realm_owners: + roles = [UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER] + else: + roles = [UserProfile.ROLE_REALM_ADMINISTRATOR] + + return UserProfile.objects.filter( + realm=self, + is_bot=False, + is_active=True, + role__in=roles, + ) + + def get_human_billing_admin_and_realm_owner_users(self) -> QuerySet["UserProfile"]: + return UserProfile.objects.filter( + Q(role=UserProfile.ROLE_REALM_OWNER) | Q(is_billing_admin=True), + realm=self, + is_bot=False, + is_active=True, + ) + + def get_active_users(self) -> QuerySet["UserProfile"]: + return UserProfile.objects.filter(realm=self, is_active=True) + + def get_first_human_user(self) -> Optional["UserProfile"]: + """A useful value for communications with newly created realms. + Has a few fundamental limitations: + + * Its value will be effectively random for realms imported from Slack or + other third-party tools. + * The user may be deactivated, etc., so it's not something that's useful + for features, permissions, etc. + """ + return UserProfile.objects.filter(realm=self, is_bot=False).order_by("id").first() + + def get_human_owner_users(self) -> QuerySet["UserProfile"]: + return UserProfile.objects.filter( + realm=self, is_bot=False, role=UserProfile.ROLE_REALM_OWNER, is_active=True + ) + + def get_bot_domain(self) -> str: + return get_fake_email_domain(self.host) + + def get_notifications_stream(self) -> Optional["Stream"]: + if self.notifications_stream is not None and not self.notifications_stream.deactivated: + return self.notifications_stream + return None + + def get_signup_notifications_stream(self) -> Optional["Stream"]: + if ( + self.signup_notifications_stream is not None + and not self.signup_notifications_stream.deactivated + ): + return self.signup_notifications_stream + return None + + @property + def max_invites(self) -> int: + if self._max_invites is None: + return settings.INVITES_DEFAULT_REALM_DAILY_MAX + return self._max_invites + + @max_invites.setter + def max_invites(self, value: Optional[int]) -> None: + self._max_invites = value + + def upload_quota_bytes(self) -> Optional[int]: + if self.upload_quota_gb is None: + return None + # We describe the quota to users in "GB" or "gigabytes", but actually apply + # it as gibibytes (GiB) to be a bit more generous in case of confusion. + return self.upload_quota_gb << 30 + + # `realm` instead of `self` here to make sure the parameters of the cache key + # function matches the original method. + @cache_with_key( + lambda realm: get_realm_used_upload_space_cache_key(realm.id), timeout=3600 * 24 * 7 + ) + def currently_used_upload_space_bytes(realm) -> int: # noqa: N805 + from zerver.models import Attachment + + used_space = Attachment.objects.filter(realm=realm).aggregate(Sum("size"))["size__sum"] + if used_space is None: + return 0 + return used_space + + def ensure_not_on_limited_plan(self) -> None: + if self.plan_type == Realm.PLAN_TYPE_LIMITED: + raise JsonableError(str(self.UPGRADE_TEXT_STANDARD)) + + def can_enable_restricted_user_access_for_guests(self) -> None: + if self.plan_type not in [Realm.PLAN_TYPE_PLUS, Realm.PLAN_TYPE_SELF_HOSTED]: + raise JsonableError(str(self.UPGRADE_TEXT_PLUS)) + + @property + def subdomain(self) -> str: + return self.string_id + + @property + def display_subdomain(self) -> str: + """Likely to be temporary function to avoid signup messages being sent + to an empty topic""" + if self.string_id == "": + return "." + return self.string_id + + @property + def uri(self) -> str: + return settings.EXTERNAL_URI_SCHEME + self.host + + @property + def host(self) -> str: + # Use mark sanitized to prevent false positives from Pysa thinking that + # the host is user controlled. + return mark_sanitized(self.host_for_subdomain(self.subdomain)) + + @staticmethod + def host_for_subdomain(subdomain: str) -> str: + if subdomain == Realm.SUBDOMAIN_FOR_ROOT_DOMAIN: + return settings.EXTERNAL_HOST + default_host = f"{subdomain}.{settings.EXTERNAL_HOST}" + return settings.REALM_HOSTS.get(subdomain, default_host) + + @property + def is_zephyr_mirror_realm(self) -> bool: + return self.string_id == "zephyr" + + @property + def webathena_enabled(self) -> bool: + return self.is_zephyr_mirror_realm + + @property + def presence_disabled(self) -> bool: + return self.is_zephyr_mirror_realm + + def web_public_streams_enabled(self) -> bool: + if not settings.WEB_PUBLIC_STREAMS_ENABLED: + # To help protect against accidentally web-public streams in + # self-hosted servers, we require the feature to be enabled at + # the server level before it is available to users. + return False + + if self.plan_type == Realm.PLAN_TYPE_LIMITED: + # In Zulip Cloud, we also require a paid or sponsored + # plan, to protect against the spam/abuse attacks that + # target every open Internet service that can host files. + return False + + if not self.enable_spectator_access: + return False + + return True + + def has_web_public_streams(self) -> bool: + if not self.web_public_streams_enabled(): + return False + + from zerver.lib.streams import get_web_public_streams_queryset + + return get_web_public_streams_queryset(self).exists() + + def allow_web_public_streams_access(self) -> bool: + """ + If any of the streams in the realm is web + public and `enable_spectator_access` and + settings.WEB_PUBLIC_STREAMS_ENABLED is True, + then the Realm is web-public. + """ + return self.has_web_public_streams() + + +post_save.connect(flush_realm, sender=Realm) + + +# We register realm cache flushing in a duplicate way to be run both +# pre_delete and post_delete on purpose: +# 1. pre_delete is needed because flush_realm wants to flush the UserProfile caches, +# and UserProfile objects are deleted via on_delete=CASCADE before the post_delete handler +# is called, which results in the `flush_realm` logic not having access to the details +# for the deleted users if called at that time. +# 2. post_delete is run as a precaution to reduce the risk of races where items might be +# added to the cache after the pre_delete handler but before the save. +# Note that it does not eliminate this risk, not least because it only flushes +# the realm cache, and not the user caches, for the reasons explained above. +def realm_pre_and_post_delete_handler(*, instance: Realm, **kwargs: object) -> None: + # This would be better as a functools.partial, but for some reason + # Django doesn't call it even when it's registered as a post_delete handler. + flush_realm(instance=instance, from_deletion=True) + + +pre_delete.connect(realm_pre_and_post_delete_handler, sender=Realm) +post_delete.connect(realm_pre_and_post_delete_handler, sender=Realm) + + +def get_realm(string_id: str) -> Realm: + return Realm.objects.get(string_id=string_id) + + +def get_realm_by_id(realm_id: int) -> Realm: + return Realm.objects.get(id=realm_id) + + +def name_changes_disabled(realm: Optional[Realm]) -> bool: + if realm is None: + return settings.NAME_CHANGES_DISABLED + return settings.NAME_CHANGES_DISABLED or realm.name_changes_disabled + + +def avatar_changes_disabled(realm: Realm) -> bool: + return settings.AVATAR_CHANGES_DISABLED or realm.avatar_changes_disabled + + +def get_org_type_display_name(org_type: int) -> str: + for realm_type_details in Realm.ORG_TYPES.values(): + if realm_type_details["id"] == org_type: + return realm_type_details["name"] + + return "" + + +class RealmDomain(models.Model): + """For an organization with emails_restricted_to_domains enabled, the list of + allowed domains""" + + realm = models.ForeignKey(Realm, on_delete=CASCADE) + # should always be stored lowercase + domain = models.CharField(max_length=80, db_index=True) + allow_subdomains = models.BooleanField(default=False) + + class Meta: + unique_together = ("realm", "domain") + + +class DomainNotAllowedForRealmError(Exception): + pass + + +class DisposableEmailError(Exception): + pass + + +class EmailContainsPlusError(Exception): + pass + + +class RealmDomainDict(TypedDict): + domain: str + allow_subdomains: bool + + +def get_realm_domains(realm: Realm) -> List[RealmDomainDict]: + return list(realm.realmdomain_set.values("domain", "allow_subdomains")) + + +class InvalidFakeEmailDomainError(Exception): + pass + + +def get_fake_email_domain(realm_host: str) -> str: + try: + # Check that realm.host can be used to form valid email addresses. + validate_email(Address(username="bot", domain=realm_host).addr_spec) + return realm_host + except ValidationError: + pass + + try: + # Check that the fake email domain can be used to form valid email addresses. + validate_email(Address(username="bot", domain=settings.FAKE_EMAIL_DOMAIN).addr_spec) + except ValidationError: + raise InvalidFakeEmailDomainError( + settings.FAKE_EMAIL_DOMAIN + " is not a valid domain. " + "Consider setting the FAKE_EMAIL_DOMAIN setting." + ) + + return settings.FAKE_EMAIL_DOMAIN diff --git a/zerver/models/recipients.py b/zerver/models/recipients.py new file mode 100644 index 0000000000000..28ba2f1a29f9e --- /dev/null +++ b/zerver/models/recipients.py @@ -0,0 +1,167 @@ +import hashlib +from collections import defaultdict +from typing import TYPE_CHECKING, Dict, List, Set + +from django.db import models, transaction +from django_stubs_ext import ValuesQuerySet +from typing_extensions import override + +from zerver.lib.display_recipient import get_display_recipient + +if TYPE_CHECKING: + from zerver.models import Subscription + + +class Recipient(models.Model): + """Represents an audience that can potentially receive messages in Zulip. + + This table essentially functions as a generic foreign key that + allows Message.recipient_id to be a simple ForeignKey representing + the audience for a message, while supporting the different types + of audiences Zulip supports for a message. + + Recipient has just two attributes: The enum type, and a type_id, + which is the ID of the UserProfile/Stream/Huddle object containing + all the metadata for the audience. There are 3 recipient types: + + 1. 1:1 direct message: The type_id is the ID of the UserProfile + who will receive any message to this Recipient. The sender + of such a message is represented separately. + 2. Stream message: The type_id is the ID of the associated Stream. + 3. Group direct message: In Zulip, group direct messages are + represented by Huddle objects, which encode the set of users + in the conversation. The type_id is the ID of the associated Huddle + object; the set of users is usually retrieved via the Subscription + table. See the Huddle model for details. + + See also the Subscription model, which stores which UserProfile + objects are subscribed to which Recipient objects. + """ + + type_id = models.IntegerField(db_index=True) + type = models.PositiveSmallIntegerField(db_index=True) + # Valid types are {personal, stream, huddle} + + # The type for 1:1 direct messages. + PERSONAL = 1 + # The type for stream messages. + STREAM = 2 + # The type group direct messages. + HUDDLE = 3 + + class Meta: + unique_together = ("type", "type_id") + + # N.B. If we used Django's choice=... we would get this for free (kinda) + _type_names = {PERSONAL: "personal", STREAM: "stream", HUDDLE: "huddle"} + + @override + def __str__(self) -> str: + return f"{self.label()} ({self.type_id}, {self.type})" + + def label(self) -> str: + from zerver.models import Stream + + if self.type == Recipient.STREAM: + return Stream.objects.get(id=self.type_id).name + else: + return str(get_display_recipient(self)) + + def type_name(self) -> str: + # Raises KeyError if invalid + return self._type_names[self.type] + + +def get_huddle_user_ids(recipient: Recipient) -> ValuesQuerySet["Subscription", int]: + from zerver.models import Subscription + + assert recipient.type == Recipient.HUDDLE + + return ( + Subscription.objects.filter( + recipient=recipient, + ) + .order_by("user_profile_id") + .values_list("user_profile_id", flat=True) + ) + + +def bulk_get_huddle_user_ids(recipient_ids: List[int]) -> Dict[int, Set[int]]: + """ + Takes a list of huddle-type recipient_ids, returns a dict + mapping recipient id to list of user ids in the huddle. + + We rely on our caller to pass us recipient_ids that correspond + to huddles, but technically this function is valid for any type + of subscription. + """ + from zerver.models import Subscription + + if not recipient_ids: + return {} + + subscriptions = Subscription.objects.filter( + recipient_id__in=recipient_ids, + ).only("user_profile_id", "recipient_id") + + result_dict: Dict[int, Set[int]] = defaultdict(set) + for subscription in subscriptions: + result_dict[subscription.recipient_id].add(subscription.user_profile_id) + + return result_dict + + +class Huddle(models.Model): + """ + Represents a group of individuals who may have a + group direct message conversation together. + + The membership of the Huddle is stored in the Subscription table just like with + Streams - for each user in the Huddle, there is a Subscription object + tied to the UserProfile and the Huddle's recipient object. + + A hash of the list of user IDs is stored in the huddle_hash field + below, to support efficiently mapping from a set of users to the + corresponding Huddle object. + """ + + # TODO: We should consider whether using + # CommaSeparatedIntegerField would be better. + huddle_hash = models.CharField(max_length=40, db_index=True, unique=True) + # Foreign key to the Recipient object for this Huddle. + recipient = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) + + +def get_huddle_hash(id_list: List[int]) -> str: + id_list = sorted(set(id_list)) + hash_key = ",".join(str(x) for x in id_list) + return hashlib.sha1(hash_key.encode()).hexdigest() + + +def get_or_create_huddle(id_list: List[int]) -> Huddle: + """ + Takes a list of user IDs and returns the Huddle object for the + group consisting of these users. If the Huddle object does not + yet exist, it will be transparently created. + """ + from zerver.models import Subscription, UserProfile + + huddle_hash = get_huddle_hash(id_list) + with transaction.atomic(): + (huddle, created) = Huddle.objects.get_or_create(huddle_hash=huddle_hash) + if created: + recipient = Recipient.objects.create(type_id=huddle.id, type=Recipient.HUDDLE) + huddle.recipient = recipient + huddle.save(update_fields=["recipient"]) + subs_to_create = [ + Subscription( + recipient=recipient, + user_profile_id=user_profile_id, + is_user_active=is_active, + ) + for user_profile_id, is_active in UserProfile.objects.filter(id__in=id_list) + .distinct("id") + .values_list("id", "is_active") + ] + Subscription.objects.bulk_create(subs_to_create) + return huddle diff --git a/zerver/models/scheduled_jobs.py b/zerver/models/scheduled_jobs.py new file mode 100644 index 0000000000000..18d84a46683a2 --- /dev/null +++ b/zerver/models/scheduled_jobs.py @@ -0,0 +1,255 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + +from typing import List, TypedDict, Union + +from django.conf import settings +from django.db import models +from django.db.models import CASCADE, Q +from django.utils.timezone import now as timezone_now +from typing_extensions import override + +from zerver.lib.display_recipient import get_recipient_ids +from zerver.lib.timestamp import datetime_to_timestamp +from zerver.models.clients import Client +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.groups import UserGroup +from zerver.models.messages import Message +from zerver.models.realms import Realm +from zerver.models.recipients import Recipient +from zerver.models.streams import Stream +from zerver.models.users import UserProfile + + +class AbstractScheduledJob(models.Model): + scheduled_timestamp = models.DateTimeField(db_index=True) + # JSON representation of arguments to consumer + data = models.TextField() + realm = models.ForeignKey(Realm, on_delete=CASCADE) + + class Meta: + abstract = True + + +class ScheduledEmail(AbstractScheduledJob): + # Exactly one of users or address should be set. These are + # duplicate values, used to efficiently filter the set of + # ScheduledEmails for use in clear_scheduled_emails; the + # recipients used for actually sending messages are stored in the + # data field of AbstractScheduledJob. + users = models.ManyToManyField(UserProfile) + # Just the address part of a full "name
      " email address + address = models.EmailField(null=True, db_index=True) + + # Valid types are below + WELCOME = 1 + DIGEST = 2 + INVITATION_REMINDER = 3 + type = models.PositiveSmallIntegerField() + + @override + def __str__(self) -> str: + return f"{self.type} {self.address or list(self.users.all())} {self.scheduled_timestamp}" + + +class MissedMessageEmailAddress(models.Model): + message = models.ForeignKey(Message, on_delete=CASCADE) + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + email_token = models.CharField(max_length=34, unique=True, db_index=True) + + # Timestamp of when the missed message address generated. + timestamp = models.DateTimeField(db_index=True, default=timezone_now) + # Number of times the missed message address has been used. + times_used = models.PositiveIntegerField(default=0, db_index=True) + + @override + def __str__(self) -> str: + return settings.EMAIL_GATEWAY_PATTERN % (self.email_token,) + + def increment_times_used(self) -> None: + self.times_used += 1 + self.save(update_fields=["times_used"]) + + +class NotificationTriggers: + # "direct_message" is for 1:1 direct messages as well as huddles + DIRECT_MESSAGE = "direct_message" + MENTION = "mentioned" + TOPIC_WILDCARD_MENTION = "topic_wildcard_mentioned" + STREAM_WILDCARD_MENTION = "stream_wildcard_mentioned" + STREAM_PUSH = "stream_push_notify" + STREAM_EMAIL = "stream_email_notify" + FOLLOWED_TOPIC_PUSH = "followed_topic_push_notify" + FOLLOWED_TOPIC_EMAIL = "followed_topic_email_notify" + TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC = "topic_wildcard_mentioned_in_followed_topic" + STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC = "stream_wildcard_mentioned_in_followed_topic" + + +class ScheduledMessageNotificationEmail(models.Model): + """Stores planned outgoing message notification emails. They may be + processed earlier should Zulip choose to batch multiple messages + in a single email, but typically will be processed just after + scheduled_timestamp. + """ + + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + message = models.ForeignKey(Message, on_delete=CASCADE) + + EMAIL_NOTIFICATION_TRIGGER_CHOICES = [ + (NotificationTriggers.DIRECT_MESSAGE, "Direct message"), + (NotificationTriggers.MENTION, "Mention"), + (NotificationTriggers.TOPIC_WILDCARD_MENTION, "Topic wildcard mention"), + (NotificationTriggers.STREAM_WILDCARD_MENTION, "Stream wildcard mention"), + (NotificationTriggers.STREAM_EMAIL, "Stream notifications enabled"), + (NotificationTriggers.FOLLOWED_TOPIC_EMAIL, "Followed topic notifications enabled"), + ( + NotificationTriggers.TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC, + "Topic wildcard mention in followed topic", + ), + ( + NotificationTriggers.STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC, + "Stream wildcard mention in followed topic", + ), + ] + + trigger = models.TextField(choices=EMAIL_NOTIFICATION_TRIGGER_CHOICES) + mentioned_user_group = models.ForeignKey(UserGroup, null=True, on_delete=CASCADE) + + # Timestamp for when the notification should be processed and sent. + # Calculated from the time the event was received and the batching period. + scheduled_timestamp = models.DateTimeField(db_index=True) + + +class APIScheduledStreamMessageDict(TypedDict): + scheduled_message_id: int + to: int + type: str + content: str + rendered_content: str + topic: str + scheduled_delivery_timestamp: int + failed: bool + + +class APIScheduledDirectMessageDict(TypedDict): + scheduled_message_id: int + to: List[int] + type: str + content: str + rendered_content: str + scheduled_delivery_timestamp: int + failed: bool + + +class ScheduledMessage(models.Model): + sender = models.ForeignKey(UserProfile, on_delete=CASCADE) + recipient = models.ForeignKey(Recipient, on_delete=CASCADE) + subject = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH) + content = models.TextField() + rendered_content = models.TextField() + sending_client = models.ForeignKey(Client, on_delete=CASCADE) + stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE) + realm = models.ForeignKey(Realm, on_delete=CASCADE) + scheduled_timestamp = models.DateTimeField(db_index=True) + read_by_sender = models.BooleanField() + delivered = models.BooleanField(default=False) + delivered_message = models.ForeignKey(Message, null=True, on_delete=CASCADE) + has_attachment = models.BooleanField(default=False, db_index=True) + + # Metadata for messages that failed to send when their scheduled + # moment arrived. + failed = models.BooleanField(default=False) + failure_message = models.TextField(null=True) + + SEND_LATER = 1 + REMIND = 2 + + DELIVERY_TYPES = ( + (SEND_LATER, "send_later"), + (REMIND, "remind"), + ) + + delivery_type = models.PositiveSmallIntegerField( + choices=DELIVERY_TYPES, + default=SEND_LATER, + ) + + class Meta: + indexes = [ + # We expect a large number of delivered scheduled messages + # to accumulate over time. This first index is for the + # deliver_scheduled_messages worker. + models.Index( + name="zerver_unsent_scheduled_messages_by_time", + fields=["scheduled_timestamp"], + condition=Q( + delivered=False, + failed=False, + ), + ), + # This index is for displaying scheduled messages to the + # user themself via the API; we don't filter failed + # messages since we will want to display those so that + # failures don't just disappear into a black hole. + models.Index( + name="zerver_realm_unsent_scheduled_messages_by_user", + fields=["realm_id", "sender", "delivery_type", "scheduled_timestamp"], + condition=Q( + delivered=False, + ), + ), + ] + + @override + def __str__(self) -> str: + return f"{self.recipient.label()} {self.subject} {self.sender!r} {self.scheduled_timestamp}" + + def topic_name(self) -> str: + return self.subject + + def set_topic_name(self, topic_name: str) -> None: + self.subject = topic_name + + def is_stream_message(self) -> bool: + return self.recipient.type == Recipient.STREAM + + def to_dict(self) -> Union[APIScheduledStreamMessageDict, APIScheduledDirectMessageDict]: + recipient, recipient_type_str = get_recipient_ids(self.recipient, self.sender.id) + + if recipient_type_str == "private": + # The topic for direct messages should always be an empty string. + assert self.topic_name() == "" + + return APIScheduledDirectMessageDict( + scheduled_message_id=self.id, + to=recipient, + type=recipient_type_str, + content=self.content, + rendered_content=self.rendered_content, + scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp), + failed=self.failed, + ) + + # The recipient for stream messages should always just be the unique stream ID. + assert len(recipient) == 1 + + return APIScheduledStreamMessageDict( + scheduled_message_id=self.id, + to=recipient[0], + type=recipient_type_str, + content=self.content, + rendered_content=self.rendered_content, + topic=self.topic_name(), + scheduled_delivery_timestamp=datetime_to_timestamp(self.scheduled_timestamp), + failed=self.failed, + ) + + +EMAIL_TYPES = { + "account_registered": ScheduledEmail.WELCOME, + "onboarding_zulip_topics": ScheduledEmail.WELCOME, + "onboarding_zulip_guide": ScheduledEmail.WELCOME, + "onboarding_team_to_zulip": ScheduledEmail.WELCOME, + "digest": ScheduledEmail.DIGEST, + "invitation_reminder": ScheduledEmail.INVITATION_REMINDER, +} diff --git a/zerver/models/streams.py b/zerver/models/streams.py new file mode 100644 index 0000000000000..edf5bc8115c2b --- /dev/null +++ b/zerver/models/streams.py @@ -0,0 +1,387 @@ +import secrets +from typing import Any, Dict, Set + +from django.db import models +from django.db.models import CASCADE, Q, QuerySet +from django.db.models.functions import Upper +from django.db.models.signals import post_delete, post_save +from django.utils.timezone import now as timezone_now +from django.utils.translation import gettext_lazy +from django_stubs_ext import StrPromise +from typing_extensions import override + +from zerver.lib.cache import flush_stream +from zerver.lib.timestamp import datetime_to_timestamp +from zerver.lib.types import DefaultStreamDict, GroupPermissionSetting +from zerver.models.groups import SystemGroups, UserGroup +from zerver.models.realms import Realm +from zerver.models.recipients import Recipient +from zerver.models.users import UserProfile + + +def generate_email_token_for_stream() -> str: + return secrets.token_hex(16) + + +class Stream(models.Model): + MAX_NAME_LENGTH = 60 + MAX_DESCRIPTION_LENGTH = 1024 + + name = models.CharField(max_length=MAX_NAME_LENGTH, db_index=True) + realm = models.ForeignKey(Realm, db_index=True, on_delete=CASCADE) + date_created = models.DateTimeField(default=timezone_now) + deactivated = models.BooleanField(default=False) + description = models.CharField(max_length=MAX_DESCRIPTION_LENGTH, default="") + rendered_description = models.TextField(default="") + + # Foreign key to the Recipient object for STREAM type messages to this stream. + recipient = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) + + # Various permission policy configurations + PERMISSION_POLICIES: Dict[str, Dict[str, Any]] = { + "web_public": { + "invite_only": False, + "history_public_to_subscribers": True, + "is_web_public": True, + "policy_name": gettext_lazy("Web-public"), + }, + "public": { + "invite_only": False, + "history_public_to_subscribers": True, + "is_web_public": False, + "policy_name": gettext_lazy("Public"), + }, + "private_shared_history": { + "invite_only": True, + "history_public_to_subscribers": True, + "is_web_public": False, + "policy_name": gettext_lazy("Private, shared history"), + }, + "private_protected_history": { + "invite_only": True, + "history_public_to_subscribers": False, + "is_web_public": False, + "policy_name": gettext_lazy("Private, protected history"), + }, + # Public streams with protected history are currently only + # available in Zephyr realms + "public_protected_history": { + "invite_only": False, + "history_public_to_subscribers": False, + "is_web_public": False, + "policy_name": gettext_lazy("Public, protected history"), + }, + } + invite_only = models.BooleanField(default=False) + history_public_to_subscribers = models.BooleanField(default=True) + + # Whether this stream's content should be published by the web-public archive features + is_web_public = models.BooleanField(default=False) + + STREAM_POST_POLICY_EVERYONE = 1 + STREAM_POST_POLICY_ADMINS = 2 + STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS = 3 + STREAM_POST_POLICY_MODERATORS = 4 + # TODO: Implement policy to restrict posting to a user group or admins. + + # Who in the organization has permission to send messages to this stream. + stream_post_policy = models.PositiveSmallIntegerField(default=STREAM_POST_POLICY_EVERYONE) + POST_POLICIES: Dict[int, StrPromise] = { + # These strings should match the strings in the + # stream_post_policy_values object in stream_data.js. + STREAM_POST_POLICY_EVERYONE: gettext_lazy("All stream members can post"), + STREAM_POST_POLICY_ADMINS: gettext_lazy("Only organization administrators can post"), + STREAM_POST_POLICY_MODERATORS: gettext_lazy( + "Only organization administrators and moderators can post" + ), + STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS: gettext_lazy( + "Only organization full members can post" + ), + } + STREAM_POST_POLICY_TYPES = list(POST_POLICIES.keys()) + + # The unique thing about Zephyr public streams is that we never list their + # users. We may try to generalize this concept later, but for now + # we just use a concrete field. (Zephyr public streams aren't exactly like + # invite-only streams--while both are private in terms of listing users, + # for Zephyr we don't even list users to stream members, yet membership + # is more public in the sense that you don't need a Zulip invite to join. + # This field is populated directly from UserProfile.is_zephyr_mirror_realm, + # and the reason for denormalizing field is performance. + is_in_zephyr_realm = models.BooleanField(default=False) + + # Used by the e-mail forwarder. The e-mail RFC specifies a maximum + # e-mail length of 254, and our max stream length is 30, so we + # have plenty of room for the token. + email_token = models.CharField( + max_length=32, + default=generate_email_token_for_stream, + unique=True, + ) + + # For old messages being automatically deleted. + # Value NULL means "use retention policy of the realm". + # Value -1 means "disable retention policy for this stream unconditionally". + # Non-negative values have the natural meaning of "archive messages older than days". + MESSAGE_RETENTION_SPECIAL_VALUES_MAP = { + "unlimited": -1, + "realm_default": None, + } + message_retention_days = models.IntegerField(null=True, default=None) + + # on_delete field here is set to RESTRICT because we don't want to allow + # deleting a user group in case it is referenced by this setting. + # We are not using PROTECT since we want to allow deletion of user groups + # when realm itself is deleted. + can_remove_subscribers_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT) + + # The very first message ID in the stream. Used to help clients + # determine whether they might need to display "more topics" for a + # stream based on what messages they have cached. + first_message_id = models.IntegerField(null=True, db_index=True) + + stream_permission_group_settings = { + "can_remove_subscribers_group": GroupPermissionSetting( + require_system_group=True, + allow_internet_group=False, + allow_owners_group=False, + allow_nobody_group=False, + allow_everyone_group=True, + default_group_name=SystemGroups.ADMINISTRATORS, + id_field_name="can_remove_subscribers_group_id", + ), + } + + class Meta: + indexes = [ + models.Index(Upper("name"), name="upper_stream_name_idx"), + ] + + @override + def __str__(self) -> str: + return self.name + + def is_public(self) -> bool: + # All streams are private in Zephyr mirroring realms. + return not self.invite_only and not self.is_in_zephyr_realm + + def is_history_realm_public(self) -> bool: + return self.is_public() + + def is_history_public_to_subscribers(self) -> bool: + return self.history_public_to_subscribers + + # Stream fields included whenever a Stream object is provided to + # Zulip clients via the API. A few details worth noting: + # * "id" is represented as "stream_id" in most API interfaces. + # * "email_token" is not realm-public and thus is not included here. + # * is_in_zephyr_realm is a backend-only optimization. + # * "deactivated" streams are filtered from the API entirely. + # * "realm" and "recipient" are not exposed to clients via the API. + API_FIELDS = [ + "date_created", + "description", + "first_message_id", + "history_public_to_subscribers", + "id", + "invite_only", + "is_web_public", + "message_retention_days", + "name", + "rendered_description", + "stream_post_policy", + "can_remove_subscribers_group_id", + ] + + def to_dict(self) -> DefaultStreamDict: + return DefaultStreamDict( + can_remove_subscribers_group=self.can_remove_subscribers_group_id, + date_created=datetime_to_timestamp(self.date_created), + description=self.description, + first_message_id=self.first_message_id, + history_public_to_subscribers=self.history_public_to_subscribers, + invite_only=self.invite_only, + is_web_public=self.is_web_public, + message_retention_days=self.message_retention_days, + name=self.name, + rendered_description=self.rendered_description, + stream_id=self.id, + stream_post_policy=self.stream_post_policy, + is_announcement_only=self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS, + ) + + +post_save.connect(flush_stream, sender=Stream) +post_delete.connect(flush_stream, sender=Stream) + + +def get_realm_stream(stream_name: str, realm_id: int) -> Stream: + return Stream.objects.get(name__iexact=stream_name.strip(), realm_id=realm_id) + + +def get_active_streams(realm: Realm) -> QuerySet[Stream]: + """ + Return all streams (including invite-only streams) that have not been deactivated. + """ + return Stream.objects.filter(realm=realm, deactivated=False) + + +def get_linkable_streams(realm_id: int) -> QuerySet[Stream]: + """ + This returns the streams that we are allowed to linkify using + something like "#frontend" in our markup. For now the business + rule is that you can link any stream in the realm that hasn't + been deactivated (similar to how get_active_streams works). + """ + return Stream.objects.filter(realm_id=realm_id, deactivated=False) + + +def get_stream(stream_name: str, realm: Realm) -> Stream: + """ + Callers that don't have a Realm object already available should use + get_realm_stream directly, to avoid unnecessarily fetching the + Realm object. + """ + return get_realm_stream(stream_name, realm.id) + + +def get_stream_by_id_in_realm(stream_id: int, realm: Realm) -> Stream: + return Stream.objects.select_related("realm", "recipient").get(id=stream_id, realm=realm) + + +def bulk_get_streams(realm: Realm, stream_names: Set[str]) -> Dict[str, Any]: + def fetch_streams_by_name(stream_names: Set[str]) -> QuerySet[Stream]: + # + # This should be just + # + # Stream.objects.select_related().filter(name__iexact__in=stream_names, + # realm_id=realm_id) + # + # But chaining __in and __iexact doesn't work with Django's + # ORM, so we have the following hack to construct the relevant where clause + where_clause = ( + "upper(zerver_stream.name::text) IN (SELECT upper(name) FROM unnest(%s) AS name)" + ) + return get_active_streams(realm).extra(where=[where_clause], params=(list(stream_names),)) + + if not stream_names: + return {} + streams = list(fetch_streams_by_name(stream_names)) + return {stream.name.lower(): stream for stream in streams} + + +class Subscription(models.Model): + """Keeps track of which users are part of the + audience for a given Recipient object. + + For 1:1 and group direct message Recipient objects, only the + user_profile and recipient fields have any meaning, defining the + immutable set of users who are in the audience for that Recipient. + + For Recipient objects associated with a Stream, the remaining + fields in this model describe the user's subscription to that stream. + """ + + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + recipient = models.ForeignKey(Recipient, on_delete=CASCADE) + + # Whether the user has since unsubscribed. We mark Subscription + # objects as inactive, rather than deleting them, when a user + # unsubscribes, so we can preserve user customizations like + # notification settings, stream color, etc., if the user later + # resubscribes. + active = models.BooleanField(default=True) + # This is a denormalization designed to improve the performance of + # bulk queries of Subscription objects, Whether the subscribed user + # is active tends to be a key condition in those queries. + # We intentionally don't specify a default value to promote thinking + # about this explicitly, as in some special cases, such as data import, + # we may be creating Subscription objects for a user that's deactivated. + is_user_active = models.BooleanField() + + # Whether this user had muted this stream. + is_muted = models.BooleanField(default=False) + + DEFAULT_STREAM_COLOR = "#c2c2c2" + color = models.CharField(max_length=10, default=DEFAULT_STREAM_COLOR) + pin_to_top = models.BooleanField(default=False) + + # These fields are stream-level overrides for the user's default + # configuration for notification, configured in UserProfile. The + # default, None, means we just inherit the user-level default. + desktop_notifications = models.BooleanField(null=True, default=None) + audible_notifications = models.BooleanField(null=True, default=None) + push_notifications = models.BooleanField(null=True, default=None) + email_notifications = models.BooleanField(null=True, default=None) + wildcard_mentions_notify = models.BooleanField(null=True, default=None) + + class Meta: + unique_together = ("user_profile", "recipient") + indexes = [ + models.Index( + fields=("recipient", "user_profile"), + name="zerver_subscription_recipient_id_user_profile_id_idx", + condition=Q(active=True, is_user_active=True), + ), + ] + + @override + def __str__(self) -> str: + return f"{self.user_profile!r} -> {self.recipient!r}" + + # Subscription fields included whenever a Subscription object is provided to + # Zulip clients via the API. A few details worth noting: + # * These fields will generally be merged with Stream.API_FIELDS + # data about the stream. + # * "user_profile" is usually implied as full API access to Subscription + # is primarily done for the current user; API access to other users' + # subscriptions is generally limited to boolean yes/no. + # * "id" and "recipient_id" are not included as they are not used + # in the Zulip API; it's an internal implementation detail. + # Subscription objects are always looked up in the API via + # (user_profile, stream) pairs. + # * "active" is often excluded in API use cases where it is implied. + # * "is_muted" often needs to be copied to not "in_home_view" for + # backwards-compatibility. + API_FIELDS = [ + "audible_notifications", + "color", + "desktop_notifications", + "email_notifications", + "is_muted", + "pin_to_top", + "push_notifications", + "wildcard_mentions_notify", + ] + + +class DefaultStream(models.Model): + realm = models.ForeignKey(Realm, on_delete=CASCADE) + stream = models.ForeignKey(Stream, on_delete=CASCADE) + + class Meta: + unique_together = ("realm", "stream") + + +class DefaultStreamGroup(models.Model): + MAX_NAME_LENGTH = 60 + + name = models.CharField(max_length=MAX_NAME_LENGTH, db_index=True) + realm = models.ForeignKey(Realm, on_delete=CASCADE) + streams = models.ManyToManyField("zerver.Stream") + description = models.CharField(max_length=1024, default="") + + class Meta: + unique_together = ("realm", "name") + + def to_dict(self) -> Dict[str, Any]: + return dict( + name=self.name, + id=self.id, + description=self.description, + streams=[stream.to_dict() for stream in self.streams.all().order_by("name")], + ) + + +def get_default_stream_groups(realm: Realm) -> QuerySet[DefaultStreamGroup]: + return DefaultStreamGroup.objects.filter(realm=realm) diff --git a/zerver/models/user_activity.py b/zerver/models/user_activity.py new file mode 100644 index 0000000000000..7f979ba1fe3ff --- /dev/null +++ b/zerver/models/user_activity.py @@ -0,0 +1,46 @@ +from datetime import timedelta + +from django.db import models +from django.db.models import CASCADE + +from zerver.models.clients import Client +from zerver.models.users import UserProfile + + +class UserActivity(models.Model): + """Data table recording the last time each user hit Zulip endpoints + via which Clients; unlike UserPresence, these data are not exposed + to users via the Zulip API. + + Useful for debugging as well as to answer analytics questions like + "How many users have accessed the Zulip mobile app in the last + month?" or "Which users/organizations have recently used API + endpoint X that is about to be desupported" for communications + and database migration purposes. + """ + + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + client = models.ForeignKey(Client, on_delete=CASCADE) + query = models.CharField(max_length=50, db_index=True) + + count = models.IntegerField() + last_visit = models.DateTimeField("last visit") + + class Meta: + unique_together = ("user_profile", "client", "query") + + +class UserActivityInterval(models.Model): + MIN_INTERVAL_LENGTH = timedelta(minutes=15) + + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + start = models.DateTimeField("start time", db_index=True) + end = models.DateTimeField("end time", db_index=True) + + class Meta: + indexes = [ + models.Index( + fields=["user_profile", "end"], + name="zerver_useractivityinterval_user_profile_id_end_bb3bfc37_idx", + ), + ] diff --git a/zerver/models/user_topics.py b/zerver/models/user_topics.py new file mode 100644 index 0000000000000..0af96bd106855 --- /dev/null +++ b/zerver/models/user_topics.py @@ -0,0 +1,76 @@ +from datetime import datetime, timezone + +from django.db import models +from django.db.models import CASCADE +from django.db.models.functions import Lower, Upper +from typing_extensions import override + +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.recipients import Recipient +from zerver.models.streams import Stream +from zerver.models.users import UserProfile + + +class UserTopic(models.Model): + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + stream = models.ForeignKey(Stream, on_delete=CASCADE) + recipient = models.ForeignKey(Recipient, on_delete=CASCADE) + topic_name = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH) + # The default value for last_updated is a few weeks before tracking + # of when topics were muted was first introduced. It's designed + # to be obviously incorrect so that one can tell it's backfilled data. + last_updated = models.DateTimeField(default=datetime(2020, 1, 1, 0, 0, tzinfo=timezone.utc)) + + class VisibilityPolicy(models.IntegerChoices): + # A normal muted topic. No notifications and unreads hidden. + MUTED = 1, "Muted topic" + + # This topic will behave like an unmuted topic in an unmuted stream even if it + # belongs to a muted stream. + UNMUTED = 2, "Unmuted topic in muted stream" + + # This topic will behave like `UNMUTED`, plus some additional + # display and/or notifications priority that is TBD and likely to + # be configurable; see #6027. Not yet implemented. + FOLLOWED = 3, "Followed topic" + + # Implicitly, if a UserTopic does not exist, the (user, topic) + # pair should have normal behavior for that (user, stream) pair. + + # We use this in our code to represent the condition in the comment above. + INHERIT = 0, "User's default policy for the stream." + + visibility_policy = models.SmallIntegerField( + choices=VisibilityPolicy.choices, default=VisibilityPolicy.MUTED + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + "user_profile", + "stream", + Lower("topic_name"), + name="usertopic_case_insensitive_topic_uniq", + ), + ] + + indexes = [ + models.Index("stream", Upper("topic_name"), name="zerver_mutedtopic_stream_topic"), + # This index is designed to optimize queries fetching the + # set of users who have special policy for a stream, + # e.g. for the send-message code paths. + models.Index( + fields=("stream", "topic_name", "visibility_policy", "user_profile"), + name="zerver_usertopic_stream_topic_user_visibility_idx", + ), + # This index is useful for handling API requests fetching the + # muted topics for a given user or user/stream pair. + models.Index( + fields=("user_profile", "visibility_policy", "stream", "topic_name"), + name="zerver_usertopic_user_visibility_idx", + ), + ] + + @override + def __str__(self) -> str: + return f"({self.user_profile.email}, {self.stream.name}, {self.topic_name}, {self.last_updated})" diff --git a/zerver/models/users.py b/zerver/models/users.py new file mode 100644 index 0000000000000..f731ba25e28da --- /dev/null +++ b/zerver/models/users.py @@ -0,0 +1,1079 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + +from email.headerregistry import Address +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set +from uuid import uuid4 + +from django.conf import settings +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager +from django.db import models +from django.db.models import CASCADE, Q, QuerySet +from django.db.models.functions import Upper +from django.db.models.signals import post_save +from django.utils.timezone import now as timezone_now +from django.utils.translation import gettext_lazy +from typing_extensions import override + +from zerver.lib.cache import ( + active_non_guest_user_ids_cache_key, + active_user_ids_cache_key, + bot_dict_fields, + bot_dicts_in_realm_cache_key, + bot_profile_cache_key, + cache_with_key, + flush_user_profile, + realm_user_dict_fields, + realm_user_dicts_cache_key, + user_profile_by_api_key_cache_key, + user_profile_by_id_cache_key, + user_profile_cache_key, +) +from zerver.lib.types import ProfileData, RawUserDict +from zerver.lib.utils import generate_api_key +from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH + +if TYPE_CHECKING: + from zerver.models import Realm + + +class UserBaseSettings(models.Model): + """This abstract class is the container for all preferences/personal + settings for users that control the behavior of the application. + + It was extracted from UserProfile to support the RealmUserDefault + model (i.e. allow individual realms to configure the default + values of these preferences for new users in their organization). + + Changing the default value for a field declared here likely + requires a migration to update all RealmUserDefault rows that had + the old default value to have the new default value. Otherwise, + the default change will only affect new users joining Realms + created after the change. + """ + + ### Generic UI settings + enter_sends = models.BooleanField(default=False) + + ### Preferences. ### + # left_side_userlist was removed from the UI in Zulip 6.0; the + # database model is being temporarily preserved in case we want to + # restore a version of the setting, preserving who had it enabled. + left_side_userlist = models.BooleanField(default=False) + default_language = models.CharField(default="en", max_length=MAX_LANGUAGE_ID_LENGTH) + # This setting controls which view is rendered first when Zulip loads. + # Values for it are URL suffix after `#`. + web_home_view = models.TextField(default="inbox") + web_escape_navigates_to_home_view = models.BooleanField(default=True) + dense_mode = models.BooleanField(default=True) + fluid_layout_width = models.BooleanField(default=False) + high_contrast_mode = models.BooleanField(default=False) + translate_emoticons = models.BooleanField(default=False) + display_emoji_reaction_users = models.BooleanField(default=True) + twenty_four_hour_time = models.BooleanField(default=False) + starred_message_counts = models.BooleanField(default=True) + COLOR_SCHEME_AUTOMATIC = 1 + COLOR_SCHEME_NIGHT = 2 + COLOR_SCHEME_LIGHT = 3 + COLOR_SCHEME_CHOICES = [COLOR_SCHEME_AUTOMATIC, COLOR_SCHEME_NIGHT, COLOR_SCHEME_LIGHT] + color_scheme = models.PositiveSmallIntegerField(default=COLOR_SCHEME_AUTOMATIC) + + # UI setting controlling Zulip's behavior of demoting in the sort + # order and graying out streams with no recent traffic. The + # default behavior, automatic, enables this behavior once a user + # is subscribed to 30+ streams in the web app. + DEMOTE_STREAMS_AUTOMATIC = 1 + DEMOTE_STREAMS_ALWAYS = 2 + DEMOTE_STREAMS_NEVER = 3 + DEMOTE_STREAMS_CHOICES = [ + DEMOTE_STREAMS_AUTOMATIC, + DEMOTE_STREAMS_ALWAYS, + DEMOTE_STREAMS_NEVER, + ] + demote_inactive_streams = models.PositiveSmallIntegerField(default=DEMOTE_STREAMS_AUTOMATIC) + + # UI setting controlling whether or not the Zulip web app will + # mark messages as read as it scrolls through the feed. + + MARK_READ_ON_SCROLL_ALWAYS = 1 + MARK_READ_ON_SCROLL_CONVERSATION_ONLY = 2 + MARK_READ_ON_SCROLL_NEVER = 3 + + WEB_MARK_READ_ON_SCROLL_POLICY_CHOICES = [ + MARK_READ_ON_SCROLL_ALWAYS, + MARK_READ_ON_SCROLL_CONVERSATION_ONLY, + MARK_READ_ON_SCROLL_NEVER, + ] + + web_mark_read_on_scroll_policy = models.SmallIntegerField(default=MARK_READ_ON_SCROLL_ALWAYS) + + # Emoji sets + GOOGLE_EMOJISET = "google" + GOOGLE_BLOB_EMOJISET = "google-blob" + TEXT_EMOJISET = "text" + TWITTER_EMOJISET = "twitter" + EMOJISET_CHOICES = ( + (GOOGLE_EMOJISET, "Google"), + (TWITTER_EMOJISET, "Twitter"), + (TEXT_EMOJISET, "Plain text"), + (GOOGLE_BLOB_EMOJISET, "Google blobs"), + ) + emojiset = models.CharField(default=GOOGLE_EMOJISET, choices=EMOJISET_CHOICES, max_length=20) + + # User list style + USER_LIST_STYLE_COMPACT = 1 + USER_LIST_STYLE_WITH_STATUS = 2 + USER_LIST_STYLE_WITH_AVATAR = 3 + USER_LIST_STYLE_CHOICES = [ + USER_LIST_STYLE_COMPACT, + USER_LIST_STYLE_WITH_STATUS, + USER_LIST_STYLE_WITH_AVATAR, + ] + user_list_style = models.PositiveSmallIntegerField(default=USER_LIST_STYLE_WITH_STATUS) + + # Show unread counts for + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_ALL_STREAMS = 1 + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS = 2 + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_NO_STREAMS = 3 + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES = [ + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_ALL_STREAMS, + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS, + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_NO_STREAMS, + ] + web_stream_unreads_count_display_policy = models.PositiveSmallIntegerField( + default=WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS + ) + + ### Notifications settings. ### + + email_notifications_batching_period_seconds = models.IntegerField(default=120) + + # Stream notifications. + enable_stream_desktop_notifications = models.BooleanField(default=False) + enable_stream_email_notifications = models.BooleanField(default=False) + enable_stream_push_notifications = models.BooleanField(default=False) + enable_stream_audible_notifications = models.BooleanField(default=False) + notification_sound = models.CharField(max_length=20, default="zulip") + wildcard_mentions_notify = models.BooleanField(default=True) + + # Followed Topics notifications. + enable_followed_topic_desktop_notifications = models.BooleanField(default=True) + enable_followed_topic_email_notifications = models.BooleanField(default=True) + enable_followed_topic_push_notifications = models.BooleanField(default=True) + enable_followed_topic_audible_notifications = models.BooleanField(default=True) + enable_followed_topic_wildcard_mentions_notify = models.BooleanField(default=True) + + # Direct message + @-mention notifications. + enable_desktop_notifications = models.BooleanField(default=True) + pm_content_in_desktop_notifications = models.BooleanField(default=True) + enable_sounds = models.BooleanField(default=True) + enable_offline_email_notifications = models.BooleanField(default=True) + message_content_in_email_notifications = models.BooleanField(default=True) + enable_offline_push_notifications = models.BooleanField(default=True) + enable_online_push_notifications = models.BooleanField(default=True) + + DESKTOP_ICON_COUNT_DISPLAY_MESSAGES = 1 + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC = 2 + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION = 3 + DESKTOP_ICON_COUNT_DISPLAY_NONE = 4 + DESKTOP_ICON_COUNT_DISPLAY_CHOICES = [ + DESKTOP_ICON_COUNT_DISPLAY_MESSAGES, + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION, + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC, + DESKTOP_ICON_COUNT_DISPLAY_NONE, + ] + desktop_icon_count_display = models.PositiveSmallIntegerField( + default=DESKTOP_ICON_COUNT_DISPLAY_MESSAGES + ) + + enable_digest_emails = models.BooleanField(default=True) + enable_login_emails = models.BooleanField(default=True) + enable_marketing_emails = models.BooleanField(default=True) + presence_enabled = models.BooleanField(default=True) + + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC = 1 + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_ALWAYS = 2 + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_NEVER = 3 + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES = [ + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC, + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_ALWAYS, + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_NEVER, + ] + realm_name_in_email_notifications_policy = models.PositiveSmallIntegerField( + default=REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC + ) + + # The following two settings control which topics to automatically + # 'follow' or 'unmute in a muted stream', respectively. + # Follow or unmute a topic automatically on: + # - PARTICIPATION: Send a message, React to a message, Participate in a poll or Edit a TO-DO list. + # - SEND: Send a message. + # - INITIATION: Send the first message in the topic. + # - NEVER: Never automatically follow or unmute a topic. + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION = 1 + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND = 2 + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION = 3 + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER = 4 + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES = [ + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION, + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION, + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER, + ] + automatically_follow_topics_policy = models.PositiveSmallIntegerField( + default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION, + ) + automatically_unmute_topics_in_muted_streams_policy = models.PositiveSmallIntegerField( + default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, + ) + automatically_follow_topics_where_mentioned = models.BooleanField(default=True) + + # Whether or not the user wants to sync their drafts. + enable_drafts_synchronization = models.BooleanField(default=True) + + # Privacy settings + send_stream_typing_notifications = models.BooleanField(default=True) + send_private_typing_notifications = models.BooleanField(default=True) + send_read_receipts = models.BooleanField(default=True) + + # Who in the organization has access to users' actual email + # addresses. Controls whether the UserProfile.email field is + # the same as UserProfile.delivery_email, or is instead a fake + # generated value encoding the user ID and realm hostname. + EMAIL_ADDRESS_VISIBILITY_EVERYONE = 1 + EMAIL_ADDRESS_VISIBILITY_MEMBERS = 2 + EMAIL_ADDRESS_VISIBILITY_ADMINS = 3 + EMAIL_ADDRESS_VISIBILITY_NOBODY = 4 + EMAIL_ADDRESS_VISIBILITY_MODERATORS = 5 + email_address_visibility = models.PositiveSmallIntegerField( + default=EMAIL_ADDRESS_VISIBILITY_EVERYONE, + ) + + EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP = { + EMAIL_ADDRESS_VISIBILITY_EVERYONE: gettext_lazy("Admins, moderators, members and guests"), + EMAIL_ADDRESS_VISIBILITY_MEMBERS: gettext_lazy("Admins, moderators and members"), + EMAIL_ADDRESS_VISIBILITY_MODERATORS: gettext_lazy("Admins and moderators"), + EMAIL_ADDRESS_VISIBILITY_ADMINS: gettext_lazy("Admins only"), + EMAIL_ADDRESS_VISIBILITY_NOBODY: gettext_lazy("Nobody"), + } + + EMAIL_ADDRESS_VISIBILITY_TYPES = list(EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP.keys()) + + display_settings_legacy = dict( + # Don't add anything new to this legacy dict. + # Instead, see `modern_settings` below. + color_scheme=int, + default_language=str, + web_home_view=str, + demote_inactive_streams=int, + dense_mode=bool, + emojiset=str, + enable_drafts_synchronization=bool, + enter_sends=bool, + fluid_layout_width=bool, + high_contrast_mode=bool, + left_side_userlist=bool, + starred_message_counts=bool, + translate_emoticons=bool, + twenty_four_hour_time=bool, + ) + + notification_settings_legacy = dict( + # Don't add anything new to this legacy dict. + # Instead, see `modern_notification_settings` below. + desktop_icon_count_display=int, + email_notifications_batching_period_seconds=int, + enable_desktop_notifications=bool, + enable_digest_emails=bool, + enable_login_emails=bool, + enable_marketing_emails=bool, + enable_offline_email_notifications=bool, + enable_offline_push_notifications=bool, + enable_online_push_notifications=bool, + enable_sounds=bool, + enable_stream_audible_notifications=bool, + enable_stream_desktop_notifications=bool, + enable_stream_email_notifications=bool, + enable_stream_push_notifications=bool, + message_content_in_email_notifications=bool, + notification_sound=str, + pm_content_in_desktop_notifications=bool, + presence_enabled=bool, + realm_name_in_email_notifications_policy=int, + wildcard_mentions_notify=bool, + ) + + modern_settings = dict( + # Add new general settings here. + display_emoji_reaction_users=bool, + email_address_visibility=int, + web_escape_navigates_to_home_view=bool, + send_private_typing_notifications=bool, + send_read_receipts=bool, + send_stream_typing_notifications=bool, + web_mark_read_on_scroll_policy=int, + user_list_style=int, + web_stream_unreads_count_display_policy=int, + ) + + modern_notification_settings: Dict[str, Any] = dict( + # Add new notification settings here. + enable_followed_topic_desktop_notifications=bool, + enable_followed_topic_email_notifications=bool, + enable_followed_topic_push_notifications=bool, + enable_followed_topic_audible_notifications=bool, + enable_followed_topic_wildcard_mentions_notify=bool, + automatically_follow_topics_policy=int, + automatically_unmute_topics_in_muted_streams_policy=int, + automatically_follow_topics_where_mentioned=bool, + ) + + notification_setting_types = { + **notification_settings_legacy, + **modern_notification_settings, + } + + # Define the types of the various automatically managed properties + property_types = { + **display_settings_legacy, + **notification_setting_types, + **modern_settings, + } + + class Meta: + abstract = True + + @staticmethod + def emojiset_choices() -> List[Dict[str, str]]: + return [ + dict(key=emojiset[0], text=emojiset[1]) for emojiset in UserProfile.EMOJISET_CHOICES + ] + + +class RealmUserDefault(UserBaseSettings): + """This table stores realm-level default values for user preferences + like notification settings, used when creating a new user account. + """ + + realm = models.OneToOneField("zerver.Realm", on_delete=CASCADE) + + +class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): + USERNAME_FIELD = "email" + MAX_NAME_LENGTH = 100 + MIN_NAME_LENGTH = 2 + API_KEY_LENGTH = 32 + NAME_INVALID_CHARS = ["*", "`", "\\", ">", '"', "@"] + + DEFAULT_BOT = 1 + """ + Incoming webhook bots are limited to only sending messages via webhooks. + Thus, it is less of a security risk to expose their API keys to third-party services, + since they can't be used to read messages. + """ + INCOMING_WEBHOOK_BOT = 2 + # This value is also being used in web/src/settings_bots.js. + # On updating it here, update it there as well. + OUTGOING_WEBHOOK_BOT = 3 + """ + Embedded bots run within the Zulip server itself; events are added to the + embedded_bots queue and then handled by a QueueProcessingWorker. + """ + EMBEDDED_BOT = 4 + + BOT_TYPES = { + DEFAULT_BOT: "Generic bot", + INCOMING_WEBHOOK_BOT: "Incoming webhook", + OUTGOING_WEBHOOK_BOT: "Outgoing webhook", + EMBEDDED_BOT: "Embedded bot", + } + + SERVICE_BOT_TYPES = [ + OUTGOING_WEBHOOK_BOT, + EMBEDDED_BOT, + ] + + # For historical reasons, Zulip has two email fields. The + # `delivery_email` field is the user's email address, where all + # email notifications will be sent, and is used for all + # authentication use cases. + # + # The `email` field is the same as delivery_email in organizations + # with EMAIL_ADDRESS_VISIBILITY_EVERYONE. For other + # organizations, it will be a unique value of the form + # user1234@example.com. This field exists for backwards + # compatibility in Zulip APIs where users are referred to by their + # email address, not their ID; it should be used in all API use cases. + # + # Both fields are unique within a realm (in a case-insensitive + # fashion). Since Django's unique_together is case sensitive, this + # is enforced via SQL indexes created by + # zerver/migrations/0295_case_insensitive_email_indexes.py. + delivery_email = models.EmailField(blank=False, db_index=True) + email = models.EmailField(blank=False, db_index=True) + + realm = models.ForeignKey("zerver.Realm", on_delete=CASCADE) + # Foreign key to the Recipient object for PERSONAL type messages to this user. + recipient = models.ForeignKey("zerver.Recipient", null=True, on_delete=models.SET_NULL) + + INACCESSIBLE_USER_NAME = gettext_lazy("Unknown user") + # The user's name. We prefer the model of a full_name + # over first+last because cultures vary on how many + # names one has, whether the family name is first or last, etc. + # It also allows organizations to encode a bit of non-name data in + # the "name" attribute if desired, like gender pronouns, + # graduation year, etc. + full_name = models.CharField(max_length=MAX_NAME_LENGTH) + + date_joined = models.DateTimeField(default=timezone_now) + + # Terms of Service version number that this user has accepted. We + # use the special value TOS_VERSION_BEFORE_FIRST_LOGIN for users + # whose account was created without direct user interaction (via + # the API or a data import), and null for users whose account is + # fully created on servers that do not have a configured ToS. + TOS_VERSION_BEFORE_FIRST_LOGIN = "-1" + tos_version = models.CharField(null=True, max_length=10) + api_key = models.CharField(max_length=API_KEY_LENGTH, default=generate_api_key, unique=True) + + # A UUID generated on user creation. Introduced primarily to + # provide a unique key for a user for the mobile push + # notifications bouncer that will not have collisions after doing + # a data export and then import. + uuid = models.UUIDField(default=uuid4, unique=True) + + # Whether the user has access to server-level administrator pages, like /activity + is_staff = models.BooleanField(default=False) + + # For a normal user, this is True unless the user or an admin has + # deactivated their account. The name comes from Django; this field + # isn't related to presence or to whether the user has recently used Zulip. + # + # See also `long_term_idle`. + is_active = models.BooleanField(default=True, db_index=True) + + is_billing_admin = models.BooleanField(default=False, db_index=True) + + is_bot = models.BooleanField(default=False, db_index=True) + bot_type = models.PositiveSmallIntegerField(null=True, db_index=True) + bot_owner = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) + + # Each role has a superset of the permissions of the next higher + # numbered role. When adding new roles, leave enough space for + # future roles to be inserted between currently adjacent + # roles. These constants appear in RealmAuditLog.extra_data, so + # changes to them will require a migration of RealmAuditLog. + ROLE_REALM_OWNER = 100 + ROLE_REALM_ADMINISTRATOR = 200 + ROLE_MODERATOR = 300 + ROLE_MEMBER = 400 + ROLE_GUEST = 600 + role = models.PositiveSmallIntegerField(default=ROLE_MEMBER, db_index=True) + + ROLE_TYPES = [ + ROLE_REALM_OWNER, + ROLE_REALM_ADMINISTRATOR, + ROLE_MODERATOR, + ROLE_MEMBER, + ROLE_GUEST, + ] + + # Whether the user has been "soft-deactivated" due to weeks of inactivity. + # For these users we avoid doing UserMessage table work, as an optimization + # for large Zulip organizations with lots of single-visit users. + long_term_idle = models.BooleanField(default=False, db_index=True) + + # When we last added basic UserMessage rows for a long_term_idle user. + last_active_message_id = models.IntegerField(null=True) + + # Mirror dummies are fake (!is_active) users used to provide + # message senders in our cross-protocol Zephyr<->Zulip content + # mirroring integration, so that we can display mirrored content + # like native Zulip messages (with a name + avatar, etc.). + is_mirror_dummy = models.BooleanField(default=False) + + # Users with this flag set are allowed to forge messages as sent by another + # user and to send to private streams; also used for Zephyr/Jabber mirroring. + can_forge_sender = models.BooleanField(default=False, db_index=True) + # Users with this flag set can create other users via API. + can_create_users = models.BooleanField(default=False, db_index=True) + + # Used for rate-limiting certain automated messages generated by bots + last_reminder = models.DateTimeField(default=None, null=True) + + # Minutes to wait before warning a bot owner that their bot sent a message + # to a nonexistent stream + BOT_OWNER_STREAM_ALERT_WAITPERIOD = 1 + + # API rate limits, formatted as a comma-separated list of range:max pairs + rate_limits = models.CharField(default="", max_length=100) + + # Default streams for some deprecated/legacy classes of bot users. + default_sending_stream = models.ForeignKey( + "zerver.Stream", + null=True, + related_name="+", + on_delete=models.SET_NULL, + ) + default_events_register_stream = models.ForeignKey( + "zerver.Stream", + null=True, + related_name="+", + on_delete=models.SET_NULL, + ) + default_all_public_streams = models.BooleanField(default=False) + + # A time zone name from the `tzdata` database, as found in zoneinfo.available_timezones(). + # + # The longest existing name is 32 characters long, so max_length=40 seems + # like a safe choice. + # + # In Django, the convention is to use an empty string instead of NULL/None + # for text-based fields. For more information, see + # https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.Field.null. + timezone = models.CharField(max_length=40, default="") + + AVATAR_FROM_GRAVATAR = "G" + AVATAR_FROM_USER = "U" + AVATAR_SOURCES = ( + (AVATAR_FROM_GRAVATAR, "Hosted by Gravatar"), + (AVATAR_FROM_USER, "Uploaded by user"), + ) + avatar_source = models.CharField( + default=AVATAR_FROM_GRAVATAR, choices=AVATAR_SOURCES, max_length=1 + ) + avatar_version = models.PositiveSmallIntegerField(default=1) + avatar_hash = models.CharField(null=True, max_length=64) + + # TODO: TUTORIAL_STATUS was originally an optimization designed to + # allow us to skip querying the OnboardingStep table when loading + # /. This optimization is no longer effective, so it's possible we + # should delete it. + TUTORIAL_WAITING = "W" + TUTORIAL_STARTED = "S" + TUTORIAL_FINISHED = "F" + TUTORIAL_STATES = ( + (TUTORIAL_WAITING, "Waiting"), + (TUTORIAL_STARTED, "Started"), + (TUTORIAL_FINISHED, "Finished"), + ) + tutorial_status = models.CharField( + default=TUTORIAL_WAITING, choices=TUTORIAL_STATES, max_length=1 + ) + + # Contains serialized JSON of the form: + # [("step 1", true), ("step 2", false)] + # where the second element of each tuple is if the step has been + # completed. + onboarding_steps = models.TextField(default="[]") + + zoom_token = models.JSONField(default=None, null=True) + + objects = UserManager() + + ROLE_ID_TO_NAME_MAP = { + ROLE_REALM_OWNER: gettext_lazy("Organization owner"), + ROLE_REALM_ADMINISTRATOR: gettext_lazy("Organization administrator"), + ROLE_MODERATOR: gettext_lazy("Moderator"), + ROLE_MEMBER: gettext_lazy("Member"), + ROLE_GUEST: gettext_lazy("Guest"), + } + + def get_role_name(self) -> str: + return str(self.ROLE_ID_TO_NAME_MAP[self.role]) + + def profile_data(self) -> ProfileData: + from zerver.models import CustomProfileFieldValue + from zerver.models.custom_profile_fields import custom_profile_fields_for_realm + + values = CustomProfileFieldValue.objects.filter(user_profile=self) + user_data = { + v.field_id: {"value": v.value, "rendered_value": v.rendered_value} for v in values + } + data: ProfileData = [] + for field in custom_profile_fields_for_realm(self.realm_id): + field_values = user_data.get(field.id, None) + if field_values: + value, rendered_value = field_values.get("value"), field_values.get( + "rendered_value" + ) + else: + value, rendered_value = None, None + field_type = field.field_type + if value is not None: + converter = field.FIELD_CONVERTERS[field_type] + value = converter(value) + + field_data = field.as_dict() + data.append( + { + "id": field_data["id"], + "name": field_data["name"], + "type": field_data["type"], + "hint": field_data["hint"], + "field_data": field_data["field_data"], + "order": field_data["order"], + "value": value, + "rendered_value": rendered_value, + } + ) + + return data + + def can_admin_user(self, target_user: "UserProfile") -> bool: + """Returns whether this user has permission to modify target_user""" + if target_user.bot_owner_id == self.id: + return True + elif self.is_realm_admin and self.realm == target_user.realm: + return True + else: + return False + + @override + def __str__(self) -> str: + return f"{self.email} {self.realm!r}" + + @property + def is_provisional_member(self) -> bool: + if self.is_moderator: + return False + diff = (timezone_now() - self.date_joined).days + if diff < self.realm.waiting_period_threshold: + return True + return False + + @property + def is_realm_admin(self) -> bool: + return self.role in (UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER) + + @is_realm_admin.setter + def is_realm_admin(self, value: bool) -> None: + if value: + self.role = UserProfile.ROLE_REALM_ADMINISTRATOR + elif self.role == UserProfile.ROLE_REALM_ADMINISTRATOR: + # We need to be careful to not accidentally change + # ROLE_GUEST to ROLE_MEMBER here. + self.role = UserProfile.ROLE_MEMBER + + @property + def has_billing_access(self) -> bool: + return self.is_realm_owner or self.is_billing_admin + + @property + def is_realm_owner(self) -> bool: + return self.role == UserProfile.ROLE_REALM_OWNER + + @is_realm_owner.setter + def is_realm_owner(self, value: bool) -> None: + if value: + self.role = UserProfile.ROLE_REALM_OWNER + elif self.role == UserProfile.ROLE_REALM_OWNER: + # We need to be careful to not accidentally change + # ROLE_GUEST to ROLE_MEMBER here. + self.role = UserProfile.ROLE_MEMBER + + @property + def is_guest(self) -> bool: + return self.role == UserProfile.ROLE_GUEST + + @is_guest.setter + def is_guest(self, value: bool) -> None: + if value: + self.role = UserProfile.ROLE_GUEST + elif self.role == UserProfile.ROLE_GUEST: + # We need to be careful to not accidentally change + # ROLE_REALM_ADMINISTRATOR to ROLE_MEMBER here. + self.role = UserProfile.ROLE_MEMBER + + @property + def is_moderator(self) -> bool: + return self.role == UserProfile.ROLE_MODERATOR + + @is_moderator.setter + def is_moderator(self, value: bool) -> None: + if value: + self.role = UserProfile.ROLE_MODERATOR + elif self.role == UserProfile.ROLE_MODERATOR: + # We need to be careful to not accidentally change + # ROLE_GUEST to ROLE_MEMBER here. + self.role = UserProfile.ROLE_MEMBER + + @property + def is_incoming_webhook(self) -> bool: + return self.bot_type == UserProfile.INCOMING_WEBHOOK_BOT + + @property + def allowed_bot_types(self) -> List[int]: + from zerver.models import Realm + + allowed_bot_types = [] + if ( + self.is_realm_admin + or self.realm.bot_creation_policy != Realm.BOT_CREATION_LIMIT_GENERIC_BOTS + ): + allowed_bot_types.append(UserProfile.DEFAULT_BOT) + allowed_bot_types += [ + UserProfile.INCOMING_WEBHOOK_BOT, + UserProfile.OUTGOING_WEBHOOK_BOT, + ] + if settings.EMBEDDED_BOTS_ENABLED: + allowed_bot_types.append(UserProfile.EMBEDDED_BOT) + return allowed_bot_types + + def email_address_is_realm_public(self) -> bool: + # Bots always have EMAIL_ADDRESS_VISIBILITY_EVERYONE. + if self.email_address_visibility == UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE: + return True + return False + + def has_permission(self, policy_name: str) -> bool: + from zerver.lib.user_groups import is_user_in_group + from zerver.models import Realm + + if policy_name not in [ + "add_custom_emoji_policy", + "create_multiuse_invite_group", + "create_private_stream_policy", + "create_public_stream_policy", + "create_web_public_stream_policy", + "delete_own_message_policy", + "edit_topic_policy", + "invite_to_stream_policy", + "invite_to_realm_policy", + "move_messages_between_streams_policy", + "user_group_edit_policy", + ]: + raise AssertionError("Invalid policy") + + if policy_name in Realm.REALM_PERMISSION_GROUP_SETTINGS: + allowed_user_group = getattr(self.realm, policy_name) + return is_user_in_group(allowed_user_group, self) + + policy_value = getattr(self.realm, policy_name) + if policy_value == Realm.POLICY_NOBODY: + return False + + if policy_value == Realm.POLICY_EVERYONE: + return True + + if self.is_realm_owner: + return True + + if policy_value == Realm.POLICY_OWNERS_ONLY: + return False + + if self.is_realm_admin: + return True + + if policy_value == Realm.POLICY_ADMINS_ONLY: + return False + + if self.is_moderator: + return True + + if policy_value == Realm.POLICY_MODERATORS_ONLY: + return False + + if self.is_guest: + return False + + if policy_value == Realm.POLICY_MEMBERS_ONLY: + return True + + assert policy_value == Realm.POLICY_FULL_MEMBERS_ONLY + return not self.is_provisional_member + + def can_create_public_streams(self) -> bool: + return self.has_permission("create_public_stream_policy") + + def can_create_private_streams(self) -> bool: + return self.has_permission("create_private_stream_policy") + + def can_create_web_public_streams(self) -> bool: + if not self.realm.web_public_streams_enabled(): + return False + return self.has_permission("create_web_public_stream_policy") + + def can_subscribe_other_users(self) -> bool: + return self.has_permission("invite_to_stream_policy") + + def can_invite_users_by_email(self) -> bool: + return self.has_permission("invite_to_realm_policy") + + def can_create_multiuse_invite_to_realm(self) -> bool: + return self.has_permission("create_multiuse_invite_group") + + def can_move_messages_between_streams(self) -> bool: + return self.has_permission("move_messages_between_streams_policy") + + def can_edit_user_groups(self) -> bool: + return self.has_permission("user_group_edit_policy") + + def can_move_messages_to_another_topic(self) -> bool: + return self.has_permission("edit_topic_policy") + + def can_add_custom_emoji(self) -> bool: + return self.has_permission("add_custom_emoji_policy") + + def can_delete_own_message(self) -> bool: + return self.has_permission("delete_own_message_policy") + + def can_access_public_streams(self) -> bool: + return not (self.is_guest or self.realm.is_zephyr_mirror_realm) + + def major_tos_version(self) -> int: + if self.tos_version is not None: + return int(self.tos_version.split(".")[0]) + else: + return -1 + + def format_requester_for_logs(self) -> str: + return "{}@{}".format(self.id, self.realm.string_id or "root") + + @override + def set_password(self, password: Optional[str]) -> None: + if password is None: + self.set_unusable_password() + return + + from zproject.backends import check_password_strength + + if not check_password_strength(password): + raise PasswordTooWeakError + + super().set_password(password) + + class Meta: + indexes = [ + models.Index(Upper("email"), name="upper_userprofile_email_idx"), + ] + + +class PasswordTooWeakError(Exception): + pass + + +def remote_user_to_email(remote_user: str) -> str: + if settings.SSO_APPEND_DOMAIN is not None: + return Address(username=remote_user, domain=settings.SSO_APPEND_DOMAIN).addr_spec + return remote_user + + +# Make sure we flush the UserProfile object from our remote cache +# whenever we save it. +post_save.connect(flush_user_profile, sender=UserProfile) + + +@cache_with_key(user_profile_by_id_cache_key, timeout=3600 * 24 * 7) +def get_user_profile_by_id(user_profile_id: int) -> UserProfile: + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(id=user_profile_id) + + +def get_user_profile_by_email(email: str) -> UserProfile: + """This function is intended to be used for + manual manage.py shell work; robust code must use get_user or + get_user_by_delivery_email instead, because Zulip supports + multiple users with a given (delivery) email address existing on a + single server (in different realms). + """ + return UserProfile.objects.select_related("realm").get(delivery_email__iexact=email.strip()) + + +@cache_with_key(user_profile_by_api_key_cache_key, timeout=3600 * 24 * 7) +def maybe_get_user_profile_by_api_key(api_key: str) -> Optional[UserProfile]: + try: + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(api_key=api_key) + except UserProfile.DoesNotExist: + # We will cache failed lookups with None. The + # use case here is that broken API clients may + # continually ask for the same wrong API key, and + # we want to handle that as quickly as possible. + return None + + +def get_user_profile_by_api_key(api_key: str) -> UserProfile: + user_profile = maybe_get_user_profile_by_api_key(api_key) + if user_profile is None: + raise UserProfile.DoesNotExist + + return user_profile + + +def get_user_by_delivery_email(email: str, realm: "Realm") -> UserProfile: + """Fetches a user given their delivery email. For use in + authentication/registration contexts. Do not use for user-facing + views (e.g. Zulip API endpoints) as doing so would violate the + EMAIL_ADDRESS_VISIBILITY_ADMINS security model. Use get_user in + those code paths. + """ + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(delivery_email__iexact=email.strip(), realm=realm) + + +def get_users_by_delivery_email(emails: Set[str], realm: "Realm") -> QuerySet[UserProfile]: + """This is similar to get_user_by_delivery_email, and + it has the same security caveats. It gets multiple + users and returns a QuerySet, since most callers + will only need two or three fields. + + If you are using this to get large UserProfile objects, you are + probably making a mistake, but if you must, + then use `select_related`. + """ + + """ + Django doesn't support delivery_email__iexact__in, so + we simply OR all the filters that we'd do for the + one-email case. + """ + email_filter = Q() + for email in emails: + email_filter |= Q(delivery_email__iexact=email.strip()) + + return UserProfile.objects.filter(realm=realm).filter(email_filter) + + +@cache_with_key(user_profile_cache_key, timeout=3600 * 24 * 7) +def get_user(email: str, realm: "Realm") -> UserProfile: + """Fetches the user by its visible-to-other users username (in the + `email` field). For use in API contexts; do not use in + authentication/registration contexts as doing so will break + authentication in organizations using + EMAIL_ADDRESS_VISIBILITY_ADMINS. In those code paths, use + get_user_by_delivery_email. + """ + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(email__iexact=email.strip(), realm=realm) + + +def get_active_user(email: str, realm: "Realm") -> UserProfile: + """Variant of get_user_by_email that excludes deactivated users. + See get_user docstring for important usage notes.""" + user_profile = get_user(email, realm) + if not user_profile.is_active: + raise UserProfile.DoesNotExist + return user_profile + + +def get_user_profile_by_id_in_realm(uid: int, realm: "Realm") -> UserProfile: + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(id=uid, realm=realm) + + +def get_active_user_profile_by_id_in_realm(uid: int, realm: "Realm") -> UserProfile: + user_profile = get_user_profile_by_id_in_realm(uid, realm) + if not user_profile.is_active: + raise UserProfile.DoesNotExist + return user_profile + + +def get_user_including_cross_realm(email: str, realm: "Realm") -> UserProfile: + if is_cross_realm_bot_email(email): + return get_system_bot(email, realm.id) + assert realm is not None + return get_user(email, realm) + + +@cache_with_key(bot_profile_cache_key, timeout=3600 * 24 * 7) +def get_system_bot(email: str, realm_id: int) -> UserProfile: + """ + This function doesn't use the realm_id argument yet, but requires + passing it as preparation for adding system bots to each realm instead + of having them all in a separate system bot realm. + If you're calling this function, use the id of the realm in which the system + bot will be after that migration. If the bot is supposed to send a message, + the same realm as the one *to* which the message will be sent should be used - because + cross-realm messages will be eliminated as part of the migration. + """ + return UserProfile.objects.select_related("realm").get(email__iexact=email.strip()) + + +def get_user_by_id_in_realm_including_cross_realm( + uid: int, + realm: Optional["Realm"], +) -> UserProfile: + user_profile = get_user_profile_by_id(uid) + if user_profile.realm == realm: + return user_profile + + # Note: This doesn't validate whether the `realm` passed in is + # None/invalid for the is_cross_realm_bot_email case. + if is_cross_realm_bot_email(user_profile.delivery_email): + return user_profile + + raise UserProfile.DoesNotExist + + +@cache_with_key(realm_user_dicts_cache_key, timeout=3600 * 24 * 7) +def get_realm_user_dicts(realm_id: int) -> List[RawUserDict]: + return list( + UserProfile.objects.filter( + realm_id=realm_id, + ).values(*realm_user_dict_fields) + ) + + +@cache_with_key(active_user_ids_cache_key, timeout=3600 * 24 * 7) +def active_user_ids(realm_id: int) -> List[int]: + query = UserProfile.objects.filter( + realm_id=realm_id, + is_active=True, + ).values_list("id", flat=True) + return list(query) + + +@cache_with_key(active_non_guest_user_ids_cache_key, timeout=3600 * 24 * 7) +def active_non_guest_user_ids(realm_id: int) -> List[int]: + query = ( + UserProfile.objects.filter( + realm_id=realm_id, + is_active=True, + ) + .exclude( + role=UserProfile.ROLE_GUEST, + ) + .values_list("id", flat=True) + ) + return list(query) + + +def bot_owner_user_ids(user_profile: UserProfile) -> Set[int]: + is_private_bot = ( + user_profile.default_sending_stream + and user_profile.default_sending_stream.invite_only + or user_profile.default_events_register_stream + and user_profile.default_events_register_stream.invite_only + ) + assert user_profile.bot_owner_id is not None + if is_private_bot: + return {user_profile.bot_owner_id} + else: + users = {user.id for user in user_profile.realm.get_human_admin_users()} + users.add(user_profile.bot_owner_id) + return users + + +def get_source_profile(email: str, realm_id: int) -> Optional[UserProfile]: + from zerver.models import Realm + from zerver.models.realms import get_realm_by_id + + try: + return get_user_by_delivery_email(email, get_realm_by_id(realm_id)) + except (Realm.DoesNotExist, UserProfile.DoesNotExist): + return None + + +@cache_with_key(lambda realm: bot_dicts_in_realm_cache_key(realm.id), timeout=3600 * 24 * 7) +def get_bot_dicts_in_realm(realm: "Realm") -> List[Dict[str, Any]]: + return list(UserProfile.objects.filter(realm=realm, is_bot=True).values(*bot_dict_fields)) + + +def is_cross_realm_bot_email(email: str) -> bool: + return email.lower() in settings.CROSS_REALM_BOT_EMAILS diff --git a/zerver/openapi/curl_param_value_generators.py b/zerver/openapi/curl_param_value_generators.py index 168c883d39729..3b7efb859b42c 100644 --- a/zerver/openapi/curl_param_value_generators.py +++ b/zerver/openapi/curl_param_value_generators.py @@ -20,7 +20,9 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.upload import upload_message_attachment from zerver.lib.users import get_api_key -from zerver.models import Client, Message, UserGroup, UserPresence, get_realm, get_user +from zerver.models import Client, Message, UserGroup, UserPresence +from zerver.models.realms import get_realm +from zerver.models.users import get_user GENERATOR_FUNCTIONS: Dict[str, Callable[[], Dict[str, object]]] = {} REGISTERED_GENERATOR_FUNCTIONS: Set[str] = set() diff --git a/zerver/openapi/python_examples.py b/zerver/openapi/python_examples.py index dda66ed85057b..1cb811951fa23 100644 --- a/zerver/openapi/python_examples.py +++ b/zerver/openapi/python_examples.py @@ -22,7 +22,8 @@ from typing_extensions import ParamSpec from zulip import Client -from zerver.models import get_realm, get_user +from zerver.models.realms import get_realm +from zerver.models.users import get_user from zerver.openapi.openapi import validate_against_openapi_schema ZULIP_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/zerver/openapi/test_curl_examples.py b/zerver/openapi/test_curl_examples.py index f487cfc972227..b009afb1983c6 100644 --- a/zerver/openapi/test_curl_examples.py +++ b/zerver/openapi/test_curl_examples.py @@ -16,7 +16,7 @@ from django.conf import settings from zulip import Client -from zerver.models import get_realm +from zerver.models.realms import get_realm from zerver.openapi import markdown_extension from zerver.openapi.curl_param_value_generators import ( AUTHENTICATION_LINE, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index c9267666037a5..caee67bd0c611 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -5606,6 +5606,17 @@ paths: in UTC seconds. required: true example: 3165826990 + - name: read_by_sender + in: query + schema: + type: boolean + description: | + Whether the message should be initially marked read by its + sender. If unspecified, the server uses a heuristic based + on the client name and the recipient. + + **Changes**: New in Zulip 8.0 (feature level 236). + example: true responses: "200": description: Success. @@ -6373,6 +6384,17 @@ paths: chosen freely by the client; the server will pass it back to the client without inspecting it, as described in the `queue_id` description. example: "100.01" + - name: read_by_sender + in: query + schema: + type: boolean + description: | + Whether the message should be initially marked read by its + sender. If unspecified, the server uses a heuristic based + on the client name. + + **Changes**: New in Zulip 8.0 (feature level 236). + example: true responses: "200": description: Success. diff --git a/zerver/tests/test_audit_log.py b/zerver/tests/test_audit_log.py index b4cf5bd91b4e6..1498636d84566 100644 --- a/zerver/tests/test_audit_log.py +++ b/zerver/tests/test_audit_log.py @@ -73,24 +73,21 @@ from zerver.lib.types import LinkifierDict, RealmPlaygroundDict from zerver.lib.utils import assert_is_not_none from zerver.models import ( - EmojiInfo, Message, Realm, RealmAuditLog, - RealmDomainDict, RealmPlayground, Recipient, Subscription, - SystemGroups, UserGroup, UserProfile, - get_all_custom_emoji_for_realm, - get_realm, - get_realm_domains, - get_realm_playgrounds, - get_stream, - linkifiers_for_realm, ) +from zerver.models.groups import SystemGroups +from zerver.models.linkifiers import linkifiers_for_realm +from zerver.models.realm_emoji import EmojiInfo, get_all_custom_emoji_for_realm +from zerver.models.realm_playgrounds import get_realm_playgrounds +from zerver.models.realms import RealmDomainDict, get_realm, get_realm_domains +from zerver.models.streams import get_stream class TestRealmAuditLog(ZulipTestCase): diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index f2dda1ab7756a..4d5bb2be9613e 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -110,17 +110,15 @@ CustomProfileField, CustomProfileFieldValue, MultiuseInvite, - PasswordTooWeakError, PreregistrationUser, Realm, RealmDomain, Stream, UserGroup, UserProfile, - clear_supported_auth_backends_cache, - get_realm, - get_user_by_delivery_email, ) +from zerver.models.realms import clear_supported_auth_backends_cache, get_realm +from zerver.models.users import PasswordTooWeakError, get_user_by_delivery_email from zerver.signals import JUST_CREATED_THRESHOLD from zerver.views.auth import log_into_subdomain, maybe_send_to_registration from zproject.backends import ( diff --git a/zerver/tests/test_bots.py b/zerver/tests/test_bots.py index 06a65616cc2f0..24692df53bdbc 100644 --- a/zerver/tests/test_bots.py +++ b/zerver/tests/test_bots.py @@ -17,18 +17,11 @@ from zerver.lib.integrations import EMBEDDED_BOTS, WebhookIntegration from zerver.lib.test_classes import UploadSerializeMixin, ZulipTestCase from zerver.lib.test_helpers import avatar_disk_path, get_test_image_file -from zerver.models import ( - Realm, - RealmUserDefault, - Service, - Subscription, - UserProfile, - get_bot_services, - get_realm, - get_stream, - get_user, - is_cross_realm_bot_email, -) +from zerver.models import Realm, RealmUserDefault, Service, Subscription, UserProfile +from zerver.models.bots import get_bot_services +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_user, is_cross_realm_bot_email # A test validator diff --git a/zerver/tests/test_cache.py b/zerver/tests/test_cache.py index f1743cd0544ef..5f0730dff5e51 100644 --- a/zerver/tests/test_cache.py +++ b/zerver/tests/test_cache.py @@ -21,7 +21,9 @@ validate_cache_key, ) from zerver.lib.test_classes import ZulipTestCase -from zerver.models import UserProfile, get_realm, get_system_bot, get_user, get_user_profile_by_id +from zerver.models import UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_system_bot, get_user, get_user_profile_by_id class AppsTest(ZulipTestCase): diff --git a/zerver/tests/test_custom_profile_data.py b/zerver/tests/test_custom_profile_data.py index 39a8f88eeae24..6b8268b24f75c 100644 --- a/zerver/tests/test_custom_profile_data.py +++ b/zerver/tests/test_custom_profile_data.py @@ -15,13 +15,9 @@ from zerver.lib.markdown import markdown_convert from zerver.lib.test_classes import ZulipTestCase from zerver.lib.types import ProfileDataElementUpdateDict, ProfileDataElementValue -from zerver.models import ( - CustomProfileField, - CustomProfileFieldValue, - UserProfile, - custom_profile_fields_for_realm, - get_realm, -) +from zerver.models import CustomProfileField, CustomProfileFieldValue, UserProfile +from zerver.models.custom_profile_fields import custom_profile_fields_for_realm +from zerver.models.realms import get_realm class CustomProfileFieldTestCase(ZulipTestCase): diff --git a/zerver/tests/test_decorators.py b/zerver/tests/test_decorators.py index af666bed0bf9e..6cbab146e015b 100644 --- a/zerver/tests/test_decorators.py +++ b/zerver/tests/test_decorators.py @@ -52,7 +52,10 @@ from zerver.lib.users import get_api_key from zerver.lib.utils import generate_api_key, has_api_key_format from zerver.middleware import LogRequests, parse_client -from zerver.models import Client, Realm, UserProfile, clear_client_cache, get_realm, get_user +from zerver.models import Client, Realm, UserProfile +from zerver.models.clients import clear_client_cache +from zerver.models.realms import get_realm +from zerver.models.users import get_user if settings.ZILENCER_ENABLED: from zilencer.models import RemoteZulipServer diff --git a/zerver/tests/test_delete_unclaimed_attachments.py b/zerver/tests/test_delete_unclaimed_attachments.py index f151d2d6678a8..807db51d36526 100644 --- a/zerver/tests/test_delete_unclaimed_attachments.py +++ b/zerver/tests/test_delete_unclaimed_attachments.py @@ -13,7 +13,8 @@ from zerver.actions.uploads import do_delete_old_unclaimed_attachments from zerver.lib.retention import clean_archived_data from zerver.lib.test_classes import UploadSerializeMixin, ZulipTestCase -from zerver.models import ArchivedAttachment, Attachment, Message, UserProfile, get_client +from zerver.models import ArchivedAttachment, Attachment, Message, UserProfile +from zerver.models.clients import get_client class UnclaimedAttachmentTest(UploadSerializeMixin, ZulipTestCase): diff --git a/zerver/tests/test_digest.py b/zerver/tests/test_digest.py index c70f859781c48..d2f4958ca31d3 100644 --- a/zerver/tests/test_digest.py +++ b/zerver/tests/test_digest.py @@ -26,18 +26,9 @@ from zerver.lib.message import get_last_message_id from zerver.lib.streams import create_stream_if_needed from zerver.lib.test_classes import ZulipTestCase -from zerver.models import ( - Client, - Message, - Realm, - RealmAuditLog, - Stream, - UserActivityInterval, - UserProfile, - get_client, - get_realm, - get_stream, -) +from zerver.models import Message, Realm, RealmAuditLog, Stream, UserActivityInterval, UserProfile +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream class TestDigestEmailMessages(ZulipTestCase): @@ -548,8 +539,6 @@ def test_new_stream_link(self) -> None: self.assertEqual(stream_info["html"], []) def simulate_stream_conversation(self, stream: str, senders: List[str]) -> List[int]: - client = "website" # this makes `sent_by_human` return True - sending_client = get_client(client) message_ids = [] # List[int] for sender_name in senders: sender = self.example_user(sender_name) @@ -557,7 +546,6 @@ def simulate_stream_conversation(self, stream: str, senders: List[str]) -> List[ content = f"some content for {stream} from {sender_name}" message_id = self.send_stream_message(sender, stream, content) message_ids.append(message_id) - Message.objects.filter(id__in=message_ids).update(sending_client=sending_client) return message_ids @@ -578,24 +566,22 @@ def populate_topic( bot_messages: int, realm: Realm, ) -> None: - def send_messages(client: Client, users: int, messages: int) -> None: + for is_bot, users, messages in [ + (False, humans, human_messages), + (True, bots, bot_messages), + ]: messages_sent = 0 while messages_sent < messages: for index, username in enumerate(self.example_user_map, start=1): - topic.add_message( - Message( - sender=self.example_user(username), sending_client=client, realm=realm - ) - ) + if self.example_user(username).is_bot != is_bot: + continue + topic.add_message(Message(sender=self.example_user(username), realm=realm)) messages_sent += 1 if messages_sent == messages: break if index == users: break - send_messages(Client(name="zulipmobile"), humans, human_messages) - send_messages(Client(name="bot"), bots, bot_messages) - def test_get_hot_topics(self) -> None: realm = get_realm("zulip") denmark = get_stream("Denmark", realm) diff --git a/zerver/tests/test_docs.py b/zerver/tests/test_docs.py index 4a869de14bbe4..b34b268112136 100644 --- a/zerver/tests/test_docs.py +++ b/zerver/tests/test_docs.py @@ -14,7 +14,8 @@ from zerver.lib.integrations import CATEGORIES, INTEGRATIONS, META_CATEGORY from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import HostRequestMock -from zerver.models import Realm, get_realm +from zerver.models import Realm +from zerver.models.realms import get_realm from zerver.views.documentation import add_api_url_context if TYPE_CHECKING: @@ -546,7 +547,7 @@ def test_CTA_text_by_plan_type(self) -> None: sign_up_now = "Create organization" upgrade_to_standard = "Upgrade to Standard" current_plan = "Current plan" - sponsorship_pending = "Sponsorship pending" + sponsorship_pending = "Sponsorship requested" # Root domain result = self.client_get("/plans/", subdomain="") diff --git a/zerver/tests/test_email_change.py b/zerver/tests/test_email_change.py index 14d4dff518c1b..65009f30f79c9 100644 --- a/zerver/tests/test_email_change.py +++ b/zerver/tests/test_email_change.py @@ -18,14 +18,9 @@ from zerver.actions.user_settings import do_change_user_setting, do_start_email_change_process from zerver.actions.users import do_deactivate_user from zerver.lib.test_classes import ZulipTestCase -from zerver.models import ( - EmailChangeStatus, - UserProfile, - get_realm, - get_user, - get_user_by_delivery_email, - get_user_profile_by_id, -) +from zerver.models import EmailChangeStatus, UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_user, get_user_by_delivery_email, get_user_profile_by_id class EmailChangeTestCase(ZulipTestCase): diff --git a/zerver/tests/test_email_mirror.py b/zerver/tests/test_email_mirror.py index abe7a61e97ea0..da93be6db6f9f 100644 --- a/zerver/tests/test_email_mirror.py +++ b/zerver/tests/test_email_mirror.py @@ -37,15 +37,10 @@ from zerver.lib.streams import ensure_stream from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import mock_queue_publish, most_recent_message, most_recent_usermessage -from zerver.models import ( - Attachment, - Recipient, - Stream, - UserProfile, - get_realm, - get_stream, - get_system_bot, -) +from zerver.models import Attachment, Recipient, Stream, UserProfile +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_system_bot from zerver.worker.queue_processors import MirrorWorker if TYPE_CHECKING: diff --git a/zerver/tests/test_email_notifications.py b/zerver/tests/test_email_notifications.py index bcf3ace6d6d8c..d2b3b82d426f8 100644 --- a/zerver/tests/test_email_notifications.py +++ b/zerver/tests/test_email_notifications.py @@ -11,15 +11,20 @@ from django.utils.timezone import now as timezone_now from django_auth_ldap.config import LDAPSearch -from zerver.actions.users import do_change_user_role from zerver.lib.email_notifications import ( enqueue_welcome_emails, get_onboarding_email_schedule, send_account_registered_email, ) -from zerver.lib.send_email import deliver_scheduled_emails, send_custom_email +from zerver.lib.send_email import ( + deliver_scheduled_emails, + send_custom_email, + send_custom_server_email, +) from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Realm, ScheduledEmail, UserProfile, get_realm +from zerver.models import Realm, ScheduledEmail, UserProfile +from zerver.models.realms import get_realm +from zilencer.models import RemoteZulipServer class TestCustomEmails(ZulipTestCase): @@ -34,12 +39,12 @@ def test_send_custom_email_argument(self) -> None: markdown_template.flush() send_custom_email( UserProfile.objects.filter(id=hamlet.id), + dry_run=False, options={ "markdown_template_path": markdown_template.name, "reply_to": reply_to, "subject": email_subject, "from_name": from_name, - "dry_run": False, }, ) self.assert_length(mail.outbox, 1) @@ -61,23 +66,21 @@ def test_send_custom_email_remote_server(self) -> None: email_subject = "subject_test" reply_to = "reply_to_test" from_name = "from_name_test" - contact_email = "zulip-admin@example.com" markdown_template_path = "templates/corporate/policies/index.md" - send_custom_email( - UserProfile.objects.none(), - target_emails=[contact_email], + send_custom_server_email( + remote_servers=RemoteZulipServer.objects.all(), + dry_run=False, options={ "markdown_template_path": markdown_template_path, "reply_to": reply_to, "subject": email_subject, "from_name": from_name, - "dry_run": False, }, ) self.assert_length(mail.outbox, 1) msg = mail.outbox[0] self.assertEqual(msg.subject, email_subject) - self.assertEqual(msg.to, [contact_email]) + self.assertEqual(msg.to, ["remotezulipserver@zulip.com"]) self.assert_length(msg.reply_to, 1) self.assertEqual(msg.reply_to[0], reply_to) self.assertNotIn("{% block content %}", msg.body) @@ -95,9 +98,9 @@ def test_send_custom_email_headers(self) -> None: ) send_custom_email( UserProfile.objects.filter(id=hamlet.id), + dry_run=False, options={ "markdown_template_path": markdown_template_path, - "dry_run": False, }, ) self.assert_length(mail.outbox, 1) @@ -113,9 +116,9 @@ def test_send_custom_email_context(self) -> None: ) send_custom_email( UserProfile.objects.filter(id=hamlet.id), + dry_run=False, options={ "markdown_template_path": markdown_template_path, - "dry_run": False, }, ) self.assert_length(mail.outbox, 1) @@ -136,9 +139,9 @@ def add_context(context: Dict[str, object], user: UserProfile) -> None: send_custom_email( UserProfile.objects.filter(id=hamlet.id), + dry_run=False, options={ "markdown_template_path": markdown_template_path, - "dry_run": False, }, add_context=add_context, ) @@ -162,10 +165,10 @@ def test_send_custom_email_no_argument(self) -> None: NoEmailArgumentError, send_custom_email, UserProfile.objects.filter(id=hamlet.id), + dry_run=False, options={ "markdown_template_path": markdown_template_path, "from_name": from_name, - "dry_run": False, }, ) @@ -173,10 +176,10 @@ def test_send_custom_email_no_argument(self) -> None: NoEmailArgumentError, send_custom_email, UserProfile.objects.filter(id=hamlet.id), + dry_run=False, options={ "markdown_template_path": markdown_template_path, "subject": email_subject, - "dry_run": False, }, ) @@ -194,10 +197,10 @@ def test_send_custom_email_doubled_arguments(self) -> None: DoubledEmailArgumentError, send_custom_email, UserProfile.objects.filter(id=hamlet.id), + dry_run=False, options={ "markdown_template_path": markdown_template_path, "subject": email_subject, - "dry_run": False, }, ) @@ -205,32 +208,12 @@ def test_send_custom_email_doubled_arguments(self) -> None: DoubledEmailArgumentError, send_custom_email, UserProfile.objects.filter(id=hamlet.id), + dry_run=False, options={ "markdown_template_path": markdown_template_path, "from_name": from_name, - "dry_run": False, - }, - ) - - def test_send_custom_email_admins_only(self) -> None: - admin_user = self.example_user("hamlet") - do_change_user_role(admin_user, UserProfile.ROLE_REALM_ADMINISTRATOR, acting_user=None) - - non_admin_user = self.example_user("cordelia") - - markdown_template_path = ( - "zerver/tests/fixtures/email/custom_emails/email_base_headers_test.md" - ) - send_custom_email( - UserProfile.objects.filter(id__in=(admin_user.id, non_admin_user.id)), - options={ - "markdown_template_path": markdown_template_path, - "admins_only": True, - "dry_run": False, }, ) - self.assert_length(mail.outbox, 1) - self.assertIn(admin_user.delivery_email, mail.outbox[0].to[0]) def test_send_custom_email_dry_run(self) -> None: hamlet = self.example_user("hamlet") @@ -241,12 +224,12 @@ def test_send_custom_email_dry_run(self) -> None: with patch("builtins.print") as _: send_custom_email( UserProfile.objects.filter(id=hamlet.id), + dry_run=True, options={ "markdown_template_path": markdown_template_path, "reply_to": reply_to, "subject": email_subject, "from_name": from_name, - "dry_run": True, }, ) self.assert_length(mail.outbox, 0) diff --git a/zerver/tests/test_embedded_bot_system.py b/zerver/tests/test_embedded_bot_system.py index 1495692ba698e..2277ff2ce3ce0 100644 --- a/zerver/tests/test_embedded_bot_system.py +++ b/zerver/tests/test_embedded_bot_system.py @@ -4,14 +4,12 @@ from typing_extensions import override from zerver.lib.bot_lib import EmbeddedBotQuitError +from zerver.lib.display_recipient import get_display_recipient from zerver.lib.test_classes import ZulipTestCase -from zerver.models import ( - UserProfile, - get_display_recipient, - get_realm, - get_service_profile, - get_user, -) +from zerver.models import UserProfile +from zerver.models.bots import get_service_profile +from zerver.models.realms import get_realm +from zerver.models.users import get_user class TestEmbeddedBotMessaging(ZulipTestCase): diff --git a/zerver/tests/test_event_queue.py b/zerver/tests/test_event_queue.py index d7697bd3411f4..59da51461af3a 100644 --- a/zerver/tests/test_event_queue.py +++ b/zerver/tests/test_event_queue.py @@ -15,7 +15,8 @@ from zerver.lib.cache import cache_delete, get_muting_users_cache_key from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import HostRequestMock, dummy_handler, mock_queue_publish -from zerver.models import Recipient, Subscription, UserProfile, UserTopic, get_stream +from zerver.models import Recipient, Subscription, UserProfile, UserTopic +from zerver.models.streams import get_stream from zerver.tornado.event_queue import ( ClientDescriptor, access_client_descriptor, diff --git a/zerver/tests/test_event_system.py b/zerver/tests/test_event_system.py index dcb879ac6c8e9..3e478a8663859 100644 --- a/zerver/tests/test_event_system.py +++ b/zerver/tests/test_event_system.py @@ -28,16 +28,11 @@ stub_event_queue_user_events, ) from zerver.lib.users import get_api_key, get_users_for_api -from zerver.models import ( - CustomProfileField, - UserMessage, - UserPresence, - UserProfile, - get_client, - get_realm, - get_stream, - get_system_bot, -) +from zerver.models import CustomProfileField, UserMessage, UserPresence, UserProfile +from zerver.models.clients import get_client +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_system_bot from zerver.tornado.event_queue import ( allocate_client_descriptor, clear_client_event_queues_for_testing, diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index af9f3abaef37f..b67c921ea5321 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -234,17 +234,17 @@ RealmUserDefault, Service, Stream, - SystemGroups, UserGroup, UserMessage, UserPresence, UserProfile, UserStatus, UserTopic, - get_client, - get_stream, - get_user_by_delivery_email, ) +from zerver.models.clients import get_client +from zerver.models.groups import SystemGroups +from zerver.models.streams import get_stream +from zerver.models.users import get_user_by_delivery_email from zerver.openapi.openapi import validate_against_openapi_schema from zerver.tornado.django_api import send_event from zerver.tornado.event_queue import ( diff --git a/zerver/tests/test_example.py b/zerver/tests/test_example.py index 7387c003dd59b..27563bcf3faa2 100644 --- a/zerver/tests/test_example.py +++ b/zerver/tests/test_example.py @@ -11,7 +11,10 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import most_recent_message from zerver.lib.users import is_administrator_role -from zerver.models import UserProfile, UserStatus, get_realm, get_stream, get_user_by_delivery_email +from zerver.models import UserProfile, UserStatus +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_user_by_delivery_email # Most Zulip tests use ZulipTestCase, which inherits from django.test.TestCase. diff --git a/zerver/tests/test_gitter_importer.py b/zerver/tests/test_gitter_importer.py index 5a8502b9c34bc..75e950acbd102 100644 --- a/zerver/tests/test_gitter_importer.py +++ b/zerver/tests/test_gitter_importer.py @@ -10,7 +10,8 @@ from zerver.data_import.gitter import do_convert_data, get_usermentions from zerver.lib.import_realm import do_import_realm from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Message, UserProfile, get_realm +from zerver.models import Message, UserProfile +from zerver.models.realms import get_realm from zproject.backends import ( AUTH_BACKEND_NAME_MAP, GitHubAuthBackend, diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 52e5f1482f882..a682fc590992b 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -24,17 +24,10 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import get_user_messages, queries_captured from zerver.lib.timestamp import datetime_to_timestamp -from zerver.models import ( - DefaultStream, - Draft, - Realm, - UserActivity, - UserProfile, - get_realm, - get_stream, - get_system_bot, - get_user, -) +from zerver.models import DefaultStream, Draft, Realm, UserActivity, UserProfile +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_system_bot, get_user from zerver.worker.queue_processors import UserActivityWorker if TYPE_CHECKING: diff --git a/zerver/tests/test_hotspots.py b/zerver/tests/test_hotspots.py index 8c12554d281d9..766b49b5d2fc6 100644 --- a/zerver/tests/test_hotspots.py +++ b/zerver/tests/test_hotspots.py @@ -10,7 +10,8 @@ get_next_onboarding_steps, ) from zerver.lib.test_classes import ZulipTestCase -from zerver.models import OnboardingStep, UserProfile, get_realm +from zerver.models import OnboardingStep, UserProfile +from zerver.models.realms import get_realm # Splitting this out, since I imagine this will eventually have most of the diff --git a/zerver/tests/test_i18n.py b/zerver/tests/test_i18n.py index 54dddfed62346..28516ce1fdfeb 100644 --- a/zerver/tests/test_i18n.py +++ b/zerver/tests/test_i18n.py @@ -13,7 +13,7 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import HostRequestMock from zerver.management.commands import makemessages -from zerver.models import get_realm_stream +from zerver.models.streams import get_realm_stream if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/tests/test_import_export.py b/zerver/tests/test_import_export.py index b36ecfdd29580..adef08fd51403 100644 --- a/zerver/tests/test_import_export.py +++ b/zerver/tests/test_import_export.py @@ -76,7 +76,6 @@ ScheduledMessage, Stream, Subscription, - SystemGroups, UserGroup, UserGroupMembership, UserMessage, @@ -84,14 +83,13 @@ UserProfile, UserStatus, UserTopic, - get_active_streams, - get_client, - get_huddle_hash, - get_realm, - get_stream, - get_system_bot, - get_user_by_delivery_email, ) +from zerver.models.clients import get_client +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_realm +from zerver.models.recipients import get_huddle_hash +from zerver.models.streams import get_active_streams, get_stream +from zerver.models.users import get_system_bot, get_user_by_delivery_email def make_datetime(val: float) -> datetime: diff --git a/zerver/tests/test_integrations_dev_panel.py b/zerver/tests/test_integrations_dev_panel.py index cf1ae3ba4b11f..0b2b17b829b0d 100644 --- a/zerver/tests/test_integrations_dev_panel.py +++ b/zerver/tests/test_integrations_dev_panel.py @@ -4,7 +4,9 @@ from django.core.exceptions import ValidationError from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Message, Stream, get_realm, get_user +from zerver.models import Message, Stream +from zerver.models.realms import get_realm +from zerver.models.users import get_user class TestIntegrationsDevPanel(ZulipTestCase): diff --git a/zerver/tests/test_invite.py b/zerver/tests/test_invite.py index d2f146868dbe8..9b0d25ca7d5d4 100644 --- a/zerver/tests/test_invite.py +++ b/zerver/tests/test_invite.py @@ -60,14 +60,14 @@ Realm, ScheduledEmail, Stream, - SystemGroups, UserGroup, UserMessage, UserProfile, - get_realm, - get_stream, - get_user_by_delivery_email, ) +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_user_by_delivery_email from zerver.views.invite import INVITATION_LINK_VALIDITY_MINUTES, get_invitee_emails_set from zerver.views.registration import accounts_home @@ -1194,7 +1194,7 @@ def test_invite_outside_domain_before_closing(self) -> None: zulip_realm.save() result = self.submit_reg_form_for_user("foo@example.com", "password") - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, 400) self.assert_in_response( "does not allow signups using emails with your email domain", result ) @@ -1221,7 +1221,7 @@ def test_disposable_emails_before_closing(self) -> None: zulip_realm.save() result = self.submit_reg_form_for_user("foo@mailnator.com", "password") - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, 400) self.assert_in_response("does not allow signups using disposable email addresses.", result) def test_invite_with_email_containing_plus_before_closing(self) -> None: @@ -1247,7 +1247,7 @@ def test_invite_with_email_containing_plus_before_closing(self) -> None: zulip_realm.save() result = self.submit_reg_form_for_user(external_address, "password") - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, 400) self.assert_in_response('does not allow signups using emails that contain "+".', result) def test_invalid_email_check_after_confirming_email(self) -> None: @@ -1263,7 +1263,7 @@ def test_invalid_email_check_after_confirming_email(self) -> None: prereg_user.save() result = self.submit_reg_form_for_user(email, "password") - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, 400) self.assert_in_response( "The email address you are trying to sign up with is not valid", result ) diff --git a/zerver/tests/test_management_commands.py b/zerver/tests/test_management_commands.py index 26bb8801b042e..4a524f5e1c03b 100644 --- a/zerver/tests/test_management_commands.py +++ b/zerver/tests/test_management_commands.py @@ -20,16 +20,10 @@ from zerver.lib.management import ZulipBaseCommand, check_config from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import most_recent_message, stdout_suppressed -from zerver.models import ( - Message, - Reaction, - Realm, - Recipient, - UserProfile, - get_realm, - get_stream, - get_user_profile_by_email, -) +from zerver.models import Message, Reaction, Realm, Recipient, UserProfile +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_user_profile_by_email class TestCheckConfig(ZulipTestCase): diff --git a/zerver/tests/test_markdown.py b/zerver/tests/test_markdown.py index e1002ac7c762a..56d9c8a9a6f8f 100644 --- a/zerver/tests/test_markdown.py +++ b/zerver/tests/test_markdown.py @@ -60,19 +60,12 @@ from zerver.lib.per_request_cache import flush_per_request_caches from zerver.lib.test_classes import ZulipTestCase from zerver.lib.tex import render_tex -from zerver.models import ( - Message, - RealmEmoji, - RealmFilter, - SystemGroups, - UserGroup, - UserMessage, - UserProfile, - get_client, - get_realm, - get_stream, - linkifiers_for_realm, -) +from zerver.models import Message, RealmEmoji, RealmFilter, UserGroup, UserMessage, UserProfile +from zerver.models.clients import get_client +from zerver.models.groups import SystemGroups +from zerver.models.linkifiers import linkifiers_for_realm +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream class SimulatedFencedBlockPreprocessor(FencedBlockPreprocessor): diff --git a/zerver/tests/test_mattermost_importer.py b/zerver/tests/test_mattermost_importer.py index 16a7ee83e8559..d7251e1385c0f 100644 --- a/zerver/tests/test_mattermost_importer.py +++ b/zerver/tests/test_mattermost_importer.py @@ -28,7 +28,9 @@ from zerver.lib.emoji import name_to_codepoint from zerver.lib.import_realm import do_import_realm from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Message, Reaction, Recipient, UserProfile, get_realm, get_user +from zerver.models import Message, Reaction, Recipient, UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_user class MatterMostImporter(ZulipTestCase): diff --git a/zerver/tests/test_message_dict.py b/zerver/tests/test_message_dict.py index 291af89b79aed..83fa79c00cf8d 100644 --- a/zerver/tests/test_message_dict.py +++ b/zerver/tests/test_message_dict.py @@ -4,6 +4,7 @@ from django.utils.timezone import now as timezone_now from zerver.lib.cache import cache_delete, to_dict_cache_key_id +from zerver.lib.display_recipient import get_display_recipient from zerver.lib.markdown import version as markdown_version from zerver.lib.message import MessageDict, messages_for_ids, sew_messages_and_reactions from zerver.lib.per_request_cache import flush_per_request_caches @@ -11,18 +12,9 @@ from zerver.lib.test_helpers import make_client from zerver.lib.topic import TOPIC_LINKS from zerver.lib.types import DisplayRecipientT, UserDisplayRecipient -from zerver.models import ( - Message, - Reaction, - Realm, - RealmFilter, - Recipient, - Stream, - UserProfile, - get_display_recipient, - get_realm, - get_stream, -) +from zerver.models import Message, Reaction, Realm, RealmFilter, Recipient, Stream, UserProfile +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream class MessageDictTest(ZulipTestCase): diff --git a/zerver/tests/test_message_edit.py b/zerver/tests/test_message_edit.py index 97673f9c321ab..4e1dddfe9f543 100644 --- a/zerver/tests/test_message_edit.py +++ b/zerver/tests/test_message_edit.py @@ -30,19 +30,11 @@ topic_has_visibility_policy, ) from zerver.lib.utils import assert_is_not_none -from zerver.models import ( - MAX_TOPIC_NAME_LENGTH, - Message, - Realm, - Stream, - SystemGroups, - UserGroup, - UserMessage, - UserProfile, - UserTopic, - get_realm, - get_stream, -) +from zerver.models import Message, Realm, Stream, UserGroup, UserMessage, UserProfile, UserTopic +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/tests/test_message_edit_notifications.py b/zerver/tests/test_message_edit_notifications.py index 8f87ec692dba0..69daf9dc5df7d 100644 --- a/zerver/tests/test_message_edit_notifications.py +++ b/zerver/tests/test_message_edit_notifications.py @@ -8,7 +8,9 @@ from zerver.lib.push_notifications import get_apns_badge_count, get_apns_badge_count_future from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import mock_queue_publish -from zerver.models import NotificationTriggers, Subscription, UserPresence, UserTopic, get_stream +from zerver.models import Subscription, UserPresence, UserTopic +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.streams import get_stream from zerver.tornado.event_queue import maybe_enqueue_notifications diff --git a/zerver/tests/test_message_fetch.py b/zerver/tests/test_message_fetch.py index a4391638c0dac..4c062ee113b3e 100644 --- a/zerver/tests/test_message_fetch.py +++ b/zerver/tests/test_message_fetch.py @@ -18,6 +18,7 @@ from zerver.actions.user_settings import do_change_user_setting from zerver.actions.users import do_deactivate_user from zerver.lib.avatar import avatar_url +from zerver.lib.display_recipient import get_display_recipient from zerver.lib.exceptions import JsonableError from zerver.lib.mention import MentionBackend, MentionData from zerver.lib.message import ( @@ -57,10 +58,9 @@ UserMessage, UserProfile, UserTopic, - get_display_recipient, - get_realm, - get_stream, ) +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream from zerver.views.message_fetch import get_messages_backend if TYPE_CHECKING: diff --git a/zerver/tests/test_message_flags.py b/zerver/tests/test_message_flags.py index a7d46655a3a61..bc906e2c07ec4 100644 --- a/zerver/tests/test_message_flags.py +++ b/zerver/tests/test_message_flags.py @@ -33,9 +33,9 @@ UserMessage, UserProfile, UserTopic, - get_realm, - get_stream, ) +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse @@ -874,16 +874,11 @@ def send_unread_pm(other_user: UserProfile) -> Message: message_id = self.send_personal_message( from_user=hamlet, to_user=other_user, - sending_client_name="some_api_program", + read_by_sender=False, ) - - # Check our test setup is correct--the message should - # not have looked like it was sent by a human. message = Message.objects.get(id=message_id) - self.assertFalse(message.sent_by_human()) - # And since it was not sent by a human, it should not - # be read, not even by the sender (Hamlet). + # This message should not be read, not even by the sender (Hamlet). um = UserMessage.objects.get( user_profile_id=hamlet.id, message_id=message_id, diff --git a/zerver/tests/test_message_notification_emails.py b/zerver/tests/test_message_notification_emails.py index ac3fda6d6a77c..35557c1c6fda3 100644 --- a/zerver/tests/test_message_notification_emails.py +++ b/zerver/tests/test_message_notification_emails.py @@ -27,15 +27,11 @@ ) from zerver.lib.send_email import FromAddress from zerver.lib.test_classes import ZulipTestCase -from zerver.models import ( - NotificationTriggers, - UserMessage, - UserProfile, - UserTopic, - get_name_keyed_dict_for_active_realm_emoji, - get_realm, - get_stream, -) +from zerver.models import UserMessage, UserProfile, UserTopic +from zerver.models.realm_emoji import get_name_keyed_dict_for_active_realm_emoji +from zerver.models.realms import get_realm +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.streams import get_stream class TestMessageNotificationEmails(ZulipTestCase): diff --git a/zerver/tests/test_message_send.py b/zerver/tests/test_message_send.py index 8992a087d86a8..bfd4ac5e825f0 100644 --- a/zerver/tests/test_message_send.py +++ b/zerver/tests/test_message_send.py @@ -47,23 +47,22 @@ ) from zerver.lib.timestamp import datetime_to_timestamp from zerver.models import ( - MAX_TOPIC_NAME_LENGTH, Message, Realm, RealmDomain, Recipient, Stream, Subscription, - SystemGroups, UserGroup, UserMessage, UserProfile, - get_or_create_huddle, - get_realm, - get_stream, - get_system_bot, - get_user, ) +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_realm +from zerver.models.recipients import get_or_create_huddle +from zerver.models.streams import get_stream +from zerver.models.users import get_system_bot, get_user from zerver.views.message_send import InvalidMirrorInputError diff --git a/zerver/tests/test_message_topics.py b/zerver/tests/test_message_topics.py index ec65b21d2847d..a9f0b714b2ec8 100644 --- a/zerver/tests/test_message_topics.py +++ b/zerver/tests/test_message_topics.py @@ -6,7 +6,10 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import timeout_mock from zerver.lib.timeout import TimeoutExpiredError -from zerver.models import Message, UserMessage, get_client, get_realm, get_stream +from zerver.models import Message, UserMessage +from zerver.models.clients import get_client +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream class TopicHistoryTest(ZulipTestCase): diff --git a/zerver/tests/test_messages.py b/zerver/tests/test_messages.py index 2fb797158cf5e..5b08732aafc9d 100644 --- a/zerver/tests/test_messages.py +++ b/zerver/tests/test_messages.py @@ -5,13 +5,8 @@ from zerver.actions.message_send import get_active_presence_idle_user_ids from zerver.lib.test_classes import ZulipTestCase -from zerver.models import ( - Message, - UserPresence, - UserProfile, - bulk_get_huddle_user_ids, - get_huddle_user_ids, -) +from zerver.models import Message, UserPresence, UserProfile +from zerver.models.recipients import bulk_get_huddle_user_ids, get_huddle_user_ids class MissedMessageTest(ZulipTestCase): diff --git a/zerver/tests/test_middleware.py b/zerver/tests/test_middleware.py index 6182ba3806d83..e701719b68e7c 100644 --- a/zerver/tests/test_middleware.py +++ b/zerver/tests/test_middleware.py @@ -11,7 +11,7 @@ from zerver.lib.test_helpers import HostRequestMock from zerver.lib.utils import assert_is_not_none from zerver.middleware import LogRequests, is_slow_query, write_log_line -from zerver.models import get_realm +from zerver.models.realms import get_realm from zilencer.models import RemoteZulipServer diff --git a/zerver/tests/test_mirror_users.py b/zerver/tests/test_mirror_users.py index b310c4f80bcad..2f7e9e461a90b 100644 --- a/zerver/tests/test_mirror_users.py +++ b/zerver/tests/test_mirror_users.py @@ -8,7 +8,10 @@ from zerver.lib.create_user import create_user_profile from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import reset_email_visibility_to_everyone_in_zulip_realm -from zerver.models import UserProfile, get_client, get_realm, get_user +from zerver.models import UserProfile +from zerver.models.clients import get_client +from zerver.models.realms import get_realm +from zerver.models.users import get_user from zerver.views.message_send import InvalidMirrorInputError, create_mirrored_message_users diff --git a/zerver/tests/test_new_users.py b/zerver/tests/test_new_users.py index cb2e392fc2ef1..14e90cf88ed5e 100644 --- a/zerver/tests/test_new_users.py +++ b/zerver/tests/test_new_users.py @@ -13,7 +13,8 @@ from zerver.actions.user_settings import do_change_user_setting from zerver.lib.initial_password import initial_password from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Message, Realm, Recipient, Stream, UserProfile, get_realm +from zerver.models import Message, Realm, Recipient, Stream, UserProfile +from zerver.models.realms import get_realm from zerver.signals import JUST_CREATED_THRESHOLD, get_device_browser, get_device_os if sys.version_info < (3, 9): # nocoverage diff --git a/zerver/tests/test_notification_data.py b/zerver/tests/test_notification_data.py index c6ce2320bd54d..3367b0383a34d 100644 --- a/zerver/tests/test_notification_data.py +++ b/zerver/tests/test_notification_data.py @@ -2,7 +2,7 @@ from zerver.lib.mention import MentionBackend, MentionData from zerver.lib.notification_data import UserMessageNotificationsData, get_user_group_mentions_data from zerver.lib.test_classes import ZulipTestCase -from zerver.models import NotificationTriggers +from zerver.models.scheduled_jobs import NotificationTriggers class TestNotificationData(ZulipTestCase): diff --git a/zerver/tests/test_outgoing_webhook_interfaces.py b/zerver/tests/test_outgoing_webhook_interfaces.py index e56e3d2e45003..bcf1675ae3486 100644 --- a/zerver/tests/test_outgoing_webhook_interfaces.py +++ b/zerver/tests/test_outgoing_webhook_interfaces.py @@ -12,14 +12,12 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.topic import TOPIC_NAME -from zerver.models import ( - SLACK_INTERFACE, - Message, - NotificationTriggers, - get_realm, - get_stream, - get_user, -) +from zerver.models import Message +from zerver.models.bots import SLACK_INTERFACE +from zerver.models.realms import get_realm +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.streams import get_stream +from zerver.models.users import get_user from zerver.openapi.openapi import validate_against_openapi_schema diff --git a/zerver/tests/test_outgoing_webhook_system.py b/zerver/tests/test_outgoing_webhook_system.py index cda83d5ff3a18..2a9620e4dd905 100644 --- a/zerver/tests/test_outgoing_webhook_system.py +++ b/zerver/tests/test_outgoing_webhook_system.py @@ -19,7 +19,9 @@ from zerver.lib.topic import TOPIC_NAME from zerver.lib.url_encoding import near_message_url from zerver.lib.users import add_service -from zerver.models import Recipient, Service, UserProfile, get_realm, get_stream +from zerver.models import Recipient, Service, UserProfile +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream class ResponseMock: diff --git a/zerver/tests/test_presence.py b/zerver/tests/test_presence.py index cfbee31d56dd6..84a63328f26fe 100644 --- a/zerver/tests/test_presence.py +++ b/zerver/tests/test_presence.py @@ -18,8 +18,8 @@ UserActivityInterval, UserPresence, UserProfile, - get_realm, ) +from zerver.models.realms import get_realm class TestClientModel(ZulipTestCase): diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index 1094d68942112..397fd7cb004ab 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -1,5 +1,6 @@ import asyncio import base64 +import logging import uuid from contextlib import contextmanager from datetime import datetime, timedelta, timezone @@ -79,7 +80,6 @@ from zerver.lib.user_counts import realm_user_count_by_role from zerver.models import ( Message, - NotificationTriggers, PushDeviceToken, Realm, RealmAuditLog, @@ -87,11 +87,13 @@ Stream, Subscription, UserMessage, + UserProfile, UserTopic, - get_client, - get_realm, - get_stream, ) +from zerver.models.clients import get_client +from zerver.models.realms import get_realm +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.streams import get_stream from zilencer.models import RemoteZulipServerAuditLog from zilencer.views import DevicesToCleanUpDict @@ -603,7 +605,10 @@ def test_send_notification_endpoint(self) -> None: "zilencer.views.send_android_push_notification", return_value=2 ) as android_push, mock.patch( "zilencer.views.send_apple_push_notification", return_value=1 - ) as apple_push, self.assertLogs( + ) as apple_push, mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=10, + ), self.assertLogs( "zilencer.views", level="INFO" ) as logger: result = self.uuid_post( @@ -658,6 +663,210 @@ def test_send_notification_endpoint(self) -> None: remote=server, ) + def test_send_notification_endpoint_on_free_plans(self) -> None: + hamlet = self.example_user("hamlet") + remote_server = RemoteZulipServer.objects.get(uuid=self.server_uuid) + RemotePushDeviceToken.objects.create( + kind=RemotePushDeviceToken.GCM, + token=hex_to_b64("aaaaaa"), + user_id=hamlet.id, + server=remote_server, + ) + + current_time = now() + message = Message( + sender=hamlet, + recipient=self.example_user("othello").recipient, + realm_id=hamlet.realm_id, + content="This is test content", + rendered_content="This is test content", + date_sent=current_time, + sending_client=get_client("test"), + ) + message.save() + + # Test old zulip server case. + self.assertIsNone(remote_server.last_api_feature_level) + old_apns_payload = { + "alert": { + "title": "King Hamlet", + "subtitle": "", + "body": message.content, + }, + "badge": 0, + "sound": "default", + "custom": { + "zulip": { + "message_ids": [message.id], + "recipient_type": "private", + "sender_email": hamlet.email, + "sender_id": hamlet.id, + "server": settings.EXTERNAL_HOST, + "realm_id": hamlet.realm.id, + "realm_uri": hamlet.realm.uri, + "user_id": self.example_user("othello").id, + } + }, + } + old_gcm_payload = { + "user_id": self.example_user("othello").id, + "event": "message", + "alert": "New private message from King Hamlet", + "zulip_message_id": message.id, + "time": datetime_to_timestamp(message.date_sent), + "content": message.content, + "content_truncated": False, + "server": settings.EXTERNAL_HOST, + "realm_id": hamlet.realm.id, + "realm_uri": hamlet.realm.uri, + "sender_id": hamlet.id, + "sender_email": hamlet.email, + "sender_full_name": "King Hamlet", + "sender_avatar_url": absolute_avatar_url(message.sender), + "recipient_type": "private", + } + payload = { + "user_id": hamlet.id, + "gcm_payload": old_gcm_payload, + "apns_payload": old_apns_payload, + "gcm_options": {"priority": "high"}, + } + + result = self.uuid_post( + self.server_uuid, + "/api/v1/remotes/push/notify", + payload, + content_type="application/json", + ) + self.assertEqual(orjson.loads(result.content)["code"], "INVALID_ZULIP_SERVER") + + remote_server.last_api_feature_level = 235 + remote_server.save() + + gcm_payload, gcm_options = get_message_payload_gcm(hamlet, message) + apns_payload = get_message_payload_apns( + hamlet, message, NotificationTriggers.DIRECT_MESSAGE + ) + payload = { + "user_id": hamlet.id, + "user_uuid": str(hamlet.uuid), + "gcm_payload": gcm_payload, + "apns_payload": apns_payload, + "gcm_options": gcm_options, + } + + # Test the case when there is no data about users. + self.assertIsNone(remote_server.last_audit_log_update) + result = self.uuid_post( + self.server_uuid, + "/api/v1/remotes/push/notify", + payload, + content_type="application/json", + ) + self.assert_json_error(result, "Your plan doesn't allow sending push notifications.") + self.assertEqual(orjson.loads(result.content)["code"], "BAD_REQUEST") + + human_counts = { + str(UserProfile.ROLE_REALM_ADMINISTRATOR): 1, + str(UserProfile.ROLE_REALM_OWNER): 1, + str(UserProfile.ROLE_MODERATOR): 0, + str(UserProfile.ROLE_MEMBER): 7, + str(UserProfile.ROLE_GUEST): 2, + } + RemoteRealmAuditLog.objects.create( + server=remote_server, + event_type=RealmAuditLog.USER_CREATED, + event_time=current_time - timedelta(minutes=10), + extra_data={RealmAuditLog.ROLE_COUNT: {RealmAuditLog.ROLE_COUNT_HUMANS: human_counts}}, + ) + remote_server.last_audit_log_update = current_time - timedelta(minutes=10) + remote_server.save() + + result = self.uuid_post( + self.server_uuid, + "/api/v1/remotes/push/notify", + payload, + content_type="application/json", + ) + self.assert_json_error(result, "Your plan doesn't allow sending push notifications.") + self.assertEqual(orjson.loads(result.content)["code"], "BAD_REQUEST") + + # Check that sponsored realms are allowed to send push notifications. + remote_server.plan_type = RemoteRealm.PLAN_TYPE_COMMUNITY + remote_server.save() + with self.assertLogs("zilencer.views", level="INFO") as logger: + result = self.uuid_post( + self.server_uuid, + "/api/v1/remotes/push/notify", + payload, + content_type="application/json", + ) + data = self.assert_json_success(result) + self.assertEqual( + { + "result": "success", + "msg": "", + "realm": None, + "total_android_devices": 1, + "total_apple_devices": 0, + "deleted_devices": {"android_devices": [], "apple_devices": []}, + }, + data, + ) + self.assertIn( + "INFO:zilencer.views:" + f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:: " + "1 via FCM devices, 0 via APNs devices", + logger.output, + ) + + # Reset the plan_type to test remaining cases. + remote_server.plan_type = RemoteRealm.PLAN_TYPE_SELF_MANAGED + remote_server.save() + + human_counts = { + str(UserProfile.ROLE_REALM_ADMINISTRATOR): 1, + str(UserProfile.ROLE_REALM_OWNER): 1, + str(UserProfile.ROLE_MODERATOR): 0, + str(UserProfile.ROLE_MEMBER): 6, + str(UserProfile.ROLE_GUEST): 2, + } + + RemoteRealmAuditLog.objects.create( + server=remote_server, + event_type=RealmAuditLog.USER_DEACTIVATED, + event_time=current_time - timedelta(minutes=8), + extra_data={RealmAuditLog.ROLE_COUNT: {RealmAuditLog.ROLE_COUNT_HUMANS: human_counts}}, + ) + remote_server.last_audit_log_update = current_time - timedelta(minutes=8) + remote_server.save() + + with self.assertLogs("zilencer.views", level="INFO") as logger: + result = self.uuid_post( + self.server_uuid, + "/api/v1/remotes/push/notify", + payload, + content_type="application/json", + ) + data = self.assert_json_success(result) + self.assertEqual( + { + "result": "success", + "msg": "", + "realm": None, + "total_android_devices": 1, + "total_apple_devices": 0, + "deleted_devices": {"android_devices": [], "apple_devices": []}, + }, + data, + ) + self.assertIn( + "INFO:zilencer.views:" + f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:: " + "1 via FCM devices, 0 via APNs devices", + logger.output, + ) + def test_subsecond_timestamp_format(self) -> None: hamlet = self.example_user("hamlet") RemotePushDeviceToken.objects.create( @@ -703,8 +912,9 @@ def test_subsecond_timestamp_format(self) -> None: time_received = time_sent + timedelta(seconds=1, milliseconds=234) with time_machine.travel(time_received, tick=False), mock.patch( "zilencer.views.send_android_push_notification", return_value=1 - ), mock.patch( - "zilencer.views.send_apple_push_notification", return_value=1 + ), mock.patch("zilencer.views.send_apple_push_notification", return_value=1), mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=10, ), self.assertLogs( "zilencer.views", level="INFO" ) as logger: @@ -1026,28 +1236,115 @@ def test_push_bouncer_api(self) -> None: class AnalyticsBouncerTest(BouncerTestCase): TIME_ZERO = datetime(1988, 3, 14, tzinfo=timezone.utc) + def assertPushNotificationsAre(self, should_be: bool) -> None: + self.assertEqual( + {should_be}, + set( + Realm.objects.all().distinct().values_list("push_notifications_enabled", flat=True) + ), + ) + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @responses.activate - def test_analytics_api(self) -> None: - """This is a variant of the below test_push_api, but using the full - push notification bouncer flow - """ + def test_analytics_failure_api(self) -> None: assert settings.PUSH_NOTIFICATION_BOUNCER_URL is not None ANALYTICS_URL = settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/server/analytics" ANALYTICS_STATUS_URL = ANALYTICS_URL + "/status" - user = self.example_user("hamlet") - end_time = self.TIME_ZERO with responses.RequestsMock() as resp, self.assertLogs( "zulip.analytics", level="WARNING" ) as mock_warning: resp.add(responses.GET, ANALYTICS_STATUS_URL, body=ConnectionError()) + Realm.objects.all().update(push_notifications_enabled=True) + send_server_data_to_push_bouncer() + self.assertEqual( + "WARNING:zulip.analytics:ConnectionError while trying to connect to push notification bouncer", + mock_warning.output[0], + ) + self.assertTrue(resp.assert_call_count(ANALYTICS_STATUS_URL, 1)) + self.assertPushNotificationsAre(False) + + with responses.RequestsMock() as resp, self.assertLogs( + "zulip.analytics", level="WARNING" + ) as mock_warning: + resp.add(responses.GET, ANALYTICS_STATUS_URL, body="This is not JSON") + Realm.objects.all().update(push_notifications_enabled=True) + send_server_data_to_push_bouncer() + self.assertTrue( + mock_warning.output[0].startswith( + f"ERROR:zulip.analytics:Exception communicating with {settings.PUSH_NOTIFICATION_BOUNCER_URL}\nTraceback", + ) + ) + self.assertTrue(resp.assert_call_count(ANALYTICS_STATUS_URL, 1)) + self.assertPushNotificationsAre(False) + + with responses.RequestsMock() as resp, self.assertLogs("", level="WARNING") as mock_warning: + resp.add(responses.GET, ANALYTICS_STATUS_URL, body="Server error", status=502) + Realm.objects.all().update(push_notifications_enabled=True) + send_server_data_to_push_bouncer() + self.assertEqual( + "WARNING:root:Received 502 from push notification bouncer", + mock_warning.output[0], + ) + self.assertTrue(resp.assert_call_count(ANALYTICS_STATUS_URL, 1)) + self.assertPushNotificationsAre(True) + + with responses.RequestsMock() as resp, self.assertLogs( + "zulip.analytics", level="WARNING" + ) as mock_warning: + Realm.objects.all().update(push_notifications_enabled=True) + resp.add( + responses.GET, + ANALYTICS_STATUS_URL, + status=401, + json={"CODE": "UNAUTHORIZED", "msg": "Some problem", "result": "error"}, + ) send_server_data_to_push_bouncer() self.assertIn( - "WARNING:zulip.analytics:ConnectionError while trying to connect to push notification bouncer\nTraceback ", + "WARNING:zulip.analytics:Some problem", mock_warning.output[0], ) self.assertTrue(resp.assert_call_count(ANALYTICS_STATUS_URL, 1)) + self.assertPushNotificationsAre(False) + + with responses.RequestsMock() as resp, self.assertLogs( + "zulip.analytics", level="WARNING" + ) as mock_warning: + Realm.objects.all().update(push_notifications_enabled=True) + resp.add( + responses.GET, + ANALYTICS_STATUS_URL, + json={ + "last_realm_count_id": 0, + "last_installation_count_id": 0, + "last_realmauditlog_id": 0, + }, + ) + resp.add( + responses.POST, + ANALYTICS_URL, + status=401, + json={"CODE": "UNAUTHORIZED", "msg": "Some problem", "result": "error"}, + ) + send_server_data_to_push_bouncer() + self.assertIn( + "WARNING:zulip.analytics:Some problem", + mock_warning.output[0], + ) + self.assertTrue(resp.assert_call_count(ANALYTICS_URL, 1)) + self.assertPushNotificationsAre(False) + + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") + @responses.activate + def test_analytics_api(self) -> None: + """This is a variant of the below test_push_api, but using the full + push notification bouncer flow + """ + assert settings.PUSH_NOTIFICATION_BOUNCER_URL is not None + ANALYTICS_URL = settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/server/analytics" + ANALYTICS_STATUS_URL = ANALYTICS_URL + "/status" + user = self.example_user("hamlet") + end_time = self.TIME_ZERO self.add_mock_response() # Send any existing data over, so that we can start the test with a "clean" slate @@ -1171,7 +1468,7 @@ def check_counts( "realm_date_created": realm.date_created, "registration_deactivated": False, "realm_deactivated": False, - "plan_type": RemoteRealm.PLAN_TYPE_SELF_HOSTED, + "plan_type": RemoteRealm.PLAN_TYPE_SELF_MANAGED, "is_system_bot_realm": realm.string_id == "zulipinternal", } for realm in Realm.objects.order_by("id") @@ -1543,7 +1840,9 @@ def test_analytics_api_foreign_keys_to_remote_realm(self) -> None: modified_user=user, event_type=RealmAuditLog.USER_CREATED, event_time=end_time, - extra_data={"data": "foo"}, + extra_data={ + RealmAuditLog.ROLE_COUNT: realm_user_count_by_role(user.realm), + }, ) send_server_data_to_push_bouncer() @@ -1613,7 +1912,7 @@ def test_remote_realm_duplicate_uuid(self) -> None: realm_date_created=realm.date_created, registration_deactivated=False, realm_deactivated=False, - plan_type=RemoteRealm.PLAN_TYPE_SELF_HOSTED, + plan_type=RemoteRealm.PLAN_TYPE_SELF_MANAGED, ) with transaction.atomic(), self.assertLogs("zulip.analytics", level="WARNING") as m: @@ -1736,7 +2035,13 @@ def verify_request_with_overridden_extra_data( modified_user=user, event_type=RealmAuditLog.USER_REACTIVATED, event_time=self.TIME_ZERO, - extra_data=orjson.dumps({RealmAuditLog.ROLE_COUNT: 0}).decode(), + extra_data=orjson.dumps( + { + RealmAuditLog.ROLE_COUNT: { + RealmAuditLog.ROLE_COUNT_HUMANS: {}, + } + } + ).decode(), ) # We use this to patch send_to_push_bouncer so that extra_data in the @@ -1775,14 +2080,32 @@ def transform_realmauditlog_extra_data( # Pre-migration extra_data verify_request_with_overridden_extra_data( - request_extra_data=orjson.dumps({"fake_data": 42}).decode(), - expected_extra_data={"fake_data": 42}, + request_extra_data=orjson.dumps( + { + RealmAuditLog.ROLE_COUNT: { + RealmAuditLog.ROLE_COUNT_HUMANS: {}, + } + } + ).decode(), + expected_extra_data={ + RealmAuditLog.ROLE_COUNT: { + RealmAuditLog.ROLE_COUNT_HUMANS: {}, + } + }, ) verify_request_with_overridden_extra_data(request_extra_data=None, expected_extra_data={}) # Post-migration extra_data verify_request_with_overridden_extra_data( - request_extra_data={"fake_data": 42}, - expected_extra_data={"fake_data": 42}, + request_extra_data={ + RealmAuditLog.ROLE_COUNT: { + RealmAuditLog.ROLE_COUNT_HUMANS: {}, + } + }, + expected_extra_data={ + RealmAuditLog.ROLE_COUNT: { + RealmAuditLog.ROLE_COUNT_HUMANS: {}, + } + }, ) verify_request_with_overridden_extra_data( request_extra_data={}, @@ -1801,19 +2124,38 @@ def transform_realmauditlog_extra_data( def test_realm_properties_after_send_analytics(self) -> None: self.add_mock_response() + with mock.patch( + "corporate.lib.stripe.RemoteRealmBillingSession.get_customer", return_value=None + ) as m: + with mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=10, + ): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + m.assert_called() + realms = Realm.objects.all() + for realm in realms: + self.assertEqual(realm.push_notifications_enabled, True) + self.assertEqual(realm.push_notifications_enabled_end_timestamp, None) + with mock.patch( "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=None ) as m: - send_server_data_to_push_bouncer(consider_usage_statistics=False) - m.assert_called() - realms = Realm.objects.all() - for realm in realms: - self.assertEqual(realm.push_notifications_enabled, True) - self.assertEqual(realm.push_notifications_enabled_end_timestamp, None) + with mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=11, + ): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + m.assert_called() + realms = Realm.objects.all() + for realm in realms: + self.assertEqual(realm.push_notifications_enabled, False) + self.assertEqual(realm.push_notifications_enabled_end_timestamp, None) dummy_customer = mock.MagicMock() with mock.patch( - "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer + "corporate.lib.stripe.RemoteRealmBillingSession.get_customer", + return_value=dummy_customer, ): with mock.patch( "corporate.lib.stripe.get_current_plan_by_customer", return_value=None @@ -1825,18 +2167,63 @@ def test_realm_properties_after_send_analytics(self) -> None: self.assertEqual(realm.push_notifications_enabled, True) self.assertEqual(realm.push_notifications_enabled_end_timestamp, None) + dummy_customer = mock.MagicMock() + with mock.patch( + "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer + ): + with mock.patch( + "corporate.lib.stripe.get_current_plan_by_customer", return_value=None + ) as m: + with mock.patch( + "corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses", + return_value=11, + ): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + m.assert_called() + realms = Realm.objects.all() + for realm in realms: + self.assertEqual(realm.push_notifications_enabled, False) + self.assertEqual(realm.push_notifications_enabled_end_timestamp, None) + + RemoteRealm.objects.filter(server=self.server).update( + plan_type=RemoteRealm.PLAN_TYPE_COMMUNITY + ) + + with mock.patch( + "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer + ) as m: + with mock.patch( + "corporate.lib.stripe.get_current_plan_by_customer", return_value=None + ) as m: + with mock.patch( + "corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses", + return_value=11, + ): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + m.assert_called() + realms = Realm.objects.all() + for realm in realms: + self.assertEqual(realm.push_notifications_enabled, True) + self.assertEqual(realm.push_notifications_enabled_end_timestamp, None) + + # Reset the plan type to test remaining cases. + RemoteRealm.objects.filter(server=self.server).update( + plan_type=RemoteRealm.PLAN_TYPE_SELF_MANAGED + ) + dummy_customer_plan = mock.MagicMock() dummy_customer_plan.status = CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE dummy_date = datetime(year=2023, month=12, day=3, tzinfo=timezone.utc) with mock.patch( - "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer + "corporate.lib.stripe.RemoteRealmBillingSession.get_customer", + return_value=dummy_customer, ): with mock.patch( "corporate.lib.stripe.get_current_plan_by_customer", return_value=dummy_customer_plan, ): with mock.patch( - "zilencer.views.RemoteRealmBillingSession.get_next_billing_cycle", + "corporate.lib.stripe.RemoteRealmBillingSession.get_next_billing_cycle", return_value=dummy_date, ) as m, self.assertLogs("zulip.analytics", level="INFO") as info_log: send_server_data_to_push_bouncer(consider_usage_statistics=False) @@ -1853,8 +2240,33 @@ def test_realm_properties_after_send_analytics(self) -> None: info_log.output[0], ) + dummy_customer_plan = mock.MagicMock() + dummy_customer_plan.status = CustomerPlan.ACTIVE + with mock.patch( + "corporate.lib.stripe.RemoteRealmBillingSession.get_customer", + return_value=dummy_customer, + ): + with mock.patch( + "corporate.lib.stripe.get_current_plan_by_customer", + return_value=dummy_customer_plan, + ): + with self.assertLogs("zulip.analytics", level="INFO") as info_log: + send_server_data_to_push_bouncer(consider_usage_statistics=False) + m.assert_called() + realms = Realm.objects.all() + for realm in realms: + self.assertEqual(realm.push_notifications_enabled, True) + self.assertEqual( + realm.push_notifications_enabled_end_timestamp, + None, + ) + self.assertIn( + "INFO:zulip.analytics:Reported 0 records", + info_log.output[0], + ) + with mock.patch("zerver.lib.remote_server.send_to_push_bouncer") as m, self.assertLogs( - "zulip.analytics", level="ERROR" + "zulip.analytics", level="WARNING" ) as exception_log: get_response = { "last_realm_count_id": 0, @@ -1864,7 +2276,7 @@ def test_realm_properties_after_send_analytics(self) -> None: def mock_send_to_push_bouncer_response(method: str, *args: Any) -> Dict[str, int]: if method == "POST": - raise Exception + raise PushNotificationBouncerRetryLaterError("Some problem") return get_response m.side_effect = mock_send_to_push_bouncer_response @@ -1874,10 +2286,10 @@ def mock_send_to_push_bouncer_response(method: str, *args: Any) -> Dict[str, int realms = Realm.objects.all() for realm in realms: self.assertFalse(realm.push_notifications_enabled) - self.assertIn( - "ERROR:zulip.analytics:Exception asking push bouncer if notifications will work.", - exception_log.output[0], - ) + self.assertEqual( + exception_log.output, + ["WARNING:zulip.analytics:Some problem"], + ) send_server_data_to_push_bouncer(consider_usage_statistics=False) @@ -1903,12 +2315,68 @@ def mock_send_to_push_bouncer_response(method: str, *args: Any) -> Dict[str, int "realm_date_created": realm.date_created, "registration_deactivated": False, "realm_deactivated": False, - "plan_type": RemoteRealm.PLAN_TYPE_SELF_HOSTED, + "plan_type": RemoteRealm.PLAN_TYPE_SELF_MANAGED, } for realm in Realm.objects.order_by("id") ], ) + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") + @responses.activate + def test_deleted_realm(self) -> None: + self.add_mock_response() + logger = logging.getLogger("zulip.analytics") + + realm_info = get_realms_info_for_push_bouncer() + + # Hard-delete a realm to test the non existent realm uuid case. + realm = get_realm("zephyr") + assert realm is not None + deleted_realm_uuid = realm.uuid + realm.delete() + + # This mock causes us to still send data to the bouncer as if the realm existed, + # causing the bouncer to include its corresponding info in the response. Through + # that, we're testing our graceful handling of seeing a non-existent realm uuid + # in that response. + with mock.patch( + "zerver.lib.remote_server.get_realms_info_for_push_bouncer", return_value=realm_info + ) as m, self.assertLogs(logger, level="WARNING") as analytics_logger: + send_server_data_to_push_bouncer(consider_usage_statistics=False) + m.assert_called() + realms = Realm.objects.all() + for realm in realms: + self.assertEqual(realm.push_notifications_enabled, True) + self.assertEqual(realm.push_notifications_enabled_end_timestamp, None) + + self.assertEqual( + analytics_logger.output, + [ + "WARNING:zulip.analytics:" + f"Received unexpected realm UUID from bouncer {deleted_realm_uuid}" + ], + ) + + # Now we want to test the other side of this - bouncer's handling + # of a deleted realm. + with self.assertLogs(logger, level="WARNING") as analytics_logger: + # This time the logger shouldn't get triggered - because the bouncer doesn't + # include .realm_locally_deleted realms in its response. + # Note: This is hacky, because until Python 3.10 we don't have access to + # assertNoLogs - and regular assertLogs demands that the logger gets triggered. + # So we do a dummy warning ourselves here, to satisfy it. + # TODO: Replace this with assertNoLogs once we fully upgrade to Python 3.10. + logger.warning("Dummy warning") + send_server_data_to_push_bouncer(consider_usage_statistics=False) + remote_realm_for_deleted_realm = RemoteRealm.objects.get(uuid=deleted_realm_uuid) + self.assertEqual(remote_realm_for_deleted_realm.registration_deactivated, True) + self.assertEqual(remote_realm_for_deleted_realm.realm_locally_deleted, True) + self.assertEqual(analytics_logger.output, ["WARNING:zulip.analytics:Dummy warning"]) + + audit_log = RemoteRealmAuditLog.objects.latest("id") + self.assertEqual(audit_log.event_type, RemoteRealmAuditLog.REMOTE_REALM_LOCALLY_DELETED) + self.assertEqual(audit_log.remote_realm, remote_realm_for_deleted_realm) + class PushNotificationTest(BouncerTestCase): @override @@ -2058,7 +2526,10 @@ def test_end_to_end(self) -> None: } with time_machine.travel(time_received, tick=False), mock.patch( "zerver.lib.push_notifications.gcm_client" - ) as mock_gcm, self.mock_apns() as (apns_context, send_notification), self.assertLogs( + ) as mock_gcm, self.mock_apns() as (apns_context, send_notification), mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=10, + ), self.assertLogs( "zerver.lib.push_notifications", level="INFO" ) as pn_logger, self.assertLogs( "zilencer.views", level="INFO" @@ -2145,7 +2616,10 @@ def test_unregistered_client(self) -> None: } with time_machine.travel(time_received, tick=False), mock.patch( "zerver.lib.push_notifications.gcm_client" - ) as mock_gcm, self.mock_apns() as (apns_context, send_notification), self.assertLogs( + ) as mock_gcm, self.mock_apns() as (apns_context, send_notification), mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses", + return_value=10, + ), self.assertLogs( "zerver.lib.push_notifications", level="INFO" ) as pn_logger, self.assertLogs( "zilencer.views", level="INFO" @@ -2388,27 +2862,40 @@ def test_send_notifications_to_bouncer(self) -> None: "zerver.lib.push_notifications.get_message_payload_gcm", return_value=({"gcm": True}, {}), ), mock.patch( - "zerver.lib.push_notifications.send_notifications_to_bouncer", return_value=(3, 5) + "zerver.lib.push_notifications.send_json_to_push_bouncer", + return_value=dict( + total_android_devices=3, + total_apple_devices=5, + deleted_devices=DevicesToCleanUpDict(android_devices=[], apple_devices=[]), + realm=None, + ), ) as mock_send, self.assertLogs( "zerver.lib.push_notifications", level="INFO" ) as mock_logging_info: handle_push_notification(user_profile.id, missed_message) mock_send.assert_called_with( - user_profile, - {"apns": True}, - {"gcm": True}, - {}, - list( - PushDeviceToken.objects.filter( - user=user_profile, kind=PushDeviceToken.GCM - ).order_by("id") - ), - list( - PushDeviceToken.objects.filter( - user=user_profile, kind=PushDeviceToken.APNS - ).order_by("id") - ), + "POST", + "push/notify", + { + "user_uuid": str(user_profile.uuid), + "user_id": user_profile.id, + "realm_uuid": str(user_profile.realm.uuid), + "apns_payload": {"apns": True}, + "gcm_payload": {"gcm": True}, + "gcm_options": {}, + "android_devices": list( + PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.GCM) + .order_by("id") + .values_list("token", flat=True) + ), + "apple_devices": list( + PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.APNS) + .order_by("id") + .values_list("token", flat=True) + ), + }, ) + self.assertEqual( mock_logging_info.output, [ @@ -3585,13 +4072,19 @@ class TestSendNotificationsToBouncer(PushNotificationTest): def test_send_notifications_to_bouncer_when_no_devices(self) -> None: user = self.example_user("hamlet") - with mock.patch("zerver.lib.remote_server.send_to_push_bouncer") as mock_send: - total_android_devices, total_apple_devices = send_notifications_to_bouncer( + with mock.patch( + "zerver.lib.remote_server.send_to_push_bouncer" + ) as mock_send, self.assertLogs( + "zerver.lib.push_notifications", level="INFO" + ) as mock_logging_info: + send_notifications_to_bouncer( user, {"apns": True}, {"gcm": True}, {}, android_devices=[], apple_devices=[] ) - self.assertEqual(total_android_devices, 0) - self.assertEqual(total_apple_devices, 0) + self.assertIn( + f"INFO:zerver.lib.push_notifications:Skipping contacting the bouncer for user {user.id} because there are no registered devices", + mock_logging_info.output, + ) mock_send.assert_not_called() @mock.patch("zerver.lib.remote_server.send_to_push_bouncer") @@ -3621,9 +4114,10 @@ def test_send_notifications_to_bouncer(self, mock_send: mock.MagicMock) -> None: ), "realm": {"can_push": True, "expected_end_timestamp": None}, } - total_android_devices, total_apple_devices = send_notifications_to_bouncer( - user, {"apns": True}, {"gcm": True}, {}, list(android_devices), list(apple_devices) - ) + with self.assertLogs("zerver.lib.push_notifications", level="INFO") as mock_logging_info: + send_notifications_to_bouncer( + user, {"apns": True}, {"gcm": True}, {}, list(android_devices), list(apple_devices) + ) post_data = { "user_uuid": user.uuid, "user_id": user.id, @@ -3640,8 +4134,10 @@ def test_send_notifications_to_bouncer(self, mock_send: mock.MagicMock) -> None: orjson.dumps(post_data), extra_headers={"Content-type": "application/json"}, ) - self.assertEqual(total_android_devices, 1) - self.assertEqual(total_apple_devices, 3) + self.assertIn( + f"INFO:zerver.lib.push_notifications:Sent mobile push notifications for user {user.id} through bouncer: 1 via FCM devices, 3 via APNs devices", + mock_logging_info.output, + ) remote_realm_count = RealmCount.objects.values("property", "subgroup", "value").last() self.assertEqual( @@ -3649,7 +4145,7 @@ def test_send_notifications_to_bouncer(self, mock_send: mock.MagicMock) -> None: dict( property="mobile_pushes_sent::day", subgroup=None, - value=total_android_devices + total_apple_devices, + value=4, ), ) @@ -3667,7 +4163,7 @@ def test_send_notifications_to_bouncer(self, mock_send: mock.MagicMock) -> None: apple_devices=[], ), } - total_android_devices, total_apple_devices = send_notifications_to_bouncer( + send_notifications_to_bouncer( user, {"apns": True}, {"gcm": True}, {}, list(android_devices), list(apple_devices) ) user.realm.refresh_from_db() @@ -4141,6 +4637,16 @@ def test_deactivate_remote_server(self) -> None: status_code=401, ) + # Now try to do a request to server/register again. Normally, this updates + # the server's registration details. But the server is deactivated, so it + # should return the corresponding error. + result = self.client_post("/api/v1/remotes/server/register", request) + self.assert_json_error( + result, + "The mobile push notification service registration for your server has been deactivated", + status_code=401, + ) + def test_push_signup_invalid_host(self) -> None: zulip_org_id = str(uuid.uuid4()) zulip_org_key = get_random_string(64) diff --git a/zerver/tests/test_queue_worker.py b/zerver/tests/test_queue_worker.py index c0a90309d272a..2e54549ba032c 100644 --- a/zerver/tests/test_queue_worker.py +++ b/zerver/tests/test_queue_worker.py @@ -26,15 +26,15 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import mock_queue_publish from zerver.models import ( - NotificationTriggers, PreregistrationUser, ScheduledMessageNotificationEmail, UserActivity, UserProfile, - get_client, - get_realm, - get_stream, ) +from zerver.models.clients import get_client +from zerver.models.realms import get_realm +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.streams import get_stream from zerver.tornado.event_queue import build_offline_notification from zerver.worker import queue_processors from zerver.worker.queue_processors import ( diff --git a/zerver/tests/test_reactions.py b/zerver/tests/test_reactions.py index b015896e04536..fdd94adc21e06 100644 --- a/zerver/tests/test_reactions.py +++ b/zerver/tests/test_reactions.py @@ -12,7 +12,8 @@ from zerver.lib.message import extract_message_dict from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import zulip_reaction_info -from zerver.models import Message, Reaction, RealmEmoji, UserMessage, get_realm +from zerver.models import Message, Reaction, RealmEmoji, UserMessage +from zerver.models.realms import get_realm if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index bdd6c59dca900..f93d525da6ca9 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -1,6 +1,8 @@ import json import os +import random import re +import string from datetime import datetime, timedelta from typing import Any, Dict, List, Union from unittest import mock @@ -47,16 +49,15 @@ RealmUserDefault, ScheduledEmail, Stream, - SystemGroups, UserGroup, UserGroupMembership, UserMessage, UserProfile, - get_realm, - get_stream, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_system_bot, get_user_profile_by_id class RealmTest(ZulipTestCase): @@ -927,6 +928,12 @@ def test_jitsi_server_url(self) -> None: result = self.client_patch("/json/realm", req) self.assert_json_error(result, "jitsi_server_url is not an allowed_type") + url_string = "".join(random.choices(string.ascii_lowercase, k=180)) + long_url = "https://jitsi.example.com/" + url_string + req = dict(jitsi_server_url=orjson.dumps(long_url).decode()) + result = self.client_patch("/json/realm", req) + self.assert_json_error(result, "jitsi_server_url is not an allowed_type") + valid_url = "https://jitsi.example.com" req = dict(jitsi_server_url=orjson.dumps(valid_url).decode()) result = self.client_patch("/json/realm", req) diff --git a/zerver/tests/test_realm_domains.py b/zerver/tests/test_realm_domains.py index 9faacd31ec09e..e78b62048a2b2 100644 --- a/zerver/tests/test_realm_domains.py +++ b/zerver/tests/test_realm_domains.py @@ -10,7 +10,8 @@ from zerver.lib.domains import validate_domain from zerver.lib.email_validation import email_allowed_for_realm from zerver.lib.test_classes import ZulipTestCase -from zerver.models import DomainNotAllowedForRealmError, RealmDomain, UserProfile, get_realm +from zerver.models import RealmDomain, UserProfile +from zerver.models.realms import DomainNotAllowedForRealmError, get_realm class RealmDomainTest(ZulipTestCase): diff --git a/zerver/tests/test_realm_emoji.py b/zerver/tests/test_realm_emoji.py index 28c592172956b..603caa3723e25 100644 --- a/zerver/tests/test_realm_emoji.py +++ b/zerver/tests/test_realm_emoji.py @@ -9,7 +9,8 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import get_test_image_file from zerver.lib.upload.base import BadImageError -from zerver.models import Realm, RealmEmoji, UserProfile, get_realm +from zerver.models import Realm, RealmEmoji, UserProfile +from zerver.models.realms import get_realm class RealmEmojiTest(ZulipTestCase): diff --git a/zerver/tests/test_realm_linkifiers.py b/zerver/tests/test_realm_linkifiers.py index c4a7d3f40689d..dd32df67ac2dc 100644 --- a/zerver/tests/test_realm_linkifiers.py +++ b/zerver/tests/test_realm_linkifiers.py @@ -6,7 +6,8 @@ from typing_extensions import override from zerver.lib.test_classes import ZulipTestCase -from zerver.models import RealmAuditLog, RealmFilter, url_template_validator +from zerver.models import RealmAuditLog, RealmFilter +from zerver.models.linkifiers import url_template_validator class RealmFilterTest(ZulipTestCase): diff --git a/zerver/tests/test_realm_playgrounds.py b/zerver/tests/test_realm_playgrounds.py index 97ac4fe43e81e..45e23b8de1106 100644 --- a/zerver/tests/test_realm_playgrounds.py +++ b/zerver/tests/test_realm_playgrounds.py @@ -1,6 +1,7 @@ from zerver.actions.realm_playgrounds import check_add_realm_playground from zerver.lib.test_classes import ZulipTestCase -from zerver.models import RealmPlayground, get_realm +from zerver.models import RealmPlayground +from zerver.models.realms import get_realm class RealmPlaygroundTests(ZulipTestCase): diff --git a/zerver/tests/test_retention.py b/zerver/tests/test_retention.py index 6523897d37ca4..f4c2b5b32953a 100644 --- a/zerver/tests/test_retention.py +++ b/zerver/tests/test_retention.py @@ -37,11 +37,11 @@ Stream, SubMessage, UserMessage, - get_client, - get_realm, - get_stream, - get_system_bot, ) +from zerver.models.clients import get_client +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_system_bot # Class with helper functions useful for testing archiving of reactions: from zerver.tornado.django_api import send_event diff --git a/zerver/tests/test_rocketchat_importer.py b/zerver/tests/test_rocketchat_importer.py index 7ea2717702872..f11533b5d2a88 100644 --- a/zerver/tests/test_rocketchat_importer.py +++ b/zerver/tests/test_rocketchat_importer.py @@ -28,7 +28,9 @@ from zerver.lib.emoji import name_to_codepoint from zerver.lib.import_realm import do_import_realm from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Message, Reaction, Recipient, UserProfile, get_realm, get_user +from zerver.models import Message, Reaction, Recipient, UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_user class RocketChatImporter(ZulipTestCase): diff --git a/zerver/tests/test_scim.py b/zerver/tests/test_scim.py index 1bab9984d46da..600c191de5f45 100644 --- a/zerver/tests/test_scim.py +++ b/zerver/tests/test_scim.py @@ -10,7 +10,8 @@ from zerver.actions.user_settings import do_change_full_name from zerver.lib.scim import ZulipSCIMUser from zerver.lib.test_classes import ZulipTestCase -from zerver.models import UserProfile, get_realm +from zerver.models import UserProfile +from zerver.models.realms import get_realm if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/tests/test_service_bot_system.py b/zerver/tests/test_service_bot_system.py index 5eda173fbcef2..ce1a0805fbc68 100644 --- a/zerver/tests/test_service_bot_system.py +++ b/zerver/tests/test_service_bot_system.py @@ -15,7 +15,9 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import mock_queue_publish from zerver.lib.validator import check_string -from zerver.models import NotificationTriggers, Recipient, UserProfile, get_realm +from zerver.models import Recipient, UserProfile +from zerver.models.realms import get_realm +from zerver.models.scheduled_jobs import NotificationTriggers BOT_TYPE_TO_QUEUE_NAME = { UserProfile.OUTGOING_WEBHOOK_BOT: "outgoing_webhooks", diff --git a/zerver/tests/test_sessions.py b/zerver/tests/test_sessions.py index ccebe1980f5a9..1d77a28a27963 100644 --- a/zerver/tests/test_sessions.py +++ b/zerver/tests/test_sessions.py @@ -18,7 +18,8 @@ user_sessions, ) from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Realm, UserProfile, get_realm +from zerver.models import Realm, UserProfile +from zerver.models.realms import get_realm class TestSessions(ZulipTestCase): diff --git a/zerver/tests/test_settings.py b/zerver/tests/test_settings.py index 29b1fbef5b86d..2862388040375 100644 --- a/zerver/tests/test_settings.py +++ b/zerver/tests/test_settings.py @@ -11,13 +11,9 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import get_test_image_file, ratelimit_rule from zerver.lib.users import get_all_api_keys -from zerver.models import ( - Draft, - NotificationTriggers, - ScheduledMessageNotificationEmail, - UserProfile, - get_user_profile_by_api_key, -) +from zerver.models import Draft, ScheduledMessageNotificationEmail, UserProfile +from zerver.models.scheduled_jobs import NotificationTriggers +from zerver.models.users import get_user_profile_by_api_key if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 95733c6ebc08d..891b3167a4063 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -76,12 +76,10 @@ Subscription, UserMessage, UserProfile, - get_realm, - get_stream, - get_system_bot, - get_user, - get_user_by_delivery_email, ) +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream +from zerver.models.users import get_system_bot, get_user, get_user_by_delivery_email from zerver.views.auth import redirect_and_log_into_subdomain, start_two_factor_auth from zerver.views.development.registration import confirmation_key from zproject.backends import ExternalAuthDataDict, ExternalAuthResult, email_auth_enabled @@ -2935,6 +2933,24 @@ def test_failed_signup_due_to_nonexistent_realm(self) -> None: form.errors["email"][0], ) + def test_signup_confirm_injection(self) -> None: + result = self.client_get("/accounts/send_confirm/?email=bogus@example.com") + self.assert_in_success_response( + [ + 'check your email account (bogus@example.com)' + ], + result, + ) + + result = self.client_get( + "/accounts/send_confirm/?email={quote(email)}", + {"email": "bogus@example.com for example"}, + ) + self.assertEqual(result.status_code, 400) + self.assert_in_response( + "The email address you are trying to sign up with is not valid", result + ) + def test_access_signup_page_in_root_domain_without_realm(self) -> None: result = self.client_get("/register", subdomain="", follow=True) self.assert_in_success_response(["Find your Zulip accounts"], result) diff --git a/zerver/tests/test_slack_importer.py b/zerver/tests/test_slack_importer.py index b1c6d8473ae88..09d27bf805f06 100644 --- a/zerver/tests/test_slack_importer.py +++ b/zerver/tests/test_slack_importer.py @@ -48,7 +48,8 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import read_test_image_file from zerver.lib.topic import EXPORT_TOPIC_NAME -from zerver.models import Message, Realm, RealmAuditLog, Recipient, UserProfile, get_realm +from zerver.models import Message, Realm, RealmAuditLog, Recipient, UserProfile +from zerver.models.realms import get_realm def remove_folder(path: str) -> None: diff --git a/zerver/tests/test_soft_deactivation.py b/zerver/tests/test_soft_deactivation.py index 71f484f7e8b2e..d7dea720c78b0 100644 --- a/zerver/tests/test_soft_deactivation.py +++ b/zerver/tests/test_soft_deactivation.py @@ -27,9 +27,9 @@ UserActivity, UserMessage, UserProfile, - get_realm, - get_stream, ) +from zerver.models.realms import get_realm +from zerver.models.streams import get_stream logger_string = "zulip.soft_deactivation" diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 16f64672ce367..fbdb1a4a45692 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -38,6 +38,10 @@ ) from zerver.actions.user_groups import add_subgroups_to_user_group, check_add_user_group from zerver.actions.users import do_change_user_role, do_deactivate_user +from zerver.lib.attachments import ( + validate_attachment_request, + validate_attachment_request_for_spectator_access, +) from zerver.lib.default_streams import ( get_default_stream_ids_for_realm, get_default_streams_for_realm_as_dicts, @@ -104,15 +108,10 @@ UserGroup, UserMessage, UserProfile, - active_non_guest_user_ids, - get_default_stream_groups, - get_realm, - get_stream, - get_user, - get_user_profile_by_id_in_realm, - validate_attachment_request, - validate_attachment_request_for_spectator_access, ) +from zerver.models.realms import get_realm +from zerver.models.streams import get_default_stream_groups, get_stream +from zerver.models.users import active_non_guest_user_ids, get_user, get_user_profile_by_id_in_realm from zerver.views.streams import compose_views if TYPE_CHECKING: diff --git a/zerver/tests/test_tutorial.py b/zerver/tests/test_tutorial.py index caa710e5eb3d5..9d0fb360ac45f 100644 --- a/zerver/tests/test_tutorial.py +++ b/zerver/tests/test_tutorial.py @@ -4,7 +4,8 @@ from zerver.actions.message_send import internal_send_private_message from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import message_stream_count, most_recent_message -from zerver.models import UserProfile, get_system_bot +from zerver.models import UserProfile +from zerver.models.users import get_system_bot class TutorialTests(ZulipTestCase): diff --git a/zerver/tests/test_typing.py b/zerver/tests/test_typing.py index 701b57d22f3cd..ca01c2bf67194 100644 --- a/zerver/tests/test_typing.py +++ b/zerver/tests/test_typing.py @@ -1,7 +1,8 @@ import orjson from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Huddle, get_huddle_hash +from zerver.models import Huddle +from zerver.models.recipients import get_huddle_hash class TypingValidateOperatorTest(ZulipTestCase): diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index 240129561a4fb..3803395d7d624 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -21,6 +21,7 @@ from zerver.actions.realm_logo import do_change_logo_source from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property from zerver.actions.user_settings import do_delete_avatar_image +from zerver.lib.attachments import validate_attachment_request from zerver.lib.avatar import avatar_url, get_avatar_field from zerver.lib.cache import cache_get, get_realm_used_upload_space_cache_key from zerver.lib.create_user import copy_default_settings @@ -39,17 +40,9 @@ from zerver.lib.upload.local import LocalUploadBackend from zerver.lib.upload.s3 import S3UploadBackend from zerver.lib.users import get_api_key -from zerver.models import ( - Attachment, - Message, - Realm, - RealmDomain, - UserProfile, - get_realm, - get_system_bot, - get_user_by_delivery_email, - validate_attachment_request, -) +from zerver.models import Attachment, Message, Realm, RealmDomain, UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_system_bot, get_user_by_delivery_email class FileUploadTest(UploadSerializeMixin, ZulipTestCase): diff --git a/zerver/tests/test_upload_local.py b/zerver/tests/test_upload_local.py index ec4c194b85e93..850726a293349 100644 --- a/zerver/tests/test_upload_local.py +++ b/zerver/tests/test_upload_local.py @@ -22,7 +22,9 @@ ) from zerver.lib.upload.base import DEFAULT_EMOJI_SIZE, MEDIUM_AVATAR_SIZE, resize_avatar from zerver.lib.upload.local import write_local_file -from zerver.models import Attachment, RealmEmoji, get_realm, get_system_bot +from zerver.models import Attachment, RealmEmoji +from zerver.models.realms import get_realm +from zerver.models.users import get_system_bot class LocalStorageTest(UploadSerializeMixin, ZulipTestCase): diff --git a/zerver/tests/test_upload_s3.py b/zerver/tests/test_upload_s3.py index 02fcff0653481..6e5c4c199318a 100644 --- a/zerver/tests/test_upload_s3.py +++ b/zerver/tests/test_upload_s3.py @@ -36,7 +36,9 @@ resize_avatar, ) from zerver.lib.upload.s3 import S3UploadBackend -from zerver.models import Attachment, RealmEmoji, UserProfile, get_realm, get_system_bot +from zerver.models import Attachment, RealmEmoji, UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_system_bot class S3Test(ZulipTestCase): diff --git a/zerver/tests/test_user_groups.py b/zerver/tests/test_user_groups.py index a1126b53f18eb..61b3a5077056b 100644 --- a/zerver/tests/test_user_groups.py +++ b/zerver/tests/test_user_groups.py @@ -32,15 +32,9 @@ is_user_in_group, user_groups_in_realm_serialized, ) -from zerver.models import ( - GroupGroupMembership, - Realm, - SystemGroups, - UserGroup, - UserGroupMembership, - UserProfile, - get_realm, -) +from zerver.models import GroupGroupMembership, Realm, UserGroup, UserGroupMembership, UserProfile +from zerver.models.groups import SystemGroups +from zerver.models.realms import get_realm class UserGroupTestCase(ZulipTestCase): diff --git a/zerver/tests/test_user_status.py b/zerver/tests/test_user_status.py index 90658c7b357e0..492e347501123 100644 --- a/zerver/tests/test_user_status.py +++ b/zerver/tests/test_user_status.py @@ -4,7 +4,8 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.user_status import UserInfoDict, get_user_status_dict, update_user_status -from zerver.models import UserProfile, UserStatus, get_client +from zerver.models import UserProfile, UserStatus +from zerver.models.clients import get_client def user_status_info(user: UserProfile, acting_user: Optional[UserProfile] = None) -> UserInfoDict: diff --git a/zerver/tests/test_user_topics.py b/zerver/tests/test_user_topics.py index b3f4af476b391..ef48fc69ec746 100644 --- a/zerver/tests/test_user_topics.py +++ b/zerver/tests/test_user_topics.py @@ -12,7 +12,8 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import get_subscription from zerver.lib.user_topics import get_topic_mutes, topic_has_visibility_policy -from zerver.models import UserProfile, UserTopic, get_stream +from zerver.models import UserProfile, UserTopic +from zerver.models.streams import get_stream class MutedTopicsTestsDeprecated(ZulipTestCase): diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index 3b1ccd12d357d..45b2293bb2e64 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -59,7 +59,6 @@ from zerver.lib.utils import assert_is_not_none from zerver.models import ( CustomProfileField, - InvalidFakeEmailDomainError, Message, OnboardingStep, PreregistrationUser, @@ -70,17 +69,18 @@ ScheduledEmail, Stream, Subscription, - SystemGroups, UserGroupMembership, UserProfile, UserTopic, - check_valid_user_ids, - filter_to_valid_prereg_users, - get_client, - get_fake_email_domain, - get_realm, +) +from zerver.models.clients import get_client +from zerver.models.custom_profile_fields import check_valid_user_ids +from zerver.models.groups import SystemGroups +from zerver.models.prereg_users import filter_to_valid_prereg_users +from zerver.models.realms import InvalidFakeEmailDomainError, get_fake_email_domain, get_realm +from zerver.models.streams import get_stream +from zerver.models.users import ( get_source_profile, - get_stream, get_system_bot, get_user, get_user_by_delivery_email, diff --git a/zerver/tests/test_webhooks_common.py b/zerver/tests/test_webhooks_common.py index 358d67f52a151..f4d55e2a338a3 100644 --- a/zerver/tests/test_webhooks_common.py +++ b/zerver/tests/test_webhooks_common.py @@ -21,7 +21,9 @@ standardize_headers, validate_extract_webhook_http_header, ) -from zerver.models import UserProfile, get_realm, get_user +from zerver.models import UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_user class WebhooksCommonTestCase(ZulipTestCase): diff --git a/zerver/tests/test_zephyr.py b/zerver/tests/test_zephyr.py index f4d04ca3ead2b..1ac021ce07033 100644 --- a/zerver/tests/test_zephyr.py +++ b/zerver/tests/test_zephyr.py @@ -6,7 +6,8 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.lib.users import get_api_key -from zerver.models import get_realm, get_user +from zerver.models.realms import get_realm +from zerver.models.users import get_user if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/tornado/views.py b/zerver/tornado/views.py index ec5b7a711f261..2f01aae247973 100644 --- a/zerver/tornado/views.py +++ b/zerver/tornado/views.py @@ -20,7 +20,9 @@ check_string, to_non_negative_int, ) -from zerver.models import Client, UserProfile, get_client, get_user_profile_by_id +from zerver.models import Client, UserProfile +from zerver.models.clients import get_client +from zerver.models.users import get_user_profile_by_id from zerver.tornado.descriptors import is_current_port from zerver.tornado.event_queue import access_client_descriptor, fetch_events, process_notification from zerver.tornado.sharding import get_user_tornado_port, notify_tornado_queue_name diff --git a/zerver/transaction_tests/test_user_groups.py b/zerver/transaction_tests/test_user_groups.py index 42d407e3ba03d..0a5b639e38c69 100644 --- a/zerver/transaction_tests/test_user_groups.py +++ b/zerver/transaction_tests/test_user_groups.py @@ -12,7 +12,8 @@ from zerver.lib.test_classes import ZulipTransactionTestCase from zerver.lib.test_helpers import HostRequestMock from zerver.lib.user_groups import access_user_group_by_id -from zerver.models import Realm, UserGroup, UserProfile, get_realm +from zerver.models import Realm, UserGroup, UserProfile +from zerver.models.realms import get_realm from zerver.views.user_groups import update_subgroups_of_user_group BARRIER: Optional[threading.Barrier] = None diff --git a/zerver/views/auth.py b/zerver/views/auth.py index 093e91d8219df..965ca96d54b02 100644 --- a/zerver/views/auth.py +++ b/zerver/views/auth.py @@ -74,10 +74,10 @@ PreregistrationUser, Realm, UserProfile, - filter_to_valid_prereg_users, - get_realm, - remote_user_to_email, ) +from zerver.models.prereg_users import filter_to_valid_prereg_users +from zerver.models.realms import get_realm +from zerver.models.users import remote_user_to_email from zerver.signals import email_on_new_login from zerver.views.errors import config_error from zproject.backends import ( diff --git a/zerver/views/custom_profile_fields.py b/zerver/views/custom_profile_fields.py index 7618124734b3e..aca41fffacc93 100644 --- a/zerver/views/custom_profile_fields.py +++ b/zerver/views/custom_profile_fields.py @@ -33,7 +33,8 @@ check_union, validate_select_field_data, ) -from zerver.models import CustomProfileField, Realm, UserProfile, custom_profile_fields_for_realm +from zerver.models import CustomProfileField, Realm, UserProfile +from zerver.models.custom_profile_fields import custom_profile_fields_for_realm def list_realm_custom_profile_fields( diff --git a/zerver/views/development/cache.py b/zerver/views/development/cache.py index 94e6f663bf52d..5b0acc595a527 100644 --- a/zerver/views/development/cache.py +++ b/zerver/views/development/cache.py @@ -7,7 +7,7 @@ from zerver.lib.cache import get_cache_backend from zerver.lib.per_request_cache import flush_per_request_caches from zerver.lib.response import json_success -from zerver.models import clear_client_cache +from zerver.models.clients import clear_client_cache ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../") diff --git a/zerver/views/development/dev_login.py b/zerver/views/development/dev_login.py index be06d18c0e748..f26296c47c333 100644 --- a/zerver/views/development/dev_login.py +++ b/zerver/views/development/dev_login.py @@ -20,7 +20,8 @@ from zerver.lib.subdomains import get_subdomain from zerver.lib.users import get_api_key from zerver.lib.validator import validate_login_email -from zerver.models import Realm, UserProfile, get_realm +from zerver.models import Realm, UserProfile +from zerver.models.realms import get_realm from zerver.views.auth import get_safe_redirect_to from zerver.views.errors import config_error from zproject.backends import dev_auth_enabled diff --git a/zerver/views/development/email_log.py b/zerver/views/development/email_log.py index 4b4fe010cb3f4..2d570b9b2a37f 100644 --- a/zerver/views/development/email_log.py +++ b/zerver/views/development/email_log.py @@ -16,7 +16,10 @@ from zerver.lib.email_notifications import enqueue_welcome_emails, send_account_registered_email from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success -from zerver.models import Realm, get_realm, get_realm_stream, get_user_by_delivery_email +from zerver.models import Realm +from zerver.models.realms import get_realm +from zerver.models.streams import get_realm_stream +from zerver.models.users import get_user_by_delivery_email from zerver.views.invite import INVITATION_LINK_VALIDITY_MINUTES from zproject.email_backends import get_forward_address, set_forward_address diff --git a/zerver/views/development/integrations.py b/zerver/views/development/integrations.py index 6ba0f006bdf8d..0edab9d494999 100644 --- a/zerver/views/development/integrations.py +++ b/zerver/views/development/integrations.py @@ -14,7 +14,8 @@ from zerver.lib.response import json_success from zerver.lib.validator import check_bool from zerver.lib.webhooks.common import get_fixture_http_headers, standardize_headers -from zerver.models import UserProfile, get_realm +from zerver.models import UserProfile +from zerver.models.realms import get_realm if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/views/message_send.py b/zerver/views/message_send.py index e70a6aeda47e6..ef4e3d5ff01ae 100644 --- a/zerver/views/message_send.py +++ b/zerver/views/message_send.py @@ -19,10 +19,11 @@ from zerver.lib.request import REQ, RequestNotes, has_request_variables from zerver.lib.response import json_success from zerver.lib.topic import REQ_topic -from zerver.lib.validator import check_string_in, to_float +from zerver.lib.validator import check_bool, check_string_in, to_float from zerver.lib.zcommand import process_zcommands from zerver.lib.zephyr import compute_mit_user_fullname -from zerver.models import Client, Message, RealmDomain, UserProfile, get_user_including_cross_realm +from zerver.models import Client, Message, RealmDomain, UserProfile +from zerver.models.users import get_user_including_cross_realm class InvalidMirrorInputError(Exception): @@ -136,6 +137,7 @@ def send_message_backend( local_id: Optional[str] = REQ(default=None), queue_id: Optional[str] = REQ(default=None), time: Optional[float] = REQ(default=None, converter=to_float, documentation_pending=True), + read_by_sender: Optional[bool] = REQ(json_validator=check_bool, default=None), ) -> HttpResponse: recipient_type_name = req_type if recipient_type_name == "direct": @@ -221,6 +223,11 @@ def send_message_backend( raise JsonableError(_("Invalid mirrored message")) sender = user_profile + if read_by_sender is None: + # Legacy default: a message you sent from a non-API client is + # automatically marked as read for yourself. + read_by_sender = client.default_read_by_sender() + data: Dict[str, int] = {} sent_message_result = check_send_message( sender, @@ -236,6 +243,7 @@ def send_message_backend( local_id=local_id, sender_queue_id=queue_id, widget_content=widget_content, + read_by_sender=read_by_sender, ) data["id"] = sent_message_result.message_id if sent_message_result.automatic_new_visibility_policy: diff --git a/zerver/views/presence.py b/zerver/views/presence.py index 651152950d7ee..56d5ab5035987 100644 --- a/zerver/views/presence.py +++ b/zerver/views/presence.py @@ -19,14 +19,8 @@ from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint from zerver.lib.users import check_can_access_user -from zerver.models import ( - UserActivity, - UserPresence, - UserProfile, - UserStatus, - get_active_user, - get_active_user_profile_by_id_in_realm, -) +from zerver.models import UserActivity, UserPresence, UserProfile, UserStatus +from zerver.models.users import get_active_user, get_active_user_profile_by_id_in_realm def get_presence_backend( diff --git a/zerver/views/push_notifications.py b/zerver/views/push_notifications.py index ed3823e5ac9ae..bfe4e116e0752 100644 --- a/zerver/views/push_notifications.py +++ b/zerver/views/push_notifications.py @@ -125,7 +125,7 @@ def self_hosting_auth_redirect( *, next_page: Optional[str] = None, ) -> HttpResponse: # nocoverage - if not settings.DEVELOPMENT or not uses_notification_bouncer(): + if not uses_notification_bouncer(): return render(request, "404.html", status=404) user = request.user diff --git a/zerver/views/realm.py b/zerver/views/realm.py index fce8ddbdd8d92..366b280ee18dc 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -32,13 +32,13 @@ from zerver.lib.validator import ( check_bool, check_capped_string, + check_capped_url, check_dict, check_int, check_int_in, check_string_in, check_string_or_int, check_union, - check_url, to_non_negative_int, ) from zerver.models import Realm, RealmReactivationStatus, RealmUserDefault, UserProfile @@ -54,6 +54,9 @@ def parse_jitsi_server_url( return value +JITSI_SERVER_URL_MAX_LENGTH = 200 + + @require_realm_admin @has_request_variables def update_realm( @@ -147,7 +150,10 @@ def update_realm( jitsi_server_url_raw: Optional[str] = REQ( "jitsi_server_url", json_validator=check_union( - [check_string_in(list(Realm.JITSI_SERVER_SPECIAL_VALUES_MAP.keys())), check_url] + [ + check_string_in(list(Realm.JITSI_SERVER_SPECIAL_VALUES_MAP.keys())), + check_capped_url(JITSI_SERVER_URL_MAX_LENGTH), + ] ), default=None, ), diff --git a/zerver/views/realm_domains.py b/zerver/views/realm_domains.py index c27de2cdbcd53..65ac5a0fe8b37 100644 --- a/zerver/views/realm_domains.py +++ b/zerver/views/realm_domains.py @@ -13,7 +13,8 @@ from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success from zerver.lib.validator import check_bool -from zerver.models import RealmDomain, UserProfile, get_realm_domains +from zerver.models import RealmDomain, UserProfile +from zerver.models.realms import get_realm_domains def list_realm_domains(request: HttpRequest, user_profile: UserProfile) -> HttpResponse: diff --git a/zerver/views/realm_emoji.py b/zerver/views/realm_emoji.py index 35bf25932c1a2..6bd3d00a45ef8 100644 --- a/zerver/views/realm_emoji.py +++ b/zerver/views/realm_emoji.py @@ -9,7 +9,8 @@ from zerver.lib.exceptions import JsonableError, ResourceNotFoundError from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success -from zerver.models import RealmEmoji, UserProfile, get_all_custom_emoji_for_realm +from zerver.models import RealmEmoji, UserProfile +from zerver.models.realm_emoji import get_all_custom_emoji_for_realm def list_emoji(request: HttpRequest, user_profile: UserProfile) -> HttpResponse: diff --git a/zerver/views/realm_linkifiers.py b/zerver/views/realm_linkifiers.py index 300e1d6f8cc31..5fa3eb9813010 100644 --- a/zerver/views/realm_linkifiers.py +++ b/zerver/views/realm_linkifiers.py @@ -15,7 +15,8 @@ from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success from zerver.lib.validator import check_int, check_list -from zerver.models import RealmFilter, UserProfile, linkifiers_for_realm +from zerver.models import RealmFilter, UserProfile +from zerver.models.linkifiers import linkifiers_for_realm # Custom realm linkifiers diff --git a/zerver/views/registration.py b/zerver/views/registration.py index b8921bb140e7a..5a35f436bdcba 100644 --- a/zerver/views/registration.py +++ b/zerver/views/registration.py @@ -73,10 +73,6 @@ ) from zerver.lib.zephyr import compute_mit_user_fullname from zerver.models import ( - MAX_LANGUAGE_ID_LENGTH, - DisposableEmailError, - DomainNotAllowedForRealmError, - EmailContainsPlusError, MultiuseInvite, PreregistrationRealm, PreregistrationUser, @@ -84,13 +80,18 @@ RealmUserDefault, Stream, UserProfile, - get_default_stream_groups, +) +from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH +from zerver.models.realms import ( + DisposableEmailError, + DomainNotAllowedForRealmError, + EmailContainsPlusError, get_org_type_display_name, get_realm, - get_source_profile, - get_user_by_delivery_email, name_changes_disabled, ) +from zerver.models.streams import get_default_stream_groups +from zerver.models.users import get_source_profile, get_user_by_delivery_email from zerver.views.auth import ( create_preregistration_realm, create_preregistration_user, @@ -272,7 +273,10 @@ def registration_helper( validators.validate_email(email) except ValidationError: return TemplateResponse( - request, "zerver/invalid_email.html", context={"invalid_email": True} + request, + "zerver/invalid_email.html", + context={"invalid_email": True}, + status=400, ) if realm_creation: @@ -293,18 +297,21 @@ def registration_helper( request, "zerver/invalid_email.html", context={"realm_name": realm.name, "closed_domain": True}, + status=400, ) except DisposableEmailError: return TemplateResponse( request, "zerver/invalid_email.html", context={"realm_name": realm.name, "disposable_emails_not_allowed": True}, + status=400, ) except EmailContainsPlusError: return TemplateResponse( request, "zerver/invalid_email.html", context={"realm_name": realm.name, "email_contains_plus": True}, + status=400, ) if realm.deactivated: @@ -908,6 +915,18 @@ def create_realm(request: HttpRequest, creation_key: Optional[str] = None) -> Ht @has_request_variables def signup_send_confirm(request: HttpRequest, email: str = REQ("email")) -> HttpResponse: + try: + # Because we interpolate the email directly into the template + # from the query parameter, do a simple validation that it + # looks a at least a bit like an email address. + validators.validate_email(email) + except ValidationError: + return TemplateResponse( + request, + "zerver/invalid_email.html", + context={"invalid_email": True}, + status=400, + ) return TemplateResponse( request, "zerver/accounts_send_confirm.html", diff --git a/zerver/views/scheduled_messages.py b/zerver/views/scheduled_messages.py index b192799baa535..e4d1a669a1428 100644 --- a/zerver/views/scheduled_messages.py +++ b/zerver/views/scheduled_messages.py @@ -16,7 +16,7 @@ from zerver.lib.scheduled_messages import get_undelivered_scheduled_messages from zerver.lib.timestamp import timestamp_to_datetime from zerver.lib.topic import REQ_topic -from zerver.lib.validator import check_int, check_string_in, to_non_negative_int +from zerver.lib.validator import check_bool, check_int, check_string_in, to_non_negative_int from zerver.models import Message, UserProfile @@ -115,6 +115,7 @@ def create_scheduled_message_backend( topic_name: Optional[str] = REQ_topic(), message_content: str = REQ("content"), scheduled_delivery_timestamp: int = REQ(json_validator=check_int), + read_by_sender: Optional[bool] = REQ(json_validator=check_bool, default=None), ) -> HttpResponse: recipient_type_name = req_type if recipient_type_name == "direct": @@ -147,5 +148,6 @@ def create_scheduled_message_backend( deliver_at, realm=user_profile.realm, forwarder_user_profile=user_profile, + read_by_sender=read_by_sender, ) return json_success(request, data={"scheduled_message_id": scheduled_message_id}) diff --git a/zerver/views/streams.py b/zerver/views/streams.py index 23e14774c9867..64611e4ffb02c 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -99,7 +99,8 @@ check_union, to_non_negative_int, ) -from zerver.models import Realm, Stream, UserGroup, UserMessage, UserProfile, get_system_bot +from zerver.models import Realm, Stream, UserGroup, UserMessage, UserProfile +from zerver.models.users import get_system_bot def principal_to_user_profile(agent: UserProfile, principal: Union[str, int]) -> UserProfile: diff --git a/zerver/views/thumbnail.py b/zerver/views/thumbnail.py index df242703e3ea2..c421360592617 100644 --- a/zerver/views/thumbnail.py +++ b/zerver/views/thumbnail.py @@ -6,9 +6,10 @@ from django.utils.translation import gettext as _ from zerver.context_processors import get_valid_realm_from_request +from zerver.lib.attachments import validate_attachment_request from zerver.lib.request import REQ, has_request_variables from zerver.lib.thumbnail import generate_thumbnail_url -from zerver.models import Realm, UserProfile, validate_attachment_request +from zerver.models import Realm, UserProfile def validate_thumbnail_request( diff --git a/zerver/views/upload.py b/zerver/views/upload.py index 03536f30246ce..1b0917e7220a4 100644 --- a/zerver/views/upload.py +++ b/zerver/views/upload.py @@ -26,6 +26,7 @@ from zerver.context_processors import get_valid_realm_from_request from zerver.decorator import zulip_redirect_to_login +from zerver.lib.attachments import validate_attachment_request from zerver.lib.exceptions import JsonableError from zerver.lib.response import json_success from zerver.lib.storage import static_path @@ -37,7 +38,7 @@ from zerver.lib.upload.base import INLINE_MIME_TYPES from zerver.lib.upload.local import assert_is_local_storage_path from zerver.lib.upload.s3 import get_signed_upload_url -from zerver.models import UserProfile, validate_attachment_request +from zerver.models import UserProfile def patch_disposition_header(response: HttpResponse, url: str, is_attachment: bool) -> None: diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py index 91af45bcca727..dd09d2be176c5 100644 --- a/zerver/views/user_groups.py +++ b/zerver/views/user_groups.py @@ -37,7 +37,8 @@ ) from zerver.lib.users import access_user_by_id, user_ids_to_users from zerver.lib.validator import check_bool, check_int, check_list -from zerver.models import UserGroup, UserProfile, get_system_bot +from zerver.models import UserGroup, UserProfile +from zerver.models.users import get_system_bot from zerver.views.streams import compose_views diff --git a/zerver/views/user_settings.py b/zerver/views/user_settings.py index c073f35cbc93a..410e309e2ef86 100644 --- a/zerver/views/user_settings.py +++ b/zerver/views/user_settings.py @@ -52,12 +52,8 @@ check_string_in, check_timezone, ) -from zerver.models import ( - EmailChangeStatus, - UserProfile, - avatar_changes_disabled, - name_changes_disabled, -) +from zerver.models import EmailChangeStatus, UserProfile +from zerver.models.realms import avatar_changes_disabled, name_changes_disabled from zerver.views.auth import redirect_to_deactivation_notice from zproject.backends import check_password_strength, email_belongs_to_ldap diff --git a/zerver/views/users.py b/zerver/views/users.py index 4ef656222c18f..ca1317bde2f44 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -86,14 +86,14 @@ check_union, check_url, ) -from zerver.models import ( +from zerver.models import Service, Stream, UserProfile +from zerver.models.realms import ( DisposableEmailError, DomainNotAllowedForRealmError, EmailContainsPlusError, InvalidFakeEmailDomainError, - Service, - Stream, - UserProfile, +) +from zerver.models.users import ( get_user_by_delivery_email, get_user_by_id_in_realm_including_cross_realm, get_user_including_cross_realm, diff --git a/zerver/views/video_calls.py b/zerver/views/video_calls.py index 1c53c7f77ea35..09f24003798c2 100644 --- a/zerver/views/video_calls.py +++ b/zerver/views/video_calls.py @@ -32,7 +32,8 @@ from zerver.lib.subdomains import get_subdomain from zerver.lib.url_encoding import append_url_query_string from zerver.lib.validator import check_bool, check_dict, check_string -from zerver.models import UserProfile, get_realm +from zerver.models import UserProfile +from zerver.models.realms import get_realm class VideoCallSession(OutgoingSession): diff --git a/zerver/webhooks/bitbucket2/tests.py b/zerver/webhooks/bitbucket2/tests.py index 437a511eccc74..3d5c76a5ab54c 100644 --- a/zerver/webhooks/bitbucket2/tests.py +++ b/zerver/webhooks/bitbucket2/tests.py @@ -4,7 +4,7 @@ from zerver.lib.test_classes import WebhookTestCase from zerver.lib.test_helpers import HostRequestMock from zerver.lib.validator import wrap_wild_value -from zerver.models import get_client +from zerver.models.clients import get_client from zerver.webhooks.bitbucket2.view import get_user_info TOPIC = "Repository name" diff --git a/zerver/webhooks/dialogflow/view.py b/zerver/webhooks/dialogflow/view.py index 15b3ea07f635b..054bb01a05f1f 100644 --- a/zerver/webhooks/dialogflow/view.py +++ b/zerver/webhooks/dialogflow/view.py @@ -7,7 +7,8 @@ from zerver.lib.response import json_success from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint from zerver.lib.validator import WildValue, check_int, check_string -from zerver.models import UserProfile, get_user +from zerver.models import UserProfile +from zerver.models.users import get_user @webhook_view("Dialogflow") diff --git a/zerver/webhooks/helloworld/tests.py b/zerver/webhooks/helloworld/tests.py index ff2e8179b22c9..92cdee952e402 100644 --- a/zerver/webhooks/helloworld/tests.py +++ b/zerver/webhooks/helloworld/tests.py @@ -1,7 +1,8 @@ from django.conf import settings from zerver.lib.test_classes import WebhookTestCase -from zerver.models import get_realm, get_system_bot +from zerver.models.realms import get_realm +from zerver.models.users import get_system_bot class HelloWorldHookTests(WebhookTestCase): diff --git a/zerver/webhooks/jira/view.py b/zerver/webhooks/jira/view.py index 7e864e22cf6d0..08118a1f17c02 100644 --- a/zerver/webhooks/jira/view.py +++ b/zerver/webhooks/jira/view.py @@ -13,7 +13,8 @@ from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint from zerver.lib.validator import WildValue, check_none_or, check_string from zerver.lib.webhooks.common import check_send_webhook_message -from zerver.models import Realm, UserProfile, get_user_by_delivery_email +from zerver.models import Realm, UserProfile +from zerver.models.users import get_user_by_delivery_email IGNORED_EVENTS = [ "attachment_created", diff --git a/zerver/webhooks/splunk/view.py b/zerver/webhooks/splunk/view.py index ba8d9b612f206..afe61492aa25e 100644 --- a/zerver/webhooks/splunk/view.py +++ b/zerver/webhooks/splunk/view.py @@ -6,7 +6,8 @@ from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint from zerver.lib.validator import WildValue, check_string from zerver.lib.webhooks.common import check_send_webhook_message -from zerver.models import MAX_TOPIC_NAME_LENGTH, UserProfile +from zerver.models import UserProfile +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH MESSAGE_TEMPLATE = """ Splunk alert from saved search: diff --git a/zerver/webhooks/teamcity/tests.py b/zerver/webhooks/teamcity/tests.py index 34b62ed1fcff9..39a11b4d802b4 100644 --- a/zerver/webhooks/teamcity/tests.py +++ b/zerver/webhooks/teamcity/tests.py @@ -2,7 +2,9 @@ from zerver.lib.send_email import FromAddress from zerver.lib.test_classes import WebhookTestCase -from zerver.models import Recipient, get_realm, get_user_by_delivery_email +from zerver.models import Recipient +from zerver.models.realms import get_realm +from zerver.models.users import get_user_by_delivery_email from zerver.webhooks.teamcity.view import MISCONFIGURED_PAYLOAD_TYPE_ERROR_MESSAGE diff --git a/zerver/worker/queue_processors.py b/zerver/worker/queue_processors.py index 5e22561bb3fef..c6a6995720219 100644 --- a/zerver/worker/queue_processors.py +++ b/zerver/worker/queue_processors.py @@ -104,12 +104,11 @@ Stream, UserMessage, UserProfile, - filter_to_valid_prereg_users, - get_bot_services, - get_client, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.bots import get_bot_services +from zerver.models.clients import get_client +from zerver.models.prereg_users import filter_to_valid_prereg_users +from zerver.models.users import get_system_bot, get_user_profile_by_id logger = logging.getLogger(__name__) diff --git a/zilencer/auth.py b/zilencer/auth.py index 858ff51599043..9136dc986af0f 100644 --- a/zilencer/auth.py +++ b/zilencer/auth.py @@ -2,6 +2,7 @@ from functools import wraps from typing import Any, Callable +import sentry_sdk from django.conf import settings from django.http import HttpRequest, HttpResponse from django.urls import path @@ -73,23 +74,30 @@ def validate_remote_server( role: str, api_key: str, ) -> RemoteZulipServer: + log_data = RequestNotes.get_notes(request).log_data + assert log_data is not None try: remote_server = get_remote_server_by_uuid(role) except RemoteZulipServer.DoesNotExist: + log_data["extra"] = "[invalid-server]" raise InvalidZulipServerError(role) if not constant_time_compare(api_key, remote_server.api_key): + log_data["extra"] = "[invalid-server-key]" raise InvalidZulipServerKeyError(role) if remote_server.deactivated: + log_data["extra"] = "[deactivated-server]" raise RemoteServerDeactivatedError if ( get_subdomain(request) != Realm.SUBDOMAIN_FOR_ROOT_DOMAIN and not settings.DEVELOPMENT_DISABLE_PUSH_BOUNCER_DOMAIN_CHECK ): # Sometimes we may want to test push bouncer logic in development. + log_data["extra"] = "[invalid-domain]" raise JsonableError(_("Invalid subdomain for push notifications bouncer")) RequestNotes.get_notes(request).remote_server = remote_server process_client(request) + sentry_sdk.set_user({"server": remote_server.uuid}) return remote_server @@ -102,6 +110,9 @@ def _wrapped_view_func( ) -> HttpResponse: role, api_key = get_basic_credentials(request) if "@" in role: + log_data = RequestNotes.get_notes(request).log_data + assert log_data is not None + log_data["extra"] = "[non-server-key]" raise JsonableError(_("Must validate with valid Zulip server API key")) try: remote_server = validate_remote_server(request, role, api_key) diff --git a/zilencer/management/commands/add_mock_conversation.py b/zilencer/management/commands/add_mock_conversation.py index c4f50876cfa51..b424faaafbe6d 100644 --- a/zilencer/management/commands/add_mock_conversation.py +++ b/zilencer/management/commands/add_mock_conversation.py @@ -11,7 +11,8 @@ from zerver.lib.emoji import get_emoji_data from zerver.lib.streams import ensure_stream from zerver.lib.upload import upload_avatar_image -from zerver.models import Message, UserProfile, get_realm +from zerver.models import Message, UserProfile +from zerver.models.realms import get_realm class Command(BaseCommand): diff --git a/zilencer/management/commands/populate_billing_realms.py b/zilencer/management/commands/populate_billing_realms.py index c23d9668732c2..941cd1682edba 100644 --- a/zilencer/management/commands/populate_billing_realms.py +++ b/zilencer/management/commands/populate_billing_realms.py @@ -26,8 +26,14 @@ from zerver.apps import flush_cache from zerver.lib.remote_server import get_realms_info_for_push_bouncer from zerver.lib.streams import create_stream_if_needed -from zerver.models import Realm, UserProfile, get_realm -from zilencer.models import RemoteRealm, RemoteZulipServer +from zerver.models import Realm, UserProfile +from zerver.models.realms import get_realm +from zilencer.models import ( + RemoteRealm, + RemoteRealmBillingUser, + RemoteServerBillingUser, + RemoteZulipServer, +) from zilencer.views import update_remote_realm_data_for_server from zproject.config import get_secret @@ -359,7 +365,8 @@ def populate_realm(customer_profile: CustomerProfile) -> Optional[Realm]: # Remote realm billing data on their local server is irrelevant. return realm - if customer_profile.sponsorship_pending: + if customer_profile.sponsorship_pending or customer_profile.is_sponsored: + # plan_type is already set correctly above for sponsored realms. customer = Customer.objects.create( realm=realm, sponsorship_pending=customer_profile.sponsorship_pending, @@ -388,11 +395,11 @@ def populate_remote_server(customer_profile: CustomerProfile) -> Dict[str, str]: ): plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY elif customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: - plan_type = RemoteZulipServer.PLAN_TYPE_SELF_HOSTED + plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY elif customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS elif customer_profile.tier is CustomerPlan.TIER_SELF_HOSTED_BASE: - plan_type = RemoteZulipServer.PLAN_TYPE_SELF_HOSTED + plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED else: raise AssertionError("Unexpected tier!") @@ -414,10 +421,14 @@ def populate_remote_server(customer_profile: CustomerProfile) -> Dict[str, str]: last_audit_log_update=timezone_now(), ) - billing_session = RemoteServerBillingSession(remote_server) + billing_user = RemoteServerBillingUser.objects.create( + full_name="Server user", + remote_server=remote_server, + email=f"{unique_id}@example.com", + ) + billing_session = RemoteServerBillingSession(remote_server, billing_user) if customer_profile.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: # Create customer plan for these servers for temporary period. - billing_session = RemoteServerBillingSession(remote_server) renewal_date = datetime.strptime(customer_profile.renewal_date, TIMESTAMP_FORMAT).replace( tzinfo=timezone.utc ) @@ -484,7 +495,15 @@ def populate_remote_realms(customer_profile: CustomerProfile) -> Dict[str, str]: ) remote_realm = RemoteRealm.objects.get(uuid=local_realm.uuid) - billing_session = RemoteRealmBillingSession(remote_realm) + user = UserProfile.objects.filter(realm=local_realm).first() + assert user is not None + billing_user = RemoteRealmBillingUser.objects.create( + full_name=user.full_name, + remote_realm=remote_realm, + user_uuid=user.uuid, + email=user.email, + ) + billing_session = RemoteRealmBillingSession(remote_realm, billing_user) # TODO: Save property audit log data for server. remote_realm.server.last_audit_log_update = timezone_now() remote_realm.server.save(update_fields=["last_audit_log_update"]) @@ -495,7 +514,8 @@ def populate_remote_realms(customer_profile: CustomerProfile) -> Dict[str, str]: billing_session.do_change_plan_type( tier=customer_profile.tier, is_sponsored=customer_profile.is_sponsored ) - create_plan_for_customer(customer, customer_profile) + if not customer_profile.is_sponsored: + create_plan_for_customer(customer, customer_profile) if customer_profile.sponsorship_pending: billing_session.update_customer_sponsorship_status(True) diff --git a/zilencer/management/commands/populate_db.py b/zilencer/management/commands/populate_db.py index 0c31bd7a98677..ad24b409f3f47 100644 --- a/zilencer/management/commands/populate_db.py +++ b/zilencer/management/commands/populate_db.py @@ -64,15 +64,13 @@ UserMessage, UserPresence, UserProfile, - flush_alert_word, - get_client, - get_or_create_huddle, - get_realm, - get_stream, - get_user, - get_user_by_delivery_email, - get_user_profile_by_id, ) +from zerver.models.alert_words import flush_alert_word +from zerver.models.clients import get_client +from zerver.models.realms import get_realm +from zerver.models.recipients import get_or_create_huddle +from zerver.models.streams import get_stream +from zerver.models.users import get_user, get_user_by_delivery_email, get_user_profile_by_id from zilencer.models import RemoteRealm, RemoteZulipServer from zilencer.views import update_remote_realm_data_for_server diff --git a/zilencer/management/commands/sync_api_key.py b/zilencer/management/commands/sync_api_key.py index 9a03255faf5d0..dbd1a042a2fba 100644 --- a/zilencer/management/commands/sync_api_key.py +++ b/zilencer/management/commands/sync_api_key.py @@ -5,7 +5,9 @@ from django.core.management.base import BaseCommand from typing_extensions import override -from zerver.models import UserProfile, get_realm, get_user_by_delivery_email +from zerver.models import UserProfile +from zerver.models.realms import get_realm +from zerver.models.users import get_user_by_delivery_email class Command(BaseCommand): diff --git a/zilencer/migrations/0052_alter_remoterealm_plan_type_and_more.py b/zilencer/migrations/0052_alter_remoterealm_plan_type_and_more.py new file mode 100644 index 0000000000000..209e24cfdb594 --- /dev/null +++ b/zilencer/migrations/0052_alter_remoterealm_plan_type_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.8 on 2023-12-13 23:20 + +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps + +PLAN_TYPE_SELF_HOSTED = 1 +PLAN_TYPE_SELF_MANAGED = 100 + + +def renumber_plan_types(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None: + RemoteZulipServer = apps.get_model("zilencer", "RemoteZulipServer") + RemoteRealm = apps.get_model("zilencer", "RemoteRealm") + RemoteRealm.objects.filter(plan_type=PLAN_TYPE_SELF_HOSTED).update( + plan_type=PLAN_TYPE_SELF_MANAGED + ) + RemoteZulipServer.objects.filter(plan_type=PLAN_TYPE_SELF_HOSTED).update( + plan_type=PLAN_TYPE_SELF_MANAGED + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("zilencer", "0051_remoterealm_is_system_bot_realm"), + ] + + operations = [ + migrations.AlterField( + model_name="remoterealm", + name="plan_type", + field=models.PositiveSmallIntegerField(db_index=True, default=PLAN_TYPE_SELF_MANAGED), + ), + migrations.AlterField( + model_name="remotezulipserver", + name="plan_type", + field=models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_MANAGED), + ), + migrations.RunPython(renumber_plan_types, reverse_code=migrations.RunPython.noop), + ] diff --git a/zilencer/migrations/0053_remoterealmauditlog_acting_remote_user_and_more.py b/zilencer/migrations/0053_remoterealmauditlog_acting_remote_user_and_more.py new file mode 100644 index 0000000000000..24a80310aee1a --- /dev/null +++ b/zilencer/migrations/0053_remoterealmauditlog_acting_remote_user_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.8 on 2023-12-14 14:18 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("zilencer", "0052_alter_remoterealm_plan_type_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="remoterealmauditlog", + name="acting_remote_user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="zilencer.remoterealmbillinguser", + ), + ), + migrations.AddField( + model_name="remoterealmauditlog", + name="acting_support_user", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), + ), + migrations.AddField( + model_name="remotezulipserverauditlog", + name="acting_remote_user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="zilencer.remoteserverbillinguser", + ), + ), + migrations.AddField( + model_name="remotezulipserverauditlog", + name="acting_support_user", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/zilencer/migrations/0054_remoterealmbillinguser_enable_maintenance_release_emails_and_more.py b/zilencer/migrations/0054_remoterealmbillinguser_enable_maintenance_release_emails_and_more.py new file mode 100644 index 0000000000000..e185634081605 --- /dev/null +++ b/zilencer/migrations/0054_remoterealmbillinguser_enable_maintenance_release_emails_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.8 on 2023-12-14 17:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zilencer", "0053_remoterealmauditlog_acting_remote_user_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="remoterealmbillinguser", + name="enable_maintenance_release_emails", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="remoterealmbillinguser", + name="enable_major_release_emails", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="remoteserverbillinguser", + name="enable_maintenance_release_emails", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="remoteserverbillinguser", + name="enable_major_release_emails", + field=models.BooleanField(default=True), + ), + ] diff --git a/zilencer/migrations/0055_remoteserverbillinguser_tos_version.py b/zilencer/migrations/0055_remoteserverbillinguser_tos_version.py new file mode 100644 index 0000000000000..d3efae3f588be --- /dev/null +++ b/zilencer/migrations/0055_remoteserverbillinguser_tos_version.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.8 on 2023-12-14 17:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zilencer", "0054_remoterealmbillinguser_enable_maintenance_release_emails_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="remoteserverbillinguser", + name="tos_version", + field=models.TextField(default="-1"), + ), + ] diff --git a/zilencer/migrations/0056_remoterealm_realm_locally_deleted.py b/zilencer/migrations/0056_remoterealm_realm_locally_deleted.py new file mode 100644 index 0000000000000..de98173657726 --- /dev/null +++ b/zilencer/migrations/0056_remoterealm_realm_locally_deleted.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.8 on 2023-12-15 13:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zilencer", "0055_remoteserverbillinguser_tos_version"), + ] + + operations = [ + migrations.AddField( + model_name="remoterealm", + name="realm_locally_deleted", + field=models.BooleanField(default=False), + ), + ] diff --git a/zilencer/models.py b/zilencer/models.py index bb95174dce1c0..b746ed55a42e5 100644 --- a/zilencer/models.py +++ b/zilencer/models.py @@ -54,13 +54,22 @@ class RemoteZulipServer(models.Model): deactivated = models.BooleanField(default=False) # Plan types for self-hosted customers + # + # We reserve PLAN_TYPE_SELF_HOSTED=Realm.PLAN_TYPE_SELF_HOSTED for + # self-hosted installations that aren't using the notifications + # service. + # + # The other values align with, e.g., CustomerPlan.TIER_SELF_HOSTED_BASE PLAN_TYPE_SELF_HOSTED = 1 - PLAN_TYPE_COMMUNITY = 100 - PLAN_TYPE_BUSINESS = 101 - PLAN_TYPE_ENTERPRISE = 102 + PLAN_TYPE_SELF_MANAGED = 100 + PLAN_TYPE_SELF_MANAGED_LEGACY = 101 + PLAN_TYPE_COMMUNITY = 102 + PLAN_TYPE_BUSINESS = 103 + PLAN_TYPE_PLUS = 104 + PLAN_TYPE_ENTERPRISE = 105 # The current billing plan for the remote server, similar to Realm.plan_type. - plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_HOSTED) + plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_MANAGED) # This is not synced with the remote server, but only filled for sponsorship requests. org_type = models.PositiveSmallIntegerField( @@ -138,18 +147,28 @@ class RemoteRealm(models.Model): registration_deactivated = models.BooleanField(default=False) # Whether the realm has been deactivated on the remote server. realm_deactivated = models.BooleanField(default=False) + # Whether we believe the remote server deleted this realm + # from the database. + realm_locally_deleted = models.BooleanField(default=False) # When the realm was created on the remote server. realm_date_created = models.DateTimeField() # Plan types for self-hosted customers + # + # We reserve PLAN_TYPE_SELF_HOSTED=Realm.PLAN_TYPE_SELF_HOSTED for + # self-hosted installations that aren't using the notifications + # service. + # + # The other values align with, e.g., CustomerPlan.TIER_SELF_HOSTED_BASE PLAN_TYPE_SELF_HOSTED = 1 - PLAN_TYPE_COMMUNITY = 100 - PLAN_TYPE_BUSINESS = 101 - PLAN_TYPE_ENTERPRISE = 102 - - # The current billing plan for the remote server, similar to Realm.plan_type. - plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_HOSTED, db_index=True) + PLAN_TYPE_SELF_MANAGED = 100 + PLAN_TYPE_SELF_MANAGED_LEGACY = 101 + PLAN_TYPE_COMMUNITY = 102 + PLAN_TYPE_BUSINESS = 103 + PLAN_TYPE_PLUS = 104 + PLAN_TYPE_ENTERPRISE = 105 + plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_MANAGED, db_index=True) @override def __str__(self) -> str: @@ -179,6 +198,9 @@ class RemoteRealmBillingUser(AbstractRemoteRealmBillingUser): TOS_VERSION_BEFORE_FIRST_LOGIN = UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN tos_version = models.TextField(default=TOS_VERSION_BEFORE_FIRST_LOGIN) + enable_major_release_emails = models.BooleanField(default=True) + enable_maintenance_release_emails = models.BooleanField(default=True) + class Meta: unique_together = [ ("remote_realm", "user_uuid"), @@ -217,6 +239,12 @@ class RemoteServerBillingUser(AbstractRemoteServerBillingUser): is_active = models.BooleanField(default=True) + TOS_VERSION_BEFORE_FIRST_LOGIN = UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN + tos_version = models.TextField(default=TOS_VERSION_BEFORE_FIRST_LOGIN) + + enable_major_release_emails = models.BooleanField(default=True) + enable_maintenance_release_emails = models.BooleanField(default=True) + class Meta: unique_together = [ ("remote_server", "email"), @@ -245,6 +273,11 @@ class RemoteZulipServerAuditLog(AbstractRealmAuditLog): server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE) + acting_remote_user = models.ForeignKey( + RemoteServerBillingUser, null=True, on_delete=models.SET_NULL + ) + acting_support_user = models.ForeignKey(UserProfile, null=True, on_delete=models.SET_NULL) + @override def __str__(self) -> str: return f"{self.server!r} {self.event_type} {self.event_time} {self.id}" @@ -267,6 +300,11 @@ class RemoteRealmAuditLog(AbstractRealmAuditLog): # The remote_id field lets us deduplicate data from the remote server remote_id = models.IntegerField(null=True) + acting_remote_user = models.ForeignKey( + RemoteRealmBillingUser, null=True, on_delete=models.SET_NULL + ) + acting_support_user = models.ForeignKey(UserProfile, null=True, on_delete=models.SET_NULL) + @override def __str__(self) -> str: return f"{self.server!r} {self.event_type} {self.event_time} {self.id}" diff --git a/zilencer/views.py b/zilencer/views.py index 1dfda4c3f8f00..77abaf37e00cd 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -28,10 +28,16 @@ RemoteRealmBillingSession, RemoteServerBillingSession, do_deactivate_remote_server, + get_push_status_for_remote_request, ) from corporate.models import CustomerPlan, get_current_plan_by_customer from zerver.decorator import require_post -from zerver.lib.exceptions import JsonableError, RemoteRealmServerMismatchError +from zerver.lib.exceptions import ( + ErrorCode, + JsonableError, + RemoteRealmServerMismatchError, + RemoteServerDeactivatedError, +) from zerver.lib.push_notifications import ( InvalidRemotePushDeviceTokenError, UserPushIdentityCompat, @@ -45,7 +51,7 @@ RealmCountDataForAnalytics, RealmDataForAnalytics, ) -from zerver.lib.request import REQ, has_request_variables +from zerver.lib.request import REQ, RequestNotes, has_request_variables from zerver.lib.response import json_success from zerver.lib.timestamp import timestamp_to_datetime from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint @@ -92,7 +98,8 @@ def deactivate_remote_server( request: HttpRequest, remote_server: RemoteZulipServer, ) -> HttpResponse: - do_deactivate_remote_server(remote_server) + billing_session = RemoteServerBillingSession(remote_server) + do_deactivate_remote_server(remote_server, billing_session) return json_success(request) @@ -148,12 +155,15 @@ def register_remote_server( else: if not constant_time_compare(remote_server.api_key, zulip_org_key): raise InvalidZulipServerKeyError(zulip_org_id) - else: - remote_server.hostname = hostname - remote_server.contact_email = contact_email - if new_org_key is not None: - remote_server.api_key = new_org_key - remote_server.save() + + if remote_server.deactivated: + raise RemoteServerDeactivatedError + + remote_server.hostname = hostname + remote_server.contact_email = contact_email + if new_org_key is not None: + remote_server.api_key = new_org_key + remote_server.save() return json_success(request, data={"created": created}) @@ -385,6 +395,13 @@ def get_remote_realm_helper( return remote_realm +class OldZulipServerError(JsonableError): + code = ErrorCode.INVALID_ZULIP_SERVER + + def __init__(self, msg: str) -> None: + self._msg: str = msg + + @has_request_variables def remote_server_notify_push( request: HttpRequest, @@ -407,6 +424,16 @@ def remote_server_notify_push( ), "Servers new enough to send realm_uuid, should also have user_uuid" remote_realm = get_remote_realm_helper(request, server, realm_uuid, user_uuid) + push_status = get_push_status_for_remote_request(server, remote_realm) + log_data = RequestNotes.get_notes(request).log_data + assert log_data is not None + log_data["extra"] = f"[can_push={push_status.can_push}/{push_status.message}]" + if not push_status.can_push: + if server.last_api_feature_level is None: + raise OldZulipServerError(_("Your plan doesn't allow sending push notifications.")) + else: + raise JsonableError(_("Your plan doesn't allow sending push notifications.")) + android_devices = list( RemotePushDeviceToken.objects.filter( user_identity.filter_q(), @@ -524,8 +551,10 @@ def truncate_payload(payload: Dict[str, Any]) -> Dict[str, Any]: timezone_now(), increment=android_successfully_delivered + apple_successfully_delivered, ) - billing_session = RemoteRealmBillingSession(remote_realm) - remote_realm_dict = billing_session.get_push_service_validity_dict() + remote_realm_dict = { + "can_push": push_status.can_push, + "expected_end_timestamp": push_status.expected_end_timestamp, + } deleted_devices = get_deleted_devices( user_identity, @@ -646,7 +675,21 @@ def update_remote_realm_data_for_server( server: RemoteZulipServer, server_realms_info: List[RealmDataForAnalytics] ) -> None: uuids = [realm.uuid for realm in server_realms_info] - already_registered_remote_realms = RemoteRealm.objects.filter(uuid__in=uuids, server=server) + all_registered_remote_realms_for_server = list(RemoteRealm.objects.filter(server=server)) + already_registered_remote_realms = [ + remote_realm + for remote_realm in all_registered_remote_realms_for_server + if remote_realm.uuid in uuids + ] + # RemoteRealm registrations that we have for this server, but aren't + # present in the data sent to us. We assume this to mean the server + # must have deleted those realms from the database. + remote_realms_missing_from_server_data = [ + remote_realm + for remote_realm in all_registered_remote_realms_for_server + if remote_realm.uuid not in uuids + ] + already_registered_uuids = { remote_realm.uuid for remote_realm in already_registered_remote_realms } @@ -681,6 +724,10 @@ def update_remote_realm_data_for_server( # Update RemoteRealm entries, for which the corresponding realm's info has changed # (for the attributes that make sense to sync like this). for remote_realm in already_registered_remote_realms: + # TODO: We'll also want to check if .realm_locally_deleted is True, and if so, + # toggle it off (and potentially restore registration_deactivated=True too), + # since the server is now sending us data for this realm again. + modified = False realm = uuid_to_realm_dict[str(remote_realm.uuid)] for remote_realm_attr, realm_dict_key in [ @@ -731,6 +778,32 @@ def update_remote_realm_data_for_server( ) RemoteRealmAuditLog.objects.bulk_create(remote_realm_audit_logs) + remote_realms_to_update = [] + remote_realm_audit_logs = [] + for remote_realm in remote_realms_missing_from_server_data: + if not remote_realm.realm_locally_deleted: + # Otherwise we already knew about this, so nothing to do. + remote_realm.realm_locally_deleted = True + remote_realm.registration_deactivated = True + + remote_realm_audit_logs.append( + RemoteRealmAuditLog( + server=server, + remote_id=None, + remote_realm=remote_realm, + realm_id=None, + event_type=RemoteRealmAuditLog.REMOTE_REALM_LOCALLY_DELETED, + event_time=now, + ) + ) + remote_realms_to_update.append(remote_realm) + + RemoteRealm.objects.bulk_update( + remote_realms_to_update, + ["realm_locally_deleted", "registration_deactivated"], + ) + RemoteRealmAuditLog.objects.bulk_create(remote_realm_audit_logs) + def get_human_user_realm_uuids(realms: List[RealmDataForAnalytics]) -> List[UUID]: # nocoverage billable_realm_uuids = [] @@ -738,6 +811,7 @@ def get_human_user_realm_uuids(realms: List[RealmDataForAnalytics]) -> List[UUID # TODO: Remove the `zulipinternal` string_id check once no server is on 8.0-beta. if ( realm.is_system_bot_realm + or realm.deactivated or realm.host.startswith("zulipinternal.") or (settings.DEVELOPMENT and realm.host.startswith("analytics.")) ): @@ -750,27 +824,35 @@ def get_human_user_realm_uuids(realms: List[RealmDataForAnalytics]) -> List[UUID @transaction.atomic def handle_customer_migration_from_server_to_realms( server: RemoteZulipServer, realms: List[RealmDataForAnalytics] -) -> None: # nocoverage +) -> None: server_billing_session = RemoteServerBillingSession(server) server_customer = server_billing_session.get_customer() if server_customer is None: return + server_plan = get_current_plan_by_customer(server_customer) + if server_plan is None: + # If the server has no current plan, either because it never + # had one or because a previous legacy plan was migrated to + # the RemoteRealm object, there's nothing to potentially + # migrate. + return + realm_uuids = get_human_user_realm_uuids(realms) if not realm_uuids: return event_time = timezone_now() remote_realm_audit_logs = [] + if ( - server_plan is not None - and server_plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY + server_plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY and server_plan.status == CustomerPlan.ACTIVE ): - assert server.plan_type == RemoteZulipServer.PLAN_TYPE_SELF_HOSTED + assert server.plan_type == RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY assert server_plan.end_date is not None remote_realms = RemoteRealm.objects.filter( - uuid__in=realm_uuids, server=server, plan_type=RemoteRealm.PLAN_TYPE_SELF_HOSTED + uuid__in=realm_uuids, server=server, plan_type=RemoteRealm.PLAN_TYPE_SELF_MANAGED ) # Verify that all the realms are on self hosted plan. @@ -780,6 +862,9 @@ def handle_customer_migration_from_server_to_realms( server_plan.status = CustomerPlan.ENDED server_plan.save(update_fields=["status"]) + server.plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED + server.save(update_fields=["plan_type"]) + # Create new legacy plan for each remote realm. for remote_realm in remote_realms: RemoteRealmBillingSession(remote_realm).migrate_customer_to_legacy_plan( @@ -795,20 +880,20 @@ def handle_customer_migration_from_server_to_realms( ) ) - # We only do this migration if there is only one realm besides the system bot realm. elif len(realm_uuids) == 1: + # Here, we have exactly one non-system-bot realm, and some + # sort of plan on the server; move it to the realm. remote_realm = RemoteRealm.objects.get( - uuid=realm_uuids[0], plan_type=RemoteRealm.PLAN_TYPE_SELF_HOSTED + uuid=realm_uuids[0], plan_type=RemoteRealm.PLAN_TYPE_SELF_MANAGED ) # Migrate customer from server to remote realm if there is only one realm. server_customer.remote_realm = remote_realm server_customer.remote_server = None server_customer.save(update_fields=["remote_realm", "remote_server"]) - # TODO: Set usage limits for remote realm and server. - # Might be better to call do_change_plan_type here. + # TODO: Might be better to call do_change_plan_type here. remote_realm.plan_type = server.plan_type remote_realm.save(update_fields=["plan_type"]) - server.plan_type = RemoteZulipServer.PLAN_TYPE_SELF_HOSTED + server.plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED server.save(update_fields=["plan_type"]) remote_realm_audit_logs.append( RemoteRealmAuditLog( @@ -818,7 +903,7 @@ def handle_customer_migration_from_server_to_realms( event_time=event_time, extra_data={ "attr_name": "plan_type", - "old_value": RemoteRealm.PLAN_TYPE_SELF_HOSTED, + "old_value": RemoteRealm.PLAN_TYPE_SELF_MANAGED, "new_value": remote_realm.plan_type, }, ) @@ -964,9 +1049,8 @@ def remote_server_post_analytics( # We need to update 'last_audit_log_update' before calling the # 'sync_license_ledger_if_needed' method to avoid 'MissingDataError' # due to 'has_stale_audit_log' being True. - RemoteZulipServer.objects.filter(uuid=server.uuid).update( - last_audit_log_update=timezone_now() - ) + server.last_audit_log_update = timezone_now() + server.save(update_fields=["last_audit_log_update"]) # Update LicenseLedger for remote_realm customers using logs in RemoteRealmAuditlog. for remote_realm in remote_realms_set: @@ -978,13 +1062,35 @@ def remote_server_post_analytics( remote_server_billing_session = RemoteServerBillingSession(remote_server=server) remote_server_billing_session.sync_license_ledger_if_needed() + log_data = RequestNotes.get_notes(request).log_data + assert log_data is not None + can_push_values = set() + + remote_realms = RemoteRealm.objects.filter(server=server, realm_locally_deleted=False) remote_realm_dict: Dict[str, RemoteRealmDictValue] = {} - remote_realms = RemoteRealm.objects.filter(server=server) + remote_human_realm_count = remote_realms.filter(is_system_bot_realm=False).count() for remote_realm in remote_realms: uuid = str(remote_realm.uuid) - billing_session = RemoteRealmBillingSession(remote_realm) - remote_realm_dict[uuid] = billing_session.get_push_service_validity_dict() - + status = get_push_status_for_remote_request(server, remote_realm) + if remote_realm.is_system_bot_realm: + # Ignore system bot realms for computing log_data + pass + elif remote_human_realm_count == 1: # nocoverage + log_data["extra"] = f"[can_push={status.can_push}/{status.message}]" + else: + can_push_values.add(status.can_push) + remote_realm_dict[uuid] = { + "can_push": status.can_push, + "expected_end_timestamp": status.expected_end_timestamp, + } + + if len(can_push_values) == 1: + can_push_value = next(iter(can_push_values)) + log_data["extra"] = f"[can_push={can_push_value}/{remote_human_realm_count} realms]" + elif can_push_values == {True, False}: + log_data["extra"] = f"[can_push=mixed/{remote_human_realm_count} realms]" + elif remote_human_realm_count == 0: + log_data["extra"] = "[0 realms]" return json_success(request, data={"realms": remote_realm_dict}) diff --git a/zproject/backends.py b/zproject/backends.py index 2828daef1eacd..f9a8ebe6064d3 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -103,22 +103,26 @@ from zerver.lib.users import check_full_name, validate_user_custom_profile_field from zerver.models import ( CustomProfileField, - DisposableEmailError, - DomainNotAllowedForRealmError, - EmailContainsPlusError, - PasswordTooWeakError, PreregistrationRealm, PreregistrationUser, Realm, UserGroup, UserGroupMembership, UserProfile, - custom_profile_fields_for_realm, +) +from zerver.models.custom_profile_fields import custom_profile_fields_for_realm +from zerver.models.realms import ( + DisposableEmailError, + DomainNotAllowedForRealmError, + EmailContainsPlusError, get_realm, + supported_auth_backends, +) +from zerver.models.users import ( + PasswordTooWeakError, get_user_by_delivery_email, get_user_profile_by_id, remote_user_to_email, - supported_auth_backends, ) from zproject.settings_types import OIDCIdPConfigDict diff --git a/zproject/sentry.py b/zproject/sentry.py index ff8850ddbfa37..7d184b4306703 100644 --- a/zproject/sentry.py +++ b/zproject/sentry.py @@ -24,7 +24,7 @@ def add_context(event: "Event", hint: "Hint") -> Optional["Event"]: return None from django.conf import settings - from zerver.models import get_user_profile_by_id + from zerver.models.users import get_user_profile_by_id with capture_internal_exceptions(): # event.user is the user context, from Sentry, which is