diff --git a/packages/console/src/ds-components/TabNav/TabNavItem.tsx b/packages/console/src/ds-components/TabNav/TabNavItem.tsx index 0a377705fdb..c0bde29ba63 100644 --- a/packages/console/src/ds-components/TabNav/TabNavItem.tsx +++ b/packages/console/src/ds-components/TabNav/TabNavItem.tsx @@ -51,7 +51,7 @@ function TabNavItem({ )} {errorCount > 0 && ( -
{t('general.tab_errors', { count: errorCount })}
+
{t('general.tab_error', { count: errorCount })}
)} ); diff --git a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx index 4e49fca2a3a..dc78727a6cc 100644 --- a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx +++ b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx @@ -62,7 +62,8 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { const isSsoEnabled = !isCloud || currentSubscriptionQuota.enterpriseSsoLimit === null || - currentSubscriptionQuota.enterpriseSsoLimit > 0; + currentSubscriptionQuota.enterpriseSsoLimit > 0 || + planId === ReservedPlanId.Pro; const { data, error } = useSWR( 'api/sso-connector-providers' diff --git a/packages/console/src/pages/EnterpriseSso/index.tsx b/packages/console/src/pages/EnterpriseSso/index.tsx index 63bb32cd0d2..e7ecc108293 100644 --- a/packages/console/src/pages/EnterpriseSso/index.tsx +++ b/packages/console/src/pages/EnterpriseSso/index.tsx @@ -37,7 +37,7 @@ function EnterpriseSso() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { isDevTenant } = useContext(TenantsContext); const { - currentSubscription: { isAddOnAvailable }, + currentSubscription: { planId, isAddOnAvailable }, currentSubscriptionQuota, } = useContext(SubscriptionDataContext); @@ -45,7 +45,8 @@ function EnterpriseSso() { page: 1, }); - const isSsoEnabled = !isCloud || currentSubscriptionQuota.enterpriseSsoLimit !== 0; + const isSsoEnabled = + !isCloud || currentSubscriptionQuota.enterpriseSsoLimit !== 0 || planId === ReservedPlanId.Pro; const url = buildUrl('api/sso-connectors', { page: String(page), diff --git a/packages/console/src/pages/Mfa/MfaForm/index.tsx b/packages/console/src/pages/Mfa/MfaForm/index.tsx index da3f718c39c..e82dd8d3c34 100644 --- a/packages/console/src/pages/Mfa/MfaForm/index.tsx +++ b/packages/console/src/pages/Mfa/MfaForm/index.tsx @@ -1,4 +1,4 @@ -import { MfaFactor, MfaPolicy, type SignInExperience } from '@logto/schemas'; +import { MfaFactor, MfaPolicy, ReservedPlanId, type SignInExperience } from '@logto/schemas'; import { useContext, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; @@ -33,9 +33,13 @@ type Props = { }; function MfaForm({ data, onMfaUpdated }: Props) { - const { currentSubscriptionQuota, mutateSubscriptionQuotaAndUsages } = - useContext(SubscriptionDataContext); - const isMfaDisabled = isCloud && !currentSubscriptionQuota.mfaEnabled; + const { + currentSubscription: { planId }, + currentSubscriptionQuota, + mutateSubscriptionQuotaAndUsages, + } = useContext(SubscriptionDataContext); + const isMfaDisabled = + isCloud && !currentSubscriptionQuota.mfaEnabled && planId !== ReservedPlanId.Pro; const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { getDocumentationUrl } = useDocumentationUrl(); diff --git a/packages/console/src/pages/Mfa/PageWrapper/index.tsx b/packages/console/src/pages/Mfa/PageWrapper/index.tsx index 87e819265f8..d198707f6ab 100644 --- a/packages/console/src/pages/Mfa/PageWrapper/index.tsx +++ b/packages/console/src/pages/Mfa/PageWrapper/index.tsx @@ -17,10 +17,10 @@ type Props = { function PageWrapper({ children }: Props) { const { isDevTenant } = useContext(TenantsContext); const { - currentSubscription: { isAddOnAvailable }, + currentSubscription: { planId, isAddOnAvailable }, currentSubscriptionQuota: { mfaEnabled }, } = useContext(SubscriptionDataContext); - const isMfaEnabled = !isCloud || mfaEnabled; + const isMfaEnabled = !isCloud || mfaEnabled || planId === ReservedPlanId.Pro; return (
diff --git a/packages/console/src/pages/OrganizationTemplate/index.tsx b/packages/console/src/pages/OrganizationTemplate/index.tsx index ff7fa80e158..1d94ae4a953 100644 --- a/packages/console/src/pages/OrganizationTemplate/index.tsx +++ b/packages/console/src/pages/OrganizationTemplate/index.tsx @@ -32,9 +32,13 @@ const basePathname = '/organization-template'; function OrganizationTemplate() { const { getDocumentationUrl } = useDocumentationUrl(); const [isGuideDrawerOpen, setIsGuideDrawerOpen] = useState(false); - const { currentSubscriptionQuota } = useContext(SubscriptionDataContext); + const { + currentSubscription: { planId }, + currentSubscriptionQuota, + } = useContext(SubscriptionDataContext); const { isDevTenant } = useContext(TenantsContext); - const isOrganizationsDisabled = isCloud && !currentSubscriptionQuota.organizationsEnabled; + const isOrganizationsDisabled = + isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro; const { navigate } = useTenantPathname(); const handleUpgradePlan = useCallback(() => { diff --git a/packages/console/src/pages/Organizations/CreateOrganizationModal/index.tsx b/packages/console/src/pages/Organizations/CreateOrganizationModal/index.tsx index 2ce43dc94b0..8cedab78617 100644 --- a/packages/console/src/pages/Organizations/CreateOrganizationModal/index.tsx +++ b/packages/console/src/pages/Organizations/CreateOrganizationModal/index.tsx @@ -40,7 +40,8 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) { data: { organizationUpsellNoticeAcknowledged }, update, } = useUserPreferences(); - const isOrganizationsDisabled = isCloud && !currentSubscriptionQuota.organizationsEnabled; + const isOrganizationsDisabled = + isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro; const { reset, diff --git a/packages/console/src/pages/Organizations/index.tsx b/packages/console/src/pages/Organizations/index.tsx index fca5ac77555..39d13fee0f8 100644 --- a/packages/console/src/pages/Organizations/index.tsx +++ b/packages/console/src/pages/Organizations/index.tsx @@ -26,7 +26,7 @@ const organizationsPathname = '/organizations'; function Organizations() { const { getDocumentationUrl } = useDocumentationUrl(); const { - currentSubscription: { isAddOnAvailable }, + currentSubscription: { planId, isAddOnAvailable }, currentSubscriptionQuota, } = useContext(SubscriptionDataContext); const { isDevTenant } = useContext(TenantsContext); @@ -34,7 +34,8 @@ function Organizations() { const { navigate } = useTenantPathname(); const [isCreating, setIsCreating] = useState(false); - const isOrganizationsDisabled = isCloud && !currentSubscriptionQuota.organizationsEnabled; + const isOrganizationsDisabled = + isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro; const upgradePlan = useCallback(() => { navigate(subscriptionPage); diff --git a/packages/core/src/libraries/quota.ts b/packages/core/src/libraries/quota.ts index 8ed7e8965da..6bd36276380 100644 --- a/packages/core/src/libraries/quota.ts +++ b/packages/core/src/libraries/quota.ts @@ -14,11 +14,14 @@ import { type CloudConnectionLibrary } from './cloud-connection.js'; export type QuotaLibrary = ReturnType; -const shouldReportSubscriptionUpdates = ( - planId: string, - key: keyof SubscriptionQuota, - isAddOnAvailable?: boolean -) => planId === ReservedPlanId.Pro && isAddOnAvailable && isReportSubscriptionUpdatesUsageKey(key); +/** + * @remarks + * Should report usage changes to the Cloud only when the following conditions are met: + * 1. The tenant is on the Pro plan. + * 2. The usage key is add-on related usage key. + */ +const shouldReportSubscriptionUpdates = (planId: string, key: keyof SubscriptionQuota) => + planId === ReservedPlanId.Pro && isReportSubscriptionUpdatesUsageKey(key); export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => { const guardTenantUsageByKey = async (key: keyof SubscriptionQuota) => { @@ -36,13 +39,12 @@ export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => { const { planId, - isAddOnAvailable, quota: fullQuota, usage: fullUsage, } = await getTenantSubscriptionData(cloudConnection); // Do not block Pro plan from adding add-on resources. - if (shouldReportSubscriptionUpdates(planId, key, isAddOnAvailable)) { + if (shouldReportSubscriptionUpdates(planId, key)) { return; } @@ -159,7 +161,7 @@ export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => { const { planId, isAddOnAvailable } = await getTenantSubscriptionData(cloudConnection); - if (shouldReportSubscriptionUpdates(planId, key, isAddOnAvailable)) { + if (shouldReportSubscriptionUpdates(planId, key) && isAddOnAvailable) { await reportSubscriptionUpdates(cloudConnection, key); } }; diff --git a/packages/core/src/utils/subscription/index.ts b/packages/core/src/utils/subscription/index.ts index 71e4e9ede7d..34f5416ff26 100644 --- a/packages/core/src/utils/subscription/index.ts +++ b/packages/core/src/utils/subscription/index.ts @@ -57,6 +57,10 @@ export const reportSubscriptionUpdates = async ( ); }; +/** + * @remarks + * Check whether the provided usage key is add-on related usage key. + */ export const isReportSubscriptionUpdatesUsageKey = ( value: string ): value is ReportSubscriptionUpdatesUsageKey => { diff --git a/packages/integration-tests/src/tests/console/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts b/packages/integration-tests/src/tests/console/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts index 288fee655cb..d2e389cf433 100644 --- a/packages/integration-tests/src/tests/console/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts +++ b/packages/integration-tests/src/tests/console/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts @@ -73,7 +73,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => { await expectErrorsOnNavTab(page, { tab: 'Sign-up and sign-in', - error: '1 errors', + error: '1 error', }); }); @@ -129,7 +129,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => { await expectErrorsOnNavTab(page, { tab: 'Sign-up and sign-in', - error: '1 errors', + error: '1 error', }); }); @@ -220,7 +220,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => { await expectSignInMethodError(page, 'Phone number'); await expectErrorsOnNavTab(page, { tab: 'Sign-up and sign-in', - error: '1 errors', + error: '1 error', }); // Disable password option for sign-in method @@ -234,7 +234,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => { await expectSignInMethodError(page, 'Phone number'); await expectErrorsOnNavTab(page, { tab: 'Sign-up and sign-in', - error: '1 errors', + error: '1 error', }); }); }); @@ -285,7 +285,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => { await expectSignInMethodError(page, 'Email address'); await expectErrorsOnNavTab(page, { tab: 'Sign-up and sign-in', - error: '1 errors', + error: '1 error', }); // Disable password option for sign-in method @@ -299,7 +299,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => { await expectSignInMethodError(page, 'Email address'); await expectErrorsOnNavTab(page, { tab: 'Sign-up and sign-in', - error: '1 errors', + error: '1 error', }); }); }); diff --git a/packages/phrases/src/locales/de/translation/admin-console/general.ts b/packages/phrases/src/locales/de/translation/admin-console/general.ts index e2fd47b4cda..d424cce11fe 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/general.ts @@ -47,7 +47,9 @@ const general = { continue: 'Fortsetzen', page_info: '{{min, number}}-{{max, number}} von {{total, number}}', learn_more: 'Mehr erfahren', - tab_errors: '{{count, number}} Fehler', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} Fehler', skip_for_now: 'Jetzt überspringen', remove: 'Entfernen', visit: 'Besuchen', diff --git a/packages/phrases/src/locales/en/translation/admin-console/general.ts b/packages/phrases/src/locales/en/translation/admin-console/general.ts index 1b5cc476588..fd43cbe2b09 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: 'Continue', page_info: '{{min, number}}-{{max, number}} of {{total, number}}', learn_more: 'Learn more', - tab_errors: '{{count, number}} errors', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} errors', skip_for_now: 'Skip for now', remove: 'Remove', visit: 'Visit', diff --git a/packages/phrases/src/locales/es/translation/admin-console/general.ts b/packages/phrases/src/locales/es/translation/admin-console/general.ts index 246bb821913..4b00c2a6274 100644 --- a/packages/phrases/src/locales/es/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/es/translation/admin-console/general.ts @@ -47,7 +47,9 @@ const general = { continue: 'Continuar', page_info: '{{min, number}}-{{max, number}} de {{total, number}}', learn_more: 'Saber más', - tab_errors: '{{count, number}} errores', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} errores', skip_for_now: 'Omitir por ahora', remove: 'Eliminar', visit: 'Visitar', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/general.ts b/packages/phrases/src/locales/fr/translation/admin-console/general.ts index f610dad3cd0..617f510b267 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/general.ts @@ -47,7 +47,9 @@ const general = { continue: 'Continuez', page_info: '{{min, number}}-{{max, number}} de {{total, number}}', learn_more: 'En savoir plus', - tab_errors: '{{count, number}} erreurs', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} erreurs', skip_for_now: 'Passer pour l`instant', remove: 'Supprimer', visit: 'Visiter', diff --git a/packages/phrases/src/locales/it/translation/admin-console/general.ts b/packages/phrases/src/locales/it/translation/admin-console/general.ts index 690332d6458..769487def32 100644 --- a/packages/phrases/src/locales/it/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/it/translation/admin-console/general.ts @@ -47,7 +47,9 @@ const general = { continue: 'Continua', page_info: '{{min, number}}-{{max, number}} di {{total, number}}', learn_more: 'Scopri di più', - tab_errors: '{{count, number}} errori', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} errori', skip_for_now: 'Salta per ora', remove: 'Rimuovi', visit: 'Visita', diff --git a/packages/phrases/src/locales/ja/translation/admin-console/general.ts b/packages/phrases/src/locales/ja/translation/admin-console/general.ts index 961660d32f4..8901c5edcee 100644 --- a/packages/phrases/src/locales/ja/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/ja/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: '続ける', page_info: '{{total}}件中{{min}}件〜{{max}}件を表示', learn_more: '詳しく見る', - tab_errors: '{{count}}件のエラーがあります', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count}}件のエラーがあります', skip_for_now: '今回はスキップする', remove: '削除する', visit: '訪問する', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/general.ts b/packages/phrases/src/locales/ko/translation/admin-console/general.ts index fe2b35870a2..b52caf58aa6 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: '계속하기', page_info: '{{min, number}}-{{max, number}} / {{total, number}}', learn_more: '더 알아보기', - tab_errors: '{{count, number}} 오류', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} 오류', skip_for_now: '지금은 건너뛰기', remove: '삭제', visit: '방문하기', diff --git a/packages/phrases/src/locales/pl-pl/translation/admin-console/general.ts b/packages/phrases/src/locales/pl-pl/translation/admin-console/general.ts index d803bf4796f..2e0502d343b 100644 --- a/packages/phrases/src/locales/pl-pl/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/pl-pl/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: 'Kontynuuj', page_info: '{{min, number}}-{{max, number}} z {{total, number}}', learn_more: 'Dowiedz się więcej', - tab_errors: '{{count, number}} błędów', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} błędów', skip_for_now: 'Pomiń na teraz', remove: 'Usuń', visit: 'Odwiedź', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/general.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/general.ts index 477510c5e26..7f674f1ab7d 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/general.ts @@ -47,7 +47,9 @@ const general = { continue: 'Continuar', page_info: '{{min, number}}-{{max, number}} de {{total, number}}', learn_more: 'Saiba mais', - tab_errors: '{{count, number}} erros', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} erros', skip_for_now: 'Pular por agora', remove: 'Remover', visit: 'Visitar', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/general.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/general.ts index c3c6a0aa6eb..1765802a833 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: 'Continuar', page_info: '{{min, number}}-{{max, number}} de {{total, number}}', learn_more: 'Saber mais', - tab_errors: '{{count, number}} erros', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} erros', skip_for_now: 'Saltar por agora', remove: 'Remover', visit: 'Visitar', diff --git a/packages/phrases/src/locales/ru/translation/admin-console/general.ts b/packages/phrases/src/locales/ru/translation/admin-console/general.ts index c4dcd3993d5..76958a47b03 100644 --- a/packages/phrases/src/locales/ru/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/ru/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: 'Продолжить', page_info: '{{min, number}}-{{max, number}} из {{total, number}}', learn_more: 'Узнать больше', - tab_errors: '{{count, number}} ошибок', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} ошибок', skip_for_now: 'Пропустить', remove: 'Удалить', visit: 'Посетить', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/general.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/general.ts index db6ab408365..17f0a3a83db 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/general.ts @@ -47,7 +47,9 @@ const general = { continue: 'Devam et', page_info: '{{min, number}}-{{max, number}} / {{total, number}}', learn_more: 'Daha fazla bilgi edinin', - tab_errors: '{{count, number}} hata', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} hata', skip_for_now: 'Şimdilik atla', remove: 'Kaldır', visit: 'Ziyaret et', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/general.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/general.ts index ba58a7e80a8..c083ec9f758 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: '继续', page_info: '{{min, number}}-{{max, number}} 共 {{total, number}} 条', learn_more: '了解更多', - tab_errors: '{{count, number}} 个错误', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} 个错误', skip_for_now: '先跳过', remove: '移除', visit: '访问', diff --git a/packages/phrases/src/locales/zh-hk/translation/admin-console/general.ts b/packages/phrases/src/locales/zh-hk/translation/admin-console/general.ts index 73fd38b1bd5..4a84ef24df4 100644 --- a/packages/phrases/src/locales/zh-hk/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/zh-hk/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: '繼續', page_info: '{{min, number}}-{{max, number}} 共 {{total, number}} 條', learn_more: '了解更多', - tab_errors: '{{count, number}} 個錯誤', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} 個錯誤', skip_for_now: '先跳過', remove: '移除', visit: '訪問', diff --git a/packages/phrases/src/locales/zh-tw/translation/admin-console/general.ts b/packages/phrases/src/locales/zh-tw/translation/admin-console/general.ts index 2b7ad3d8f63..6c6bf507410 100644 --- a/packages/phrases/src/locales/zh-tw/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/zh-tw/translation/admin-console/general.ts @@ -46,7 +46,9 @@ const general = { continue: '繼續', page_info: '{{min, number}}-{{max, number}} 共 {{total, number}} 條', learn_more: '了解更多', - tab_errors: '{{count, number}} 個錯誤', + /** UNTRANSLATED */ + tab_error_one: '{{count, number}} error', + tab_error_other: '{{count, number}} 個錯誤', skip_for_now: '先跳過', remove: '移除', visit: '訪問',