0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

refactor(console): add machine-to-machine roles limit guard (#5131)

This commit is contained in:
Xiao Yijun 2023-12-21 07:42:59 +08:00 committed by GitHub
parent e4c73e7bb7
commit 2e9ccb08bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 200 additions and 93 deletions

View file

@ -0,0 +1,98 @@
import { type RoleResponse, RoleType, ReservedPlanId } from '@logto/schemas';
import { useContext } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import useSWR from 'swr';
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
import PlanName from '@/components/PlanName';
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button';
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
import { hasReachedQuotaLimit } from '@/utils/quota';
import { buildUrl } from '@/utils/url';
type Props = {
roleType: RoleType;
selectedScopesCount: number;
isCreating: boolean;
onClickCreate: () => void;
};
function Footer({ roleType, selectedScopesCount, isCreating, onClickCreate }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { currentTenantId } = useContext(TenantsContext);
const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
const { data: [, roleCount] = [] } = useSWR<[RoleResponse[], number]>(
isCloud &&
buildUrl('api/roles', {
page: String(1),
page_size: String(1),
type: roleType,
})
);
const hasRoleReachedLimit = hasReachedQuotaLimit({
quotaKey: roleType === RoleType.User ? 'rolesLimit' : 'machineToMachineRolesLimit',
plan: currentPlan,
usage: roleCount ?? 0,
});
const hasBeyondScopesPerRoleLimit = hasReachedQuotaLimit({
quotaKey: 'scopesPerRoleLimit',
plan: currentPlan,
/**
* If usage equals to the limit, it means the current role has reached the maximum allowed scope.
* Therefore, we should not assign any more scopes at this point.
* However, the currently selected scopes haven't been assigned yet, so we subtract 1
* to allow the assignment when the scope count equals to the limit.
*/
usage: selectedScopesCount - 1,
});
if (currentPlan && (hasRoleReachedLimit || hasBeyondScopesPerRoleLimit)) {
return (
<QuotaGuardFooter>
<Trans
components={{
a: <ContactUsPhraseLink />,
planName: <PlanName name={currentPlan.name} />,
}}
>
{/* User roles limit paywall */}
{hasRoleReachedLimit &&
roleType === RoleType.User &&
t('upsell.paywall.roles', { count: currentPlan.quota.rolesLimit ?? 0 })}
{hasRoleReachedLimit &&
roleType === RoleType.MachineToMachine &&
/* Todo @xiaoyijun [Pricing] Remove feature flag */
(!isDevFeaturesEnabled && currentPlan.id === ReservedPlanId.Free
? t('upsell.paywall.deprecated_machine_to_machine_feature')
: t('upsell.paywall.machine_to_machine_roles', {
count: currentPlan.quota.machineToMachineRolesLimit ?? 0,
}))}
{/* Role scopes limit paywall */}
{!hasRoleReachedLimit &&
hasBeyondScopesPerRoleLimit &&
t('upsell.paywall.scopes_per_role', {
count: currentPlan.quota.scopesPerRoleLimit ?? 0,
})}
</Trans>
</QuotaGuardFooter>
);
}
return (
<Button
isLoading={isCreating}
htmlType="submit"
title="roles.create_role_button"
size="large"
type="primary"
onClick={onClickCreate}
/>
);
}
export default Footer;

View file

@ -4,14 +4,11 @@ import { ReservedPlanId, RoleType, internalRolePrefix } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
import KeyboardArrowUp from '@/assets/icons/keyboard-arrow-up.svg';
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
import FeatureTag from '@/components/FeatureTag';
import PlanName from '@/components/PlanName';
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
import RoleScopesTransfer from '@/components/RoleScopesTransfer';
import { isDevFeaturesEnabled } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider';
@ -23,10 +20,9 @@ import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
import { ReservedPlanName } from '@/types/subscriptions';
import { trySubmitSafe } from '@/utils/form';
import { hasReachedQuotaLimit } from '@/utils/quota';
import Footer from './Footer';
import * as styles from './index.module.scss';
type RadioOption = { key: AdminConsoleKey; value: RoleType; hasPaywall: boolean };
@ -37,7 +33,6 @@ const radioOptions: RadioOption[] = [
];
export type Props = {
totalRoleCount: number;
onClose: (createdRole?: Role) => void;
};
@ -49,7 +44,7 @@ type CreateRolePayload = Pick<Role, 'name' | 'description' | 'type'> & {
scopeIds?: string[];
};
function CreateRoleForm({ totalRoleCount, onClose }: Props) {
function CreateRoleForm({ onClose }: Props) {
const { currentTenantId } = useContext(TenantsContext);
const [isTypeSelectorVisible, setIsTypeSelectorVisible] = useState(false);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -63,7 +58,6 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
} = useForm<CreateRoleFormData>({ defaultValues: { type: RoleType.User } });
const api = useApi();
const roleScopes = watch('scopes', []);
const onSubmit = handleSubmit(
trySubmitSafe(async ({ name, description, type, scopes }) => {
@ -83,24 +77,6 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
})
);
const isRolesReachLimit = hasReachedQuotaLimit({
quotaKey: 'rolesLimit',
plan: currentPlan,
usage: totalRoleCount,
});
const isScopesPerReachLimit = hasReachedQuotaLimit({
quotaKey: 'scopesPerRoleLimit',
plan: currentPlan,
/**
* If usage is equal to the limit, it means the current role has reached the maximum allowed scope.
* Therefore, we should not assign any more scopes at this point.
* However, the currently selected scopes haven't been assigned yet, so we subtract 1
* to allow the assignment when the scope count is equal to the limit.
*/
usage: roleScopes.length - 1,
});
return (
<ModalLayout
title="roles.create_role_title"
@ -110,66 +86,14 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
targetBlank: 'noopener',
}}
size="large"
footer={(() => {
if (
currentPlan?.name === ReservedPlanName.Free &&
watch('type') === RoleType.MachineToMachine
) {
return (
<QuotaGuardFooter>
<Trans
components={{
a: <ContactUsPhraseLink />,
}}
>
{t('upsell.paywall.deprecated_machine_to_machine_feature')}
</Trans>
</QuotaGuardFooter>
);
}
if (isRolesReachLimit && currentPlan) {
return (
<QuotaGuardFooter>
<Trans
components={{
a: <ContactUsPhraseLink />,
planName: <PlanName name={currentPlan.name} />,
}}
>
{t('upsell.paywall.roles', { count: currentPlan.quota.rolesLimit ?? 0 })}
</Trans>
</QuotaGuardFooter>
);
}
if (isScopesPerReachLimit && currentPlan && !isRolesReachLimit) {
return (
<QuotaGuardFooter>
<Trans
components={{
a: <ContactUsPhraseLink />,
planName: <PlanName name={currentPlan.name} />,
}}
>
{t('upsell.paywall.scopes_per_role', {
count: currentPlan.quota.scopesPerRoleLimit ?? 0,
})}
</Trans>
</QuotaGuardFooter>
);
}
if (!isRolesReachLimit && !isScopesPerReachLimit) {
return (
<Button
isLoading={isSubmitting}
htmlType="submit"
title="roles.create_role_button"
size="large"
type="primary"
onClick={onSubmit}
/>
);
}
})()}
footer={
<Footer
roleType={watch('type')}
selectedScopesCount={watch('scopes', []).length}
isCreating={isSubmitting}
onClickCreate={onSubmit}
/>
}
onClose={onClose}
>
<form>

View file

@ -12,11 +12,10 @@ import type { Props as CreateRoleFormProps } from '../CreateRoleForm';
import CreateRoleForm from '../CreateRoleForm';
type Props = {
totalRoleCount: number;
onClose: () => void;
};
function CreateRoleModal({ totalRoleCount, onClose }: Props) {
function CreateRoleModal({ onClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { navigate } = useTenantPathname();
@ -51,7 +50,7 @@ function CreateRoleModal({ totalRoleCount, onClose }: Props) {
}}
/>
) : (
<CreateRoleForm totalRoleCount={totalRoleCount} onClose={onCreateFormClose} />
<CreateRoleForm onClose={onCreateFormClose} />
)}
</ReactModal>
);

