feat(billing): enforce free-plan student cap across the app (#733)
* feat(billing): enforce free-plan student cap across the app
Free (BASIC) plan orgs were able to exceed the advertised 20-student
limit because the cap existed only as dashboard cosmetic copy with no
server-side enforcement.
Add a single source of truth (getStudentLimit / STUDENT_LIMITS) and a
central guard (assertStudentCapacityOrThrow) wired into every path that
creates a new student-role org member: self-enrolment, course-invite
acceptance, bulk audience import, and org-invite acceptance. The guard
throws UPGRADE_REQUIRED (403) and notifies org admins by email (throttled
to once per org per day) when the limit is hit.
Surface the limit in the UI: disable the audience import button and the
course new-student invite section with upgrade banners for admins, and
show a "contact your administrator" message on the public enrol page for
students (who cannot upgrade). Expose a studentLimitReached flag from the
public course endpoint so the enrol button can be disabled proactively.
Add a one-time (manual, non-cron) db script,
db:notify-students-over-limit, to email admins of orgs already over the
limit at rollout.
* feat(billing): guard remaining student-creating paths against the cap
The initial cap enforcement missed several service entry points that also
create a new student-role org member. Add the capacity guard to:
- autoEnrollStudent (public org self-join on a tenant site)
- cohort enrollment (bulk group enrollment and single member accept)
- ensureProgramCourseAccess (program/cohort-granted course access)
For the cohort and program paths, only genuinely new members are counted
(existing org members are skipped) so re-enrolling current students is
never falsely blocked. The core path enforces via the shared limit check;
it throws UPGRADE_REQUIRED without the admin email since the queue lives
in the api layer.
* fix(billing): don't show cap block to existing org members on enroll page
The public course-by-slug endpoint is unauthenticated, so the enroll
page's studentLimitReached flag is org-wide and can't tell whether the
current viewer is already an org member. An existing student of a full
BASIC org would see Join disabled even though the backend correctly lets
existing members enroll without consuming a new seat.
Only pre-block anonymous visitors (would-be new joiners). Logged-in users
are governed by the membership-aware backend, which allows existing
members and returns UPGRADE_REQUIRED (surfaced via the enroll onError
mapping) for genuinely new students.
* feat(billing): deliver org plan-limits via /account with an extensible registry
Replace the ad-hoc, per-surface audience fetches (each calling getOrgAudience
just to read a count) with a single, future-proof delivery of org usage +
limits on the /account payload, gated to admin/tutor members (students never
receive limit data; a plain COUNT, no student rows).
Model it generically so future org limits (courses, etc.) drop in without
reshaping the payload or re-plumbing the frontend:
- New packages/utils plan-limits registry: PLAN_LIMIT_RESOURCES, PLAN_LIMITS
(reuses STUDENT_LIMITS), getPlanLimit, and a JSON-safe ResourceUsage /
OrgUsageLimits shape with isResourceLimitReached / resourceLimitRemaining.
- getAccountData attaches a per-org `limits` map for admin/tutor orgs.
- Store selectors currentOrgLimits / studentUsage / isStudentLimitReached;
sidebars, upgrade card, People pages, and invite modals read these instead
of fetching audience rows.
Bring cohort surfaces to parity with course: cohort People page banner and
cohort sidebar People upgrade icon, matching the course equivalents.
Also: close the upgrade modal that opened over the invite modal (drop the
`add` param via openUpgradeModalOver), redesign the sidebar upgrade card
(progress toward the student limit), add a Progress `variant`, and document
the no-complex-expressions-in-object-literals rule in AGENTS.md.
* refactor(billing): address PR review on the limits work
- Dedupe new student profile IDs before the cohort seat check so duplicate
batch entries can't overcount against the cap (Greptile P1).
- Drive the org-sidebar upgrade indicator from a dynamic nav-item `upgrade`
flag (audience config declares `upgradeResource: 'students'`; groups set it
from a `limitsReached` map) instead of a hardcoded path check.
- Collapse the audience Import button into a single button with ternary
`disabled`/`href`.
- Simplify the notify-students-over-limit upgrade link to app.classroomio.com.
- Drop the comment on the org `limits` field.
- Add .greptile/rules.md documenting the manual one-time db/src/scripts
convention (operator-controlled reruns) so idempotency isn't reflagged.
* fix(billing): name the org in the enroll capacity message
Change the enroll-page student-limit message from "This course has reached
its student capacity" to "{orgName} has reached its student capacity", since
the cap is org-wide, not course-specific. Pass orgName from the loaded org on
the page and from the currentOrg store in the enroll onError snackbar.
* style(billing): use PremiumIcon for the Authentication upgrade indicator
Replace the RocketIcon on paid settings sub-items (e.g. Authentication) with
the PremiumIcon used on the Audience item, right-aligned (ml-auto) to match.
This commit is contained in:
@@ -709,7 +709,8 @@
|
||||
"cancel": "Annuller",
|
||||
"confirm": "Slet",
|
||||
"snackbar_success": "Medlem slettet"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "Du har nået din plans elevgrænse. Opgrader for at importere flere elever."
|
||||
},
|
||||
"community": {
|
||||
"title": "Fællesskab",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "Tilbage til kurset",
|
||||
"join_course": "Deltag i kursus",
|
||||
"enrolled_success": "Du er tilmeldt dette kursus.",
|
||||
"enrolled_verify_email": "Du er tilmeldt! Bekræft din e-mail for at få adgang til dette kursus."
|
||||
"enrolled_verify_email": "Du er tilmeldt! Bekræft din e-mail for at få adgang til dette kursus.",
|
||||
"student_limit_reached": "{orgName} har nået sin elevkapacitet. Kontakt venligst din organisations administrator."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "Gratis",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "Multi-brug (7 dage)",
|
||||
"multi_use_30d": "Multi-brug (30 dage)",
|
||||
"custom": "Brugerdefineret"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "Du har nået din plans elevgrænse. Opgrader for at invitere flere elever."
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "Statistik",
|
||||
"analytics": "Analytics",
|
||||
"compliance": "Overholdelse",
|
||||
"cohorts": "Programmer"
|
||||
"cohorts": "Programmer",
|
||||
"upgrade_now": "Opgrader nu",
|
||||
"upgrade_students": "{studentCount} af {studentLimit} elever",
|
||||
"upgrade_left": "{count} tilbage"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "Media Manager - ClassroomIO",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "Widgets",
|
||||
"tags": "Tags",
|
||||
"students": "studerende",
|
||||
"settings": "Indstillinger"
|
||||
"settings": "Indstillinger",
|
||||
"cohorts": "Kohorter"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -709,7 +709,8 @@
|
||||
"cancel": "Abbrechen",
|
||||
"confirm": "Löschen",
|
||||
"snackbar_success": "Mitglied gelöscht"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "Sie haben das Studentenlimit Ihres Plans erreicht. Führen Sie ein Upgrade durch, um mehr Schüler zu importieren."
|
||||
},
|
||||
"community": {
|
||||
"title": "Gemeinschaft",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "Zurück zum Kurs",
|
||||
"join_course": "Nehmen Sie am Kurs teil",
|
||||
"enrolled_success": "Sie sind für diesen Kurs eingeschrieben.",
|
||||
"enrolled_verify_email": "Sie sind eingeschrieben! Bestätigen Sie Ihre E-Mail, um auf diesen Kurs zuzugreifen."
|
||||
"enrolled_verify_email": "Sie sind eingeschrieben! Bestätigen Sie Ihre E-Mail, um auf diesen Kurs zuzugreifen.",
|
||||
"student_limit_reached": "{orgName} hat seine Studentenkapazität erreicht. Bitte wenden Sie sich an den Administrator Ihrer Organisation."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "Kostenlos",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "Mehrfachnutzung (7 Tage)",
|
||||
"multi_use_30d": "Mehrfachnutzung (30 Tage)",
|
||||
"custom": "Benutzerdefiniert"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "Sie haben das Studentenlimit Ihres Plans erreicht. Führen Sie ein Upgrade durch, um weitere Schüler einzuladen."
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "Statistiken",
|
||||
"analytics": "Analytik",
|
||||
"compliance": "Compliance",
|
||||
"cohorts": "Programme"
|
||||
"cohorts": "Programme",
|
||||
"upgrade_now": "Jetzt upgraden",
|
||||
"upgrade_students": "{studentCount} von {studentLimit} Studenten",
|
||||
"upgrade_left": "{count} übrig"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "Medienmanager - ClassroomIO",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "Widgets",
|
||||
"tags": "Schlagworte",
|
||||
"students": "Studenten",
|
||||
"settings": "Einstellungen"
|
||||
"settings": "Einstellungen",
|
||||
"cohorts": "Kohorten"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1259,6 +1259,9 @@
|
||||
"early_adopter": "Become an Early Adopter",
|
||||
"unlock": "Unlock unlimited features and invest in our future",
|
||||
"upgrade": "Upgrade Now",
|
||||
"upgrade_now": "Upgrade now",
|
||||
"upgrade_students": "{studentCount} of {studentLimit} students",
|
||||
"upgrade_left": "{count} left",
|
||||
"cohorts": "Cohorts"
|
||||
},
|
||||
"pricing": {
|
||||
@@ -3054,6 +3057,7 @@
|
||||
"to_add": "To add tutor to your organization,",
|
||||
"go_to": "go to organization settings",
|
||||
"invite_students": "Add Students",
|
||||
"student_limit_reached": "You've reached your plan's student limit. Upgrade to invite more students.",
|
||||
"via_link": "You can invite students via an invite link",
|
||||
"via_qr": "You can invite students via QR",
|
||||
"download_qr": "Download QR code",
|
||||
@@ -3373,7 +3377,8 @@
|
||||
"back_to_course": "Back to course",
|
||||
"join_course": "Join Course",
|
||||
"enrolled_success": "You're enrolled in this course.",
|
||||
"enrolled_verify_email": "You're enrolled! Verify your email to access this course."
|
||||
"enrolled_verify_email": "You're enrolled! Verify your email to access this course.",
|
||||
"student_limit_reached": "{orgName} has reached its student capacity. Please contact your organization administrator."
|
||||
},
|
||||
"course_content": "Course content",
|
||||
"modules": "modules",
|
||||
@@ -3558,6 +3563,7 @@
|
||||
"page_subtitle": "See who belongs to your org, who is learning, who is invited, and how to reach them.",
|
||||
"export": "Export",
|
||||
"import_users": "Import Users",
|
||||
"import_limit_reached": "You've reached your plan's student limit. Upgrade to import more students.",
|
||||
"no_audience": "No audience to manage",
|
||||
"manage": "Manage all your students here and stay connected with them",
|
||||
"name": "Name",
|
||||
|
||||
@@ -709,7 +709,8 @@
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Eliminar",
|
||||
"snackbar_success": "Miembro eliminado"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "Has alcanzado el límite de estudiantes de tu plan. Actualice para importar más estudiantes."
|
||||
},
|
||||
"community": {
|
||||
"title": "Comunidad",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "volver al curso",
|
||||
"join_course": "Unirse al curso",
|
||||
"enrolled_success": "Estás inscrito en este curso.",
|
||||
"enrolled_verify_email": "¡Estás inscrito! Verifica tu correo electrónico para acceder a este curso."
|
||||
"enrolled_verify_email": "¡Estás inscrito! Verifica tu correo electrónico para acceder a este curso.",
|
||||
"student_limit_reached": "{orgName} ha alcanzado su capacidad de estudiantes. Comuníquese con el administrador de su organización."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "Gratis",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "Multiuso (7 días)",
|
||||
"multi_use_30d": "Multiuso (30 días)",
|
||||
"custom": "personalizado"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "Has alcanzado el límite de estudiantes de tu plan. Actualice para invitar a más estudiantes."
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "Estadísticas",
|
||||
"analytics": "Analítica",
|
||||
"compliance": "Cumplimiento",
|
||||
"cohorts": "Programas"
|
||||
"cohorts": "Programas",
|
||||
"upgrade_now": "Actualiza ahora",
|
||||
"upgrade_students": "{studentCount} de {studentLimit} estudiantes",
|
||||
"upgrade_left": "{count} restante"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "Administrador de medios - ClassroomIO",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "widgets",
|
||||
"tags": "Etiquetas",
|
||||
"students": "estudiantes",
|
||||
"settings": "Configuración"
|
||||
"settings": "Configuración",
|
||||
"cohorts": "Cohortes"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -709,7 +709,8 @@
|
||||
"cancel": "Annuler",
|
||||
"confirm": "Supprimer",
|
||||
"snackbar_success": "Membre supprimé"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "Vous avez atteint la limite d'étudiants de votre plan. Mettez à niveau pour importer plus d'étudiants."
|
||||
},
|
||||
"community": {
|
||||
"title": "Communauté",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "Retour au cours",
|
||||
"join_course": "Rejoindre le cours",
|
||||
"enrolled_success": "Vous êtes inscrit à ce cours.",
|
||||
"enrolled_verify_email": "Vous êtes inscrit ! Vérifiez votre e-mail pour accéder à ce cours."
|
||||
"enrolled_verify_email": "Vous êtes inscrit ! Vérifiez votre e-mail pour accéder à ce cours.",
|
||||
"student_limit_reached": "{orgName} a atteint sa capacité d'accueil. Veuillez contacter l'administrateur de votre organisation."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "Gratuit",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "Multi-usage (7 jours)",
|
||||
"multi_use_30d": "Multi-usage (30 jours)",
|
||||
"custom": "Personnalisé"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "Vous avez atteint la limite d'étudiants de votre plan. Mettez à niveau pour inviter plus d'étudiants."
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "Statistiques",
|
||||
"analytics": "Analyse",
|
||||
"compliance": "Conformité",
|
||||
"cohorts": "Programmes"
|
||||
"cohorts": "Programmes",
|
||||
"upgrade_now": "Mettre à niveau maintenant",
|
||||
"upgrade_students": "{studentCount} sur {studentLimit} étudiants",
|
||||
"upgrade_left": "Il reste {count}"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "Gestionnaire de médias - ClassroomIO",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "Widgets",
|
||||
"tags": "Balises",
|
||||
"students": "Étudiants",
|
||||
"settings": "Paramètres"
|
||||
"settings": "Paramètres",
|
||||
"cohorts": "Cohortes"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -709,7 +709,8 @@
|
||||
"cancel": "रद्द करें",
|
||||
"confirm": "हटाएँ",
|
||||
"snackbar_success": "सदस्य हटा दिया गया"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "आप अपनी योजना की छात्र सीमा तक पहुँच गए हैं। अधिक छात्रों को आयात करने के लिए अपग्रेड करें।"
|
||||
},
|
||||
"community": {
|
||||
"title": "समुदाय",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "पाठ्यक्रम पर वापस जाएँ",
|
||||
"join_course": "पाठ्यक्रम में शामिल हों",
|
||||
"enrolled_success": "आप इस पाठ्यक्रम में नामांकित हैं।",
|
||||
"enrolled_verify_email": "आप नामांकित हैं! इस पाठ्यक्रम तक पहुँचने के लिए अपना ईमेल सत्यापित करें।"
|
||||
"enrolled_verify_email": "आप नामांकित हैं! इस पाठ्यक्रम तक पहुँचने के लिए अपना ईमेल सत्यापित करें।",
|
||||
"student_limit_reached": "{orgName} अपनी छात्र क्षमता तक पहुंच गया है। कृपया अपने संगठन प्रशासक से संपर्क करें."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "निःशुल्क",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "बहु-उपयोग (7 दिन)",
|
||||
"multi_use_30d": "बहु-उपयोग (30 दिन)",
|
||||
"custom": "कस्टम"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "आप अपनी योजना की छात्र सीमा तक पहुँच गए हैं। अधिक छात्रों को आमंत्रित करने के लिए अपग्रेड करें।"
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "आँकड़े",
|
||||
"analytics": "विश्लेषिकी",
|
||||
"compliance": "अनुपालन",
|
||||
"cohorts": "कार्यक्रम"
|
||||
"cohorts": "कार्यक्रम",
|
||||
"upgrade_now": "अभी अपग्रेड करें",
|
||||
"upgrade_students": "{studentLimit} छात्रों की {studentCount}",
|
||||
"upgrade_left": "{गिनती} बाएँ"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "मीडिया मैनेजर - क्लासरूमआईओ",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "विजेट्स",
|
||||
"tags": "टैग",
|
||||
"students": "छात्र",
|
||||
"settings": "सेटिंग्स"
|
||||
"settings": "सेटिंग्स",
|
||||
"cohorts": "समानता रखने वाले लोग"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -709,7 +709,8 @@
|
||||
"cancel": "Anuluj",
|
||||
"confirm": "Usuń",
|
||||
"snackbar_success": "Członek usunięty"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "Osiągnąłeś limit studentów w swoim planie. Uaktualnij, aby zaimportować więcej uczniów."
|
||||
},
|
||||
"community": {
|
||||
"title": "Społeczność",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "Powrót na kurs",
|
||||
"join_course": "Dołącz do kursu",
|
||||
"enrolled_success": "Zapisano Cię na ten kurs.",
|
||||
"enrolled_verify_email": "Jesteś zapisany! Zweryfikuj swój e-mail, aby uzyskać dostęp do tego kursu."
|
||||
"enrolled_verify_email": "Jesteś zapisany! Zweryfikuj swój e-mail, aby uzyskać dostęp do tego kursu.",
|
||||
"student_limit_reached": "Organizacja {orgName} osiągnęła już limit uczniów. Skontaktuj się z administratorem organizacji."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "Bezpłatny",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "Wielokrotnego użytku (7 dni)",
|
||||
"multi_use_30d": "Wielokrotnego użytku (30 dni)",
|
||||
"custom": "Niestandardowe"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "Osiągnąłeś limit studentów w swoim planie. Uaktualnij, aby zaprosić więcej uczniów."
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "Statystyki",
|
||||
"analytics": "Analityka",
|
||||
"compliance": "Zgodność",
|
||||
"cohorts": "Programy"
|
||||
"cohorts": "Programy",
|
||||
"upgrade_now": "Uaktualnij teraz",
|
||||
"upgrade_students": "{studentCount} z {studentLimit} uczniów",
|
||||
"upgrade_left": "Pozostało {count}"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "Menedżer multimediów - ClassroomIO",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "Widżety",
|
||||
"tags": "Tagi",
|
||||
"students": "Studenci",
|
||||
"settings": "Ustawienia"
|
||||
"settings": "Ustawienia",
|
||||
"cohorts": "Kohorty"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -709,7 +709,8 @@
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Excluir",
|
||||
"snackbar_success": "Membro excluído"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "Você atingiu o limite de alunos do seu plano. Atualize para importar mais alunos."
|
||||
},
|
||||
"community": {
|
||||
"title": "Comunidade",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "De volta ao curso",
|
||||
"join_course": "Participe do curso",
|
||||
"enrolled_success": "Você está inscrito neste curso.",
|
||||
"enrolled_verify_email": "Você está inscrito! Verifique seu e-mail para acessar este curso."
|
||||
"enrolled_verify_email": "Você está inscrito! Verifique seu e-mail para acessar este curso.",
|
||||
"student_limit_reached": "{orgName} atingiu sua capacidade de alunos. Entre em contato com o administrador da sua organização."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "Grátis",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "Multiuso (7 dias)",
|
||||
"multi_use_30d": "Multiuso (30 dias)",
|
||||
"custom": "Personalizado"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "Você atingiu o limite de alunos do seu plano. Atualize para convidar mais alunos."
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "Estatísticas",
|
||||
"analytics": "Análise",
|
||||
"compliance": "Conformidade",
|
||||
"cohorts": "Programas"
|
||||
"cohorts": "Programas",
|
||||
"upgrade_now": "Atualize agora",
|
||||
"upgrade_students": "{studentCount} de {studentLimit} alunos",
|
||||
"upgrade_left": "{contagem} restantes"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "Gerenciador de mídia - ClassroomIO",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "Widgets",
|
||||
"tags": "Etiquetas",
|
||||
"students": "Alunos",
|
||||
"settings": "Configurações"
|
||||
"settings": "Configurações",
|
||||
"cohorts": "Coortes"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -709,7 +709,8 @@
|
||||
"cancel": "Отмена",
|
||||
"confirm": "Удалить",
|
||||
"snackbar_success": "Участник удален"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "Вы достигли лимита учащихся, предусмотренного вашим планом. Обновите, чтобы импортировать больше студентов."
|
||||
},
|
||||
"community": {
|
||||
"title": "Сообщество",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "Вернуться к курсу",
|
||||
"join_course": "Присоединиться к курсу",
|
||||
"enrolled_success": "Вы записаны на этот курс.",
|
||||
"enrolled_verify_email": "Вы записаны! Подтвердите email, чтобы получить доступ к курсу."
|
||||
"enrolled_verify_email": "Вы записаны! Подтвердите email, чтобы получить доступ к курсу.",
|
||||
"student_limit_reached": "В {orgName} достигнута вместимость учащихся. Обратитесь к администратору вашей организации."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "Бесплатно",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "Многоразовое использование (7 дней)",
|
||||
"multi_use_30d": "Многоразовое использование (30 дней)",
|
||||
"custom": "Пользовательский"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "Вы достигли лимита учащихся, предусмотренного вашим планом. Обновите, чтобы пригласить больше студентов."
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "Статистика",
|
||||
"analytics": "Аналитика",
|
||||
"compliance": "Соответствие",
|
||||
"cohorts": "Программы"
|
||||
"cohorts": "Программы",
|
||||
"upgrade_now": "Обновите сейчас",
|
||||
"upgrade_students": "{studentCount} из {studentLimit} студентов",
|
||||
"upgrade_left": "{count} осталось"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "Медиа-менеджер — ClassroomIO",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "Виджеты",
|
||||
"tags": "Теги",
|
||||
"students": "Студенты",
|
||||
"settings": "Настройки"
|
||||
"settings": "Настройки",
|
||||
"cohorts": "когорты"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -709,7 +709,8 @@
|
||||
"cancel": "Hủy bỏ",
|
||||
"confirm": "Xóa",
|
||||
"snackbar_success": "Thành viên đã bị xóa"
|
||||
}
|
||||
},
|
||||
"import_limit_reached": "Bạn đã đạt đến giới hạn sinh viên trong kế hoạch của mình. Nâng cấp để nhập thêm sinh viên."
|
||||
},
|
||||
"community": {
|
||||
"title": "cộng đồng",
|
||||
@@ -894,7 +895,8 @@
|
||||
"back_to_course": "Quay lại khóa học",
|
||||
"join_course": "Tham gia khóa học",
|
||||
"enrolled_success": "Bạn đã ghi danh khóa học này.",
|
||||
"enrolled_verify_email": "Bạn đã ghi danh! Xác minh email để truy cập khóa học này."
|
||||
"enrolled_verify_email": "Bạn đã ghi danh! Xác minh email để truy cập khóa học này.",
|
||||
"student_limit_reached": "{orgName} đã đạt đủ số lượng sinh viên. Vui lòng liên hệ với quản trị viên tổ chức của bạn."
|
||||
},
|
||||
"pricing_section": {
|
||||
"free": "miễn phí",
|
||||
@@ -1226,7 +1228,8 @@
|
||||
"multi_use_7d": "Sử dụng nhiều lần (7 ngày)",
|
||||
"multi_use_30d": "Sử dụng nhiều lần (30 ngày)",
|
||||
"custom": "tùy chỉnh"
|
||||
}
|
||||
},
|
||||
"student_limit_reached": "Bạn đã đạt đến giới hạn sinh viên trong kế hoạch của mình. Nâng cấp để mời thêm sinh viên."
|
||||
}
|
||||
},
|
||||
"marks": {
|
||||
@@ -2811,7 +2814,10 @@
|
||||
"stats": "Thống kê",
|
||||
"analytics": "Phân tích",
|
||||
"compliance": "Tuân thủ",
|
||||
"cohorts": "Chương trình"
|
||||
"cohorts": "Chương trình",
|
||||
"upgrade_now": "Nâng cấp ngay bây giờ",
|
||||
"upgrade_students": "{studentCount} trong số {studentLimit} sinh viên",
|
||||
"upgrade_left": "còn {count}"
|
||||
},
|
||||
"media_manager": {
|
||||
"page_title": "Quản lý truyền thông - ClassroomIO",
|
||||
@@ -3636,7 +3642,8 @@
|
||||
"widgets": "Widget",
|
||||
"tags": "Thẻ",
|
||||
"students": "Sinh viên",
|
||||
"settings": "Cài đặt"
|
||||
"settings": "Cài đặt",
|
||||
"cohorts": "Nhóm thuần tập"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user