From 4cf89d273e4fbe68804f86d3c2c1139ae3ee6c5a Mon Sep 17 00:00:00 2001 From: Stout Ni Date: Thu, 30 May 2024 23:06:58 +0800 Subject: [PATCH] feat(i18n): complete Simplified Chinese translations (#14474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(i18n): complete Simplified Chinese translation * chore: check for missing translation items * fix: put the prevent merge conflict line at the end * feat: put new translations at the beginning * Rename translate-locales.ts to check-missing-translations.ts * Update package.json to use check-missing-translations --------- Co-authored-by: Keith Williams Co-authored-by: Peer Richelsen Co-authored-by: Omar López --- apps/web/package.json | 3 +- .../public/static/locales/zh-CN/common.json | 209 +++++++++++++++++- .../web/scripts/check-missing-translations.ts | 51 +++++ 3 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 apps/web/scripts/check-missing-translations.ts diff --git a/apps/web/package.json b/apps/web/package.json index e688e41f08..259fdaf8a2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,7 +17,8 @@ "lint": "eslint . --ignore-path .gitignore", "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", "lint:report": "eslint . --format json --output-file ../../lint-results/web.json", - "check-changed-files": "ts-node scripts/ts-check-changed-files.ts" + "check-changed-files": "ts-node scripts/ts-check-changed-files.ts", + "translate-locales": "ts-node scripts/check-missing-translations.ts" }, "engines": { "node": "18", diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index 148ebdcbd6..e01ee00bff 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -18,11 +18,11 @@ "verify_email_banner_body": "验证您的电子邮件地址以确保最佳的电子邮件和日历递送能力", "verify_email_email_header": "验证您的电子邮件地址", "verify_email_email_button": "验证电子邮件", - "verify_email_change_description": "您最近更改了登陆您的帐户 {{appName}} 时所使用的电子邮箱地址。 请点击下方按钮以确认您的新邮箱地址。", + "verify_email_change_description": "您最近更改了登陆您的帐户 {{appName}} 时所使用的电子邮箱地址。请点击下方按钮以确认您的新邮箱地址。", "verify_email_change_success_toast": "将您的电子邮箱地址变更为 {{email}}", "verify_email_change_failure_toast": "更新电子邮箱地址时出现了错误。", "change_of_email": "请确认您为您的帐户 {{appName}} 设置的新的邮箱地址", - "change_of_email_toast": "我们向您的电子邮箱 {{email}} 发送了一封确认邮件。 在您点击了确认邮件中的链接后, 我们将为您更新您的电子邮箱地址。", + "change_of_email_toast": "我们向您的电子邮箱 {{email}} 发送了一封确认邮件。在您点击了确认邮件中的链接后, 我们将为您更新您的电子邮箱地址。", "copy_somewhere_safe": "将此 API 密钥保存在安全的地方。您将无法再次查看它。", "verify_email_email_body": "请点击下面的按钮来验证您的电子邮件地址。", "verify_email_by_code_email_body": "请使用下面的代码验证您的电子邮件地址。", @@ -763,7 +763,7 @@ "team_billing_description": "管理您团队的计费", "upgrade_to_flexible_pro_title": "我们更改了团队的计费方式", "upgrade_to_flexible_pro_message": "您的团队中有没有位置的成员。升级到专业版来获得更多位置。", - "changed_team_billing_info": "自2022年1月起,我们将按每个位置向团队成员收取费用。 您的团队中免费获得专业版的成员现在正在进行为期14天的试用。 试用期结束后,这些成员将在您的团队中隐藏,除非您现在升级到专业版。", + "changed_team_billing_info": "自2022年1月起,我们将按每个位置向团队成员收取费用。您的团队中免费获得专业版的成员现在正在进行为期14天的试用。试用期结束后,这些成员将在您的团队中隐藏,除非您现在升级到专业版。", "create_manage_teams_collaborative": "创建并管理团队以使用协作功能。", "only_available_on_pro_plan": "此功能仅在 Pro 套餐中可用", "remove_cal_branding_description": "若想从您的预约页面中去掉 {{appName}} 的品牌标志,您需要升级到专业版账户。", @@ -2166,5 +2166,208 @@ "email_team_invite|content|invited_to_regular_team": "{{invitedBy}} 已邀请您加入他们在 {{appName}} 上的团队“{{teamName}}”。{{appName}} 是一个活动安排调度程序,让您和您的团队无需通过电子邮件沟通即可安排会议。", "privacy": "隐私", "cookie_consent_checkbox": "我同意 Cal.com 的隐私政策以及 cookie 使用政策", + "old_email_address": "旧邮箱", + "already_signed_up_for_this_booking_error": "您已经注册了此预订。", + "hosts_unavailable_for_booking": "有些主持人无法预订。", + "general_prompt": "一般提示", + "begin_message": "开始消息", + "user_invited_you_to_subteam": "{{user}} 在 {{appName}} 上邀请您加入组织 {{parentTeamName}} 的团队 {{team}}", + "set_availbility_description": "为您想要被预订的时间设置日程。", + "share_a_link_or_embed_description": "分享您的 {{appName}} 链接或将其嵌入您的网站。", + "please_schedule_future_call": " 如果我们在 {{seconds}} 秒内不可用,请安排未来的通话。", + "multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)", + "use_cal_ai_to_make_call_description": "使用 Cal.ai 获得人工智能驱动的电话号码或拨打电话给访客。", + "you_must_be_logged_in_to": "您必须登录 {{url}}", + "display_add_to_calendar_organizer": "使用“添加到日历”电子邮件作为组织者", + "if_enabled_email_address_as_organizer": "如果启用,我们将显示您“添加到日历”中的电子邮件地址作为组织者,并在那里发送确认电子邮件", + "cannot_be_used_with_paid_event_types": "不能用于付费的活动类型", + "warning_payment_instant_meeting_event": "即时会议暂不支持定期活动和支付应用程序", + "you_cannot_see_teams_of_org": "您无法看到私有组织的团队。", + "rating": "评分", + "upgrade_to_cal_ai_phone_number_description": "升级到企业版以生成可以呼叫客人安排电话的人工智能代理电话号码", + "event_type_not_found": "活动类型未找到", + "pending_organization_invites": "待处理的组织邀请", + "limit_booking_only_first_slot": "仅限预订第一个时段", + "limit_booking_only_first_slot_description": "允许每天仅预订第一个时段", + "only_admin_can_see_members_of_org": "该组织是私有的,只有该组织的管理员或所有者才能查看其成员。", + "only_admin_can_manage_sso_org": "只有组织的管理员或所有者才能管理 SSO 设置", + "date_overrides_delete_on_date": "删除 {{date}} 上的日期覆盖", + "sso_configuration_description_orgs": "配置 SAML/OIDC SSO 并允许组织成员使用身份提供者登录", + "email_no_user_invite_heading_subteam": "您已被邀请加入 {{parentTeamName}} 组织的一个团队", + "email_user_invite_subheading_subteam": "{{invitedBy}} 在 {{appName}} 上邀请您加入他们组织 {{parentTeamName}} 中的团队 {{teamName}}。{{appName}} 是一个活动日程安排程序,使您和您的团队无需电子邮件即可安排会议。", + "unlocked_for_members": "对成员解锁", + "apps_unlocked_for_members_description": "成员将能够看到激活的应用程序并将能够编辑任何应用程序设置", + "apps_locked_by_team_admins_description": "您将能够看到激活的应用程序,但将无法编辑任何应用程序设置", + "apps_unlocked_by_team_admins_description": "您将能够看到激活的应用程序并将能够编辑任何应用程序设置", + "workflows_locked_for_members_description": "成员无法将他们的个人工作流程添加到此事件类型。成员将能够看到活动的团队工作流程,但将无法编辑任何工作流程设置。", + "workflows_unlocked_for_members_description": "成员将能够将他们的个人工作流程添加到此事件类型。成员将能够看到活动的团队工作流程,但将无法编辑任何工作流程设置。", + "workflows_locked_by_team_admins_description": "您将能够看到活动的团队工作流程,但将无法编辑任何工作流程设置或将您的个人工作流程添加到此事件类型。", + "workflows_unlocked_by_team_admins_description": "您将能够在此事件类型上启用/禁用个人工作流程。您将能够看到活动的团队工作流程,但将无法编辑任何团队工作流程设置。", + "filters": "过滤", + "remove_filters": "清楚所有过滤", + "create_instant_meeting_webhook_description": "为此事件类型创建第一个以“创建即时会议”为触发器的 Webhook", + "upload_cal_video_logo": "上传 Cal 视频 Logo", + "update_cal_video_logo": "更新 Cal 视频 Logo", + "upload_banner": "上传横幅", + "cal_video_logo_upload_instruction": "为了确保您的徽标在 Cal 视频的深色背景下可见,请上传浅色图像以保持透明度,格式为 PNG 或 SVG。", + "connect_all_calendars_description": "{{appName}} 从您所有现有的日历中读取可用性。", + "workflow_automation_description": "使用工作流程个性化您的日程安排体验", + "scheduling_for_your_team_description": "使用集体和循环安排为您的团队安排日程", + "directory_name": "目录名称", + "directory_provider": "目录提供程序", + "directory_scim_url": "SCIM 基本 URL", + "directory_scim_token": "SCIM Bearer Token", + "directory_scim_url_copied": "SCIM 基本 URL 已复制", + "directory_scim_token_copied": "SCIM Bearer Token 已复制", + "directory_sync_info_description": "您的身份提供者将要求提供以下信息来配置 SCIM。请按照说明完成设置。", + "directory_sync_configure": "配置目录同步", + "directory_sync_configure_description": "选择一个身份提供者为您的团队配置目录。", + "directory_sync_title": "配置身份提供程序以开始使用 SCIM。", + "directory_sync_created": "目录同步连接已创建。", + "directory_sync_description": "使用您的目录提供商配置和取消配置用户。", + "directory_sync_deleted": "目录同步连接已删除。", + "directory_sync_delete_connection": "删除连接", + "directory_sync_delete_title": "删除目录同步连接", + "directory_sync_delete_description": "您确定要删除此目录同步连接吗?", + "directory_sync_delete_confirmation": "此操作无法撤消。这将永久删除目录同步连接。", + "oauth_form_title": "OAuth 客户端创建表单", + "oauth_form_description": "这是创建新 OAuth 客户端的表单", + "calendars_were_checking_for_conflicts": "我们正在检查冲突的日历", + "event_setup_multiple_payment_apps_error": "每种事件类型只能启用一个支付应用程序。", + "number_in_international_format": "请输入国际格式的号码。", + "branded_subdomain": "品牌子域名", + "branded_subdomain_description": "获取您自己的品牌子域名,例如 acme.cal.com", + "org_insights": "全组织范围", + "org_insights_description": "了解您的整个组织如何花费时间", + "extensive_whitelabeling_description": "使用您自己的 Logo、颜色等来自定义您的日程安排体验", + "unlimited_teams": "无限团队", + "unlimited_teams_description": "根据需要向您的组织添加任意数量的子团队", + "unified_billing": "统一计费", + "unified_billing_description": "添加一张信用卡来支付您团队的所有订阅费用", + "advanced_managed_events": "高级托管事件类型", + "advanced_managed_events_description": "添加一张信用卡来支付您团队的所有订阅费用", + "enterprise_description": "升级到企业版以创建您的组织", + "create_your_org": "创建您的组织", + "create_your_org_description": "升级到企业版获得子域名、统一计费、分析见解、全面的白标功能等等", + "other_payment_app_enabled": "每种事件类型只能启用一个支付应用程序", + "admin_delete_organization_description": "
  • 属于该组织的团队及其事件类型也将被删除
  • 曾属于该组织的用户不会被删除,他们的事件类型也将保持不变。
  • 用户名将被更改,以允许他们在组织之外存在
", + "admin_delete_organization_title": "是否删除 {{organizationName}}?", + "published": "已发布", + "unpublished": "未发布", + "publish": "发布", + "org_publish_error": "无法发布组织", + "troubleshooter_tooltip": "打开问题排查器并找出您的日程安排有什么问题", + "troubleshooter": "问题排查器", + "number_to_call": "电话号码", + "guest_name": "访客姓名", + "guest_email": "访客邮箱", + "guest_company": "访客公司", + "please_install_a_calendar": "请安装日历", + "instant_tab_title": "即时预订", + "instant_event_tab_description": "让访客立即预订", + "uprade_to_create_instant_bookings": "升级到企业版,让访客加入与会者可以直接进入的即时通话。这仅适用于团队事件类型", + "dont_want_to_wait": "不想等待?", + "meeting_started": "会议开始", + "pay_and_book": "付费预订", + "cal_ai_event_tab_description": "让 AI 代理为您预订", + "booking_not_found_error": "找不到预订信息", + "booking_seats_full_error": "预订座位已满", + "missing_payment_credential_error": "缺少付款凭据", + "missing_payment_app_id_error": "缺少支付应用 ID", + "not_enough_available_seats_error": "预订中没有足够的可用座位", + "user_redirect_title": "{{username}} 目前暂时离开。", + "user_redirect_description": "同时,{{profile.username}} 将代表 {{username}} 负责所有新安排的会议。", + "out_of_office": "外出", + "out_of_office_description": "在您外出时在您的个人资料中配置操作。", + "send_request": "发送请求", + "start_date_and_end_date_required": "需要开始日期和结束日期", + "start_date_must_be_before_end_date": "开始日期必须在结束日期之前", + "start_date_must_be_in_the_future": "开始日期必须是未来的某个时间", + "user_not_found": "用户未找到", + "out_of_office_entry_already_exists": "外出条目已存在", + "out_of_office_id_required": "需要外出条目 ID", + "booking_redirect_infinite_not_allowed": "已经存在从该用户到您的预订重定向。", + "success_entry_created": "成功创建了一个新条目", + "booking_redirect_email_subject": "预订重定向通知", + "booking_redirect_email_title": "预订重定向通知", + "booking_redirect_email_description": "您已经收到来自 {{toName}} 的预订重定向,因此在时间间隔内他们的个人资料链接将被重定向到您的:", + "success_accept_booking_redirect": "您已接受此预订重定向请求。", + "success_reject_booking_redirect": "您已拒绝此预订重定向请求。", + "copy_link_booking_redirect_request": "复制链接以分享请求", + "booking_redirect_request_title": "预订重定向请求", + "select_team_member": "选择团队成员", + "going_away_title": "要离开吗? 只需将您的个人资料链接标记为在一段时间内不可用。", + "redirect_team_enabled": "将您的个人资料重定向到其他团队成员", + "redirect_team_disabled": "将您的个人资料重定向到其他团队成员(需要团队计划)", + "out_of_office_unavailable_list": "外出不可用列表", + "success_deleted_entry_out_of_office": "已成功删除条目", + "temporarily_out_of_office": "暂时外出?", + "add_a_redirect": "添加重定向", + "create_entry": "创建条目", + "time_range": "时间范围", + "automatically_add_all_team_members": "添加所有团队成员,包括未来成员", + "redirect_to": "重定向到", + "having_trouble_finding_time": "找不到合适的时间?", + "assignment_description": "安排每个人都有空时的会议,或轮换团队成员", + "lowest": "最低", + "low": "低", + "medium": "中", + "high": "高", + "Highest": "最高", + "send_booker_to": "将预订者发送到", + "set_priority": "设置优先级", + "priority_for_user": "{{userName}} 的优先级", + "change_priority": "更改优先级", + "field_identifiers_as_variables": "使用字段标识符作为自定义事件重定向的变量", + "field_identifiers_as_variables_with_example": "使用字段标识符作为自定义事件重定向的变量(例如 {{variable}})", + "account_already_linked": "帐户已链接", + "email_team_invite|subject|added_to_org": "{{user}} 将您添加到 {{appName}} 上的组织 {{team}}", + "email_team_invite|subject|invited_to_org": "{{user}} 邀请您加入 {{appName}} 上的组织 {{team}}", + "email_team_invite|subject|added_to_subteam": "{{user}} 将您添加到 {{appName}} 上的组织 {{parentTeamName}} 的团队 {{team}} 中", + "email_team_invite|subject|invited_to_subteam": "{{user}} 邀请您加入 {{appName}} 上的组织 {{parentTeamName}} 的团队 {{team}}", + "email_team_invite|heading|added_to_org": "您已被添加到 {{appName}} 组织", + "email_team_invite|heading|invited_to_org": "您已被邀请加入 {{appName}} 组织", + "email_team_invite|heading|added_to_subteam": "您已被添加到组织 {{parentTeamName}} 的一个团队中", + "email_team_invite|heading|invited_to_subteam": "您已被邀请加入组织 {{parentTeamName}} 的一个团队", + "email_team_invite|content|added_to_org": "{{invitedBy}} 已将您添加到组织 {{teamName}} 中。", + "email_team_invite|content|invited_to_org": "{{invitedBy}} 已邀请您加入组织 {{teamName}}。", + "email_team_invite|content|added_to_subteam": "{{invitedBy}} 已将您添加到他们组织 {{parentTeamName}} 的团队 {{teamName}} 中。{{appName}} 是一个活动协调调度器,使您和您的团队能够在不需繁杂的邮件往来的情况下安排会议。", + "email_team_invite|content|invited_to_subteam": "{{invitedBy}} 邀请您加入他们的组织 {{parentTeamName}} 中的团队 {{teamName}}。{{appName}} 是一款日程安排工具,可让您和您的团队在不必反复发送电子邮件的情况下安排会议。", + "email|existing_user_added_link_will_change": "接受邀请后,您的链接将更改为您的组织域名,但请不要担心,所有之前的链接仍然可以正常工作并会进行适当的重定向。

请注意:您所有的个人事件类型都将被移入 {teamName} 组织,这也可能包括潜在的个人链接。

对于个人事件,我们建议您使用个人电子邮件地址创建一个新帐户。", + "email|existing_user_added_link_changed": "您的链接已经从 {prevLinkWithoutProtocol} 修改为 {newLinkWithoutProtocol}。但是别担心,所有以前的链接仍然有效并适当重定向。

请注意:您的所有个人活动类型均已移至 {teamName} 组织中,其中可能还包含潜在的个人链接。

请登录并确保您的新组织帐户中没有私人事件。

对于个人活动,我们建议使用个人电子邮件地址创建一个新帐户。

享受新的干净链接:{newLinkWithoutProtocol}", + "email_organization_created|subject": "您的组织已创建完成", + "your_current_plan": "您当前的计划", + "organization_price_per_user_month": "$37 每用户每月(最低 30 个席位)", + "privacy_organization_description": "管理您团队的隐私设置", + "team_will_be_under_org": "新团队会在您的组织之下", + "add_group_name": "添加小组名", + "group_name": "小组名", + "routers": "途径", + "primary": "主要", + "make_primary": "设置为主要", + "add_email": "添加邮箱", + "add_email_description": "添加电子邮件地址以替换您的主要地址,或在您的活动类型中用作备用电子邮件。", + "confirm_email": "确认您的邮箱", + "confirm_email_description": "我们向 {{email}} 发送了一封邮件。点击邮件里的链接来确认这个地址。", + "send_event_details_to": "发送事件详情给", + "select_members": "选择成员", + "lock_event_types_modal_header": "我们该如何处理您成员现有的事件类型?", + "org_delete_event_types_org_admin": "您所有成员的个人事件类型(管理的事件类型除外)将被永久删除。他们将无法创建新的事件类型。", + "org_hide_event_types_org_admin": "您成员的个人事件类型将从个人资料中隐藏(管理的事件类型除外),但链接仍将处于活动状态。他们将无法创建新的事件类型。 ", + "hide_org_eventtypes": "隐藏个人事件类型", + "delete_org_eventtypes": "删除个人事件类型", + "lock_org_users_eventtypes": "锁定个人事件类型创建", + "lock_org_users_eventtypes_description": "阻止成员创建自己的事件类型。", + "make_a_call": "打电话", + "submit_feedback": "提交反馈", + "host_no_show": "您的主持人没有出现", + "no_show_description": "您可以与他们重新安排另一次会议", + "how_can_we_improve": "我们如何改进我们的服务?", + "most_liked": "你最喜欢什么?", + "review": "评论", + "reviewed": "已评论", + "unreviewed": "未评论", + "rating_url_info": "评级反馈表的 URL", + "no_show_url_info": "无反馈的 URL", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/scripts/check-missing-translations.ts b/apps/web/scripts/check-missing-translations.ts new file mode 100644 index 0000000000..d032b48085 --- /dev/null +++ b/apps/web/scripts/check-missing-translations.ts @@ -0,0 +1,51 @@ +import { readFileSync, readdirSync, writeFileSync } from "fs"; +import { join } from "path"; + +const TEMPLATE_LANGUAGE = "en"; +const SPECIFIC_LOCALES = process.argv.slice(2) || []; +const LOCALES_PATH = join(__dirname, "../public/static/locales"); + +const ALL_LOCALES = readdirSync(LOCALES_PATH); + +const templateJsonPath = join(LOCALES_PATH, `${TEMPLATE_LANGUAGE}/common.json`); +const templateJson: { [key: string]: string } = JSON.parse(readFileSync(templateJsonPath, "utf-8")); + +const missingTranslationLocales: string[] = []; +// If locales are not specified, then check all folders under `public/static/locales` +(SPECIFIC_LOCALES.length ? SPECIFIC_LOCALES : ALL_LOCALES).forEach((locale: string) => { + if (locale === TEMPLATE_LANGUAGE) return; + if (!ALL_LOCALES.includes(locale)) { + missingTranslationLocales.push(locale); + console.log(` + ❌ ${locale} is not found in ${LOCALES_PATH}! + If you want to create a new locale, Please create common.json under ${join(LOCALES_PATH, locale)}. + `); + return; + } + + const localeJsonPath = join(LOCALES_PATH, `${locale}/common.json`); + const localeJson: { [key: string]: string } = JSON.parse(readFileSync(localeJsonPath, "utf8")); + + if (Object.keys(templateJson).length === Object.keys(localeJson).length) return; + + const missingTranslations: { [key: string]: string } = {}; + missingTranslationLocales.push(locale); + Object.entries(templateJson).forEach(([key, value]: [string, string]) => { + if (key in localeJson) return; + + missingTranslations[key] = value; + }); + + const newLocaleJson = { + ...missingTranslations, + ...localeJson, + }; + writeFileSync(localeJsonPath, JSON.stringify(newLocaleJson, null, 2)); +}); + +if (missingTranslationLocales.length) { + console.log("🌍 The following locales need to be translated: "); + console.log(` ${missingTranslationLocales.join(", ")}`); +} else { + console.log("💯 All the locales are completely translated!"); +}