View file

@ -196,10 +196,8 @@ function Roles() {
onRetry: async () => mutate(undefined, true),
}}
widgets={
isCreating &&
totalCount !== undefined && (
isCreating && (
<CreateRoleModal
totalRoleCount={totalCount}
onClose={() => {
navigate({ pathname: rolesPathname, search });
}}

View file

@ -40,6 +40,12 @@ const paywall = {
'Sie haben das Limit von {{count, number}} <planName/>-Rollen erreicht. Upgraden Sie Ihren Plan, um zusätzliche Rollen und Berechtigungen hinzuzufügen. Zögern Sie nicht, <a>Kontaktieren Sie uns</a>, wenn Sie Hilfe benötigen.',
roles_other:
'Sie haben das Limit von {{count, number}} <planName/>-Rollen erreicht. Upgraden Sie Ihren Plan, um zusätzliche Rollen und Berechtigungen hinzuzufügen. Zögern Sie nicht, <a>Kontaktieren Sie uns</a>, wenn Sie Hilfe benötigen.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'Sie haben das Limit von {{count, number}} Berechtigungen pro Rolle von <planName/> erreicht. Upgraden Sie Ihren Plan, um zusätzliche Rollen und Berechtigungen hinzuzufügen. Bei Fragen stehen wir Ihnen gerne zur Verfügung. <a>Kontaktieren Sie uns</a>.',
scopes_per_role_other:

View file

@ -39,6 +39,10 @@ const paywall = {
'{{count, number}} role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
roles_other:
'{{count, number}} roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'{{count, number}} permission per role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. For any assistance, feel free to <a>contact us</a>.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'Has alcanzado el límite de {{count, number}} roles de <planName/>. Actualiza el plan para agregar roles y permisos adicionales. Si necesitas ayuda, no dudes en <a>contactarnos</a>.',
roles_other:
'Has alcanzado el límite de {{count, number}} roles de <planName/>. Actualiza el plan para agregar roles y permisos adicionales. Si necesitas ayuda, no dudes en <a>contactarnos</a>.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'Has alcanzado el límite de {{count, number}} permisos por rol de <planName/>. Actualiza el plan para agregar roles y permisos adicionales. Si necesitas ayuda, no dudes en <a>contactarnos</a>.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
"Vous avez atteint la limite de {{count, number}} rôle de <planName/>. Mettez à niveau votre plan pour ajouter des rôles et des permissions supplémentaires. Nhésitez pas à <a>nous contacter</a> si vous avez besoin d'aide.",
roles_other:
"Vous avez atteint la limite de {{count, number}} rôles de <planName/>. Mettez à niveau votre plan pour ajouter des rôles et des permissions supplémentaires. Nhésitez pas à <a>nous contacter</a> si vous avez besoin d'aide.",
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'Vous avez atteint la limite de {{count, number}} permission par rôle de <planName/>. Mettez à niveau votre plan pour ajouter des rôles et des permissions supplémentaires. Pour toute assistance, nhésitez pas à <a>nous contacter</a>.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'Hai raggiunto il limite di {{count, number}} ruoli di <planName/>. Aggiorna il piano per aggiungere ruoli e autorizzazioni aggiuntive. Non esitare a <a>contattarci</a> se hai bisogno di assistenza.',
roles_other:
'Hai raggiunto il limite di {{count, number}} ruoli di <planName/>. Aggiorna il piano per aggiungere ruoli e autorizzazioni aggiuntive. Non esitare a <a>contattarci</a> se hai bisogno di assistenza.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'Hai raggiunto il limite di {{count, number}} autorizzazioni per ruolo di <planName/>. Aggiorna il piano per aggiungere ruoli e autorizzazioni aggiuntive. Non esitare a <a>contattarci</a> se hai bisogno di assistenza.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'{{count, number}}の<planName/>ロール制限に達しました。追加のロールと権限を追加するにはプランをアップグレードしてください。<a>お問い合わせ</a>は何かお手伝いが必要な場合はお気軽にどうぞ。',
roles_other:
'{{count, number}}の<planName/>ロール制限に達しました。追加のロールと権限を追加するにはプランをアップグレードしてください。<a>お問い合わせ</a>は何かお手伝いが必要な場合はお気軽にどうぞ。',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'{{count, number}}の<planName/>ロールあたりの許可制限に達しました。追加のロールと権限を追加するにはプランをアップグレードしてください。<a>お問い合わせ</a>は何かお手伝いが必要な場合はお気軽にどうぞ。',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'<planName/>의 {{count, number}}개 역할 한도에 도달했습니다. 플랜을 업그레이드하여 추가 역할과 권한을 추가하세요. 도움이 필요하면 <a>문의하기</a>로 연락 주세요.',
roles_other:
'<planName/>의 {{count, number}}개 역할 한도에 도달했습니다. 플랜을 업그레이드하여 추가 역할과 권한을 추가하세요. 도움이 필요하면 <a>문의하기</a>로 연락 주세요.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'<planName/>의 {{count, number}}개 역할 당 권한 한도에 도달했습니다. 플랜을 업그레이드하여 추가 역할과 권한을 추가하세요. 도움이 필요하면 <a>문의하기</a>로 연락 주세요.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'Osiągnięto limit {{count, number}} ról w planie <planName/>. Ulepsz plan, aby dodać dodatkowe role i uprawnienia. Jeśli potrzebujesz pomocy, nie wahaj się <a>skontaktować z nami</a>.',
roles_other:
'Osiągnięto limit {{count, number}} ról w planie <planName/>. Ulepsz plan, aby dodać dodatkowe role i uprawnienia. Jeśli potrzebujesz pomocy, nie wahaj się <a>skontaktować z nami</a>.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'Osiągnięto limit {{count, number}} uprawnień na rolę w planie <planName/>. Ulepsz plan, aby dodać dodatkowe role i uprawnienia. W razie potrzeb, skontaktuj się z nami <a>tutaj</a>.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'Atingiu o limite de {{count, number}} funções de <planName/>. Atualize o plano para adicionar funções e permissões adicionais. Não hesite em <a>Contacte-nos</a> se precisar de ajuda.',
roles_other:
'Atingiu o limite de {{count, number}} funções de <planName/>. Atualize o plano para adicionar funções e permissões adicionais. Não hesite em <a>Contacte-nos</a> se precisar de ajuda.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'Atingiu o limite de {{count, number}} permissões por função de <planName/>. Atualize o plano para adicionar funções e permissões adicionais. Não hesite em <a>Contacte-nos</a> se precisar de ajuda.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'Atingiu o limite de {{count, number}} funções de <planName/>. Atualize o plano para adicionar funções e permissões adicionais. Não hesite em <a>Contacte-nos</a> se precisar de ajuda.',
roles_other:
'Atingiu o limite de {{count, number}} funções de <planName/>. Atualize o plano para adicionar funções e permissões adicionais. Não hesite em <a>Contacte-nos</a> se precisar de ajuda.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'Atingiu o limite de {{count, number}} permissões por função de <planName/>. Atualize o plano para adicionar funções e permissões adicionais. Não hesite em <a>Contacte-nos</a> se precisar de ajuda.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'Достигнут лимит {{count, number}} ролей в плане <planName/>. Повысьте план, чтобы добавить дополнительные роли и разрешения. Если вам нужна помощь, не стесняйтесь <a>связаться с нами</a>.',
roles_other:
'Достигнут лимит {{count, number}} ролей в плане <planName/>. Повысьте план, чтобы добавить дополнительные роли и разрешения. Если вам нужна помощь, не стесняйтесь <a>связаться с нами</a>.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'Достигнут лимит {{count, number}} разрешений на роль в плане <planName/>. Повысьте план, чтобы добавить дополнительные роли и разрешения. Если вам нужна помощь, не стесняйтесь <a>связаться с нами</a>.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'{{count, number}} <planName/> rol sınırına ulaşıldı. İlave roller ve izinler eklemek için planı yükseltin. Yardıma ihtiyacınız olursa, <a>iletişime geçin</a>.',
roles_other:
'{{count, number}} <planName/> rol sınırına ulaşıldı. İlave roller ve izinler eklemek için planı yükseltin. Yardıma ihtiyacınız olursa, <a>iletişime geçin</a>.',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'{{count, number}} <planName/> rol başına izin sınırına ulaşıldı. İlave roller ve izinler eklemek için planı yükseltin. Yardıma ihtiyacınız olursa, <a>iletişime geçin</a>.',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'已达到<planName/>的{{count, number}}个角色限制。升级计划以添加额外的角色和权限。如需任何帮助,请<a>联系我们</a>。',
roles_other:
'已达到<planName/>的{{count, number}}个角色限制。升级计划以添加额外的角色和权限。如需任何帮助,请<a>联系我们</a>。',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'已达到<planName/>的{{count, number}}个角色每个权限限制。升级计划以添加额外的角色和权限。如需任何帮助,请<a>联系我们</a>。',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'已達到<planName/>的{{count, number}}個角色限制。升級計劃以添加額外的角色和權限。如需任何協助,歡迎<a>聯繫我們</a>。',
roles_other:
'已達到<planName/>的{{count, number}}個角色限制。升級計劃以添加額外的角色和權限。如需任何協助,歡迎<a>聯繫我們</a>。',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'已達到<planName/>的{{count, number}}個角色每個權限限制。升級計劃以添加額外的角色和權限。如需任何協助,歡迎<a>聯繫我們</a>。',
scopes_per_role_other:

View file

@ -40,6 +40,12 @@ const paywall = {
'已達到<planName/>的{{count, number}}個角色限制。升級計劃以添加額外的角色和權限。如需任何幫助,請<a>聯繫我們</a>。',
roles_other:
'已達到<planName/>的{{count, number}}個角色限制。升級計劃以添加額外的角色和權限。如需任何幫助,請<a>聯繫我們</a>。',
/** UNTRANSLATED */
machine_to_machine_roles:
'{{count, number}} machine-to-machine role of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
/** UNTRANSLATED */
machine_to_machine_roles_other:
'{{count, number}} machine-to-machine roles of <planName/> limit reached. Upgrade plan to add additional roles and permissions. Feel free to <a>contact us</a> if you need any assistance.',
scopes_per_role:
'已達到<planName/>的{{count, number}}個角色每個權限限制。升級計劃以添加額外的角色和權限。如需任何幫助,請<a>聯繫我們</a>。',
scopes_per_role_other: